❯ sudo arp-scan -l [sudo] password for Pepster: Interface: eth0, type: EN10MB, MAC: 5e:bb:f6:9e:ee:fa, IPv4: 192.168.60.100 Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan) 192.168.60.1 00:50:56:c0:00:08 VMware, Inc. 192.168.60.2 00:50:56:e4:1a:e5 VMware, Inc. 192.168.60.128 08:00:27:b0:12:bc PCS Systemtechnik GmbH 192.168.60.254 00:50:56:f6:18:1e VMware, Inc.
4 packets received by filter, 0 packets dropped by kernel Ending arp-scan 1.10.0: 256 hosts scanned in 2.036 seconds (125.74 hosts/sec). 4 responded ❯ export ip=192.168.60.128 ❯ rustscan -a $ip .----. .-. .-. .----..---. .----. .---. .--. .-. .-. | {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| | | .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ | `-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-' The Modern Day Port Scanner. ________________________________________ : http://discord.skerritt.blog : : https://github.com/RustScan/RustScan : -------------------------------------- Scanning ports like it's my full-time job. Wait, it is.
[~] The config file is expected to be at "/home/Pepster/.rustscan.toml" [!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers [!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. Open 192.168.60.128:22 Open 192.168.60.128:80 [~] Starting Script(s) [~] Starting Nmap 7.95 ( https://nmap.org ) at 2025-04-07 12:48 CST Initiating ARP Ping Scan at 12:48 Scanning 192.168.60.128 [1 port] Completed ARP Ping Scan at 12:48, 0.06s elapsed (1 total hosts) Initiating Parallel DNS resolution of 1 host. at 12:48 Completed Parallel DNS resolution of 1 host. at 12:48, 2.08s elapsed DNS resolution of 1 IPs took 2.08s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0] Initiating SYN Stealth Scan at 12:48 Scanning 192.168.60.128 [2 ports] Discovered open port 80/tcp on 192.168.60.128 Discovered open port 22/tcp on 192.168.60.128 Completed SYN Stealth Scan at 12:48, 0.03s elapsed (2 total ports) Nmap scan report for 192.168.60.128 Host is up, received arp-response (0.00048s latency). Scanned at 2025-04-07 12:48:16 CST for 0s PORT STATE SERVICE REASON 22/tcp open ssh syn-ack ttl 64 80/tcp open http syn-ack ttl 64 MAC Address: 08:00:27:B0:12:BC (PCS Systemtechnik/Oracle VirtualBox virtual NIC) Read data files from: /usr/share/nmap Nmap done: 1 IP address (1 host up) scanned in 2.35 seconds Raw packets sent: 3 (116B) | Rcvd: 3 (116B)
❯ curl $ip/login.php ……………………省略……………… <script> // Validación en el cliente para evitar caracteres HTML prohibidos functionvalidarUsername() { var username = document.getElementById("username").value; var regex = /[<>"'&]/; if (regex.test(username)) { alert("El nombre de usuario contiene caracteres HTML prohibidos."); return false; } return true; } </script>
const jwtSecret = 'unSecretoMuySecreto123!@#'; // Clave secreta para firmar el token
// Middleware con validación intencionalmente vulnerable function authenticateToken(req, res, next) { let token; // Se busca el token en la cabecera Authorization o en el cuerpo de la petición const authHeader = req.headers['authorization']; if (authHeader && authHeader.startsWith('Bearer ')) { token = authHeader.split(' ')[1]; } elseif (req.body && req.body.token) { token = req.body.token; }
if (!token) { return res.status(401).json({ message: 'Token no provisto' }); }
// IMPORTANTE: // Se utiliza jwt.decode en lugar de jwt.verify, por lo que no se valida la firma del token. const decoded = jwt.decode(token); if (!decoded) { return res.status(403).json({ message: 'Token inválido' }); } req.user = decoded; next(); }
// Información de la API con detalle de los endpoints disponibles const apiInfo = { name: 'API de Comandos con JWT (CTF)', version: '1.2.0', description: 'API para autenticación y ejecución de comandos utilizando JWT. En este reto, la validación del token es vulnerable y permite modificar el parámetro "role" para obtener acceso a /command (solo para rol admin).', endpoints: { "/": { method: "GET", description: "Muestra la información de la API y la descripción de los endpoints disponibles." }, "/login": { method: "POST", description: "Permite iniciar sesión. Se espera un body en formato JSON con 'username' y 'password'. Si el login es correcto, se retorna un token JWT. Ejemplo: { \"username\": \"jose\", \"password\": \"FuLqqEAErWQsmTQQQhsb\" }" }, "/command": { method: "POST", description: "Ejecuta un comando del sistema para usuarios autenticados con rol admin. Se espera un body en formato JSON con 'command' y 'token' o enviando el token en la cabecera 'Authorization'. Ejemplo: { \"command\": \"ls -la\", \"token\": \"token_jwt\" }" } } };
// Endpoint raíz que muestra la información de la API app.get('/', (req, res) => { res.json(apiInfo); });
// Credenciales válidas para el usuario jose const validUsername = 'Jose'; const validPassword = 'FuLqqEAErWQsmTQQQhsb'; const validRole = 'user';
// Endpoint para login app.post('/login', (req, res) => { const { username, password } = req.body; if (username === validUsername && password === validPassword) { // Crear token JWT con expiración de 1 hora, incluyendo el rol del usuario const token = jwt.sign({ username: username, role: validRole }, jwtSecret, { expiresIn: '1h' }); res.json({ message: 'Login correcto', token }); } else { res.status(401).json({ message: 'Usuario o contraseña incorrectos' }); } });
// Endpoint para ejecutar comando, protegido por JWT y accesible solo para rol admin app.post('/command', authenticateToken, (req, res) => { // Verificar que el rol del usuario sea admin if (req.user.role !== 'admin') { return res.status(403).json({ message: 'Acceso no autorizado, solo admin puede ejecutar comandos' }); }
const { command } = req.body;
// Advertencia: ejecutar comandos basados en entrada del usuario puede ser peligroso. // Se recomienda sanitizar la entrada y limitar los comandos permitidos. exec(command, (error, stdout, stderr) => { if (error) { return res.status(500).json({ error: error.message }); } if (stderr) { return res.status(500).json({ stderr }); } res.json({ stdout }); }); });
// El servidor escucha en el puerto 3000 y en localhost app.listen(3000, '127.0.0.1', () => { console.log('Servidor iniciado en el puerto 3000, accesible únicamente desde localhost'); });
哦,我知道为什么jwt的secret key可以任意了,原来注释里面写了
代码中使用jwt.decode而不是jwt.verify,因此不验证令牌的签名,跳过签名验证了
Root提权
好了,回到整正题,发现有个奇怪的程序拥有cap_setuid=ep的能力
1 2 3 4 5 6 7 8
ctesias@TheHackersLabs-TokenOfHate:~$ 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/yournode cap_setuid=ep ctesias@TheHackersLabs-TokenOfHate:~$ /usr/bin/yournode Welcome to Node.js v18.19.0. Type ".help"for more information. >