## https://sploitus.com/exploit?id=F0666787-BCF6-595C-918A-632B2AEBA0A4
# CVE-2026-73847 β emlog AI Assistant CSRF β SQL Execution β Admin Takeover
PoC for missing CSRF protection on emlog pro's AI Assistant `execute_tool` endpoint, which lets an attacker ride an admin's authenticated session to run arbitrary SQL against the site database β including a full admin account takeover.
| | |
|---|---|
| **CVE** | [CVE-2026-73847](https://vulners.com/cve/CVE-2026-73847) |
| **CNA** | GitHub |
| **Advisory** | [GHSA-v6wr-4x55-7qp5](https://github.com/emlog/emlog/security/advisories/GHSA-v6wr-4x55-7qp5) |
| **CVSS 3.1** | 6.8 Medium β `AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N` |
| **CWE** | CWE-352 (CSRF), CWE-1275 (Improper SameSite), CWE-798 (Hardcoded write-confirmation string) |
| **Affected** | emlog pro through 2.6.23 |
| **Credit** | Dostxodjayev Abdullox ([@squeeze440](https://github.com/squeeze440)) β pending correction in the CVE record, see below |
## Root cause
emlog pro's admin panel ships an AI assistant that can execute SQL on the admin's behalf via `POST /admin/ai.php?action=execute_tool`. Several issues stack on this one endpoint:
1. **No CSRF token.** Every other destructive action file in `admin/` calls `LoginAuth::checkToken()` first (e.g. `admin/media.php:140`). `admin/ai.php` never does.
2. **Auth is session-cookie-only** (`admin/ai.php:152`, `User::isAdmin()`) β no Origin/Referer check.
3. **The write-confirmation gate is a hardcoded public string.** `include/service/ai.php:594`: `if (trim($confirm_code) !== 'confirm')`. Any forged request just sends `confirm_code=confirm`.
4. **Read-only SQL needs no confirmation at all** (`include/service/ai.php:578,589`) β a bare authenticated request can `SELECT` any table.
5. **Only the `blog` table is write-protected** (`include/service/ai.php:591`) β `user` and every other table are fully writable.
6. **Password redaction is alias-bypassable.** Redaction only matches the literal output column name `password` (`include/service/ai.php:822-828`) β `SELECT password AS pwd_hash FROM user` returns the raw hash.
7. **Auth cookie has no `SameSite` attribute** (`include/lib/loginauth.php:99`), leaving Chrome's default "Lax+POST" grace window (roughly the first two minutes after login) as the only thing standing between this and reliable cross-site delivery.
Chained together: one forged request from an admin's browser reads every table (including password hashes) and writes to every table except `blog`, including a direct `user.password` overwrite.
## PoC
### Part 1 β raw impact chain (`poc_raw_impact.sh`)
Isolates the SQL/auth-bypass primitive from the CSRF delivery question. Run against a local instance you control:
```bash
./poc_raw_impact.sh http://TARGET admin ''
```
This logs in as admin, dumps password hashes via column-alias bypass, overwrites the admin password directly through the `user` table, then logs in again with the attacker-chosen password from a fresh cookie jar β proving full account takeover once one authenticated request reaches the endpoint.

### Part 2 β real cross-site CSRF delivery (`poc_csrf.html`)
Settles the SameSite question with a live browser rather than assuming it. Serve `poc_csrf.html` from any origin distinct from the target (a different IP is enough β Chrome treats distinct literal IPs as separate sites) and get a logged-in admin to open it within roughly two minutes of signing in:
```bash
python3 -m http.server 8888
# then point poc_csrf.html's form action at your target and get it opened
```
The form auto-submits on load, POSTing a forged `query_database` call with `confirm_code=confirm` cross-site.





Verified live: the forged cross-site POST carried the admin's real auth cookie (`sec-fetch-site: cross-site`, cookie attached), returned `200 {"code":0,"msg":"ok",...}`, and the injected row was confirmed present via a follow-up authenticated read. Repeating the identical request ~48 minutes later against the same, now-aged cookie jar failed β no cookie was attached and the server returned an unauthenticated redirect, confirming the roughly two-minute Lax+POST window is the real constraint (reflected in `AC:H`).
## Impact
- Full database read: every table/column, including password hashes and any secrets in `emlog_options` (SMTP creds, API keys, etc.).
- Full database write to every table except `blog`, including `user` β role/password/email overwrite, demonstrated end-to-end as account takeover.
- Limited to accounts with `role=admin`; `writer`/`editor` are blocked by `User::checkRolePermission()`. Not privilege escalation from a lower role β it turns one malicious link click by a logged-in admin into full, silent site compromise.
## Fix
Add `LoginAuth::checkToken()` to `execute_tool`, set `SameSite=Strict` on the auth cookie, and replace the static `confirm_code` string with a real per-session, single-use token. Full remediation detail in the [advisory](https://github.com/emlog/emlog/security/advisories/GHSA-v6wr-4x55-7qp5).
## Disclosure timeline
- 2026-07-31 β Reported via GitHub Security Advisories per emlog's own SECURITY.md.
- 2026-08-01 β Maintainer published the advisory and requested a CVE.
- 2026-08-16 β CVE-2026-73847 assigned by GitHub (CNA).
**Note on credit:** GitHub-as-CNA published the CVE record without a `credits` entry, despite the GHSA itself crediting and accepting the reporter. A correction request was sent to `security-advisories@github.com` on 2026-08-16; this README will be updated if the record is corrected.
## Disclaimer
Published after the advisory was public and a CVE assigned, for defensive/educational use. Do not run this against an emlog instance you don't own or aren't explicitly authorized to test.