← all walkthroughs

WifineticTwo

Linux· Medium
owned
2026-09-03
time to own
16m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found only two exposed services on the target — SSH and a web console on port 8080 that turned out to be OpenPLC, an industrial control (ICS) management platform. OpenPLC was left on its factory default login (openplc:openplc), and because the OpenPLC runtime itself runs as root, the built-in 'Hardware Layer Code Box' — a feature that compiles operator-supplied C code into the PLC runtime (CVE-2021-31630) — gave me a root shell on the container the instant they logged in.

From that root foothold the container was found to have a physical Wi-Fi adapter (wlan0) attached, exposing a second, air-gapped-looking network whose single access point advertised the SSID 'plcrouter'. The container joined that network, coming up on $INTERNAL_HOST2/24 with the access point itself as the default route, and the real prize was waiting one hop away: the wireless router ($INTERNAL_HOST, an OpenWrt device) accepted SSH as root with a blank password, handing over full control of the network's gateway.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export INTERNAL_HOST="<another-host-reached-after-pivoting>"
export INTERNAL_HOST2="<another-host-reached-after-pivoting>"
export INTERNAL_HOST3="<another-host-reached-after-pivoting>"
export INTERNAL_HOST4="<another-host-reached-after-pivoting>"

Attack path — how the box was taken

1EnumerationNetwork service enumeration
Scanned the target and identified an OpenPLC management console
A port scan showed only two services exposed: SSH (22/tcp) and an HTTP service (8080/tcp). Requesting the web root redirected to /login and returned a Werkzeug/Python-served page branded 'Welcome to OpenPLC' — an industrial control system (ICS) programmable logic controller management console.
Nmap found 22/tcp ssh and 8080/tcp http-proxy; HTTP 302 redirect to /login with 'Welcome to OpenPLC' login form (Werkzeug/1.0.1 Python/2.7.18).
Exact commands 2
shell

Confirm the two open ports and identify OpenPLC/Werkzeug.

$ nmap -p22,8080 -sV -T4 $TARGET
shell

Follow the redirect to the OpenPLC login page.

$ curl -ksS -L --max-time 15 -D - http://$TARGET:8080/
2Initial AccessDefault/hardcoded credentials (CWE-798)
Logged into OpenPLC using unchanged default credentials
OpenPLC ships with a well-known default account, openplc:openplc, intended to be changed on first setup. It had not been changed on this host, so submitting those credentials authenticated the session and returned a valid dashboard session cookie.
POST /login with username=openplc&password=openplc returned HTTP 302 to /dashboard with a valid session cookie.
Exact commands 2
shell

Fetch the login form and CSRF/session cookie.

$ curl -sS -c cookies.txt http://$TARGET:8080/login
shell

Authenticate with the OpenPLC default credentials.

$ curl -sS -i -b cookies.txt -c cookies.txt -X POST -d 'username=openplc&password=openplc' http://$TARGET:8080/login
FixChange the OpenPLC default administrator credentialsCritical
WeaknessThe OpenPLC web console was left on its out-of-the-box default account (openplc:openplc), so anyone who could reach port 8080 could log in as an administrator without any prior knowledge of the environment.
FixChange the default openplc account password immediately after deployment, enforce a strong unique password policy, and place the OpenPLC management interface behind network access controls (VPN/firewall) so it is not reachable from untrusted networks.
3ExploitationOS command/code injection via compiled hardware-layer code (CVE-2021-31630)
Compiled a malicious C payload through the Hardware tab to get code execution as root
OpenPLC's Hardware tab includes a 'Hardware Layer Code Box' where an authenticated user can paste raw C code (normally used to interface custom I/O hardware) into functions like updateCustomOut(). Saving the change triggers the server to compile that code into the PLC runtime, and clicking Start PLC on the Dashboard executes it. This is CVE-2021-31630 — because the OpenPLC service itself runs as root, the compiled reverse-shell payload executed with root privileges on the underlying container (hostname attica01).
The listener caught a shell that reported uid=0(root) gid=0(root) groups=0(root) on host attica01 the moment the PLC was started.
Exact commands 3
shell

Start a listener before triggering the payload.

$ nc -lvnp 4444
shell

Compile step; replace listener IP/port in the payload.

$ # In the browser: Hardware tab -> paste a C reverse-shell payload into updateCustomOut() -> Save changes (compiles)
shell

Executes the compiled code as root; catches the shell on the nc listener.

$ # Dashboard -> Start PLC
FixPatch OpenPLC and stop running the PLC runtime as rootCritical
WeaknessOpenPLC's Hardware Layer Code Box compiles operator-supplied C code and runs it as part of the runtime with no sandboxing or restriction, and that runtime process itself ran as root — so any authenticated user could compile and execute arbitrary code with full system privileges (CVE-2021-31630).
FixUpgrade OpenPLC to a version that fixes CVE-2021-31630, restrict who can edit hardware/custom-code fields to trusted engineers only, and run the OpenPLC service under a dedicated low-privilege system account rather than root so that even a successful code-injection is contained.
4Post-ExploitationContainer/host capability over-exposure
Confirmed root access and an over-privileged container
The obtained shell was verified as root inside a container-style network namespace (eth0@if18 on $INTERNAL_HOST3/24, plus a physical wlan0 adapter). The process held an unusually broad Linux capability set — including cap_sys_module, cap_sys_admin and cap_sys_rawio — far beyond what a PLC runtime needs, which is what later allowed direct control of the host's wireless hardware from inside the container.
Id: uid=0(root); ip -brief address showed eth0@if18 $INTERNAL_HOST4/24 and wlan0; capsh --print showed cap_sys_module,cap_sys_rawio,cap_sys_admin in the current set.
Exact commands 4
shell

Confirm root and map network interfaces.

$ id; hostname; ip -brief address; ip route
uid=0(root)
shell

Confirm unrestricted sudo (ALL:ALL) ALL, though root was already held.

$ sudo -n -l 2>/dev/null
shell

Enumerate the process's Linux capabilities.

$ capsh --print 2>/dev/null || getcap -r / 2>/dev/null
shell

Locate the local flag file (result: <user.txt>).

$ find / -type f -name user.txt -exec cat {} \; 2>/dev/null
FixReduce container privileges and remove unnecessary hardware accessHigh
WeaknessThe OpenPLC container was granted a broad Linux capability set (cap_sys_module, cap_sys_admin, cap_sys_rawio, etc.) and direct access to a physical wlan0 wireless adapter — far more than an application container needs — turning a single web-app compromise into full control over host-level and wireless hardware.
FixRun the OpenPLC container with a minimal capability set (drop ALL and add back only what's required), do not pass host network hardware such as wireless adapters into application containers, and apply a seccomp/AppArmor profile to further restrict syscalls.
5DiscoveryWireless network discovery
Found a second network on the container's Wi-Fi adapter
The container's wlan0 adapter could see exactly one nearby access point, broadcasting the SSID 'plcrouter' on channel 1 with WPA2/CCMP encryption, at -30 dBm — a signal that strong means the radio is effectively next to the antenna. An application container with a view of a wireless network nobody expected it to reach is the whole opportunity here: whatever is on the other side of that AP is reachable from a web-app compromise.
Iw dev wlan0 scan showed a single BSS 02:00:00:00:01:00, SSID: plcrouter, freq 2412 MHz (channel 1), RSN with group cipher CCMP, signal -30.00 dBm.
Exact commands 2
shell

Confirm the wireless adapter is present.

$ ip link show wlan0
shell

Enumerate nearby access points and read their encryption.

$ iw dev wlan0 scan | grep -E 'SSID|BSS|signal|RSN'
6ExploitationLateral movement onto a second network via container-attached wireless hardware
Joined the plcrouter network from inside the container
A credential for the plcrouter network was added to the engagement's credential store, and wlan0 came up associated to it: the interface held $INTERNAL_HOST2/24 and the routing table gained a default route via $INTERNAL_HOST — the access point itself — alongside the container's original route out over eth0. The container now had a foot in both networks at once, which is exactly what passing host wireless hardware into an application container buys an me.
Ip -brief address show wlan0 reported wlan0 UP $INTERNAL_HOST2/24, and ip route show listed 'default via $INTERNAL_HOST dev wlan0' beside the existing default via &lt;an-address-on-that-network&gt; dev eth0.
Exact commands 1
shell

Confirm the wireless interface is up on the new network and note its default route.

$ id; ip -brief address show wlan0; ip route show
7Lateral MovementSSH authentication with a blank/empty root password (CWE-521 / valid accounts)
SSH'd into the wireless gateway as root with a blank password
The default route pointed at $INTERNAL_HOST, so that address was the next target. It answers SSH, and its root account had no password set at all — the connection was made in batch mode, which refuses to prompt for a password, and it still returned a root shell. The banner-free 'ash' shell and the missing hostname binary identify it as an OpenWrt router: full administrative control of the gateway every device on that wireless network trusts, and /root/root.txt with it.
Ssh root@$INTERNAL_HOST in batch mode returned uid=0(root) gid=0(root), 'ash: hostname: not found', and the contents of /root/root.txt.
Exact commands 1
shell

BatchMode refuses to prompt, so a shell here proves the root account has no password; result: <root.txt>.

$ ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 root@$INTERNAL_HOST 'id; hostname; cat /root/root.txt'
uid=0(root) gid=0(root)
FixSet a root password and restrict SSH access on the OpenWRT gatewayCritical
WeaknessThe wireless gateway router ($INTERNAL_HOST, OpenWRT/LuCI) had no password set on its root account at all, so SSH accepted 'root' with an empty password and handed over full administrative control of the router.
FixSet a strong root password (or disable password auth entirely) immediately after OpenWRT setup, restrict SSH access to trusted management networks/interfaces, and prefer key-based authentication over passwords for router administration.

Exposed services

22/tcp
8080/tcp