Introduction
The present document analyzes previous disclosed use-after-free in Hewlett Packard Enterprise Intelligent Management Center, a network management platform. The vulnerability was reported by Steven Seeley to the Zero Day Initiative (ZDI) disclosure program identified as CVE-2017-12561.
The flaw allows to perform an unauthenticated remote attack and execute arbitrary code on vulnerable installations. HP fixed this vulnerability in October 2017, and all vulnerable instances should be updated to version HPE Intelligent Management Center (iMC) iMC Plat 7.3 E0504P4 or earlier to avoid being exposed according to the released security security bulletin.
Background
Before going deep into the details, we must be aware of some technical topics, namely the target installation environment and an overview of ASN.1 BER Encoding as the packets to communicate with the server are in this format.
Installation Environment
Following the exercise’s description, the target was installed in a Microsoft Windows Server 2008 R2
virtual machine. It was a good challenge because everything was
in this OS is obsolete. After looking at the manual, I could configure the SQL Server and the sa account. The installation of the patched version was straightforward - the same for applying the vulnerable binary as it was simply by installing its components.
The first task is to know how the software works. After reading the documentation,installing both vulnerable and patched versions of HP iMC, and saving the respective DBman binaries for further analysis using IDA.
As seen with the netstat command, port 2810
regarding DBman is listening, and the service is ready to be analyzed.
Quick Intro to ASN.1 BER Encoding
ASN.1 (Abstract Syntax Notation One) is a notation for describing abstract types. A type is a set of values. A value of a given ASN.1 type is an element of the type’s set. ASN.1 has four types: simple types, which are “atomic” and have no components; structured types, which have components; tagged types, which are derived from other types; and other types, which include the CHOICE type and the ANY type.
The following table shows a list of ASN.1 types.
Type | Tag number (decimal) | Tag number (hexadecimal) |
---|---|---|
INTEGER | 2 | 02 |
BIT STRING | 3 | 03 |
OCTET STRING | 4 | 04 |
NULL | 05 | 05 |
OBJECT IDENTIFIER | 6 | 06 |
SEQUENCE and SEQUECE OF | 16 | 10 |
SET and SET OF | 17 | 11 |
PrintableString | 19 | 13 |
T61String | 20 | 14 |
UTCTime | 23 | 17 |
BER Encoding
BER (Basic Encoding Rules) is a subset of the ASN.1 standard. Its purpose is to allow to encode or represent values of each ASN.1
type as octets. BER uses a form of encoding commonly known as Tag-Length-Value
.
Each item is encoded as a tag, indicating what type it is, a length implying the object’s size, and a value containing the object’s actual contents.
There are various methods to encode values using BER. For more detail, see here.
Vulnerability Analysis
Patch Diffing
The patch code (E0504) for this vuln should not suffer a lot of changes since it is a close release from the vulnerable version (E0504P04).
Using IDA with the BinDiff plugin can give an idea of what functions were modified.
The matched functions tab shows two modified functions, sub_4562C0 and sub_46CFD0.
On the one hand, checking sub_4562C0, nothing, at first sight, looks to be relevant.
On the other hand, sub_46CFD0 seems to be interesting as it appears inside a big switch-case where the condition is the opcode (10012) described in the advisory.
These are good hints to discover the flow and trigger the use-after-free vulnerability.
Inspecting this function, nothing looks to be related to a use-after-free. The next step is analyzing the code related to the 10012 opcode
.
Code Analysis
Firstly, the code checks the number of bytes received from the socket according to the documentation.
It reads 4 bytes of data and allocates a buffer of that size. So from this code, we can think the software expects a packet with the format: opcode+size_buffer+buffer_data
Testing it with a simple python client it logs some odd errors because everything should have the same size.
#!/usr/bin/python3
from pwn import *
from struct import pack, unpack
def main():
host = '192.168.1.133'
port = 2810 # default port according to documentation
conn = remote(host, port)
#sends --> opcode,size of buffer being allocated, buffer data
payload = b''
payload += pack('>i',10012) # opcode
payload += pack('>i',144) # size of buffer
payload += pack('>i',0x41*144) # buffer data
conn.sendline(payload)
main()
DBman logs the runtime errors in C:\Program Files\iMC\dbman\log\dbman_debug.log
. The following logs show
the result triggered by the above script.
From the decompiled code, we identified the error message. This code block is executed when the size of buffer_data is not equal to size_buffer. In other words, it fails the check ( *(_DWORD *)recv_size_buffer == size_data ).
Let’s examine the code in case this check is valid.
The first condition is checking if the size of the buffer sent in the packet corresponds to the data buffer size. After, check if the buffer data from the packet is correctly allocated to a buffer.
If everything goes well, an ASN object is created and releases the buffer sent with the packet using the free function. After it, as a good practice, the reference to this object is Nullified to clean the pointer.
After this, a function named AsnRemoteReservedFileRemove looks to initialize an ASN object using octets.
After this function appears, another function with a variable with AsnRemoteReservedFileRemove::BDec attributed.
Again an ASN reference should be related to ANS.1 format. Searching on the internet for DBman and ASN keywords appeared, and some blog posts about other vulnerabilities in DBman.
I read this post about opcode 10020. The post mentions how DBman packets are constructed and confirms the use of ASN.1 BER encoded objects in the communication packets.
Obs: After researching what SNACC is, I found out that is a ASN.1 compiler to C/C++
The post mentions a client named Intelligent Deployment Monitoring Agent.
In fact, it makes sense if the DBman service is listening, a client to communicate with the server should exist.
Inspecting the AsnRemoteReservedFileRemove.class, we the similarity with the decompiled we have seen previously.
It seems to be a custom implementation of ASN.1 BER Encoded packets for DBman. Now we can build our client as we know the packet structure accepted by the server.
####################################
# ASN.1 BER encoded packets script #
####################################
#!/usr/bin/python3
from pwn import *
from struct import pack, unpack
from pyasn1.type.univ import Sequence, OctetString, Integer, Null
from pyasn1.codec.ber import encoder
import time
class AsnRemoteReservedFileRemove():
def __init__(self, reservedFilePath, backupPath, backFileExt, time):
#super(AsnRemoteReservedFileRemove, self).__init__()
self.reservedFilePath = reservedFilePath
self.backupPath = backupPath
self.backFileExt = backFileExt
self.time = time
def encode(self):
self.message = Sequence()
self.message.setComponentByPosition(0, OctetString(self.reservedFilePath))
self.message.setComponentByPosition(1, OctetString(self.backupPath))
self.message.setComponentByPosition(2, OctetString(self.backFileExt))
self.message.setComponentByPosition(3, Integer(self.time))
self.message.setComponentByPosition(4, Null())
return encoder.encode(self.message)
def main():
host = '192.168.1.133'
port = 2810 # default port according to documentation
conn = remote(host, port)
# sends --> opcode,size of buffer being allocated, buffer data
msg_obj = AsnRemoteReservedFileRemove("C:\\Users\\Eduardo\\down.PS1", "C:\\Users\\Eduardo\\imc_patched\down.PS1",".log", time.time())
asnEncoded_msg = msg_obj.encode()
payload = b''
payload += pack('>i',10012) # opcode
payload += pack('>i',len(asnEncoded_msg)) # size of buffer
payload += asnEncoded_msg # buffer data
conn.sendline(payload)
main()
With this script, I could unintentionally trigger an error that appeared to crash DBman.
Why this failed?
Somehow the if condition was true. Inside it is the string “dbman_decode_len failed” which was the one logged when the crash happened. So this function must somehow be trying to decode the ASN.1 object sent in the packet, which failed during the process.
Checking the decompiled jar, there is no reference to this string. Analyzing the function looks like it is trying to check if the size from the data_buffer is right and tries to store the length in a variable if the check is acceptable.
For curiosity checked aLiuan814ucj variable reference. It is located in the data section, which seems to be a string already present at the compilation time.
Looking in the decompiled jar and found it being used as a key with DES symmetric algorithm.
So sub_454120 should be related to DES decryption using liuan814 as the key. Not sure if this is the case, but since the function is about decoding the ASN.1 object, it seems to be trying to decipher the data_buffer and check if everything goes well. \
The function sub_454BA0 with the rest division by 8 appears to check if the data_buffer octets are compliant. As it deals with ASN.1 OctetString types, the remainder should always be zero. If not, there is something wrong with the object.
When successful returns the buffer size. If the payload triggered the error, the ASN.1 object octets remainder after division should not be zero. Printing the size of the encoded BER object, it outputs 90.
90 % 8 = 11,25 |
The remainder is not zero, so DBman can’t decode the object and logs the error. DBman, after that, crashes and reboots. Something unusual is happening after the logging function (sub_475470). In this case function, sub_45EC20 should have an issue.
Use-After-Free (UAF)
Root Cause Analysis
Checking sub_45EC20, there is no relevant information.
Checking the disassembled view of this function, there is no interesting information too.
But there are two paths the flow can take. Let’s debug the software to get more details. Olly Debugger was the debugger used for this task.
Putting a breakpoint in 0045EC20, which is the address of sub_45EC200. The plan is to execute the payload and iterate with Ollydbg and see what happens when the application crashes.
Iterating through these instructions at address 0045EC63 (call EAX
), there is a call to a resolved address (0045EAB0) in runtime.
Also, we can see the payload in memory.
The application flow is redirected to a new complete function that could be resolved by looking at the decompiler. Keeping stepping forward, it appears to be a free function. The pointer to be freed is 027A95E0.
After jumping to that function, the HeapFree function frees the pointer 027A95E0. According to the documentation, this function frees a memory block allocated from a heap.
The pointer seen before is as pMemory argument (a pointer to the memory block to be freed).
Checking the contents pointed by this pointer before being freed.
After executing the HeapFree function, the block content is overwritten by junk data. 027A95E0 points to a new address -> 027A9760.
Continuing to step forward, we reached a part of the code where is used the pointer 027A95E0(released before). The content (027A9760) of the pointer is copied to EAX.
The next instruction is to use the content of this pointer as a reference (DWORD PTR DS: [EAX + 14]). After dereferencing, the relative address points to junk data, as seen in the image below.
The junk data is copied to EDX, which will be used with a Call instruction a few instructions below. Stepping to reach the call EDX, we see the redirection address FEEEFEEE.
Redirecting to this address triggers an access violation, and the program crashes.
Conclusion: The crash occurs because the application tries to dereference a previously freed pointer, resulting in a Use-After-Free.
Extra
After resolving the call to EAX, the address of sub_0045EAB0 in OllyDbg, checking that function in IDA, there is a call to operator delete.
Comparing the code decompiled by IDA and what was presented by OllyDbg, delete and free are equivalent functions as described here.