0x00 前言
在本文中,我们介绍了TinyWall在2.1.13版本前存在的一个本地提权漏洞,本地用户可以借助该漏洞提升至SYSTEM
权限。除了命名管道(Named Pipe)通信中存在的.NET反序列化漏洞之外,我们也介绍了一个认证绕过缺陷。
0x01 背景
TinyWall是采用.NET开发的一款本地防火墙,由单个可执行文件构成。该可执行程序会以SYSTEM
权限运行,也会运行在用户上下文中,方便用户进行配置。服务端在某个命名管道上监听传入消息,消息使用BinaryFormatter
以序列化对象流的形式传输。然而这里存在一个身份认证检查机制,需要进一步研究。在本文中我们将详细分析这种机制,因为其他产品也有可能使用这种机制来防御未授权访问风险。
为了简单起见,下文中我们将使用“服务端”来表示接收消息的SYSTEM
上下文进程,使用“客户端”来表示位于已认证用户上下文中的发送进程。需要注意的是,已认证用户并不需要任何特殊权限(如SeDebugPrivilege
)就能利用本文描述的该漏洞。
0x02 命名管道通信
许多(安全)产品会使用命名管道(Named Pipe)作为进程间通信的渠道(可以参考各种反病毒产品)。命名管道有一个优势,服务端进程可以通过Windows的认证模型来获取发送方的其他信息,比如原始进程ID、安全上下文等。从编程角度来看,我们可以通过Windows API来访问命名管道,但也可以通过直接访问文件系统来实现。我们可以通过命名管道的名称,配合\\.\pipe\
前缀来访问命名管道文件系统(NPFS)。
如下图所示,该产品用到了TinyWallController
命名管道,并且任何已认证用户可以访问并写入该管道。
0x03 SYSTEM进程
首先我们来看一下命名管道的创建及使用过程。当TinyWall启动时,会调用PipeServerWorker
方法完成命名管道创建操作。Windows提供了一个API:System.IO.Pipes.NamedPipeServerStream
,其中某个构造函数以System.IO.Pipes.PipeSecurity
作为参数,这样用户就能使用SecurityIdentifiers
等类,通过System.IO.PipeAccessRule
对象实现细粒度的访问控制。此外,从上图中我们可以观察到,这里唯一的限制条件在于客户端进程必须在已认证用户上下文中运行,但这看上去似乎并不是一个硬性限制。
然而如上图所示,实际上这里还存在其他一些检查机制。该软件实现了一个AuthAsServer()
方法,会进一步检查一些条件。我们需要到达调用ReadMsg()
的代码块,该调用负责反序列化已收到的消息内容。
如果未能通过检查,则代码会抛出异常,内容为“Client authentication failed”(客户端认证失败)。跟踪代码流程后,我们找到了一个“认证检查”代码块,代码逻辑基于进程ID来检查,判断服务端与客户端进程的MainModule.FileName
是否一致。开发者之所以使用这种逻辑,可能是想确保相同的、可信的TinyWall程序能通过命名管道来发送和接收封装好的消息。
我们可以在调试上下文中使用原始程序,这样就不会破坏MainModule.FileName
属性,从而绕过该限制。接下来我们先使用调试器来验证不可信的反序列化操作。
0x04 测试反序列化
因此,为了测试是否可以使用恶意对象来反序列化,我们可以使用如下方法。首先,我们通过调试器(比如dnSpy)启动(而不是attach)TinyWall程序,在客户端向管道写入消息之前的位置上设置断点,这样我们就能修改序列化后的对象。在运行过程中,我们可以考虑在Windows System.Core.dll
中的System.IO.PipeStream.writeCore()
方法上设置断点,以便完成修改操作。完成这些设置后,很快断点就会被触发。
现在,我们可以使用ysoserial.NET和James Forshaw的TypeConfuseDelegate
gadget来创建恶意对象,弹出计算器。在调试器中,我们使用System.Convert.FromBase64String("...")
表达式来替换当前值,并且相应地调整计数值。
释放断点后,我们就能得到以SYSTEM
权限运行的计算器进程。由于反序列化操作会在显式转换前触发,因为我们的确能完成该任务。如果大家不喜欢出现InvalidCastExceptions
,那么可以将恶意对象放在TinyWall的PKSoft.Message
对象参数成员中,这个练习留给大家来完成。
0x05 伪造MainModule.FileName
通过调试客户端验证反序列化缺陷后,接下来我们可以看一下是否能抛开调试器完成该任务。因此,我们必须绕过如下限制:
GetNamedPipeClientProcessId()
这个Windows API用来获取特定命名管道的客户端进程标识符。在最终的PoC(Exploit.exe
)中,我们的客户端进程必须通过某种方式伪造MainModule.FileName
属性,以便匹配TinyWall的程序路径。该属性通过System.Diagnostics.ProcessModule
的System.Diagnostics.ModuleInfo.FileName
成员来获取,后者通过psapi.dll
的GetModuleFileNameEx()
原生调用来设置。这些调动位于System.Diagnostics.NtProcessManager
上下文中,用来将.NET环境转换为Windows原生API环境。因此,我们需要研究一下是否可以控制该属性。
经研究证明,该属性来自于PEB(Process Environment Block)结构,而进程所有者可以完全控制该区块。PEB在用户模式下可写,我们可以使用NtQueryInformationProcess
,第一时间获得进程PEB的句柄。_PEB
结构由多个元素所构成,如PRTL_USER_PROCESS_PARAMETERS ProcessParameters
以及双向链表PPEB_LDR_DATA Ldr
等。这两者都可以用来覆盖内存中相关的Unicode字符串。第一个结构可以用来伪造ImagePathName
及CommandLine
,但对我们而言更有趣的是双向链表,其中包含FullDllName
以及BaseDllName
。这些PEB元素正是TinyWall中MainModule.FileName
代码所提取的元素。此外,Phrack在2007年的一篇文章中也详细解释了相关的底层数据结构。
幸运的是,Ruben Boonen(@FuzzySec)已经在这方面做了一些研究,发布了多个PowerShell脚本。其中有个Masquerade-PEB
脚本,可以操控正在运行进程的PEB,在内存中伪造前面提到的属性值。稍微修改该脚本后(该练习同样留给大家来完成),我们可以成功伪造MainModule.FileName
值。
即使我们可以将PowerShell代码移植成C#代码,但我们还是偷了个懒,在C#版的Exploit.exe
中直接导入System.Management.Automation.dll
。我们创建了一个PowerShell实例,读取经过修改的Masquerade-PEB.ps1
,然后调用相应代码,希望能够成功伪造Exploit.exe
的PEB元素。
使用Sysinternals Process Explorer之类的工具检查试验结果,我们可以验证这个猜想,为了后续利用奠定基础,在不需要调试器配合的情况下弹出计算器。
0x06 弹出计算器
现在距离完整利用代码只差一步之遥,前面我们在Exploit.exe
刚开始运行时调用James Forshaw的TypeConfuseDelegate
代码以及Ruben Boonen的PowerShell脚本,现在我们可以进一步连接到TinyWallController
命名管道。更具体一些,我们需要将System.IO.Pipes.NamedPipeClientStream
变量pipeClient
与弹出计算器的gadget一起传入BinaryFormatter.Serialize()
。
感谢Ruben Boonen之前的研究成果,同时在Markus Wulftange小伙伴的帮助下,我很快就实现了完整版利用代码。
0x07 漏洞披露
我们于2019年11月27日向TinyWall开发者反馈了漏洞细节,官方在2.1.13版(2019年12月31日发布)中修复了该漏洞。