Redis CVE-2016-8339 分析

Redis CVE-2016-8339 分析

前言:

影响版本: Redis 3.2.x-3.2.4.

分析版本: Redis 3.2.0

环境:ubuntu 17.10

git clone https://github.com/antirez/redis.git

切换到历史版本

git reset --hard 670586715a19e7aff

0x01 漏洞类型:数组越界

数组下标越界导致溢出(Redis是使用标准C语言开发的)。

0x02 漏洞原理:

在 Redis 中,CONFIG SET parameter value 命令可以动态的修改服务器配置,而无需重启。其中,有一条命令CONFIG SET client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds> 可以给当前未连接到服务端的某一类客户端设置”客户端输出缓冲区限制”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    /* Finally set the new config */
for (j = 0; j < vlen; j += 4) {
int class;
unsigned long long hard, soft;
int soft_seconds;

class = getClientTypeByName(v[j]); //返回值 -1 到 3
hard = strtoll(v[j+1],NULL,10);
soft = strtoll(v[j+2],NULL,10);
soft_seconds = strtoll(v[j+3],NULL,10);

server.client_obuf_limits[class].hard_limit_bytes = hard;
server.client_obuf_limits[class].soft_limit_bytes = soft;
server.client_obuf_limits[class].soft_limit_seconds = soft_seconds;
}
sdsfreesplitres(v,vlen);
} config_set_special_field("notify-keyspace-events") {
int flags = keyspaceEventsStringToFlags(o->ptr);

if (flags == -1) goto badfmt;
server.notify_keyspace_events = flags;

通过 getClientTypeByName函数可以获取类型,我们转到这个函数的定义可以发现

1
2
3
4
5
6
7
int getClientTypeByName(char *name) {
if (!strcasecmp(name,"normal")) return CLIENT_TYPE_NORMAL;
else if (!strcasecmp(name,"slave")) return CLIENT_TYPE_SLAVE;
else if (!strcasecmp(name,"pubsub")) return CLIENT_TYPE_PUBSUB;
else if (!strcasecmp(name,"master")) return CLIENT_TYPE_MASTER;
else return -1;
}

通过简单字符串类型比较,然后返回不同的值。返回值的定义宏如下:

1
2
3
4
5
6
7
#define CLIENT_TYPE_NORMAL 0 /* Normal req-reply clients + MONITORs */
#define CLIENT_TYPE_SLAVE 1 /* Slaves. */
#define CLIENT_TYPE_PUBSUB 2 /* Clients subscribed to PubSub channels. */
#define CLIENT_TYPE_MASTER 3 /* Master. */
#define CLIENT_TYPE_OBUF_COUNT 3 /* Number of clients to expose to output
buffer configuration. Just the first
three: normal, slave, pubsub. */

然后将返回的值放在 class字符串中,然后紧接着作为数组下标进行处理。

1
2
3
server.client_obuf_limits[class].hard_limit_bytes = hard;
server.client_obuf_limits[class].soft_limit_bytes = soft;
server.client_obuf_limits[class].soft_limit_seconds = soft_seconds;

我们查看server.client_obuf_limits[class]的解析:

1
2
3
4
5
6
7
clientBufferLimitsConfig client_obuf_limits[CLIENT_TYPE_OBUF_COUNT]; //数组大小为3
/* AOF persistence */
int aof_state; /* AOF_(ON|OFF|WAIT_REWRITE) */
int aof_fsync; /* Kind of fsync() policy */
char *aof_filename; /* Name of the AOF file */
int aof_no_fsync_on_rewrite; /* Don't fsync if a rewrite is in prog. */
int aof_rewrite_perc; /* Rewrite AOF if % growth is > M and... */
1
2
3
4
5
6
7
#define CLIENT_TYPE_NORMAL 0 /* Normal req-reply clients + MONITORs */
#define CLIENT_TYPE_SLAVE 1 /* Slaves. */
#define CLIENT_TYPE_PUBSUB 2 /* Clients subscribed to PubSub channels. */
#define CLIENT_TYPE_MASTER 3 /* Master. */
#define CLIENT_TYPE_OBUF_COUNT 3 /* Number of clients to expose to output
buffer configuration. Just the first
three: normal, slave, pubsub. */

可以看到,getClientTypeByName 函数解析客户端类型并返回一个值存储在class变量中,它的取值范围是[-1, 3],接下来client_obuf_limits使用class变量作为下标去访问结构体数组并赋值。然而从client_obuf_limits的定义处可以发现,它的长度是3。

这就意味着,这存在着数组下标越界的可能性,由于后续操作是写,所以存在越界写。我们进一步查看关于clientBufferLimitsConfig 结构体的定义

1
2
3
4
5
typedef struct clientBufferLimitsConfig {
unsigned long long hard_limit_bytes;
unsigned long long soft_limit_bytes;
time_t soft_limit_seconds;
} clientBufferLimitsConfig;

结构体大小为 24 字节,这说明攻击者最多向client_obuf_limits数组后写入24字节的数据。

1
2
3
4
5
6
7
clientBufferLimitsConfig client_obuf_limits[CLIENT_TYPE_OBUF_COUNT]; //数组大小为3
/* AOF persistence */
int aof_state; /* AOF_(ON|OFF|WAIT_REWRITE) */
int aof_fsync; /* Kind of fsync() policy */
char *aof_filename; /* Name of the AOF file */
int aof_no_fsync_on_rewrite; /* Don't fsync if a rewrite is in prog. */
int aof_rewrite_perc; /* Rewrite AOF if % growth is > M and... */

由上部分内容,我们大概可以知道client_obuf_limits数组是redisServer结构体的一个成员,它的后面紧跟着AOF状态域(Redis 将所有对数据库进行过写入的命令记录到 AOF 文件, 以此达到记录数据库状态的目的)。攻击者是可以覆盖到这些域并写入数据的。

0x03 利用

由于只能写入24 字节的内容,我们仅仅只能覆盖到后面的,在这些域中暂时只看到aof_filename这个指针存在利用点。

what is AOF?

Redis 分别提供了 RDB 和 AOF 两种持久化机制:

  • RDB 将数据库的快照(snapshot)以二进制的方式保存到磁盘中。
  • AOF 则以协议文本的方式,将所有对数据库进行过写入的命令(及其参数)记录到 AOF 文件,以此达到记录数据库状态的目的。

AOF doing ?

Redis 将所有对数据库进行过写入的命令(及其参数)记录到 AOF 文件, 以此达到记录数据库状态的目的, 为了方便起见, 我们称呼这种记录过程为同步。

举个例子, 如果执行以下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
redis> RPUSH list 1 2 3 4
(integer) 4

redis> LRANGE list 0 -1
1) "1"
2) "2"
3) "3"
4) "4"

redis> KEYS *
1) "list"

redis> RPOP list
"4"

redis> LPOP list
"1"

redis> LPUSH list 1
(integer) 3

redis> LRANGE list 0 -1
1) "1"
2) "2"
3) "3"

那么其中四条对数据库有修改的写入命令就会被同步到 AOF 文件中:

1
2
3
4
5
6
7
RPUSH list 1 2 3 4

RPOP list

LPOP list

LPUSH list 1

为了处理的方便, AOF 文件使用网络通讯协议的格式来保存这些命令。

比如说, 上面列举的四个命令在 AOF 文件中就实际保存如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
*2
$6
SELECT
$1
0
*6
$5
RPUSH
$4
list
$1
1
$1
2
$1
3
$1
4
*2
$4
RPOP
$4
list
*2
$4
LPOP
$4
list
*3
$5
LPUSH
$4
list
$1
1

除了 SELECT 命令是 AOF 程序自己加上去的之外, 其他命令都是之前我们在终端里执行的命令。

同步命令到 AOF 文件的整个过程可以分为三个阶段:

  1. 命令传播:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。
  2. 缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的 AOF 缓存中。
  3. 文件写入和保存:AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保存条件被满足的话, fsync 函数或者 fdatasync函数会被调用,将写入的内容真正地保存到磁盘中。

利用思路

aof_filename这个字符指针值得注意,通过修改这个指针,在具体的环境下

(1)通过修改这个指针,在具体的环境下,攻击者可以达到利用AOF数据覆写任意文件的目的;

(2)加载通过其他途径构造的恶意AOF文件,来进行进一步的攻击。

POC

config set client-output-buffer-limit "master 1094795585 1094795585 1094795585"

效果如下:

可以发现 aof_stateaof_filename、以及aof_no_fsync_on_rewrite 被覆写了。

0x04 漏洞修复

redis 的修复记录:

https://github.com/antirez/redis/commit/6d9f8e2462fc2c426d48c941edeb78e5df7d2977

class 的值了做了进一步判断。

0x05 参考

http://redisbook.readthedocs.io/en/latest/internal/aof.html