DDCTF 2019 homebrew event loop

这道题的代码让我分析了很久,一开始没搞清楚整个代码体系的执行结构。结合了网上师傅的WP和百度了之后才搞清楚执行结构

贴上代码:

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
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)://将字符串 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):
//截取 event:和;的中间部分 比如event="action:buy;shop" 就可以得到 buy,
其实就是action对应的值
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: //while循环,只要队列里面有事件就一直进行事件的处理。
# `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('#')
//获取参数,比如: event=action:view;shop#1#2 就可以获得参数shop,1,2
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
//主要是添加后缀,如果是event是a开头,就添上_handler,因为后面的函数都是_handler或者_function 结尾
ret_val = event_handler(args) //执行eval(函数),然后再配合这一步给它传参
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

payload: action:trigger_event#;action:buy;5#action:get_flag;

@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.<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('&', '&amp;').replace('\t', '&nbsp;'*4).replace(
' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').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 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! ;) <br />'


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')

整个代码的执行逻辑:

把要执行的方法加入 队列 (这里通过trigger_event()函数实现 ),然后再通过 execute_event_loop()这个函数来依次执行队列里面的函数。

重点来看一下两个和flag 有关的函数:

1
2
3
4
5
6
7
8
9
10
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')

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! ;) <br />'

get_flag_handler函数,会进行判断,如果 num_items >=5 的时候,就会把show_flag_function方法添加到队列中,并且传的参数就是flag,而这个trigger_event 方法 会把event 加入到 session[log]里面,我们就可以从session里面拿到flag ,所以现在的焦点是如何才能调用get_flag_handler函数呢,因为是涉及到数量问题,我们肯定要看buy 函数的逻辑:

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_handler之后,会先把num_items的数量进行增加,然后再把consume_point_function加入队列,执行这个来判断,如果分数不够买这么多的,就抛出一个异常,这里想法就诞生了,如果我们让 get_flag_handler再consume_point_function之前先入队列,就可以再session处拿到flag了 。execute_event_loop()函数 的eval()用来执行队列里面的方法,又因为没有过滤 # ,所以我们可以利用这个把后面的注释掉。而调用一个 trigger_event()函数,把 buy 和 get_flag 作为参数传进去,形成两个先后事件,那么consume_point就会排在get_flag之后,我们的目的就达到了

payload:?action:trigger_event#;action:buy;5#action:get_flag

进入队列的顺序:

1
2
3
4
action:trigger_event#;action:buy;5#action:get_flag
action:buy:5
action:get_flag
func:consume_point

然后就可以拿到session,通过 flask-unsign进行解密就可以看到flag

5.png

6.png

涉及到数量买卖,很有可能就是逻辑问题。