有点意思的一道代码审计题
先简单串烧一下一些基本知识点
python文件源码
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 from flask import Flask, session, request, Responseimport urllibapp = Flask(__name__) app.secret_key = '*********************' url_prefix = '/d5afe1f66147e857' def FLAG (): return '*********************' def trigger_event (event ): //trigger_event:标识触发事件,取值为 INSERT、UPDATE 或 DELETE; 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 = 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. <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 = '' 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() 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('&' , '&' ).replace('\t' , ' ' *4 ).replace( ' ' , ' ' ).replace('<' , '<' ).replace('>' , '>' ).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 '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' ) if __name__ == '__main__' : app.run(debug=False , host='0.0.0.0' )
0X01 开始代码审计 😓
首先我们从路由 入手,然后我们慢慢去看它调用了哪些函数,这里只用了一个路由
当我们看到第81行就知道,querystring = urllib.unquote(request.query_string) 接收url? 后面的所有的值,然后进行url编码,传入参数querystring 中
接着有个判断条件
1 if querystring == '' or (not querystring.startswith('action:' )) or len (querystring) > 100
结合上面,如果没有传递任何参数为空或者不是以action开头
1 (not querystring.startswith('action:' )
1 or len (querystring) > 100
1 querystring = 'action:index;False#False'
后面的内容就是我们买钻石的网站,我们先盲猜一下 num_items 是我们买东西的清单,如果我们什么都没买,就是初始化session中的列表
1 2 3 session['num_items' ] = 0 session['points' ] = 3 session['log' ] = []
从现在来看,之前的一切都是在为我们买东西做准备,接收了我们的参数以后,如果我们没有买东西,就是我们初步登录的这个界面,将我们一切东西初始化。重点是下面三个
request.prev_session = dict(session) 这把刚刚初始化的session用字典的形式传给了这个参数到了90行,我们看到了一个函数 trigger_event ,我们在vscode上面跟进这个函数
可以看到,实际上**trigger_event的形参 event** 就是我们刚刚获得url?后面的字符串 querystring 。并且将它加入到
session['log'] 这个日志
问题来了,下面两个if语句,是什么意思呢?
第一个
举个例子 ,也就是要后面5个,前面都不要了
如果我们刚刚传入的参数也就是url?后面的字符串是列表类型,就合并。这两个列表 request.event_queue 和 **event**合并在一起。可能有人会问 request.event_queue 是什么,就在前面才定义 😢
这个时候,你也许会问,它之前在路由定义的,现在函数里面能用吗?可以,因为它是全局变量,即使函数没有声明,也可以使用。 * 顺便说一下,列表也是可以合并的,a=[1,5] b=[3,4,5] a+b=[1,3,4,5,5] *
如果没有进行第二个if条件判断,就执行 request.event_queue.append(event) 加入到这个列表当中。
这个时候我们来看91行的return返回函数 return execute_event_loop() ,我们在vscode上面跟进函数
首先初始化设置了两个参数
1 2 3 valid_event_chars = set ( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#' ) resp = None
进入while循环吗,我们再来想一下request.enent_queque是什么东西?
也就是我们url?后面的字符串,加入到这个列表中。以后不会再重复了
while循环一进来就是这个
就是将我们刚刚输入的字符串的列表第一个赋值给 event ,然后删除了第一个值,因为第一个值已经给了 event ,然后删除了第一个值,因为第一个值已经给了 event ,没必要留着
if not event.startswith(('action:', 'func:')):
如果我们第一个字符串开头不是 action 或 func ,就进入if判断语句继续。下一个for循环一次检验 event 中有没有字符,,可能有人忘了 valid_event_chars: 是什么…
重点来了
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 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. <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 = '' 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
这个开头 is_action = event[0] == 'a' 作用是什么,我们还不知道,先放着
下面两个我们可以看到有同一个函数 get_mid_str
** action = get_mid_str(event, ':', ';') **
args = get_mid_str(event, action+';').split('#')
在vscode里面跟进这个函数
这个函数的大概作用是
action 是由实际作用,因为 **eval ** 函数会用到,args函数不知道有啥用,大佬的wp是:返回列表到args里,所以很明显,我们上传的参数就是action开头,才能上传过来
大佬的wp更直观
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 def ACTION_handler ():pass event = 'action:ACTION;ARGS0#ARGS1#ARGS2' is_action = event[0 ] == 'a' action = get_mid_str(event, ':' , ';' ) print '[!] action:' ,actionargs = get_mid_str(event, action+';' ).split('#' ) print '[!] args:' ,argsevent_handler = eval (action + ('_handler' if is_action else '_function' )) print '[!] event_handler:' ,event_handler
看到第九行我们的event是这个样子,我们运行会得到什么?
1 2 3 [!] action: ACTION [!] args: ['ARGS0', 'ARGS1', 'ARGS2'] [!] event_handler: <function ACTION_handler at 0x00000000035A4B38>
event_handler 函数就是用 eval 拼接,从而得到了处理函数, eveal ** 函数的本质就是将字符串str当成有效的表达式来求职并且返回计算结果,程序过滤了大部分的特殊符号,导致我们不能随意使用代码注入,不过由于 ARGS 使用 # 进行分割,而# ** 在python代码中是注释符,在 action 中加入#,可以把后面的 _handler 注释掉。上面的代码用 event = 'action:str#;ARGS0#ARGS1#ARGS2' 进行测试
1 2 3 [!] action: str# [!] args: ['ARGS0', 'ARGS1', 'ARGS2'] [!] event_handler: <type 'str'>
其他没啥分析,我们找到可以控制的点
我们去找找如何得到falg(因为我们有eval执行函数)
我们看到FLAG函数是不带参数
现在,我们可以控制 event_handler 运行指定的函数,不过还有一个问题是FLAG()函数是不带参数,而 args 为**list** ,直接传入 action:FLAG ,将产生报错
为什么其他参数不行?
这里是参数args的
那么没办法,只好分析源码,我们发现 show_flag_function 是没办法得到falg,因为return flag 被注释掉了,只是将它放到flag中。想要得到flag只能用**get_flag_handler()**可以得到flag,而得到flag的条件是是 if session['num_items'] >= 5: ,于是我们进入题目界面,去买钻石💎,发现最多买3个,不能买5个以及5个以上。我们看一下买钻石的函数
发现存在逻辑漏洞:就是我们的钱无论够不够,它都会给我们先加上,然后扣掉
我们发现第148行,无论我们的钱够不够,都先给我们加上,之后再扣掉
若让 eval() 去执行 trigger_event() ,并且在后面跟两个命令作为参数,分别是 buy 和 get_flag ,那么**buy**和 **get_flag **便先后进入队列。
根据顺序会先执行 buy_handler() ,此时 consume_point 进入队列,排在 get_flag 之后,我们的目标达成。
我们构造plyadload
1 2 action:trigger_event%23 ;action:buy;5 %23action:get_flag;
我们把得到的session放到KALI里面的flask-session-cookie-manager-master进行解密
1 python3 flask_session_cookie_manager3.py decode -c 'session'
func:show_flag;flag{d07646de-b436-4966-ad68-fd2fc9d9764f} ❣️ ❣️ ❣️ ❣️