今天在buuctf上嘗試了一下[0CTF 2016]piapiapia之愚見這個(gè)題目,點(diǎn)進(jìn)去之后是登錄界面和一張貓的動(dòng)圖,進(jìn)行了爆破和簡單的注入嘗試,掃描了一下目錄,一無所獲。
在網(wǎng)上看了看大佬的wp,發(fā)現(xiàn)有www.zip源碼泄露(個(gè)人認(rèn)為嚴(yán)格意義上不算泄露),不同掃描器內(nèi)的字典也不一樣,據(jù)說能在本題中掃描出來的有dirmap、dirsearch、nikto,我偷懶直接將源碼下載下來進(jìn)行審計(jì)。
代碼量并不大,比較友好,按照所學(xué),先看config.php,再是class.php,

我們要有一個(gè)意識(shí):這是我們下載下來的源碼,并非真正的服務(wù)端運(yùn)行的源碼,本地搭建做題環(huán)境時(shí)要改成自己的用戶名、密碼之類,在服務(wù)端docker的主機(jī)里,$flag變量應(yīng)該存的就是我們要的flag。
繼續(xù)審計(jì)代碼,我們可以發(fā)現(xiàn)在class.php中有本題關(guān)鍵的兩個(gè)類和相關(guān)函數(shù),其中的函數(shù)寫的較為全面(增改查都涉及)、認(rèn)真(變量有單引號(hào)保護(hù)且過濾了單引號(hào)和一些關(guān)鍵詞),若想注入恐怕需要費(fèi)點(diǎn)功夫,其余的php文件亮點(diǎn)不多、比較平常,但還有三個(gè)點(diǎn)需要注意:
一是update.php里有一個(gè)序列化操作,與之對(duì)應(yīng)的有一個(gè)反序列化操作,在注入可能不好用的情況下,這很有可能是本題的關(guān)鍵;


二是update.php里對(duì)傳入的變量做了簡單的檢查,

這里比較有意思的是,前兩個(gè)是沒有按相應(yīng)規(guī)則匹配到文本則執(zhí)行die()函數(shù),也就是說無論preg_match()返回值為0或null或false皆會(huì)die出,而第三個(gè)檢查則不是這樣,是如果匹配到非字母數(shù)字或nickname長度大于10則die出,這里我們就可以操作了,控制nickname為一個(gè)數(shù)組,這樣的話兩個(gè)判斷條件為false或NULL,故不會(huì)die出。


可以想象這是出題人故意設(shè)計(jì)的,否則為什么不接著前兩個(gè)if判斷的格式寫。由此看來,nickname為我們可以較為完全的控制、利用的變量。
三是有profile.php的功能是展示文件,說是展示,實(shí)為讀取,


剛才我們?cè)赾onfig.php里想到要讀取服務(wù)端上config.php的源碼,而這里是整個(gè)題目里唯一的可以讀取文件的點(diǎn)。
現(xiàn)在我們有個(gè)大體思路了,update.php中有一個(gè)$profile數(shù)組變量,這個(gè)數(shù)組里有$phone, $email, $nickname, $photo幾個(gè)變量,序列化后以profile字段存入數(shù)據(jù)庫,而我們?nèi)绻芸刂苝hoto變量為"config.php",則能在訪問profile.php時(shí)獲得base64編碼之后的config.php源碼。
下面問題來了,怎么控制$photo?

update.php中的文件上傳為$photo變量唯一的來源,無法實(shí)際利用,到了這里如果不知道本題想考的知識(shí)點(diǎn)——PHP序列化長度變化導(dǎo)致字符逃逸,我們就可以放棄了。
閱讀了諸位大佬的wp后(沒辦法,太菜了),再經(jīng)過動(dòng)手實(shí)踐,我對(duì)這個(gè)知識(shí)點(diǎn)有了一定的理解,和大家分享一下。
大佬說:看過PHP的底層代碼后,發(fā)現(xiàn)PHP反序列化中值的字符讀取多少其實(shí)是由表示長度的數(shù)字控制的,而且只要整個(gè)字符串的前一部分能夠成功反序列化,這個(gè)字符串后面剩下的一部分將會(huì)被丟棄,舉個(gè)例子:


這樣很正常,下面我們引出知識(shí)點(diǎn)。


我們可以看到,原來的字符串Northind內(nèi)被填充了幾個(gè)字符串,先是 """ 三個(gè)雙引號(hào),再是正常結(jié)尾時(shí)需要有的 ";} 三個(gè)字符,在PHP進(jìn)行反序列化時(shí),由字符串初始位置向后讀取8個(gè)字符,即使遇到字符串分解符單雙引號(hào)也會(huì)繼續(xù)向下讀,此處讀取到 North""" ,而后遇到了正常的結(jié)束符,達(dá)成了正常反序列化的條件,反序列化結(jié)束,后面的 ind";}? 幾個(gè)字符均被丟棄。
借用大佬的一個(gè)例子(https://www.cnblogs.com/litlife/p/11690918.html),簡單分享一下這個(gè)知識(shí)點(diǎn)的應(yīng)用


這里的username我們可控,bad_str函數(shù)會(huì)把反序列化后的字符串中的單引號(hào)替換“no”,我們做個(gè)分析,嘗試著修改該用戶的簽名,用到的當(dāng)然是本題的知識(shí)點(diǎn),

我們要記住一點(diǎn),我們的字符串是在某變量被反序列化得到的字符串受某函數(shù)的所謂過濾處理后得到的,而且經(jīng)過處理之后,字符串的某一部分會(huì)加長,但描述其長度的數(shù)字沒有改變(該數(shù)字由反序列化時(shí)變量的屬性決定),就有可能導(dǎo)致PHP在按該數(shù)字讀取相應(yīng)長度字符串后,本來屬于該字符串的內(nèi)容逃逸出了該字符串的管轄范圍,輕則反序列化失敗,重則自成一家成為一個(gè)獨(dú)立于原字符串的變量,若是這個(gè)獨(dú)立出來的變量末尾是個(gè) ";} ,則可能會(huì)導(dǎo)致反序列化成功結(jié)束,后面的內(nèi)容也就被丟棄了。此處能逃逸的字符串的長度由經(jīng)過濾后字符串增加的長度決定,如上圖第四個(gè)語句,@號(hào)內(nèi)就是我們要逃逸出來的字符串,長度為33,百分號(hào)內(nèi)為我們輸入的username變量,要想讓@號(hào)內(nèi)的字符串逃逸,就需要原來的字符串增長33,這樣的話@號(hào)內(nèi)的字符串被擠出,username的正常部分和增長的部分正好被PHP解析為一整個(gè)變量,@號(hào)內(nèi)的內(nèi)容就被解析為一個(gè)獨(dú)立的變量,而且因?yàn)樗淖詈笥?";} ,使反序列化成功結(jié)束。
為了增長33,我們需要username里加入33個(gè)單引號(hào),它們會(huì)被替換為33個(gè)no,使長度增加33,由此以來,上圖中x的值也可以確定了,輸入的username即為Northind'''''''''''''''''''''''''''''''''";i:1;s:18:"Today is Northind!";},x為它的長度(74),所以我們最后得到的字符串為:
a:2:{i:0;s:74:"Northind'''''''''''''''''''''''''''''''''"(注意最后這里是個(gè)雙引號(hào));i:1;s:18:"Today is Northind!";};i:1;s:15:"Today is Mondy!";}


我們可以看到,在這個(gè)反序列化字符串被過濾后,里面的單引號(hào)全部被替換為“no”,使"Northind"+"no"*33的長度之和等于74,配合上我們傳入的",滿足PHP反序列化的條件之一,后面的";i:1;s:18:"Today is Northind!";}先閉合了一個(gè)變量的正確格式,又寫入了一個(gè)變量正確格式,最后閉合了一個(gè)反序列化操作。該擠出的被擠出逃逸了,該丟棄的丟棄了,任務(wù)也完畢了。這是我們的分析,接下來實(shí)際傳個(gè)username變量進(jìn)去看看,

可以發(fā)現(xiàn),成功修改了簽名。
下面回到piapiapia這個(gè)題,既然要用到反序列化長度逃逸,必然是先把變量序列化,然后進(jìn)行過濾,在過濾的過程中把某個(gè)關(guān)鍵詞替換成了長度更長的關(guān)鍵詞,導(dǎo)致長度加長,最終引起逃逸,而在class.php中,我們可以看到:

我們只有傳入的字符串中有'where'關(guān)鍵字,被替換為'hacker'關(guān)鍵字,才會(huì)讓長度加一,否則長度不變。在這里眼尖的大佬直接就看出端倪了,而我水平著實(shí)有限,不讀讀大佬的博客是真看不出來。
下面我們來分析piapiapia這個(gè)題應(yīng)該怎樣構(gòu)造字符串,
經(jīng)過本地調(diào)試,我們可以知道,正常情況下,原始的$profile字符串為 (此處已經(jīng)將nickname以數(shù)組類型傳參):
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:"nickname";a:1:{i:0;s:5:"kendo";}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}
因?yàn)槲覀兿氚裵hoto改為config.php,我們目的字符串的前身可知矣(為了看著方便還是把可控部分的雙引號(hào)改為%):
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:"nickname";a:1:{i:0;s:5:%kendo";}s:5:"photo";s:10:"config.php";}%;}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}
我們要逃逸的部分為";}s:5:"photo";s:10:"config.php";},長度為34,需要在nickname[0]里添加34個(gè)where才能被成功逃逸。
我們可以構(gòu)造構(gòu)造,并在本地試著反序列化看看能否輸出有效信息,

所以我們的目標(biāo)字符串為:
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:%nickname";a:1:{i:0;s:204:"34個(gè)where";}s:5:"photo";s:10:"config.php";}%;}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}
為了方便好看還是將可控部分的雙引號(hào)改為%,
然后簡單的復(fù)制了profile的代碼和filter函數(shù),建了個(gè)PHP文件,


輸出中有base64編碼的內(nèi)容,解碼即為config.php

結(jié)果看來,已經(jīng)可以成功讀取本地的config.php,接下來的內(nèi)容不難,在docker上訪問register.php,注冊(cè)用戶,在update.php里輸入一定的字符,抓包,

將nickname那里改為nickname[],(根據(jù)直覺這么改就能傳數(shù)組,實(shí)際也確實(shí)是這樣),再將他的值改為我們構(gòu)造的字符串中%內(nèi)部分,


頁面有warning是因?yàn)槲覀兊膎ickname為數(shù)組類型,但無傷大雅,訪問profile.php,查看源代碼,


將這個(gè)內(nèi)容base64解碼,就能讀取服務(wù)端的config.php,得到flag。
幾點(diǎn)心得體會(huì):
1.掃描器之間也有些差別,有的掃描器確實(shí)是掃不出來,這就讓我們不得不多備一兩個(gè)掃描器;
2.還是要多動(dòng)手,多去實(shí)踐才能弄懂;
3.先發(fā)散找思路,再縮小范圍,最后去嘗試;
4.我對(duì)PHP底層的東西的了解為0,日后有機(jī)會(huì)一定要多多了解。