[DDCTF 2019]homebrew event loop

之前不会的题,跟着WP复现一下,学点新知识

考点:

逻辑漏洞
flask session解密

解题:
给了源码:

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
181
182
183
184
185
186
187
188
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):
    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` 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('#')
            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. 
'
                resp += '<a href="./?action:view;index">Go back to index.html</a>
'
                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
 
 
@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.
'.format(
        session['num_items'], session['points'])
    if page == 'index':
        html += '<a href="./?action:index;True%23False">View source code</a>
'
        html += '<a href="./?action:view;shop">Go to e-shop</a>
'
        html += '<a href="./?action:view;reset">Reset</a>
'
    elif page == 'shop':
        html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a>
'
    elif page == 'reset':
        del session['num_items']
        html += 'Session reset.
'
    html += '<a href="./?action:view;index">Go back to index.html</a>
'
    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>
'
            html += '<a href="./?action:view;index">Go back to index.html</a>
'
 
        for line in source:
            if bool_download_source != 'True':
                html += line.replace('&', '&').replace('\t', ' '*4).replace(
                    ' ', ' ').replace('<', '<').replace('>', '>').replace('\n', '
')
            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
'.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! ;) 
'
 
 
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')

测试了一下网页功能发现

[INFO] you have 0 diamonds, 3 points now.

也就是初始我们有三点,可以重置

然后审计源码

和flag有关得到函数有这两个。

我们可以发现,如果购买flag至少需要5点,而我们只有三点。怎么办呢?

flaget_flag_handler函数中调用了trigger_event()跟进

可以发现,这个函数往 session 里写了日志,而这个日志里就有 flag,

但若想正确调用show_flag_function(),必须满足session[‘num_items’] >= 5。

购买函数中

我们大概理解一下可以发现存在一个逻辑漏洞

buy_handler()这个函数会先把num_items的数目给你加上去,然后再执行consume_point_function(),

若points不够consume_point_function()会把num_items的数目再扣回去。

其实就是先给了货后,无法扣款,然后货被拿跑了

那么我们只要赶在货被抢回来之前,先执行get_flag_handler()即可。
函数trigger_event()维护了一个命令执行的队列,只要让get_flag_handler()赶在consume_point_function()之前进入队列即可

主函数中,eval可控

利用eval()可以导致任意命令执行,使用注释符可以 bypass 掉后面的拼接部分。

若让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;
访问后解flasksession即可
参考:https://blog.cindemor.com/post/ctf-web-16.html

发表评论

邮箱地址不会被公开。 必填项已用*标注