CTF Web安全

[DDCTF 2019]homebrew event loop

Posted on 2020-03-17,6 min read

题目给出了源码。并且有购买points功能。其他没了
首先审计代码。删了部分代码

from flask import Flask, session, request, Response
import urllib

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



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('\t', '&nbsp;'*4).replace(
                    ' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<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 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')

execute_event_loop是起到路由功能。对URL中参数进行分割等。然后执行对应的函数
我们可以复制下来。本地跑下

接着我们看下得到flag的地方。

如果session[num_items]>=5。就会调用了trigger_event。接着看看这个函数是干啥的
将要执行的函数和参数。放入request的队列中。然后依次执行

也就是说。我们要满足session[num_items]=5。继续看num_items在哪可以加

以buy_handler(1)这样购买。然后num_item就会+1
会把func:consume_point;num_items传入队列执行
执行的是consume_point_function(num_items)

作用是判断session中的points是否小于我们想要购买的数量。如果小于。那么就再减掉
就是。我们购买5个flag。但是。只有3个金币。它会先购买5个。然后判断钱是不是够。不够就再减去
OK。现在大致思路就搞懂了。execute_event_loop函数。接受输入。决定执行什么函数。
执行函数时。会把函数加入队列。然后再从队列中取出按顺序执行
如果我们直接调用buy_flag(5)。先将buy_flag(5)执行。然后再执行判断。如果钱不够就会减掉。
我们再执行。get_flag的时候。就失败。。
得在buy_flag(5)后。get_flag
仔细看看execute_event_loop函数处理路由的过程:
action(函数名)是第一个冒号后面的值。然后截取出来的字符串。再截取分号前面的值

参数呢。也是通过分割得到的。
取函数名+分号后面的值。用#来分割。作为参数

重复写。就能构造一个参数。
然后带入eval执行。。由于他是用#来注释。那么#用于注释python的代码eval('func:456#123')
井号会注释掉123
这里我们可以调用任意函数。那么我们直接调用trigger_event。event传参。为两个函数名和参数。加入队列。由于execute_event_loop还会对队列中的参数进行处理。我们直接传actionxx这种格式就好了
例如event(['action:buy:5'],'action:get_flag')
这样我们就能买5个num_items。然后再调用get_flag。注意此时队列中顺序如下。
buy:5
get_flag
buy:5触发的consume_point_function函数。

得到session。用flask_session_manager解密即可

下一篇: Wallbreaker_Easy(Bypass disable_function)→