0x00 前言
2019年9月,微软公布了安全补丁,以修复Azure DevOps (ADO)及Team Foundation Server (TFS)中的远程代码执行(RCE)漏洞。在这个严重级别的漏洞中,攻击者需要将一个精心构造的文件上传到存在漏洞的ADO或TFS服务器仓库,等待系统索引该文件。成功利用后,就可以在目标系统上实现代码执行效果。该漏洞由Mikhail Shcherbakov向ZDI提交,同时小伙伴也提供了CVE-2019-1306的细节分析。
0x01 漏洞分析
BinaryFormatter是.NET平台上常用的二进制序列化类,同时也是一个反序列化类,默认配置下并不安全。早在2012年,James Forshaw在Black Hat的一次演讲中已经提到过关于.NET序列化的第一个gadget。随后,.NET开发者开始呼吁“不要使用BinaryFormatter来反序列化不可信的数据”,或者至少不要使用默认配置的BinaryFormatter。然而在现代复杂的系统中,想要区分可信及不可信的数据是非常有挑战的一个任务。典型的例子就是最近被修复的CVE-2019-1306,该漏洞可以影响Microsoft Azure DevOps Server。
Microsoft Azure DevOps Server(之前的名称为TFS)是一种CI/CD/源代码控制/问题跟踪/Wiki系统,该系统架构复杂,包含各种功能以及许多内部数据格式,因此是查找不安全反序列化漏洞的一个绝佳目标。由于Azure DevOps Server提供了自托管版本,我们可以使用静态及动态方法来分析相关程序。大多数应用使用.NET编写,因此我开发了适用于CIL(通用中间语言)数据流(Data Flow)及控制流(Control Flow)的分析工具:DeReviewer。该工具支持类DSL语法,可以描述存在漏洞的模式及payload,可以自动测试payload、构建调用图,分析可能存在漏洞方法的调用路径。我运行DeReviewer来分析Microsoft Azure DevOps Server,找到了一个有趣的调用路径,其中涉及到BinaryFormatter::Deserialize
方法。
我们可以放大上图,发现Microsoft.VisualStudio.Services.Search.Server.Jobs.dll
程序集中包含DeserializeToObject
方法。如果我们反编译该方法,就能看到其中以不安全方式使用了BinaryFormatter
。
0x02 漏洞利用
如果攻击者能将任意二进制数组传递给arrayBytes
参数,那么就有可能实现远程代码执行。然而,Microsoft.VisualStudio.Services.Search.Server.Jobs.dll
的代码由后台服务TFSJobAgent所使用,该服务会构建并处理内部索引。TFSJobAgent
只使用了不安全实现的DeserializeToObject
来反序列化自己的索引,索引数据看上去非常可信。接下来让我们详细分析TFSJobAgent
的具体设计细节。
当TFSJobAgent
服务加载Azure DevOps组织账户Wiki页面的索引时,就会调用DeserializeToObject
方法。TFSJobAgent
服务会使用多个方法来创建并更新Wiki索引,其中一种方法涉及到一个爬虫(crawler),爬虫会监控Git仓库,当有新的改动被push到服务端时,就会更新对应的索引。用户需要设置Azure DevOps账户,以便在Git中存储Wiki页面。系统上权限较低的用户只需要在Web接口中点击“Publish code as wiki”就能完成该操作。在这种情况下,TFSJobAgent
服务会运行爬虫,解析新的Wiki内容,随后将结果序列化为索引文件,目前一切操作看上去都比较安全。Microsoft.VisualStudio.Services.Search.Parser.WikiParserExecutor
类会转换来自Git的二进制内容,使用Markdig
库将其解析为Markdown格式。负责这些逻辑的代码片段如下所示:
看到这段代码时,我都有点难以置信。如果Markdig
在解析Wiki页面内容时抛出异常,那么该方法就会使用未解析的二进制来初始化ParsedData
类。随后,Content
字段会被存储在索引中。后续代码期望内部索引数据已经经过验证,会使用DeserializeToObject
方法来重构ParsedMarkDownData
对象。因此,我们需要找到一些无效的Markdown文本来触发异常,将该文本与payload组合使用,存放到文件中,将文件push到Git作为Wiki页面。这看上去是实现RCE的不错思路。Azure DevOps Server并没有使用最新版的Markdig
库,这也方便我们搜索哪些是无效的Markdown文本。我从GitHub上下载了Markdig库的源码,首先查看了单元测试。我在对应版本的Markdig上测试了Markdig.Tests.MiscTests::TestInvalidCodeEscape
,解析器的确会抛出异常。根据此次测试,攻击者可以使用“`**Header**t来内容的正常解析逻辑。整个探索过程只花了我10分钟,让我们拥抱开源及GitHub!
接下来就是生成适用于BinaryFormatter
的RCE payload。James Forshaw在2017年描述过针对BinaryFormatter
的TypeConfuseDelegate
gadget,大家可以参考Project Zero Team的博客了解更多内容。payload生成器的代码如下所示。
接下来将payload与不正确的Markdown字符串结合:
将结果保存成文件,commit到Git仓库,push到Azure DevOps Server ,然后……什么都没发生!TFSJobAgent
并没有按照预期运行任何cmd命令。我做了一些调试,更深入分析代码,发现爬虫会验证来自Git的所有文件,只为文本文件创建索引。这一点很正常,但很有可能导致我们无法利用成功。然而,代码又再次让我大吃一惊。爬虫使用了FileTypeUtil::DetermineEncoding
方法,根据内容的前几个字节来“猜测”内容的Unicode编码。这些逻辑发生在payload push之前。
让我们研究一下经过BinaryFormatter
序列化后的数据头,微软在官方文档中描述了这个格式。经过BinaryFormatter
序列化后的数据流的第一个字节必须为0
,接下来4
个字节为RootId
,默认情况下等于整数类型的1
。FileTypeUtil::DetermineEncoding
方法只有一个允许的头部,从0
偏移处开始,为0x0000FEFF
。因此,我们可以将头部中的RootId
及序列化后的数据改为0x00FEFF00
。如下代码会在添加不正确的Markdown字符串之前执行该操作:
将结果存储到文件中,再次push到Azure DevOps Server。我在演示视频中记录了攻击过程,攻击者只需要能够访问Git仓库,就能实现RCE效果。
0x03 总结
微软已经修复了CVE-2019-1306,用户需要为Azure DevOps Server打上补丁。需要注意的是,Windows的自动更新中并没有包含该补丁,我们需要手动安装补丁。开发者可以在BinaryFormatter
配置中添加自定义SerializationBinder,只允许反序列化已知类型的数据,这也是反序列化不可信数据的最佳实践。即使我们认为面对的是可信的数据,我们也应当为不安全的序列化类应用相同的防御方法。从本文中大家可知,现代复杂系统的内部实现非常复杂,虽然内部索引看上去比较可信,是经过解析及验证后的数据,但攻击者仍然可以找到利用路径。