今天在buuctf上嘗試了一下[De1CTF 2019]SSRF Me這個(gè)題目,名字直接提示了考點(diǎn)所在,提醒我們注意利用服務(wù)端發(fā)起的請求。
Hint中寫道flag在./flag.txt中,引導(dǎo)我們考慮如何利用SSRF讀取flag.txt。
點(diǎn)開靶機(jī),可見代碼,其使用了Python的Flask框架。
代碼流程比較清晰,先聲明了一個(gè)Task類,綁定了若干個(gè)路由,聲明了幾個(gè)函數(shù)。我們訪問/De1ta,并傳參,服務(wù)端把我們傳入的參數(shù)經(jīng)過waf()函數(shù)處理后,構(gòu)造一個(gè)Task的對象。
個(gè)人認(rèn)為,值得引起注意的點(diǎn)至少有如下幾個(gè):1. 第31行和第41行對action的值進(jìn)行判斷,但此處使用的是 'in' 而不是 '==',這意味著我們可以包含兩種action;2. scan函數(shù)中的第82行,urlopen(param)可能是本題的關(guān)鍵點(diǎn),這正與SSRF相合。
根據(jù)第二點(diǎn),我們回溯一下哪里調(diào)用了scan()函數(shù),發(fā)現(xiàn)僅有一處,它會scan我們可控的self.param。
我們注意到從此處self.param傳入到scan函數(shù)里面的urlopen(),沒有對self.param進(jìn)行過濾,對param進(jìn)行的過濾僅僅是在最開始構(gòu)造對象時(shí)的一個(gè)waf()函數(shù),它檢測param是否已'gopher'或'file'開頭,以此試圖阻止我們使用gopher協(xié)議和file協(xié)議讀取它本地的文件。我們在最開始學(xué)習(xí)文件包含漏洞的時(shí)候,參數(shù)直接寫文件名也可以讀取相應(yīng)文件,個(gè)人理解這里的協(xié)議默認(rèn)就是file協(xié)議,所以過濾可以說是沒有效果的,我們?nèi)裟芸刂苝aram為'./flag.txt',則能讀取其中內(nèi)容。flag在./flag.txt中,而能讀取該文件的機(jī)會僅此一處,這里必然是破題的起點(diǎn)。
此處若能成功讀取flag.txt,則將其中的內(nèi)容寫入result.txt中。
我們往下看:如果 'read' in self.action,則將result.txt讀取并存到result['data']中,Exec()函數(shù)的最后,還返回了result,而在challenge()函數(shù)中,則返回了json.dumps(task.Exec()),亦即我們的此處的result。
如此一來,我們便有了一個(gè)大致思路:先進(jìn)入/De1ta綁定的challenge()函數(shù),將在Exec中的scan部分中將flag.txt的內(nèi)容存入result.txt,然后從read部分中將其存到result字典中讀出,再以json形式返回到客戶端,我們就能得到flag。
接下來我們按著這個(gè)思路走,主體就在Exec()函數(shù)中。首先,要想進(jìn)入對action判讀的部分,必須先過一關(guān),即if (self.checkSign()),我們跟進(jìn)它,發(fā)現(xiàn)它的要求是 getSign(self.action,self.param) ==self.sign,我們繼續(xù)跟進(jìn)getSign()函數(shù),
一開始看到這里,確實(shí)是觸及知識的盲區(qū)了,于是在Python Shell里簡單的做了下實(shí)驗(yàn):
看來hexdigest()函數(shù)是幫助我們的。到現(xiàn)在,我們還面臨一個(gè)問題是:secret_key是未知的,雖然一次運(yùn)行過程中是個(gè)定值,但只能在運(yùn)行代碼的那一端獲取。只要我們用心通讀代碼,就會知道這不是問題,在/geneSign綁定的geneSign中,將返回getSign('scan', param),通過這個(gè)返回值,我們也許能夠使用哈希長度擴(kuò)展攻擊,但我想的是直接在geneSign()中獲取我們想要的字符串的哈希值。依我愚見,這是本題的本意。
經(jīng)過這一番轉(zhuǎn)換,我們可以這樣理解,在checkSign()函數(shù)的比較中,左邊是getSign(self.action, self.param),右邊是getSign('scan', param1),如果再寫深一點(diǎn),左邊是md5(key + self.param + self.action),考慮到param會傳入urlopen(),我們可以寫成md5(key + 'flag.txt' + self.action),其中,為了保證Exec()函數(shù)中scan部分和read部分都能被執(zhí)行,self.action必須有'readscan'或'scanread'這樣的子串。等號右邊是md5(key + param1 + 'scan'),依此判斷,self.action應(yīng)包含'readscan'子串,如果不多給自己加戲的話,可以將self.action定為'readscan'。
這樣一來,等號左邊為md5(key + 'flag.txt' + 'readscan'),已被寫死,等號右邊的未定的參數(shù)也可也可確定,即md5(key + 'flag.txtread' + 'scan')。param1為'flag.txtread'依此傳參,可得一個(gè)哈希值,這就是我們將傳給self.sign的值。
到這里,這個(gè)題基本上就結(jié)束了,過了這個(gè)checkSign()函數(shù),下面的代碼都會按照我們構(gòu)劃的思路去執(zhí)行。
傳參給/De1ta,即可拿到flag。
收獲
1.本題難度不高,選擇這個(gè)題目寫文章作為對自己表達(dá)能力的提升。
2.個(gè)人愚見,代碼審計(jì)類型的題目總體上講沒有什么特別的套路,尤其是這種體量較小的代碼,也不需要很大的腦洞,只要有較為扎實(shí)的基本功,認(rèn)真的審計(jì)代碼,找出關(guān)鍵點(diǎn),耐心回溯與跟進(jìn),搞清流程,構(gòu)建一個(gè)大體的思路,去實(shí)踐去驗(yàn)證它的可行性,即使沒有解出題目,也一定有提升。
3.知識方面,在其他前輩的博客中學(xué)習(xí)到了繞過本題waf()函數(shù)的方法,即使用local_file協(xié)議,前輩還提到:file協(xié)議也調(diào)用了封裝的local_file協(xié)議。自身修行不夠,還沒有達(dá)到深入底層的境界,也算是get到新的知識點(diǎn)了。
4.有的前輩的博客里使用哈希長度擴(kuò)展攻擊解題,看起來確實(shí)是簡單一些,但個(gè)人認(rèn)為這個(gè)方法也還是建立在對代碼有一定的理解的基礎(chǔ)上的。