很有意思的一道題
訪問頁面之后是登錄界面
嘗試弱口令登錄一下,無果
查看源代碼之后也沒有什么提示,掃描敏感目錄,發現有源碼泄露。
這里我用御劍沒有掃出來源碼泄露,可能跟掃描線程太快了有關,查看www.zip里面泄露的源代碼
class.php里面定義了user和mysql兩個類
config.php里面是服務器搭建環境的時候設置的參數,如果讀者有自己本地搭建環境的經驗就會知道,我們下載下來源碼之后,還需要根據自己本地的環境進行相應的配置,比如說我們需要在config.php里面設置自己本地數據庫的用戶名和密碼,這些在下載下來的config.php的源代碼里面都是暫時空缺的。
所以題目環境docker下發的時候,一定也設置了自己本地的$flag的值,於是我們的目標就是需要讀取服務器端config.php文件,就能夠得到flag了。
在register.php里面,可以看到是注冊一個用戶,輸入用戶名和密碼,接着跳轉到index.php界面進行登錄
在index.php里面我們輸入用戶名和密碼進行登錄,接着跳轉到profile.php頁面。
在此之前我們需要傳遞$profile的值
這里對我們輸入的phone,email,nickname都進行了過濾,在源碼的注釋里面我已經進行了說明,很明顯最后一個if語句的判斷跟前兩者有所不同,前兩者如果phone為數組的話,匹配失敗則為null,取反后就die,而第三個if當nickname為數組的時候,它不會匹配到非數字字母的值,也就為false,長度處使用數組strlen函數也會失效,返回NULL,則繞過了此處的if過濾。
這里添加一個知識點,正則表達式[]內的^表示匹配不存在這類字符的,如圖上的第三個if里面的,表示匹配非數字字母的值,在[]外的^表示匹配字符串開頭。
所以對於輸入的nickname的值我們是可控的。
再去看profile.php
可以看到這里對$profile的值進行了反序列化,接着依次讀取,此時對於$photo有一個file_get_contents()文件讀取函數,所以這里是我們破題的關鍵。
值得一提的是,在class.php的mysql類的定義中,過濾函數為
可以看出來將黑名單數組里面的值都換成了hacker
現在我們整理一下思路,update.php中有一個$profile數組變量,這個數組里有$phone, $email, $nickname, $photo幾個變量,序列化后profile字段存入數據庫,當我們訪問profile.php的時候,則會從數據庫里面讀取profile字段同時反序列化,我們需要控制$photo為config.php,才可以在訪問的時候獲取到base64編碼之后的有flag值的服務器端的config.php
而我們唯一能夠控制的變量則是之前說到的nickname,參考了很多師傅的WP之后,寫一下這道題的主要知識點:
PHP序列化長度變化導致字符逃逸
首先, PHP反序列化中值的字符讀取多少其實是由表示長度的數字控制的,而且只要整個字符串的前一部分能夠成功反序列化,這個字符串后面剩下的一部分將會被丟棄
簡單舉幾個例子大家就明白了
得到這個結果很正常
接着我們改變一點點
得到的結果變成了hello woxxx
我們可以看到,原來的字符串hello world內被填充了幾個字符串,即xxx”;},在PHP進行反序列化時,由字符串初始位置向后讀取8個字符,即使遇到字符串分解符單雙引號也會繼續向下讀,此處讀取到 woxxx ,而后遇到了正常的結束符”;},達成了正常反序列化的條件,反序列化結束,后面的 rld”;} 幾個字符均被丟棄。
我們用另外一個例子來學習一下這個知識點的應用
可以看到bad_str函數會將序列化之后的單引號轉換成為字符串no,實際上這里就已經有了長度的變化,因為單引號長度是1,而no字符串長度是2
接着,我們修改用戶的簽名
這里偷一張別的師傅的圖,雖然輸入的值不同,但是思想是相同的
在我們這里,則是想將hello world改變掉。
於是構造
輸出的結果跟我們想要的是相同的,hello world變成了hhh
參考一下大佬博客里面對此的解釋
在反序列化輸出之前,我們的字符串是在某過濾函數的過濾替換之后得到的,在經過過濾處理了之后,字符串的某一部分會加長,但是描述其長度的數字沒有發生改變(由反序列化時變量的屬性決定),這就可能導致PHP在按描述其長度的數字讀取相應長度的字符串之后,本該屬於該字符串的內容逃逸出了該字符串的管轄范圍。輕則反序列化失敗,重則自成一家成為一個獨立於源字符串的變量,若是這個獨立出來的變量末尾是個結束符";},則可能導致反序列化成功結束,而后面的內容也順理成章的被丟棄了,此處能夠逃逸出的字符串長度由過濾后字符串增加的長度決定,如上圖第四個語句,@號內就是我們要逃逸出來的字符串,長度為33,百分號內為我們輸入的username變量,要想讓@號內的字符串逃逸,我們就需要原來的字符串增加33,這樣的話@號內的字符串就會被擠出它原來所在的位置,username的正常部分和增長的部分正好被php解析成一整個變量,@號內的內容就被解析成一個獨立的變量,而且因為它的最后有";},所以使反序列化成功結束。
再回到我在本地的例子上面,我們為了使hhh替換掉hello world,於是這一段是需要逃逸出來的字符串
";i:1;s:3:"hhh";}
長度為17,而單引號替換成no之后長度只是增加1,所以為了增加17個長度,我們需要17個單引號,這樣才能夠將逃逸字符串擠出原來的位置。
緊接着,17*no+test,一共是38個字符,所以在s處我們填寫長度為38
最后的$fakes就為
a:2:{i:0;s:38:"test'''''''''''''''''";i:1;s:3:"hhh";};i:1;s:11:"hello world";}
所以在此處我們最終字符串替換成為了hhh
先閉合了一個變量的正確格式,又寫入了一個變量的正確格式,最后閉合了一個反序列化的操作。該擠出的被擠出逃逸了,該丟棄的丟棄了,最后想要達成的目標也實現了。
於是我們再回歸到題目里面來,因為我們最后是從數據庫里面讀取反序列化之后的結果,所以我們先在本地搭建序列化,看看序列化的格式之后編寫相應的payload
可以看到,根據泄露的源碼序列化之后的結果為
a:4:{s:5:"phone";s:6:"123456";s:5:"email";s:12:"test@126.com";s:8:"nickname";a:1:{i:0;s:4:"hell";}s:5:"photo";s:5:"hello";}
我們能夠構造的是nickname,在這里我已經是傳遞數組給他了
我們需要將photo的值hello改變成config.php
根據之前的基礎知識,本地的直接構造為:
a:4:{s:5:"phone";s:6:"123456";s:5:"email";s:12:"test@126.com";s:8:"nickname";a:1:{i:0;s:72:"hell''''''''''''''''''''''''''''''''''";}s:5:"photo";s:10:"config.php";}s:5:"photo";s:5:"hello";}
需要逃逸的字符串為
";}s:5:"photo";s:10:"config.php";}
長度為34,所以添加34個單引號,長度為34*2+4即72
可以看到本地的查看的文件已經修改為了config.php
我們在題目里面使用相同的思想
因為where轉換成hacker會由5–>6,字符有一個增加,所以我們為了逃逸34個字符,就添加34*where
警告無傷大雅,因為我們傳遞的是數組類型的值,所以這里會有警告
源代碼里面的base64編碼就是config.php的base64編碼,解碼即可
可以看到成功讀取了config.php,里面有flag的值
貼上參考的師傅的博客鏈接
https://www.jianshu.com/p/3b44e72444c1