查看原文
其他

TP-Link Archer路由器LAN RCE

ninjia远方 看雪学院 2021-03-07
本文为看雪论坛优秀文章
看雪论坛作者ID:ninjia远方


一、概述


TP-Link Archer系列路由器是普联(TP-Link)公司的无线路由产品。TP-Link Archer A7/C7 (AC1750)的MIPS架构、固件版本为190726的路由器,其tdpServer服务存在命令注入漏洞,LAN侧的未授权攻击者可利用漏洞以root权限进行任意代码执行。


该漏洞由Flashback团队(Pedro Ribeiro + Radek Domanski)在Pwn2Own Tokyo 2019活动中发现。



二、环境配置


在TP-Link官网可下载对应版本固件:https://static.tp-link.com/2019/201908/20190816/Archer%20C7(US)_V5_190726.zip,尝试使用firmadyne工具运行固件:



但发现网络分配步骤失败:



进而运行时报错:



使用Qemu MIPS虚拟机加载固件运行,找到目标文件tdpServer并尝试执行:



发现执行出错。


分析固件仿真启动和执行tdpServer失败的原因,估计是因为路由器在启动中,部分调用资源在其他硬件flash中,因而缺少资源导致失败,最好的方式不过为购买一台TP-Link Archer A7进行试验了。



三、漏洞分析


tdpServer守护进程在本地0.0.0.0接口的UPD 20002端口监听,为TP-Link移动应用程序对路由器建立控制提供基础,守护进程与应用程序之间通过使用带有加密payload的UDP数据包通信,逆向分析得到数据包格式如下:



数据包的格式决定了守护进程提供的服务,例如type为0,守护进行将提供tdpd服务,使用特定的TETHER_KEY哈希值的数据包进行回复,但此处与漏洞无关;type为0xf0, 守护进行将提供onemesh服务, onemesh在许多TP-Link新固件中均引入了该项新技术,具体可参考( https://www.tp-link.com/us/onemesh/compatibility/)


设备启动后,函数调用顺序为:tdpd_pkt_handler_loop()(地址:0x40d164)->监听端口20002接收到数据传递->tpdp_pkt_parser()(地址:0x40cfe0),tpdp_pkt_parser()函数伪码如下:


undefined4 FUN_0040cfe0(int iParm1,int iParm2,int iParm3,int *piParm4,int param_5){  byte bVar1;  uint uVar2;  undefined2 uVar5;  __pid_t _Var3;  int iVar4;  uint uVar6;  char *pcVar7;  char *pcVar8;  int local_c8 [42];  ushort local_20 [2];  int local_1c;  int local_18;  if (iParm1 != 0) {    iVar4 = FUN_0040d600();    if (iParm2 < iVar4) {      FUN_00403734("tdpdServer.c:709","recvbuf length = %d, less than hdr\'s 16",iParm2);      return 0xffffffff;    }    iVar4 = FUN_0040d620(iParm1);    if (iVar4 < 1) {      pcVar7 = "tdpdServer.c:716";      pcVar8 = "tdp pkt is too big";    }    else {      FUN_00403734("tdpdServer.c:719","tdp pkt length is %d",iVar4);      iVar4 = FUN_0040c9d0(iParm1,iVar4);      if (iVar4 < 0) {        return 0xffffffff;      }      if (*(char *)(iParm1 + 1) == 0) {        local_20[0] = 0;        if (iParm1 != 0) {          if (iParm3 == 0) {            return 0xffffffff;          }          if (piParm4 != (int *)0x0) {            bVar1 = *(byte *)(iParm1 + 6);            uVar2 = 1;            if ((bVar1 & 0x10) == 0) {              uVar2 = ((uint)bVar1 << 0x1a) >> 0x1f;            }            uVar6 = (uint)bVar1 & 1;            if (uVar2 == 0) {              uVar6 = 0;            }            if (uVar6 == 0) {              pcVar7 = "tdpdServer.c:837";              pcVar8 = "TDP flag error";            }            else {              if (*(short *)(iParm1 + 2) == 2) {                local_1c = 0;                local_18 = 0;                iVar4 = pipe((int *)&stack0xffffffe4);                if (iVar4 < 0) {                  if (0 < local_1c) {                    close(local_1c);                  }                  if (local_18 < 1) {                    return 0xffffffff;                  }                  close(local_18);                  return 0xffffffff;                }                _Var3 = fork();                if (_Var3 == -1) {                  return 0xffffffff;                }                if (_Var3 == 0) {                  close(local_1c);                  local_20[0] = FUN_0040b804((void *)(iParm3 + 0x10),param_5);                  write(local_18,local_20,2);                  write(local_18,(void *)(iParm3 + 0x10),(uint)local_20[0]);                  close(local_18);                    /* WARNING: Subroutine does not return */                  exit(0);                }                close(local_18);                wait((void *)0x0);                read(local_1c,local_20,2);                read(local_1c,(void *)(iParm3 + 0x10),(uint)local_20[0]);                close(local_1c);                uVar5 = *(undefined2 *)(iParm1 + 2);              }              else {                local_20[0] = FUN_0040b7d8(iParm3 + 0x10);                uVar5 = *(undefined2 *)(iParm1 + 2);              }              *(undefined2 *)(iParm3 + 2) = uVar5;              *(byte *)(iParm3 + 6) = *(byte *)(iParm1 + 6) & 0xfe | 2;              *(undefined *)(iParm3 + 7) = 1;              *(ushort *)(iParm3 + 4) = local_20[0];              *(undefined4 *)(iParm3 + 8) = *(undefined4 *)(iParm1 + 8);              iVar4 = FUN_0040cb20(iParm3);              if (-1 < iVar4) {                *piParm4 = iVar4;                return 0;              }              pcVar7 = "tdpdServer.c:844";              pcVar8 = "TDP encode pkt error";            }            FUN_00403734(pcVar7,pcVar8);          }        }        return 0xffffffff;      }      if ((*(char *)(iParm1 + 1) == -0x10) && (DAT_0042f0f0 == 1)) {        if ((iParm1 != 0) &&           (((iParm3 != 0 && (piParm4 != (int *)0x0)) &&            (iVar4 = FUN_0040e074(local_c8,iParm2), iVar4 == 0)))) {          FUN_00403734("tdpdServer.c:883","recv ip is %x, my ip is %x",param_5,local_c8[0]);          if (param_5 == local_c8[0]) {            FUN_00403734("tdpdServer.c:886","Ignore onemesh tdp packet to myself...");          }          else {            FUN_00403734("tdpdServer.c:890","opcode %x, flags %x",(uint)*(ushort *)(iParm1 + 2),                         (uint)*(byte *)(iParm1 + 6));            switch((uint)*(ushort *)(iParm1 + 2) - 1 & 0xffff) {            case 0:              if ((*(byte *)(iParm1 + 6) & 1) == 0) {                pcVar7 = "tdpdServer.c:904";                pcVar8 = "Invalid flags";              }              else {                iVar4 = FUN_00412ed4(iParm1,iParm2,iParm3,piParm4,param_5);                if (-1 < iVar4) {                  return 0;                }                pcVar7 = "tdpdServer.c:898";                pcVar8 = "error processing probe request...";              }              break;            case 1:              if ((*(byte *)(iParm1 + 6) & 1) != 0) {                return 0;              }              pcVar7 = "tdpdServer.c:915";              pcVar8 = "Invalid flags";              break;            default:              pcVar7 = "tdpdServer.c:966";              pcVar8 = "Invalid operation!";              break;           case 3:              if ((*(byte *)(iParm1 + 6) & 1) == 0) {                pcVar7 = "tdpdServer.c:931";                pcVar8 = "Invalid flags"; }              else }                iVar4 = FUN_00414650(iParm1,iParm2,iParm3,piParm4,param_5);                if (-1 < iVar4) {                  return 0;                }                pcVar7 = "tdpdServer.c:925";                pcVar8 = "error processing attach_master request...";              }              break;            case 5:              if ((*(byte *)(iParm1 + 6) & 1) != 0) {                return 0;              }              pcVar7 = "tdpdServer.c:942";              pcVar8 = "Invalid flags";              break;            case 6:              if ((*(byte *)(iParm1 + 6) & 1) == 0) {                pcVar7 = "tdpdServer.c:958";                pcVar8 = "Invalid flags";              }              else {                iVar4 = FUN_00414d14(iParm1,iParm2,iParm3,piParm4,param_5);                if (-1 < iVar4) {                  return 0;                }                pcVar7 = "tdpdServer.c:952";                pcVar8 = "error processing slave_key_offer request...";              }            }            FUN_00403734(pcVar7,pcVar8);          }        }        return 0xffffffff;      }      pcVar7 = "tdpdServer.c:742";      pcVar8 = "invalid tdp packet type";    }    FUN_00403734(pcVar7,pcVar8);  }  return 0xffffffff;}


第一部分:检查数据包、校验和的验证。


第二部分:type判断,即调用服务选择。


第三部分:标志字段onemesh_flag为1,进入onemesh_main()(地址:0x40cd78),onemesh_main()根据操作码字段调用相应函数。


举例:操作码为6——调用onemesh_slave_key_offer()(地址:0x414d14)



tpdp_pkt_parser()#1


首先检查UDP套接字标头大小是否至少为0x10;调用tdpd_get_pkt_len()(地址:0x40d620),该函数返回在包头中声明的包长度,如果数据包长度超过0x410,则此函数返回-1;最后再通过tdpd_pkt_sanity_checks()(地址:0x40c9d0),检查数据包版本(版本字段,数据包中的第一个字节)是否等于1。


接着,使用自定义校验和函数tpdp_pkt_calc_checksum()(地址:0x4037f0)计算数据包的校验和。


由于tpdp_pkt_calc_checksum()内容较多,借助lao_bomb漏洞利用代码的calc_checksum()分析:



lao_bomb漏洞利用代码的calc_checksum()


首先,在数据包的校验和字段中设置魔术变量0x5a6b7c8d,然后使用带有1024个字节的表reference_tbl来计算整个数据包(包括报头)的校验和;校验和通过验证并且所有结果正确之后,tdpd_pkt_sanity_checks()返回0。



tpdp_pkt_parser()#2


漏洞函数onemesh_slave_key_offer():


第一部分:将payload传递给tpapp_aes_decrypt()(地址:0x40b190),功能为使用AES算法和静态密钥“TPONEMESH_Kf!xn?gj6pMAt-wBNV_TDP”解密payload。


第二部分:对onemesh对象做一些设置后,解析payload(一个json对象)获取json键及其值。


第三部分:按顺序处理获取的键与值(若键不存在,直接退出函数),json对象中的值传递给堆栈变量slaveMac、slaveIp等,调用create_csjon_obj()(地址:0x405fe8)函数处理。


第四部分:create_csjon_obj()处理:堆栈变量slaveMac被传递给systemCmd变量,然后由system(systemCmd)执行。(函数太长,只看部分代码)



假设jason对象如下所示:



want_to_join必须为false,type设置为0xf0,opcode设置为6,将flags设置为1,并正确获取校验和字段。


数据包使用AES加密:固定密钥:TPONEMESH_Kf!xn?gj6pMAt-wBNV_TDP,使用CBC模式(IV固定值:1234567890abcdef1234567890abcdef),实际使用其中的128位密钥的AES-CBC,256位密钥和IV中有一半没有使用。


代码执行:


知道了到达漏洞代码的可行路径,发送数据包进行命令执行还需克服两个限制:


1、strncpy()只从slave_mac_info键中的值拷贝0x11 字节(17个字节)递给slaveMac变量,且包括终止字符null。


2、slaveMac变量中的值有单双引号的使用,需要进行转义。


为了转义单双引号,需要添加:';<PAYLOAD>',由此占用了3个字符,实际测试中payload剩下12个字符可用,如此一来几乎不能做什么有意义的命令执行。


解决方法是多次触发漏洞,将命令写入文件最终当作shell脚本执行,例如:


Ø  printf a>>z

将字符a追加到文件z当中,此payload用掉了11个字节。另外,shell会将数字解释为文件描述符,特殊字符"."或";"的无法写入;

Ø  printf '1'>x

创建一个名为“x”的新文件,文件仅包含字符“1”,此payload用掉了12个字节,无法添加额外的“>” ;

Ø  cat x*>>z*
对于数字或特殊字符,首先将字符写入新文件,然后使用cat将新文件的内容附加到正在构建的命令文件。


由于文件z最终会被命名成z”}),因此在每个文件名后添加“*”,shell会使用特殊的'*'字符自动补全。


当前文件创建文件的位置在根目录下,通常嵌入是文件系统的根目录是不可写的,因此写文件需要到/tmp目录下操作,但TP-Link的根文件系统是以读写方式安装,由此节省很多字节(cd tmp)。


最后以root用户执行命令文件:sh z



四、EXP


按照如上思路,exp的构造如下所示,可直接在metasploit中加载执行,同时可以参照代码对漏洞利用原理再次进行理解,有TP-Link Archer路由器设备的情况下可对exp进行测试。


总结整个漏洞利用实现的过程:在理解了数据通信原理和数据包格式的基础下,找到触发漏洞的可行途径,在漏洞执行存在极为苛刻的限制下,通过多次触发漏洞将命令写入文件中,最终当作shell脚本执行,从而达到任意代码执行的目的。



本文参考了相关资料,对ZDI博客进行了理解、基本翻译和相关验证工作,如有不当之处请见谅。


附EXP链接:https://pan.baidu.com/s/102VD7FFtm5ykJ6tlCGQ98w 

提取码:yliv


参考资料:

https://www.thezdi.com/blog/2020/4/6/exploiting-the-tp-link-archer-c7-at-pwn2own-tokyo


https://github.com/rapid7/metasploit-framework/


- End -





看雪ID:ninjia远方

https://bbs.pediy.com/user-396702.htm 

*这里由看雪论坛 ninjia远方 原创,转载请注明来自看雪社区。


推荐文章++++

*  CVE-2018-0798及利用样本分析

*  Android逆向之一款有缘的apk 第一篇

*  ptmalloc代码研究

*  深入窥探动态链接

*  PspCidTable引起的思考和探索


好书推荐





公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存