HPE Intelligent Management Center: a case study on the reliability of security fixes

This post highlights several mistakes in the patches released for vulnerabilities affecting various services of HPE Intelligent Management Center, with a focus on its native binaries.

Author: István Kurucsai

During our work on N-day vulnerabilities, we encounter many different issues with security patches that can leave users of the affected product at risk even if they keep their systems up-to-date. Some fixes don’t attempt to address the underlying vulnerability but apply trivial changes that break the provided proof-of-concept exploit. Examples include removing the specific path used to trigger the issue while leaving others available or adding a layer of encryption to the communication protocol to which the exploit could easily be adapted. Other times, new vulnerabilities are introduced as a result of the code changes or previously unreachable vulnerabilities become exposed. Sometimes the fix is simply incorrect, e.g. it adds a new check for the wrong function call or with the wrong boundary conditions. There are cases where the original analysis of the vulnerability by the security researcher is incomplete, therefore the fixes and detection filters based on it are likely to be incomplete, too. And then there are the patches that don’t actually contain any relevant changes and the vulnerability remains exploitable while the issue is marked as resolved.

We encounter these cases almost weekly and have presented some examples on this blog, including one on the incomplete public analyses of an Advantech WebAccess vulnerability and one on the failed patch for Intel Security (McAfee) True Key.

Background

HPE Intelligent Management Center is a network management platform with a history of a wide range of vulnerabilities affecting it. It has a vast attack-surface, including web based components and native binaries implementing custom protocols. While the analysis was done on the Linux releases of IMC, it is important to note that the Windows and Linux versions are compiled from the same code base and share the same vulnerabilities.

Hiding Vulnerabilities

A common mistake we encounter during our day-to-day work is when a patch only removes a possible path for triggering the vulnerability instead of actually fixing the issue. A prime example of this is the attempts made at patching several issues affecting the dbman service, which is responsible for the backup and restoration of the databases used by IMC. The Linux version doesn’t have stack cookies and isn’t compiled as PIE. It listens on TCP port 2810 and expects a simple packet header consisting of an opcode and the data length, followed by a DER encoded ASN.1 message. Several vulnerabilities affecting it, including command injections, an arbitrary file write and a stack buffer overflow were published by ZDI in 2017 under the identifiers ZDI-17-336 through ZDI-17-343 and ZDI-17-481 through ZDI-17-484. These are quite similar in nature and close to each other in the code base. Let’s take a quick look at ZDI-17-336/CVE-2017-5820, titled Hewlett Packard Enterprise Intelligent Management Center dbman Opcode 10004 Command Injection Remote Code Execution Vulnerability.

Without going into too much detail, opcode 10004 corresponds to the BackupZipFile operation of dbman. After parsing the packet header and the ASN.1 message, control flow ends up in the CDbBackup::BackupOneLocalZipFile method, which constructs a command line that includes unescaped data from several message fields. This command line is then passed to the runCommand function, which is a wrapper around system, resulting in a command injection vulnerability. For the code snippet below, note that OneZipFileBackupObj is the parsed, unsanitized message from the packet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SNACC::AsnRet *__userpurge CDbBackup::BackupOneLocalZipFile@(SNACC::AsnRet *retstr, const CDbBackup *const this, SNACC::AsnOneZipFileBackupPara *const OneZipFileBackupObj, int ifHost) {

    // removed for brevity

    v9 = (const std::basic_string<char,std::char_traits,std::allocator > *)SNACC::AsnOcts::data((SNACC::AsnOcts *)OneZipFileBackupObj->sqlScript);
    std::operator+<char,std::char_traits,std::allocator>(&strScriptTmp, v9, '"');

    // removed for brevity

    v15 = (const std::basic_string<char,std::char_traits,std::allocator > *)SNACC::AsnOcts::data((SNACC::AsnOcts *)OneZipFileBackupObj->zipfileName);
    std::operator+<char,std::char_traits,std::allocator>(&v166, &strScriptTmp, " ");
    std::operator+<char,std::char_traits,std::allocator>(&v165, &v166, v15);

    // removed for brevity

    v26 = runCommand(strScriptTmp._M_dataplus._M_p);

The First Fix

In the HPE Security Bulletin corresponding to ZDI-17-336/ CVE-2017-5820, the first patched version is indicated as IMC PLAT 7.3 E0504P04. Looking at the supposedly patched 7.3 E0506 version, the CDbBackup::BackupOneLocalZipFile function seems identical, containing the same vulnerability. Examining its caller, BackupZipFile, the only difference is that a new function, dbman_decode_len, is invoked before the ASN.1 decoding. It’s a rather short method, decrypting the input using DES in ECB mode with a static key, which is liuan814 in version 7.3 E0506.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int __cdecl dbman_decode_len(unsigned __int8 *_pcarrrCiphertext, int _pnCiphertext, unsigned __int8 **_pcarrExpress, int *_nExpress)
{
    void *v4; // ebx
    int result; // eax
    int v6; // eax

    v4 = malloc(_pnCiphertext);
    result = -2;
    if ( v4 )
    {
        memset(v4, 0, _pnCiphertext);
        if ( des_setup(shareKey, &dbman_decode_len(unsigned char *,int,unsigned char **,int *)::desKey)
          || (v6 = des_decrypt(_pcarrrCiphertext,
                               _pnCiphertext,
                               (unsigned __int8 *)v4,
                               _pnCiphertext,
                               &dbman_decode_len(unsigned char *,int,unsigned char **,int *)::desKey),
              v6 < 0) )
        {
            free(v4);
            result = -1;
        }
        else
        {
            *_nExpress = v6;
            *_pcarrExpress = (unsigned __int8 *)v4;
            result = 0;
        }
    }
    return result;
}

Examining the call sites for dbman_decode_len, they correspond to the opcodes implicated in the published vulnerabilities, meaning that this is the supposed fix for the issues. While this breaks exploits developed for previous versions of IMC, it’s in no way a proper patch. Simply encrypting the message with the static key enables exploitation of the original vulnerabilities.

The Second Fix

Looking at the newest version (7.3 E0605H05), the vulnerabilities are still present, unpatched. However, the encryption scheme was altered. dbman_decode_len became a simple wrapper around decryptMsg, which reads in keying material from two files in the IMC installation directory, common\conf\ks.dat and server\conf\imchw.conf. This is then used to derive a 256-bit IV and encryption key. These are passed into decryptMsgAes, along with the incoming data from the message, which decrypts it using AES_256_CBC and processing of the message continues as before.

The contents of the ks.dat and imchw.conf are randomly generated upon install or update of the product, therefore interacting with the handlers of the vulnerable message types is impossible without having access to those files. However, IMC has a significant attack-surface and any file read or write vulnerability can be turned into a command injection by leaking or overwriting the key files and triggering the original vulnerabilities. It should also be noted that both schemes are only applied to opcodes in which vulnerabilities were reported, other opcodes remain reachable as before.

New Vulnerability Introduced In The Second Fix

Looking at the decryptMsgAes function, it can be seen that a new vulnerability was introduced that results in a stack buffer overflow. For the code snippet below, note that src is the message read from the network, iEncLen is its length and strDecrypt is a heap allocated buffer in which the decrypted message is passed back to the caller. While there are no meaningful limits on the length of the input message, the code assumes that when decrypted, it fits into 4096 bytes. EVP_DecryptUpdate is part of the OpenSSL library and its documentation states that

the decrypted data buffer out passed to EVP_DecryptUpdate() should have sufficient room for (inl + cipher_block_size) bytes

The variable iEncLen corresponds to inl from the above quote. Since the input can be larger than 4096 bytes, a stack-based buffer overflow can be triggered on line 16 of the snippet below. There’s also the possibility for a heap buffer overflow on lines 20-21 but EVP_DecryptFinal will fail on incorrect padding and it’s impossible to create valid padding without knowing the IV and key. Even though the overflow contents cannot be controlled without knowing the AES key and IV, when combined with a file read or write vulnerability to leak or change the keying material, this issue could be turned into a reliable exploit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int __cdecl decryptMsgAes(unsigned __int8 *src, const unsigned int iEncLen, const unsigned __int8 *strPwd, const unsigned __int8 *strIv, const char *strAlg, bool *const padding, unsigned __int8 *strDecrypt, unsigned int *const iLen)
{
    int v8; // eax
    int v9; // eax
    int v11; // eax
    unsigned __int8 cDecrypt2[4096]; // [esp+28h] [ebp-20A0h]
    unsigned __int8 cDecrypt1[4096]; // [esp+1028h] [ebp-10A0h]
    EVP_CIPHER_CTX ctx; // [esp+2028h] [ebp-A0h]
    int outLenExtra; // [esp+20B4h] [ebp-14h]
    int outLen; // [esp+20B8h] [ebp-10h]

    // removed for brevity

    memset(cDecrypt1, 0, 0x1000u);
    memset(cDecrypt2, 0, 0x1000u);
    if ( EVP_DecryptUpdate(&ctx, cDecrypt1, &outLen, src, iEncLen) )
    {
        if ( EVP_DecryptFinal(&ctx, cDecrypt2, &outLenExtra) )
        {
            memcpy(strDecrypt, cDecrypt1, outLen);
            memcpy(&strDecrypt[outLen], cDecrypt2, outLenExtra);
            *iLen = outLenExtra + outLen;
            EVP_CIPHER_CTX_cleanup(&ctx);
            return 0;
        }

This vulnerability remains unpatched for the time being.

Failed Patches

It’s not uncommon for us to see patches that attempt to solve the root cause of an issue but fail to do so.

Stack Buffer Overflow In tftpserver

The IMC suite includes a TFTP server, implemented by the tftpserver service, which is used to distribute configuration files for devices. The TFTP protocol supports setting the blksize option on a connection, which allows the client and server to negotiate a blocksize more applicable to the network medium. This option is also supported by tftpserver and can be set by a client to an arbitrary 4-byte value as can be seen on the code snippet from the 7.3 E0506 version shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void __cdecl TFTP::handleOption(TFTP *const this, const $02CB5AD45871301F5DFC396880685A28 *const result)
{

    // removed for brevity

    for ( i = 0; ; ++i )
    {

        // removed for brevity

        if ( !std::string::compare((std::string *)&v3[i], "blksize") )
        {
            this->m_pkg_LimitSize = __strtol_internal(result->options._M_impl._M_start[i].optionValue._M_dataplus._M_p, 0, 10, 0);
            break;
        }
    }
    std::vector<TFTPOption,std::allocator>::operator=(&this->m_options, &result->options);
}

Later on, this value is used to determine not only the block size for the network transmission but also for the file read and write operations. In the TFTP::handleRRQ function, which is responsible for the handling of RRQs (file read requests by the client), the contents of the requested file are read in blksize sized chunks into a fixed size stack buffer of 10000 bytes, as shown below.

1
2
3
4
5
6
7
8
9
10
11
void __cdecl TFTP::handleRRQ(TFTP *const this, const $02CB5AD45871301F5DFC396880685A28 *const result)
{

    // removed for brevity

    char buf[10000]; // [esp+2760h] [ebp-27D8h]

    // removed for brevity

    memset(buf, 0, 10000u);
    if ( TFTP::getFileData(this, *(const char **)v28, 0, this->m_pkg_LimitSize, buf) == -1 )

The TFTP::getFileData function reads in the requested number of bytes into the stack buffer using fread (code below). Since m_pkg_LimitSize is attacker-controlled, this results in a stack buffer overflow, exploitable by first uploading the payload file to the server using a write request, then setting the blksize option to a value larger than 10000 and requesting the same file via a read request.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl TFTP::getFileData(TFTP *const this, const char *filename, int pos, int size, char *buf)
{

    // removed for brevity

    v6 = fopen(filename, "rb");
    if ( v6 )
    {
        if ( fseek(v6, pos, 0) )
        {
            v5 = 0;
            fclose(v6);
        }
        else
        {
            v5 = fread(buf, 1u, size, v6);

HPE released a bulletin for the vulnerability, which states that it is fixed in version 7.3 E0605P04. Examining the updated tftpserver binary, the only relevant change seems to be in the TFTP::getFileData function, in which a check was added to ensure that the size variable passed in is below 65536 (0x10000), as shown on the following code extract.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int __cdecl TFTP::getFileData(TFTP *const this, const char *filename, int pos, int size, char *buf)
{

    // removed for brevity

    if ( size <= 65536 )
    {
        v5 = -1;
        v7 = fopen(filename, "rb");
        if ( v7 )
        {
            if ( fseek(v7, pos, 0) )
            {
                v5 = 0;
                fclose(v7);
            }
            else
            {
                v5 = fread(buf, 1u, size, v7);

                // removed for brevity

            }
        }
    }
    else
    {
        v5 = -1;
        XLOG::LogError(-1, "[TFTP::getFileData] Data is too large, more than 64k.");
    }
    return v5;
}

There are multiple issues with this. As established previously, the buf argument points to a stack buffer that is 10000 bytes in size, so the check still allows an overflow of more than 50Kb. Besides that, the size argument is a signed integer and the blksize is also stored in a signed integer with an arbitrary value, meaning that negative values would also pass the check.

The latest version of IMC, v7.3 (E0605H05), was released on 08-Oct-2018 and still contains this incorrect fix.

The LogMsg Stack Buffer Overflows

An issue that took two iterations to patch properly is the vulnerability in the LogMsg method of the dbman service. The code of the function from version 7.3 E0504 can be seen below. As the name suggests, it’s used to log diagnostic messages. Accordingly, it has around 500 call-sites, many with arbitrarily long attacker-controlled data in the message, which can all result in a stack buffer overflow on line 10.

1
2
3
4
5
6
7
8
9
10
11
12
int LogMsg(int Level, char *FuncName, char *fmt, ...)
{
    char Msg[8192]; // [esp+20h] [ebp-2008h]
    va_list ap; // [esp+2020h] [ebp-8h]
    va_list __varargs; // [esp+203Ch] [ebp+14h]

    va_start(__varargs, fmt);
    memset(Msg, 0, 8192u);
    va_copy(ap, __varargs);
    vsprintf(Msg, fmt, __varargs);
    return LogMsg_P(Level, FuncName, Msg);
}

The vulnerability was patched in version 7.3 E0504P04, according to the HPE security bulletin. The fixed code is shown below and looks OK from a cursory glance, the length of the data written is limited by the use of the vsnprintf function.

1
2
3
4
5
6
7
8
9
10
11
12
int LogMsg(int Level, char *FuncName, char *fmt, ...)
{
    char Msg[8192]; // [esp+20h] [ebp-2008h]
    va_list ap; // [esp+2020h] [ebp-8h]
    va_list __varargs; // [esp+203Ch] [ebp+14h]

    va_start(__varargs, fmt);
    memset(Msg, 0, 8192u);
    va_copy(ap, __varargs);
    vsnprintf(Msg, 8192u, fmt, __varargs);
    return LogMsg_P(Level, FuncName, Msg);
}

However, the Msg buffer is then passed to the LogMsg_P function, which writes the data to the appropriate log file. It suffers from the same vulnerability as LogMsg. Even though the destination buffer on line 13 below is 8192 bytes long and the length of Msg is limited to 8192 bytes by the patch, there are up to a hundred bytes prepended to the actual message, so that the sprintf call can still write out-of-bounds. What is interesting is that triggering the original vulnerability is impossible without also triggering the one in LogMsg_P, meaning that the change probably wasn’t tested at all.

1
2
3
4
5
6
7
8
9
10
11
12
13
int __cdecl LogMsg_P(int Level, char *FuncName, char *Msg)
{

    // removed for brevity

    char LogMsg[8192]; // [esp+2Ch] [ebp-200Ch]

    memset(LogMsg, 0, 8192u);
    GetTime(LogMsg);

    // removed for brevity

    sprintf(LogMsg, "%s [%s] [%s] %s\n", LogMsg, g_AlarmName[Level], FuncName, Msg);

The second attempt replaced the sprintf call in LogMsg_P with an appropriately sized snprintf, actually fixing the issue. During the six-month period between the release of the two bulletins, IMC installations remained vulnerable to a supposedly patched vulnerability.

Conclusion

The N-day feed of Exodus provides detailed analysis of publicly disclosed security issues. These include many similar cases, where a vulnerability continues to pose a threat even after applying the vendor-supplied patch. It enables our customers to assess the real risks associated with vulnerabilities and implement proper detection and defensive measures. With our rigorously tested exploit code (supplied as part of the feed), organizations no longer need to rely on minimal proof of concepts that are usually available publicly. Our offerings can be leveraged by Red Teams to gain a foothold in the enterprise during penetration tests even when public exploit code does not exist or is simply unreliable.