CVE-2025-61983
An out-of-bounds write vulnerability exists in the tmpServer opcode 0x442 functionality of Tp-Link AX53 v1.0 1.3.1 Build 20241120 rel.54901(5553). A specially crafted network packets can lead to arbitrary code execution. An attacker can send packets to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
Tp-Link Archer AX53 v1.0 1.3.1 Build 20241120 rel.54901(5553)
Archer AX53 v1.0 - https://www.tp-link.com/my/support/download/archer-ax53/
9.1 - CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H
CWE-787 - Out-of-bounds Write
The TP-Link Archer AX53 AX3000 Dual Band Gigabit Wi-Fi 6 Router is currently among the most popular routers sold online, and boasts impressive gigabit speeds for the price. This router also features remote cloud access via the TP-Link HomeShield application and smart home functionality.
In order to facilitate remote management of the TP-Link AX53 and many other TP-Link devices, the HomeShield phone app can connect to the device through the cloud. After authentication, an SSH port forward is setup from the cloud to the TP-Link router, which then allows for communication to network services listening on the TP-Link router’s localhost interfaces. Specifically for the HomeShield app, this is the tmpServer network service, which listens on TCP 127.0.0.1:20002. This service is able to modify a large amount of settings and configuration on the TP-Link router, however it is not 1-1 to the admin webportal that is accessible via LAN. Regardless, we start from where the tmpServer service starts parsing network packets:
000247b4 int32_t p2_recv(struct tmp_client* tmpcli)
// [...]
00024808 int32_t bytes_recvd =
00024808 recv(__fd: tmpcli->fd, __buf: get_read_buf_ptr(tmpcli), __n: get_bytes_left_in_buf(tmpcli), __flags: 0) // [1]
00024808
00024818 if (bytes_recvd s< 0)
0002482c log("tmpdServer.c:1382", "TMP RECV ERROR")
00024830 return 0xffffffff
00024830
// [...]
000248dc tmpcli->insize += bytes_recvd
000248fc log("tmpdServer.c:1398", "TMP RECV DATA length = %d", tmpcli->insize)
000248fc
00024910 while (true)
00024910 if (tmpcli->insize s<= 2)
00024914 return 0
00024914
00024920 uint32_t authhdrlen = GET_AUTH(tmpcli) // [2]
00024920
00024930 if (authhdrlen s< 0)
00024944 log("tmpdServer.c:1413", "GET AUTH ERROR")
00024948 return 0xffffffff
00024948
00024960 if (tmpcli->insize s<= authhdrlen)
00024964 return 0
00024964
00024970 int32_t* datasize = GET_PKT_BY_STATUS(tmpcli) // [3]
As always, our code flow starts from the call to recv[1], and in this case up to 0x4000 bytes can be sent in a single message or set of messages and then read in without error. The check at [2] doesn’t really do much important if our opcode is not \xfe, which it never is, and then at [3] some actual checks are made depending on the state of the client connection. A quick digression into the client connection structure before continuing into the code:
struct tmp_client __packed
{
uint32_t fd;
void* sysinfo;
uint32_t status;
struct TmpPktIn* tdpin;
uint32_t insize;
struct TdpPkt* out;
uint32_t outpkt_size;
uint32_t serviceType;
uint32_t serviceMask;
uint32_t authHdrLen;
};
Most of these fields should be self explanatory, but it’s worth noting that the status field always starts at 0x1 for new connections, the tdpin buffer is max size 0x4000, and the tdpout buffer is also max size 0x4000. Continuing in the code, before we can send any actual data, we must perform a mini handshake in the form of ASSOCIATION packets, which we can see continuing into GET_PKT_BY_STATUS[3]:
000245e8 int32_t GET_PKT_BY_STATUS(struct tmp_client* tmpcli)
000245fc uint32_t status = tmpcli->status
000245fc
00024604 if (status s>= 1)
0002460c if (status s<= 2)
0002462c log("tmpdServer.c:1264", "TMP GET ASSOC PKT FROM BUF")
00024638 return check_assoc_pktlen_at_least_4(tmpcli)
00024638
00024614 if (status == 3)
00024650 log("tmpdServer.c:1268", "TMP GET DATA PKT FROM BUF")
0002465c return get_data_pkt_from_buf(tmpcli)
0002465c
00024664 return 0xffffffff
As mentioned, our client connection always starts at 0x1, so we hit the function at 0x24638, which just checks to see if the size of the input packet minus the size of the auth header (which can be up to 0x40) is greater than 0x4. Assuming this is true, we step back up to the function that started with recv:
//[...]
00024970 int32_t* datasize = GET_PKT_BY_STATUS(tmpcli) // [3]
00024980 if (datasize s< 0)
00024994 log("tmpdServer.c:1427", "GET PKT ERROR")
00024998 return 0xffffffff
00024998
000249a8 if (datasize == 0)
000249a8 break
000249a8
000249d0 int32_t result = ENTER_ASSOC_OR_DATA_CENTER(tmpcli, datasize) //[4]
000249d0
Immediately after hopefully passing the initial checks, we enter the function at [4] which processes both ASSOC packets and DATA packets:
00023d10 int32_t ENTER_ASSOC_OR_DATA_CENTER(struct tmp_client* tmpcli, int32_t* datasize)
00023d28 uint32_t status = tmpcli->status
00023d28
00023d30 if (status s>= 1)
00023d38 if (status s<= 2)
00023d58 log("tmpdServer.c:1087", "ENTER ASSOC CENTER")
00023d64 return assoc_parse_set_tmpcli_status(tmpcli) // [5]
00023d64
00023d40 if (status == 3)
00023d8c return decode_data_pkt(tmpcli, datasize, log("tmpdServer.c:1091", "ENTER DATA CENTER")) // [6]
00023d8c
00023d94 return 0
Since our status is still currently 0x1, we enter the function at [5]:
00023610 int32_t assoc_parse_set_tmpcli_status(struct tmp_client* tmpcli)
00023624 int32_t var_18 = 0
0002362c int32_t var_1c = 0
00023634 int32_t var_20 = 0
0002363c uint32_t authHdrLen = tmpcli->authHdrLen
0002363c
0002364c if (tmpcli == 0)
00023650 return 0xffffffff
00023650
00023674 memset(s: tmpcli->out, c: 0, n: 0x4000)
0002367c struct TdpPkt* out = tmpcli->out
00023694 out->rbuf_start_ver = 1
000236a8 out->reserved_0x0_or_0xf0 = 0
000236b4 out->opcode:1.b = 0
000236c4 struct assoc_pkt* assoc = tmpcli->tdpin + authHdrLen
000236c4
000236d8 if (tmpcli->status == 1)
000236e8 if (zx.d(assoc->assoc_opcode) == 1 && is_zero(assoc->is_a_request) != 0)
00023718 log("tmpdServer.c:897", "GET TMP ASSOC REQUEST PKT")
00023724 out->opcode.b = TDP_REPEATER_SG_0x2
00023754 int32_t result = send(__fd: tmpcli->fd, __buf: tmpcli->out, __n: 4, __flags: 0)
00023754
00023768 if (4 != result)
00023770 result = 0xffffffff
00023770
0002377c set_tmpcli_status(tmpcli, status: 2)
000238ac return result
Without much detail, for ASSOC packets, there’s only rudimentary checks, and a packet containing just \x00\x00\x01\x00 will allow us to get past the first step. This sets our status to 0x2, and if we send another packet, we will hit a different code branch within assoc_parse_set_tmpcli_status:
000237fc if (zx.d(assoc->assoc_opcode) == 2 && is_zero(assoc->is_a_request) != 0
000237fc && is_one_and_zero(assoc->needs_1, assoc->needs_0) != 0)
00023854 log("tmpdServer.c:920", "GET TMP ASSOC ACCEPT PKT")
00023860 set_tmpcli_status(tmpcli, status: 3)
00023868 return 0
Likewise for a status of 0x2, the checks are still rudimentary and a packet of \x01\x00\x02\x00 allows us to reach the actual data packet processing within decode_data_pkt(tmpcli, datasize, log("tmpdServer.c:1091", "ENTER DATA CENTER") which requires us to have a status of 0x3 [6]:
000238bc int32_t decode_data_pkt(struct tmp_client* tmpcli, uint32_t datasize, int32_t arg3)
000238ec uint32_t authHdrLen = tmpcli->authHdrLen
000238f8 int32_t out_function_cb = 0
000238fc void* tmp = tmpcli
000238fc
00023904 if (tmp != 0)
0002390c tmp = tmpcli->status
0002390c
00023914 if (tmp == 3)
0002391c arg3 = 0x4000
00023920 tmp = datasize
00023920
00023928 if (0x4000 s>= tmp)
00023954 struct TmpDataPkt* in = tmpcli->tdpin + authHdrLen
00023974 int32_t decode_ret = check_tdp_data_in(in: tmpcli->tdpin + authHdrLen, inpsize: datasize) // [7]
Starting out, assuming we’re within the 0x4000 size limit, we hit the first check on our data packet at [7]. Since the format for DATA packets is not the same as ASSOC packets, an example DATA HELLO packet is given below:
pkt3 = b""
#pkt3 += b"\x00" # opcode. \xfe => needs auth len/auth header
#pkt3 += b"\x00" # serNameLen
#pkt3 += b"\x00" # service mask
# auth stuff would go here
# need at least 0x10 bytes for datapkt
pkt3 += b"\x01" # Need 0x1
pkt3 += b"\x00" # Need 0x0
pkt3 += b"\x04" # opcode # opcode != 0x5 => len 0x0
pkt3 += b"\x00" # idk 0x3
pkt3 += b"\x00\x00" # datasize BE, max 0x3ff0 (doesn't include 0x10 hdr)
pkt3 += b"\x00" # flags # opcode != 0x5 => 0x0
pkt3 += b"\x00" # errcode
pkt3 += b"\x00\x00\x00\x00" # tdp_sn
pkt3 += b"\x00\x00\x00\x00" # checksum
Continuing into check_tdp_data_in:
00022e0c int32_t check_tdp_data_in(struct TmpDataPkt* in, int32_t inpsize)
// [...]
00022e34
00022e68 if (is_one_and_zero(in->need_0x1, in->need_0x0) != 0 && is_zero(in->need_0x0) != 0) // [8]
00022e9c uint32_t r0_5 = ntohl(in->checksum)
00022eb8 in->checksum = htonl(0x5a6b7c8d)
00022ec4 int32_t r0_8 = do_checksoum(in, inpsize)
00022ec4
00022ed8 if (r0_8 != r0_5) // [9]
00022ef4 log("tmpdServer.c:681", "TMP curCheckSum=%x; newCheckSum=%x", r0_5, r0_8, inpsize, in)
00022ef8 return 3
00022ef8
00022f08 in->checksum = r0_5
00022f28 in->datalen = ntohs(in->datalen)
00022f28
00022f60 if (zx.d(in->opcode) == 5 && zx.d(in->datalen) != 0 && zx.d(in->datalen) + 0x10 != inpsize)
00022f78 log("tmpdServer.c:694", "TMP DATA TRANSFER PKT LENGTH ERROR %d", inpsize, inpsize, inpsize, in)
00022f7c return 4
00022f7c
00022fa0 if (zx.d(in->opcode) != 5 && zx.d(in->datalen) != 0)
00022fc0 log("tmpdServer.c:701", "TMP PKT LENGTH ERROR %x", zx.d(in->datalen))
00022fc4 return 4
00022fc4
00022fe4 if (is_zero_(in->flag) == 0) // [10]
00023004 log("tmpdServer.c:708", "TMP FLAG ERROR %x", zx.d(in->flag))
00023008 return 5
00023008
00023028 in->tmp_sn = ntohl(in->tmp_sn)
00023048 log("tmpdServer.c:716", "TMP SN = %x", in->tmp_sn)
0002304c return 0
0002304c
00022e88 return 1
The main things to be aware of here are that we need 0x1 and 0x0 as our first two bytes to pass the check at [8], we need to have a correct CRC to pass the check at [9], and we also need to have our flag field set to 0x0 to pass the check at [10]. Assuming that’s all good, we return back up to decode_data_pkt:
00023974 int32_t decode_ret = check_tdp_data_in(in: tmpcli->tdpin + authHdrLen, inpsize: datasize) // [7]
00023984 if (decode_ret s< 0)
00023998 log("tmpdServer.c:974", "TMP DECODE PKT error!!!")
0002399c return 0xffffffff
0002399c
000239c0 memset(s: tmpcli->out, c: 0, n: 0x4000)
000239c8 struct TdpPkt* out = tmpcli->out
000239d8 int32_t build_an_outpkt_flag
000239d8
000239d8 if (decode_ret == 0)
00023a2c uint32_t opcode = zx.d(in->opcode)
00023a2c
00023a34 if (opcode == 5)
00023ad0 log("tmpdServer.c:1003", "TMP RECV DATA PKT")
00023ae8 tmpcli->sysinfo = get_sysinfo() + 0x3c
00023af8 out->tdp_sn = in->tmp_sn
00023b1c out->errcode = handle_data_recv_(tmpcli, datasize, out_cb: &out_function_cb) & 0xff // [11]
00023b1c
00023b2c if (zx.d(out->errcode) != 0)
00023b8c log("tmpdServer.c:1018", "TMP DISPATCH PKT ERROR")
00023b98 out->opcode.b = TDP_IDK_0x6
00023ba4 out->size_of_data = 0
00023bac build_an_outpkt_flag = 0
00023b2c else
00023b4c log("tmpdServer.c:1011", "TMP DISPATCH PKT OK %d", tmpcli->outpkt_size)
00023b58 out->opcode.b = TMP_DATA_TRANSFER
00023b6c out->size_of_data = (tmpcli->outpkt_size).w & 0xffff
00023b74 build_an_outpkt_flag = 1
00023a34 else if (opcode s<= 5)
00023a44 if (opcode != 4)
00023c20 log("tmpdServer.c:1044", "TMP RECV ASSOC PKT and CLOSE SOCK")
00023c24 return 0xffffffff
00023c24
00023a70 log("tmpdServer.c:994", "TMP RECV HELLO PKT") // [12]
00023a88 tmpcli->sysinfo = get_sysinfo() + 0x3c
00023a94 out->opcode.b = TDP_ATTACH_MASTER
00023aa0 out->size_of_data = 0
00023ab0 out->tdp_sn = in->tmp_sn
00023ab8 build_an_outpkt_flag = 1
00023a3c else if (opcode == 6)
00023bc4 log("tmpdServer.c:1026", "TMP RECV BYE PKT")
00023bd0 out->opcode.b = TDP_IDK_0x6
00023bdc out->size_of_data = 0
00023be4 build_an_outpkt_flag = 0
00023a50 else
00023a58 if (opcode != 0xff)
00023c20 log("tmpdServer.c:1044", "TMP RECV ASSOC PKT and CLOSE SOCK")
00023c24 return 0xffffffff
00023c24
00023bfc log("tmpdServer.c:1034", "TMP RECV RENEGOTIATE PKT, SEND BYE PKT TO APP")
00023c00 send_bye_pkt_to_app()
00023c08 build_an_outpkt_flag = 0
000239d8 else
000239f0 log("tmpdServer.c:983", "TMP PKT error NO. %d", decode_ret, decode_ret, datasize, tmpcli)
000239fc out->opcode.b = TDP_IDK_0x6
00023a0c out->errcode = decode_ret.b & 0xff
00023a18 out->size_of_data = 0
00023a20 build_an_outpkt_flag = 0
Before we can actually send DATA packets with, well, data, we first have to send a dummy DATA HELLO packet with an opcode of 0x5, such that we hit the code path at [12]. After this has happened, we can now send a DATA packet of similar format to hit the branch at [11] and then enter handle_data_recv:
00023e4c int32_t handle_data_recv_(struct tmp_client* tmpcli, int32_t datasize, void* out_cb)
00023e68 char var_25 = 2
00023e90 uint32_t authHdrLen = tmpcli->authHdrLen
00023e9c int32_t var_24_1
00023e9c __builtin_memset(dest: &var_24_1, ch: 0, count: 0x14)
00023e9c
00023ea8 if (tmpcli == 0)
00023eac return 0xffffffff
00023eac
00023ec8 // okay, finally used lol
00023ec8 char serviceType = tmpcli->tdpin->sername[0xd + authHdrLen]
00023ec8
// [...]
000245a0
000245a0 while (true)
000245b8 psess = (&psess1)[x].sesfunc
000245b8
000245c0 if (psess == 0)
000245c0 break
000245c0
000242e4 log("tmpdServer.c:1193", "serviceMask is 0x%x, try to login serviceType 0x%x", tmpcli->serviceMask,
000242e4 zx.d((&psess1)[x].servicetype))
00024308 log("tmpdServer.c:1196", "serviceType is %d, pSession->serviceType is %d", zx.d(serviceType), tmpcli->serviceType)
0002432c log("tmpdServer.c:1197", "User Name is %s", &tmpcli->tdpin->sername)
0002432c
00024370 if (check_service_type_and_mask(servicemask: tmpcli->serviceMask, serviceytpe: (&psess1)[x].servicetype) != 0)
000243d8 if (tmpcli->serviceType == 0 && zx.d((&psess1)[x].servicetype) == zx.d(serviceType))
00024418 int32_t r0_16 = get_cli_by_ST(ST: serviceType)
0002444c int32_t idk = (&psess1)[x].idk
0002444c
00024454 if (r0_16 u>= idk)
0002447c log("tmpdServer.c:1216", "TMP_HDR_ERR_BT_EXCEED", &psess1, idk)
00024480 return 7
00024480
00024464 tmpcli->serviceType = zx.d(serviceType)
00024464
00024494 // 0x1 or 0xf1
00024494 if (tmpcli->serviceType != 0 && tmpcli->serviceType == zx.d((&psess1)[x].servicetype))
0002452c uint32_t r0_20 = // actually call psess func
0002452c (&psess1)[x].sesfunc(&tmpcli->tdpin->sername[0xd + authHdrLen], datasize - 0x10, &tmpcli->out->payload, out_cb)
0002452c
0002453c if (r0_20 s< 0)
00024550 log("tmpdServer.c:1231", "TMP_HDR_ERR_BT")
00024554 return 6
00024554
00024564 tmpcli->outpkt_size = r0_20
00024578 log("tmpdServer.c:1235", "TMP_HDR_ERR_NONE")
0002457c return 0
To summarize the above code without getting too much into detail - based on the uint8_t service_type byte of our input packet, the code will walk a corresponding set of structures to find the opcode that we’re sending and then calling that specific function. An example packet calling the service_type opcode of 0x421 is given below:
#######################################
pkt4 = b""
pkt4 += b"\x01" # Need 0x1
pkt4 += b"\x00" # Need 0x0
pkt4 += b"\x05" # opcode # opcode != 0x5 => len 0x0
pkt4 += b"\x00" # idk 0x3
pkt4 += b"\x00\x00" # datasize BE, max 0x3ff0 (doesn't include 0x10 hdr).
pkt4 += b"\x00" # flags # opcode != 0x5 => 0x0
pkt4 += b"\x00" # errcode?
pkt4 += b"\x00\x00\x00\x00" # tdp_sn??
pkt4 += b"\x00\x00\x00\x00" # checksum
# start of psession layer
pkt4 += b"\x01" # service type (\x01 or \xf1)
pkt4 += b"\x01" # version
psess_opcode = 0x421
pkt4 += struct.pack(">H",psess_opcode)
// Begin actual opcode data
We’re almost to the point of talking about specific opcodes, but one more digression must be allowed. Every opcode follows the same overall code flow - our opcode-specific packet data is read in either as a JSON string which is converted to cjson objects or it’s read in via a basic TLV format. This parsed data is potentially written to specific offsets within a size 0x8000 static buffer, with each of the offsets and resulting data formats being opcode specific. This size 0x8000 buffer is then passed to luci wrappers around TP-Link specific lua bytecode binaries which all call their own specific functions. After the lua bytecode binary has finished, the output data is written back into this 0x8000 sized buffer and, assuming no errors have occurred, data is copied to the 0x4000 sized struct TdpPkt* out member of our client session, which is then sent back to us. Unfortunately, this basic overview is extremely important for understanding any of the tmpServer vulnerabilities, so a quick summary of the summary:
[recv()] -> [0x4000 client session input data] -> [cjson or TLV parsing] -> [opcode specific 0x8000 buffer struct] -> [luci form data] -> [lua bytecode] -> [same 0x8000 buffer response data] -> [ 0x4000 client session output buffer ] -> [send()]
Continuing on, finally we can start talking about vulnerability-specific code. Let us examine the code handling opcode 0x442:
00030dd0 int32_t opcode_0x442(struct psess_farg* inp)
00030df4 int16_t var_1a_1
00030df4 __builtin_memset(dest: &var_1a_1, ch: 0, count: 0x12)
00030e04 int32_t var_20 = 0
00030e0c int32_t var_24 = 0
00030e0c
00030e20 if (inp == 0)
00030e24 return 0xffffffff
00030e24
00030e38 void* rptr = &inp->payload[8] // [13]
00030e48 int32_t totelen = inp->payload_len - 8 // [14]
00030e48
00030f3c while (true)
00030f3c if (totelen s<= 0)
00030f40 return 0
00030f40
00030e54 uint32_t tag = read_ushort_LE(rptr)
00030e60 int16_t mlen = read_aligned_ushort_p8(rptr)
00030e78 int32_t var_14_1
00030e78
00030e78 if (tag == 0x403)
00030e8c clear_bigstruct()
00030ea0 var_14_1 = iterloop_tlv_write_bigstruct(rptr, dst: &bigstruct) // [15]
00030e78 else if (tag == 0x442)
00030ec0 clear_bigstruct()
00030ed4 var_14_1 = write_tlv_404_or_405(src: rptr, dst: &bigstruct)
00030ed4
00030f08 if (var_14_1 == 0xffffffff)
00030f08 break
00030f08
00030f20 totelen -= zx.d(mlen)
00030f30 rptr += zx.d(mlen)
00030f30
00030f0c return 0xffffffff
For this opcode our input is treated as a set of Type-Length-Value structs. At [13], the first two bytes are read big endian as the type, and at [14] the next two bytes are read in as the size with 0x4 added, aligned to 0x4 bytes, and then another 0x4 added. Based on the type value, we parse the value differently, but for this opcode we choose TLV type 0x403 and enter the function at [15]:
00030404 int32_t iterloop_tlv_write_bigstruct(char* rptr, char* dst)
0003042c int16_t var_1e_1
0003042c __builtin_memset(dest: &var_1e_1, ch: 0, count: 0x16)
0003042c
0003045c if (rptr == 0 || dst == 0)
00030460 return 0xffffffff
00030460
00030470 void* inp = &rptr[4]
00030480 int32_t totalbytes = read_aligned_ushort_p8(rptr)
00030480
000305c4 while (true)
000305c4 if (totalbytes s<= 0)
000305c8 return 0
000305c8
0003048c uint32_t r0_3 = read_ushort_LE(inp)
00030498 int16_t r0_5 = read_aligned_ushort_p8(inp)
000304b0 int32_t err
000304b0
000304b0 if (r0_3 == 0x405)
00030520 err = safe_copy_varlen(inp, out: &dst[0x11], len: 0x12)
000304b0 else
000304d8 if (r0_3 == 0x446)
00030540 err = safe_copy_varlen(inp, out: &dst[0x23], len: 0x10)
000304d8 else if (r0_3 == 0x448) // [16]
00030548 int32_t i_1
00030548 int32_t i = i_1
00030550 i_1 = i + 1
00030580 err = safe_copy_varlen(inp, out: &dst[i * 0x1f + 0x33], len: 0x1f) // [17]
00030580
000304c8 if (r0_3 == 0x404)
00030500 err = safe_copy_varlen(inp, out: dst, len: 0x11)
00030500
00030590 if (err == 0xffffffff)
00030590 break
00030590
000305a8 totalbytes -= zx.d(r0_5)
000305b8 inp += zx.d(r0_5)
000305b8
00030594 return 0xffffffff
Within this subsequent function we find another looping TLV read that handles TLVs the exact same. In this function if we provide an opcode of 0x448 we hit the branch at [16], which writes up to 0x1f bytes into an offset determined by how many times we’ve sent the opcode 0x448 [17]. While this might not initially seem to be a vulnerability since our input buffer is 0x4000 max size and the destination of the write is our size 0x8000 big buffer, there’s a crucial detail within the functions that copy TLV data that we must look at:
0001ed4c int32_t safe_copy_varlen(void* inp, char* out, uint32_t len)
0001ed68 int32_t var_c = 0
0001ed68
0001ed74 if (inp == 0)
0001ed78 return 0xffffffff
0001ed78
0001ed8c memset(s: out, c: 0, n: len)
0001ed8c
0001edb0 if (copy_varlen_buf(src: inp, dst: out, dstlen: len - 1) s< 0) // [18]
0001edb4 return 0xffffffff
0001edb4
0001edd8 out[read_p2_ushort_LE(inp)] = 0
0001eddc return 0
Again, nothing out of the ordinary, so lets look within copy_varlen_buf:
0001ecbc int32_t copy_varlen_buf(void* src, void* dst, uint32_t dstlen)
0001ecdc if (src == 0)
0001ece0 return 0xffffffff
0001ece0
0001ecfc if (read_p2_ushort_LE(src) u> dstlen)
0001ed00 return 0xffffffff
0001ed00
0001ed14 memset(s: dst, c: 0, n: dstlen)
0001ed38 memcpy(dest: dst, src: src + 4, n: read_p2_ushort_LE(src))
0001ed3c return 0
While it might not be obvious, the issue lies in the fact that both the copy_varlen_buf and the parent safe_copy_varlen functions do not actually return the amount of bytes that were read in from the input buffer. One might expect that reading a variable length buffer would cause the write and read pointers to both advance the same amount within the destination and source buffers, however if we pass TLVs of length 0x0, then the write at [17] still occurs at an offset that increases by 0x1f every iteration. As such, we can advance the write pointer 0x1f bytes for every 0x8 bytes of TLV that we provide, thus easily allowing us to advance the write pointer outside of the bounds of our size 0x8000 bigstruct, resulting in an out-of-bounds write in other global variables, potentially leading to code execution.
***********************************************************************************
r0 : 0x83ff3 | r9 : 0x0
r1 : 0x0 | r10 : 0x1
r2 : 0x1f | r11[S] : 0x7e821a84
r3 : 0x84012 | r12[X] : 0x74288
r4[X] : 0x383a8 | sp[S] : 0x7e821a68
r5[H] : 0x1c05010 | lr[X] : 0x1ed90
r6 : 0x1 | pc[L] : 0x76fbb82c <memset+20>
r7[X] : 0x255ec | cpsr : 0x20000010
r8 : 0x20 | fpscr : <unavailable>
tpidruro : <unavailable> |
***********************************************************************************
0x76fbb81c <memset+4>: bxeq lr
0x76fbb820 <memset+8>: add r3, r0, r2
0x76fbb824 <memset+12>: cmp r2, #2
0x76fbb828 <memset+16>: uxtb r1, r1
=> 0x76fbb82c <memset+20>: strb r1, [r3, #-1]
0x76fbb830 <memset+24>: strb r1, [r0]
0x76fbb834 <memset+28>: bxls lr
0x76fbb838 <memset+32>: strb r1, [r3, #-2]
0x76fbb83c <memset+36>: cmp r2, #6
***********************************************************************************
#0 0x76fbb82c in memset () from /home/thiefy/boop/tplink/ax3000/dumped_fw/newest_1.3.1/lib/ld-musl-arm.so.1
#1 0x0001ed90: (0x10000 0x64000 0x54000 0x0 r-xp /usr/bin/tmpServer)
***********************************************************************************
[^_^] 2025_10_09_09_12_16_069311 - Got a crash! SIGSEGV, 0x76fbb82c
[-.-]> bt
#0 0x76fbb82c in memset () from /home/thiefy/boop/tplink/ax3000/dumped_fw/newest_1.3.1/lib/ld-musl-arm.so.1
#1 0x0001ed90 in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
[o.o]> frame 1
#1 0x0001ed90 in ?? ()
Vendor advisory: https://www.tp-link.com/us/support/faq/4943/
2025-10-28 - Vendor Disclosure
2026-02-03 - Vendor Patch Release
2026-03-16 - Public Release
Discovered by Lilith >_> of Cisco Talos.