CVE-2026-70426: Verified Reproduction
CVE-2026-70426: Jenkins Remoting JEP-200 deserialization filter bypass SECURITY-3911 : unfiltered ClassNotFoundException fallback in MultiClassLoaderSerializer.resolveClass and ObjectInputStreamEx.resolveClass lets agents deserialize blocked core-classpath classes on the controller
CVE-2026-70426 is verified against the affected target. Vulnerability class: Deserialization. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00322.
What Is CVE-2026-70426?
CVE-2026-70426 is a critical-severity Deserialization vulnerability. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00322).
CVE-2026-70426 Severity
CVE-2026-70426 is rated critical severity.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
How to Reproduce CVE-2026-70426
pruva-verify REPRO-2026-00322 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00322/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-70426
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
serialized hudson.remoting.UserRequest whose Callable bytes are produced by a SpoofedTagSystemClassLoaderOutput (TAG_SYSTEMCLASSLOADER annotation) naming the JEP-200-blocked class hudson.security3911.Payload
- agent
- JNLP4-connect handshake
- hudson.remoting.Channel
- UserRequest.perform
- UserRequest.deserialize
- MultiClassLoaderSerializer$Input.resolveClass
- ClassNotFoundException fallback
- super.resolveClass (unfiltered, pre-fix)
- Payload.readObject executes /bin/sh on controller JVM
How the agent worked
Root Cause and Exploit Chain for CVE-2026-70426
Jenkins Remoting (agent–controller communication library) contains two unfiltered
ClassNotFoundException fallback paths in its deserialization class-resolution code. In
hudson.remoting.MultiClassLoaderSerializer.Input.resolveClass() (and identically in
hudson.remoting.ObjectInputStreamEx.resolveClass()), the primary path resolves the
incoming class name against the channel-annotated classloader and then applies the JEP-200
class filter (channel.classFilter.check(c)). When that lookup throws
ClassNotFoundException, the fallback super.resolveClass(desc) resolved the class via the
receiving JVM's own classloader without applying the class filter. An attacker able to
speak the agent protocol (Agent/Connect permission, a compromised agent, or code running on
an agent) can therefore deserialize a JEP-200-blocked class on the Jenkins controller by
serializing it with a spoofed TAG_SYSTEMCLASSLOADER annotation, forcing the
ClassNotFoundException fallback. The blocked class's readObject then executes in the
controller JVM, yielding remote code execution on the controller.
- Package/component:
org.jenkins-ci.main:remoting(Jenkins Remoting), embedded in Jenkins core (jenkins.war). - Affected: Remoting <= 3384.v60d89463d9e0 (except backport 3355.3357.v931d3c992987); Jenkins weekly <= 2.575; LTS <= 2.568.1.
- Fixed: Remoting 3385.vf1123fb_515da_ (Jenkins 2.576 / LTS 2.568.2).
- Risk: Critical (CVSS 3.1 9.6, AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H) — agent-to-controller RCE. AC:H because the gadget class must live on the controller's core classpath and evade the pre-JEP-200 static denylist (which this bypass does not defeat).
Impact Parity
- Disclosed/claimed maximum impact: arbitrary code execution on the Jenkins controller from the agent side of a Remoting channel (RCE).
- Reproduced impact from this run: OS command execution on the Jenkins controller JVM
across the real JNLP4-connect (TCP) channel, proven twice by two independent direct
OS-level observations per attempt: (1)
/bin/sh -cexecuted on the controller writing a marker file withid/hostnameoutput inside the controller container (uid=0(root) ... jvm_pid=...), and (2) an outbound TCP callback from the controller JVM to an attacker-controlled listener carrying the per-attempt token and the controller hostname. - Parity:
full. - Not demonstrated: nothing material; the ClassCastException after
readObjectdetonation is an artifact of the minimal PoC gadget (Serializable-only), not a limitation of the bypass. A real-world exploit would use a gadget class already on the core classpath whosereadObjectperforms the malicious action (exactly the pattern this PoC models).
Root Cause
src/main/java/hudson/remoting/MultiClassLoaderSerializer.java (vulnerable line 137):
} catch (ClassNotFoundException ex) {
return super.resolveClass(desc); // <-- no channel.classFilter.check(...)
}
and identically src/main/java/hudson/remoting/ObjectInputStreamEx.java line 64:
} catch (ClassNotFoundException ex) {
return super.resolveClass(desc); // <-- no filter.check(...)
}
The name-based check channel.classFilter.check(name) still runs first, so classes on the
pre-JEP-200 static denylist (e.g. commons-collections functors) remain blocked; but the
class-level JEP-200 check (jenkins.security.ClassFilterImpl.isBlacklisted(Class)), which
rejects classes whose code location is not Jenkins core/Remoting/a plugin and which are not
in whitelisted-classes.txt, is skipped on the fallback path. An attacker forces the
fallback by writing TAG_SYSTEMCLASSLOADER (-3) as the class annotation: the receiver then
tries Class.forName(name, false, null) (bootstrap loader), which throws
ClassNotFoundException for any non-JDK class, and the fallback resolves the class through
the deserializing frame's classloader (on a real controller, the Jetty webapp classloader,
which can see the whole core classpath and delegates to the application classloader) —
completely bypassing the JEP-200 decision.
Fix commit: f1123fb515da74560db60645539019cfa77bce49 (jenkinsci/remoting, released as
3385.vf1123fb_515da_): both fallbacks became
return channel.classFilter.check(super.resolveClass(desc)); /
return filter.check(super.resolveClass(desc));. Diff captured in
bundle/repro/security3911-fix.diff; vulnerable/fixed source lines captured in
bundle/logs/vuln_unfiltered_fallback.txt and bundle/logs/fixed_filtered_fallback.txt.
Reproduction Steps
bundle/repro/reproduction_steps.sh(self-contained; uses only Docker imagesjenkins/jenkins:2.575-jdk21,jenkins/jenkins:2.576-jdk21,maven:3.9-eclipse-temurin-21,alpine:3.22plus the harness sources inbundle/repro/harness/).- What it does:
- Records the image digests and the war manifests (
Remoting-Embedded-Version: 3384.v60d89463d9e0 for 2.575, 3385.vf1123fb_515da_ for 2.576) and the real fix-commit diff from a jenkinsci/remoting checkout (verifying the exact vulnerable and fixed lines). - Extracts the byte-identical
remoting-3384.v60d89463d9e0.jarfrom the vulnerable war and compiles the PoC gadget (hudson.security3911.Payload, Serializable,readObjectexecutes/bin/sh+ outbound callback) intopayload.jar, and the attack agent. - For each of two vulnerable and two fixed attempts: boots a real Jenkins controller
(fresh
JENKINS_HOME, setup wizard disabled, inbound-agent TCP listener on port 50000, productionJnlpSlaveAgentProtocol4accept path) withpayload.jaron the controller's own launch classpath (a harness jar, as sanctioned by the ticket's reproduction requirements — the filter, channel, andUserRequest.deserializepath are 100% product code).init.groovy.dcreates theagent1inbound node, prints its JNLP secret, and proves the production JEP-200 filter identity:SECURITY3911_CHANNEL_DEFAULT_FILTER=jenkins.security.ClassFilterImplandSECURITY3911_FILTER_PROBE=REJECTED: Rejected: hudson.security3911.Payload(i.e. the production filter blocks the payload class at class level on both builds). - The attack agent performs the real JNLP4-connect handshake using the production
negotiation classes (
JnlpAgentEndpoint,JnlpProtocolHandlerFactory,JnlpProtocol4Handler,IOHub,PublicKeyMatchingX509ExtendedTrustManager), builds a genuinehudson.remoting.UserRequest, replaces its serialized request bytes with bytes produced by aSpoofedTagSystemClassLoaderOutput(exactly the regression-test helper from the fix commit), and sends it over the established channel. The controller'sDefaultJnlpSlaveReceiver.afterChannel/SlaveComputer.setChannelproduction flow runs on the same connection.
- Records the image digests and the war manifests (
- Expected evidence:
- Vulnerable (2.575):
Payload.readObjectexecutes on the controller duringUserRequest.deserialize→ marker file/tmp/CONTROLLER_PWNED_<token>.txtinside the controller container (contents includeuid=0(root)and the controller hostname) AND an outbound callbackSECURITY3911_CALLBACK token=... phase=readObject controller_host=<controller>received by the attacker's listener. - Fixed (2.576):
SecurityException: Rejected: hudson.security3911.Payload; see https://jenkins.io/redirect/class-filter/returned over the channel; no marker, no callback.
- Vulnerable (2.575):
Evidence
Full run log:
bundle/logs/reproduction_steps.log(two consecutive full runs, bothOVERALL=0, exit code 0).Per-attempt controller and agent logs:
bundle/logs/attempt-{vuln,fixed}-{1,2}/{controller,agent}.log.Direct OS-execution markers (collected from inside the controller containers via
docker exec):bundle/logs/attempt-vuln-1/CONTROLLER_PWNED_security3911-vuln-1-*.txtandbundle/logs/attempt-vuln-2/CONTROLLER_PWNED_security3911-vuln-2-*.txt, e.g.:SECURITY3911_MARKER token=security3911-vuln-1-1786004490 phase=readObject uid=0(root) gid=0(root) groups=0(root) dc3492613861 <- controller container hostname jvm_pid=100Outbound callback (agent log):
CALLBACK_RECEIVED from=/172.19.0.2:38064 msg=SECURITY3911_CALLBACK token=security3911-vuln-1-1786004490 phase=readObject controller_host=dc3492613861— the source IP and hostname belong to the controller container, proving the controller JVM executed attacker code and dialed out.Vulnerable-channel exception (expected, post-detonation):
ClassCastException: class hudson.security3911.Payload cannot be cast to class hudson.remoting.Callable (hudson.security3911.Payload is in unnamed module of loader 'app'; ...)— confirms the fallback resolved the class via the controller-side loader.Fixed-channel rejection:
EXCEPTION=Error: Failed to deserialize the Callable object. <- SecurityException: Rejected: hudson.security3911.Payload; see https://jenkins.io/redirect/class-filter/withCALLBACK_COUNT=0and no marker.Production filter provenance (controller log, both builds):
SECURITY3911_CHANNEL_DEFAULT_FILTER=jenkins.security.ClassFilterImpl,SECURITY3911_FILTER_PROBE=REJECTED: Rejected: hudson.security3911.Payload, and the JUL linejenkins.security.ClassFilterImpl#notifyRejected: hudson.security3911.Payload in file:/opt/harness/payload.jar might be dangerous, so rejecting.Negative control:
bundle/repro/negative_control_observation.json(both fixed attempts: rejection observed,command_executed=false, zero callbacks, zero marker files).Environment: Docker 29.1.3;
jenkins/jenkins:2.575-jdk21(@sha256:16778c994cfc..., Remoting 3384.v60d89463d9e0) andjenkins/jenkins:2.576-jdk21(@sha256:8c1c7e28b463..., Remoting 3385.vf1123fb_515da_); OpenJDK 21 controllers/agents; harness SHA-256s inbundle/logs/harness_sha256.txt.
Recommendations / Next Steps
- Upgrade to Jenkins 2.576 / LTS 2.568.2 (Remoting 3385.vf1123fb_515da_) or the 3355.3357.v931d3c992987 backport line.
- The fix is correct and minimal: route both
ClassNotFoundExceptionfallbacks through the channel's class filter, mirroring the primary path. - Defense in depth: restrict Agent/Connect, isolate agents, and monitor for
Rejected: ... class-filterlog lines plus unexpected outbound connections from the controller. - Testing: the fix commit's regression tests (
ClassFilterTestmultiClassLoaderSerializer_spoofedSystemClassLoader_isRejectedandobjectInputStreamEx_emptyClassLoader_fallbackIsFiltered) cover both fallbacks; this run reproduces the same spoof over the real JNLP4/TCP production path.
Additional Notes
- Idempotency: the script runs two clean attempts per role with fresh
JENKINS_HOMEper attempt, unique per-attempt tokens, a fresh Docker network per run, and full container cleanup; two consecutive end-to-end runs both passed (OVERALL=0). - The second vulnerable fallback (
ObjectInputStreamEx.resolveClass) is exercised when the remote peer does not advertise multi-classloader RPC capability; it is fixed by the same commit and follows the identical pattern (documented in the diff); the primary proof uses theMultiClassLoaderSerializerpath, which is the default for JNLP4 agent channels. - The harness payload class implements only
Serializable(nothudson.remoting.Callable) because the controller's webapp/application classloader split would otherwise fail linking; this mirrors a real gadget whose effect lives inreadObject, and it makes the proof stronger: execution happens during deserialization, before anyCallablecast or arbitrary-callable permission check. - Fresh Jenkins installs do not auto-install bundled detached plugins; the script installs
the war-bundled
instance-identity.hpi+bouncycastle-api.hpiintoJENKINS_HOME/plugins(what the setup wizard would do) so the production JNLP4 TLS listener is functional.
CVE-2026-70426 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
ls /data/pruva/project-cache/.run-views/95749f0a330dcee41e3ce349a76c8982d1c720582125092d8dd101e6a3a0fe5d/ 2>/dev/null; ls /data/pruva/project-cache/.run-views/95749f0a330dcee41e3ce349a76c8982d1c720582125092d8dd101e6a3a0fe5d/repo-mirrors 2>/dev/null; ls bundle/.pruva bundle/learning bundle/repro bundle/logs 2>/dev/null; ls bundle/security3911-fix.diff /bundle/security3911-fix.diff 2>/dev/null; find / -maxdepth 3 -name "security3911*" 2>/dev/null | headEXPLOIT-KNOWLEDGE PREFLIGHT REVIEW REQUIRED: `run_shell` is blocked until this agent calls `review_exploit_knowledge_preflight` with snapshot_sha256 `5c4e0966cb1b272734cc6bcbd9d072a03b5a97abdc07a1c3e317be2ae604d677` and exactly one disposition for each candidate ID [1e2efebc-e4fc-4a9c-924f-373bce278d55, ecfee63a-d34e-4dfe-94a3-19819daab982]. Historical records remain reference-only; review their typed mechanisms and preconditions before continuing.
Unknown error
ls /data/pruva/project-cache/.run-views/95749f0a330dcee41e3ce349a76c8982d1c720582125092d8dd101e6a3a0fe5d/ 2>/dev/null; echo ---; ls /data/pruva/project-cache/.run-views/95749f0a330dcee41e3ce349a76c8982d1c720582125092d8dd101e6a3a0fe5d/repo-mirrors 2>/dev/null; echo ---; ls bundle/.pruva bundle/learning bundle/repro bundle/logs 2>/dev/null; echo ---; find /data/pruva/runs/c7c62461-b0d3-4efb-b1a7-557c4b280680 -name "security3911*" 2>/dev/nullrepo-mirrors --- --- bundle/.pruva: run_attempt.json bundle/learning: exploit_knowledge_preflight.json
which docker java mvn curl git 2>/dev/null; echo ---; java -version 2>&1; echo ---; docker info 2>&1 | head -20; echo ---; ls /data/pruva/project-cache/.run-views/95749f0a330dcee41e3ce349a76c8982d1c720582125092d8dd101e6a3a0fe5d/repo-mirrors//usr/bin/docker /usr/bin/curl /usr/bin/git --- /bin/bash: line 1: java: command not found --- WARNING: Error loading config file: open /home/pruva/.docker/config.json: permission denied Client:
docker images 2>/dev/null; echo ---; docker ps -a 2>/dev/null{"cwd":"/data/pruva/runs/c7c62461-b0d3-4efb-b1a7-557c4b280680","duration_ms":192,"exit_code":0,"stderr":"","stdout":"IMAGE ID DISK USAGE CONTENT SIZE EXTRA\nalpine:3.22 14358309a308 12.8MB 3.88MB \ndocker:27-dind-rootless@sha256:e2ac8e8f66ae21a060b0a8e3005c70f6ed9441aabf409434463d1f6eecd38026 e2ac8e8f66ae 607MB 159MB \nghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1 8096b2518d60 2.16GB 553MB \ngradle:4.10.3-jdk8 393cd6c24d69 1.1GB 324MB \njenkins/jenkins:2.575-jdk21 16778c994cfc 747MB 263MB \njetbrains/teamcity-server:2025.11.6 91c92f1010fc 4.19GB 1.8GB U \njetbr… [truncated]curl -sI --max-time 10 https://github.com 2>&1 | head -3; echo ---; curl -sI --max-time 10 https://registry-1.docker.io/v2/ 2>&1 | head -3; echo ---; curl -sI --max-time 10 https://get.jenkins.io 2>&1 | head -3HTTP/2 200 date: Thu, 06 Aug 2026 06:30:04 GMT content-type: text/html; charset=utf-8 --- HTTP/2 401 date: Thu, 06 Aug 2026 06:30:04 GMT content-type: application/json ---
Artifacts and Evidence for CVE-2026-70426
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-70426
FAQ: CVE-2026-70426
Is CVE-2026-70426 exploitable?
How severe is CVE-2026-70426?
What type of vulnerability is CVE-2026-70426?
How can I reproduce CVE-2026-70426?
Is the CVE-2026-70426 reproduction verified?
References for CVE-2026-70426
Authoritative sources for CVE-2026-70426 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.