从LoveSQL入门SQL注入:万能密码与联合查询全解析

发布时间:2026/9/16 23:30:40
从LoveSQL入门SQL注入:万能密码与联合查询全解析 提到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是密码框里填的字符串。如果你老老实实输入admin和123456这条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 11#假设程序照样执行拼接最终SQL语句变成SELECT * FROM users WHERE username admin or 11# AND password 123456先看admin这部分它把原本的username 这个单引号闭合掉了。接着or 11是一个恒真的条件因为1永远等于1。最后面的#是MySQL的注释符它的作用是把后面所有的内容全部注释掉也就是说 AND password 123456这段代码对数据库来说是不存在的。于是整个语句的逻辑简化成了SELECT * FROM users WHERE username admin or 11username admin可能为假但没关系只要or 11为真整条WHERE条件就为真。数据库会返回users表里的第一条记录通常是admin用户程序一看查询结果不为空就认为登录成功了。这个逻辑我换个方式讲你肯定秒懂假设门卫查人需要“姓名对 AND 工牌对”两个条件同时满足才放行结果你对门卫说“我认识管理员张三或者你直接放我进去”且后半句永远成立。那门卫的条件判断就变成了“认识张三”或者“永远为真”这还拦得住谁2.2 在LoveSQL里实际试一把打开登录页面用户名输入admin or 11#密码随便填比如123456点击登录。如果一切正常页面会跳转到一个新的后台页面而不是停留在登录页报“用户名或密码错误”。我实际打的时候登录后页面会直接打印出当前登录用户的用户名和密码字段。这一步其实就是验证了万能密码确实打通了登录校验。这里有个细节要注意#在不同场景下有不一样的表现。在MySQL里#是完整的行注释符但在浏览器直接提交表单时#有可能会被当作URL片段标识符处理导致传到后台的内容不完整。所以如果你在URL里测试注入最好把#替换成%23这是它的URL编码形式。不过在LoveSQL这个登录框场景里由于是表单POST提交#一般能正常传递但养成用%23的习惯没有坏处。2.3 万能密码的常见变体admin or 11#只是最经典的一种写法实际做题过程中还可能遇到各种变体。比如MySQL里除了#还有一个注释符是--注意两个短横线后面必须跟一个空格或者行尾在URL里不方便打空格时可以用--因为在URL解析时会变成空格。常见的万能密码payload还有这些or11# or 11-- OR aa admin-- union select 1,2,3#核心思路其实都一样闭合掉前面的单引号构造一个恒真条件再注释掉后面原本的校验逻辑。如果你在别的靶场遇到这些变体别觉得眼花万变不离其宗回到SQL拼接的原点去分析就很容易看懂。2.4 万能密码的意义确认注入点打完万能密码之后不要急着高兴。它真正的价值不只是“能登录进去”而是帮你确认了后台存在SQL注入漏洞。如果你往用户名框里填一个单引号页面报出了SQL语法错误那就说明输入内容确实被拼进了SQL语句注入点确定无疑。从CTF做题的角度来说万能密码登录相当于拿到了后台的“入场券”。但题目如果只设置到登录为止那这个题的难度就太低了。LoveSQL的巧妙之处在于登录进去之后的页面还藏着一个带参数的查询入口顺着这个入口才能拿到真正的flag。所以接下来才是重头戏——联合查询注入。3. 联合查询注入一步一步拖库3.1 从登录后台到注入点的转移万能密码登录成功后页面会显示一些基本信息和几个链接。这时候仔细看地址栏和页面里的URL会发现类似check.php?usernameadmin或者welcome.php?id1这样的参数传递。这意味着后台还有一个根据参数查询数据库的功能。比如有个页面会根据你传入的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 1、order by 2、order by 3……直到页面报错为止。ORDER BY是SQL里用来排序的关键字它可以接受数字表示按第几个字段排序。实际操作时我们注入的内容大概是这样的/check.php?usernameadmin order by 1%23 /check.php?usernameadmin order by 2%23 /check.php?usernameadmin order by 3%23 /check.php?usernameadmin order by 4%23当order by 4时页面报错说明当前查询只有3个字段。为什么因为如果原始SQL查询的字段数小于排序数字数据库就会提示“Unknown column”之类的错误。这个操作的本质就是通过排序来测试字段边界就像你拿一把尺子量桌子长度量到超过桌沿的那一格就说明长度到顶了。这里就是我当年卡住的地方。我当时直接在用户名框测order by一直报错后来才发现注入点已经转移到后台页面的URL参数里了登录框那边是POST请求直接在浏览器地址栏操作是无效的。所以一定记得哪里的参数可控、哪里会引发报错哪里才是当前的注入点。3.3 第二步用union select找显示位确定字段数是3个之后接下来做一件事让数据库执行我们自定义的SELECT语句并且把结果打印到页面上。/check.php?usernameadmin union select 1,2,3%23这里有个关键技巧为了让UNION前面的查询结果为空从而让页面显示我们union select出来的数据通常会把可控参数的原始值改成一个数据库中不存在的值。比如把admin改成-1、0或者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为例修改参数后页面会在对应位置显示2和3两个数字说明查询结果的第2个字段和第3个字段是直接输出到页面上的。这就是我们后面放数据的“显示器”已知有3列第2列和第3列回显联合注入的条件完全齐了。3.4 第三步获取数据库名和版本先来点开胃菜把数据库版本和当前数据库名拿出来。MySQL提供了一些内置函数version()返回数据库版本database()返回当前使用的数据库名user()返回当前数据库用户。把第2个字段的位置替换成database()试试/check.php?usernamex 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.schematainformation_schema是MySQL自带的一个“数据库的数据库”它里面记录了这个MySQL实例中所有的库、表、字段等元数据信息。schemata表存的是所有数据库的名字schema_name就是库名字段。group_concat()函数可以把多行结果合并成一行用逗号分隔方便在页面上一次性展示。执行之后页面上会列出这个MySQL里所有的库名包括系统自带的information_schema、mysql、performance_schema以及题目自己创建的geek库等。这个技巧直接回答了“mysql数据库如何通过sql注入获取所有的数据库名”这个问题逻辑就是先确定回显位再通过information_schema.schemata查出所有库然后进入指定库找表。3.5 第四步获取当前数据库的所有表名拿到了库名之后下一步是查这个数据库下面有哪些表。表名的信息存在information_schema.tables表里关键字段是table_name和table_schema。union select 1,group_concat(table_name),3 from information_schema.tables where table_schemageek这里where table_schemageek的意思是只筛选geek这个库下的表。实际做题时建议直接把table_schemadatabase()写进去好处是即使换了一个题、库名变了payload依然通用因为database()会自动取当前库的名字。提交之后页面上会列出库里的所有表名。LoveSQL这道题里通常能看到类似users、flag之类的表名。看到flag表基本就能猜到最终的结果藏在这里面但先别急着激动还是按流程把表结构查出来。3.6 第五步获取指定表的字段名现在我们已经知道目标库和表名了要拿数据还得知道表里有哪些字段。字段信息存在information_schema.columns表里关键字段是column_name筛选条件是库名和表名都匹配union select 1,group_concat(column_name),3 from information_schema.columns where table_schemageek and table_nameflag执行之后页面上会打印出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语法错误关闭了引号报错那就是字符型注入需要想办法闭合前引号。如果页面正常显示或者提示“参数错误”可能是数字型注入可以直接拼接数字和运算符。字符型注入的判断方式也有区别。比如id1报错id1 --恢复正常说明后台的语句是where id$id这种形式单引号闭合后需要注释符处理尾部。数字型注入则更简单直接id1 and 11页面正常id1 and 12页面异常就能确认注入存在。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参数就强制要求是整数不是整数直接拒绝白名单是只允许特定值通过比如排序字段只允许传id、time、price这几个字段名其他一律拦截。数据库权限的最小化也很重要。即使发生注入攻击者能用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这道题最值得做的恰恰是关掉所有辅助工具老老实实用手工把整个流程走一遍。等你把联合查询的六个步骤练顺了再回头看任何一道注入题你都会有一种“万变不离其宗”的底气。