从LoveSQL入门SQL注入:万能密码与联合查询全解析
2026/9/16 23:30:22 网站建设 项目流程

提到SQL注入,很多刚入门CTF的朋友第一个想到的题目应该就是它——极客大挑战2019的LoveSQL。这道题在Web方向几乎算得上是“新手村必刷任务”,原因很简单:它把一个登录框做成了完整的SQL注入教学现场,从万能密码登录到联合查询拖库,一套流程走下来,你对注入的理解会比看十篇科普文章都深刻。

这道题适合谁来练?特别适合刚接触Web安全、还没系统学过注入原理的人。它没有花里胡哨的过滤,没有二次注入那种绕来绕去的脑洞,连报错信息都给你看得明明白白。你只要能理解“用户在输入框里填的东西,最后是怎么拼进SQL语句的”,就能把这题从头到尾拿下。而且它不仅能让你拿到flag,还会让你把SQL注入里最核心的几板斧全部练一遍:万能密码绕过、字段数判断、union联合查询、information_schema元数据库的用法。

我当年第一次打这道题的时候,其实已经看过不少注入相关的文章,但真到自己上手,还是在order by那一步卡了半天。后来把整条链路打通之后才发现,这题设计得是真的友好——它把每个阶段该看到的现象都安排得明明白白,你只要顺着现象往下推,就能一步一步把数据库里的家底全翻出来。下面我把完整的解题思路和背后的原理一起拆开来讲,争取让没打过的新手看完也能自己复现一遍。

1. 先搞清楚LoveSQL到底考什么

1.1 题目场景与基本信息

打开题目环境,映入眼帘的就是一个典型的登录页面,用户名输入框、密码输入框、登录按钮,朴实无华。URL路径通常是/sqli/这样的形式,有点经验的师傅一眼就能猜到是SQL注入的靶场。

这题的定位是“最基础的SQL注入”,但它其实包含了两层递进的任务:第一层是用万能密码绕过登录校验,进入后台页面;第二层是进入后台后,发现页面里藏着另一个注入点,通过这个注入点把数据库里的数据全部拖出来。很多新手打到万能密码登录成功就以为结束了,结果发现没有flag,其实就是漏了后面这一步。

1.2 SQL注入的本质:一句话讲透

SQL注入之所以能成立,核心原因只有一个:程序把用户输入的内容,直接拼接进了SQL语句,并且把拼接后的结果当作代码来执行

举个例子,后台登录的逻辑代码可能是这样的:

$sql = "SELECT * FROM users WHERE username = '$user' AND password = '$pass'"; $result = mysqli_query($conn, $sql);

这里$user是你在用户名框里填的字符串,$pass是密码框里填的字符串。如果你老老实实输入admin123456,这条SQL就变成:

SELECT * FROM users WHERE username = 'admin' AND password = '123456'

数据库拿到这条语句后,会去查有没有同时满足用户名和密码都匹配的记录,查到了就允许登录,查不到就拒绝。问题在于,$user$pass完全由用户控制,如果我在用户名里填的不是一个正常的字符串,而是一段精心构造的SQL片段,那拼接出来的整条语句就会变得不可描述。

你可以把这条SQL想象成一个填空题:程序给你留好了两个空,一个填用户名,一个填密码。但它没有检查你填的内容是否真的是“用户名”和“密码”,而是原封不动地塞进句子里。那我直接在空里写一段“让句子永远为真”的内容,整个校验逻辑就形同虚设了。

1.3 为什么说这道题是“最基础”的

基础体现在三个地方。第一,没有过滤。不管你在参数里传单引号、双引号、注释符还是空格,后台统统照单全收,不会有WAF拦你,也没有代码层面过滤危险字符。第二,错误信息完整。如果你在参数后面加个单引号导致SQL语法错误,页面会直接把数据库的报错信息打出来,这就等于把注入点的存在明明白白告诉了你。第三,有显式回显。联合查询的结果会直接展示在页面上,不用像盲注那样一个字一个字去猜。

这三条叠加在一起,就意味着你可以用最“教科书”的方式,把SQL注入从发现到利用的完整流程走一遍。等你把这道题吃透了,再去看那些加了过滤、加了waf、不在页面显示数据的靶场题,你会清楚它们到底“卡”在哪一步,以及为什么需要那些绕过技巧。

2. 万能密码登录:原理与实操

2.1 万能密码的SQL原理

万能密码为什么能“万能”?关键在于单引号的闭合和注释符的利用。我们往用户名框里输入这样一段:

admin' or 1=1#

假设程序照样执行拼接,最终SQL语句变成:

SELECT * FROM users WHERE username = 'admin' or 1=1#' AND password = '123456'

先看admin'这部分,它把原本的username = '这个单引号闭合掉了。接着or 1=1是一个恒真的条件,因为1永远等于1。最后面的#是MySQL的注释符,它的作用是把后面所有的内容全部注释掉,也就是说' AND password = '123456'这段代码对数据库来说是不存在的。

于是整个语句的逻辑简化成了:

SELECT * FROM users WHERE username = 'admin' or 1=1

username = 'admin'可能为假,但没关系,只要or 1=1为真,整条WHERE条件就为真。数据库会返回users表里的第一条记录(通常是admin用户),程序一看查询结果不为空,就认为登录成功了。

这个逻辑我换个方式讲你肯定秒懂:假设门卫查人需要“姓名对 AND 工牌对”两个条件同时满足才放行,结果你对门卫说“我认识管理员张三,或者你直接放我进去”,且后半句永远成立。那门卫的条件判断就变成了“认识张三”或者“永远为真”,这还拦得住谁?

2.2 在LoveSQL里实际试一把

打开登录页面,用户名输入:

admin' or 1=1#

密码随便填,比如123456,点击登录。

如果一切正常,页面会跳转到一个新的后台页面,而不是停留在登录页报“用户名或密码错误”。我实际打的时候,登录后页面会直接打印出当前登录用户的用户名和密码字段。这一步其实就是验证了万能密码确实打通了登录校验。

这里有个细节要注意:#在不同场景下有不一样的表现。在MySQL里,#是完整的行注释符,但在浏览器直接提交表单时,#有可能会被当作URL片段标识符处理,导致传到后台的内容不完整。所以如果你在URL里测试注入,最好把#替换成%23,这是它的URL编码形式。不过在LoveSQL这个登录框场景里,由于是表单POST提交,#一般能正常传递,但养成用%23的习惯没有坏处。

2.3 万能密码的常见变体

admin' or 1=1#只是最经典的一种写法,实际做题过程中还可能遇到各种变体。比如MySQL里除了#,还有一个注释符是--,注意两个短横线后面必须跟一个空格(或者行尾),在URL里不方便打空格时,可以用--+,因为+在URL解析时会变成空格。

常见的万能密码payload还有这些:

'or'1'='1'# ' or 1=1--+ ' OR 'a'='a admin'-- ' union select 1,2,3#

核心思路其实都一样:闭合掉前面的单引号,构造一个恒真条件,再注释掉后面原本的校验逻辑

如果你在别的靶场遇到这些变体,别觉得眼花,万变不离其宗,回到SQL拼接的原点去分析就很容易看懂。

2.4 万能密码的意义:确认注入点

打完万能密码之后,不要急着高兴。它真正的价值不只是“能登录进去”,而是帮你确认了后台存在SQL注入漏洞。如果你往用户名框里填一个单引号,页面报出了SQL语法错误,那就说明输入内容确实被拼进了SQL语句,注入点确定无疑。

从CTF做题的角度来说,万能密码登录相当于拿到了后台的“入场券”。但题目如果只设置到登录为止,那这个题的难度就太低了。LoveSQL的巧妙之处在于,登录进去之后的页面还藏着一个带参数的查询入口,顺着这个入口才能拿到真正的flag。所以接下来才是重头戏——联合查询注入。

3. 联合查询注入:一步一步拖库

3.1 从登录后台到注入点的转移

万能密码登录成功后,页面会显示一些基本信息和几个链接。这时候仔细看地址栏和页面里的URL,会发现类似check.php?username=admin或者welcome.php?id=1这样的参数传递。

这意味着后台还有一个根据参数查询数据库的功能。比如有个页面会根据你传入的id去数据库里查对应的用户信息并展示出来。这种“参数直出数据”的场景,是联合查询注入最典型的利用现场。联合查询的核心武器是SQL里的UNION SELECT,它能把两条SELECT语句的结果合并成一张表返回。

要使用UNION,有一个硬性条件:前后两条SELECT语句查询的字段数量必须一致。什么意思呢?页面原本的查询语句可能是SELECT username, password FROM users WHERE id = 1,查的是2个字段,那我UNION SELECT后面就必须跟2个字段,不然数据库直接报错。而通常源码里到底查了几个字段,我们看不到,所以第一步就是“数”出这个字段数。

3.2 第一步:用order by判断字段数

方法很简单,在参数后面依次尝试order by 1order by 2order by 3……直到页面报错为止。ORDER BY是SQL里用来排序的关键字,它可以接受数字,表示按第几个字段排序。

实际操作时,我们注入的内容大概是这样的:

/check.php?username=admin' order by 1%23 /check.php?username=admin' order by 2%23 /check.php?username=admin' order by 3%23 /check.php?username=admin' order by 4%23

order by 4时页面报错,说明当前查询只有3个字段。为什么?因为如果原始SQL查询的字段数小于排序数字,数据库就会提示“Unknown column”之类的错误。这个操作的本质就是通过排序来测试字段边界,就像你拿一把尺子量桌子长度,量到超过桌沿的那一格就说明长度到顶了。

这里就是我当年卡住的地方。我当时直接在用户名框测order by,一直报错,后来才发现注入点已经转移到后台页面的URL参数里了,登录框那边是POST请求,直接在浏览器地址栏操作是无效的。所以一定记得:哪里的参数可控、哪里会引发报错,哪里才是当前的注入点。

3.3 第二步:用union select找显示位

确定字段数是3个之后,接下来做一件事:让数据库执行我们自定义的SELECT语句,并且把结果打印到页面上。

/check.php?username=admin' union select 1,2,3%23

这里有个关键技巧:为了让UNION前面的查询结果为空,从而让页面显示我们union select出来的数据,通常会把可控参数的原始值改成一个数据库中不存在的值。比如把admin改成-10或者x,这样前面的WHERE username = 'x'查不到数据,返回空集合,后面的union select 1,2,3就成了唯一的结果。

如果你用admin' union select 1,2,3%23,页面可能显示的还是admin原本的数据,因为两条结果合并了,而且多余的UNION结果不一定被显示。但改成x' union select 1,2,3%23,页面上就能明确看到哪些数字位置被回显了。

以LoveSQL为例,修改参数后页面会在对应位置显示23两个数字,说明查询结果的第2个字段和第3个字段是直接输出到页面上的。这就是我们后面放数据的“显示器”,已知有3列,第2列和第3列回显,联合注入的条件完全齐了。

3.4 第三步:获取数据库名和版本

先来点开胃菜,把数据库版本和当前数据库名拿出来。MySQL提供了一些内置函数,version()返回数据库版本,database()返回当前使用的数据库名,user()返回当前数据库用户。

把第2个字段的位置替换成database()试试:

/check.php?username=x' union select 1,database(),3%23

页面上原来显示数字2的位置,变成了一个数据库名,比如geek。这一步非常关键,因为后面查表名的时候,table_schema字段就用得上这个库名。

获取所有数据库名是同一个套路,只是把database()函数换成查询information_schema库里的数据。这条语句建议背下来,是所有MySQL联合注入题目的通用招式:

union select 1,group_concat(schema_name),3 from information_schema.schemata

information_schema是MySQL自带的一个“数据库的数据库”,它里面记录了这个MySQL实例中所有的库、表、字段等元数据信息。schemata表存的是所有数据库的名字,schema_name就是库名字段。group_concat()函数可以把多行结果合并成一行,用逗号分隔,方便在页面上一次性展示。

执行之后,页面上会列出这个MySQL里所有的库名,包括系统自带的information_schemamysqlperformance_schema,以及题目自己创建的geek库等。这个技巧直接回答了“mysql数据库如何通过sql注入获取所有的数据库名”这个问题,逻辑就是:先确定回显位,再通过information_schema.schemata查出所有库,然后进入指定库找表。

3.5 第四步:获取当前数据库的所有表名

拿到了库名之后,下一步是查这个数据库下面有哪些表。表名的信息存在information_schema.tables表里,关键字段是table_nametable_schema

union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='geek'

这里where table_schema='geek'的意思是只筛选geek这个库下的表。实际做题时建议直接把table_schema=database()写进去,好处是即使换了一个题、库名变了,payload依然通用,因为database()会自动取当前库的名字。

提交之后,页面上会列出库里的所有表名。LoveSQL这道题里通常能看到类似usersflag之类的表名。看到flag表基本就能猜到最终的结果藏在这里面,但先别急着激动,还是按流程把表结构查出来。

3.6 第五步:获取指定表的字段名

现在我们已经知道目标库和表名了,要拿数据,还得知道表里有哪些字段。字段信息存在information_schema.columns表里,关键字段是column_name,筛选条件是库名和表名都匹配:

union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='geek' and table_name='flag'

执行之后,页面上会打印出flag表所有的字段名。我自己打的时候,这一步最兴奋,因为眼看着离最终的目标越来越近。查到字段名之后,剩下的就是最直接的“select”操作了。

3.7 第六步:拖出数据,拿到flag

字段名都清楚了,直接查数据:

union select 1,group_concat(flag),3 from geek.flag

这里我把库名和表名用点连在一起写成geek.flag,相当于先指定库再指定表。也可以写成:

union select 1,group_concat(flag),3 from flag

只要当前默认数据库就是geek,直接写表名也行。提交之后,页面上会直接打印出flag。如果你查的是users表,通常还会看到用户名和密码字段,密码往往是一串MD5值,拿到cmd5之类的地方在线解密就能看到明文,这个环节也挺有成就感的。

4. 从LoveSQL延伸:注入点判断与绕过思路

4.1 快速判断注入类型的方法

LoveSQL是字符型注入,因为参数值外层有单引号包裹。但并不是所有题目都这么直接,快速判断是数字型还是字符型是基本功。

最常用的方法就是“加单引号看反应”。在参数后面加一个单引号:

  • 如果页面上出现SQL语法错误,关闭了引号报错,那就是字符型注入,需要想办法闭合前引号。
  • 如果页面正常显示或者提示“参数错误”,可能是数字型注入,可以直接拼接数字和运算符。

字符型注入的判断方式也有区别。比如id=1'报错,id=1' --+恢复正常,说明后台的语句是where id='$id'这种形式,单引号闭合后需要注释符处理尾部。数字型注入则更简单,直接id=1 and 1=1页面正常,id=1 and 1=2页面异常,就能确认注入存在。

4.2 过滤字符后的绕过思路

热搜词里提到的“sql过滤字符后手工注入漏洞测试(第2题)”,就是在LoveSQL这种入门题的基础上,加了常见的过滤规则。比如后台把空格、单引号、关键字给过滤掉了,这时候怎么办?

先说空格被过滤的情况。SQL里很多地方可以用注释符代替空格,比如用/**/来分隔关键字:

union/**/select/**/1,2,3

这种写法对MySQL来说是合法的,关键字之间不一定非得是空格,注释符一样能起到分隔作用。

如果关键字本身被过滤,比如select不让出现,那可以尝试大小写混写SeLeCt(前提是过滤规则没做大小写归一化),或者双写绕过seleselectct——过滤程序通常会把匹配到的关键词删掉,删掉一次之后剩下的字符刚好拼成完整的select,原理有点像“把叠起来的A4纸撕掉一层,底下那层还在”。

至于单引号被过滤的情况,在数字型注入里影响不大,因为数字本来就不需要引号;但如果表名字段值需要引号,就可以考虑用十六进制编码来替代,比如把flag转成十六进制的0x666c6167,这样就能绕开引号过滤。

这些技巧在ctfshow的web入门sql注入系列、pikachu靶场、dvwa的SQL Injection模块里都有对应的练习场景。我建议你打LoveSQL掌握基础流程之后,去dvwa把SQL Injection的low等级做一遍,你会发现几乎一模一样的套路;再往medium、high等级做,就能慢慢体会到过滤和防护手段是怎么一步步把注入难度抬上去的。

4.3 为什么其他靶场同样值得刷

LoveSQL属于“一题串一条线”的类型,适合入门;但如果想彻底巩固SQL注入的手感,靶场的系统性练习不可少。dvwa的SQL Injection模块分三个等级,low等级无过滤,medium等级用了mysqli_real_escape_string转义特殊字符,high等级改用了预编译查询,层层递进刚好展示攻击与防御的对抗过程。pikachu靶场则专门设计了“字符型注入”“搜索型注入”“XX型注入”等细分类别,每一类都有独立的示例环境。ctfshow的web入门sql注入系列更是把注入题型做了极其细致的拆分,从最简单的联合注入一路做到堆叠注入、报错注入、布尔盲注、时间盲注。

我个人建议的练习路径是:LoveSQL打完,建立完整的联合注入思路;然后去dvwa把low和medium两个等级都打通,感受字符型和数字型的差异;接着去pikachu把各个注入分类过一遍,最后再回来打ctfshow的sql注入专项。走完这一圈,市面上绝大多数注入题你都能看出个大概思路。

5. 防止SQL注入:从攻击视角看防御

5.1 参数化查询为什么能防注入

打完这些靶场题,很多人会问同一个问题:现实中还有没有SQL注入?答案是越来越少,但仍然存在。根本原因是现代开发框架普遍采用了参数化查询(Prepared Statement)来处理SQL。

参数化查询的理念是:SQL语句的“骨架”和用户输入的数据彻底分离。数据库先解析语句结构,确定“这是一条查询username和password的语句”,然后再把用户输入当作纯数据填充进去。这样一来,用户输入内容再像SQL语句,也不会被当作代码执行。打个比方,以前是让你直接填一句话进答题卡,填的内容会被当作正确答案的一部分;现在改成先写明“这句话里的空白处填写姓名”,你哪怕往空白处写一段SQL语句,它也只是被当成一个字符串存起来而已。

5.2 开发侧的其他防护手段

除了参数化查询,常见的防御手段还有输入校验和白名单机制。输入校验是在后端检查用户输入是否符合预期格式,比如id参数就强制要求是整数,不是整数直接拒绝;白名单是只允许特定值通过,比如排序字段只允许传idtimeprice这几个字段名,其他一律拦截。

数据库权限的最小化也很重要。即使发生注入,攻击者能用union select查数据,但如果连接数据库的账号只有查询权限而没有修改权限,打击面就会小很多。有些系统设计上会专门拆分“读账号”和“写账号”,防止一处被攻破导致整库沦陷。

这些防御手段在CTF题目里也经常作为背景设定出现。比如有的题你怎么注入都没反应,可能就是后台用了参数化查询;有的题过滤了空格说明存在黑名单逻辑。理解了防御手段,你回头再看攻击payload会更有感觉——你知道你在跟什么样的防御机制对抗,以及为什么某些绕过手法能够生效。

5.3 CTF与真实场景的差异

说句实在话,CTF里的SQL注入是“被洗干净”的经典重现,它把漏洞场景抽象成了最适合教学的形式。现实中你要遇到的SQL注入,往往是藏在复杂的业务逻辑里:可能有waf在前面挡着,可能有编码问题导致注入点藏在二次解码之后,可能遇到的是NoSQL注入,还可能需要绕过各种JSON解析的限制。

但不管场景多复杂,核心能力都是相通的——你能不能从一段SQL语句的拼接逻辑中找出可控的变量,能不能构造出不破坏语法的注入内容。这个能力,恰恰就是从LoveSQL这种“最基础的SQL注入”开始练起来的。这也是我把这道题反复推荐给新人的原因,它像是一座桥,一头连着完全不懂注入的小白,另一头通向更复杂的Web安全世界。

6. 常见问题与排查技巧实录

6.1 为什么#注释符没有生效

这是新手最容易踩的坑。在URL里直接传#时,浏览器可能把它当成了页面锚点的开始,导致后面的参数内容根本不会发到服务器。所以URL注入场景下,#一定要写成%23。另外,如果你写的是--注释,注意--后面必须有一个空格,URL里不方便打空格就用--+,加号在URL解码后会变成空格。

判断注释是否生效有个简单方法:用order by测试时,如果加了注释符还是报错,先把注释符去掉再试一次。如果去掉报错、加上还报错,说明注释符写法有问题;如果去掉不报错,那说明原本就没闭合成功。

6.2 为什么union select后面必须跟够字段数

UNION操作的规则是前后两个SELECT查询的列数必须一致,否则MySQL会直接报“The used SELECT statements have a different number of columns”错误。所以先根据order by的报错边界确认字段数,再决定union select后面写几个值。如果字段数多,多出来的位置直接用数字占位就行,完全没有影响。

6.3 为什么group_concat的内容显示不全

有时候表里的数据很多,group_concat把所有结果拼成一大串字符,页面显示时可能被截断,或者因为长度限制显示不全。这时候有两个办法:一是用limit逐条查看具体数据,比如limit 0,1看第一条、limit 1,1看第二条;二是在payload里用substr()或者left()函数对结果做截取,分段查看。CTF题目一般数据量不大,group_concat基本够用,但养成用limit的习惯对后面打盲注大有帮助。

6.4 为什么密码字段是密文

很多题目在users表里存的是MD5值,这是为了模拟真实环境下的密码存储方式。联合注入查到密码后,把密文扔到cmd5这类在线解密平台跑一下,通常就能得到明文。不过有的题目故意设置了高强度密码或者加盐哈希,在线解密平台跑不出来,这时候你就需要关注题目本身是不是还有别的数据表——比如专门有个flag表,那就不用纠结密码密文了,直接去查flag表就行。

6.5 注入点到底选哪个参数

登录页有用户名和密码两个参数,后台页面还有URL参数,到底注入哪一个?核心判断标准是:哪个参数会把内容拼进SQL语句且返回结果可观测,就注入哪个。LoveSQL里,用户名参数可以用来做万能密码登录,但联合注入更方便的是后台页面的URL参数,因为它直接返回查询结果。遇到不确定的题目,可以把每个参数都试着加单引号看反应,报错最明显或者回显最直接的那个,通常就是正主。

最后分享一个我个人的心得:打SQL注入题,千万别急着上sqlmap。工具确实快,但如果你连字段数都不会手工判断,连报错信息都看不明白,那工具跑出来的结果对你来说只是一个“答案”,而不是一种“能力”。LoveSQL这道题最值得做的,恰恰是关掉所有辅助工具,老老实实用手工把整个流程走一遍。等你把联合查询的六个步骤练顺了,再回头看任何一道注入题,你都会有一种“万变不离其宗”的底气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询