[DDCTF 2019]homebrew event loop

/ 0评 / 0

之前不会的题,跟着WP复现一下,学点新知识
考点:
逻辑漏洞
flask session解密
解题:
给了源码:

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:
        # <code class="prettyprint" >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.
'
                resp += 'Go back to index.html
'
                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.
'.format(
        session['num_items'], session['points'])
    if page == 'index':
        html += 'View source code
'
        html += 'Go to e-shop
'
        html += 'Reset
'
    elif page == 'shop':
        html += 'Buy a diamond (1 point)
'
    elif page == 'reset':
        del session['num_items']
        html += 'Session reset.
'
    html += 'Go back to index.html
'
    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 += 'Download this .py file
'
            html += 'Go back to index.html
'
        for line in source:
            if bool_download_source != 'True':
                html += line.replace('&', '&').replace('\t', ' '*4).replace(
                    ' ', ' ').replace('<', '<').replace('>', '>').replace('\n', '
')
            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
'.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! ;)
'
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')

测试了一下网页功能发现
[INFO] you have 0 diamonds, 3 points now.
也就是初始我们有三点,可以重置
然后审计源码

和flag有关得到函数有这两个。
我们可以发现,如果购买flag至少需要5点,而我们只有三点。怎么办呢?
flaget_flag_handler函数中调用了trigger_event()跟进

可以发现,这个函数往 session 里写了日志,而这个日志里就有 flag,
但若想正确调用show_flag_function(),必须满足session['num_items'] >= 5。
购买函数中

我们大概理解一下可以发现存在一个逻辑漏洞
buy_handler()这个函数会先把num_items的数目给你加上去,然后再执行consume_point_function(),
若points不够consume_point_function()会把num_items的数目再扣回去。
其实就是先给了货后,无法扣款,然后货被拿跑了
那么我们只要赶在货被抢回来之前,先执行get_flag_handler()即可。
函数trigger_event()维护了一个命令执行的队列,只要让get_flag_handler()赶在consume_point_function()之前进入队列即可

主函数中,eval可控
利用eval()可以导致任意命令执行,使用注释符可以 bypass 掉后面的拼接部分。
若让eval()去执行trigger_event(),并且在后面跟两个命令作为参数,分别是buy和get_flag,那么buy和get_flag便先后进入队列。
根据顺序会先执行buy_handler(),此时consume_point进入队列,排在get_flag之后,我们的目标达成。
所以最终 Payload 如下:

<span class="hljs-attribute">action</span>:trigger_event%<span class="hljs-number">23</span>;<span class="hljs-attribute">action</span>:buy;<span class="hljs-selector-tag">5%</span><span class="hljs-selector-tag">23action</span><span class="hljs-selector-pseudo">:get_flag</span>;
访问后解flasksession即可
参考:https://blog.cindemor.com/post/ctf-web-16.html

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注