



























So, Mongoose. If you’ve never heard of it, you’ve almost certainly used a device that runs it. It’s a single-file, cross-platform embedded network library written in C by Cesanta that provides HTTP/HTTPS, WebSocket, MQTT, mDNS and more, designed specifically for embedded systems and IoT devices where something like OpenSSL would be way too heavy. Their own website claims deployment on hundreds of millions of devices by companies like Siemens, Schneider Electric, Broadcom, Bosch, Google, Samsung, Qualcomm and Caterpillar. They even claim it runs on the International Space Station. We’re talking everything from smart home gateways and IP cameras to industrial PLCs, SCADA systems and, apparently, space.

One of Mongoose’s key selling points is its built-in TLS 1.3 implementation (MG_TLS_BUILTIN). Instead of linking against OpenSSL or mbedTLS, you get TLS right out of the box, including mutual TLS (mTLS) for client certificate authentication. This is particularly appealing for embedded devices where every kilobyte of firmware matters and cross-compiling OpenSSL for some obscure MIPS or ARM SoC is a pain. Sounds great, right?

During one of the usual weekend fun projects, I found three vulnerabilities in Mongoose v7.20, each independently exploitable: complete bypass of mTLS authentication, preauth RCE as root via a heap overflow in the client public key parsing logic, and preauth RCE via a single UDP packet through mDNS. No authentication required for any of them. Not that authentication can’t be bypassed anyway :D
[email protected] with full technical details, weaponized exploits and proposed fixes.mg_connection->fn function pointer → shellcode execution as root. (CVSS 7.3 High, CWE-122 Heap-based Buffer Overflow)All three affect Mongoose versions 7.0 through 7.20. Fixed in version 7.21.
A remote unauthenticated attacker can:
Mongoose is deployed on hundreds of millions of devices by companies including Siemens, Schneider Electric, Broadcom, Bosch, Google, Samsung, Qualcomm, and Caterpillar. Any device using MG_TLS_BUILTIN or mDNS is potentially affected:
MG_TLS_BUILTIN to OpenSSL or mbedTLS for your TLS implementation.Let’s start with the fun one, the one that made me literally say “no way” out loud. Mutual TLS (mTLS) is the gold standard for device-to-device authentication in IoT deployments. Instead of passwords or API keys, both the server and the client present X.509 certificates signed by a trusted Certificate Authority. The server verifies the client’s certificate against its CA, and only if the signature checks out does the client get access.
In Mongoose’s built-in TLS implementation, this verification happens in mg_tls_verify_cert_signature(). Here’s the relevant code path from tls_builtin.c line 1527:
1 | if (issuer->pubkey.len == 64) { |
When the CA certificate uses a P-384 (secp384r1) ECDSA public key the function returns 1 (success) without performing any signature verification at all. The comment even says “ignore secp386 for now”.
What does this mean in practice? If your Mongoose mTLS server uses a P-384 CA (which is a perfectly reasonable and increasingly common choice since P-384 provides 192-bit security vs P-256’s 128-bit), then any client certificate is accepted. It doesn’t matter who signed it. It doesn’t matter if you generated it yourself five seconds ago with a completely random CA. The server will let you in.
This is bad enough on its own. mTLS is specifically designed to prevent unauthorized access to sensitive management interfaces. But it gets worse.
This one is independent from Bug 1. The heap overflow triggers during certificate parsing in the TLS handshake. It doesn’t matter whether the certificate passes verification or not, because the vulnerable memmove happens before any signature check. Any TLS client that sends a crafted certificate with an oversized RSA public key can trigger it.
In mg_tls_recv_cert() (this line), when Mongoose processes a client certificate during the TLS handshake, it copies the certificate’s public key into a fixed-size buffer:
1 | memmove(tls->pubkey, ci->pubkey.buf, ci->pubkey.len); |
The pubkey field inside tls_data is a fixed 528-byte buffer. The length ci->pubkey.len comes directly from parsing the client’s X.509 certificate DER - which the attacker fully controls. There is no bounds check.
Great.
An 8192-bit RSA key has a modulus of ~1037 bytes. That’s 509 bytes past the end of the 528-byte buffer, overflowing across the heap into adjacent allocations.
Since Mongoose is distributed as a single-file C library, it gets compiled into an enormous variety of targets - from Linux and FreeBSD servers to bare-metal microcontrollers, FreeRTOS, Zephyr, and other real-time operating systems. The heap layout, available hardening, and exploitability will differ across each one. That said, on embedded MIPS devices compiled with -z execstack (which is extremely common - no PIE, no canaries, no RELRO), this is game over. While PT_GNU_STACK RWE technically marks the stack as executable, on MIPS Linux the kernel sets READ_IMPLIES_EXEC as a side effect, which makes the heap executable too. On older uClibc-based embedded targets, PT_GNU_STACK may not even be processed at all, meaning the stack (and heap) are executable by default. Either way, the overflow executes with whatever privileges the server runs as - typically root on IoT devices.
The entire attack happens during the TLS handshake, before any HTTP request is processed. On a typical IoT device compiled with no hardening (which is the norm, not the exception), this is a reliable, single-shot preauth remote code execution.
This one is different from the TLS bugs. It doesn’t require mTLS, it doesn’t require TLS at all. It requires a single UDP packet.
Mongoose includes mDNS (multicast DNS) support for service discovery - the same protocol that lets your phone find printers and smart home devices on the local network. When a device registers an mDNS service (like _http._tcp), it responds to PTR queries with multiple DNS records: a PTR record pointing to the service name, an SRV record with the hostname and port, a TXT record with device metadata, and an A record with the IP address.
The function handle_mdns_record() in mongoose.c (dns.c line 388) allocates a fixed-size stack buffer for this response:
1 | uint8_t buf[sizeof(struct mg_dns_header) + 256 + sizeof(mdns_answer) + 4]; |
That buffer was sized for a single DNS name (max 256 bytes). But a PTR response packs four records into it sequentially, and the critical copy in build_txt_record() has no bounds check:
1 | memcpy(p, r->txt.buf, r->txt.len), p += r->txt.len; |
The response size formula is:
1 | total = 82 + srvcproto.len + 2 * respname.len + txt.len |
With standard IoT device metadata - a 63-character hostname and ~450 bytes of TXT records (firmware version, model, serial number, capabilities - perfectly normal stuff per RFC 6763):
1 | total = 82 + 10 + 2*63 + 450 = 668 bytes |
That’s a 386-byte overflow on a 282-byte buffer. On the stack. From a single UDP packet.
If you’re using Mongoose with MG_TLS_BUILTIN in production - especially on embedded devices with no hardening, apply the fixes above. Now.
By the way, this is the second time I pwn a major project that claims to be covered by oss-fuzz. And I do this as a noob, just for fun on the weekends. Funny how the pros never seem to question the effectiveness of oss-fuzz with the same passion they use while attacking AI assisted security research :D

Stay safe out there. And maybe don’t roll your own TLS.
Hack the planet!
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。