sql预编译注入

1. SQL注入基础

1.1. 什么是sql注入

SQL注入(SQL Injection)是一种代码注入技术,攻击者通过在用户可控的输入位置插入SQL代码,使应用程序执行非预期的SQL语句,从而破坏原有的SQL语句语义。当应用程序将这些带有恶意SQL代码的输入直接拼接到SQL查询语句中执行时,就会导致SQL注入漏洞。

简单来说就是传入的恶意参数被当做sql语句执行了

1.2. 传统SQL注入的原理与类型

1.2.1. 原理

SQL注入的根本原因是应用程序没有正确区分代码和数据。当用户输入被直接拼接到SQL语句中时,输入中的特殊字符可能会改变SQL语句的结构和语义,导致非预期的执行结果。

示例

// 不安全的代码
$query = "SELECT * FROM users WHERE username = '" . $_POST['username'] . "' AND password = '" . $_POST['password'] . "'";

如果用户输入:'OR '1'='1,最终SQL会变成:

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '...'

这就可以实现一个任意密码登录

1.2.2. 类型

讲完的原理再来讲下类型

1.2.2.1. 联合查询注入

通过union拼接查询语句获取大量的信息
利用:模糊查询猜测列名、表名---->union查询构造select语句进行查询返回大量数据

1.2.2.2. 报错注入

利用系统返回的错误信息来获取有用的信息
需要用到的函数updataxmlfloor向下取整、group by分组排列、count统计数量、concat连接字符串updatexml0x7e 等价于 ~ ,将查询语句和特殊符号拼接在一起,就可以将查询结果显示在报错信息中

1.2.2.3. 布尔盲注

kobe%27%20and%20 ascii ( substr(database(),§1§,1))=§115§--+

1.2.2.4. 时间盲注

构造语句,通过页面响应时长来判断信息。
例如:id=1 and if(length((select database()))>1,sleep(3),1)--+
sleep被禁用 使用 get_lockheavy_querybenchmark

1.2.2.5. 宽字节注入

使用gbk编码, %df/ 组成了一个汉字 `綅,使得转义字符不起作用,单引号逃脱

1.2.2.6. 二次注入

特殊字符进行了转义处理,在从数据库中取脏数据,发生二次注入,比如同时注册两个账admin,admin’,然后修改admin’密码成功把admin密码修改了

1.2.2.7. DNSlog注入

Dns在域名解析时会留下解析记录,利用 load_file() 函数发起请求,使用Dnslog接受请求,可以获取数据。

1.3. 防御SQL注入的常见方法

1.3.1. 参数化查询(预编译)

     $stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
     $stmt->bind_param("ss", $username, $password);

1.3.2. 黑白名单

黑名单:对特殊的字符例如括号斜杠进行转义过滤删除;
白名单:对用户的输入进行正则表达式匹配限制

1.3.3. 最小化权限原则

  • 应用程序使用的数据库账户应只具备必要的最小权限
  • 避免使用数据库管理员账户运行应用
    很多时候sql注入可能导致RCE,如sql注入->sql写webshell,这会导致更大的危害,
    而导致的原因往往就是sql数据库用户的权限过大。如dba权限

1.3.4. 上WAF

  • 部署WAF过滤恶意请求

1.3.5. 数据库加密

  • 加密存储敏感数据,使得即使数据被窃取也难以利用

1.3.6. 站库分离

主要是防止写webshell

1.3.7. 提高开发人员的安全意识

漏洞产生的根本原因都是人的原因。
只有提高人员的安全意识才能从根本上杜绝漏洞

2. 预编译机制详解

2.1. 预编译的工作原理

预编译语句(Prepared Statements)是一种数据库功能,它将SQL语句的编译与执行分为两个独立阶段:

2.1.1. 工作流程

1.准备阶段:
应用程序:发送带占位符的SQL模板(不含实际数据)
数据库:分析SQL结构并生成执行计划

2.执行阶段:
应用程序:只发送参数值
数据库:将值填入预先准备好的执行计划并执行

2.1.2. 安全机制

预编译能防SQL注入的核心原理在于SQL语句结构与用户输入数据的分离:

  • 语句结构已固定:SQL的语法结构在准备阶段就已确定,后续传入的参数无法改变SQL的语义
  • 参数仅作为数据:即使参数中包含SQL关键字或特殊字符,也只会被视为普通数据值,而非SQL代码
  • 类型安全:参数绑定通常伴随类型指定,确保输入按预期类型处理

2.1.3. 性能优势

除安全性外,预编译还具有性能优势:

  • 减少解析开销:SQL模板只解析一次,多次执行使用同一执行计划
  • 批量处理:可以一次准备SQL,多次使用不同参数执行
  • 减少网络传输:执行阶段只需传输参数,而非完整SQL

2.2. 真预编译vs模拟预编译

PDO::ATTR_EMULATE_PREPARES 配置是否启用模拟预编译

2.2.1. 真预编译(Real Prepares)

真预编译是指由数据库服务器真正执行SQL语句的预处理:
模拟预编译 没有参数绑定 预编译的过程,只是对符号做了过滤 转义

特点:

  • 完整的两阶段处理在数据库端进行
  • SQL模板和参数分别发送到数据库
  • 参数值永远不会被解析为SQL代码的一部分
    安全性:
  • 提供最高级别的SQL注入防护
  • 可防止多语句攻击(如;分隔的多条SQL)
  • 参数不会被错误解析,即使数据库驱动存在漏洞
准备阶段
   客户端 ──[SQL模板]──> 数据库服务器
               ↓
   解析SQL、生成语法树、优化、生成执行计划
               ↓
   客户端 <──[statement_id]── 数据库服务器

执行阶段
客户端 ──[statement_id + 参数值]──> 数据库服务器
                   ↓
将参数值填入预编译语句的执行计划
                   ↓
客户端 <──[查询结果]── 数据库服务器

2.2.2. 模拟预编译(Emulated Prepares)

模拟预编译是在客户端(应用程序)模拟预编译过程:

特点:

  • 在应用程序端将参数值转义并插入SQL模板
  • 向数据库发送完整的SQL语句,而非分离的模板和参数
  • 数据库接收到的是拼接后的SQL,不执行真正的预处理
    安全风险:
  • 依赖客户端转义机制的正确性
  • 不同字符集可能导致转义失效
  • 在某些情况下可能仍存在SQL注入风险

区别与选择

特性 真预编译 模拟预编译
防SQL注入效果 更可靠 依赖转义正确性
处理多语句攻击 有效防御 可能有漏洞
性能(大量查询) 更优 较差
性能(单次查询) 可能略差 可能略好
服务器负载 较高 较低
兼容性 依赖数据库支持 更广泛

2.3. 各语言中的预编译实现

2.3.1. PHP中的预编译

PDO(PHP Data Objects)

// 默认为模拟预编译
$pdo = new PDO("mysql:host=localhost;dbname=test", "user", "password");

// 启用真预编译
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);

// 使用预编译
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]);
  • PDO默认使用模拟预编译
  • PHP 5.3.6+版本可通过设置PDO::ATTR_EMULATE_PREPARES为false启用真预编译
  • 需要指定正确的字符集(如charset=utf8mb4)防止字符编码问题

MySQLi

$mysqli = new mysqli("localhost", "user", "password", "database");

// 使用预编译
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
  • MySQLi默认使用真预编译
  • 通过类型指定参数("ss"表示两个字符串参数)提供额外安全性

2.3.2. Java中的预编译

JDBC

// 使用PreparedStatement
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
  • JDBC默认使用真预编译
  • 强类型参数绑定(setString, setInt等)
  • 可通过prepareStatement(sql, ResultSet.TYPE_SCROLL_SENSITIVE)等参数调整行为

Hibernate/JPA

// 使用参数化查询
String hql = "FROM User WHERE username = :username AND password = :password";
Query query = session.createQuery(hql);
query.setParameter("username", username);
query.setParameter("password", password);
List<User> result = query.list();
  • ORM框架自动处理预编译
  • 支持命名参数(:parameter)和位置参数(?)
  • 自动处理类型转换和转义

2.3.3. Python中的预编译

MySQL Connector

import mysql.connector

conn = mysql.connector.connect(user='user', password='password', database='testdb')
cursor = conn.cursor(prepared=True)  # 启用预编译支持

query = "SELECT * FROM users WHERE username = %s AND password = %s"
cursor.execute(query, (username, password))
  • 需显式启用预编译支持
  • 使用%s作为通用占位符(不同于字符串格式化中的%s)

psycopg2 (PostgreSQL)

import psycopg2

conn = psycopg2.connect("dbname=test user=user password=password")
cur = conn.cursor()

cur.execute("SELECT * FROM users WHERE username = %s AND password = %s", 
           (username, password))
  • 自动使用预编译
  • 参数可以是元组、列表或字典

SQLAlchemy

from sqlalchemy import text

# 使用参数化查询
stmt = text("SELECT * FROM users WHERE username = :username AND password = :password")
result = conn.execute(stmt, username=username, password=password)
  • 抽象层自动处理预编译
  • 支持多种数据库后端
  • 提供ORM和底层SQL两种接口

2.3.4. 其他语言实现

Node.js (mysql2)

const mysql = require('mysql2');

const connection = mysql.createConnection({
  host: 'localhost',
  user: 'user',
  password: 'password',
  database: 'test'
});

const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
connection.execute(query, [username, password], (err, results) => {
  // 处理结果
});

C# (ADO.NET)

using (SqlConnection connection = new SqlConnection(connectionString))
{
    SqlCommand command = new SqlCommand("SELECT * FROM users WHERE username = @Username", connection);
    command.Parameters.AddWithValue("@Username", username);
    
    connection.Open();
    SqlDataReader reader = command.ExecuteReader();
    // 处理结果
}