Dos Comandos por Uno
OS Command Injection
// OBJETIVO
Inyectar un segundo comando en la herramienta de ping para que el servidor lea /flag.txt.
// ZONA DE ATAQUE
Corre en un iframe de origen opaco: no puede leer este sitio ni tu progreso. Cuando logres el exploit, la flag aparece dentro de la app — copiala y verificala.
// CONSULTÁ
Pistas
Código vulnerable
El comando se arma por concatenación de strings y se ejecuta en una shell:
app.post('/api/ping', (req, res) => {
const { host } = req.body;
exec(`ping -c1 ${host}`, (err, stdout) => { // ← host sin escapar, en shell
res.json({ output: stdout });
});
});
Como exec pasa la línea entera a /bin/sh, cualquier metacaracter de shell en
host rompe el comando previsto: 8.8.8.8; cat /flag.txt corre el ping y
luego un cat arbitrario. Los separadores ;, &&, || y | encadenan
comandos que el atacante controla.
Remediación
No pases datos del usuario por una shell: ejecutá el binario directamente con sus argumentos como lista, y validá la entrada.
- const { host } = req.body;
- exec(`ping -c1 ${host}`, (err, stdout) => {
- res.json({ output: stdout });
- });
+ const { host } = req.body;
+ if (!/^[a-zA-Z0-9.-]+$/.test(host)) return res.status(400).json({ error: 'host inválido' });
+ execFile('ping', ['-c1', '--', host], (err, stdout) => { // sin shell
+ res.json({ output: stdout });
+ });
Por qué funciona: execFile (a diferencia de exec) no invoca una shell, así
que ; o | viajan como parte del argumento, no como sintaxis: no hay segundo
comando que ejecutar. La validación con una allowlist de caracteres y el --
(fin de opciones) endurecen aún más la entrada.