CVE-2021-4045
CVE-2021-4045 是 TP-Link Tapo C200 摄像头中发现的 命令注入 漏洞,允许攻击者 以 root 权限完全控制设备。该漏洞影响 所有早于 1.1.16 Build 211209 Rel. 37726N 的固件版本。
🔗 INCIBE 官方公告: https://www.incibe.es/incibe-cert/alerta-temprana/vulnerabilidades/cve-2021-4045 (请替换为真实链接)
🔧 推荐解决方案:将固件更新至 1.1.16 或更高版本。
$ nmap -sV -p- 192.168.1.81
**结果**:```
PORT STATE SERVICE
443/tcp open https
554/tcp open rtsp
2020/tcp open xinupageserver
8800/tcp open sunwebadmin
可以看到,设备有一些有趣的开放端口。我首先测试了443端口。虽然nmap明确表明它使用https,但在初始扫描时,我忽略了这一点,花了很长时间以为443端口使用的是http。因此,我只尝试了http://192.168.1.81:443而不是https://192.168.1.81:443,所以只得到了400响应。正如我在引言中所说,这个过程充满了失误。至于其他端口,上面运行的服务对我来说完全陌生,没有找到任何明确的信息。那时,我已经没有已知的选项了,所以是时候进一步深入研究了。
----[ 获取Shell ]-------------------------------
在购买摄像头之前,我在网上搜索了关于该设备的先前研究,幸运的是,我找到了这个GitHub仓库,人们在那里合作进行逆向工程。其中一个问题解释了如何通过UART端口获得控制台访问权限,这在当时我完全不知道怎么做。于是我学习了基础知识,并购买了一个USB转TTL转换器进行连接。
借助提到的问题,我用刀和螺丝刀打开了设备,并迅速找到了UART。经过几次尝试和足够的耐心,我终于成功地将一些线焊接到焊盘上。
然后,该检查焊接是否足够好以进行数据传输了。我将线连接到USB适配器,注意UART的Rx连接到适配器的Tx,反之亦然,然后将适配器连接到我的电脑。再次感谢提到的问题,我知道串行连接的波特率是57600,于是我运行了:
$ sudo screen /dev/tty.usbserial-0001 57600
其中'/dev/tty.usbserial-0001'是适配器连接的USB端口,同时也为设备供电。我立即开始接收数据,太好了。
然而,我仍然没有控制台访问权限。我接收到的只是设备的启动序列,实际上是U-Boot引导加载程序。它看起来像这样:
U-Boot 2014.01-v1.2 (Jul 16 2021 - 18:41:10)
Board: IPCAM RTS3903 CPU: 500M :rx5281 prid=0xdc02 force spi nor mode DRAM: 64 MiB @ 1066 MHz Skipping flash_init Flash: 0 Bytes flash status is 0, 0, 0 SF: Detected XM25QH64A with page size 256 Bytes, erase size 64 KiB, total 8 MiB Using default environment
Autobooting in 1 seconds copying flash to 0x81500000 flash status is 0, 0, 0 SF: Detected XM25QH64A with page size 256 Bytes, erase size 64 KiB, total 8 MiB SF: 8388608 bytes @ 0x0 Read: OK
[...]
按下Enter后,系统要求输入用户名和密码。感谢那个GitHub问题,我们知道凭据,因此可以使用用户'root'和密码'slprealtek'成功登录,最终获得控制台访问权限。
确认连接正常后,我需要加固焊接,因为在安装外壳过程中它断过两次。我使用了热熔胶来固定所有线缆,然后关闭设备,断开所有电机。现在,我的测试单元准备好了。
----[ 探索设备 ]--------------------------
现在我们已经有了一个外壳,让我们探索设备:
root@SLP:~# uname -a Linux SLP 3.10.27 #1 PREEMPT Wed Nov 11 20:42:05 CST 2020 rlx GNU/Linux
root@SLP:~# cat /etc/openwrt_version 12.09-rc1
可以看到,这是一台运行Linux 3.10.27的OpenWRT机器。现在让我们检查活跃进程和开放端口:
root@SLP:~# ps PID USER VSZ STAT COMMAND 1 root 2328 S init 2 root 0 SW [kthreadd] 3 root 0 SW [ksoftirqd/0] 4 root 0 SW [kworker/0:0] 5 root 0 SW< [kworker/0:0H] 6 root 0 SW [kworker/u2:0] 7 root 0 SW [rcu_preempt] 8 root 0 SW [rcu_bh] 9 root 0 SW [rcu_sched] 10 root 0 SW< [khelper] 11 root 0 SW< [writeback] 12 root 0 SW< [bioset] 13 root 0 SW< [kblockd] 14 root 0 SW [khubd] 15 root 0 SW [kworker/0:1] 16 root 0 SW [kswapd0] 17 root 0 SW [fsnotify_mark] 18 root 0 SW< [crypto] 27 root 0 SW [kworker/u2:1] 46 root 0 SW< [deferwq] 47 root 0 SW< [kworker/0:1H] 247 root 2328 S -ash 262 root 0 SW [irq/27-gpio res] 273 root 0 SW< [cryptodev_queue] 282 root 860 S /sbin/hotplug2 --override --persistent --set-rules-f 304 root 888 S /sbin/ubusd 325 root 8152 S tp_manage 357 root 3416 S /usr/bin/ledd 361 root 3408 S /sbin/msglogd 367 root 3220 S /usr/sbin/netlinkd 370 root 5468 S < /usr/bin/system_state_audio 379 root 10180 S /usr/sbin/wlan-manager 491 root 1636 S /sbin/netifd 492 root 1520 S /usr/sbin/connModed 494 root 11488 S /usr/bin/dsd 496 root 1532 S /usr/sbin/connModed 502 root 7640 S /bin/cloud-service 520 root 4360 S /bin/cloud-brd -c /var/etc/cloud_brd_conf 653 root 15020 S /bin/cloud-client 830 root 2320 S /usr/sbin/telnetd -b 127.0.0.1 861 root 3852 S /usr/sbin/uhttpd -f -h /www -T 180 -A 0 -n 8 -R -r C 870 root 6048 S /usr/bin/relayd 872 root 5948 S /usr/bin/rtspd 879 root 4612 S /usr/bin/p2pd 884 root 11152 S /bin/dn_switch 889 root 4180 S /bin/storage_manager 920 root 40940 S /bin/cet 956 root 32336 S /bin/vda 960 root 3808 S /bin/wtd 970 root 11288 S /bin/nvid 1019 root 2332 S udhcpc -p /var/run/static-dhcpc.pid -s /lib/netifd/s 1037 root 0 SW [RTW_CMD_THREAD] 1059 root 1212 S wpa_supplicant -B -Dwext -iwlan0 -P/tmp/supplicant_p 1089 root 2332 S /usr/sbin/ntpd -n -p time.nist.gov -p 133.100.9.2 -p 1103 root 3840 S /usr/bin/motord 1447 root 2324 R ps
root@SLP:~# netstat -natpu Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:8800 0.0.0.0:* LISTEN 920/cet tcp 0 0 127.0.0.1:929 0.0.0.0:* LISTEN 875/p2pd tcp 0 0 0.0.0.0:20002 0.0.0.0:* LISTEN 325/tp_manage tcp 0 0 0.0.0.0:2020 0.0.0.0:* LISTEN 969/nvid tcp 0 0 0.0.0.0:554 0.0.0.0:* LISTEN 920/cet tcp 0 0 127.0.0.1:23 0.0.0.0:* LISTEN 832/telnetd tcp 0 0 127.0.0.1:921 0.0.0.0:* LISTEN 878/relayd tcp 0 0 127.0.0.1:922 0.0.0.0:* LISTEN 877/rtspd tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 863/uhttpd tcp 0 0 192.168.1.80:37380 52.19.66.90:443 ESTABLISHED 507/cloud-brd udp 0 0 0.0.0.0:20002 0.0.0.0:* 325/tp_manage udp 0 0 0.0.0.0:38000 0.0.0.0:* 1087/ntpd udp 0 0 0.0.0.0:3702 0.0.0.0:* 969/nvid
我们可以看到nmap扫描中看到的那些开放端口背后的进程,比如uhttpd或cet。我特别关注了uhttpd进程,因为它是https服务器背后的进程(当时我仍然认为它是http),并且我已经非常熟悉http协议。
uhttpd是OpenWRT为运行该发行版的嵌入式设备创建的一个web服务器。此时,我想知道是否可以获取有关它的更多信息,比如源代码,或者至少是路径。我访问了OpenWRT维基,学习了uhttpd和OpenWRT的一般知识。在OpenWRT机器上,有一个叫做统一配置接口(UCI)的系统,基本上用于轻松配置系统服务。使用这个,我们可以获得uhttpd的配置:
root@SLP:~# uci show | grep uhttpd ucitrack.@uhttpd[0]=uhttpd ucitrack.@uhttpd[0].init=uhttpd uhttpd.main=uhttpd uhttpd.main.listen_https=443 uhttpd.main.home=/ww uhttpd.main.rfc1918_filter=1 uhttpd.main.max_requests=8 uhttpd.main.cert=/tmp/uhttpd.crt uhttpd.main.key=/tmp/uhttpd.key uhttpd.main.cgi_prefix=/cgi-bin uhttpd.main.lua_prefix=/luci uhttpd.main.lua_handler=/usr/lib/lua/luci/sgi/uhttpd.lua uhttpd.main.script_timeout=180 uhttpd.main.network_timeout=180 uhttpd.main.tcp_keepalive=0 uhttpd.px5g=cert uhttpd.px5g.days=3600 uhttpd.px5g.bits=1024 uhttpd.px5g.country=CN uhttpd.px5g.state=China uhttpd.px5g.location=China uhttpd.px5g.commonname=TP-Link upnpc.uhttpd=entry upnpc.uhttpd.proto=TCP upnpc.uhttpd.ext_port=80 upnpc.uhttpd.desc=uhttpd
这里有一些有趣的参数。首先,'uhttpd.main.home'指向服务器的根目录,所以我们可以找到一些web服务器文件。然后,'uhttpd.main.lua_handler'指向用于在服务器启动时初始化Lua运行环境的Lua处理脚本,因为uhttpd支持Lua脚本,所以那里可能有更多有趣的文件。然而,'/www'目录是空的,系统中也没有'/usr/lib/lua/luci'下的'sgi'目录或'uhttpd.lua'文件。我试图找到关于这个uhttpd实例如何工作的信息,但什么也没找到,只有指向空处的配置参数。
此时,我知道解决方案是直接分析'uhttpd'二进制文件并进行逆向工程,但在此之前,我想创建一个测试环境,了解向web服务器发出请求时内部发生了什么,因为根据进程创建的方式,没有任何输出到任何地方。
我尝试运行ps命令输出中进程861的命令:
$ /usr/sbin/uhttpd -f -h /www -T 180 -A 0 -n 8 -R -r C
然而,我遇到了很多错误,没能让它运行起来。由于我无法在不同端口上创建相同的uhttpd进程,我尝试通过检查进程的'/proc'条目来查找缺失的输出,以尝试读取它们是否存在(如PwnFunction的这段视频中所解释的)。但有一个大问题:
root@SLP:~# sudo ls -l /proc/864/fd/ lrwx------ 1 root root 64 Nov 10 22:44 0 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 1 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 2 -> /dev/null lrwx------ 1 root root 64 Nov 10 22:44 3 -> anon_inode:[eventpoll] lrwx------ 1 root root 64 Nov 10 22:44 4 -> socket:[1830]
所有'stdin'、'stdout'和'stderr'的文件描述符都被重定向到了'/dev/null',这基本上将它们重定向到了一个无法找到的黑洞。我陷入了困境,不知道该怎么办。由于我已经在'/proc'条目下,我开始研究,因为我不记得'/proc'条目有那么多关于进程的信息,而且我很好奇。多亏了这种偶然的好奇心,我遇到了'environ'条目,它包含该进程的所有环境变量。其中一个环境变量是:
UHTTPD_ARGS=-h /www -T 180 -A 0 -n 8 -R -r C200 -C /tmp/uhttpd.crt -K /tmp/uhttpd.key -s 443
我立刻意识到ps显示的命令是错误的,后来发现这是因为UART接口没有足够的带宽来显示所有字符。这是另一个错误,它教会了我重要的教训:永远不要相信UART端口的输出。
现在我终于可以用相同的参数创建另一个uhttpd实例,并且不将管道重定向到'/dev/null',以便在进行逆向工程时测试二进制文件。
----[ 使用Ghidra逆向工程uhttpd ]--------
这是我第一次使用Ghidra。我看过一些视频和文章(感谢stacksmashing和liveoverflow提供的内容,内容丰富且易于理解),但从未真正使用过,所以这是一个非常好的学习机会。
我打开了uhttpd二进制文件,经过几次尝试,发现语言是MIPS32,小端序,带有mips16e。一些函数名随二进制文件一起提供,但其他没有。我还花了一些时间重命名函数,因为Ghidra似乎经常弄错外部函数,并为它们创建奇怪的包装器,例如:
我分析了`main()`函数和其他重要函数,以理解二进制文件的逻辑和结构。我找到了一些有趣的、已识别的函数,包括`do_login()`和`uh_slp_proto_request()`。稍后会更多地谈论后者。
在初次接触之后,我开始寻找错误。作为一个在溢出漏洞方面完全的新手,我首先寻找了对system()、exec()和popen()的调用,以检查是否存在我可以轻松利用的命令注入漏洞。我真是太幸运了。
'exec_and_read_json()'函数使用'popen()'来执行命令:
ejecutar_y_leer_json
'exec_and_read_json()'函数被两个未命名的函数使用,我分别将它们命名为'set_language()'和'wifi_connect()'。这些函数分别负责语言设置和Wi-Fi连接(显然)。'wifi_connect()'似乎解析了单引号('),而'set_language()'则没有。这意味着如果我们能控制'set_language()'函数的输入,我们就能成功注入自己的命令:
conexion_wifi set_language
'set_language()'函数被'uh_slp_proto_request()'使用,这是我之前提到的函数,它接收来自用户的解析数据作为输入。
main_function_1 main_function_2
为了解析用户数据,uh_slp_proto_request()检查它是否是有效的JSON对象。然后,它获取由键method标识的字符串值和由params标识的字典值(至少我认为是这样,因为Ghidra无法解析函数调用,但看起来是这样工作的)。根据所选方法,uh_slp_proto_request()选择要执行的函数。
因此,通过发送以下有效载荷:{"method": "setLanguage", "params":{}}
我们正确地调用函数'set_language()'并将'{}'作为参数'language_json'传递。然后,在'set_language()'内部,对象'language_json'被转换为字符串并直接插入到"ubus call system_state_audio set_language '%s'"中执行。
发送此有效载荷时:{"method": "setLanguage", "params": {"payload": "'; touch poc;'"}}
将执行以下操作。
ubus call system_state_audio set_language '{"payload": "'; touch poc;'"}'
实际上包含3条命令:
ubus call system_state_audio set_language '{"payload": "' touch poc '"}'
第二条命令使我们能够完全执行代码。
现在,函数'uh_slp_proto_request()'被另一个未命名的函数使用,该函数管理所有请求,我将其称为'main_server_function()'。如果请求有效(不超过最大长度,根据服务器配置使用'http'或'https'等),'main_server_function()'会检查URL是否包含'/cgi-bin/luci'或'/web-static'。如果不是,则调用'uh_slp_proto_request()'。
uh_slp_proto_request_entrypoint
通过进行测试并向摄像机发送几个请求,我们可以验证'uh_slp_proto_request()'使用的数据是标准的POST数据。因此,如果我们向'/'发送一个带有上述有效载荷的POST请求,'uh_slp_proto_request()'将处理这些数据,调用'set_language()',我们的有效载荷将被注入到由'exec_and_get_result()'执行的命令中。
如您所见,我没有提到任何关于身份验证的内容,因为'setLanguage()'函数可以在未登录的情况下调用。这使得任何用户都无需身份验证即可通过单个请求完全控制摄像机。
----[ 利用 ]----------------------------------
现在,是时候编写利用程序了。我花了一些时间研究如何使用netcat获取反向shell。看起来很简单,但我没能让它工作。我发现BusyBox中安装的netcat版本在功能上非常有限,因此传统的反向shell不起作用。然而,我在PayloadsAllTheThings仓库中找到了我想要的东西(一如既往),并得到了完美的反向shell。由于uhttpd以root身份运行(多亏了TP-Link),我们只需发送一个恶意的POST请求即可获得最高权限的shell。该利用程序可在GitHub页面上获取:https://github.com/hacefresko/CVE-2021-4045/blob/master/pwntapo.py

🚨 经验教训:
https和http在端口443上的区别,浪费了时间直到发现错误。