在土豆家族提权中,主要是利用SeImpersonatePrivilege权限或SeAssignPrimaryTokenPrivilege权限(除开 Hot Potato),这也是通常在服务账户中所用有的权限,所以在渗透中也会经常从IIS、sqlserver等服务账户中提权到SYSTEM。通常以下用户拥有SeImpersonatePrivilege权限:

  • 本地管理员账户(不包括管理员组普通账户)和本地服务帐户
  • 由SCM启动的服务 在土豆家族提权的具体实现中,本质上都是relay,跟域渗透中的relay区别就在于这是本地relay。但是服务账户在Windows权限模型中本身就拥有很高的权限,所以微软不认为这是一个漏洞。

本地测试发现在本地策略中授予管理员组普通用户 SeImpersonatePrivilege 特权,在 cmd.exe 中 whoami /priv 也不显示该特权,且无法利用,而 SeAssignPrimaryTokenPrivilege 特权则可以正常授予普通用户,利用 ntrights +r SeAssignPrimaryTokenPrivilege -u "xxxxxx"image.png

HotPotato

这个是最初的potato,核心思想是是WPAD或LLMNR/NBNS投毒(细节部分还需要使用DNS exhaust的手段来使DNS解析失败从而走广播LLMNR/NBNS),让某些高权限系统服务请求自己监听的端口,并要求NTLM认证,然后relay到本地的SMB listener,所以其本质上是一个HTTP SMB的reflection NTLM relay。

主要影响Windows 7、8、10、Server 2008 和 Server 2012。

windows域名的解析顺序一般是:hosts>DNS>LLMNR>NBNS,前两种就不说了.

NBNS

NBNS (Net BIOS Name Service)是 Windows 系统中广泛被使用的 UDP 广播服务,即命名查询服务。该服务使用 UDP 协议实现,可以通过发送局域网内广播来实现本地名称解析。类似于 TCP/IP 协议中的 DNS,它负责查找目标机器相应的 IP 地址,并赋予一个 NetBIOS 名称。微软 WINS 服务就是采用 NBNS协议。

系统进行一个名字查询的逻辑如下:

    1. 首先查询本地的 hosts 文件
    1. DNS Lookup 查询(本地 DNS cache,再向 DNS 服务器请求)
    1. NBNS 查询

NBNS 的逻辑是向本地所有主机广播一条消息,谁是 xxx,如果谁响应了该广播消息,谁就是 xxx。 在内网渗透测试时,攻击者往往会监听 NBNS 广播消息,并且会应答自己是 xxx,这就是NBNS 欺骗;ARP欺骗是 MAC 层的欺骗方式。

而在提权中会遇到两个问题。

  • 我们需要使用到NBNS欺骗,但是如果网络中有 DNS 记录,此时就不会用到 NBNS 协议,可以通过 UDP 端口耗尽的攻击技术,让所有 DNS 查询失败,从而必须使用 NBNS 协议
  • NBNS包有1个2字节的TXID字段,必须进行请求响应的匹配,在这里我们假定没有权限进行流量的监听,也就不知道是在哪一个端口进行通信,但由于是通过UDP进行传输,因此可以通过1-65535之间进行泛洪猜测。

LLMNR&WPAD

LLMNR(Link-Local Multicast Name Resolution)是一种名称解析协议,它允许计算机在同一局域网(LAN)内通过多播(multicast)方式解析名称到 IP 地址,而无需依赖传统的 DNS 服务器。LLMNR 通过多播地址 224.0.0.252(IPv4)或 FF02::1:3(IPv6)进行工作。当一台计算机需要解析一个名称但无法通过 DNS 服务器解析时,它会发送一个 LLMNR 查询包到这个多播地址。网络中其他支持 LLMNR 的设备会收到这个请求,如果某台计算机知道该名称的 IP 地址,它会响应并将结果返回给请求者。 LLMNR协议一般工作于5355端口,LLMNR解析过程:

  1. 先检查本地NetBios缓存。
  2. 没有则向局域网224.0.0.252(ff02::fb)广播地址广播LLMNR协议数据包
  3. 收到该广播的数据包,若是要找的主机,则向广播主机单播一个返回数据包

例如利用net use去建立一个不存在的IPC连接时, image.png 这样看起来可以发现LLMNR和arp很像,所以其也存在常规的欺骗攻击,也可以利用中间人用来截获smb hash。

而WPAD是什么呢,wpad全称是Web Proxy Auto-Discovery Protocol,通过让浏览器自动发现代理服务器,定位代理配置文件PAC(在下文也叫做PAC文件或者wpad.dat),下载编译并运行,最终自动使用代理访问网络。 但如何查找到这个文件的位置呢,它在本地网络上搜索名为wpad的计算机以找到该文件。然后执行以下步骤:

  • 1.如果配置了DHCP服务器,则客户端从DHCP服务器中检索wpad.dat文件(如果成功则执行步骤4)
  • 2.wpad.corpdomain.com查询被发送到DNS服务器以查找分发Wpad配置的设备。(如果成功则执行第4步)
  • 3.发送WPAD的LLMNR或NBNS查询(如果成功,请转到第4步,否则无法使用代理)
  • 4.下载wpad.dat并使用它。

我们确实是可以利用 dns 或者 dhcp 毒化,来操控流量指向,但这种方式很容易被拦截,而 LLMNR则不一样,它是通过广播告诉同一内网下的所有 windows,它就是wpad 服务器,这样当你的浏览器设置为’自动检测代理设置”的情况下,它就会下载攻击者事先准备好的wpad.dat 文件,这样一来,客户端的流量就会经过攻击者的机器,例如: image.png 我们手动设置一下代理服务器,并打开自动检测设置,便能看到其以广播方式发送NBNS数据包请求wpad.dat。 image.png image.png

在Windows 系统里,IE 浏览器会自动检测 IE 代理配置信息,方式是访问,http://wpad/wpad.dat&#8221,WPAD 是不一定存在于网络中,因为即使有 DNS 服务器,也没有必要解析 WPAD,除非网络想通过配置脚本自动配置网络中的代理信息,这种情况很方便。因此在 hosts、DNS 查询都不能获取 WPAD 的情况下,系统必然使用 NBNS 进行名字查询,此时可以通过 NBNS 欺骗,告知自己就是 WPAD 可以构造 HTTP 服务器,响应 HTTP http://wpad/wpad.dat&#8221 查询

这样的话,就将查询 WPAD 的流量全部引导至本地 127.0.0.1,即使低权限用户发出的对 WPAD 的 NBNS 欺骗,高权限进程也会受影响,认为 WPAD 就是欺骗后的结果。包括本地管理员进程和 SYSTEM 进程。

HTTP to SMB NTLM Relay

image.png image.png 微软通过补丁封堵了 SMB SMB 协议的重放反射攻击,但是 HTTP SMB 这种跨协议的攻击仍然有效。所以HotPotato即利用了HTTP SMB relay来实现最终的权限提升。

实现

Hot Potato 攻击最终结合了这几点,实现权限提升:

  1. NBNS 欺骗
  2. 构造本地 HTTP,响应 WPAD
  3. HTTP SMB NTLM Relay
  4. 等待高权限进程的访问,即激活更新服务(低权限可激活)

首先创建了NBNSSpoofer类,如何设置了disable_spoof参数为false,则用来进行udf泛洪猜测,同时也耗尽所有udf端口让dns服务失败。 image.png 同时也创建了HTTPNtlmHandler类启动http监听服务,用来做httpsmb relay攻击。

首先设置了一个假的WPAD服务器,它会截取尝试获取wpad.dat文件的请求,使得所有流量都通过127.0.0.1上运行的服务器重定向。 image.png

当有别的请求访问时,都会通过302重定向重定向到 http://localhost/GETHASHESxxxxx,其中xxxxx对应一个唯一标识符 image.png

并且截获到带有GETHASHES的请求,如果没有Authorization头,就会设置401状态码并要求进行NTLM认证。这种是基于http协议的ntlm认证和smb协议稍有不同,过程如图。 image.png image.png

然后就relay到smb即可,首先会提取Authorization消息头中携带Type1(协商) message,然后返回一个WWW-Authenticate: NTLM challenge头,携带challenge。并且会开始smbrelay的线程。 image.png image.png

在smbrelay中,主要是利用ntlmQueue队列中的认证数据来尝试执行cmd.exe,如果成功就在队列中添加一个byte 99的标志 image.png

完成前两个阶段,state会设置为1,继续把客户端加密过的challenge添加到ntlmQueue队列再relay到smb就能完成认证,然后以创建运行用户定义命令的新系统服务。这里最终会利用99标志位判断是否relay成功。 image.png

在doPsexec中最终通过调用 CreateServiceW 创建一个新的服务,并通过 StartService 启动它 image.png

完成的提权流程图。 image.png

局限&后续

  • 对于没有不携带defender的windows server ,需要手动检查 Windows 更新,并且如果网络已经有WPAD的 DNS 条目,重新让DNS端口耗尽会有很多麻烦。
  • 在Windows 8/10/Server 2012中,Windows Update 似乎不再遵守Internet 选项中设置的代理设置或检查 WPAD,而是使用netsh winhttp 代理控制 Windows 更新的代理设置。需要利用自动更新机制,该机制每天下载证书信任列表 (CTL),但需要等待24 小时或找到其他方式来触发此更新。
  • Microsoft 通过使用已经在进行中的质询来禁止相同协议的 NTLM 身份验证来修补此问题 (MS16-075)。这意味着从一台主机到其自身的 SMBSMB NTLM 中继将不再起作用。
  • MS16-077 WPAD 名称解析将不使用 NetBIOS (CVE-2016-3213) 并且在请求 PAC 文件时不发送凭据 (CVE-2016-3236)。WAPD MITM Attack 已修补。

Rotten Potato & Juicy Potato

这两种potato是一个原理,区别就是JuicyPotato是对Rotten Potato的完善。 此技术不适用于 >= Windows 10 1809 和 Windows Server 2019 的版本。这种不同于初始的Potato,它是通过DCOM call来使服务向攻击者监听的端口发起连接并进行NTLM认证。

DCOM(分布式组件对象模型)是微软基于组件对象模型(COM)的一系列概念和程序接口,它支持不同的两台机器上的组件间的通信,不论它们是运行在局域网、广域网、还是Internet上。利用这个接口,客户端程序对象能够向网络中另一台计算机上的服务器程序对象发送请求。

DCOM和COM组件的主要区别就是DCOM 是基于 COM 的扩展,它允许 COM 组件跨网络在不同计算机上进行通信,主要使用RPC协议,而COM只是用于本地调用。所以可以将DCOM理解为通过RPC实现的COM,所以其在横向移动的时候也能利用。

几个小知识:

  1. 使用 DCOM 时,如果以服务的方式远程连接,那么权限为 System,例如 BITS 服务。当然该服务也需要以高权限运行。
  2. 使用 DCOM 可以通过 TCP 连接到本机的一个端口,发起 NTLM 认证,该认证可以被重放
  3. LocalService 用户默认具有 SeImpersonate 和 SeAssignPrimaryToken 权限
  4. 开启 SeImpersonate 权限后,能够在调用 CreateProcessWithToken 时,传入新的 Token 创建新的进程
  5. 开启 SeAssignPrimaryToken 权限后,能够在调用 CreateProcessAsUser 时,传入新的 Token 创建新的进程

OBJREF

在DCOM中,OBJREF(对象引用)是一个重要的概念。它表示一个远程对象的引用,用于在远程调用时传递对象的信息。OBJREF包含了对象的OXID、OID和IPID等信息,用于指定对象的位置和接口。

OXID(对象交互标识符)是OBJREF中的一个重要部分,它表示一个远程对象的唯一标识符,用于在远程调用时识别该对象。OXID通常由一个GUID和一个服务器地址组成,用于指定该对象所在的计算机。

OID(对象标识符)也是OBJREF中的一个重要部分,它表示一个对象的唯一标识符,用于在单个计算机中识别该对象。OID通常由一个GUID组成,用于唯一标识一个对象。

IPID(对象接口标识符)也是OBJREF中的一个重要部分,它表示一个对象的接口的唯一标识符,用于在单个计算机中识别该接口。IPID通常由一个GUID组成,用于唯一标识一个接口。

在DCOM中,OBJREF是一个重要的概念,它表示一个远程对象的引用。OBJREF包含了对象的OXID、OID和IPID等信息,用于指定对象的位置和接口。在进行远程调用时,OBJREF用于传递对象的信息,从而实现对象的交互和通信。

滥用COM

Rotten Potato使用CoGetInstanceFromIStorage通过指定的类标识符(CLSID)从给定的存储对象(由 IStorage 接口表示)中创建并初始化一个 COM 对象。它可以通过指定服务器信息、类标识符、外部接口、上下文环境和存储对象来创建一个新的COM对象实例,并通过指定接口标识符来返回该实例。它可以用于在应用程序中访问和使用存储在存储对象中的COM对象。

函数原型如下,其中pClsid指向要从存储中创建的 COM 对象的类 ID (CLSID)。pStg指向存储对象 (IStorage 接口),其中包含持久化的 COM 对象,COM 将从这个存储中加载对象。pResults指向一个 MULTI_QI 结构数组,COM 将返回指定数量的接口(dwCount),每个接口都会通过 MULTI_QI 结构返回接口指针及其结果。

HRESULT CoGetInstanceFromIStorage(
  LPUNKNOWN pUnkOuter,
  CLSID     *pClsid,
  LPUNKNOWN pUnkReserved,
  DWORD     dwClsCtx,
  LPSTORAGE pStg,
  DWORD     dwCount,
  MULTI_QI  *pResults
);

在这样一段代码中,Rotten Potato先用CreateStorage创建了一个IStorage对象(关于IStorage),并通过TestClass实现了IStorage和IMarshal接口,所以其可以处理 COM 对象的自定义封送(序列化、解序列化等)以及对存储对象进行操作。最后调用CoGetInstanceFromIStorage尝试从TestClass存储对象中获取COM对象(BITS)的实例。 image.png

其中4991d34b-80a1-4291-83b6-3328366b9097就是BITS服务的CLSID。00000000-0000-0000-C000-000000000046IUnknown接口的guid,所以这里会尝试查询com对象的IUnknown接口。 image.png

在testclass中主要在于这两个方法。GetUnmarshalClass用于返回一个 Guid,表示用于解封送对象的类,这里00000306-0000-0000-C000-000000000046是PointerMoniker,它允许将一个 COM 对象的指针封装在一个 Moniker 中,也就是指的是对象的引用,参考CreatePointerMoniker的使用,所以在调用CoGetInstanceFromIStorage时,COM服务会获取反序列化的Class类型,然后根据类型进行下一步反序列化。

image.png

MarshalInterface用于在调用在序列化对象时,写入了引用的对象,并且将对象引用的端口监听地址修改成自己启动的假的IRemUnknownServer。所以这里我们在告诉COM我们想要一个BITS对象的实例,我们想要从 127.0.0.1端口 6666加载它,因此,现在 DCOM对象反序列化的时候会尝试在端口 6666 上与我们通信,我们已在该端口上启动了本地 TCP 侦听器。如果我们以正确的方式回复,则 COM(以 SYSTEM 帐户运行)会尝试与我们执行 NTLM 身份验证。

image.png

而为什么能欺骗com对象访问我们的TCP监听器,与DCOM通信过程有关。

代码通过传递BITS的CLSID和IStorage对象实例给CoGetInstanceFromIStorage函数,使rpcss激活BITS服务,随后rpcss的DCOM OXID resolver会解析序列化数据中的OBJREF拿到DUALSTRINGARRAY字段,该字段指定了host[port]格式的location,绑定对象时会向其中的host[port]发起DCE/RPC(Distributed Computing Environment)请求。这个host[port]由攻击者监听的,如果攻击者要求NTLM身份验证,高权限服务就会发送net-NTLM进行认证。

实现

rotten potato的代码实现是基于Hot potato修改的,基本保留了原有的代码,所以这里只看添加部分吧,并且作者原文分析的非常清楚了。经过上述分析,现在我们已经可以在6666端口充当中间人了,这里通过DCERPCNtlmHandler开启了一个server,用于进行NTLM中继。并且另一个线程也执行了BootstrapComMarshal函数来调用COM服务。 image.png

在DCERPCNtlmHandler中主要进行这么几个事情:

  1. 监听 DCOM 客户端连接:代码在端口 6666 上等待 DCOM 客户端发起连接。
  2. 连接 Windows RPC 服务:在接受 DCOM 客户端的连接后,程序会建立到 Windows RPC 服务(端口 135)的连接。
  3. 中继 NTLM 身份验证:程序将客户端和 Windows RPC 服务的 NTLM 验证流量通过代理中继。通过将来自客户端的身份验证请求发送到 Windows RPC 服务,再将 RPC 服务返回的 NTLM 挑战发送回客户端,从而完成身份验证。
  4. 多次尝试:程序会在循环中多次尝试建立这些连接,直到服务成功启动并处理身份验证流量。

image.png

最终会在NTLMRelayingProxy完成NTLM 中继和本地令牌协商。这里翻译部分作者原文rotten-potato,对rpc协议的请求处理确实很巧妙

这里作者使用非常巧妙的一点来避免不同版本的Windows RPC报文的问题,即将我们在TCP端口6666上从COM接收到的任何数据包中继回TCP端口135上的本地 Windows RPC 监听器。由于我们收到的这些数据包是有效RPC对话的一部分,无论我们使用的是什么版本的Windows都会做出适当的反应。然后,我们可以使用从 135上的Windows RPC收到的这些数据包作为我们对COM的回复的模板。

具体可以看图,我们收到的第一个数据包(数据包 #7)来自端口 6666(我们的侦听器,这是 COM 与我们通信)。接下来,我们将同一个数据包(数据包 #9)中继到 TCP 135 上的 RPC。然后在数据包 #11 中,我们收到来自 RPC(TCP 135)的回复,在数据包 #13 中,我们将该回复中继到 COM。 我们只需重复此过程,直到进行 NTLM 身份验证为止。您可以将这些初始数据包视为最终 NTLM 身份验证的准备阶段。

image.png

在startRelay中首先开始与com对象的relay,然后开始和rpc的relay image.png

在两个线程中,主要逻辑就是,把com对象穿过来的认证请求先发给rpc,然后把rpc返回的结果当作模版进行处理,再发回给com对象,重复次过程,知道完成NTLM身份验证。 image.png

两个线程都会通过doNTLMMagic处理ntlm数据,这里主要讨论enable_token为true时候的过程,因为smb relay基本和rotten potato一样。在ntlm_bytes[8] == 1时即代表协商阶段,会开始token relay线程,大体过程如下。

  • InitializeServer函数中,为了使用 NTLM 身份验证在本地协商安全令牌,必须首先调用函数AcquireCredentialsHandle来获取我们需要的数据结构的句柄,其中其中SECPKG_CRED_BOTH代表着验证传入凭据或使用本地凭据准备传出令牌。此标志启用其他两个标志,hCred指向CredHandle结构的指针,用于接收凭证句柄。
  • 然后在第一次调用AcceptSecurityContext时,将从该数据包中提取的 NTLM Type 1(Negotiate)消息作为输入传递 image.png

如图,当我们从 COM 收到 NTLM 类型 1(协商)消息时,我们会撕掉数据包的 NTLM 部分(如下所示),并使用它来开始本地协商令牌的过程,传给AcceptSecurityContextimage.png

在这里需要看一下AcceptSecurityContext的函数解析。AcceptSecurityContext 用于服务器端接收和处理客户端发送的安全令牌(如 NTLM 或 Kerberos 令牌)。在网络协议中,服务器通过该函数验证客户端的身份,并可能发送响应令牌以继续身份验证过程。如果使用的是多步骤身份验证协议,服务器会多次调用这个函数来处理身份验证。

拿NTLM认证过程举例就是,我们可以把每次客户端发来的令牌传给AcceptSecurityContext就会逐渐建立一个完整的安全上下文,并且能够通过这个上下文模拟客户端的权限去执行cmd,所以在上述过程中我们会调用两次AcceptSecurityContext,一次即质询阶段,另一次在协商阶段,最终能够模拟COM对象服务的权限,这里就是SYSTEM。

SECURITY_STATUS AcceptSecurityContext(
    PCredHandle phCredential,       // 服务器的凭据句柄
    PCtxtHandle phContext,          // 安全上下文句柄,如果是第一次调用则为空
    PSecBufferDesc pInput,          // 客户端发送的安全令牌
    unsigned long fContextReq,      // 请求的上下文属性
    unsigned long TargetDataRep,    // 数据表示格式,一般为 SECURITY_NATIVE_DREP
    PCtxtHandle phNewContext,       // 返回新的上下文句柄
    PSecBufferDesc pOutput,         // 返回给客户端的令牌
    unsigned long *pfContextAttr,   // 返回的上下文属性
    PTimeStamp ptsExpiry            // 上下文的有效期
);

此时在已经将NTLM 类型 1(协商)数据包转发到端口 135 上的 RPC了,RPC 现在将使用 NTM 类型 2(质询)数据包进行回复。在这个阶段,需要将数据包中的challenge进行替换。 image.png

所以rpc返回的数据包和最后发送会com的数据包会有一点差异。 image.png

而为什么要替换challenge,可以看看challenge是怎么来的,在协商的结尾调用 new_ntlmQueueOut.Take();获取了challenge,而new_ntlmQueueOutTokenRelay函数的参数。具体如图,可以看到其实在第一次调用AcceptSecurityContext时,其会生成返回给客端的令牌,保存在ServerToken中,而ServerToken会被添加到hashesOut中也即new_ntlmQueueOut,所以能够调用Take获取,而生成的令牌也就是在质询阶段需要返回的challenge。如果不进行替换,第二次调用AcceptSecurityContext就会失败。这里也许需要看看本地 NTLM 身份验证的工作原理。 image.png

现在我们已将修改后的 NTLM Type 2(协商)数据包转发到 COM,其中“Challenge”和“Reserved”字段与“AcceptSecurityContext”的输出相匹配。“Reserved”字段实际上是对 SecHandle 的引用,当 SYSTEM 帐户收到 NTLM Type 2 消息时,它将在内存中后台执行身份验证。这就是为什么更新“Reserved”字段如此重要的原因。否则,它将向 RPC 而不是 US 进行身份验证。

最后COM 将向我们发送 NTLM Type 3(身份验证)数据包。该数据包为空(因为此处的所有实际身份验证都发生在内存中),但我们将使用它来最终调用“AcceptSecurityContext”,然后,我们可以使用最后一次调用AcceptSecurityContext的结果来调用“ImpersonateSecurityContext”来获取模拟令牌。 image.png

最后我们以一张图来看看整个Rotten Potato提权的大体流程: image.png

局限

参考https://decoder.cloud/2018/10/29/no-more-rotten-juicy-potato/

  • COM 不与我们的本地侦听器交谈,即现在DCOM组件不会和除135端口以外的其他端口进行通信,这意味着无法通过6666端口充当中间人
  • 将数据包发送到我们控制下侦听端口 135 的主机,然后将数据转发到我们本地的 COM 侦听器是行不通的。问题是在这种情况下,客户端不会协商本地身份验证。

PrintSpoofer(PipePotato or BadPotato)

这个提权过程和原理较为简洁。不同于上述windows 本地提权方式,PrintSpoofer利用pipe管道,并且配合Spoof打印机在UNC路径的漏洞,让SYSTEM权限运行的spoofer服务强制连接到我们创建的恶意pipe管道符,最终通过ImpersonateNamedPipeClient来模拟已连接的管道客户端权限(还有类似的ImpersonatedLoggedOnUserRpcImpersonateClient函数)。

pipe

管道(PIPE)是一项古老的技术,可以在 Unix、Linux、Windows 等多种操作系统中找到,其本质是用于进程间通信的共享内存区域。在 Windows 系统中,存在两种类型的管道:匿名管道(Anonymous Pipes)和命名管道(Named Pipes)。

  • 匿名管道 匿名管道用于重定向子进程的标准输入或输出,以便它可以与其父进程交换数据。若要(双工操作)双向交换数据,必须创建两个匿名管道。父进程使用写入句柄将数据写入到一个管道,而子进程则使用该管道的读取句柄从该管道读取数据。同样,子进程将数据写入其他管道,父进程从中读取数据。匿名管道不能通过网络使用,也不能在不相关的进程之间使用。

  • 命名管道 命名管道用于在不是相关进程的进程之间传输数据,以及不同计算机上的进程之间的数据。通常,命名管道服务器进程会创建具有已知名称或要与其客户端通信的名称的命名管道。知道管道名称的命名管道客户端进程可以打开其另一端,但受命名管道服务器进程指定的访问限制。服务器和客户端都连接到管道后,可以通过对管道执行读取和写入操作来交换数据。

在本地可以有几种方式都能查看所有的管道列表:

  • powershell
# PowerShell V3 以下版本
[System.IO.Directory]::GetFiles("\\.\pipe\")
# PowerShell V3 以上版本
Get-ChildItem "\\.\pipe\"
  • pipelist.exe
  • 浏览器:file://.//pipe//

Impersonate a Named Pipe Client

主要利用ImpersonateNamedPipeClient函数实现,功能相似的有RpcImpersonateClient,对于公开 RPC/COM 接口的 Windows 服务,每当您调用由作为高特权帐户运行的服务公开的 RPC 函数时,该服务可能会调用 RpcImpersonateClient() 函数来模拟客户端,以在客户端的安全上下文中运行代码或创建进程,从而降低特权提升漏洞的风险。

这里完善一下itm4n的代码分析一下

// PipePotatoTest.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
 
#include <windows.h>
#include <iostream>
#include <sddl.h>  // ConvertStringSecurityDescriptorToSecurityDescriptor
#include <tchar.h> // TCHAR
 
// 声明自定义函数
void PrintLastErrorAsText(DWORD errorCode);
void PrintTokenUserSidAndName(HANDLE hToken);
void PrintTokenImpersonationLevel(HANDLE hToken);
void PrintTokenType(HANDLE hToken);
void DoSomethingAsImpersonatedUser();
 
// main 函数入口
int wmain(int argc, wchar_t* argv[])
{
    if (argc < 2) {
        wprintf(L"Usage: %ls <PipeName>\n", argv[0]);
        return -1;
    }
 
    HANDLE hPipe = INVALID_HANDLE_VALUE;
    LPWSTR pwszPipeName = argv[1];
    SECURITY_DESCRIPTOR sd = { 0 };
    SECURITY_ATTRIBUTES sa = { 0 };
    HANDLE hToken = INVALID_HANDLE_VALUE;
 
    if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) {
        wprintf(L"InitializeSecurityDescriptor() failed. Error: %d - ", GetLastError());
        PrintLastErrorAsText(GetLastError());
        return -1;
    }
 
    sa.nLength = sizeof(sa);
    sa.bInheritHandle = FALSE;
    sa.lpSecurityDescriptor = &sd;
 
    if (!ConvertStringSecurityDescriptorToSecurityDescriptor(L"D:(A;OICI;GA;;;WD)", SDDL_REVISION_1, &sa.lpSecurityDescriptor, NULL)) {
        wprintf(L"ConvertStringSecurityDescriptorToSecurityDescriptor() failed. Error: %d - ", GetLastError());
        PrintLastErrorAsText(GetLastError());
        return -1;
    }
 
    hPipe = CreateNamedPipe(
        pwszPipeName,
        PIPE_ACCESS_DUPLEX,  // 双向访问
        PIPE_TYPE_BYTE | PIPE_WAIT,  // 字节流 + 等待模式
        10,  // 最大实例数
        2048,  // 输出缓冲区大小
        2048,  // 输入缓冲区大小
        0,     // 默认超时时间
        &sa    // 安全属性
    );
 
    if (hPipe != INVALID_HANDLE_VALUE) {
        wprintf(L"[*] Named pipe '%ls' listening...\n", pwszPipeName);
        ConnectNamedPipe(hPipe, NULL);
        wprintf(L"[+] A client connected!\n");
 
        if (ImpersonateNamedPipeClient(hPipe)) {
            if (OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken)) {
                PrintTokenUserSidAndName(hToken);
                PrintTokenImpersonationLevel(hToken);
                PrintTokenType(hToken);
 
                DoSomethingAsImpersonatedUser();
 
                CloseHandle(hToken);
            }
            else {
                wprintf(L"OpenThreadToken() failed. Error = %d - ", GetLastError());
                PrintLastErrorAsText(GetLastError());
            }
        }
        else {
            wprintf(L"ImpersonateNamedPipeClient() failed. Error = %d - ", GetLastError());
            PrintLastErrorAsText(GetLastError());
        }
 
        CloseHandle(hPipe);
    }
    else {
        wprintf(L"CreateNamedPipe() failed. Error: %d - ", GetLastError());
        PrintLastErrorAsText(GetLastError());
    }
 
    return 0;
}
 
// 打印最后一个错误信息
void PrintLastErrorAsText(DWORD errorCode)
{
    LPWSTR errMsg = NULL;
    DWORD size = FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
        NULL,
        errorCode,
        0,
        (LPWSTR)&errMsg,
        0,
        NULL
    );
 
    if (size) {
        wprintf(L"%ls\n", errMsg);
        LocalFree(errMsg);
    }
    else {
        wprintf(L"Unknown error.\n");
    }
}
 
// 打印模拟令牌用户的SID和名称
void PrintTokenUserSidAndName(HANDLE hToken)
{
    DWORD dwSize = 0;
    GetTokenInformation(hToken, TokenUser, NULL, 0, &dwSize);
    PTOKEN_USER pTokenUser = (PTOKEN_USER)malloc(dwSize);
 
    if (GetTokenInformation(hToken, TokenUser, pTokenUser, dwSize, &dwSize)) {
        LPWSTR sidString;
        ConvertSidToStringSid(pTokenUser->User.Sid, &sidString);
        wprintf(L"User SID: %ls\n", sidString);
        LocalFree(sidString);
    }
 
    free(pTokenUser);
}
 
// 打印模拟级别
void PrintTokenImpersonationLevel(HANDLE hToken)
{
    DWORD dwSize = 0;
    GetTokenInformation(hToken, TokenImpersonationLevel, NULL, 0, &dwSize);
    SECURITY_IMPERSONATION_LEVEL impLevel;
    if (GetTokenInformation(hToken, TokenImpersonationLevel, &impLevel, sizeof(impLevel), &dwSize)) {
        wprintf(L"Impersonation Level: %d\n", impLevel);
 
    }
}
 
// 打印令牌类型
void PrintTokenType(HANDLE hToken)
{
    DWORD dwSize = 0;
    TOKEN_TYPE tokenType;
    GetTokenInformation(hToken, TokenType, &tokenType, sizeof(tokenType), &dwSize);
    wprintf(L"Token Type: %s\n", tokenType == TokenPrimary ? L"Primary" : L"Impersonation");
}
 
// 模拟用户操作
void DoSomethingAsImpersonatedUser()
{
    wprintf(L"[+] Performing operations as impersonated user...\n");
    // 在这里执行模拟用户权限下的操作
}

关于CreateNamedPipeA,函数结构如下,其中lpName是管道name,lpSecurityAttributes表示管道的权限,

HANDLE CreateNamedPipeA(
  [in]           LPCSTR                lpName,
  [in]           DWORD                 dwOpenMode,
  [in]           DWORD                 dwPipeMode,
  [in]           DWORD                 nMaxInstances,
  [in]           DWORD                 nOutBufferSize,
  [in]           DWORD                 nInBufferSize,
  [in]           DWORD                 nDefaultTimeOut,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
);

这里设置everyone权限,以防访问失败,sddl为"D:(A;OICI;GA;;;WD)。在创建了管道之后,我们利用ConnectNamedPipe来等待访问者连接,当存在连接,便可以使用神奇的ImpersonateNamedPipeClient来模拟客户端安全上下文。

根据MSDN,通常有以下情况,所有模拟函数(包括ImpersonateNamedPipeClient)都允许请求的模拟:

现在我们用管理员创建pipe,因为其默认有SeImpersonatePrivilege权限,并用普通用户访问,很明显我们获取到了一个模拟级别为2的模拟令牌。

image.png

例如这里我们可以最后用CreateProcessAsUser创建新的进程。

image.png

所以问题就是如何诱骗帐户NT AUTHORITY\SYSTEM连接到我们的命名管道。

实现

当然答案就是用打印机服务,其实在AD渗透中,我们就会用到过SpoolSample来强制 Windows 主机通过 MS-RPRN RPC 接口向其他计算机进行身份验证,这对于配置了非约束委派的系统有很大的攻击性。在spoolsv.exe服务有一个公开的 RPC 服务,里面有RpcRemoteFindFirstPrinterChangeNotificationEx函数,同样SpoolSample也利用了它。

DWORD RpcRemoteFindFirstPrinterChangeNotificationEx( 
    /* [in] */ PRINTER_HANDLE hPrinter,
    /* [in] */ DWORD fdwFlags,
    /* [in] */ DWORD fdwOptions,
    /* [unique][string][in] */ wchar_t *pszLocalMachine,
    /* [in] */ DWORD dwPrinterLocal,
    /* [unique][in] */ RPC_V2_NOTIFY_OPTIONS *pOptions)

根据文档,此功能_创建一个远程更改通知对象,该对象监视打印机对象的更改,并使用或向打印客户端发送更改通知RpcRouterReplyPrinter``RpcRouterReplyPrinterEx。而在函数结构中,pszLocalMachine参数需要传递 UNC 路径,所以这些通知明显是通过命令管道传给对象的。

而这里还需要结合到管道符对于UNC路径的处理小漏洞,pszLocalMachine参数需要传递 UNC 路径,传递 \\127.0.0.1 时,服务器会访问 \\127.0.0.1\pipe\spoolss,但这个管道已经被系统注册了,如果我们传递 \\127.0.0.1\pipe 则因为路径检查而报错

但当传递 \\127.0.0.1/pipe/foo 时,校验路径时会认为 127.0.0.1/pipe/foo 是主机名,随后在连接 named pipe 时会对参数做标准化,将 / 转化为 \,于是就会连接 \\127.0.0.1\pipe\foo\pipe\spoolss,攻击者就可以注册这个 named pipe 从而窃取 client 的 token。 image.png

在代码中,首先检查一下权限 image.png

然后GenerateRandomPipeName生成了一个随机的uuid,用于标识我们创建的管道符号。 image.png

然后创建管道符和等待连接。 image.png

在TriggerNamedPipeConnection中最终会调用RpcRemoteFindFirstPrinterChangeNotificationEx出发spoofer访问恶意pipe image.png image.png

局限

  • 该手段由于并不是微软承认的一个漏洞,因此实际上还是比较实用和不受限制的,但是由于近年来在PrintNightmare爆发之后,很多企业会选择关闭spoolss服务,因此使得Printerbug失效,从而导致pipePotato的失效
  • 并且UNC路径解析错误也被修复后,整个提权也失去作用。

RoguePotato

在上述potato被提出之后,微软进行了一些修复。

  • OXID 解析器是“rpcss”服务的一部分,在端口 135 上运行。从 Windows 10 1809 和 Windows Server 2019 开始,不再可能在 135 以外的端口上查询 OXID 解析器,即高版本Windows DCOM解析器不允许OBJREF中的DUALSTRINGARRAY字段指定端口号。
  • 如果您指定远程 OXID 解析器,则请求将使用“匿名登录”进行处理。

所以如果还想通过OXID解析器进行欺骗RPC,就需要寻找别的方法。

  • 为了绕过这个限制并能做本地令牌协商,RoguePotato在一台远程主机上的135端口做流量转发,将其转回受害者本机端口,并写了一个恶意RPC OXID解析器。
  • 但是ncacn_ip_tcp返回的是Identification Token(识别令牌),没什么用,通常需要的是Impersonation Token。所以其也结合了PrintSpoofer的思路,使用ncacn_np:localhost/pipe/roguepotato[\pipe\epmapper]让RPCSS连接。

所以这里代码中主要存在两个实现方法,一个是去开启一个恶意RPC OXID解析器,另一个则是同Rotten Potato里使用CoGetInstanceFromIStorage去进行DCOM滥用。 image.png

深入分析OXID解析序列

这块的逻辑主要在RunRogueOxidResolver函数中,首先会调用RpcServerUseProtseqEpA指定RPC服务使用的传输协议,ncacn_ip_tcp代表基于 TCP/IP 的 RPC 传输协议。然后调用RpcServerRegisterIf2注册一个可调用接口,IObjectExporter_v0_0_s_ifspec即对应,RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH指定接口允许使用安全回调函数,即使客户端没有经过身份验证。 image.png

然后调用RpcServerRegisterAuthInfoA 函数来注册 RPC 服务器的身份验证信息,这里指定NTLM认证。并且将Endpoint Map(终结点映射器)的信息报存在pbindingVector中。 image.png

在 DCOM 或 RPC 系统中,终结点指的是服务器在网络上监听的特定协议、IP 地址和端口。由于服务器可以使用动态终结点(动态端口分配),客户端并不知道服务器绑定在哪个端口上。为了解决这个问题,RPC 系统引入了终结点映射器。

在 Windows 上,终结点映射器 是一个由操作系统管理的服务,用于支持 RPC 通信。Windows 的终结点映射器负责维护所有 RPC 服务的终结点,并向客户端提供这些终结点的信息。 • 静态终结点:服务器使用特定的协议和端口(例如 ncacn_ip_tcp:5000)作为静态终结点。客户端事先知道终结点信息,无需查询终结点映射器。 • 动态终结点:服务器动态分配端口号,客户端需要通过终结点映射器来查找这些动态分配的终结点。

服务器端: • 服务器注册 RPC 接口时,调用 RpcEpRegister 将其接口与绑定的终结点(如端口号、命名管道等)注册到终结点映射器。 • 服务器可以使用静态终结点(固定的端口号)或动态终结点(动态分配端口)。如果使用动态终结点,服务器会通过RpcServerInqBindings 查询分配到的终结点信息,并将其注册到终结点映射器。

客户端: • 客户端在不知道服务器的具体终结点的情况下,通过调用 RpcMgmtEpEltInqBegin 和 RpcMgmtEpEltInqNext 来查询终结点映射器,获取服务器端的终结点信息(例如 TCP 端口、命名管道等)。 • 或者客户端可以通过 ResolveOxid 或 ResolveOxid2 来解析服务器的 OXID,从而间接获得服务器的终结点信息。

同样也可以用rpcview查看终结点映射器的部分信息. image.png

解析oxid的逻辑在ResolveOxid2中,ResolveOxid2 是一种远程过程调用,用来解析对象标识符 (OXID) 并获取与对象对应的绑定信息(包括协议、端点等)。

  • 99fcfec4-5260-101b-bbcb-00aa0021347a代表IObjectExporter接口的uuid
  • endpoint指向使用命名管道符,用于 DCOM 的远程调用绑定,并且这里使用了UNC路径的欺骗让rpc连接到恶意的管道符号,这一点跟pringSpoofer一致,同时指定使用ncacn_np协议,果注释掉这一行并使用 0x07,则会切换为 ncacn_ip_tcp 协议(基于 TCP 的 RPC 通信)。
  • nEntries 是多字符串数组的条目数,包括端点字符串、协议、身份验证等内容。最后这些RPC 的字符串绑定信息都会存储到 DUALSTRINGARRAY 结构体。 image.png

再来看看流量方面,这里.197是另外一台可控主机,将135端口流量转发到待提权windows上。 sudo socat tcp-listen:135,reuseaddr,fork tcp:10.21.171.88:9999 .88是本地windows,开启恶意rpc服务器监听9999端口。 \RoguePotato.exe -r 10.21.213.197 -e "C:\windows\system32\cmd.exe" -l 9999

首先在调用rpc之前会进行一个TCP三次握手保证连接,然后会有4个DCERPC数据包

在第一个DCERPC数据包中,涉及我们注册的两个接口对象,就是IOXIDResolver,这就是 COM 中解析 OXID的接口。同时还要求利用NTLM身份认证,很明显此时发送的是NTLM 协商消息 (NEGOTIATE)。所以可以看出来这个包大体只是用来查询相关接口的。 image.png

第二个包主要是对于上述接口查询的请求进行确认,然后进行NTLM的质询阶段,会发送challenge。 image.png

第三包和第二个差不多,第四个包已经进行完成了ntlm认证,并且这里能够看到是匿名登录。因为指定远程 OXID 解析器,则请求将使用“匿名登录”进行处理。 image.png

此时DCOM 服务器将连接到 RPC 服务器以执行_IRemUnkown2_接口调用,在执行 IRemUnknown2 接口调用时,通常会触发权限检查,以确保客户端有权访问该远程对象。这时可能会触发 SecurityCallback,来进行权限认证和安全验证。 image.png

所以在作者最开始的实验中,此处已经可以触发身份验证回调,如果我们拥有SE_IMPERSONATE_NAME权限,我们可以通过RpcImpersonateClient()调用模拟调用者,但很明显这是匿名用户,最终只能获取到一个Identification Token,并不能实现真正的提权。

在完成NTLM身份认证后,开始解析 OXID流程,最终目的其实就是获取OBJREF(对象引用)。 image.png

参考MSDN流程 image.png

首先在SERVERALIVE_REQ中会调用ServerAlive2 image.png

在第一个IOXIDResolver包中,RPC回复了可用协议 image.png

第二个包客户端则开始请求调用RPC的ResolveOxid2, image.png

在第三个包,RPC服务器返回绑定信息,这里能看出即对应绑定的principalNameendpoint。最后一个包则是客户端的确认回复。 image.png

此时客户端获取了完整的OBJREF信息,则去调用相应的COM对象,而访问对象正是恶意命名管道符,后续则和PrintSpoofer一致。

局限

感觉和PrintSpoofer差不太多,并且还需要另一台主机作流量转发。

SweetPotato

repo: https://github.com/CCob/SweetPotato

这是COM/WinRM/Spoolsv的集合版,也就是Juicy/PrintSpoofer,上述已经分析过。

PetitPotam

PrintNotifyPotato

CoercedPotato

参考文章