玩轉Hacker101 CTF(五)


hi,大家好,我又來放writeup啦!經過一個周末的頭腦風暴,我終於拿到了第十四題的flag,所以接着第一篇第二篇第三篇還有第四篇的進度,這次和大家一起學習Hacker101 CTF的第十二、十三、十四題:

廢話不多說,上題!

 

第十二題TempImage

這道題難度適中,考察文件上傳漏洞,打開主頁,大大的上傳點:

點開:

文件上傳漏洞的姿勢很多,防護措施也五花八門,我整理了一下,畫成了下面的腦圖:

可能有遺漏的,歡迎在評論區補充。

檢測文件上傳點有無問題,我的習慣是直接做個圖片馬,抓上傳包,因為這樣最節省時間,先用copy做個圖片馬:

注意php.php中的內容先不要急着放一句話,先放個phpinfo()試試,成功了再放webshell,把上傳包抓下來,送到Repeater模塊:

放包:

返回了302跳轉:

Follow redirection,跟蹤跳轉:

返回狀態200,應該已經傳上去了,在瀏覽器上訪問一下跳轉后的地址:

圖片正常,為了判斷后台有無修改圖片內容(例如刪除腳本標簽頭、圖片重渲染等),需要將這場圖片下載下來,與原始圖片作對比:

簡單的對比使用windows自帶的comp命令就好:

可以看到文件內容沒有修改。接下來探測文件后綴有無限制,重新上傳shell.png抓包,或者直接在剛才抓下的包中修改文件名為shell.php,放包:

可見文件雖然上傳上去了,但后綴名卻還是png,這是為什么呢?仔細看了一下上傳的包,發現最下面有貓膩:

看,這里又出現了一個filename,如果我們改變它,再上傳會怎么樣呢,shell.png改為shell.php,放包:

看,成功的上傳了php后綴的文件,是不是大功告成了呢?在瀏覽器里訪問一下上傳上去的php文件,本以為會出現phpinfo的頁面,然而並沒有:

明明訪問的是php文件,返回的卻是圖片,說明我們傳上去的php文件沒有解析,出現這種狀況的原因一般是在files這個目錄下有個.htaccess文件,其中配置了該目錄下的所有文件都不可作為腳本解析,實現這個目標的.htaccess配置有多種,下面是其中一種:

php_flag engine off

總之,就是files目錄下的文件不可以當作php腳本執行,那么就要思考其他路子,試想,雖然files目錄下的文件不可以當作php腳本執行,但是其他目錄比方說網站根目錄總可以的吧,那么有沒有辦法把文件傳到網站根目錄下呢?我們現在可以控制filename了,如果filename中帶上../路徑會發生什么呢,修改上傳包中filename參數的值為:../shell.php:

看,也許是提示我們思路正確,第一個flag已經出來了。仔細觀察一下報錯:

<br /> <b>Warning</b>: move_uploaded_file(files/6c992b5b4654d3da3eb9e414afbc38e2_../shell.php): failed to open stream: No such file or directory in <b>/app/doUpload.php</b> on line <b>7</b><br /> <br /> <b>Warning</b>: move_uploaded_file(): Unable to move '/tmp/phpfeEYvz' to 'files/6c992b5b4654d3da3eb9e414afbc38e2_../shell.php' in <b>/app/doUpload.php</b> on line <b>7</b><br /> ERROR: Upload failed<br>^FLAG^ebe27115de38566ea08f91ef913b3416edcfb3301ec9e958695bac47277aeba7$FLAG$ 

注意其中的路徑files/6c992b5b4654d3da3eb9e414afbc38e2_../shell.php,顯然,如果要把shell.php上傳到與files目錄的上級目錄,我們應該把filename的值改為:

/../../shell.php,再次發包:

成功!瀏覽器訪問一下:

OK,成功執行,接下來只需要把圖片馬中的payload改為一句話木馬,重復上述過程,用菜刀連接即可:

在index.php找到了第二個flag:

順帶看了一下files目錄,里面果然有.htaccess,內容和我想得差不多:

 

第十三題H1 Thermostat

很easy的android抓包分析及逆向,也就是簽到題的難度,簡單過一下,

打開主頁,提示apk正在生成:

稍后刷新,出現apk下載鏈接:

拖到模擬器安裝打開,我用的是mumu模擬器,注意修改模擬器的wifi代理地址為10.0.2.2

配置fiddler抓包

第一個flag就在請求頭中,接下來反編譯apk,我用的工具是gda,翻了翻源碼,找到第二個flag,發現第一個flag也在里面,剛剛完全不需要抓包的,真是簡單到家了啊!

 

第十四題Model E1337 v2 – Hardened Rolling Code Lock

這道題是Expert難度,是第十一題的加強版本,我花了一個周末死了無數腦細胞才解決掉它,來一起看一看,打開主頁:

與十一題一模一樣,依然是個猜數字游戲,輸入code,返回期望值,如果輸入的code與期望值相同,則拿到flag:

得先想辦法找到后台源碼,我用的是爆破的辦法,字典用的是SecLists中的

SecLists-masterDiscoveryWeb-Contentraft-medium-words.txt,最好放在vps上用wfuzz爆破,

wfuzz -w path/raft-medium-words.txt --hc 404 http://xx.xx.xx.xx/xx/FUZZ

本地用burpsuite爆破很慢,再給大家安利一下:vps+wfuzz+seclists的組合真香。

很快出了結果:

源碼所在的頁面是rng,其實如果仔細回想一下第十一題,猜也能猜到,因為第十一題中的源碼邏輯文件就是rng.py。

來讀源碼:

import random

def setup(seed): global state state = 0 for i in range(16): cur = seed & 3 seed >>= 2 state = (state << 4) | ((state & 3) ^ cur) state |= cur << 2 def next(bits): global state ret = 0 for i in range(bits): ret <<= 1 ret |= state & 1 for k in range(3): state = (state << 1) ^ (state >> 61) state &= 0xFFFFFFFFFFFFFFFF state ^= 0xFFFFFFFFFFFFFFFF for j in range(0, 64, 4): cur = (state >> j) & 0xF cur = (cur >> 3) | ((cur >> 2) & 2) | ((cur << 3) & 8) | ((cur << 2) & 4) state ^= cur << j return ret setup((random.randrange(0x10000) << 48) | (random.randrange(0x10000) << 32) | (random.randrange(0x10000) << 16) | random.randrange(0x10000)) 

這道題的代碼與第十一題有三處不同:

一是setup函數的調用,十一題是

setup((random.randrange(0x10000)<<16) | random.randrange(0x10000))

也就是seed的范圍在2的32次方內,這對個人計算機來說是一個可以接受的值,所以可以使用爆破的辦法,而這里

setup((random.randrange(0x10000) << 48) | (random.randrange(0x10000) << 32) | (random.randrange(0x10000) << 16) | random.randrange(0x10000))

顯然,seed的范圍在2的64方內,這對個人計算機來說是個天文數字了,所以這里直接爆破行不通,需要分析代碼尋找竅門。

仔細觀察setup函數中的下列代碼片段:

......
for i in range(16): cur = seed & 3 seed >>= 2 ...... 

現在seed已經是64bit的一個數了,那么cur = seed & 3代表取出seed的最低2bit位給cur,然后seed >>= 2代表着seed右移2bit位,這個過程在循環
for i in range(16)中,也就是說每次循環seed的最低兩位賦予了cur,用簡單的話描述這個循環對seed的影響就是:seed的最低2*16即32個bit位參與了運算,而且在setup函數的其他地方沒有用到seed,所以說只有seed的低32位對setup函數有影響,再換句話說就是:setup(0xFFFFFFFF11111111)和setup(0x11111111)對整個腳本生成期望值的影響是一樣的!

為了確認這個結論,我又寫了個腳本來跟蹤seed的每一位,看由setup函數生成的state的每一位狀態:

seed = ['b0','b1','b2','b3','b4','b5','b6','b7','b8','b9','b10','b11','b12','b13','b14','b15','b16','b17','b18','b19','b20','b21','b22','b23','b24','b25','b26','b27','b28','b29','b30','b31','b32','b33','b34','b35','b36','b37','b38','b39','b40','b41','b42','b43','b44','b45','b46','b47','b48','b49','b50','b51','b52','b53','b54','b55','b56','b57','b58','b59','b60','b61','b62','b63'] state = [] def myxor(a1,a2): global state arrlen = max(len(a1),len(a2)) if len(a1) > len(a2): temp = ['']*(len(a1)-len(a2)) temp.extend(a2) a2 = temp else: temp = ['']*(len(a2)-len(a1)) temp.extend(a1) a1 = temp result = [] for i in range(arrlen): if a1[i] == '': result.append(a2[i]) elif a2[i] == '': result.append(a1[i]) else: result.append('(' + a1[i] + ' XOR ' + a2[i] + ')') return result def myor(a1,a2): global state arrlen = max(len(a1),len(a2)) if len(a1) > len(a2): temp = ['']*(len(a1)-len(a2)) temp.extend(a2) a2 = temp else: temp = ['']*(len(a2)-len(a1)) temp.extend(a1) a1 = temp result = [] for i in range(arrlen): if a1[i] == '': result.append(a2[i]) elif a2[i] == '': result.append(a1[i]) else: result.append('(' + a1[i] + ' OR ' + a2[i] + ')') return result for x in range(16): cur = seed[-2:] seed = seed[:-2] temp1 = state[:] temp1.extend(['','','','']) temp2 = state[-2:] temp2 = myxor(temp2,cur) state = myor(temp1,temp2) cur.extend(['','']) state = myor(state,cur) print("init_state:",state) 

最后的輸出:

init_state: ['b62', 'b63', 'b62', 'b63', 'b60', 'b61', '(b62 XOR b60)', '(b63 XOR b61)', 'b58', 'b59', '((b62 XOR b60) XOR b58)', '((b63 XOR b61) XOR b59)', 'b56', 'b57', '(((b62 XOR b60) XOR b58) XOR b56)', '(((b63 XOR b61) XOR b59) XOR b57)', 'b54', 'b55', '((((b62 XOR b60) XOR b58) XOR b56) XOR b54)', '((((b63 XOR b61) XOR b59) XOR b57) XOR b55)', 'b52', 'b53', '(((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52)', '(((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR
b53)', 'b50', 'b51', '((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50)', '((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51)', 'b48', 'b49', '(((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48)', '(((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49)', 'b46', 'b47', '((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46)', '((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47)', 'b44', 'b45', '(((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44)', '(((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45)', 'b42', 'b43', '((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42)', '((((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43)', 'b40', 'b41', '(((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42) XOR b40)', '(((((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43) XOR b41)', 'b38', 'b39', '((((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42) XOR b40) XOR b38)', '((((((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43) XOR b41) XOR b39)', 'b36', 'b37', '(((((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42) XOR b40) XOR b38) XOR b36)', '(((((((((((((b63 XOR b61) XOR b59) XOR b57)
XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43) XOR b41) XOR b39) XOR b37)', 'b34', 'b35', '((((((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42) XOR b40) XOR b38) XOR
b36) XOR b34)', '((((((((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43) XOR b41) XOR b39) XOR b37) XOR b35)', 'b32', 'b33', '(((((((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR
b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42) XOR b40) XOR b38) XOR b36) XOR b34) XOR b32)', '(((((((((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43) XOR b41) XOR b39) XOR b37) XOR b35) XOR b33)']

可以看到,由setup函數改變的state只受seed中b32~b63這32個bit位的影響,seed的高32bit位相當於被丟棄未參與運算。

所以我們依舊可以爆破seed來解這道題,因為我們只需要爆破其低32bit位,最多也就是2的32次方中可能性。

但簡單的爆破還是不能滿足我的需求,因為在第十一題中,我所使用的爆破代碼跑了2個小時才得到結果,而在這題中,還有兩處不同,分別是第十九行加了個循環以及bits變為了64而不是26:

這樣一方面進一步混淆了state,另一方面也會導致我爆破的計算量x3x64/26,如果簡單套用上一題的爆破代碼,大概要十四五個小時,這是讓人無法接受的,所以我決定再分析一下代碼,看能否找到弱點優化我的爆破代碼。

注意第15~18行代碼:

ret = 0 for i in range(bits): ret <<= 1 ret |= state & 1 

每次調用next(64)時ret的初始值為0,bits為64,那么循環中的代碼就可以表述為:

ret左移1bit位,state的最低位放在ret的最低位上,如此循環64次。並且下文中ret的值沒有再被修改。所以如果把setup()調用后state的狀態記為S0,其最低位bit位記為S0[63],然后看第16~27行代碼

    for i in range(bits): ret <<= 1 ret |= state & 1 for k in range(3): state = (state << 1) ^ (state >> 61) state &= 0xFFFFFFFFFFFFFFFF state ^= 0xFFFFFFFFFFFFFFFF for j in range(0, 64, 4): cur = (state >> j) & 0xF cur = (cur >> 3) | ((cur >> 2) & 2) | ((cur << 3) & 8) | ((cur << 2) & 4) state ^= cur << j 

將i=0這次循環結束時state的狀態記為S1,其最低bit位記為S1[63],將i=2這次循環結束時state的狀態記為S2,其最低bit位記為S2[63],以此類推,那么第一個ret就可以表示為:

S0[63],S1[63],...S63[63]

同樣的,第二個ret可以表示為:

S64[63],S65[63],...S127[63]

而ret我們是知道的,那么在爆破過程中,我們驗證一個seed能否產生我們想要的ret值時,就不需要將ret完全計算出來再比較,我們只需要計算中間過程的state,將它相應的位與ret相應的位進行比較可以了,這樣可以大大減少我們的計算量,依照這個思路完成的代碼如下:

#include <stdio.h> unsigned long long state = 0; unsigned long long ret0 = 8097447744684720271; //todo unsigned long long ret1 = 9416998937824950119; //todo void setup(unsigned long long seed) { state = 0; unsigned long long cur = 0; for(unsigned i = 0; i < 16; i++) { cur = seed & 3; seed >>= 2; state = (state << 4) | ((state & 3ll) ^ cur); state |= cur << 2; } } void modifyState() { for (unsigned m = 0; m < 3; m++) { state = (state << 1) ^ (state >> 61); state &= 0xFFFFFFFFFFFFFFFF; state ^= 0xFFFFFFFFFFFFFFFF; for (unsigned j = 0; j < 64; j += 4) { unsigned long long cur = (state >> j) & 0xF; cur = (cur >> 3) | ((cur >> 2) & 2) | ((cur << 3) & 8) | ((cur << 2) & 4); state ^= cur << j; } } } unsigned long long next(unsigned bits) { unsigned long long ret = 0; for (unsigned i = 0; i < bits; i++) { ret <<= 1; ret |= state & 1; for (unsigned m = 0; m < 3; m++) { state = (state << 1) ^ (state >> 61); state &= 0xFFFFFFFFFFFFFFFF; state ^= 0xFFFFFFFFFFFFFFFF; for (unsigned j = 0; j < 64; j += 4) { unsigned long long cur = (state >> j) & 0xF; cur = (cur >> 3) | ((cur >> 2) & 2) | ((cur << 3) & 8) | ((cur << 2) & 4); state ^= cur << j; } } } return ret; } unsigned int check(unsigned long long ret,unsigned int j){ if(((ret >> j) & 1) == (state & 1)){ modifyState(); if(j > 0){ check(ret,j-1); }else{ return 1; } }else{ return 2; } } int main(int argc, char* argv[]) { unsigned long long seed = 1; unsigned printtimes = 0; while (seed) { if(seed / 0x1000000 == printtimes){ printf("now seed is :%I64xn",seed); printtimes++; } setup(seed); if(check(ret0,63) == 1){ printf("ret0 matched,now state is:%I64x and seed is :%I64xn",state,seed); if(check(ret1,63) == 1){ printf("ret1 matched,now state is:%I64x and seed is :%I64xn",state,seed); printf("And next ret is :%I64x",next(64)); break; } } seed++; } while (getchar() != 'e') { //防止跑出結果一聲不響的退出 } } 

注意在VS中編譯這段代碼時,要選擇Debug x64模式,release模式可能會跑不出來,我猜可能是編譯器代碼優化出了問題,如果你知道怎么解決,歡迎告訴我。
用上面編譯出來的exe跑,十幾分鍾就出了結果,我的CPU還是很一般的貨:

看,seed的值在范圍0~0xFFFFFFFF中算是靠中間了,如果你還想更快,可以使用多線程,估計五、六分鍾就能出結果,不過要注意給state加上訪問鎖。
跑出來的值是16進制的,轉為10進制提交,就拿到了flag:

本文由安全客原創發布 
轉載,請參考 轉載聲明,注明出處:  https://www.anquanke.com/post/id/181456 
安全客 - 有思想的安全新媒體


免責聲明!

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



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