Orbital
4 minutos de lectura
Se nos proporciona un sitio web como este:
También tenemos el código fuente en Python.
Análisis del código fuente
La aplicación web está construida con Flask. Se puede encontrar una vulnerabilidad clara de inyección SQL (SQLi) en database.py
:
from colorama import Cursor
from application.util import createJWT, passwordVerify
from flask_mysqldb import MySQL
mysql = MySQL()
def query(query, args=(), one=False):
cursor = mysql.connection.cursor()
cursor.execute(query, args)
rv = [dict((cursor.description[idx][0], value)
for idx, value in enumerate(row)) for row in cursor.fetchall()]
return (rv[0] if rv else None) if one else rv
def login(username, password):
# I don't think it's not possible to bypass login because I'm verifying the password later.
user = query(f'SELECT username, password FROM users WHERE username = "{username}"', one=True)
if user:
passwordCheck = passwordVerify(user['password'], password)
if passwordCheck:
token = createJWT(user['username'])
return token
else:
return False
def getCommunication():
return query('SELECT * from communication')
Además, la contraseña se verifica contra un hash MD5:
def passwordVerify(hashPassword, password):
md5Hash = hashlib.md5(password.encode())
if md5Hash.hexdigest() == hashPassword: return True
else: return False
def isAuthenticated(f):
@wraps(f)
def decorator(*args, **kwargs):
token = session.get('auth')
if not token:
return abort(401, 'Unauthorised access detected!')
verifyJWT(token)
return f(*args, **kwargs)
return decorator
Si analizamos blueprints/routes.py
vemos que mientras estemos autenticados, podremos acceder a /export
, que es un endpoint que nos permite descargar archivos del servidor. Aquí podemos realizar un ataque de navegación de directorios (../
) y obtener una vulnerabilidad de lectura de archivo locales (Local File Read). Esto nos sirve para exfiltrar archivos confidenciales del servidor (por ejemplo, la flag):
@api.route('/export', methods=['POST'])
@isAuthenticated
def exportFile():
if not request.is_json:
return response('Invalid JSON!'), 400
data = request.get_json()
communicationName = data.get('name', '')
try:
# Everyone is saying I should escape specific characters in the filename. I don't know why.
communicationName = communicationName.replace('../', '')
return send_file(f'/communications/{communicationName}', as_attachment=True)
except:
return response('Unable to retrieve the communication'), 400
Observe que hay un filtro que reemplaza ../
por una cadena vacía. Esto es fácil de saltar con un payload como ....//
, ya que después de eliminar ../
, el payload restante será ../
, y el ataque de navegación de directorios funcionará.
Explotación de SQLi
La vulnerabilidad existe porque el servidor ingresa a la variable username
directamente en la consulta, suponiendo que la variable no contiene nada malicioso. Si usamos una comilla doble, la sintaxis de consulta se modificará y podríamos controlar la base de datos completa.
Observe que la consulta debe devolver un nombre de usuario y un hash que coincida con el hash de la contraseña introducida en el formulario de inicio de sesión (debido al comentario y el método de autenticación implementado).
Podemos hacer esto usando una consulta UNION
como esta:
SELECT username, password FROM users WHERE username = "" UNION SELECT "<user>", "<password-hash>"
Echando un vistazo a entrypoint.sh
, vemos que hay un usuario llamado admin
. Esta será nuestra víctima. La contraseña que estableceremos es asdf
. Por lo tanto, ingresaremos la siguiente cadena en el campo de usuario:
" UNION SELECT "admin", "912ec803b2ce49e4a541068d495ab570
Observe que el hash MD5 para asdf
es:
$ echo -n asdf | md5sum
912ec803b2ce49e4a541068d495ab570 -
Y estamos dentro:
Explotación de LFR
La funcionalidad de exportación está en la parte inferior de la página:
Podemos usar Burp Suite (Repeater) para analizar la petición y la respuesta:
Como prueba de concepto, podemos leer el archivo /etc/passwd
usando la navegación de directorios (Directory Traversal) y el bypass:
Antes de solicitar la flag, debemos verificar cuál es el nombre de archivo exacto. En el Dockerfile
, vemos que se copia como signal_sleuth_firmware
:
FROM python:3.8-alpine
# Install packages
RUN apk add --no-cache --update mariadb mariadb-client supervisor gcc musl-dev mariadb-connector-c-dev
# Upgrade pip
RUN python -m pip install --upgrade pip
# Install dependencies
RUN pip install Flask flask_mysqldb pyjwt colorama
# Setup app
RUN mkdir -p /app
RUN mkdir -p /communication
# Switch working environment
WORKDIR /app
# Add application
COPY challenge .
# Setup supervisor
COPY config/supervisord.conf /etc/supervisord.conf
# Expose port the server is reachable on
EXPOSE 1337
# Disable pycache
ENV PYTHONDONTWRITEBYTECODE=1
# copy flag
COPY flag.txt /signal_sleuth_firmware
COPY files /communications/
# create database and start supervisord
COPY --chown=root entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
Flag
Entonces, leamos la flag:
HTB{s3qu3l_4nd_lf1s_4r3_fun!!}
En realidad, la flag hace referencia a SQLi basado en tiempo, que es una técnica utilizada para volcar campos de la base de datos por caracteres (uno por uno) usando un oráculo de tiempo. Es posible usar herramientas como sqlmap
para automatizar el ataque. Sin embargo, el enfoque de Union-based SQLi también es válido e incluso más inteligente.