
Proof-of-concept and lab for CVE-2026-75827, a Grav arbitrary file write via Blueprint dynamic-data error_log, with reproduction script and Docker lab.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · CVE-2026-75827
Grav 2.0.13 - getgrav
I am @abraxas_null. Loopback lab. The client is CVE-2026-75827-Abraxas-Labs.py.
The denylist missed error_log. Grav through 2.0.14 allowlists Class::method dynamic-data providers and denylists bare functions. A page-edit account plants a Form blueprint data-options@ directive. GET of that public form runs call_user_func_array on a bare PHP function. error_log type 3 appends attacker bytes to an attacker path. Then GET the file. Confirmed on tag 2.0.13. Patched in 2.0.15. This is not an upload action=.
| CVE | CVE-2026-75827 · CVE.org |
| CWE | CWE-94 |
| CVSS | High: 8.8 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Product | Grav |
| Affected | all versions through 2.0.13 (inclusive) |
| Patched | 2.0.15 and later |
| Auth | authenticated (page-edit) |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only |
Someone with page-edit or blueprint-config plants data-options@: ['error_log', payload, 3, web-path]. A later GET of that form appends the payload. Point the path at a web-accessible .php and it is RCE. The lab stops at a unique string in poc-witness.txt. It is not unauthenticated RCE from a cold site.
The GHSA named the denylist hole. I read isSafeDynamicCall, then Utils::isDangerousFunction, then the Form page. error_log is not on the list. The Class::method half already used a positive allowlist after GHSA-7pgq. The bare-function half did not.
Plant, then GET. The fixture page is already in the lab tree. GET /poc-form. Grav builds the form, hits dynamicData, calls error_log. Then GET /poc-witness.txt. Unique string, not a homepage, not a 404. Multiple GETs of the form append.
Wrong turns that already cost time: treating this as an upload action=; treating the form 200 as the proof (still Grav theme); treating Composer yelling about PHP 8.2 as a miss (the write still landed); putting system() in the message. The second GET is the tell.
Port 8088. Grav 2.0.13 admin skeleton, Form plugin on, web root writable. PHP 8.2 image is fine; Composer will complain.
Target only 127.0.0.1:8088 (or the loopback you bound).
cd lab
docker compose up --force-recreate
python3 ../CVE-2026-75827-Abraxas-Labs.py
Witness: GET /poc-witness.txt body contains POCWitness75827. Home HTML or empty 404 is not it.
Ways to lose without learning anything:
poc-witness.txtsystem() / exec() PHP payloadUpdate Grav to 2.0.15 or newer. Re-run CVE-2026-75827-Abraxas-Labs.py against the patched build: POCWitness75827 must not appear.
github.com/getgrav/grav/security/advisories/GHSA-f8wv-xp27-6gq7
www.vulncheck.com/advisories/grav-before-arbitrary-file-write-via-error-log
github.com/CVEProject/cvelistV5/tree/main/cves/2026/75xxx/CVE-2026-75827.json
Abraxas Labs: abraxaslabs.tech · github.com/abraxas · @abraxas_null
GNU Affero GPL v3.0. See LICENSE.
The client talks to loopback. Using it against systems you do not own is not authorized by Abraxas Labs. No warranty.