一、FLask_FileUpload

进入是一个提交的网址,随便提交一个png图片,返回显示:

看网页源码:

这是一道非常典型的 CTF Web 类的文件上传漏洞(File Upload Vulnerability)题目。
<form action="/uploader" method="POST" …>:这是一个表单,点击提交时,会通过 POST 请求将数据发送到后端的 /uploader 路由(接口)。
enctype="multipart/form-data":这是标准的用于上传文件的编码类型。
accept=".jpg,.png":这行代码告诉浏览器,在弹出的文件选择框中,默认只允许用户选择 .jpg 或 .png 格式的图片。这个 accept 属性只在用户的浏览器端起作用。可以通过抓包工具(比如 Burp Suite)拦截请求,轻易地修改上传的文件名或后缀;或者直接在浏览器前端修改这段 HTML 代码把 accept 删掉。
<!– Give me the file, and I will return the Running results by python to you! –>:它的意思是:“给我文件,我会用 Python 运行它,并把运行结果返回给你!”
这道题的逻辑非常明确:
目标:后端服务器会接收上传的文件,并且会把它当成 Python 脚本去执行。需要利用这个特性去读取服务器上的 Flag。
障碍:前端有 .jpg 和 .png 的限制。
行动方向:
-
编写一个能够读取系统命令或读取文件的 Python 脚本(例如,包含读取 flag 文件内容的代码)。
-
绕过前端的图片后缀限制Python 代码上传到服务器。
-
服务器端如果没有做严格的后端校验,就会执行Python 代码,并将包含 Flag 的结果回显在网页上。
ok,所以利用 os.system 借用服务器,在服务器内部执行“”打开并阅读 /flag 文件”的指令。写一个.txt,另存为png格式后上传:



得到flag。
二、点login没反应

点进去是一个登录界面,试着先用万能密码admin' OR 1=1登录,然后点login没反应,完美契合题目,于是直接查看源码:

这里也证实了,最后一行type="button",就意味着只是一个普通的按钮,本身没有任何提交表单的功能。在标准的 HTML 中,如果要让表单回车或点击直接提交,按钮类型应该是 type="submit"。
这段代码中有一个admin.css比较可疑,直接访问看看:

这里给了提示,其他的代码都是普通的样式设置代码,那就直接访问这个看看,得到:

ok很明了了,这是一道考察 PHP 反序列化(Unserialize) 和 Cookie 伪造 的题目
-
$KEY = 'ctf.bugku.com';这里定义了一个字符串常量 $KEY。
-
$cookie = $_COOKIE['BUGKU'];后端读取发送的 HTTP 请求中名为 BUGKU 的 Cookie 的值。
-
if(isset($_GET['19605'])) { show_source(__FILE__); }这就是刚才加上 ?19605 能看到源码的原因。只要 URL 带有这个参数它就直接显示源码,并停止往下执行获取 Flag 的逻辑。
-
elseif (unserialize($cookie) === "$KEY") { echo $flag; }这是拿 Flag 的终极条件! 它的意思是:如果传入的那个叫 BUGKU 的 Cookie,经过 unserialize()(反序列化)函数处理后,它的值严格等于(===)刚才定义的 $KEY(也就是 'ctf.bugku.com'),就把 Flag 打印出来!
所以首先即使要逆向思考,如何把 'ctf.bugku.com' 这个字符串进行 PHP 序列化?
在 PHP 中,字符串的序列化格式非常固定: s:字符串长度:"具体的字符串内容";
-
类型是字符串(String),所以用 s。
-
字符串 'ctf.bugku.com' 的长度是 13 个字符。
-
所以,它序列化后的结果就是:s:13:"ctf.bugku.com"。所以回到原始界面,自己伪造一个cookie,进入原网页,F12,在存储这里选择cookie然后点+号新增一个,名称就是BUGKU,值就是刚才序列化后的s:13:"ctf.bugku.com",然后直接F5刷新,此时,请求发给后端,由于没有了 ?19605,它会进入 elseif 分支;同时它读取到了伪造的 Cookie,反序列化后完美匹配 $KEY,即出flag。

三、兔年大吉

打开之后页面上直接显示源代码:
这段 PHP 代码是一道典型的CTF 反序列化漏洞题目,核心利用 PHP 魔法方法的自动调用特性构造POP 链(Property-Oriented Programming),最终实现任意命令执行。
什么是魔法方法?
一句话:不用喊自己会跑的特殊代码。
这道题里所有带__开头的方法全是魔法方法,比如:
- __destruct():当这个 "东西" 被扔掉 / 销毁的时候,自动跑
- __set():当给这个 "东西" 一个它没有的属性时,自动跑
- __call():当让这个 "东西" 做一个它不会的动作时,自动跑
总结: 魔法方法就是程序里设定好的、在特定时刻会自动触发的动作。
什么是“序列化”与“反序列化”?
要理解 POP 链,得先知道什么是序列化和反序列化:
-
序列化 (Serialize): 就像把家里的宜家衣柜拆成一块块木板,装进扁平的纸箱里。为什么要拆?为了方便运输(在网络上传输数据)或塞进仓库(存进数据库)。
-
反序列化 (Unserialize): 就像把收到的纸箱打开,照着图纸重新拼成一个完整的衣柜。
关键点来了: 当 PHP 拼装衣柜(反序列化)的时候,如果这个衣柜有 __wakeup() 这个魔法方法,它拼装完就会自动执行这个方法!
什么是“POP链” (POP Chain)?
简单比喻:精心设计的“多米诺骨牌”。
POP 全称是 Property-Oriented Programming(面向属性编程)。
假设你是一个黑客,你发现网站有一个漏洞:它会把你发给它的“纸箱”(序列化数据)直接拆开组装(反序列化),并且组装后会自动触发魔法方法。
但是,那个自动触发的魔法方法本身可能并不危险。怎么办呢?黑客就开始找“多米诺骨牌”了:
黑客发现:对象 A 被唤醒时(触发 __wakeup),它会自动去摸一下 对象 B。
黑客又发现:对象 B 被摸的时候,会自动去按一下 对象 C 的开关。
黑客还发现:对象 C 的开关一旦被按下,就会执行系统命令(比如删库跑路)。
于是,黑客在自己的电脑上,把 A、B、C 三个对象按照这个顺序组装好,修改它们的属性(Properties),然后把它们“序列化”打包成一个纸箱,发送给服务器。
服务器收到纸箱,一打开(反序列化):
-
对象 A 醒了,自动触发魔法方法,摸了对象 B…
-
对象 B 被摸,按了对象 C 的开关…
-
对象 C 执行了危险命令!
啪!多米诺骨牌全倒了。
这就是 POP 链:利用 PHP 中相互关联的类和魔法方法,把一个个原本无害的动作像多米诺骨牌一样连接起来,最终引导程序去执行一段危险的代码。
把这两个概念结合起来看,反序列化漏洞的本质就是:服务器太轻易地相信了外面传进来的“纸箱”,而在拆纸箱的过程中,不小心触发了黑客提前布置好的“多米诺骨牌(POP链)”。
ok了解了以上概念后,回看这段源代码,发现它定义了 4 个类(Happy/Nevv/Rabbit/Year),并通过$_GET['pop']参数把输入的序列化字符串传入unserialize()函数执行反序列化操作。这就是漏洞的触发入口—— 可控的反序列化输入。
本题涉及的核心魔法方法及触发条件如下:
| __destruct() | 对象被销毁时(脚本执行结束自动触发) |
| __set() | 给不存在 / 私有的属性赋值时 |
| __get() | 访问不存在 / 私有的属性时 |
| __invoke() | 将对象当作函数调用时(如$obj()) |
| __call() | 调用对象不存在的方法时 |
| __wakeup() | 反序列化完成时自动调用(本题需要绕过) |
我们的目标是执行任意系统命令,而代码中唯一能执行命令的位置是Happy类的__call()方法,编写一段脚本:搭建一层套一层的对象结构,把 system 命令和读取 flag 的指令封装在最内层私有变量;最后输出一段 URL 编码后的序列化 payload,用于打 PHP 反序列化漏洞题目,目标是读取服务器 /flag 文件。
<?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));
?>

执行 php payload.php 输出的这一大串字符 = 可直接传输的反序列化漏洞攻击载荷(Payload)。于是加在URL后面,访问得到flag。

四、聪明的PHP

打开之后显示:

-
"pass a parameter" (传递一个参数): 服务器正在等待通过 GET 或 POST 方式传递一个参数给它。由于不知道参数名,我们需要猜解或爆破。
-
"maybe the flag file's filename is random" (也许 flag 文件的文件名是随机的): 这句话是核心关键。如果在普通的任意文件读取漏洞中,我们通常会直接尝试读取 /flag 或 flag.php。但既然文件名是随机的(比如叫 flag_2a9b3c.txt),我们就不能直接去读。这意味着我们需要一个能够执行系统命令(RCE)的方法来列出当前目录下的文件,也就是执行 ls 命令。
所以尝试拼接下常见的参数名cmd并赋值为系统命令ls,得到源代码。
在源代码里能看到这道题只是检查value,前面的key参数随意,所以这里我用cmd成功了,用什么都能成功,然后可以看到使用smarty函数:

$smarty->display("eval:".$value)表明当用户的输入(即 $_GET 参数中的 $value)与 eval: 资源标识符拼接并传递给 display() 函数时,Smarty 会将拼接后的字符串作为模板代码进行动态解析。如果传入符合 Smarty 语法的标签(通常包含在 {} 括号中),引擎就会将其解析并执行。这就是非常经典的服务器端模板注入 (SSTI) 漏洞,在 PHP 环境下,这通常会直接导致远程代码执行 (RCE)。

同时这里面有两个过滤器,第一个阻止对flag的输入,无论大小写(/i);第二个是一个针对常见危险 PHP 函数和 Linux 命令的黑名单,它拦截了常用于命令执行的函数(如 system, exec, eval, assert)以及常用于读取文件的函数或命令(如 readfile, cat, file, fgets)。如果输入匹配了上述任何关键字,程序就会渲染无害的 ./template.html,从而拦截攻击。
既然 system 和 exec 被禁用了,常见的替代品包括 passthru, shell_exec, popen, 或者 proc_open。我们试试passthru,使用passthru ls -a命令显示服务器目录下的文件。

start.sh是执行脚本,访问得到:

说明flag文件是带着_下划线的,那就是在_12529里面,于是使用showsource函数访问_12529文件,输入{show_source('/_12529')}URL编码后访问得到:

五、闪电十六鞭

我喜欢这种题目啊哈哈哈:

直接开始代码审计:

好的,那开始构建payload:
1. 绕过 echo/print 和 () 限制:使用短标签闭合
既然不能调用函数(小括号被禁),也不能使用 echo 等关键字输出,我们可以直接闭合 eval 中的 PHP 标签,利用 PHP 短标签直接输出变量。
-
在 eval() 中,如果使用 ?> 闭合了 PHP 代码,其后面的内容会被当作普通 HTML 文本直接输出到页面上。
-
紧接着使用 <?=(等同于 echo 的短标签,且在 PHP 5.4+ 中默认开启不受限制),强制输出想要的值。
2. 绕过 flag 关键字限制:异或运算 + 可变变量
不能直接写出 $flag,但 PHP 允许动态拼接变量名(如 ${'f'.'lag'})。由于点号 . 也被禁了,可以使用字符串异或(XOR)来凭空捏造出 flag 这个词
-
字符 ! 的 ASCII 码是 33。
-
'!' ^ 'G' (33 ^ 71) = 102 = 'f'
-
'!' ^ 'M' (33 ^ 77) = 108 = 'l
-
'!' ^ '@' (33 ^ 64) = 97 = 'a'
-
'!' ^ 'F' (33 ^ 70) = 103 = 'g'
-
因此,'!!!!' ^ 'GM@F' 就等于字符串 'flag'
-
结合可变变量语法:${'!!!!'^'GM@F'} 等价于 $flag。
3. 满足严格的 49 字符长度限制:利用注释填充
${'!!!!'^'GM@F'}这段代码的长度是 22 个字符。 但第二关要求长度必须刚好等于 49,不多不少。所以我们需要补充27 个字符。为了保证代码能正常解析,可以在 <?= 和 $ 之间插入一段多行注释 /*…*/ 来占位。多行注释本身的符号 // 占 4 个字符,所以里面需要塞入 23 个任意字符(比如数字)。 填充后的部分:/*12345678901234567890123*/ (长度刚好 27)。
最终传入的字符串:?><?=/*12345678901234567890123*/${'!!!!'^'GM@F'};

补充:
服务器的执行逻辑如下:
传参检测:有 flag 参数,通过第一关。
长度检测:严格等于 49 个字符,通过第二关。
正则检测:没有任何被黑名单拦截的字符,通过第三关。
终极执行 (eval):
-
服务器执行 eval("?><?=/*12345678901234567890123*/${'!!!!'^'GM@F'};")
-
?> 立即打断了当前的 PHP 解析。
-
<?= 开始强制输出后面表达式的值。
-
计算 '!!!!'^'GM@F' 得到 'flag',取出 $flag 的值(即通关密钥)并直接回显到浏览器页面上。
-
eval 执行完毕,返回 NULL。
最后的判定:
-
代码判断 NULL === sha1($flag),结果为假。
-
走到 else,页面上紧接着输出 '马老师发生甚么事了'。
虽然最终的代码判定失败了,但因为我们在 eval 执行期间已经强行把 $flag 的内容打印到了缓冲区,最终得出flag。