[DDCTF 2019]homebrew event loop


0x00 知識點

邏輯漏洞:

異步處理導致可以先調用增加鑽石,再調用計算價錢的。也就是先貨后款。

eval函數存在注入,可以通過#注釋,我們可以傳入路由action:eval#;arg1#arg2#arg3這樣注釋后面語句並可以調用任意函數,分號后面的#為傳入參數,參數通過#被分割為參數列表.

flask session解密
網上有腳本

0x01解題

題目給了我們源碼了

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. <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 = ''
                # 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.<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('&', '&amp;').replace('\t', '&nbsp;'*4).replace(
                    ' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').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 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:
        # 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')

先貼上師傅博客:

https://blog.cindemor.com/post/ctf-web-16.html
分析一下:

# flag獲取函數def FLAG()

# 以下三個函數負責對參數進行解析。
# 1. 添加log,並將參數加入隊列def trigger_event(event)

# 2. 工具函數,獲取prefix與postfix之間的值
def get_mid_str(haystack, prefix, postfix=None):

# 3. 從隊列中取出函數,並分析后,進行執行。(稍后進行詳細分析)
def execute_event_loop()

# 網站入口點
def entry_point()

# 頁面渲染,三個頁面:
index/shop/resetdef view_handler()

# 下載源碼
def index_handler(args)

# 增加鑽石
def buy_handler(args)

# 計算價錢,進行減錢
def consume_point_function(args)

# 輸出flagdef show_flag_function(args)
def get_flag_handler(args)

有這么兩個跟 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_consume

buy_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的返回

參考鏈接:

https://blog.cindemor.com/post/ctf-web-16.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM