Today we are going to talk about one of the types of malicious instructions exploiting remote software vulnerabilities, particularly memory vulnerabilities. Historically, such sets of instructions are called shell codes. Previously such attacks used to grant access to shell, and somehow it became the custom. Typical memory vulnerabilities exploited by shell codes are, first of all, buffer overrun, stock variables and other structures overrun.
The high fidelity positioning, data density and such other intricacies characteristic of modern HDD can be the subject matter for a great many articles, but we are not going to explore the disk mechanics or process physics profoundly, let’s focus on the most interesting component for us — electronics, instead.
In late March 2020, a bug was discovered in a popular web-based tool called GitLab. The error enables the attacker to advance from reading files in the system to executing arbitrary commands. The vulnerability was recognized critical because the attacker doesn’t need any special rights in the target system. This article explains the origin of the bug and shows how to exploit it.
The exploit was developed by an Austrian researcher and developer William Bowling (vakzz). He found out that under certain conditions, the class UploadsRewriter does not check the path to the file. This allows the attacker to copy any file in the system and use it as an attachment when an issue is moved from one project to another one.
The researcher transformed this particular local file reader into an RCE vulnerability. The attacker can read the file secrets.yml containing a token used to sign cookies, while a specially crafted and signed cookie makes it possible to execute arbitrary code on the server.
The vulnerability belongs to the path traversal category; its registration number in the National Vulnerability Database is CVE-2020-10977. The bug affects EE/CE GitLab versions starting from 8.5 и 12.9, respectively. In the framework of the bug bounty program, GitLab has paid $20,000 to William Bowling for its discovery.
It’s not a big deal to create test environment for this bug because GitLab has an official docker repository. Using a simple command, I can run the container with any given version of the application. I choose the latest vulnerable release: 12.9.0.
docker run --rm-d--hostnamegitlab.vh -p443:443 -p80:80 -p2222:22 --name gitlab gitlab/gitlab-ce:12.9.0-ce.0
“CE” refers to the Community Edition; in theory, I could use the Enterprise Edition (EE) as well – but it requires a key for the trial period. CE is sufficient for demonstration purposes because both versions are equally vulnerable.
At the first start, GitLab asks to set an admin password. The default login is firstname.lastname@example.org.
Then I have to create two new projects.
The test system is up and running. However, I also need to download the GitLab source code to demonstrate what its sections contain the error.
Reading local files
The problem originates from the Clone Issue feature.
I create a new issue in the Test project.
When you create an issue, you can produce its description in the Markdown format and upload an arbitrary file (e.g. a screenshot showing the error or a log file) to simplify the life of the developers.
All uploaded files are stored in the folder /var/opt/gitlab/gitlab-rails/uploads/ on the hard drive. The FileUploader class is responsible for this.
The name transmitted during the upload becomes the file name.
After uploading the attachment, a link to it in the Markdown format is added to the issue description, and I save it.
GitLab allows you to move issues from one project to another; this is very useful if an issue affects several products of the same developer.
I press the button and select a project to move the issue to.
When you move in issue, it closes in the old project and appears in the new one.
Important: the attachments are copied, not moved. In other words, the program creates new files and links to them.
Time to examine the code and find out how the transfer is implemented. All issue-related routes are stored in the routes folder, in the file issues.rb. The move route (the one that processes the user’s POST request with the required parameters) is also kept there.
At this point, Issues::UpdateService.new is called; and the project ID, user who has initiated the move, and project the issue is moved to are passed as arguments. Then control is transferred to the class UpdateService, which, in turn, calls the method move_issue_to_new_project.
165:# Return a new uploader with a file copy on another project
183:record_upload# after_store is not triggered
As you can see, neither find_file, nor copy_to, nor copy_file checks the file name; therefore, any file in the system can be easily transformed into an attachment.
To test this hypothesis, I exit the directory using a standard method: ../. I must determine the required number of steps ‘up’. By default, the full path to uploaded files in the GitLab container is as shown on the screenshot below.
The full path to the picture from my issue is as follows:
A long piece of code in the middle is the unique hash of the current project. Therefore, I need at least ten ../ constructs to get to the root directory of the container.
Now let’s try to read the file /etc/passwd. I edit the issue description and add the required number of ../ constructs into the path to the file. I recommend using more such constructs to make sure that you get where you want to get.
Then I save and move the file to another project.
Now I can download the passwd file and view the content of /etc/passwd.
In other words, I can read everything accessible to the user on whose behalf GitLab is running (in the case of Docker, it is git). And the question is: can I find any useful stuff there?
From file reading to code execution
Of course, such a large-scale project as GitLab contains plenty of interesting files (various access tokens, data stored in private repositories, configs, etc.), and the attacker can read them and use the information to compromise the system. In addition, GitLab includes a very special file: data contained in it enable you to execute any code in the system. The path to this file is as follows:
The file stores the important, secret_key_base variable, which is used to sign cookies.
Similar to many modern solutions, cookies in GitLab are serialized and signed to prevent their substitution. The signature is verified on the server side, and only then the cookie is deserialized. By default, the serializer is defined as :hybrid.
This enables me use to the Marshal format and call various objects. I will need the Embedded Ruby (ERB) template engine, which allows inter alia to execute console commands. In ERB, expression values are described in constructs having the following format: <% [expression] %>. Inside this tag, I can call functions of the template engine, including code written in Ruby. To execute this system command, I can use either backticks or %x.
erb=ERB.new("<%= `uname -a` %>")
For the testing purposes, I am going to use the test docker container and the command that calls the Rails console: gitlab-rails console.
The next construct is required to declare the result method of the ERB class deprecated.
In other words, any 32-character string consisting of characters from 0 to 9 and from a to f would fit. Then I save the issue and move it to another project. As a result, I get the attached secrets.yml file.
To perform the next step, I launch GitLab and specify the obtained variable as secret_key_base. This allows me to generate a cookie in GitLab that contains payload and has a valid signature: this cookie will pass the verification on the attacked machine. Because I am dealing with a test system, I perform all manipulations on it. First, I run gitlab-rails console again.
Then I create a new request and specify GitLab environment variables as the config.
Voila! The command has been executed, and the file /tmp/owned successfully created.
Vulnerability demonstration (video)
The examined vulnerability is easy-to-exploit and very dangerous. GitLab is a popular tool; so, thousands of services critical for corporate networking infrastructures are at risk. Just imagine that attackers can gain access to source files of all your projects!..
The bug had persisted in the code for more than four years and successfully migrated from one branch to another starting from version 8.5 released in February 2016. Therefore, upgrade to version 12.9.1 ASAP to get rid of this vulnerability!