0x01 前言
最近身邊有萌新想打ctf,我作爲一個曾經接觸過一點點ctf的業餘菜雞,就索性做了一道Web題。這篇文章主要是面向想開始打ctf的萌新,所以很多地方可能都比較簡單。如有錯誤之處,歡迎各位指正。
0x02 Flask 簡介
Flask庫是一個非常小型的Python Web開發框架。它有兩個主要依賴:路由、調試和 Web 服務器網關接口(Web Server Gateway Interface,WSGI)子系統由 Werkzeug(http://werkzeug.pocoo.org/)提供;模板系統由 Jinja2(http://jinja.pocoo.org/)提供。Werkzeug 和 Jinjia2 都是由 Flask 的核心開發者開發而成。這裏我們要重點了解一下路由。
Flask需要知道對每個 URL 請求該運行哪些代碼,所以保存了一個 URL 到Python 函數之間的映射關係。處理 URL 和函數之間關係的程序稱爲路由。在 Flask 程序中定義路由的最簡便方式,是使用程序實例提供的 app.route 修飾器,把修飾的函數註冊爲路由。下面的例子說明了如何使用這個修飾器聲明路由:
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
這個例子就是把 index() 函數註冊爲程序根地址的處理程序。如果部署程序的服務器域名爲 www.example.com,在瀏覽器中訪問 http://www.example.com 後,會觸發服務器執行 index() 函數。這個函數的返回值稱爲響應,是客戶端接收到的內容。如果客戶端是 Web 瀏覽器,響應就是顯示給用戶查看的文檔。
要啓動服務器也很簡單,程序實例用 run 方法啓動 Flask 集成的開發 Web 服務器即可:
if __name__ == '__main__':
app.run(debug=True)
其中,name=='main' 是 Python 的慣常用法,在這裏確保直接執行這個腳本時才啓動開發Web 服務器。服務器啓動後,會進入輪詢,等待並處理請求。輪詢會一直運行,直到程序停止,比如按Ctrl-C 鍵。有一些選項參數可被 app.run() 函數接受用於設置 Web 服務器的操作模式。在開發過程中啓用調試模式會帶來一些便利,比如說激活調試器和重載程序。要想啓用調試模式,我們可以把 debug 參數設爲 True。要想讓所有主機都可以訪問,可以設置參數host="0.0.0.0"。
0x03 詳細分析
賽題如下:
該Web題下載下來以後源碼如下:
import os
import json
from shutil import copyfile
from flask import Flask,request,render_template,url_for,send_from_directory,make_response,redirect
from werkzeug.middleware.proxy_fix import ProxyFix
from flask import jsonify
from hashlib import md5
import signal
from http.server import HTTPServer, SimpleHTTPRequestHandler
os.environ['TEMP']='/dev/shm'
app = Flask("access")
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1 ,x_proto=1)
@app.route('/',methods=['POST', 'GET'])
def index():
if request.method == 'POST':
f=request.files['file']
os.system("rm -rf /dev/shm/zip/media/*")
path=os.path.join("/dev/shm/zip/media",'tmp.zip')
f.save(path)
os.system('timeout -k 1 3 unzip /dev/shm/zip/media/tmp.zip -d /dev/shm/zip/media/')
os.system('rm /dev/shm/zip/media/tmp.zip')
return redirect('/media/')
response = render_template('index.html')
return response
@app.route('/media/',methods=['GET'])
@app.route('/media',methods=['GET'])
@app.route('/media/<path>',methods=['GET'])
def media(path=""):
npath=os.path.join("/dev/shm/zip/media",path)
if not os.path.exists(npath):
return make_response("404",404)
if not os.path.isdir(npath):
f=open(npath,'rb')
response = make_response(f.read())
response.headers['Content-Type'] = 'application/octet-stream'
return response
else:
fn=os.listdir(npath)
fn=[".."]+fn
f=open("templates/template.html")
x=f.read()
f.close()
ret="<h1>文件列表:</h1><br><hr>"
for i in fn:
tpath=os.path.join('/media/',path,i)
ret+="<a href='"+tpath+"'>"+i+"</a><br>"
x=x.replace("HTMLTEXT",ret)
return x
os.system('mkdir /dev/shm/zip')
os.system('mkdir /dev/shm/zip/media')
app.run(host="0.0.0.0",port=8080,debug=False,threaded=True)
接下來就開始詳細介紹。
這是一個用來實現文件在線解壓的網站,首先看首頁對應的index函數,當訪問網站首頁時,可以採用GET和POST請求兩種方式,對應的響應函數爲index函數。題目裏還有兩個html文檔,一個用來當上傳文件時,一個用來渲染頁面,這裏就不貼出來了。當在首頁通過html上傳文檔時,會執行index函數,會執行如下命令來刪除文件:
rm -rf /dev/shm/zip/media/*
然後拼接路徑,並將上傳的文件保存到這個路徑:/dev/shm/zip/media/tmp.zip
之後執行命令解壓:
timeout -k 1 3 unzip /dev/shm/zip/media/tmp.zip -d /dev/shm/zip/media/
解壓後將壓縮文件刪除:
rm /dev/shm/zip/media/tmp.zip
第二個函數是media函數,有三種url請求都由這個函數來響應,其中有一個需要特別關注的,就是
@app.route('/media/<path>',methods=['GET'])
這是動態路由,就是當請求的時候如果在media的後面再加上一個字符串,可以將這個字符串作爲參數傳遞給path變量,執行media函數中的內容:
def media(path=""):
npath=os.path.join("/dev/shm/zip/media",path)
if not os.path.exists(npath):
return make_response("404",404)
if not os.path.isdir(npath):
f=open(npath,'rb')
response = make_response(f.read())
response.headers['Content-Type'] = 'application/octet-stream'
return response
else:
fn=os.listdir(npath)
fn=[".."]+fn
f=open("templates/template.html")
x=f.read()
f.close()
ret="<h1>文件列表:</h1><br><hr>"
for i in fn:
tpath=os.path.join('/media/',path,i)
ret+="<a href='"+tpath+"'>"+i+"</a><br>"
x=x.replace("HTMLTEXT",ret)
return x
仔細觀察可以看到,path作爲參數會被拼接到npath變量中,然後當npath不存在時,返回404;當npath不是目錄時,會使用open函數打開,並返回給請求者。題目中提到了flag位於根目錄,而path我們又可以控制,那麼我們只要能夠通過一種方式,將npath變成/flag是不是就可以了呢?心裏想,直接用相對路徑../實現路徑穿越不就妥了?這麼簡單的嗎?
0x04 峯迴路轉
說幹就幹,我把path設置成../../../../flag,直接在瀏覽器請求http://example.com/media/../../../../flag 結果發現不對,瀏覽器直接把我這些../全去掉了?url變成了http://example.com/media/flag,whatfuck? 第一反應是瀏覽器的問題,那就換個瀏覽器,結果發現也不行,然後想了想,那就用burp suite吧,結果還是不行。
最後,我在自己電腦上運行這個代碼,開始各種調試,最後發現把斜槓換成兩個反斜槓就可以了,能夠實現路徑穿越,讀取上一層的文件內容,可是換成題目中就是不行。我仔細想了想應該是因爲我本地是windows,而服務器是Linux,所以纔不行的。眼看着時間不早了該睡覺了,還是沒搞出來,於是喝了一杯茶,想了想,最後靈機一動,還是沒想出來怎麼搞。算了,睡覺吧。
到了第二天早上,我覺得這個必須得搞定。於是,我想到了Linux中的軟鏈接!突然一下子就明白該怎麼搞了!於是,打開我塵封已久的kali虛擬機,然後慢慢悠悠的敲下了如下命令:
成功創建了一個指向/flag的軟鏈接,但是這個軟鏈接怎麼利用呢?這個網站既然是用來在線解壓的,那我就把軟鏈接打成一個壓縮包傳上去,它直接就會被解壓到當前目錄。然後我直接點擊該文件,直接就下載下來一個車文件,打開即可看到flag內容爲:
flag{NeV3r_trUSt_Any_C0mpresSeD_file}
最後終於搞定了,這一刻還是很開心的。作爲一個業餘ctf菜雞選手,實在是不容易。還是要多做題,多開闊眼界,學習各種騷操作!