1. 从靶场到实战:为什么我们要亲手“拆”一个SQL注入?
大家好,我是老张,在安全圈摸爬滚打十来年了。今天我们不聊那些高深莫测的零日漏洞,也不讲复杂的攻击链,就从一个最基础、但也是最经典的Web漏洞——SQL注入开始。很多刚入门Web安全的朋友,一看到“注入”两个字就觉得头大,各种术语、Payload看得眼花缭乱。其实,SQL注入的原理,说白了就是“骗”数据库执行了不该执行的命令,就像你跟一个死板的机器人管家对话,你突然用了一句它没预料到的语法,它就懵了,把家里保险柜的密码都告诉你了。
这次我们用的“练功房”,是BUUCTF平台上的N1BOOK系列。这个系列特别适合新手,因为它把真实的漏洞环境做成了靶场,让你可以安全地、合法地“搞破坏”。我们今天要拆解的,就是N1BOOK第一章的这道SQL注入题。我当年带新人,第一课也往往是这个。为什么?因为它足够典型,流程完整,从发现漏洞到最终拿到“战利品”(flag),每一步你都能亲手操作,成就感直接拉满。
你可能会问,现在框架这么成熟,还有SQL注入吗?我告诉你,不仅有,而且在实际的渗透测试和众测项目中,它依然是出现频率极高的漏洞之一。理解它,不仅是入门Web安全的基石,更是你日后理解更复杂漏洞(比如二阶注入、盲注)的钥匙。咱们不搞纯理论,今天就跟着我,像侦探破案一样,一步步把这个字符型SQL注入给“拆”明白。你只需要一个浏览器,一颗好奇心,咱们这就开始。
2. 案发现场初探:如何发现那个可疑的“入口”?
2.1 观察URL,找到传参点
打开靶场链接,第一件事不是急着输入什么and 1=1,而是先“看”。这是老安全员的习惯。你看浏览器顶部的地址栏,URL大概长这样:http://xxx/target.php?id=1。这个?id=1就是我们要重点关注的地方。
?后面跟着的id=1,在Web里叫做“GET参数”。它就像是给服务器的一个指令,告诉服务器:“我要看id等于1的那篇文章(或商品、用户信息)”。服务器拿到这个id=1,很可能会把它拼接到一条SQL查询语句里,比如:SELECT title, content FROM articles WHERE id = 1。问题就出在这个“拼接”上。如果服务器没有对咱们输入的id值进行严格的检查和过滤,那我们就可以在id上做文章,让后面拼接的SQL语句“变味”。
所以,我们的核心思路就是:通过控制这个id参数,尝试改变原有SQL语句的逻辑,从而让数据库执行我们想要的查询,泄露原本不该泄露的信息。 题目已经提示这是SQL注入题,那我们就已经成功了一半——目标明确了。
2.2 注入类型诊断:是数字型还是字符型?
找到入口后,下一步是判断漏洞的类型。这就像医生看病,得先确定是感冒还是胃炎,才能对症下药。SQL注入主要分两大类:数字型和字符型。它们的区别在于,参数值在后台SQL语句中被处理的方式。
- 数字型:参数直接被当作数字使用。例如
WHERE id = 1。这种注入通常更简单,因为不需要考虑闭合引号。 - 字符型:参数被单引号(或双引号)包裹,当作字符串处理。例如
WHERE username = 'admin'。这种注入需要我们先闭合前面的引号,再插入我们的Payload,最后可能还要处理后面的引号。
怎么判断呢?靠“试探”。我们通过输入一些特殊的测试Payload,观察页面的回显(显示结果)是否发生变化。这里我分享一个我常用的、像敲门一样温柔的测试方法:
- 正常访问:
?id=1。记住页面正常的样子。 - 逻辑真测试:
?id=1 and 1=1。and 1=1是一个永远为真的条件。如果页面正常显示,说明and这个关键词被执行了,有注入的可能。如果报错或空白,那可能就没戏,或者过滤了and。 - 逻辑假测试:
?id=1 and 1=2。and 1=2是一个永远为假的条件。如果页面内容消失(或变成空、报错),那就进一步证实了注入点的存在,因为假条件导致整个查询没找到数据。
但是,在字符型注入里,直接这么测可能会语法错误,因为原语句可能是 id='1',你拼接后成了 id='1 and 1=1',数据库会认为 '1 and 1=1' 是一个完整的、奇怪的字符串,不会报错,但也不会按你的逻辑走。所以我们需要考虑引号。
- 尝试闭合引号(关键步骤):
?id=1'。就加一个单引号。如果页面报错了(提示SQL语法错误),那十有八九是字符型注入!因为我们的引号破坏了原语句的闭合,变成了id='1'',多了一个引号。 - 字符型确认测试:
?id=1' and '1'='1。这里我构造了一个聪明的闭合:我输入1',闭合了原左引号,然后加上and条件,再自己补一个'1'='1的真条件,最后那个'1还会和SQL语句里原本就有的右引号配对。这样整个语句就是合法的:id='1' and '1'='1'。如果页面正常,说明成功。?id=1' and '1'='2。同理,构造一个假条件:id='1' and '1'='2'。如果页面异常(无数据),那就铁板钉钉是字符型注入了。
在实际操作这道N1BOOK题目时,你会发现用 ' and 1=1 --+ 和 ' and 1=2 --+ 测试,回显一正一反。这里的 --+ 是注释符(-- 是SQL注释,+在URL中代表空格),它的作用是注释掉原SQL语句中我们后面的引号,避免语法错误。这个测试结果直接告诉我们:这是一个字符型注入点,并且原SQL语句是用单引号包裹参数的。
3. 深入敌后:摸清数据库的“底细”
3.1 判断字段数,为联合查询铺路
确认了注入类型,我们就像拿到了房间的钥匙。但进去之后,你不能乱翻,得先看看这个房间(这次查询)到底能摆下多少件“家具”(即查询结果包含多少列)。这一步至关重要,尤其是为我们接下来要用的“联合查询”(UNION SELECT)做准备。UNION操作要求前后两个SELECT语句查询的列数必须相同。
我们用 ORDER BY 子句来探测。ORDER BY 原本是用来按某一列排序的,比如 ORDER BY 1 就是按第一列排序。如果ORDER BY 5,但查询结果只有3列,数据库就会报错:“Unknown column '5' in 'order clause'”。我们就利用这个报错来试探。
具体操作:
- 输入:
?id=1' order by 1 --+。页面正常。 - 输入:
?id=1' order by 2 --+。页面正常。 - 输入:
?id=1' order by 3 --+。页面正常。 - 输入:
?id=1' order by 4 --+。页面报错了!
这说明,当前这个查询语句,返回的结果只有 3 列。这个数字“3”我们要牢记在心。这里有个小技巧,为了不让原查询的结果干扰我们看UNION的结果,我们通常会把id设成一个不存在的值,比如-1或者0,这样原查询结果为空,页面上显示的就全是我们UNION进去的内容了。
3.2 获取数据库名,找到“大楼”名称
知道了列数,我们就可以施展UNION SELECT大法了。联合查询的精髓在于,我们可以把另一个我们自定义的查询结果,“拼接”到原查询结果后面一起显示出来。第一步,我们先搞清楚我们连接的是哪个数据库。
Payload如下:
?id=-1' union select 1, database(), 3 --+
我们来拆解一下这个“咒语”:
-1': 闭合引号,并使原查询无结果。union select: 开始我们的联合查询。1, database(), 3: 这就是我们自定义的查询,要查3列(因为前面判断出是3列)。database()是一个MySQL内置函数,返回当前数据库的名字。我们把它放在第二列的位置(通常页面回显会选取其中某一列或某几列显示出来,我们需要试探哪个位置能显示)。第一列和第三列我们用数字1和3占位。--+: 注释掉后面的所有内容,保证SQL语句完整。
执行后,页面原本显示文章标题或内容的地方,很可能就变成了数据库的名字,比如 n1book。恭喜你,你已经拿到了这座“数据大楼”的钥匙牌!
3.3 列出所有表名,发现“目标房间”
知道了大楼名字,我们得看看楼里有哪些“房间”(表)。在MySQL中,有一个名为 information_schema 的数据库,它就像是整个数据库系统的“户口本”,里面记录了所有数据库、表、列的信息。我们接下来就要去查它。
Payload升级:
?id=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
这个Payload稍微复杂点,但别怕:
group_concat(table_name): 这是一个聚合函数。因为一个数据库里通常有很多表,group_concat()能把所有查询到的表名用逗号连接起来,变成一行显示,非常方便。不然你可能要手动一个个猜。from information_schema.tables: 从“户口本”的tables表里查。where table_schema=database(): 条件限制:只查当前数据库(database()函数返回我们刚拿到名字的那个库)下的表。
执行后,你可能会看到类似这样的回显:2, articles, fl4g, users...。这里2是我们占位的第二列,后面就是表名列表了。火眼金睛的你,一定发现了那个看起来就很特别的 fl4g 表。没错,按照CTF的套路,flag十有八九就在这里面!
4. 最终一击:从目标表中提取Flag
4.1 探查表结构,找到“保险箱”
找到了fl4g这个目标房间,但我们还不知道房间里哪个“保险箱”(列)装着我们要的flag。我们继续查询“户口本”。
Payload再进化:
?id=-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='fl4g' --+
这次我们从 information_schema.columns 表里查列信息。条件更精确了:数据库名是当前库 并且 表名是 'fl4g'。注意,这里的 'fl4g' 是字符串,所以要用单引号括起来。
结果可能会显示:2, id, fllllag。一个叫 fllllag 的列赫然在目,这名字起得,生怕我们找不到似的。
4.2 查询数据,收获胜利果实
万事俱备,只差临门一脚。现在我们知道:
- 数据库名:
n1book - 表名:
fl4g - 列名:
fllllag
那么,最终的查询Payload就非常直观了:
?id=-1' union select 1,2,group_concat(fllllag) from fl4g --+
或者,为了更清晰,你也可以写成:
?id=-1' union select 1,database(), group_concat(fllllag) from fl4g --+
这样,第二列显示数据库名,第三列直接显示出fl4g表里fllllag列的所有内容。执行之后,梦寐以求的flag(格式通常像flag{xxxx-xxxx-xxxx})就会出现在页面上了!
5. 原理深潜与防御思考:为什么这样就能成功?
走完了整个流程,是不是觉得有点像在玩一个解谜游戏?但作为学习者,我们不能只满足于“怎么做”,还得明白“为什么能这么做”。这背后的原理,其实就两点:
-
字符串拼接的祸根:Web应用为了动态生成SQL语句,往往会采用字符串拼接的方式,比如:
$sql = "SELECT * FROM users WHERE id = '" . $_GET['id'] . "'";当用户输入
1' union select 1,2,3 --+时,拼接后的SQL就变成了:SELECT * FROM users WHERE id = '1' union select 1,2,3 --+'由于
--+注释掉了后面的单引号,这条语句完全合法,数据库会执行它,返回两个结果集的合并。 -
过高的数据库权限:很多Web应用使用的数据库账户,拥有对
information_schema等系统库的读取权限。这就相当于给了攻击者一张完整的建筑蓝图。
那么,如何防御呢?从我多年的开发和安全审计经验来看,最有效、最根本的方法是:
- 使用参数化查询(预编译语句):这是终极解决方案。让SQL语句的“骨架”和“数据”分开传送,数据库会严格区分指令和参数,从根本上杜绝拼接。这是所有现代编程语言和框架(如Java的MyBatis、Python的SQLAlchemy、PHP的PDO)都强烈推荐的方式。
- 最小权限原则:给Web应用数据库账户分配刚好够用的权限,比如只授予特定表的增删改查权,绝不授予
DROP、FILE或是对information_schema的查询权限。 - 严格的输入验证与过滤:虽然这不是银弹,但可以作为辅助。对输入的类型、长度、格式进行严格检查。但记住,永远不要试图用“过滤关键词”来防御SQL注入,因为绕过方法太多了。
6. 举一反三:你的实战练习清单
掌握了这个基础字符型注入,你的Web安全实战才算刚刚起步。我建议你按照下面这个清单,去BUUCTF、DVWA、SQLi-Labs这些靶场里继续练习,把每个类型都亲手“拆”一遍:
- 数字型注入:尝试找一道参数是数字型(没有引号包裹)的题目,流程类似,但更简单,不需要考虑闭合引号。
- 报错注入:当页面没有正常回显,但会把数据库错误信息打印出来时,学习使用
updatexml()、extractvalue()等函数故意触发报错来带出数据。 - 布尔盲注:页面没有任何数据回显,只有对错两种状态(比如“存在”或“不存在”)。学习通过构造真/假条件,像猜二进制一样一位位地猜出数据。
- 时间盲注:连对错状态都没有,只能通过让数据库执行睡眠函数(如
sleep(5))来判断注入是否成功。这是最考验耐心的一种。 - 堆叠查询:尝试用分号
;执行多条SQL语句,体验更强大的控制力(但并非所有环境都支持)。
每攻克一种类型,你对SQL注入的理解就会深一层。最后,别忘了去学学SQL语法本身,SELECT、UNION、WHERE、JOIN……这些命令就像是你手中的工具,工具用熟了,思路自然就开阔了。安全之路,动手远比空想重要。今天就到这里,希望这个详细的拆解能帮你推开Web安全实战的大门。遇到问题别怕,多试错,多查资料,每个高手都是从踩坑开始的。
382



被折叠的 条评论
为什么被折叠?



