depy

It is a long and beautiful life.

反序列化字符逃逸

网络安全

前言

学长今天早上问了我一道CTF题目,代码如下:

<?php
    include_once 'flag.php';
    highlight_file(__FILE__);
    // Security filtering function
    function filter($str){
        return str_replace('secure', 'secured', $str);
    }
    class Hacker{
        public $username = 'margin';
        public $password = 'margin123';
    }
    $h = new Hacker();
    if (isset($_POST['username']) && isset($_POST['password'])){
        // Security filtering
        $h->username = $_POST['username'];
        $c = unserialize(filter(serialize($h)));
        if ($c->password === 'hacker'){
            echo $flag;
        }
    }

上面代码的意思是新建一个对象,当password属性为hacker的时候,会输出flag。

几个关键的点:

  1. 可控的参数是username,password属性写死的

  2. 有一个不明所以的filter方法

一开始看也没看觉得是很垃圾的一道题目,新建个序列化类替换属性,让他反序列化,后来才看见他的对象并不可控。

问了下王哥,考点是反序列化字符逃逸,fine,又是我没接触过的知识点,又可以学习了。

php的特性

PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),

60640e9f07db1.png

60640ef2b556e.png

60640f1aa6725.png

很明显,当存在}来结尾之后,无论之后存在什么内容,都不会影响最终的反序列化。

所以上面的代码怎么做就很明显了。

解题

$h->username = $_POST['username'];
$c = unserialize(filter(serialize($h)));

这里通过post方法,获取到username,覆盖属性username,然后进入下面的代码中。先对对象序列化,然后再过滤字符,最后反序列化。

做题之前,得想清楚我们的目的,覆盖password属性为hacker。

那么我们就需要构造出这样的序列化内容:

O:6:"Hacker":2:{s:8:"username";s:6:"margin";s:8:"password";s:5:"hacker";}s:8:"password";s:9:"margin123";}

发现 

";s:8:"password";s:5:"hacker";}

如果作为username的一部分,那么反序列化的时候就会在}前结束,从而覆盖password为hacker。

60641453db9c1.png

但是好气啊,反序列化是要根据s:41那个数字进行反序列化的。

1123213123明显没有41位,所以会导致反序列化不会到达预期效果,而是从当前位置往后截取41位当作username。

606415e7d91a8.png

606415fb1f5f6.png

那么我们就需要一个方法让他读取的内容没有到我们设置的password上。

我们观察到有个filter方法,他会把secure变成secured

效果是这样:

  1. 输入secure";s:8:"password";s:6:"hacker";} 进行序列化效果如下

606416b68c05a.png

O:6:"Hacker":2:{s:8:"username";s:37:"secure";s:8:"password";s:6:"hacker";}";s:8:"password";s:9:"margin123";}

进行filter后

O:6:"Hacker":2:{s:8:"username";s:37:"secured";s:8:"password";s:6:"hacker";}";s:8:"password";s:9:"margin123";}

各自反序列化后 一个成功 一个反序列化失败

606417749d1b8.png

因为这个时候 s:37 原本读到的是

606417eb40afd.png

但是secure扩充了一位 也就是上面的字符串secure变成secured 也就是总字数是38 

这时读取到了; 没有读取到} 那么}就变成单独存在的东西然后就要报错了 所以反序列化失败

也就是 每次多一个secure 总字数会多一个 读取的内容会往前一位,那么不妨设想,一直多一个secure,那么总有一个时间点

6064189d09306.png

那么就符合我们的预期了,username已经读取完了。就轮到我们够造的password反序列化了,刚好反序列化是hacker,到}了,所以后面的内容全部丢弃。

那就计算一下吧

606418fdb3953.png

这些一共有31位 每多一个secure 就会往前一位

所以只要填充31个secure就可以了

60641953006bd.png


60641966287df.png

那么那道题就是这样子的做法 先设置变量 进入判断 填充31个secure进行字符逃逸,最终反序列化后的属性就会被我们构造的hacker替换

606419d475a98.png