[DDCTF 2019]homebrew event loop

[DDCTF 2019]homebrew event loop

1.view-source python代码审计,trigger_event函数接收到来自url的字符串参数,首先先在session['log']后追加这次的访问字符串,然后判断日志log数组长度是否大于5,是则清空最早的一次记录,保存后五次的访问记录。如果字符串参数是数组形式,则将请求的event_queue全部追加。这个功能就是初始化event_queue数组和session中log更新的作用。

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)

2.get_mid_str分割字符串参数,出现的位置分别是action = get_mid_str(event, ':', ';')以及args = get_mid_str(event, action+';').split('#'),其中假设字符串参数为action:index;True#False,第一次分割为index;True#False,然后再次变为action = index;同理,第二次分割args变为数组[‘True’,‘False’]。

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

3.这个函数首先request.event_queue首个元素取出作为event,然后再将其后移一位。将event进行判断是否以action:和func:开头,以及是否包含其他除字母数字的字符,最后根据数据第1个字符判断是否为action然后进行分割,最后分割出的index添加上_handle字符成为访问路由,如果不为action则加上_function成为访问路由。到底就将参数args放入路由里面作为响应返回。


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

4.审计/url_prefix路由中的功能entry_point(),该功能就是session以及querystring语句的初始化,并保存到event_queue中。

@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()

5.view_handler()功能是返回主界面视图。

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

6.buy_handler函数是我们购买物品时的功能,这也是漏洞的产生点,首先它判断我们购买的数量是否小于0,然后直接将我们购买的数量保存再session中,并没有立刻扣分,随机就将我们购买的数量转到consume_point_function(args)函数中进一步判断。

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'])

7.在这个函数中,判断我们session中的积分是否小于我们要购买的数量,否则购买成功并将数量和积分改变后保存到session中,是则立刻返回。

def consume_point_function(args):
    point_to_consume = int(args[0])
    if session['points'] < point_to_consume:
        raise RollBackException()
    session['points'] -= point_to_consume

8.get_flag_handler函数是当我们的购买数量大于5时,将flag加载到show_flag_function中,这里注意读取的flag作为trigger_event参数传入了。

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

9.通过上面代码分析,那么就有以下几条规则:

  • 第一:可以注意到execute_event_loop()函数,它是按照trigger_event()函数处理之后的request.event_queue队列的顺序来依次执行该队列保存的方法的。
  • 第二:在buy_handler(args)方法可以注意到,该方法是先判断我们输入的数量是否大于0,然后再把session['num_items']的数量给添加我们输入的数量。最后,再通过trigger_event()函数将consume_point_function(args)方法加入到request.event_queue队列,随后通过该方法进行校验。
  • 第三:通过第一点和第二点,可以发现我们在购买完指定数量后,它添加到session['num_items']中,然后才按照队列顺序执行的consume_point_function方法校验。那么,我们是不是可以在它执行了buy_handler方法后,再执行get_flag_handler(args)方法,最后才通过consume_point_function方法校验呢?
  • 第四:通过第三点,那就可以发现了,execute_event_loop()函数它是通过request.event_queue队列中的顺序来执行方法的,那么我们如果将get_flag_handler(args)方法插在第三点中的两个方法中间,就能将flag读取到session中。而且队列的插入是通过trigger_event()函数来依次实现的,而且该函数如果参数为数组,那么会直接将数组追加到request.event_queue队列。所以,我们只需要把["action:buy;5","action:get_flag;"]作为参数来实现在校验前就获取flag的效果。

10.那么,我们就需要想办法来执行trigger_event()函数,我们可以看到在execute_event_loop()存在下面的语句,该语句将通过action = get_mid_str(event, ':', ';')处理后的action进行了拼接执行,同时args也是通过 args = get_mid_str(event, action+';').split('#')来分割截取的,在这里就能实现上一步的操作了:

	try:
      event_handler = eval(
           action + ('_handler' if is_action else '_function'))
      ret_val = event_handler(args)

11.首先我们要绕过action + _handler的字符拼接,这里就能用到python的注释符#来绕过了,于是可以变成action:trigger_event#;,这里最后加单引号是为了符合解析规则。

‘12.这一步就是将args参数进行分割成数组来传入,根据get_mid_str(event, action+';').split('#')的切割规则,我们传入action:buy;5#action:get_flag;,利用注释符实现参数切割传入到trigger_event函数进行加入request.event_queue队列中。

13.于是最终的payload:?action:trigger_event#;action:buy;5#action:get_flag;,然后用flask-cookie-session-manager解码一下,就能得到flag了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值