https://blog.cindemor.com/post/ctf-web-16.html
分析一下:
有这么两个跟 flag 有关的函数:
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: trigger_event(\'func:show_flag;\' + FLAG()) trigger_event(\'action:view;index\')可以看到show_flag_function()无法直接展示出 flag,先看看get_flag_handler()中用到的trigger_event()函数:
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:这个函数往 session 里写了日志,而这个日志里就有 flag,并且 flask 的 session 是可以被解密的。只要后台成功设置了这个 session 我们就有机会获得 flag。
但若想正确调用show_flag_function(),必须满足session[\'num_items\'] >= 5。
购买num_items需要花费points,而我们只有 3 个points,如何获得 5 个num_items?
先看看购买的机制:
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_consumebuy_handler()这个函数会先把num_items的数目给你加上去,然后再执行consume_point_function(),若points不够consume_point_function()会把num_items的数目再扣回去。
其实就是先给了货后,无法扣款,然后货被拿跑了
那么我们只要赶在货被抢回来之前,先执行get_flag_handler()即可。
函数trigger_event()维护了一个命令执行的队列,只要让get_flag_handler()赶在consume_point_function()之前进入队列即可。看看最关键的执行函数:
仔细分析execute_event_loop,会发现里面有一个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;要注意执行buy_handler函数后事件列表末尾会加入consume_point_function函数,在最后执行此函数时校验会失败,抛出RollBackException()异常,但是不会影响session的返回