Saltar al contenido
AZ Tools

Inspector de pickle de Python

Un pickle no es en realidad un formato de datos: es un programa para una pequeña máquina de pila, y pickle.load lo ejecuta. Sus opcodes pueden importar cualquier módulo, llamar a cualquier invocable y construir cualquier clase antes de que aparezca un solo valor. Por eso esta herramienta nunca deserializa nada. Lee el flujo de bytes igual que pickletools.dis: opcode a opcode, con su desplazamiento, su argumento decodificado y su efecto sobre la pila, y reconstruye solo la parte del valor que puede reconstruirse sin entrar en Python. Diccionarios, listas, tuplas, conjuntos, cadenas, cadenas de bytes, enteros de cualquier tamaño, flotantes, booleanos y None vuelven tal cual; todo lo que exigiría una importación aparece como marcador de posición explícito en vez de como una suposición. La versión del protocolo se informa por partida doble, porque las dos respuestas pueden no coincidir: del protocolo 2 en adelante el archivo abre con un opcode PROTO que declara la versión, mientras que los protocolos 0 y 1 no declaran nada, así que también se calcula el protocolo más alto que exige algún opcode, y se avisa cuando un archivo se declara más antiguo de lo que es. La tabla del memo muestra qué objetos se guardaron y se volvieron a recuperar, que es la única forma de ver las referencias compartidas y los ciclos: una lista que se contiene a sí misma es la recuperación de una clave guardada mientras esa misma lista todavía se estaba construyendo. Los flujos de protocolo 4 y 5 van en marcos, y la tabla de marcos indica dónde empieza cada bloque. El panel de hallazgos es lo primero que conviene leer. Nombra todas las construcciones que ejecutarían código al cargar: GLOBAL y STACK_GLOBAL para los nombres importados, REDUCE para la llamada, INST, OBJ, NEWOBJ y NEWOBJ_EX para la construcción de clases, BUILD para __setstate__, PERSID para el gancho persistent_load del propio cargador y EXT1/2/4 para el registro de copyreg, donde el nombre ni siquiera está en el archivo. Además enumera cada módulo y atributo que el cargador buscaría y señala los que aparecen en exploits publicados, como os.system, subprocess, builtins.eval o posix.system. Un pickle sin ninguno de esos opcodes es datos; uno que los tiene es un programa, y ninguna inspección hace seguro cargarlo desde un origen que no controlas. Los archivos malformados se nombran en lugar de leerse a medias: un flujo truncado, los bytes que quedan tras STOP (un segundo pickle escondido tras el primero) y la recuperación de una clave de memo que nunca se guardó se informan como tales.

Cómo usar

  1. Suelta en la caja un .pkl, un .pickle o cualquier objeto serializado con pickle. Nada se sube y nada se deserializa: el archivo solo se desensambla.
  2. Lee primero el panel de hallazgos. «Solo datos» significa que ningún opcode del archivo puede importar ni llamar nada. Si aparecen importaciones, trata el archivo como código no fiable y no lo cargues.
  3. Compara el protocolo declarado con el necesario. Un archivo que declara el protocolo 2 pero usa opcodes del 4 fue montado a mano, no escrito por pickle.dumps.
  4. Recorre la tabla de opcodes. La sangría sigue el anidamiento de MARK, así que cada contenedor se lee como un bloque, y la columna de la pila indica cuántos valores siguen vivos tras cada paso.
  5. Abre la tabla del memo para localizar objetos compartidos: una clave con reutilización mayor que cero es un mismo objeto que aparece en varios sitios, y así es también como se escribe un ciclo.

Preguntas frecuentes

¿Es seguro abrir aquí un pickle en el que no confío?
Sí, porque aquí no se deserializa nada. El archivo se lee opcode a opcode, exactamente como lo lee pickletools.dis, y lo único que ocurre con un opcode GLOBAL o REDUCE es que se imprimen sus nombres de módulo y atributo. No se importa nada ni se llama a nada, y la reconstrucción se detiene en los valores que pueden formarse solo con opcodes. El archivo tampoco sale de tu navegador: se lee con la File API y se analiza en la propia página. Lo que la herramienta no puede hacer es volver seguro ese archivo después; si el panel de hallazgos enumera importaciones, un pickle.load sobre ese mismo archivo seguirá realizándolas.
¿Por qué puede ejecutar código un pickle? Parece un formato de serialización.
Porque serializar objetos arbitrarios exige una manera de recrearlos, y la respuesta de Python es __reduce__: el objeto declara qué invocable, con qué argumentos, lo reconstruirá. El flujo guarda entonces un nombre que importar y una llamada que hacer, y el deserializador ejecuta ambas cosas. Ese único mecanismo es lo que hace serializables los objetos datetime, los arrays de numpy y cualquier clase propia, y es el mismo que usa un atacante escribiendo os.system en lugar de datetime.datetime. Nada en el formato distingue los dos casos, y por eso leer los opcodes es la única manera de saber qué hará un archivo.
¿Para qué sirve el memo y cómo aparece un ciclo?
El memo es la tabla de objetos que el serializador ya ha escrito. Cuando el mismo objeto reaparece, la segunda vez es una recuperación del memo: PUT y GET en el protocolo 0, BINPUT y BINGET después, y MEMOIZE a partir del protocolo 4, que numera las claves de forma implícita en lugar de escribirlas. Eso es lo que preserva la identidad: dos nombres que apuntaban a una lista siguen apuntando a una sola lista tras la carga. Un ciclo es ese mismo mecanismo un paso más allá: el contenedor se guarda en el memo antes de escribir su contenido, así que el contenido puede referirse a él. Por eso una lista que se contiene a sí misma es representable, y por eso aquí se muestra la referencia en vez de expandirla sin fin.
¿Qué cambia realmente entre las seis versiones del protocolo?
El protocolo 0 es ASCII: enteros, flotantes y cadenas se escriben como texto terminado en salto de línea, por eso un pickle de protocolo 0 casi se lee a simple vista. El 1 añade la versión binaria de casi todo. El 2 añade construcción eficiente de clases con NEWOBJ, tuplas de un byte y booleanos de verdad. El 3 añade objetos bytes, que Python 2 no podía representar. El 4 añade los marcos, que cortan el flujo en bloques con longitud por delante para que el lector tome trozos enteros, además de tamaños de 64 bits, STACK_GLOBAL, soporte nativo de conjuntos y claves de memo implícitas. El 5 añade los búferes fuera de banda, con los que un array grande viaja junto al flujo y no dentro de él: por eso NEXT_BUFFER puede aparecer sin datos detrás.
El archivo tiene bytes después de STOP. ¿Qué significa?
STOP termina un pickle, y todo lo que viene después pertenece a lo siguiente. Escribir varios pickles en un mismo archivo es un patrón corriente —un bucle de pickle.dump sobre un archivo abierto, releído con un bucle de pickle.load—, así que los bytes sobrantes suelen significar simplemente que el archivo contiene una serie de registros y no un único objeto. También es la forma de esconder una segunda carga útil tras una primera inofensiva, porque un programa que llama a load una sola vez ve solo el primer objeto y nunca mira el resto. Esta herramienta desensambla el primer pickle e indica cuántos bytes quedan; para examinar el resto, corta el archivo en el desplazamiento de STOP y abre la cola por separado.
¿Puede mostrarme el contenido de un array de numpy o de un DataFrame de pandas?
Los valores no, porque esos objetos se reconstruyen llamando a numpy y a pandas: el flujo nombra algo como numpy.core.multiarray._reconstruct o una clase de pandas, le entrega una cadena de bytes, y el array solo existe después de esa llamada. Lo que sí ves es la forma del archivo: los nombres que importaría, los búferes en bruto que transporta y sus tamaños, que suele bastar para saber si un archivo es lo que dice ser. En un archivo de protocolo 5 los datos del array pueden no estar siquiera dentro: NEXT_BUFFER significa que la carga útil viajó fuera de banda y tiene que aportarla quien llama.

Herramientas relacionadas