[DDCTF 2019]homebrew event loop
先简单串烧一下一些基本知识点
1 from flask import Flask, session, request, Response 2 import urllib 3 4 app = Flask(__name__) 5 app.secret_key = '*********************' # censored 6 url_prefix = '/d5afe1f66147e857' 7 8 9 def FLAG(): 10 return '*********************' # censored 11 12 13 def trigger_event(event): //trigger_event:标识触发事件,取值为 INSERT、UPDATE 或 DELETE; 14 session['log'].append(event) 15 if len(session['log']) > 5: 16 session['log'] = session['log'][-5:] 17 if type(event) == type([]): 18 request.event_queue += event 19 else: 20 request.event_queue.append(event) 21 22 23 def get_mid_str(haystack, prefix, postfix=None): 24 haystack = haystack[haystack.find(prefix)+len(prefix):] 25 if postfix is not None: 26 haystack = haystack[:haystack.find(postfix)] 27 return haystack 28 29 30 class RollBackException: 31 pass 32 33 34 def execute_event_loop(): 35 valid_event_chars = set( 36 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#') 37 resp = None 38 while len(request.event_queue) > 0: 39 # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......" 40 event = request.event_queue[0] 41 request.event_queue = request.event_queue[1:] 42 if not event.startswith(('action:', 'func:')): 43 continue 44 for c in event: 45 if c not in valid_event_chars: 46 break 47 else: 48 is_action = event[0] == 'a' 49 action = get_mid_str(event, ':', ';') 50 args = get_mid_str(event, action+';').split('#') 51 try: 52 event_handler = eval( 53 action + ('_handler' if is_action else '_function')) 54 ret_val = event_handler(args) 55 except RollBackException: 56 if resp is None: 57 resp = '' 58 resp += 'ERROR! All transactions have been cancelled. <br />' 59 resp += '<a href="./?action:view;index">Go back to index.html</a><br />' 60 session['num_items'] = request.prev_session['num_items'] 61 session['points'] = request.prev_session['points'] 62 break 63 except Exception, e: 64 if resp is None: 65 resp = '' 66 # resp += str(e) # only for debugging 67 continue 68 if ret_val is not None: 69 if resp is None: 70 resp = ret_val 71 else: 72 resp += ret_val 73 if resp is None or resp == '': 74 resp = ('404 NOT FOUND', 404) 75 session.modified = True 76 return resp 77 78 79 @app.route(url_prefix+'/') 80 def entry_point(): 81 querystring = urllib.unquote(request.query_string) 82 request.event_queue = [] 83 if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100: 84 querystring = 'action:index;False#False' 85 if 'num_items' not in session: 86 session['num_items'] = 0 87 session['points'] = 3 88 session['log'] = [] 89 request.prev_session = dict(session) 90 trigger_event(querystring) 91 return execute_event_loop() 92 93 # handlers/functions below -------------------------------------- 94 95 96 def view_handler(args): 97 page = args[0] 98 html = '' 99 html += '[INFO] you have {} diamonds, {} points now.<br />'.format( 100 session['num_items'], session['points']) 101 if page == 'index': 102 html += '<a href="./?action:index;True%23False">View source code</a><br />' 103 html += '<a href="./?action:view;shop">Go to e-shop</a><br />' 104 html += '<a href="./?action:view;reset">Reset</a><br />' 105 elif page == 'shop': 106 html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />' 107 elif page == 'reset': 108 del session['num_items'] 109 html += 'Session reset.<br />' 110 html += '<a href="./?action:view;index">Go back to index.html</a><br />' 111 return html 112 113 114 def index_handler(args): 115 bool_show_source = str(args[0]) 116 bool_download_source = str(args[1]) 117 if bool_show_source == 'True': 118 119 source = open('eventLoop.py', 'r') 120 html = '' 121 if bool_download_source != 'True': 122 html += '<a href="./?action:index;True%23True">Download this .py file</a><br />' 123 html += '<a href="./?action:view;index">Go back to index.html</a><br />' 124 125 for line in source: 126 if bool_download_source != 'True': 127 html += line.replace('&', '&').replace('\t', ' '*4).replace( 128 ' ', ' ').replace('<', '<').replace('>', '>').replace('\n', '<br />') 129 else: 130 html += line 131 source.close() 132 133 if bool_download_source == 'True': 134 headers = {} 135 headers['Content-Type'] = 'text/plain' 136 headers['Content-Disposition'] = 'attachment; filename=serve.py' 137 return Response(html, headers=headers) 138 else: 139 return html 140 else: 141 trigger_event('action:view;index') 142 143 144 def buy_handler(args): 145 num_items = int(args[0]) 146 if num_items <= 0: 147 return 'invalid number({}) of diamonds to buy<br />'.format(args[0]) 148 session['num_items'] += num_items 149 trigger_event(['func:consume_point;{}'.format( 150 num_items), 'action:view;index']) 151 152 153 def consume_point_function(args): 154 point_to_consume = int(args[0]) 155 if session['points'] < point_to_consume: 156 raise RollBackException() 157 session['points'] -= point_to_consume 158 159 160 def show_flag_function(args): 161 flag = args[0] 162 # return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it. 163 return 'You naughty boy! ;) <br />' 164 165 166 def get_flag_handler(args): 167 if session['num_items'] >= 5: 168 # show_flag_function has been disabled, no worries 169 trigger_event('func:show_flag;' + FLAG()) 170 trigger_event('action:view;index') 171 172 173 if __name__ == '__main__': 174 app.run(debug=False, host='0.0.0.0')



if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100
- 结合上面,如果没有传递任何参数为空或者不是以action开头
(not querystring.startswith('action:')
- 又或者上传参数长度大于100
or len(querystring) > 100
- 那么就会进入条件判断语句,强化初始化参数
querystring = 'action:index;False#False'
session['num_items'] = 0 session['points'] = 3 session['log'] = []
从现在来看,之前的一切都是在为我们买东西做准备,接收了我们的参数以后,如果我们没有买东西,就是我们初步登录的这个界面,将我们一切东西初始化。重点是下面三个

request.prev_session = dict(session) 这把刚刚初始化的session用字典的形式传给了这个参数到了90行,我们看到了一个函数trigger_event

可以看到,实际上trigger_event的形参event 就是我们刚刚获得url?后面的字符串querystring 。并且将它加入到
session['log']这个日志
问题来了,下面两个if语句,是什么意思呢?
- 第一个


- 第二个


这个时候,你也许会问,它之前在路由定义的,现在函数里面能用吗?可以,因为它是全局变量,即使函数没有声明,也可以使用。 * 顺便说一下,列表也是可以合并的,a=[1,5] b=[3,4,5] a+b=[1,3,4,5,5] *
如果没有进行第二个if条件判断,就执行request.event_queue.append(event) 加入到这个列表当中。

首先初始化设置了两个参数
valid_event_chars = set( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#') resp = None
进入while循环吗,我们再来想一下request.enent_queque是什么东西?

while循环一进来就是这个



重点来了
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
这个开头is_action = event[0] == 'a' 作用是什么,我们还不知道,先放着
下面两个我们可以看到有同一个函数get_mid_str
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')

这个函数的大概作用是

action 是由实际作用,因为eval 函数会用到,args函数不知道有啥用,大佬的wp是:返回列表到args里,所以很明显,我们上传的参数就是action开头,才能上传过来
大佬的wp更直观
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 def ACTION_handler():pass event = 'action:ACTION;ARGS0#ARGS1#ARGS2' is_action = event[0] == 'a' action = get_mid_str(event, ':', ';') print '[!] action:',action args = get_mid_str(event, action+';').split('#') print '[!] args:',args event_handler = eval(action + ('_handler' if is_action else '_function')) print '[!] event_handler:',event_handler
看到第九行我们的event是这个样子,我们运行会得到什么?
[!] action: ACTION [!] args: ['ARGS0', 'ARGS1', 'ARGS2'] [!] event_handler: <function ACTION_handler at 0x00000000035A4B38>
[!] action: str# [!] args: ['ARGS0', 'ARGS1', 'ARGS2'] [!] event_handler: <type 'str'>
其他没啥分析,我们找到可以控制的点
我们去找找如何得到falg(因为我们有eval执行函数)
我们看到FLAG函数是不带参数

现在,我们可以控制event_handler 运行指定的函数,不过还有一个问题是FLAG()函数是不带参数,而args 为list ,直接传入action:FLAG ,将产生报错
为什么其他参数不行

这里是参数args的

那么没办法,只好分析源码,我们发现show_flag_function 是没办法得到falg,因为return flag 被注释掉了,只是将它放到flag中。想要得到flag只能用get_flag_handler()可以得到flag,而得到flag的条件是是if session['num_items'] >= 5: ,于是我们进入题目界面,去买钻石💎,发现最多买3个,不能买5个以及5个以上。我们看一下买钻石的函数

发现存在逻辑漏洞:就是我们的钱无论够不够,它都会给我们先加上,然后扣掉
我们发现第148行,无论我们的钱够不够,都先给我们加上,之后再扣掉
若让eval()去执行trigger_event(),并且在后面跟两个命令作为参数,分别是buy和get_flag,那么buy和get_flag便先后进入队列。
根据顺序会先执行buy_handler(),此时consume_point进入队列,排在get_flag之后,我们的目标达成。
我们构造plyadload
action:trigger_event%23;action:buy;5%23action:get_flag;

我们把得到的session放到KALI里面的flask-session-cookie-manager-master进行解密
python3 flask_session_cookie_manager3.py decode -c 'session'



浙公网安备 33010602011771号