zoukankan      html  css  js  c++  java
  • [DDCTF 2019]homebrew event loop

    0x00 知识点

    逻辑漏洞:

    异步处理导致可以先调用增加钻石,再调用计算价钱的。也就是先货后款。

    eval函数存在注入,可以通过#注释,我们可以传入路由action:eval#;arg1#arg2#arg3这样注释后面语句并可以调用任意函数,分号后面的#为传入参数,参数通过#被分割为参数列表.

    flask session解密
    网上有脚本

    0x01解题

    题目给了我们源码了

    from flask import Flask, session, request, Response
    import urllib
    
    app = Flask(__name__)
    app.secret_key = '*********************'  # censored
    url_prefix = '/d5afe1f66147e857'
    
    
    def FLAG():
        return '*********************'  # censored
    
    
    def trigger_event(event):
        session['log'].append(event)
        if len(session['log']) > 5:
            session['log'] = session['log'][-5:]
        if type(event) == type([]):
            request.event_queue += event
        else:
            request.event_queue.append(event)
    
    
    def get_mid_str(haystack, prefix, postfix=None):
        haystack = haystack[haystack.find(prefix)+len(prefix):]
        if postfix is not None:
            haystack = haystack[:haystack.find(postfix)]
        return haystack
    
    
    class RollBackException:
        pass
    
    
    def execute_event_loop():
        valid_event_chars = set(
            'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
        resp = None
        while len(request.event_queue) > 0:
            # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
            event = request.event_queue[0]
            request.event_queue = request.event_queue[1:]
            if not event.startswith(('action:', 'func:')):
                continue
            for c in event:
                if c not in valid_event_chars:
                    break
            else:
                is_action = event[0] == 'a'
                action = get_mid_str(event, ':', ';')
                args = get_mid_str(event, action+';').split('#')
                try:
                    event_handler = eval(
                        action + ('_handler' if is_action else '_function'))
                    ret_val = event_handler(args)
                except RollBackException:
                    if resp is None:
                        resp = ''
                    resp += 'ERROR! All transactions have been cancelled. <br />'
                    resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
                    session['num_items'] = request.prev_session['num_items']
                    session['points'] = request.prev_session['points']
                    break
                except Exception, e:
                    if resp is None:
                        resp = ''
                    # resp += str(e) # only for debugging
                    continue
                if ret_val is not None:
                    if resp is None:
                        resp = ret_val
                    else:
                        resp += ret_val
        if resp is None or resp == '':
            resp = ('404 NOT FOUND', 404)
        session.modified = True
        return resp
    
    
    @app.route(url_prefix+'/')
    def entry_point():
        querystring = urllib.unquote(request.query_string)
        request.event_queue = []
        if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
            querystring = 'action:index;False#False'
        if 'num_items' not in session:
            session['num_items'] = 0
            session['points'] = 3
            session['log'] = []
        request.prev_session = dict(session)
        trigger_event(querystring)
        return execute_event_loop()
    
    # handlers/functions below --------------------------------------
    
    
    def view_handler(args):
        page = args[0]
        html = ''
        html += '[INFO] you have {} diamonds, {} points now.<br />'.format(
            session['num_items'], session['points'])
        if page == 'index':
            html += '<a href="./?action:index;True%23False">View source code</a><br />'
            html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
            html += '<a href="./?action:view;reset">Reset</a><br />'
        elif page == 'shop':
            html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
        elif page == 'reset':
            del session['num_items']
            html += 'Session reset.<br />'
        html += '<a href="./?action:view;index">Go back to index.html</a><br />'
        return html
    
    
    def index_handler(args):
        bool_show_source = str(args[0])
        bool_download_source = str(args[1])
        if bool_show_source == 'True':
    
            source = open('eventLoop.py', 'r')
            html = ''
            if bool_download_source != 'True':
                html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
                html += '<a href="./?action:view;index">Go back to index.html</a><br />'
    
            for line in source:
                if bool_download_source != 'True':
                    html += line.replace('&', '&amp;').replace('	', '&nbsp;'*4).replace(
                        ' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').replace('
    ', '<br />')
                else:
                    html += line
            source.close()
    
            if bool_download_source == 'True':
                headers = {}
                headers['Content-Type'] = 'text/plain'
                headers['Content-Disposition'] = 'attachment; filename=serve.py'
                return Response(html, headers=headers)
            else:
                return html
        else:
            trigger_event('action:view;index')
    
    
    def buy_handler(args):
        num_items = int(args[0])
        if num_items <= 0:
            return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
        session['num_items'] += num_items
        trigger_event(['func:consume_point;{}'.format(
            num_items), 'action:view;index'])
    
    
    def consume_point_function(args):
        point_to_consume = int(args[0])
        if session['points'] < point_to_consume:
            raise RollBackException()
        session['points'] -= point_to_consume
    
    
    def show_flag_function(args):
        flag = args[0]
        # return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
        return 'You naughty boy! ;) <br />'
    
    
    def get_flag_handler(args):
        if session['num_items'] >= 5:
            # show_flag_function has been disabled, no worries
            trigger_event('func:show_flag;' + FLAG())
        trigger_event('action:view;index')
    
    
    if __name__ == '__main__':
        app.run(debug=False, host='0.0.0.0')
    

    先贴上师傅博客:

    https://blog.cindemor.com/post/ctf-web-16.html
    分析一下:

    # flag获取函数def FLAG()
    
    # 以下三个函数负责对参数进行解析。
    # 1. 添加log,并将参数加入队列def trigger_event(event)
    
    # 2. 工具函数,获取prefix与postfix之间的值
    def get_mid_str(haystack, prefix, postfix=None):
    
    # 3. 从队列中取出函数,并分析后,进行执行。(稍后进行详细分析)
    def execute_event_loop()
    
    # 网站入口点
    def entry_point()
    
    # 页面渲染,三个页面:
    index/shop/resetdef view_handler()
    
    # 下载源码
    def index_handler(args)
    
    # 增加钻石
    def buy_handler(args)
    
    # 计算价钱,进行减钱
    def consume_point_function(args)
    
    # 输出flagdef show_flag_function(args)
    def get_flag_handler(args)
    

    有这么两个跟 flag 有关的函数:

    def show_flag_function(args):
        flag = args[0]
        #return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
        return 'You naughty boy! ;) <br />'
    def get_flag_handler(args):
        if session['num_items'] >= 5:
            trigger_event('func:show_flag;' + FLAG())
        trigger_event('action:view;index')
    
    

    可以看到show_flag_function()无法直接展示出 flag,先看看get_flag_handler()中用到的trigger_event()函数:

    def trigger_event(event):
        session['log'].append(event)
        if len(session['log']) > 5: session['log'] = session['log'][-5:]
        if type(event) == type([]):
            request.event_queue += event
        else:
    

    这个函数往 session 里写了日志,而这个日志里就有 flag,并且 flask 的 session 是可以被解密的。只要后台成功设置了这个 session 我们就有机会获得 flag。

    但若想正确调用show_flag_function(),必须满足session['num_items'] >= 5。

    购买num_items需要花费points,而我们只有 3 个points,如何获得 5 个num_items?

    先看看购买的机制:

    def buy_handler(args):
        num_items = int(args[0])
        if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
        session['num_items'] += num_items 
        trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])
    def consume_point_function(args):
        point_to_consume = int(args[0])
        if session['points'] < point_to_consume: raise RollBackException()
        session['points'] -= point_to_consume
    

    buy_handler()这个函数会先把num_items的数目给你加上去,然后再执行consume_point_function(),若points不够consume_point_function()会把num_items的数目再扣回去。
    其实就是先给了货后,无法扣款,然后货被拿跑了

    那么我们只要赶在货被抢回来之前,先执行get_flag_handler()即可。
    函数trigger_event()维护了一个命令执行的队列,只要让get_flag_handler()赶在consume_point_function()之前进入队列即可。看看最关键的执行函数:

    仔细分析execute_event_loop,会发现里面有一个eval函数,而且是可控的!

    利用eval()可以导致任意命令执行,使用注释符可以 bypass 掉后面的拼接部分。

    若让eval()去执行trigger_event(),并且在后面跟两个命令作为参数,分别是buy和get_flag,那么buy和get_flag便先后进入队列。

    根据顺序会先执行buy_handler(),此时consume_point进入队列,排在get_flag之后,我们的目标达成。

    所以最终 Payload 如下:

    action:trigger_event%23;action:buy;5%23action:get_flag;
    

    要注意执行buy_handler函数后事件列表末尾会加入consume_point_function函数,在最后执行此函数时校验会失败,抛出RollBackException()异常,但是不会影响session的返回

    参考链接:

    https://blog.cindemor.com/post/ctf-web-16.html

  • 相关阅读:
    django框架之有名分组、无名分组、路由分发、反向解析等相关内容-62
    django框架之请求生命周期及ORM引入等相关内容-61
    django框架之目录介绍等相关内容-60
    django框架之引入及基础介绍等相关内容-59
    前端之JavaScript练习等相关内容-58
    django之jwt多方式登陆及多表群操作增删改查等相关内容-90
    Linux基础之虚拟机创建、网卡激活、切换YUM源及用户级别等相关内容-92
    django框架之auth组件等相关内容-76
    django框架之单表查询等相关内容-67
    自律
  • 原文地址:https://www.cnblogs.com/wangtanzhi/p/12309335.html
Copyright © 2011-2022 走看看