DDCTF2019-Web-homebrew event loop(python flask代码审计)

进入网站,有以下几个功能:
1、查看源码
2、商店购买diamonds
3、重置Session
4、回到主页

刚开始是有3 points ,一个point可以买一个diamond,下面进行源码审计:

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

app = Flask(__name__)
app.secret_key = '*********************'  # censored
url_prefix = '/d5afe1f66147e857'


def FLAG():
    return 'flag{************************}'  # 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. 
'
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('app1.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')

拿flag的点就在get_flag_handler函数这,这个函数会通过trigger_event来调用FLAG(),这样会把flag的信息写到session中,而flask session是加密的,我们最后还需要对其进行解密。

那么怎么调用这个函数呢,大致浏览代码,entry_point()函数为入口,querystring是我们的传参:
在这里插入图片描述

然后记性trigger_event(querystring),再调用execute_event_loop()函数。

trigger_event函数的功能就是把传参加入到event_queue[]中,然后再execute_event_loop中进行利用。

再看看execute_event_loop()函数。

		event = request.event_queue[0]
        request.event_queue = request.event_queue[1:]

取出event_queue中的值(也就是我们存放进去的参数),然后去除第一个元素,相当于把首个元素弹出并进行利用。
然后看这一段

			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)

get_mid_str函数的作用就是在第一个参数的字符串中截取第二个参数和第三个参数中间的字符串,如果第三个参数为空,则截取出字符串中第二个参数之后的所有字符。
以#为分隔符分隔event中的action,然后和_handler进行拼接,eval的作用是执行语句并返回结果,因为可以随意传递参数,所以我们可以利用这个拼接buy_handler函数和get_flag_handler函数,然后执行这些函数拿到flag。

构造?action:trigger_event%23;action:buy;7%23action:get_flag;

%23为#的urlencode,有注释作用,该payload传递之后,entry_point函数中的trigger_event(querystring)其实就是trigger_event(trigger_event),然后trigger_event被传入event_queue,接下来在execute_event_loop中eventaction:trigger_event#;action:buy;7#action:get_flag;,到下面action为trigger_event#,args[‘action:buy;7’, ‘action:get_flag;’],在eval中由于action中的#,所以trigger_event不会被拼接,所以event_handler==trigger_event,然后下面的event_handler(args)也就是trigger_event(args),然后action:buy;7和action:get_flag;被append到event_queue中,然后就会在while循环中被eval拼接并先后执行buy_handler和get_flag_handler,而且买的是7个diamonds。

这样就达到我们的目的了,接下来对session解密就可拿到flag:
在这里插入图片描述

你可能感兴趣的:(python代码审计,赛题复现,CTF学习)