sqli-labs 是我见过最适合新手入门 SQL 注入的靶场,没有之一。Less 1 又是整个靶场的第一关,难度最低,但它把 SQL 注入最核心的“判断注入点、确认列数、找显示位、联合查询脱库”这条完整链路都串起来了。我第一次接触 SQL 注入就是从这一关开始的,当时照着网上的教程一步步敲,磕磕绊绊半天才搞明白每个参数为什么要这么改。这关如果吃透了,后面几十关的核心思路都会顺畅很多。
这篇文章我会把 Less 1 从环境准备到最终拿到数据的全过程完整走一遍,每一步都会解释“为什么要这么做”,而不是单纯丢给你几条 SQL 语句。适合完全零基础、刚搭好靶场但不知道从哪下手的初学者,也适合对注入原理一知半解、想回头补基础的人。
1. 环境搭建与靶场准备
1.1 本地实验环境怎么配
sqli-labs 是一个基于 PHP + MySQL 的靶场,所以本地环境需要能跑 PHP 和 MySQL。新手最省事的方案是装一个集成环境,常见的几款各有优劣:
- phpStudy:国内用的人多,启动快,自带 PHP 和 MySQL,非常适合新手。注意选一个稳定的版本,别追最新版。
- XAMPP:跨平台,干净清爽,适合喜欢自己动手折腾的。默认面板是英文,对部分新手可能有点门槛。
- LAMP / LNMP 手动部署:适合有一定 Linux 基础的人,熟悉一下原生环境没有坏处,但对纯新手不推荐,容易在环境问题上卡住。
我用的是 phpStudy,版本是较稳定的 2018 版,PHP 版本选的 5.6(后面会说为什么不建议用 7.x)。安装完成后,把 Web 服务器的根目录设为sqli-labs所在的目录,然后启动 Apache 和 MySQL。
1.2 靶场源码部署与常见坑
从官方仓库把靶场源码下载下来后,解压到 Web 根目录,比如C:\phpstudy_pro\WWW\sqli-labs。这时候访问http://localhost/sqli-labs/,会看到靶场的首页。
第一次进来要初始化数据库,页面顶部会提示你创建一个数据库。正常流程是点击“Setup/reset Database”链接,它会自动创建security库和对应表。这一关数据量不大,整个过程几秒钟。
我遇到过几个常见问题,提前写在这里给你避坑:
- 页面能打开但点链接报数据库连接错误:大概率是数据库账号密码不对。默认配置里连接信息写在
sql-connections/sql-connect.php里,确认用户名和密码跟你环境里 MySQL 的一致。 - 创建数据库时报错:先检查 MySQL 是不是真的启动了,再确认当前用户有没有建库权限。phpStudy 默认 root 用户密码是空的,如果改了密码,记得同步改靶场配置文件。
- PHP 版本问题:如果你用的是 PHP 7.x 或更高版本,某些代码会报
mysql_*函数未定义的错误,因为老靶场用的是 PHP 5 时代的mysql_系列函数。最简单的办法是切换 PHP 版本到 5.6,或者开启兼容扩展。
环境一旦通了,能正常打开 Less 1 的页面,接下来的所有操作就都能在自己电脑上复现。这也是靶场最大的价值:你可以随便折腾,不用担心把别人的系统搞坏。
2. SQL 注入原理与 Less 1 关卡分析
2.1 为什么会存在 SQL 注入漏洞
要理解 Less 1,得先明白一句话:SQL 注入的本质是用户输入被拼进了 SQL 语句,并且被执行了。
举个生活化的例子。你在一家餐厅点餐,菜单上是固定的菜品。正常流程是你在菜单上勾选“红烧肉”,后厨照着菜单做菜。但是如果服务员偷懒,直接把你的原话当成菜名递给后厨,你说“红烧肉;再给我两瓶酒”,后厨可能真会照做。SQL 注入就是这么回事——程序把用户输入的参数原封不动地塞进 SQL 查询里,没有做任何过滤和校验。
Less 1 的后台查询逻辑大致是这样:
$sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1";注意看,这里的$id是从 URL 参数?id=直接取出来的,外面加了一对单引号。如果你正常请求?id=1,最终执行的 SQL 就长这样:
SELECT * FROM users WHERE id='1' LIMIT 0,1看起来没什么问题。但如果我传的id不是数字,而是一段精心构造的字符串呢?比如id=1',那 SQL 就会变成:
SELECT * FROM users WHERE id='1'' LIMIT 0,1单引号数量不配对,SQL 语法错误,数据库就会抛异常。网页上通常会把错误信息直接显示出来——这就是我们用来判断注入点的突破口。
2.2 请求特征与注入点判断
打开 Less 1 的页面,点一下“Let's inject”,URL 会变成类似这样:
http://localhost/sqli-labs/Less-1/?id=1页面上一大段文字,最显眼的是“You are in...”,以及一行看似莫名其妙的代码。这行代码其实就是后台查询语句的预览,它非常诚实地告诉了你:你的输入被直接拼接到 SQL 里了。
接下来我们做第一步测试:把id=1改成id=1'(数字后面加一个单引号),回车。你会发现页面变了,不再显示“You are in...”,而是蹦出一段错误:
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'',这正好对应查询语句WHERE id='$id'中夹着输入的那两个单引号。 - SQL 语句末尾还有一段
LIMIT 0,1。这是靶场自己加的,限制只显示一条结果。我们后面联合查询时不用管它,只需要在输入末尾加上注释符,把 LIMIT“吃掉”就行。 - 报错信息直接回显到页面上。这意味着后面的手工注入里,我们能看到很多有用的数据库反馈信息。
到这一步,已经可以确认:这个参数存在字符型注入,并且是通过单引号闭合的。但还不够,我们还需要确认查询结果是几列数据,也就是字段数量。这一步是联合查询的前提,也是 Less 1 的核心考点。
3. Less 1 超详细实战过程
3.1 第一步:判断字段数量
知道存在注入之后,第一步就是确定当前的查询返回了几个字段。最经典的办法是用ORDER BY子句。
ORDER BY在 SQL 里是用来排序的,它后面可以跟列名,也可以跟数字。数字表示“按第几列排序”。如果指定的列数超出了查询结果的总列数,数据库就会报错。比如我们依次尝试:
?id=1' ORDER BY 1 --+ ?id=1' ORDER BY 2 --+ ?id=1' ORDER BY 3 --+ ?id=1' ORDER BY 4 --+前三次页面都正常显示,到了ORDER BY 4时又报错了,报错信息里会出现Unknown column '4' in 'order clause'。这说明当前查询一共只有 3 列。
为什么 1、2、3 都行,4 就不行?因为users表恰好有 3 个字段,ORDER BY最多只能排到 3。这一步的结果决定了后面UNION SELECT里我们得填几个占位符。
这里有个细节值得注意:我用的注释符是--+,不是#,也不是单纯的--。原因后面专门讲,先记住这个写法。
3.2 第二步:构造 UNION SELECT
知道了 3 列,就可以上UNION SELECT了。UNION的作用是把两条查询语句的结果合并在一起,但有个硬性条件:前后两条查询返回的列数必须相等。
所以我们这么写:
?id=1' UNION SELECT 1,2,3 --+页面还是正常显示,但等一等,细看页面中的内容,会发现它多显示了两行,原本一张表格的位置出现了2和3。这说明什么?说明UNION SELECT 1,2,3里的第 2 个和第 3 个字段被直接展示在页面上。
为什么要这么做?因为我们要知道哪些列的数据会回显到前端。如果页面上显示 2 和 3,就代表第 2 列和第 3 列的数据能让我们看到。后面我们把 2 和 3 替换成数据库的函数或字段名,就能把信息读出来。
但这里有个小坑:现在的输出是 1、2、3,因为第一条查询其实也返回了一行数据(就是id=1那条)。为了让页面显示UNION SELECT的这部分,通常把前面的id改成不存在的值,比如-1:
?id=-1' UNION SELECT 1,2,3 --+改成-1后,第一条查询结果为空,页面上就只显示UNION SELECT产生的2和3了,回显更干净。这一步也解释了为什么很多注入教程里喜欢用-1而不是1。
3.3 第三步:获取数据库名与版本信息
拿到显示位之后,就可以利用 MySQL 自带的信息函数了。先把第 2 列替换成database(),第 3 列替换成version(),看数据库名字和版本号:
?id=-1' UNION SELECT 1,database(),version() --+页面上第二块会显示当前数据库名,第三块会显示 MySQL 版本号。我这边执行完得到的结果是security和5.5.53。
这一步的意义比单纯拿个名字重要得多:
database()告诉我们当前所在的数据库是security,后面查表时要在security库下找。version()告诉我们 MySQL 版本,如果版本高于 5.0,就可以用信息字典(information_schema)来查全库的表结构;如果是 4.x 老版本,那只能用暴力猜测表名的方式,难度会大不少。
在 MySQL 5.0 以上版本中,information_schema是一个系统库,里面藏着所有数据库的元数据信息。这是脱库的钥匙。
3.4 第四步:查询所有表名
拿到数据库名之后,就要看这个库里面有哪些表。我们用information_schema.tables来查:
?id=-1' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema='security' --+这里的group_concat()函数可以把多行结果拼成一行字符串,非常实用。不加它的话,如果有多张表,页面上只会显示第一张表的表名,因为每行结果只有一列能回显。
我在本地执行的结果是:
emails,referers,uagents,users一共四张表,重点肯定是users。为什么?直觉上它最容易存用户名和密码,而且前面报错信息里看到的SELECT * FROM users恰恰印证了这一点。
3.5 第五步:查询列名
知道表名之后,下一步查列名。用information_schema.columns:
?id=-1' UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_schema='security' AND table_name='users' --+执行后,页面上显示:
id,username,password很标准的三个字段:id是用户编号,username是用户名,password是密码。这也正好对应ORDER BY那一步判断出来的 3 列结构。
3.6 第六步:脱库拿数据
列名都齐了,剩下就是直接查数据。把username和password都拼在显示位上,用group_concat确保所有行都能看到:
?id=-1' UNION SELECT 1,group_concat(username),group_concat(password) FROM security.users --+页面上哗啦啦列出来一堆用户名和密码,第一关到此就算通关了。
整个过程中,其实我们完全可以在第三步之后直接用select * from users一把梭,把整张表的内容拉出来。但从教学角度看,一步一步拆解表名、列名、数据,会让你对information_schema的使用有更扎实的理解。后面在更复杂的场景里(比如需要绕过某些过滤时),这种拆解能力会派上大用场。
4. 实战中的报错与排查实录
4.1 为什么我用 --+ 而不是 -- 或
这是新手最容易卡住的地方之一。SQL 标准里--是注释符,但它后面必须跟一个空格才能生效。你直接写:
?id=1' --在 MySQL 里是有效的,因为--后面有一个空格。但如果你在浏览器地址栏里输入--加空格,空格会被 URL 编码成%20,或者被某些浏览器吞掉,导致注释不生效。
#号在 MySQL 里也是注释符,而且不需要加空格。但麻烦的是,URL 里的#会被浏览器当成“页面锚点”处理,请求根本不会把#后面的内容发到服务器。所以直接用#也不行,除非把它写成%23。
--+之所以能通杀大多数场景,是因为+在 URL 解码后会变成空格,变相满足了--后必须跟空格的要求。所以在手工注入时,我几乎都用--+。但也别死记这一个,有些 Web 层过滤会对--做拦截,到时候还得灵活换%23或--%20。
4.2 union select 出来还是原页面内容
出现这个现象,通常有两个原因:
一是前面提到的第一条查询有结果。如果?id=1本身有数据,页面优先渲染的是第一条查询的结果,你用UNION追加的数据根本没有在显示位展示。解决方法是把id改成不存在的值,比如-1或者999。
二是列数对不上。UNION SELECT 1,2,3如果跟前面查询的列数不一致,数据库直接报错。这时候回到ORDER BY那一步重新确认列数。
4.3 回显内容被截断
group_concat默认有长度限制(默认group_concat_max_len是 1024),如果数据量非常大,结果会被截断。我们这关只有十几个用户,完全不用担心。但如果后续在数据量大的场景下脱库,可以顺便加上:
?id=-1' UNION SELECT 1,group_concat(username SEPARATOR 0x3c62723e),group_concat(password SEPARATOR 0x3c62723e) FROM security.users --+设置group_concat_max_len的方式是:
?id=-1' UNION SELECT 1,@@group_concat_max_len,3 --+或者启动 MySQL 时加上参数,但手工注入场景下通常只需要对值大一点即可。
4.4 页面不显示任何内容,但不出错
这种情况往往是因为闭合方式判断错了。Less 1 是单引号闭合,如果你用的是数字型注入的思路(不带引号),?id=1 and 1=1这种测试就会显得“没有反应”,因为整个 SQL 变成了:
SELECT * FROM users WHERE id='1 and 1=1' LIMIT 0,1这里1 and 1=1被当成了字符串,查不到数据,页面自然显示不出来。解决办法是回到最开始的引号测试,先把闭合方式试出来。
5. 从 Less 1 延伸出去的注入思路
5.1 字符型与数字型的判断差异
Less 1 是典型的字符型注入,输入的参数被单引号包起来了。在真实环境中,你还会碰到数字型注入,参数没有引号包裹,直接拼进 SQL。判断方法其实很简单:在参数后面加and 1=1和and 1=2,如果页面表现不一样,基本就是数字型;如果毫无变化,大概率是字符型,还得再补一个引号试试。
这背后的逻辑是:数字型注入的参数直接参与运算,and 1=1为真、and 1=2为假,结果自然不同。字符型注入的参数被当成了字符串的一部分,加and 1=1只会让整个字符串查询不到结果。
5.2 拿到数据之后能做什么
很多人打完靶场就结束了,但真实场景里拿到数据之后还有很长的路要走。密码字段在真实系统中通常不是明文,而是哈希值(比如 MD5、SHA1 等)。你还要根据哈希特征判断算法,然后用在线字典或彩虹表去解。Less 1 里的密码是明文,纯粹是为了教学方便。
另外,数据之外的东西往往更有价值:比如数据库版本、当前用户权限、是否支持文件读写、能不能拿到 UDF 或into outfile写入一句话木马。这些在后续的 Less 关卡里都会陆续出现,Less 1 只是起点。
5.3 怎么把这一关的能力用到后续关卡
sqli-labs 的关卡难度是递进的。Less 2 是数字型注入,Less 3 和 Less 4 分别是有单引号括号和双引号括号包裹的注入。你会发现核心手法完全一样,变化的只是“怎么把输入从引号的包裹里逃出来”。Less 5 之后开始进入报错注入、布尔盲注、时间盲注和无回显注入,这些对 SQL 基础的要求会明显提高。
我的建议是:Less 1 这关不要只满足于“跑通”,最好能把手动拼 SQL 的过程重复三到五遍,直到不看笔记也能从ORDER BY一路写到group_concat脱库。肌肉记忆在这种基础关卡里特别重要,后面遇到复杂的 WAF 绕过时,你才能把注意力放在绕过策略上,而不是卡在基础语法上。
6. 总结:Less 1 带给我的实战启发
我自己在带新人或者写教学材料的时候,反复强调一个观点:Less 1 看上去简单,但它教会你的是一整套 SQL 注入的思维方法——每一步都去确认、把自己放在数据库执行的角度去思考“这条 SQL 会变成什么样”。当你习惯了这种思维,再去看后面的 Less 2、Less 3,其实就是在处理“如何把注入载荷构造得能让后台 SQL 按你的意愿执行”这件事。
最后再分享一个小经验:动手敲 SQL 的时候,最好自己开一个 MySQL 命令行,把后台拼接完成的语句手动复制进去执行一遍。比如查到某个 payload 之后,先在命令行里跑一次SELECT * FROM users WHERE id='-1' UNION SELECT 1,2,3 --+',看看真实的输出长什么样,再对比网页上的回显。这样做几次,你对 SQL 语句的敏感度提升得比你想象中快很多。靶场存在的意义不是让你复制几段 payload,而是让你真正理解每个参数背后的数据库逻辑。