极客大挑战 LoveSQL 零基础SQL注入通关全解析

极客大挑战 LoveSQL 零基础SQL注入通关全解析 很多刚接触CTF的朋友拿到[极客大挑战 2019]LoveSQL这道题第一反应往往是盯住万能密码这四个字试了一下admin or 11#没进去就懵了转头去问工具结果连注入点在哪儿都没搞清楚。这道题在各大CTF平台的入门推荐里出镜率很高标签是最基础的SQL注入但我觉得它真正适合入门的原因不是因为它简单是因为它把SQL注入最常见的两条路——登录绕过和联合查询——完整串了一遍。这次我把整个过程拆开讲从万能密码的SQL语义到order by测字段、union select回显、information_schema爆库每一步都聊清楚为什么这么做适合刚学完SQL基础语法、准备上手第一个SQL注入实战题的人。1. 先把题目环境和工具认知对齐LoveSQL到底考什么1.1 题面信息与考点定位先看清楚这道题的身份信息。极客大挑战是2019年举办的一场CTF赛事LoveSQL是其中的Web方向题目。题面就是一个极简的登录表单看起来人畜无害。它考的核心只有一个SQL注入而且是以最直白的方式呈现——没有WAF、没有过滤、没有字符替换所有输入都会原封不动地拼进SQL语句里。这对入门者来说是最友好的状态。很多讲SQL注入的文章一上来就是布尔盲注、时间盲注、二次注入、宽字节注入新手看完直接劝退。LoveSQL的价值恰恰在于它把SQL注入最基础、最高频的两种利用形态摆了出来登录场景下用万能密码绕过身份校验登录后通过查询接口用联合查询拖库这两个场景对应了SQL注入在实际业务里最常见的危害越权登录和数据泄露。把这道题彻底吃透你对SQL注入到底是什么、为什么能造成危害会有一个非常直观的体感。1.2 做这道题需要准备什么先说环境。这道题在BUUCTF等平台上可以直接在线作答目标是一个远程靶标不需要本地搭环境。启动之后你会拿到一个形如node4.buuoj.cn:xxxxx的地址端口是随机分配的。做题要准备的东西不多一个浏览器但最好不是裸浏览器。建议打开开发者工具切到Network面板随时观察请求的完整内容。很多注入问题本质上是请求没发对看网络面板一眼就明白了。一个请求改包工具Burp Suite Community版就行。某些情况下浏览器地址栏会“帮你”处理URL特殊字符导致注入语句变了样这时候用Burp直接构造原始HTTP请求最干净。一个SQL基础速查表。不需要背但information_schema的用法、group_concat的作用需要随时能查到。工具这块我要多说一句第一遍做这道题强烈建议完全手工作业不要开 sqlmap。手工注入的过程就是理解SQL注入原理的过程你亲手敲一条union select看着页面回显出数据和工具跑出结果学习效果差异巨大。等你手工走通一遍再用sqlmap验证一次那时候工具只是你的辅助而不是你的拐杖。这道题数据量很小手工注入也就是十几条语句的事。另外做题的时候把请求历史点开看一看。登录页的POST请求、登录成功后的302跳转、查询页的GET请求每个都值得点开看一遍。这不是浪费时间这是让你知道注入点到底在哪个参数上页面背后到底发生了什么。2. 万能密码登录一句话绕过背后的SQL语义2.1 登录逻辑在代码里的真实形态先想一个问题一个登录功能服务端拿到用户名和密码之后是怎么验证的最常见的写法长这样以PHP为例$sql SELECT * FROM users WHERE username . $_POST[username] . AND password . $_POST[password] . ; $result mysql_query($sql); if (mysql_num_rows($result) 0) { // 登录成功 } else { // 登录失败 }注意看这段代码的关键点用户名和密码不是作为参数传进查询的而是被直接拼进了SQL字符串。如果$_POST[username]是普通的字符串 admin拼出来的SQL是SELECT * FROM users WHERE usernameadmin AND password123456这条语句的意思是从 users 表里找同时满足 username 等于 admin、password 等于 123456 的记录。找到就说明账号密码都对登录成功。这是开发者预期中的逻辑。但如果 username 输入的不是普通字符串而是一段精心构造的SQL片段那么拼出来的SQL语句就会彻底变味。这就是SQL注入真正的起点开发者的预期输入是数据攻击者输入的却是代码而程序没有区分这两者。2.2 为什么 or 11 能骗过密码校验万能密码最经典的形态是用户名admin or 11# 密码随便填假设密码填 123那么拼接出来的SQL语句变成SELECT * FROM users WHERE usernameadmin or 11# AND password123在MySQL里#是注释符它后面的内容会被当成注释不参与执行。所以数据库实际执行的语句是SELECT * FROM users WHERE usernameadmin or 11这条语句的语义是查询 users 表中满足 usernameadmin 这个条件或者满足 11 这个条件的所有记录。11 是一个永真式任何情况下都为真。整条 WHERE 条件恒为真所以这条查询会返回 users 表中的所有记录。此时再看登录判断逻辑if (mysql_num_rows($result) 0)查询结果行数大于0就登录成功。users 表里哪怕只有一条记录结果行数也大于0于是登录直接被放行。这里要理解一个关键点万能密码不是“猜中了密码”而是改变了整个查询的语义。它让条件判断从“用户名和密码是否正确”变成了“是否存在任何用户”。一旦查询语句的语义被改变后续判断逻辑就全部失去了意义。你也可以说万能密码的本质是给WHERE条件塞了一个恒真的OR分支。2.3 常见万能密码变形与这道题的实际绕过在实际做题中admin or 11#已经能覆盖大部分场景。但不同题目的SQL写法有细微差别所以万能密码也有各种变体载荷拼接后的关键SQL片段效果admin or 11#WHERE usernameadmin or 11#恒真绕过 or 11#WHERE username or 11#恒真绕过admin or 11WHERE usernameadmin or 11恒真绕过admin-- -WHERE usernameadmin-- -注释掉后半段admin/*WHERE usernameadmin/*块注释截断关于这几种变体可以这样记核心思路只有两个方向一是制造恒真条件让WHERE整体为真二是把密码条件注释掉让它失去约束。or 11这类写法是在单引号闭合上做了调整适合原始SQL中密码字段不在末尾的场景--注释则适合密码条件在后面、注释掉之后直接绕过的情况。但 LoveSQL 这道题有个实际细节需要注意直接用admin or 11#登录你会看到登录成功但跳转后的页面并不是直接给你flag而是一个带输入框的查询页。这说明万能密码登录只是第一步它帮你进入了登录后的系统真正的flag藏在数据库里还要通过查询接口进一步提取。我第一次做这题时犯过一个低级错误在URL里直接提交admin or 11#结果发现#后面的内容根本没发到服务端。原因是#在URL中代表锚点标识符浏览器根本不会把它作为请求内容传输。解决办法是把#做URL编码变成%23或者直接用Burp Suite改包发送。这是很多新手卡在第一步的直接原因也是万能密码这个知识点里最容易被忽略的实操细节。3. 登录之后的第二战场联合查询注入的完整链路3.1 登录成功后的页面与新的输入点万能密码登录成功后页面变成一个带输入框的查询页面。输入数字id会返回对应用户信息。你输入1回显id为1的用户输入2回显id为2的用户。这说明页面把查询结果直接渲染出来了——有回显适合联合查询。到这里先顺手做一步测试输入1看看反应。如果页面出现SQL语法报错类似You have an error in your SQL syntax那就可以确认这是一个字符型注入点单引号进来了而且没有被转义。LoveSQL这里会直接报错SQL注入的入口坐实。多说一句为什么不先测数字型注入因为这里是输入了一个数字id没错但我们要看的是后台SQL到底是把它当数字还是当字符串拼接。1报错说明后台SQL很可能是WHERE id1这种带引号的写法单引号被当成了字符串闭合符多出来的一个引号导致语法错误。这种题就是字符型注入后面所有payload都要带引号闭合。3.2 用 order by 试探字段数量确认注入点之后第二步是确定查询语句的字段数量。方法是逐步增大order by后面的数字看在哪一步开始报错1 order by 1# 1 order by 2# 1 order by 3# 1 order by 4#order by在SQL中是用来排序的order by N表示按第N列排序。如果查询结果里没有第N列数据库就会报错。所以当你看到order by 3正常回显order by 4开始报错时就能断定这条查询语句的字段数是3。这里必须理解背后的逻辑联合查询注入中union select后面的字段数必须和原查询一致否则MySQL会直接报错。所以字段数是联合查询的基准。很多新手上来就union select 1,2,3根本不关心字段数报错了也不知道为什么。用order by从1开始递增试探是最稳的方法不要贪快跳着试一次加1看到报错临界点就停。3.3 union select 联合查询与回显位置确认知道字段数是3之后就可以上联合查询了。但有一个细节union会把原查询的结果和后面查询的结果合并展示如果原查询本身有结果展示的时候可能优先显示原查询的内容干扰你观察注入结果。所以要先构造一个让原查询结果为空的id值把舞台让给union部分。最常用的做法是传负数-1 union select 1,2,3%23数据库里通常没有负数id原查询查不到数据此时union select 1,2,3的结果就能直接显示在页面上。如果页面回显出 1、2、3 三个数字说明三个字段都在页面上有回显位置后面可以随便挑一个位置放查询函数。如果只显示部分数字比如只显示2和3说明有一部分字段虽然在SQL查询里但没有被输出到页面。这种情况只能挑有回显的那一列来注入。LoveSQL比较厚道三个字段都有回显。这里我要特别强调%23的问题。实际提交时语句里的#必须URL编码成%23否则请求发出去是不完整的。如果你发现-1 union select 1,2,3#点出去没反应先怀疑是不是#被浏览器吃了。另一个注释写法是----后面必须跟一个空格在URL里写成--加号相当于空格。这两个注释方式在MySQL里都可用但#更简单建议固定用%23。4. 从库名到表名到字段名手工注入的核心语句拆解4.1 获取当前数据库名回显点确定之后第一步是获取当前使用的数据库名。把回显数字3的位置替换成database()函数-1 union select 1,2,database()%23页面第三个位置会输出geek。这就是当前数据库的名字。很多人会问为什么第一步必须先拿数据库名因为在MySQL里表名和字段名都是挂在具体数据库下面的不指定数据库去查information_schema结果会混入系统自带的一大堆库表信息干扰判断。先拿到geek这个库名后续查表名和字段名时就能精确限定范围。database()是MySQL自带函数返回当前连接的数据库名。CTF题目里95%的注入点都能通过它直接暴露库名。这一条语句也是所有联合查询注入的第一块拼图。4.2 利用 information_schema 获取表名接下来获取数据库中所有的表名。这里要用到MySQL自带的系统信息库information_schema-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schemageek%23这条语句的作用是从information_schema.tables表中查找数据库geek下面所有表名并用group_concat把多行结果拼接成一行输出。回显结果是l0ve1ysq1这个表名一看就是出题人改过的故意把 love 写成 l0vesq1 代替 sql。抄这个表名的时候要格外小心中间是数字0不是字母o大小写也不能错否则SQL语法没问题但查不到数据排查起来很恼火。下面解释一下原理。information_schema是MySQL内置的元数据库它保存了整个MySQL实例的元数据信息。其中tables表记录了所有数据库的表名columns表记录了所有表的字段名。只要注入点所在的数据库账号对这些系统表有读权限——在CTF靶场里通常都有——攻击者就能通过查询这两个表把整个数据库结构一点点摸出来。group_concat是一个聚合函数能把多行结果合并成一行用逗号分隔。在SQL注入里它非常实用因为很多页面的回显位置只有一个单独查一行行的表名会非常麻烦用group_concat可以一次性在唯一回显点上看到全部结果。4.3 获取字段名与最终数据已知表名是l0ve1ysq1接下来查这张表里的所有字段名-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_namel0ve1ysq1%23回显结果是id,username,password到这步整张表的结构已经清楚了三个字段分别是id、username、password。最后一步就是查数据。把username和password字段拼出来-1 union select 1,group_concat(username),group_concat(password) from l0ve1ysq1%23回显的username列会出现 admin、cl4y 这样的用户名password列会出现一长串内容仔细看那一串里就藏着flag。这里我要补充一个非常容易踩的坑很多人在这一步只盯着username列看或者只回显了第一个字段的值就觉得没数据了。实际这道题的flag就在password字段里。出题人把flag直接放在了用户表的password字段所以group_concat(password)才会输出那串看起来不像密码的内容。做题的时候所有字段都要查一遍尤其是名字看着像敏感字段的。另一个问题是内容显示不全。group_concat把多行拼接后如果结果太长页面上可能显示不完整flag末尾就看不见了。解决办法是缩小查询范围一次只查一个字段或者用limit分批查-1 union select 1,2,password from l0ve1ysq1 limit 1%23如果还嫌长可以配合substr函数按字符截取-1 union select 1,2,substr(password,1,20) from l0ve1ysq1 limit 1%23这道题的flag不算太长一般能完整显示但分段查询这种思路在高难度题目里几乎是必备技能。遇到长内容先用length()测总长度再截取分段看比抱着页面刷新强得多。5. 站在防御视角看这道题漏洞根因和同类问题的排查思路5.1 漏洞根因拼接SQL与参数化查询很多人做完这道题就急着做下一道但我一直觉得这道题最大的价值在于它把SQL注入的成因展示得非常干净——服务端把用户输入直接拼进了SQL字符串。用代码对比看得最清楚。漏洞代码$sql SELECT * FROM users WHERE username . $_POST[username] . AND password . $_POST[password] . ;修复后的代码用参数化查询以PDO为例$stmt $pdo-prepare(SELECT * FROM users WHERE username ? AND password ?); $stmt-execute([$_POST[username], $_POST[password]]); $user $stmt-fetch();参数化查询的原理是SQL语句的结构在发送给数据库之前就已经固定了用户名和密码作为参数值单独传递数据库会把它们当作纯粹的数据值不会当成SQL代码去解析。即使用户名输入的是admin or 11#它也只是那个位置的一个普通字符串查询语义完全不变。打个比方拼接SQL是照着纸条念指令纸条上写把张三叫进来如果有人在纸条上加了半句然后把保险柜打开念的人会把整句都执行。参数化查询则是提前设好流程去叫一个人名字待定不管名字栏填的是什么执行的都只是叫一个人这个动作。CTF里你构造万能密码、构造union select本质上就是在那个名字待定的空格里填入了改变了整条指令的额外内容。而当代码用了参数化查询之后你填进去的一切都只是名字本身。5.2 从CTF延伸到真实业务的排查思路SQL注入在真实业务里并不是什么稀罕事只是需要授权范围内才能测试。就算不做渗透以一个开发或者运维的视角去理解这道题也有实际价值。平时排查一段可疑的代码逻辑我的思路一般是这样先全局搜索字符串拼接进入SQL的地方。在PHP里找$_GET、$_POST直接出现在mysql_query、mysqli_query附近在Java里找Statement.executeQuery配合字符串连接在Python里找cursor.execute加%或format。确认这些查询是否使用了预处理语句。用了prepare/execute的注入风险基本消除没用预处理的就要警惕。用单引号、布尔条件11、12做基础探测看请求对应的响应是否有稳定差异。有差异基本说明SQL上下文可控。如果确认存在注入尽快评估影响范围——是否涉及敏感库表账号权限是普通还是高权限。CTF里的靶场和真实系统的区别主要在防御层的复杂度上真实环境可能有过滤、有白名单、有WAF、有编码陷阱但底层的原理和排查思路是一致的。能看穿LoveSQL的拼接逻辑你在真实代码里遇到类似写法时就会有一种这里可能有事的直觉这是入门阶段最该建立的东西。6. 实操中的翻车经验做这道题最容易卡住的5个位置6.1 高频翻车点汇总把做这道题时学员和同领域朋友反馈过的问题汇总了一下排在前面的高频问题就这几个#号被URL吞掉。这是最常见的卡点。#在URL里是锚点标识浏览器不会把它发给服务端。所有带#的payload必须写成%23或者用Burp改包。单引号闭合不对。admin or 11#不进很可能是原始SQL的拼接位置和你预期的不一样。可以试试admin or 11这种闭合方式或者在密码字段里也塞注入语句。跳过order by直接union。不知道字段数就上union selectMySQL直接报错。这时候回头老老实实把order by从1递增看到3正常4报错你就知道自己站在哪里了。表名和字段名抄错。l0ve1ysq1这个表名中间的数字0经常被抄成字母o一抄错后面全白费。建议直接从回显里复制不要手打。忽略回显位置。union select 1,2,3回显之后一定要确认哪个位置显示在页面上。如果某个位置没显示内容后续查询就尽量别放那个位置。还有一个容易被忽略的请求细节LoveSQL登录后的查询接口参数可能通过GET传递也可能通过POST传递。如果在地址栏改了参数但页面没变化用Burp看一下原始请求确认参数实际是在URL行还是请求体里。6.2 做完这道题之后怎么继续进阶LoveSQL完成之后进阶路径我建议这样走先做同系列的[极客大挑战 2019]BabySQL。它和LoveSQL同源但加入了一些关键字过滤你会看到同样的注入点在加了过滤之后难度立刻上了一个台阶。再做DVWA的SQL Injection模块从Low到Medium到High逐个打感受过滤强度逐级升高的过程。Medium对参数做了转义High则用了安全得多的查询方式这个过程能帮你建立防御手段如何一步步堵住漏洞的认知。回过头来用sqlmap把LoveSQL跑一遍对比手工注入的结果顺便学习--dbs、-D geek、-T l0ve1ysq1、-C password、--dump这些参数的含义。SQL注入入门阶段的手法其实非常有限联合查询、报错注入、盲注、堆叠注入核心思想都是围绕SQL语句的语义做文章。LoveSQL把最基础的两个场景演示清楚之后后面所有的题目都是在绕过更多限制这个方向上叠加复杂程度。先把底层的语义搞清楚后面遇到的过滤器、编码层、WAF都只是加在外面的壳。这道题看起来只有两个输入点但它的完整链路覆盖了SQL注入从发现、利用到脱库的所有基础动作。把每一步都亲手敲一遍、理解一遍比草草做完一百道简单题都管用。