SQL注入实战:sqlilabs Less1-20手注通关指南

发布时间:2026/10/18 5:26:33
SQL注入实战:sqlilabs Less1-20手注通关指南
1. 为什么我建议你从 Less1-20 开始啃 SQL 注入如果你刚接触 Web 安全或者已经刷过一些靶场但总觉得“会了又好像没会”那我建议你把 sqlilabs 的 Less1 到 Less20 老老实实手注一遍。这二十关不是随便凑数的它几乎把注入测试里最常见的几类场景都覆盖了联合查询、报错注入、布尔盲注、时间盲注、堆叠查询、POST 注入、Cookie 注入、Referer 注入、User-Agent 注入、二次注入。你把这二十关吃透后面再遇到真实业务里的注入点脑子里会自动跳出对应的测试路径而不是对着一个参数发呆。我自己带过几个刚入行的朋友他们最大的问题不是不会用工具而是拿到一个注入点不知道从哪下手。sqlilabs 前二十关的价值就在于它把“注入点出现在哪里”和“数据怎么带出来”这两个核心问题拆得很细。Less1 到 Less4 是 GET 型联合查询Less5 到 Less6 是报错注入Less7 是文件导出Less8 到 Less10 是盲注Less11 到 Less17 把注入点搬到了 POST、Cookie、Referer、User-Agent、UA 这些位置Less18 到 Less20 则是 Header 注入和二次注入。每一关都在逼你换一种思路。这篇文章我不打算只给你一串 payload 让你复制粘贴。那种写法你刷完就忘。我会把每一关的注入点位置、闭合方式、判断逻辑、数据提取路径都拆开讲顺便把我自己踩过的坑和调试技巧一并写出来。你跟着走一遍至少能做到看到一个参数知道先测什么、再测什么、报错怎么读、盲注怎么加速。提示sqlilabs 是本地搭建的练习环境所有测试都在你自己的机器或授权环境中进行。真实业务系统未经授权不要碰这是底线。2. 环境准备与通用测试思路2.1 本地环境怎么搭最省事sqlilabs 的搭建方式很多我试过几种最省事的还是用集成环境。如果你用 phpStudy 或者 XAMPP直接把 sqlilabs 目录丢到 Web 根目录下然后改一下sql-connections/db-creds.inc里的数据库账号密码浏览器访问setup-db.php初始化数据库就行。PHP 版本建议用 5.6 到 7.4 之间太新的 PHP 版本有些老函数会报弃用警告虽然不影响注入但页面会多出一堆提示看着烦。数据库字符集这块要注意sqlilabs 默认用的是 GBK 或者 latin1这直接影响到你后面 Less1 到 Less4 的联合查询能不能正常回显中文。如果你发现 union 查询出来的数据是乱码先别怀疑 payload 写错了去检查一下数据库和表的字符集。我一般会在初始化之后手动确认一下security库的字符集省得后面来回折腾。2.2 手注的通用判断流程不管你面对的是哪一关手注的判断流程基本是固定的。我把它总结成四步找注入点、判闭合、定列数、取数据。找注入点就是看哪个参数参与了数据库查询通常加个单引号看页面有没有变化。判闭合是确定这个参数是用单引号、双引号还是括号包裹的这决定了你后面 payload 怎么拼。定列数是为了 union 查询做准备用order by二分法最快。取数据就是根据回显情况选择联合查询、报错注入还是盲注。这四步里判闭合是最容易出错的。很多人加了个单引号看到报错就以为闭合方式是单引号其实还要看报错信息里单引号的位置。比如报错显示1 LIMIT 0,1那说明你的单引号被转义了或者闭合方式不对。我一般会同时试、、)、))这几种看哪种能让页面恢复正常或者报错信息发生变化。注意每换一种闭合方式都要重新确认页面状态。不要凭感觉猜一定要看回显。2.3 工具选型手注为主工具为辅我建议 Less1 到 Less20 全部手注一遍不要一上来就开 sqlmap。原因很简单工具会帮你自动判断闭合和注入类型你跳过了最关键的思考过程。等你手注把这二十关的逻辑都摸清了再用 sqlmap 去验证和加速那时候你才知道工具在干什么它报的 payload 为什么是那个样子。当然手注不代表不能用辅助手段。浏览器开发者工具、Burp Suite 的 Repeater、甚至一个简单的 HackBar 插件都能帮你快速改参数和看响应。我个人的习惯是 Burp 抓包之后丢到 Repeater 里反复调 payload这样比在浏览器地址栏里改要方便得多尤其是 POST 注入和 Header 注入那几关。3. Less1-4联合查询注入的四种闭合3.1 Less1单引号字符型注入Less1 是最基础的一关注入点在id参数闭合方式是单引号。你访问?id1正常显示?id1报错?id1 --恢复正常基本就能确定是单引号字符型。接下来用order by定列数?id1 order by 3 --正常order by 4报错说明当前查询是三列。联合查询的 payload 是?id-1 union select 1,2,3 --。这里为什么用-1而不是1因为如果 id 存在页面会优先显示 id 对应的数据union 的结果可能被挤掉。用-1让前半段查询为空union 的结果就能直接回显在页面上。你会在页面上看到 2 和 3 的位置有回显这两个位置就是后面取数据的地方。取库名的 payload 是?id-1 union select 1,database(),3 --取表名用group_concat(table_name)配合information_schema.tables取列名同理。这一关的重点是理解 union 查询的回显位置以及--注释掉后面 SQL 语句的作用。--是 SQL 注释符在 URL 里代表空格合起来就是--把后面的LIMIT 0,1注释掉。3.2 Less2数字型注入Less2 和 Less1 的页面长得一模一样但闭合方式不同。你加单引号发现页面报错但报错信息里单引号的位置和 Less1 不一样。Less2 是数字型不需要单引号闭合直接?id1 order by 3 --就能用。很多人在这里会惯性思维加单引号结果一直报错。数字型注入的 payload 是?id-1 union select 1,2,3 --注意这里没有单引号。判断数字型还是字符型最直接的方法就是看加单引号和不加单引号的区别。数字型加单引号会报错不加单引号正常字符型加单引号报错加单引号加注释恢复正常。这一关就是让你体会这个区别。3.3 Less3单引号加括号闭合Less3 的闭合方式是)也就是单引号加右括号。你试?id1报错?id1) --恢复正常说明 SQL 语句里 id 被()包裹。这种闭合方式在真实业务里也不少见尤其是 PHP 代码里写WHERE id ($id)这种形式。payload 是?id-1) union select 1,2,3 --。这一关的坑在于如果你只试单引号闭合会一直报错因为括号没闭合。我建议你在判闭合的时候把)和))都试一遍看哪个能让页面恢复正常。Less3 和 Less4 就是专门练这个的。3.4 Less4双引号加括号闭合Less4 的闭合方式是)双引号加右括号。你试?id1报错?id1) --恢复正常。payload 是?id-1) union select 1,2,3 --。这一关和 Less3 对称一个是单引号加括号一个是双引号加括号。这四关连起来看其实就是四种最常见的闭合方式单引号、无引号、单引号加括号、双引号加括号。你把 Less1 到 Less4 的闭合判断过程练熟后面遇到任何注入点判闭合这一步都不会卡住。我自己的习惯是拿到一个参数先加单引号看报错信息里单引号是怎么被包裹的然后决定用哪种闭合方式。实操心得判闭合的时候不要只看页面是否报错还要看报错信息里你的输入被怎么处理了。报错信息是最好的老师。4. Less5-7报错注入与文件导出4.1 Less5报错注入取数据Less5 的页面和 Less1 一样但你加单引号之后页面只显示“You are in...”没有回显数据。这时候 union 查询就用不了了因为页面不显示查询结果。你需要用报错注入让数据库把数据通过报错信息带出来。报错注入常用的函数是updatexml和extractvalue。payload 是?id1 and updatexml(1,concat(0x7e,database(),0x7e),1) --。0x7e是波浪号~用来分隔数据方便你在报错信息里定位。执行之后页面会报错报错信息里就包含库名。这一关的重点是理解报错注入的原理数据库在执行updatexml时第二个参数应该是合法的 XPath 表达式你传一个带波浪号和库名的字符串进去数据库会报错并把这个字符串拼到报错信息里。extractvalue同理只是参数位置不同。4.2 Less6双引号报错注入Less6 和 Less5 逻辑一样只是闭合方式变成了双引号。payload 是?id1 and updatexml(1,concat(0x7e,database(),0x7e),1) --。这一关没什么新东西就是让你练一下双引号闭合下的报错注入。报错注入有个限制updatexml和extractvalue能带出的数据长度有限一般是 32 个字符左右。如果你要取的库名或表名很长需要用substr或mid分段取。我一般会先用group_concat把多个表名拼起来如果超长再分段。4.3 Less7文件导出到本地Less7 的页面有一个“File upload”的提示但实际注入点还是id参数。这一关练的是into outfile文件导出。payload 是?id1)) union select 1,?php phpinfo(); ?,3 into outfile C:/phpstudy/www/shell.php --。这里有几个前提数据库用户需要有FILE权限secure_file_priv不能限制导出路径目标目录要有写权限。我本地测试的时候secure_file_priv默认是空可以直接导出。如果你导出失败先查一下show variables like %secure_file_priv%看看是不是路径被限制了。注意文件导出在真实环境里风险很高而且现在很多数据库默认关闭了FILE权限。这一关主要是让你理解这个手法实际测试中不要轻易尝试写文件。5. Less8-10盲注的三种形态5.1 Less8布尔盲注Less8 加单引号之后页面没有报错也没有回显只是“You are in...”消失了。这说明页面不显示数据库错误也不显示查询结果只能通过页面状态的变化来判断。这就是布尔盲注。布尔盲注的核心是把问题变成“是/否”的判断。比如?id1 and length(database())8 --如果页面正常显示“You are in...”说明库名长度是 8如果页面空白说明不是 8。通过不断调整判断条件你可以一个字符一个字符地把数据猜出来。布尔盲注的 payload 结构是?id1 and ascii(substr(database(),1,1))115 --。substr取第一个字符ascii转成 ASCII 码然后和 115 比较。如果页面正常说明第一个字符是s。你可以用二分法加速先判断大于 100 还是小于 100逐步缩小范围。这一关手动猜很累我建议你写个简单的 Python 脚本来自动化。脚本逻辑就是遍历字符位置和 ASCII 码范围根据页面响应判断真假。这样你能把精力放在理解盲注逻辑上而不是重复劳动。5.2 Less9时间盲注Less9 比 Less8 更极端页面无论真假都显示一样的内容你没法通过页面状态判断。这时候只能用时间盲注通过sleep函数让数据库延迟响应根据响应时间判断真假。payload 是?id1 and if(length(database())8,sleep(3),0) --。如果库名长度是 8页面会延迟 3 秒才返回如果不是立即返回。通过响应时间差你可以判断条件真假。时间盲注的坑在于网络延迟和服务器负载会影响判断。我一般会把sleep时间设成 3 到 5 秒太短了容易误判太长了测试效率低。另外如果你用 Burp 抓包可以在 Repeater 里看响应时间比在浏览器里看更准。5.3 Less10双引号时间盲注Less10 和 Less9 逻辑一样只是闭合方式变成了双引号。payload 是?id1 and if(length(database())8,sleep(3),0) --。这一关就是让你练一下双引号闭合下的时间盲注。盲注这三关连起来其实是在教你一个递进的过程有回显用联合查询有报错用报错注入没回显没报错用布尔盲注连页面状态都没变化用时间盲注。你在真实业务里遇到注入点也是按这个顺序去试。实操心得盲注自动化脚本不要一上来就写得很复杂先把单个字符的判断跑通再套循环。我见过有人脚本写了一大堆结果连第一个字符都没猜对就是判断逻辑写错了。6. Less11-17POST 与 Header 注入6.1 Less11POST 表单注入从 Less11 开始注入点从 URL 参数变成了 POST 表单。你需要在登录框里输入用户名和密码后端用SELECT语句验证。注入点在uname参数闭合方式是单引号加括号。payload 是uname-1 union select 1,database(),3 -- passwd1submitSubmit。注意这里用-1让前半段查询为空union 的结果就能回显。POST 注入和 GET 注入的逻辑完全一样只是传参方式不同。你用 Burp 抓包之后在 Repeater 里改uname参数就行。这一关的坑在于登录框的 SQL 语句可能是SELECT * FROM users WHERE username$uname AND password$passwd你需要在uname里闭合单引号然后用--注释掉后面的密码判断。如果你在passwd里注入可能因为uname先被判断而失败。6.2 Less12POST 报错注入Less12 和 Less11 类似但页面不回显查询结果只显示登录成功或失败。这时候需要用报错注入。payload 是uname-1 and updatexml(1,concat(0x7e,database(),0x7e),1) -- passwd1submitSubmit。这一关的闭合方式是)你需要先判断清楚。我一般会在uname里试、)、这几种看哪种能触发报错。POST 注入的判断过程和 GET 注入一样只是改参数的位置从 URL 变成了表单。6.3 Less13POST 双引号加括号Less13 的闭合方式是)payload 是uname-1) and updatexml(1,concat(0x7e,database(),0x7e),1) -- passwd1submitSubmit。这一关和 Less12 对称一个是单引号加括号一个是双引号加括号。6.4 Less14POST 双引号报错Less14 的闭合方式是双引号payload 是uname-1 and updatexml(1,concat(0x7e,database(),0x7e),1) -- passwd1submitSubmit。这一关没什么新东西就是练双引号闭合下的 POST 报错注入。6.5 Less15POST 布尔盲注Less15 页面只显示登录成功或失败没有报错信息。你需要用布尔盲注。payload 是unameadmin and length(database())8 -- passwd1submitSubmit。如果页面显示登录成功说明条件为真如果显示失败说明条件为假。这一关的坑在于你需要先找到一个存在的用户名比如admin然后用and拼接判断条件。如果你用-1或者不存在的用户名页面永远显示失败没法判断真假。6.6 Less16POST 双引号布尔盲注Less16 和 Less15 逻辑一样闭合方式变成双引号加括号。payload 是unameadmin) and length(database())8 -- passwd1submitSubmit。这一关就是练双引号加括号闭合下的布尔盲注。6.7 Less17POST 密码修改注入Less17 是一个密码修改页面注入点在uname参数。这一关的 SQL 语句是UPDATE users SET password$passwd WHERE username$uname。你需要用报错注入把数据带出来。payload 是unameadmin and updatexml(1,concat(0x7e,(select database()),0x7e),1) -- passwd1submitSubmit。注意这里updatexml里的子查询需要用括号包起来因为updatexml的第二个参数不能直接写select语句。这一关的坑在于UPDATE语句的报错注入和SELECT语句略有不同你需要确保uname参数能触发updatexml执行。如果页面没有报错可能是uname不存在或者闭合方式不对。提示POST 注入和 Header 注入在真实业务里很常见因为很多开发者只关注 URL 参数忽略了表单和请求头。你在测试的时候不要只盯着 URL把 POST 数据、Cookie、Referer、User-Agent 都看一遍。7. Less18-20Header 注入与二次注入7.1 Less18User-Agent 注入Less18 的注入点在User-Agent请求头。后端会把 User-Agent 记录到数据库里你用 Burp 抓包修改 User-Agent 的值加上单引号和报错注入 payload。payload 是User-Agent: and updatexml(1,concat(0x7e,database(),0x7e),1) and 11。注意这里闭合方式是单引号而且后面需要补一个and 11让 SQL 语句完整。这一关的坑在于User-Agent 的值会被拼接到INSERT语句里你需要确保闭合之后 SQL 语法正确。7.2 Less19Referer 注入Less19 的注入点在Referer请求头逻辑和 Less18 一样。payload 是Referer: and updatexml(1,concat(0x7e,database(),0x7e),1) and 11。这一关就是让你练一下 Referer 头的注入。Header 注入的特点是你没法直接在浏览器地址栏里改必须用 Burp 或者类似的抓包工具。我一般会在 Burp 的 Repeater 里改 Header然后看响应。Header 注入在真实业务里也不少尤其是那些记录访问日志的功能。7.3 Less20Cookie 注入Less20 的注入点在Cookie里的uname参数。payload 是Cookie: unameadmin and updatexml(1,concat(0x7e,database(),0x7e),1) --。这一关的闭合方式是单引号你需要先判断 Cookie 里的值是怎么被拼接到 SQL 语句里的。Cookie 注入的坑在于Cookie 的值可能会被 URL 编码你需要在 Burp 里注意编码问题。另外Cookie 注入通常发生在“记住我”或者“用户偏好”这类功能里后端会把 Cookie 的值查数据库。7.4 二次注入的思路Less20 之后其实还有二次注入的内容但前二十关里 Less20 已经涉及了 Cookie 注入。二次注入的核心思路是你先把恶意数据存到数据库里然后在另一个功能点触发查询数据被带出来。这种注入方式比直接注入更隐蔽因为注入点和使用点不在同一个地方。我建议你在刷完前二十关之后自己搭一个简单的二次注入场景练一下。比如注册一个用户名为admin --的账号然后用这个账号登录看会不会触发 SQL 错误。二次注入的关键是找到“存储”和“使用”两个环节。实操心得Header 注入和 Cookie 注入在真实业务里往往被忽略因为很多人只测 URL 和表单。你在测试的时候把 Burp 抓到的每一个请求头都看一遍尤其是那些会被后端记录的值。8. 常见问题与排查技巧实录8.1 报错信息不显示怎么办如果你加单引号之后页面没有报错可能是后端关闭了错误显示。这时候你需要用盲注。先试布尔盲注看页面状态有没有变化如果页面状态也没变化再试时间盲注。不要死磕报错注入换一种思路。8.2 union 查询不回显怎么办union 查询不回显通常有两个原因一是列数不对二是回显位置被前半段查询占了。你先用order by确认列数然后用-1让前半段查询为空。如果还是不回显可能是页面只显示特定列的数据你需要调整 union 里的字段顺序。8.3 盲注脚本跑得太慢怎么办盲注脚本慢是因为你要一个字符一个字符地猜。加速的方法有几种用二分法代替线性遍历用ascii和substr配合把sleep时间设短一点但增加重试次数。我一般会把 ASCII 码范围先缩小到 32 到 126然后二分查找。8.4 文件导出失败怎么办文件导出失败先查secure_file_priv如果是NULL说明禁止导出如果是空说明可以导出到任意路径如果是一个具体路径只能导出到那个路径。另外数据库用户需要有FILE权限目标目录要有写权限。这几个条件缺一不可。8.5 POST 注入抓不到包怎么办POST 注入需要用 Burp 或者浏览器开发者工具抓包。如果你用 Burp确保代理设置正确浏览器访问的是本地环境。如果抓不到包检查一下 Burp 的拦截开关是不是开着或者浏览器的代理插件有没有生效。问题可能原因排查方法加单引号不报错错误显示关闭改用盲注union 不回显列数不对或回显位置被占用 order by 定列数用 -1 让前半段为空盲注太慢线性遍历改用二分法文件导出失败权限或路径限制查 secure_file_priv 和 FILE 权限POST 抓不到包代理或拦截设置问题检查 Burp 代理和浏览器代理8.6 独家避坑技巧我踩过最大的坑是判闭合的时候太着急看到一个报错就以为闭合方式对了结果后面 payload 一直不成功。后来我养成了一个习惯每换一种闭合方式先确认页面状态再确认报错信息最后才写完整 payload。这个习惯帮我省了很多时间。另一个坑是盲注的时候没有先确认数据库类型。sqlilabs 用的是 MySQL但真实业务里可能是其他数据库。不同数据库的盲注函数不一样比如 MySQL 用substr和ascii其他数据库可能用substring和unicode。你先确认数据库类型再写 payload能少走很多弯路。9. 刷完前二十关之后怎么继续前二十关刷完你基本掌握了注入测试的主要类型。接下来我建议你做三件事一是把每一关的 payload 整理成自己的笔记标注清楚闭合方式、注入类型、关键函数二是用 sqlmap 跑一遍前二十关对比工具的判断和你自己的判断有什么不同三是找一个真实的开源项目在本地搭起来试着找注入点。sqlilabs 后面的关卡还有宽字节注入、堆叠注入、HTTP 参数污染等内容你可以继续往后刷。但前二十关是基础基础不牢后面越刷越迷糊。我见过有人刷到三十多关回头问他 Less1 的闭合方式他还是说不清楚。这种刷法没意义。最后分享一个小技巧你在刷每一关的时候不要只看 payload 能不能跑通还要想一下这关的 SQL 语句大概长什么样。比如 Less1 的 SQL 大概是SELECT * FROM users WHERE id$id LIMIT 0,1Less11 的 SQL 大概是SELECT * FROM users WHERE username$uname AND password$passwd LIMIT 0,1。你能还原出 SQL 语句说明你真的理解了这一关的注入逻辑。这个习惯对你后面分析真实业务的注入点非常有帮助。