之前不会的题,跟着WP复现一下,学点新知识
考点:
逻辑漏洞
flask session解密
解题:
给了源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | 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. ' resp += '<a href="./?action:view;index">Go back to index.html</a> ' 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 += '<a href="./?action:index;True%23False">View source code</a> ' html += '<a href="./?action:view;shop">Go to e-shop</a> ' html += '<a href="./?action:view;reset">Reset</a> ' elif page == 'shop': html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a> ' elif page == 'reset': del session['num_items'] html += 'Session reset. ' html += '<a href="./?action:view;index">Go back to index.html</a> ' 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> ' html += '<a href="./?action:view;index">Go back to index.html</a> ' 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') |
测试了一下网页功能发现
[INFO] you have 0 diamonds, 3 points now.
也就是初始我们有三点,可以重置
然后审计源码
和flag有关得到函数有这两个。
我们可以发现,如果购买flag至少需要5点,而我们只有三点。怎么办呢?
flaget_flag_handler函数中调用了trigger_event()跟进
可以发现,这个函数往 session 里写了日志,而这个日志里就有 flag,
但若想正确调用show_flag_function(),必须满足session[‘num_items’] >= 5。
购买函数中
我们大概理解一下可以发现存在一个逻辑漏洞
buy_handler()这个函数会先把num_items的数目给你加上去,然后再执行consume_point_function(),
若points不够consume_point_function()会把num_items的数目再扣回去。
其实就是先给了货后,无法扣款,然后货被拿跑了
那么我们只要赶在货被抢回来之前,先执行get_flag_handler()即可。
函数trigger_event()维护了一个命令执行的队列,只要让get_flag_handler()赶在consume_point_function()之前进入队列即可
主函数中,eval可控
利用eval()可以导致任意命令执行,使用注释符可以 bypass 掉后面的拼接部分。
若让eval()去执行trigger_event(),并且在后面跟两个命令作为参数,分别是buy和get_flag,那么buy和get_flag便先后进入队列。
根据顺序会先执行buy_handler(),此时consume_point进入队列,排在get_flag之后,我们的目标达成。
所以最终 Payload 如下: