我先用一个具体的测试场景来开这个题。上一期我们聊了信息收集阶段该看什么、该收什么,这一期直接把视角切到 Web 实战里最经典、也最容易打出东西的一类漏洞:SQL 注入。为什么先讲它?因为直到今天,大量业务系统的主要数据存储仍然是关系型数据库,而 SQL 注入的本质就是“用户的输入变成了 SQL 语句的一部分”。只要这一点成立,后面无论是拖库、越权还是后台接管,都只是利用深度的问题。这一期我会从底层原理开始讲,然后重点拆解全场景下的挖掘思路——不是只盯着id=1后面加单引号,而是把 GET、POST、JSON、Header、Cookie 这些入口全部过一遍,最后结合手工验证和工具配合,把挖洞的完整思路串起来。
先说清楚一件事:下文所有操作场景均默认在授权范围内进行。我平时给团队做培训时反复强调一句话——漏洞挖掘的第一步不是找注入点,而是确认你对目标系统有没有合法的测试权限。未授权检测的性质完全不一样,这一点希望大家牢牢记住。
1. 先把底层原理讲透:SQL 注入到底是怎么发生的
1.1 不是“没过滤”,而是数据和代码没有分离
很多初学者对 SQL 注入有一个误解,觉得注入是因为程序“忘了过滤特殊字符”。这个理解不能说全错,但很片面。真正的核心在于:程序把用户的输入直接拼接进了 SQL 语句,导致输入被数据库引擎当成了 SQL 代码来执行。
用一段最常见的代码来说明:
$id = $_GET['id']; $sql = "SELECT * FROM products WHERE id = " . $id; $result = mysqli_query($conn, $sql);这段代码的问题不是“没有过滤'”,而是根本没有区分“数据”和“代码”。当用户提交id=1时,SQL 是正常的查询语句。但当用户提交id=1 UNION SELECT ...时,整个字符串变成了两条语义完全不同的 SQL 被拼接在一起执行。
如果给一个生活化的类比,就像是物业登记访客时,把访客姓名直接写进了门禁系统的执行脚本里。访客填的名字本来只是数据,结果因为系统没有做任何区分,访客填张三 && 打开所有门,系统就真的把开门指令当作名字的一部分执行了。这不是“没拦住关键字”,而是系统根本没有把“名字”和“指令”当成两回事。
在防御侧,最核心的对应方案就是参数化查询。同样一段逻辑,用参数绑定的方式写:
$stmt = $conn->prepare("SELECT * FROM products WHERE id = ?"); $stmt->bind_param("i", $id); $stmt->execute();这样数据库在解析阶段就把?的位置固定成了“数据位”,无论用户传什么,它都只会被当作一个字符串值,不会再参与 SQL 语法解析。理解了这一点,就有了对注入漏洞最本质的判断能力。
1.2 从一条 SQL 的执行链路看注入点为什么能成立
我们再把视角往底层挪一点,看看一条 SQL 在被数据库执行之前,到底经历了什么。数据库引擎收到 SQL 文本之后,大致有这么几步:
- 词法分析:把 SQL 文本拆成一个个 token,比如关键字、标识符、运算符、字符串常量。
- 语法解析:根据语法规则,把 token 序列构造成一棵语法树。注意,这一步是“理解语义”的关键,也就是说
WHERE id = 1和WHERE id = 1 UNION SELECT ...解析出来的语法树是完全不同的。 - 生成执行计划:优化器选择它认为成本最低的执行路径。
- 执行并返回结果。
注入的本质之所以成立,就是因为步骤 1 和步骤 2 发生在用户输入拼接完成之后。程序先把用户输入拼成完整字符串,再把整个字符串交给数据库去解析,此时用户输入已经不再是一个独立的数据单元了,而是被“喂”进了语法解析器。你可以把它理解为:有人在菜谱的“调料”栏里写了一行字,结果厨师把这行字也当成做菜步骤去执行了。
基于这个原理,我们可以推导出很多判断方法。比如为什么“加单引号”是最经典的探测方式?因为如果拼接处的 SQL 是WHERE id = '1',你提交1'的时候,整个字符串变成WHERE id = '1'',此时语法解析会认为多了一个单引号,导致语句不合法,于是数据库报错;这个报错就是一个“程序把输入拼进了 SQL”的强信号。反过来,如果我们提交的内容根本没有进入 SQL 执行上下文,那无论如何构造都不会引起 SQL 语法类报错。
所以说,报错回显、布尔差异、时间延迟、联合查询回显,所有注入判断技术的底层逻辑,都是围绕“输入内容是否被当成了 SQL 代码”来设计的。把握住这一层,你在任何新框架、新语言、新数据库面前都不会慌,因为原理不变,变的只是语法细节。
1.3 从回显差异快速区分注入点的类型
实际测试中,第一步要判断的就是注入点的类型。不同数据库、不同写法,注入点大致可以分成两类:数字型和字符型。判断的核心逻辑非常朴素:用“真值表达式”去干扰原来的查询条件,看页面的表现是否跟着变。
拿数字型举例。原始 URL 是product.php?id=1,我们构造id=1 AND 1=1,如果页面正常返回,再构造id=1 AND 1=2,如果页面没有返回数据,那基本可以确定:这个id参数是直接拼接在 SQL 里,且没有加引号包裹。
字符型的判断方式稍有差别。假设后端 SQL 是WHERE name = '$name',那我们在参数里提交test' AND '1'='1,最终拼接结果是WHERE name = 'test' AND '1'='1',语义恒真,页面正常;提交test' AND '1'='2,结果是WHERE name = 'test' AND '1'='2',语义恒假,页面异常。这里的核心操作是“先闭合前一个单引号”,让自己输入的 SQL 片段从一个字符串值中“逃逸”出来,成为真正可执行的语法结构。
我记得第一次带新人做这步时,他会问:为什么非要用AND '1'='1?直接用test' #把后面注释掉不行吗?其实两种都能用,但用AND '1'='1的方式更有普适性——有些数据库不认#注释,有些场景下注释符会被过滤。在没有工具辅助、纯手工判断的阶段,多用“恒真 vs 恒假”对比法是最稳妥的。后面的全场景挖掘部分,我会反复用到这个思路。
2. 全场景挖掘:注入点藏在哪些位置
2.1 别只盯着 URL 上的 id——传统参数位也要区分业务类型
刚开始接触 SQL 注入的人,最容易形成的一个习惯是:打开某个页面,看到 URL 里有个数字参数,就条件反射地测试。这个习惯本身没错,但如果你想从“会测”进阶到“会挖”,就必须建立一个新的认知:每一个参数都有它的“业务语义”,而业务语义往往决定了注入能否走到数据获取这一步。
比如商品列表页的category_id、文章详情页的article_id、用户中心的order_id,这些看起来都是数字型参数,但后面的处理逻辑可能完全不一样。有的后端会先做一次类型转换,比如intval($_GET['id']),这样你传字符串进去也会被强行转成数字,注入直接失效;有的后端直接把参数拼进了 SQL,没有任何类型限制,这时候就是标准的数字型注入。所以拿到参数之后,第一件事不是急着上 payload,而是先试一下:传一个非数字的字符串,看系统报错还是不报错、返回差异大不大。
POST 参数也是同一个逻辑。登录接口的username、password,搜索框的keyword,查询接口的start_time、end_time。其中搜索框是一个很特殊的高频注入位,因为很多后端代码会写成:
WHERE title LIKE '%$keyword%'这种模糊匹配拼接方式,不仅要处理引号闭合,还要注意%和_这两个通配符的干扰。我在测试里经常发现,一个搜索接口,开发者想着“这是模糊搜索,总该安全了吧”,结果连基本过滤都没有,直接在%通配符的位置闭合引号,注入就能成立。测试时可以先提交keyword=test' AND '1'='1,再提交keyword=test' AND '1'='2,观察返回结果是否出现差异。
路径参数也算传统位置里容易被忽略的一类。比如 RESTful 风格的接口/api/products/1,这里的1可能是后端用正则从路径里取出来再拼 SQL 的。浏览器地址栏里看着没有问号参数,有些人就漏掉了。测路径参数的方式和 GET 参数一样,直接在路径里改值看反应。
2.2 JSON 参数与嵌套结构——移动端接口必须过一遍
现在前后端分离已经是绝对主流,尤其是移动端 App 的后端接口,大量使用application/json作为请求体格式。这种情况下,如果你只盯着 URL 上的参数测,就会漏掉很大一片攻击面。JSON 请求体里同样可以存在注入,而且很多团队的 WAF 规则对 JSON 结构内的检测更松散,导致这里的挖掘成功率反而更高。
看一个典型的例子:
POST /api/user/login Content-Type: application/json { "username": "admin", "password": "123456", "channel": "ios" }如果后端的登录逻辑用了 MyBatis 类似的${}拼接,或者 PHP 里直接用字符串拼接,那么username和password这两个字段都可能存在注入点。测试时不用管格式问题,直接在 JSON 的值里加单引号,观察接口返回的报错信息。比如把username改成admin',如果接口返回 SQL 语法错误相关内容,那就是一个绝佳的入口。
JSON 参数的另外一个特点是“嵌套结构容易漏测”。有些接口的请求体长这样:
{ "user": { "profile": { "nickname": "test", "sign": "hello" } } }新手经常只在外层字段上做文章,忽略了内层的nickname、sign。但很多后端同学写代码时会图省事,直接用json_decode之后把某个深层字段拼进 SQL。我测过的接口里,就有过最外层字段全被参数化、内层某个字段却裸拼接的情况。所以凡是 JSON 请求体,不管嵌套多少层,每个字符串字段都值得过一遍注入测试。
2.3 HTTP 头部字段与业务参数——日志、统计、地域功能都是重灾区
很多时候,注入点不在业务参数里,而在那些“看起来像是系统自己生成的”数据上——HTTP 头部字段。这里要纠正一个常见的偏见:不是只有User-Agent和Referer才值得测,X-Forwarded-For、X-Real-IP、Client-IP这些头部同样可能被后端取出来写入数据库。
什么场景下这些头部会进入 SQL?最常见的是日志记录和访问统计。很多系统会做“访问者来源分析”,把用户 UA、来源页面、IP 地址存到数据库里。比方说一段常见的逻辑:
$ip = $_SERVER['HTTP_X_FORWARDED_FOR']; $sql = "INSERT INTO visit_logs (ip) VALUES ('$ip')";这种情况下,你在请求里把X-Forwarded-For改成1.2.3.4' OR '1'='1' #,如果后端没有过滤,这个字段就会直接成为一个注入点。我实际测试时,用这种头部注入拿过不少后台的日志库。更关键的是,很多研发团队的代码审查会盯着业务参数,却很少会注意IP、UA这类“辅助字段”的拼接逻辑,这就给挖掘留下了空间。
测试方法也很简单:抓包之后在请求头里找到对应的字段(如果没有就自己加),把值改成单引号,看看页面或接口的返回有没有变。如果加了单引号之后出现数据库报错,或者返回内容和正常值有可观察的差异,那就果断往深挖。不要觉得“这个接口是记录日志的,就算注入了又能怎样”——记录日志的库可能是主库,也可能和其他业务表关联,能跑通UNION SELECT的话,同样可以读数据。
2.4 Cookie 中的会话数据——购物车、语言偏好这类参数别忽略
最后一个容易被忽视的场景是 Cookie。很多系统会把“购物车商品 ID”“最近浏览记录”“语言偏好”这类信息序列化后直接放在 Cookie 里,然后在后端读出来拼 SQL 查询。
比如一个常见的场景:语言偏好参数。假如接口/api/index的 Cookie 里有一个lang=en,后端处理逻辑可能是:
$lang = $_COOKIE['lang']; $sql = "SELECT * FROM articles WHERE lang = '$lang' AND status = 1";这个时候,你把 Cookie 里的lang改成en' UNION SELECT ... #,注入点就出现了。看起来“只是一个语言选项而已”,但它照样能成为攻击面的一部分。很多白帽子习惯性地只测 URL 参数和 POST 参数,会把 Cookie 这块整个漏掉,非常可惜。
测试前先想清楚业务逻辑:哪些数据是服务端需要根据 Cookie 去数据库里查的?购物车 ID、用户 ID、历史浏览记录、语言、地区、主题皮肤——凡是涉及数据库查询的,都有拼接可能。老手和新手的差别,往往就在于能不能快速识别出“这个参数后面大概跟着一条 SQL”。
3. 实操环节:从发现参数到拿到数据
3.1 手工探测三板斧:单引号、恒真、恒假
商量完了场景,我们进入实操。我的建议是所有注入测试都从手工开始,即使最后要用工具,也一定要先手动确认注入点存在、类型清楚,再交给工具去跑数据。原因很简单:工具跑出来的结果,如果连你都不知道是怎么来的,漏洞报告就没法写清楚。
手工探测第一步:提交单引号。不管是 GET、POST 还是 Header,先往参数里放一个',看返回是否出现异常。这一步的作用是触发 SQL 语法错误,从报错里获取最直接的信息。如果出现了类似You have an error in your SQL syntax、ORA-00933、Unclosed quotation mark这样的关键字,注入点基本实锤。
第二步:恒真恒假对比。用AND 1=1和AND 1=2(数字型场景),或者' AND '1'='1和' AND '1'='2(字符型场景),观察页面内容是否有差异。如果恒真时页面返回正常、恒假时页面异常,那这就是一个“有差异”的注入点。如果两个都正常或两个都异常,说明输入可能没有直接影响查询逻辑,需要换一种闭合方式继续测。
第三步:尝试注释符。确定字符型之后,尝试用--+、#、-- -、/*...*/这类注释符号把后面的语句结构截断掉。为什么要注释?因为原始 SQL 中我们闭合的引号后面往往还跟着其他条件,比如' AND status=1,如果不注释掉,我们构造的语句可能因为多出来的引号或条件而没法正常执行。注释操作是手工验证里必不可少的一环。
这三步做完,你对这个注入点的存在性、类型、可闭合性已经有了基本判断。接下来再考虑是走联合查询、报错注入,还是盲注。
3.2 联合查询:先数清字段列数再拼 SELECT
联合查询是最理想的注入利用方式,因为它可以直接把查询结果展示在页面上。但有个前提条件:页面必须能看到本次查询的结果。如果是那种“真假值判断”的接口,联合查询不一定好使。
联合查询的第一步是确认字段列数,用ORDER BY试。假设注入点在id=1,我们依次提交:
id=1 ORDER BY 1 id=1 ORDER BY 2 id=1 ORDER BY 3当提交到ORDER BY 10且页面报错时,就说明 SELECT 查询的字段数少于 10,逐渐缩小范围,直到找到那个“刚好不报错”的最大值。这个值就是当前查询的字段总数。为什么用ORDER BY而不是直接UNION SELECT?因为UNION要求前后两个查询的字段数一致,如果不知道列数,UNION SELECT 1,2,3和原始查询列数不匹配,数据库会直接报错,且不返回我们需要的数据。先确定列数,再去考虑 select 的内容,这是联合查询最稳妥的执行路径。
确认列数之后,就可以构造:
id=1 UNION SELECT 1,2,3,4,5如果页面上回显了某个编号,比如“2”出现在页面标题里,说明第 2 列的位置可以展示数据。这时候把数字替换成database()、version()、@@datadir这类函数,就能直接读取数据库信息。接下来就是把information_schema里表和字段的信息查出来,逐步拿到目标数据。
3.3 无回显场景:布尔盲注和时间盲注怎么打
联合查询好用,但实际挖掘中会遇到大量“无回显”场景。什么是无回显?就是页面不会展示 SQL 查询的结果,只告诉你这个操作“成功”或“失败”、返回“有数据”还是“无数据”。这种情况下,我们需要把一次查询拆成很多次“真/假”判断,逐位猜解字符,这就是盲注。
布尔盲注的核心思路:用AND条件让原始查询的结果发生改变,然后对比页面的响应差异。典型的 payload 结构:
id=1 AND SUBSTRING((SELECT DATABASE()),1,1)='a'如果数据库名第一个字符确实是a,原始查询结果正常返回;如果不是,结果为空,页面表现不同。逐个字符、逐个位置地猜,最终拼出整个数据库名。这个过程非常慢,但它的通用性极强——不需要任何回显位置,只需要“页面对真假条件有可观察的差异”。判断可不可用,只需要试AND 1=1和AND 1=2,只要有差异,布尔盲注就能跑。
比布尔盲注更慢的是时间盲注。当页面连布尔差异都没有——无论真条件还是假条件都返回同样的内容——我们就只能通过“延时函数”来传递信息。以 MySQL 为例:
id=1 AND IF(SUBSTRING((SELECT DATABASE()),1,1)='a',SLEEP(3),0)如果数据库名第一个字符是a,数据库等待 3 秒再响应;否则很快返回。通过测量响应时间差异,可以逐位猜数据。时间盲注的难点在于网络波动会干扰判断,所以实践中一般会设置比较明显的时间差,比如 3 秒或 5 秒,并多次请求取平均值来降低误差。无论是布尔盲注还是时间盲注,手工执行都极其耗时,所以确认注入点之后,这类场景我通常直接交给工具跑,但前提是“这个注入口已经被手工人肉确认过”。
3.4 用什么工具配合——sqlmap 的实战姿势
说到工具,sqlmap在 SQL 注入测试里是绕不开的。我的用法是:手工确认了注入点之后,用 sqlmap 批量跑数据、跑库表、跑字段,然后再手动复核关键结果。
常用命令大致是这样的:
# 基础检测 sqlmap -u "http://target.com/product.php?id=1" --batch # 指定出库 sqlmap -u "http://target.com/product.php?id=1" --dbs # 指定库出表 sqlmap -u "http://target.com/product.php?id=1" -D dbname --tables # 指定表出字段 sqlmap -u "http://target.com/product.php?id=1" -D dbname -T users --columns # 指定字段出数据 sqlmap -u "http://target.com/product.php?id=1" -D dbname -T users -C username,password --dump如果是 POST 请求,可以用--data指定请求体,也可以直接把抓包保存成文件:
sqlmap -r request.txt --batch-r指定 HTTP 请求原文文件,这样可以把 Cookie、Header、POST 体一起带上去,减少漏测。
工具虽好用,但它不是万能的。遇到过不少场景:sqlmap因为超时设置或者 WAF 拦截跑了半天没结果,但手工用布尔盲注就注进去了。原因通常是目标系统的响应时间不稳定,或者 WAF 对 sqlmap 的请求特征做了识别。此时更合理的做法是手工确认注入点和注入类型,然后用本地脚本跑盲注,反而更快。工具是帮手,不是依赖。
4. 进阶技巧与防御视角:从绕过到修复
4.1 为什么 payload 会被拦——常见过滤与绕过思路
实战中经常遇到的情况是:手工测的时候明明有注入点,但一上 sqlmap 或者一发典型 payload 就被拦截了。这通常不是注入点消失了,而是有 WAF 或应用层过滤在工作。理解过滤规则之后,绕过就是一个“根据规则调整表达方式”的过程。
先说一个最基本的:关键字匹配。很多过滤规则是拿请求里的字符串去和“黑名单”比对,比如看到union、select、sleep就拦截。针对这种情况,可以尝试:
- 大小写变形:
Union SeLeCt。如果后端过滤区分大小写,这个就绕过去了。 - 注释符拆分:
UN/**/ION SE/**/LECT。MySQL 会把注释符直接忽略掉,拼起来还是UNION SELECT。 - 等价函数替换:
SUBSTRING换成MID或SUBSTR,SLEEP换成BENCHMARK,DATABASE()换成SCHEMA()。 - 十六进制编码:把字符串用
0x...形式表示。
但要注意,现在稍微像样的 WAF 都会做规整化处理,把注释符、大小写都归一化了再检测。所以绕 WAF 的“技术含量”更多地体现在:找到 WAF 检测的盲区。比如某些 WAF 只检测查询字符串,不检测 JSON 请求体的嵌套字段;某些 WAF 只处理application/x-www-form-urlencoded,遇到multipart/form-data解析就不完整。这就是为什么前面强调全场景挖掘——换个数据入口,往往就绕过了拦得死死的 URL 检测逻辑。
4.2 从注入点到数据获取的完整路径
前面一直在讲“找入口”,这里把从注入到数据获取的完整路径串一遍,方便你建立整体感。
- 识别入口:找参数、找拼接点,判断是数字型还是字符型。
- 确认注入:用单引号、恒真恒假、注释符这三板斧验证。
- 判断可利用方式:有回显优先联合查询,有报错信息优先报错注入,两者都不可用再考虑盲注。
- 查库名:有回显直接
UNION SELECT database();无回显用SUBSTRING逐字符猜。 - 查表名:从
information_schema.tables查库下所有表。 - 查字段名:从
information_schema.columns查表的字段。 - 取数据:把目标字段查出来,必要时做解码、去重、关联分析。
这一套流程里,最难的不是单个步骤的技术,而是“判断到底能不能用某种方式”。比如你看到页面有报错回显,第一反应应该是报错注入而不是联合查询——因为报错注入往往只需一条 payload 就能带出数据,比联合查询去逐个猜列名高效得多。以 MySQL 的extractvalue为例:
id=1 AND extractvalue(1,concat(0x7e,(SELECT database()),0x7e))如果数据库返回类似XPATH syntax error: '~dbname~'的内容,数据库名就在报错信息里。这种“拿报错当回显”的思路,是经验积累的核心体现。
4.3 防御视角:为什么参数化查询能彻底解决注入
作为漏洞挖掘者,理解防御方案同样重要。这不仅是为了写报告时给修复建议,更关键的是:你知道开发者会怎么修,也就知道哪类代码不容易出问题、哪类代码容易出问题,反过来指导自己的挖掘方向。
SQL 注入最稳妥的修复方式是参数化查询,也叫预编译。Java 里对应PreparedStatement,PHP 里对应PDO的 prepare 模式,Python 里对应参数占位符%s。无论哪种语言,思想都是一致的:SQL 结构和数据分离。结构在预编译阶段就确定了,用户输入只能作为数据传参,无法再参与语法解析。
现实世界中,代码不安全的根因往往不是“不知道要参数化”,而是“偷懒拼接”。比如动态排序场景,需要根据用户传的字段名来排序,开发者图省事直接拼接:
ORDER BY $sort_field此时字段白名单反而是更合理的方案:定义好可用的排序字段列表,用户传的值必须在这个列表里才能使用,否则用默认值。这个思路我在测试中也经常用来“预判”易注入点——凡是需要动态拼表名、字段名、排序条件的业务逻辑,注入的概率都明显偏高。
5. 常见问题与排查技巧实录
5.1 单引号打上去没反应,可能是哪些情况
这是新手问得最多的一个问题。提交'之后页面毫无变化,不一定说明没有注入点,还可能是这几种情况:
- 参数经过了类型转换,比如
intval()强制转成了数字,单引号直接被丢弃。 - 页面开启了自定义错误处理,SQL 报错被吞掉了,没有展示在页面上。
- 后端用了参数化查询,单引号作为合法数据被传递了,自然不会报错。
- 你测的参数根本没有进入数据库查询逻辑,比如只是个前端展示参数。
排查思路也很简单:先确认这个参数真的会被服务端用到。判断方法之一是修改参数值,看看返回内容是否变化;如果参数值改成 2 和改成 3 页面完全一样,那它很可能只是个“摆设”,不值得继续投入。
5.2 布尔盲注响应不稳定,怎么解决
布尔盲注最忌讳的就是“页面本身有随机变化”。我遇到过一些情况:目标首页有一个随机推荐模块,每次请求内容都不一样;或者目标链路里有多层缓存,某些请求命中缓存,导致真假条件都返回同样内容。这类环境会造成大量的误判。
解决办法有两个方向:一是“观察点”选对,找页面中稳定不变的文本作为判断依据,不要用随机模块;二是多次请求取多数结果,比如同一个条件请求三次,至少有两次表现一致才记录这次判断结果。我在写自动脚本时会加入这样的逻辑,手工测的时候就多看一眼,别急着下结论。
5.3 sqlmap 跑不出结果,如何手工补救
sqlmap 跑不动的原因很多:请求超时、WAF 拦截、注入点需要特定前置条件(比如先登录)、数据库是少见类型。遇到这种情况,我的习惯是退回到手工确认的三板斧,先把注入点性质彻底搞明白:
- 如果确认是字符型,且闭合方式是
',构造:
test' AND SUBSTRING((SELECT DATABASE()),1,1)='a' #观察真假两种条件下的响应差异。这一步确认布尔盲注可行之后,写一个小脚本逐位跑。跑布尔盲注的脚本不用很复杂,Python 里用 requests 库就能做:
import requests url = "http://target.com/product.php?id=1" result = "" for i in range(1, 32): for c in "abcdefghijklmnopqrstuvwxyz0123456789_": payload = f"1 AND SUBSTRING(DATABASE(),{i},1)='{c}'" r1 = requests.get(url, params={"id": payload}) if b"marker_string" in r1.content: result += c print(result) break核心逻辑就是“一个字符一个字符地猜”,真条件能拿到正常页面,假条件拿不到。脚本越简单越不容易写错,跑起来慢点没关系,关键要稳。很多看起来“工具搞不定”的目标,最后都是靠这类手工脚本解决的。
5.4 授权测试中的几个实用建议
最后分享几条从实际项目中总结出来的经验,也是合规和执行层面的细节:
- 测试范围以授权书为准,不在范围内的系统、接口、数据库,一律不碰。这不仅是合规问题,也是保护自己的基本前提。
- 写测试报告时注明“影响范围”和“复现条件”。一个合格的漏洞报告,至少要让开发人员按照步骤就能复现问题,而不是只丢一句“存在 SQL 注入”。
- 数据获取阶段坚持最小化原则,能证明漏洞影响即可,没有必要把整个库拖下来。
- 测试时间尽量避开业务高峰期。延时注入会消耗数据库资源,在交易类系统上肆无忌惮地跑
SLEEP(5)很可能影响线上业务,这个责任谁都担不起。
我个人在带团队做授权检测时的习惯是:先摸清所有入口、圈定优先级,再逐个深入,而不是用一个 payload 把所有参数扫一遍。因为后者看起来覆盖面广,实际上很容易被 WAF 打草惊蛇,而且误报率也不低。SQL 注入的挖掘,说到底是一个“理解业务、理解拼接、理解数据库”的综合过程,越到后面越考验你对整个请求链路细节的把控。
这套思路从我早期做安全测试一直沿用到现在,不管目标换成什么框架、什么数据库,基本都能快速产出结果。下一篇我们继续沿着 Web 实战的方向走,会聊一聊认证逻辑里的那些坑。