一、SQL注入基础理论
1.1 什么是SQL注入
SQL注入是一种常见的Web安全问题,攻击者通过在Web应用程序的输入字段中插入恶意的SQL语句,改变原本SQL查询的逻辑,实现非法获取数据、篡改数据、执行系统命令等操作。这种情况产生的根本原因在于应用程序对用户输入数据的合法性没有进行严格的验证和过滤,导致用户输入的数据被当作SQL语句的一部分执行。
1.2 SQL注入的危害
数据泄露:攻击者可以通过SQL注入获取数据库中的敏感信息,如用户账号、密码、身份证号等。
数据篡改:修改数据库中的数据,如篡改用户的交易记录、修改网站内容等。
权限提升:在某些情况下,攻击者可以利用SQL注入获取数据库管理员权限,进一步控制整个服务器。
服务器被控制:通过SQL注入执行系统命令,控制服务器,如上传恶意文件、开启后门等。
1.3 SQL注入的类型
数字型注入:当输入的参数为数字类型时,SQL语句中没有使用引号包裹参数,如 SELECT * FROM users WHERE id = 1 。
字符型注入:输入的参数为字符类型,SQL语句中使用引号包裹参数,如 SELECT * FROM users WHERE username = 'admin' 。
宽字节注入:利用GBK等宽字节编码的特性,绕过对单引号等特殊字符的过滤。
盲注:在无法直接获取数据库返回结果的情况下,通过构造条件语句,根据页面返回的状态(如页面正常、页面错误、响应时间等)来推断数据库中的信息,包括布尔盲注和时间盲注。
二次注入:攻击者将恶意数据存储在数据库中,当应用程序再次调用该数据进行SQL操作时,触发SQL注入。
堆叠注入:在一条SQL语句中执行多条SQL语句,通过分号(;)分隔不同的SQL命令 。
二、SQL注入实战案例
2.1 搭建测试环境
我们可以使用PHP + MySQL搭建一个简单的Web应用来模拟相关场景。
数据库表结构:
CREATE DATABASE testdb;
USE testdb;
CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY,username VARCHAR(50),password VARCHAR(50)
);
INSERT INTO users (username, password) VALUES ('admin', 'admin123'), ('user1', 'user123');
PHP代码(存在非安全设计):
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "testdb";// 创建连接
$conn = new mysqli($servername, $username, $password, $dbname);// 检查连接
if ($conn->connect_error) {die("连接失败: ". $conn->connect_error);
}$user_input = $_GET["username"];
$sql = "SELECT * FROM users WHERE username = '$user_input'";
$result = $conn->query($sql);if ($result->num_rows > 0) {while($row = $result->fetch_assoc()) {echo "id: ". $row["id"]. " Name: ". $row["username"]. "<br>";}
} else {echo "没有找到匹配的用户";
}$conn->close();
?>
访问链接 http://localhost/test.php?username=admin 时,会正常查询并显示用户信息。
2.2 数字型SQL注入实战
假设SQL查询语句为 SELECT * FROM products WHERE id = 1 ,我们可以尝试以下操作:
输入 1 or 1=1 ,此时SQL语句变为 SELECT * FROM products WHERE id = 1 or 1=1 ,由于 1=1 恒成立,会返回所有产品信息。
2.3 字符型SQL注入实战
对于 SELECT * FROM users WHERE username = 'admin' 这样的查询语句:
输入 ' or '1'='1 ,SQL语句变为 SELECT * FROM users WHERE username = '' or '1'='1' ,同样因为 '1'='1' 恒成立,会返回所有用户信息。
输入 ' and 1=2 union select 1,2 -- - , ' and 1=2 使前面的查询条件不成立, union select 1,2 会将 1 和 2 作为查询结果返回, -- - 注释掉后面的单引号,防止语法错误。
2.4 盲注实战
布尔盲注
假设页面只有两种返回状态:有数据返回时显示 “查询成功”,无数据返回时显示 “查询失败”。我们可以通过构造条件语句来推断数据库中的信息。例如,判断数据库中是否存在 admin 用户:
输入 ' and exists(select * from users where username='admin') -- - ,如果页面显示 “查询成功”,则说明存在 admin 用户。
时间盲注
当页面没有明显的回显信息时,可以利用时间盲注。例如,判断数据库中 users 表的记录数是否大于10:
' and if((select count(*) from users)>10, sleep(5), 1) -- -
如果页面响应时间超过5秒,说明 users 表的记录数大于10。
三、SQL注入遇到的问题及解决方案
3.1 特殊字符过滤
很多应用程序会对单引号、分号等特殊字符进行过滤,导致相关操作失败。
解决方案:
使用编码绕过,如将单引号 ' 编码为 %27 (URL编码)。
利用宽字节注入,在单引号前添加 %df (GBK编码下, %df' 会组成一个合法的汉字,从而绕过单引号过滤)。
3.2 预编译语句的使用
预编译语句可以有效防范相关风险,因为它将用户输入的数据和SQL语句进行了分离。
示例代码(PHP PDO预编译):
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "testdb";try {$conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);// 设置 PDO 错误模式为异常$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);$user_input = $_GET["username"];$sql = "SELECT * FROM users WHERE username = :username";$stmt = $conn->prepare($sql);$stmt->bindParam(':username', $user_input);$stmt->execute();$result = $stmt->fetchAll(PDO::FETCH_ASSOC);if (count($result) > 0) {foreach ($result as $row) {echo "id: ". $row["id"]. " Name: ". $row["username"]. "<br>";}} else {echo "没有找到匹配的用户";}
} catch(PDOException $e) {echo "Error: ". $e->getMessage();
}
$conn = null;
?>
在上述代码中, bindParam 方法会自动对用户输入进行转义,确保数据的安全性。
四、绕过WAF(Web应用防火墙)
4.1 WAF的工作原理
WAF通过检测HTTP请求中的数据,与预设的规则库进行匹配,一旦发现请求中包含可疑的特征,就会拦截该请求。
4.2 绕过WAF的方法
大小写混合:将SQL关键字进行大小写混合,如 SeLeCt ,可能绕过部分基于规则的WAF。
内联注释:使用MySQL的内联注释,如 /*!SELECT*/ ,部分WAF可能无法识别这种形式的SQL关键字。
双写绕过:将关键字拆分后双写,例如 selseselectct ,当WAF过滤掉第一个 select 后,仍能组成完整的 select 关键字。
编码绕过:对相关语句进行URL编码、Unicode编码等,如将 select 编码为 %73%65%6C%65%63%74 ,绕过基于明文匹配的WAF。
利用WAF的逻辑漏洞:某些WAF在处理长请求、多参数请求时可能存在逻辑问题,可以构造特殊的请求格式来绕过检测。
五、总结
SQL注入是Web应用中非常危险的安全问题,掌握SQL注入的原理、实战技巧以及绕过方法,对于渗透测试人员来说至关重要。同时,开发人员也应该重视输入验证和使用安全的数据库操作方式(如预编译语句),从根源上避免出现相关安全隐患。在实际的渗透测试和安全防护过程中,需要不断学习和实践,以应对不断变化的攻击和防御技术。