DDCTF2019-homebrew-event-loop
Created At : 2025-05-26 16:39
Count:1.9k
Views 👀 :
DDCTF2019-homebrew-event-loop 首先打开网页
上面显示现在你有0个钻石,3个积分
然后点击Go to e-shop,看到可以用一个积分买一个钻石。
点击view source code,可以查看源码,然后python代码审计
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 from flask import Flask, session, request, Responseimport urllib app = Flask(__name__) app.secret_key = '*********************' url_prefix = '/d5afe1f66147e857' def FLAG (): return '*********************' 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 haystackclass RollBackException : pass def execute_event_loop (): valid_event_chars = set ( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#' ) resp = None while len (request.event_queue) > 0 : 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 = '' 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()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 htmldef 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_consumedef show_flag_function (args ): flag = args[0 ] return 'You naughty boy! ;) <br />' def get_flag_handler (args ): if session['num_items' ] >= 5 : trigger_event('func:show_flag;' + FLAG()) trigger_event('action:view;index' )if __name__ == '__main__' : app.run(debug=False , host='0.0.0.0' )
首先先看看怎么获取flag
1 2 3 4 5 166 def get_flag_handler(args):167 if session[168 # show_flag_function has been disabled, no worries169 trigger_event(170 trigger_event(
session[‘num_items’] 大于等于5的话,就会把flag放在session里面
1 2 3 4 5 153 def consume_point_function(args):154 point_to_consume = int (args[0 ])155 if session[156 raise RollBackException()157 session[
这个函数会先判断session中的points是否小于我们想要购买的数量。如果小于的话就减掉。例如我们购买5个flag,但是只有3个金币,会先购买5个吗,然后判断钱是不是够,不够就再减去。
然后再从路由来看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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()
这里调用了trigger_event函数
1 2 3 4 5 6 7 8 def trigger_event(event ): session['log' ].append(event )#将event 添加到session['log' ]这个列表中 if len(session['log' ]) > 5 : #如果列表session['log' ]中的元素数量大于等于5 session['log' ] = session['log' ][-5 :]#session['log' ]取后五个元素 if type (event ) == type ([]): #如果event 的类型是列表 request.event_queue += event #两个列表相加,在列表request.event_queue中添加一个元素 event else : request.event_queue.append(event ) #在列表request.event_queue中添加一个元素 event
再跟进一下execute_event_loop函数
1 2 3 4 else : is_action = event[0] == 'a' action = get_mid_str (event, ':' , ';' ) args = get_mid_str (event, action+';' ).split ('#' )
action的话会直接返回第一个;之后的内容 参数这里用#做了一下分割,并返回一个列表到args里
1 2 event_handler = eval(action + ('_handler' if is_action else '_function' )) ret_val = event_handler(args)
action的话会直接返回第一个;之后的内容 参数这里用#做了一下分割,并返回一个列表到args里
1 2 event_handler = eval(action + ('_handler' if is_action else '_function' )) ret_val = event_handler(args)
这里有一个任意函数调用。action传入之后会有一个后缀拼接,但是可以直接用#绕过,因为是eval执行的,eval会把这个字符串当作python 代码执行,所以后缀就绕过了。所以可以action,trigger_event#;来调用自己绕过后缀拼接。从而执行多个函数
发现存在逻辑漏洞:就是我们的钱无论够不够,它都会给我们先加上,然后扣掉 我们发现第148行,无论我们的钱够不够,都先给我们加上,之后再扣掉
若让eval()去执行trigger_event(),并且在后面跟两个命令作为参数,分别是buy和get_flag,那么buy和get_flag便先后进入队列。 根据顺序会先执行buy_handler(),此时consume_point进入队列,排在get_flag之后,我们的目标达成。
最终payload为:
1 ?action:trigger_event%23 ;action:buy;5% 23 action:get_flag;
然后使用bp抓包可以抓到session
然后使用解密脚本解密
得到
1 ZnVuYzpzaG93 X2 ZsYWc7 ZmxhZ3 swNTE4 M2 QwZC0 wMjRkLTQ1 ZmMtOGJlMy1 iMTM0 NGEzMTQ5 MjJ9
解密之后得到flag
1 func :show_flag;flag{05183 d0d-024 d-45 fc-8 be3-b1344a314922}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。