25 de marzo de 2025
![]()
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.
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.
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.
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.

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