2017-05-18 10:21:31
阅读:193次
点赞(0)
收藏
来源: googleprojectzero.blogspot.tw
作者:华为未然实验室
翻译:华为未然实验室
预估稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
前言
影响互操作技术的漏洞是一类较为有趣的安全漏洞,这是因为这些漏洞通常会影响使用该技术的任何应用程序,无论应用程序实际执行什么操作。同样,在很多情况下,开发人员难以在不使用该技术的情况下推出缓解措施,但有时候却做不到。
我发现.NET的组件对象模型(COM)互操作性层存在此类漏洞,这使.NET跨权限边界用于分布式COM(DCOM)本质上是不安全的。本文将描述一些可对此进行滥用的方法,首先是获得提升的权限,然后是一个远程代码执行漏洞。
背景知识
回顾.NET的历史可以知道,很多其早期基础是试图制作一个更好的COM版本。这使微软很注重确保,虽然.NET本身可能不是COM,但其必须能够与COM互操作。因此,.NET可以用于实现和使用COM对象。比如,不用在COM对象上调用QueryInterface,你只需要将对象投射到兼容COM的接口上。以C#实现进程外COM服务器很简单,如下所示:
客户端现在可使用其CLSID(由COMClass上的Guid属性定义)连接到COM服务器。实际上这很简单,因为.NET中的大量核心类被标记为COM可见,并注册为任何COM客户端(即使未以.NET编写)可用。
为了使这一切都有效,.NET运行时向开发人员隐藏了大量的样板。有几种机制可影响此样板互操作性代码,比如InterfaceType属性,其定义COM接口是源自IUnknown还是IDispatch,但大多数情况下,你得到的是所给予的。
开发人员可能没有意识到的一点是,不仅是您指定的接口从.NET COM对象导出,运行时还会添加一些“管理”接口。这些接口通过将.NET对象包装在COM可调用包装器(CCW)中来实现。
我们可以枚举CCW所暴露的接口。以System.Object为例,下表展示了支持的接口及每个接口实现的方式(在运行时动态实现或在运行时内部静态实现)。
_Object接口指的是System.Object类的COM可见表示,其是所有.NET对象的根,其必须动态生成,因为其依赖于被暴露的.NET对象。另一方面,IManagedObject由运行时本身实现,且实现在所有CCW中共享。
我从2013年开始关注.NET暴露的COM攻击面,彼时我正在研究IE沙箱逃逸。您可以在沙箱之外访问的COM对象之一是.NETClickOnce部署代理(DFSVC),其原来是以.NET实现,这可能并不足为奇。我实际上发现了两个问题,不是在DFSVC本身,而是在由所有.NET COM对象暴露的_Object接口。_Object接口如下所示(以C++)。
第一个bug(导致CVE-2014-0257)在于GetType方法。该方法返回一个可用于访问.NET反射API的COM对象。由于返回的_Type COM对象正在服务器中运行,所以您可以调用一系列方法,从而可访问Process.Start方法,您可以调用该方法实现沙箱逃逸。如欲了解更多细节,请查看我编写并放在Github上的PoC。微软通过阻止通过DCOM访问反射API解决了此问题。
第二个问题更微妙,是.NET互操作特性(大概没有人认为是安全隐患)的副产品。加载.NET运行时需要相当多的额外资源,因此,对于本机COM客户端调用.NET COM服务器上的方法,默认是让COM和CCW管理通信,即使这样有损性能。微软可以选择使用COM封送器强制.NET在客户端加载,但这样似乎有点过头,更别说客户端甚至可能没有安装兼容的.NET版本了。
当.NET与COM对象交互时,其会创建反向CCW——运行时可调用包装器(RCW)。这是一个.NET对象,其实现COM接口的运行时版本,并将其编组到COM对象。现在COM对象完全有可能实际上是用.NET编写的,甚至可能在相同的应用程序域中。如果.NET无所作为,可能对性能造成双倍影响,在RCW中编组,以调用一个COM对象,这实际上是一个托管对象的CCW。
尝试从CCW“展开”托管对象并获取一个真正的.NET对象是很好的。这是这段中的淘气鬼使坏的地方,IManagedObject接口,如下所示:
当.NET运行时获得一个COM对象时,其将通过一个过程来确定其是否可以从其CCW“展开”对象,并避免创建一个RCW。该过程被记录,但总而言之,运行时将执行以下操作:
1.调用COM对象上的QueryInterface来确定其是否实现IManagedObject接口。如果没有,则返回合适的RCW。
2.调用接口上的GetObjectIdentity。如果GUID与每次运行时GUID(在运行时启动时生成)匹配,且AppDomain ID与当前的AppDomain ID匹配,则在运行时表中查找CCW值,并提取指向真实管理对象的指针并将其返回。
3.调用接口上的GetSerializedBuffer。运行时将检查.NET对象是否可序列化,如果可以,则其将对象传递给BinaryFormatter :: Serialize,并将结果打包到二进制字符串(BSTR)中。这将返回给客户端,客户端现在将尝试通过调用BinaryFormatter :: Deserialize将缓冲区反序列化到对象实例。
第2和3步似乎都是坏主意。比如,在第2步中时,每运行时GUID不能被猜到,如果您可以访问同一进程中的任何其他对象(例如由服务器本身暴露的COM对象),则您可以调用对象上的GetObjectIdentity,并将GUID和AppDomain ID重播回服务器。这并没有给您带来太多好处,CCW值只是一个数字不是一个指针,所以,你最多能提取已经有CCW的对象。
相反,真正棘手的是第3步。无论什么语言(比如Java、php、Ruby,等等),任意反序列化都是危险的,.NET亦不例外。显然这是我们可以利用的一个问题,我们先从权限提升角度看一看。
提升权限
我们如何让以.NET编写的COM服务器进行任意反序列化?我们需要服务器尝试为通过COM暴露的可序列化.NET对象创建RCW。如果一般而言可以做到这一点的话是很好的,就是这么凑巧,在标准_Object接口上存在一个我们可以传递任何对象的函数,Equals方法。Equals的目的是比较两个对象的等同性。如果我们将.NET COM对象传递给服务器的Equals方法,则运行时必须尝试将其转换为RCW,以便托管实现可以使用它。在这一点上,运行时需要有所帮助,并检查其是否真的是一个CCW包装的.NET对象。服务器运行时调用GetSerializedBuffer,导致服务器进程中的任意反序列化。
这是我第二次利用ClickOnce部署代理(导致CVE-2014-4073)。利用这一点的技巧是将序列化的Hashtable发送到包含IHashCodeProvider接口的COM实现的服务器。当Hashtable运行其自定义反序列化代码时,其需要重建其内部散列结构,其通过在每个密钥上调用IHashCodeProvider :: GetHashCode来实现。通过添加一个可序列化的Delegate对象,作为其中一个密钥,我们将把它传递给客户端。通过以本地代码编写客户端,通过IManagedObject的自动序列化将不会在将委托传回给我们时发生。委托对象卡在服务器进程内,但CCW暴露给我们,我们可以调用。调用委托会导致在服务器上下文中执行指定的函数,这样我们能以服务器权限启动新进程。因为这一般都有效,所以我还编写了一个工具来为任何.NET COM服务器完成此工作,请见github。
微软本可以通过更改IManagedObject :: GetSerializedBuffer的行为来修复CVE-2014-4073,但是没有。相反,微软以本机代码重写了代理。还发布了一篇博客文章,警告开发人员.NET DCOM的危险。然而,他们并没有弃用任何API以在.NET中注册DCOM对象,因此除非开发人员的安全意识极高,并且碰巧阅读了微软的安全博客,否则他们可能不会意识到这是一个问题。
这类bug到今天一直存在,比如,当我最近收到一个新的工作笔记本电脑时,我做了我通常会做的事情,枚举安装了什么OEM“增值”软件,看看是否有什么可以利用。结果表明,作为音频驱动程序包的一部分安装了由杜比编写的COM服务。经过几分钟的检查,基本上枚举COM服务器的可访问接口,我发现其是用.NET编写的(IManagedObject的存在总是一个大的赠品)。我启动了我的利用工具,在不到5分钟的时间内,实现了在本地系统中的代码执行。现在已作为CVE-2017-7293被修复。由于.NET DCOM基本上不安全,杜比唯一可以做的是以本地代码重写服务。
攻击Caller
找到IManagedObject bug类的一个新实例让我开始关注其其他影响。首先要强调的是,服务器本身并无漏洞,只有当我们能强制服务器充当回调攻击应用程序的DCOM客户端时,该漏洞才能被利用。通过托管COM互操作调用DCOM对象的任何.NET应用程序都应该有类似的问题,而不仅仅是服务器。DCOM可能有什么常见的用例,特别是在现代企业环境中?
我首先想到的是windows Management Instrumentation(WMI)。现代版本的Windows可以使用WS-Management(WSMAN)协议连接到远程WMI实例,但由于遗留原因,WMI仍然支持DCOM传输。WMI的一个用例是扫描企业机器是否有恶意行为。这种复苏的原因之一是Powershell(在.NET中实现)具有易于使用的WMI支持。如果PS或者.NET尝试访问网络中受影响的工作站,那么或许PS或.NET本身容易受到此种攻击?
查看MSDN后可知,.NET通过System.Management命名空间支持WMI。这从.NET的开始就一直存在。其支持对WMI的远程访问,考虑到类的年代,其先于WSMAN,因此几乎可以肯定在后台使用DCOM。在PS前端,通过诸如Get-WmiObject之类的cmdlet支持WMI。PS版本3(在Windows 8和Server 2008中引入)添加了一组新的cmdlet,包括Get-CimInstance。阅读相关链接可知,引入CIM cmdlet、支持WSMAN的原因显而易见,并且链接明确指出“旧”WMI cmdlet使用DCOM。
WMI cmdlets:
优点:相比WsMan Cmdlets提供更好的任务抽象,输出是一个.NET对象
缺点:使用非标准DCOM协议,不适用于非Windows
在这一点上,我们可以直接进入.NET和PS类库的RE,但有一个更简单的方法。通过观察到WMI服务器的DCOM RPC流量,可能能够看到.NET客户端是否查询IManagedObject。Wireshark已经有一个DCOM解析器,为我们节省了很多麻烦。出于测试目的,我设置了两个虚拟机,一个是用Windows Server 2016(作为域控制器),另一个是用Windows 10(作为域上的客户端)。然后我从客户端的域管理员发出一个简单的WMI PS命令‘Get-WmiObject Win32_Process -ComputerName dc.network.local’,同时使用Wireshark监控网络。以下图片显示了我观察到的内容:
屏幕截图显示了来自PS客户端(192.168.56.102)的DC服务器上WMI DCOM对象(192.168.56.50)的初始创建请求。我们可以看到其在查询IWbemLoginClientID接口,这是初始化过程的一部分(如MS-WMI中所述)。然后,客户端尝试请求其他几个接口;尤其是其请求IManagedObject。这几乎肯定表明使用PS WMI cmdlet的客户端是有漏洞的。
为了测试这是否真的是一个漏洞,我们需要一个假的WMI服务器。似乎这是一个很大的挑战,但是我们需要做的就是修改winmgmt服务的注册,以指向我们的假实现。只要该服务随后用CLSID {8BC3F05E-D86B-11D0-A075-00C04FB68820}注册一个COM类,COM启动器将启动服务,并为任何客户端提供我们的假WMI对象的实例。回顾我们的网络捕获,结果是对IManagedObject的查询没有发生在主类上,而是在从IWbemLevel1Login :: NTLMLogin返回的IWbemServices对象上。但是没关系,其只是添加了一些额外的样板代码。为了确保其有效,我们将实现以下代码,其将告诉反序列化代码来查找一个名为Badgers的未知程序集。
如果我们成功注入序列化流,那么我们可以预期PS进程尝试查找Badgers.dll文件,并使用我们发现的Process Monitor。
链接解串器
当利用反序列化实现本地权限提升时,我们可以确保我们可以连接到服务器并运行任意代理。在RCE案例中我们没有这样的保证。如果WMI客户端启用了默认Windows防火墙规则,那么我们几乎肯定无法连接到委托对象所做的RPC端点。我们还需要被允许通过网络登录到运行WMI客户端的机器,我们的受攻击机器可能无法登录到域,或者企业策略可能会阻止除所有者外的任何人登录到客户端机器。
因此,我们需要一个稍微不同的计划,不是通过暴露一个新的委托对象来主动攻击客户端,相反,我们将给它传递一个字节流(反序列化时会执行所需的操作)。在理想的世界中,我们会发现一个可以为我们执行任意代码的可序列化类。可悲的是(据我所知)没有这样的类存在。因此,我们需要找到一系列“Gadget”类,这些类链接在一起可执行所需的效果。
所以在这种情况下,我倾向于编写一些快速分析工具,.NET支持相当不错的反射API,因此找到基本信息(比如一个类是否可序列化或一个类支持哪个接口)很容易做到。我们还需要一个要检查的程序集的列表,我知道的最快的方法是使用作为.NET SDK一部分安装的gacutil实用程序(并随Visual Studio一起安装)。运行命令gacutil / l> assemblies.txt来创建可以加载和处理的程序集名称列表。对于第一遍,我们将寻找可序列化且其中有委托的任何类,这些可能是执行操作时执行任意代码的类。使用我们的程序集列表,我们可以编写一些如下所示的简单代码来找到这些类,为每个程序集名称字符串调用FindSerializableTypes:
在我的系统中,该分析只产生了大约20个类,其中许多类实际上是在不分布在默认安装中的F#库中。但一个类确实引起了我的注意——System.Collections.Generic.ComparisonComparer<T>。您可以在参考源中找到该实现,但其很简单,全貌如下:
这个类包装了一个Comparison<T>委托——使用两个通用参数(相同类型),并返回一个整数,调用委托来实现IComparer <T>接口。虽然类是内部的,但其创建通过Comparer<T>::Create静态方法暴露。这是链的第一部分,通过这个类和一些序列化代理的推拿,我们可以将IComparer<T>::Compare链接到Process::Start,并获得创建的任意进程。现在我们需要链的下一部分,用任意的参数来调用该比较器对象。
比较器对象在通用.NET集合类中被大量使用,许多这些集合类也有自定义反序列化代码。在这种情况下,我们可以滥用SortedSet <T>类,反序列化使用内部比较器对象重建其集合以确定排序顺序。传递给比较器的值是集合中的条目,这完全在我们的控制之下。我们来写一些测试代码来检查其是否和我们预期的一样有效:
有关这个代码唯一奇怪的是TypeConfuseDelegate。这是一个长期存在的问题,.NET代理并不总是强制执行其类型签名,特别是返回值。在这种情况下,我们创建一个两个条目的多播代理(按顺序运行多个单个代理的委托),将一个委托设置为返回一个int的String :: Compare,将另一个设置为返回Process类实例的Process::Start。即使在反序列化和调用两种单独的方法时这也有效。然后其将返回创建的作为整数的进程对象,这意味着其会将指针返回到进程对象的实例。所以我们最终得到如下链条:
虽然这是一个非常简单的链,但它有一些问题,因此对于我们的用途其不太理想:
1. Comparer <T> :: Create方法和相应的类仅在.NET 4.5中引入,涵盖Windows 8及更高版本,但不涵盖Windows 7。
2.漏洞利用部分依赖于代理的返回值的类型混淆。虽然其只是将Process对象转换为一个整数,但这有点不太理想,可能会有意想不到的副作用。
3.启动一个流程略显嘈杂,从内存加载我们的代码更好。
所以我们需要找到更好的东西。我们需要一些至少在.NET 3.5上有效的东西,这是Windows 7上Windows Update会自动更新到的版本。此外,其不应该依赖于未定义的行为或从DCOM通道外部加载我们的代码,例如通过HTTP连接。这对我似乎是个挑战。
改善链条
在查看可序列化的其他一些类时,我在System.Workflow.ComponentModel.Serialization命名空间中注意到了一些。该命名空间包含属于Windows Workflow Foundation的一部分的类,其是用于构建执行管道以执行一系列任务的一组库。这一点有点意思,事实证明,我之前利用过核心功能——作为Windows Powershell中代码完整性的一个绕过。
这使我找到了ObjectSerializedRef类。这看起来很像一个将反序列化任何对象类型的类。而不仅仅是序列化的。如果是这样的话,那么这是一个用于建立功能更强大的反序列化链的非常强大的原语。
查看实现后可知,该类被用作通过ActivitiySurrogateSelector类暴露的序列化替代。这是.NET序列化API的一个特性,您可以在序列化过程中指定“代理选择器”,其将用代理类替换对象。当流反序列化时,此代理类包含足够的信息来重建原始对象。一个用例是处理非可序列化类的序列化,但是ObjectSerializedRef超出了特定的用例,并允许您反序列化任何内容。按顺序进行的测试:
ObjectSurrogate类似乎没任何问题。这个类完全毁灭了确保不可信的BinaryFormatter流的任何希望,其可以从.NET 3.0获得。任何没有标记为可序列化的类现在都是目标。在反序列化过程中调用一个任意委托很简单,因为开发人员不会做任何事情来防范这种攻击方式。
现在选择一个目标来建立我们的反序列化链。我本可以选择进一步探讨Workflow类,但是API是可怕的(实际上在.NET 4中,微软用一个新的、稍微更好一些的API代替了旧的)。相反,我会选择一个非常易于使用的目标,语言集成查询(LINQ)。
LINQ作为核心语言特性在.NET 3.5中引入。一种类似于SQL的新语法引入了C#和VB编译器,以跨可枚举对象执行查询,比如列表或字典。基于长度过滤名称列表并返回大写列表的语法示例如下:
您也可以不将LINQ视作查询语法,而是视作在.NET中执行列表推导的一种方法。将“select”视为等同于“map”,将“where”视为等同于“filter”可能更有意义。查询语法下是在System.Linq.Enumerable类中实现的一系列方法。您可以使用正常的C#语法而不是查询语言编写,如果这样做,则前面的例子变成如下所示:
方法(如Where)需要两个参数、一个列表对象(这在上面的例子中是隐藏的)及一个委托来调用枚举列表中的每个条目。委托通常由应用程序提供,但是没有什么可以阻止您使用系统方法替换委托。要记住的重要事情是在枚举列表之前不会调用委托。这意味着我们可以使用LINQ方法构建一个枚举列表,使用ObjectSurrogate进行序列化(LINQ类本身不是可序列化的),然后,如果我们能强制反序列化列表被枚举,其将执行任意代码。
使用LINQ作为原语,我们可以创建一个列表,当枚举时,该列表按以下顺序将字节数组映射到该字节数组中的一个类型的实例:
唯一棘手的部分是第2步,我们想提取一个特定的类型,但我们唯一真正的选择是使用Enumerable.Join方法,这需要投机取巧才能使其有效。一个更好的选择是使用Enumerable.Zip,但这只在.NET 4中引入。所以,我们将获取加载的程序集中的所有类型,并全部创建,如果我们只有一个类型,那么这不会有什么区别。以C#实现是怎样的?
C#实现中唯一不明显的部分是Assembly :: GetTypes的委托。我们需要的是一个接受一个Assembly对象并返回一个Type对象的列表的委托。然而,由于GetTypes是一个实例方法,默认是捕获Assembly类并将其存储在委托对象内,这将导致一个不接收参数并返回一个Type列表的委托。我们可以通过使用反射API为实例成员创建一个开放委托来解决这个问题。一个开放的委托不存储对象实例,而是将其作为额外的Assembly参数公开,这正是我们想要的。
使用我们的枚举列表,我们可以让程序集加载及让我们自己的代码执行,但是我们如何枚举列表启动链?为此,我尝试找到一个当调用ToString(一个很常见的方法)时会枚举列表的类。这在Java中很简单,几乎所有的集合类都有这个确切的行为。可悲的是,似乎.NET在这方面与Java不同。所以我修改了我的分析工具,以尝试寻找可以帮我们达到目的的小工具。长话短说,我通过三个单独的类发现了一个从ToString到IEnumerable的链。该链如下所示:
我们完成了吗?还没有,还差一步,我们需要在反序列化期间调用任意对象上的ToString。当然,如果我不是已经有一个方法来完成,我不会选择ToString。在这最后一个案例中,我会回到滥用Hashtable。在Hashtable类的反序列化期间,其将重建其密钥集,我们对此已有了解,因为这是我利用本地EoP的序列化的方法。如果两个密钥相同,则反序列化将会失败,Hashtable会抛出异常,导致运行以下代码:
该密钥被传递到值数组中的GetResourceString,以及对资源字符串的引用。资源字符串以及传递到String.Format的密钥被查找。生成的资源字符串具有格式化代码,因此当String.Format遇到非字符串值时,其调用对象上的ToString将其格式化。这导致在反序列化期间调用ToString,从而会踢掉事件链,从而引发我们从内存中加载任意.NET组件并在WMI客户端的上下文中执行代码。
您可以查看我添加到问题跟踪器的最新PoC中的最终实现。
结论
微软通过确保System.Management类从不直接为WMI对象创建RCW修复了RCE(远程代码执行)问题。但是,此修复程序不会影响.NET中任何其他DCOM的使用,因此特权.NET DCOM服务器仍然存在漏洞,其他远程DCOM应用程序也可能受到攻击。
此外,这应该是一个教训,切勿使用.NET BinaryFormatter类反序列化不受信任的数据。无论如何这样做也是危险的,但开发人员似乎放弃了制作安全的可序列化类的任何希望。ObjectSurrogate的存在实际上意味着,运行时中的每个类都是可序列化的,无论原始开发人员希望与否。
最后一个想法,您应该始终对中间件的安全性实施持怀疑态度,尤其是当您不能检查其所作所为时。IManagedObject的问题是与生俱来的,难以移除,所以正确修复很难。
本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。
原文链接:https://googleprojectzero.blogspot.tw/2017/04/exploiting-net-managed-dcom.html