Resumen
protodump es una herramienta escrita en Go que procesa descriptores de Protocol Buffers contenidos en un fichero de entrada y los vuelca como ficheros .proto en un directorio de salida elegido por el usuario. El problema está en cómo decide dónde escribir cada fichero.
El nombre del fichero de salida se construye a partir de datos que vienen del propio descriptor que se está analizando (su name y la opción go_package), es decir, de la entrada que controla quien prepara el fichero. Para evitar que esa ruta se salga del directorio de salida, el código resuelve los enlaces simbólicos con EvalSymlinks y después comprueba:
strings.HasPrefix(base, outputDirAbs)
Esa comprobación es puramente textual: solo mira si la cadena de la ruta empieza por la cadena del directorio de salida, no si está realmente dentro de él. Si la salida es /tmp/out, la ruta /tmp/out_pwn/evil.proto también empieza por /tmp/out, así que pasa el filtro aunque sea un directorio hermano. Con un ../ en el nombre del descriptor basta para llegar ahí.
El investigador señaló la cadena completa en el código:
pkg/protodump/proto.go(líneas 47-55):Filename()compone la ruta relativa congo_packagey la ruta del descriptor, ambos controlados por el atacante.cmd/protodump/main.go(líneas 98-100): esefilenamese pasa tal cual awriteFile(...).cmd/protodump/main.go(líneas 32-35): los directorios se calculan conpath.Clean(fileDir)y se unen al de salida, de modo que los../acaban moviendo la ruta hacia el padre o un hermano.cmd/protodump/main.go(líneas 41-47): la contención se comprueba constrings.HasPrefix(base, outputDirAbs).cmd/protodump/main.go(líneas 51-58): tras el filtro,MkdirAllyWriteFilecrean y escriben la ruta final.
Pasos de reproducción
- Compilar
protodump. - Generar un fichero de entrada cuyo descriptor se llame
../out_pwn/evil.proto:
name = b'../out_pwn/evil.proto'
with open('/tmp/evil.bin', 'wb') as f:
f.write(bytes([0x0a, len(name)]) + name + b'\x00')
- Crear el directorio de salida y un directorio hermano cuyo nombre empiece por el mismo prefijo:
mkdir -p /tmp/out /tmp/out_pwn
- Ejecutar la herramienta indicando
/tmp/outcomo salida. La propia herramienta informa de que ha escrito en el directorio vecino:
/tmp/protodump -file /tmp/evil.bin -output /tmp/out
Wrote /tmp/out_pwn/evil.proto
- Comprobar el resultado con
ls /tmp/outyls /tmp/out_pwn:/tmp/outsigue vacío yevil.protoha aparecido en/tmp/out_pwn.
Impacto
Si una víctima ejecuta protodump sobre un fichero preparado por un atacante, este puede crear o sobrescribir ficheros .proto fuera de la carpeta de salida elegida, en directorios vecinos cuyo nombre comparta prefijo con ella. Eso compromete la integridad de los artefactos de proyectos adyacentes. La condición necesaria es que exista ese directorio hermano con prefijo coincidente.
El reporte se clasificó con severidad media, se envió el 28 de marzo de 2026, consta como resuelto y se divulgó el 31 de marzo de 2026.
Qué aprender de este caso
- Busca
strings.HasPrefix(ostartsWithen otros lenguajes) usado para decidir si una ruta está dentro de un directorio: si no se compara contra el directorio terminado en separador (/tmp/out/), cualquier hermano como/tmp/out_pwnlo burla. - En Go, la comprobación correcta es calcular
filepath.Rel(outputDir, destino)y rechazar el resultado si empieza por.., o comparar conoutputDir + string(os.PathSeparator); desde Go 1.24 también existeos.Rootpara limitar las escrituras a un directorio. - En herramientas que convierten un fichero en un árbol de ficheros (extractores, descompiladores, volcadores como este), revisa de dónde sale cada nombre de salida: si viene de metadatos del propio fichero analizado, es entrada del atacante.
- Al probar el filtro, no te quedes en
../../etc: prueba saltos a directorios hermanos que compartan prefijo con la carpeta de destino, que son los que sobreviven a unHasPrefix.