Gogs Argument Injection RCE: Ketika --exec Flag Bocor ke git rebase. Di internal/database/pull.go, fungsi Merge() memanggil git rebase:
if _, stderr, err = process.ExecDir(-1, tmpBasePath,
fmt.Sprintf("PullRequest.Merge (git rebase): %s", tmpBasePath),
"git", "rebase", "--quiet", pr.BaseBranch, remoteHeadBranch); err != nil {
pr.BaseBranch datang langsung dari URL parameter hasil strings.Split dari path PR. Posisi string di escape tetapi posisi string escape di dalam argumen array, yang ada setelah --quiet dan tidak memiliki tanda -- atau strip dua (2) yang dimana ini merupakan pembeda opsi dan argumen posisional.
POSIX mendefinisikan strip dua sebagai flag karena setiap string yang di mulai dengan tanda -- di tools cli merupakan tanda flag dari fungsi help. Jadi pr.BsaeBranch bernilai --exec=touch${IFS}/tmp/rce_proof dibaca sebagau instruksi eksekusi bukan branch.
Sebelum PR dibuat, Gogs memanggil RevParse dari library git-module untuk menvalidasi ref: git rev-parse --verify <ref>
Untuk ref --exec=touch${IFS}/tmp/rce_proof, rev-parse --verify akan gagal, ref ini tidak ada di repository dan tidak dapat di-resolve ke objek Git manapun.
Attacker membuat branch dengan nama tersebut menggunakan git push setelah branch yang dibuat sudah ada di remote. validasi ini memeriksa apakah ref bisa di-resolve atau tidak. tetapi tidak memeriksa apa ada nama ref aman untuk diinterpolasi ke dalam command line tanpa flag -- separator.
Nilai yang terbentuk disimpan ke dalam database dan dikembalikan apa adanya ke Merge() dan tidak melakukan normalisasi, tidak ada re-validation saat merge. Database jadinya bertindak sebagai transport yang opaque, yang masuk atau inputan keluar dengan bentuk yang sama karena tidak dilakukan normalisasi terlebih dahulu atau penyaringan data.
Git branch name secara teknis memperbolehkan karakter $, {, }, =, dan -. Itu sudah cukup untuk menyusun payload tanpa spasi yang dilarang di nama branch Git. ${IFS} di shell bash/sh expand ke spasi, jadi: --exec=touch${IFS}/tmp/rce_proof. Ketika dijalankan value tersebut via sh -x setelah commit di-replay. Shell yang dipanggil kemudian mengubah ${IFS} menjadi spas dan mengeksekusi touch /tmp/rce_proof.
Untuk payload yang membutuhkan karakter yang dilarang di nama file/branch (:, ~, ^, ?, *, [, \, //) misalnya URL untuk reverse shell ada jalur base64: --exec=echo${IFS}<base64_payload>|base64${IFS}-d|sh
Di Windows, karakter | dilarang di nama file NTFS (dan Git menyimpan branch refs sebagai file di refs/heads/), sehingga pendekatan inline ini tidak bisa dipakai langsung. Solusinya adalah menaruh payload di file yang di-commit ke repository. Ada lapisan tambahan di Windows karena MSYS2's sh mangles metacharacter shell sebelum sampai ke PowerShell. Pendekatan yang berfungsi adalah commit dua file .abcdef yang memanggil cmd.exe //c .abcdef.bat, dan .abcdef.bat yang berisi payload PowerShell asli. //c adalah MSYS2 escaping untuk /c, sehingga eksekusi berpindah langsung ke cmd.exe tanpa melewati interpretasi shell lagi.
Execution Flow di MergeStyleRebase
Ketika merge button ditekan, Merge() menjalankan serangkaian perintah git secara berurutan di temporary directory:
Step 1 aman karena flag -b di git clone sudah di-quote dan mengeksekusi argumen berikutnya sebagai value branch, bukan sebagai option. Step 3 adalah lokasi vulnerability tidak ada -- sebelum pr.BaseBranch, sehingga string --exec=... diterima sebagai flag. Step 5 gagal dan Merge() mengembalikan HTTP 500. RCE sudah terjadi di Step 3, sebelum error itu muncul. Error tidak membatalkan eksekusi yang sudah terjadi.
Konsekuensi dari abort di tengah proses: repository target tertinggal dalam state partial rebase dan rebase tidak selesai, tidak di-abort dengan bersih. Ini berarti exploit hanya bisa di-fire satu kali per repository. Untuk attacker yang membuat repo sendiri, ini tidak masalah karena repo langsung dihapus setelah eksekusi.
Race Condition di testPatch dan Kenapa PR Bisa Jadi Mergeable
Ada satu kondisi yang perlu dipenuhi sebelum merge button tersedia: PR harus berstatus PullRequestStatusMergeable. Untuk branch dengan nama berbahaya, ini bergantung pada timing internal Gogs. Saat PR pertama kali dibuat, testPatch() memanggil UpdateLocalCopyBranch(pr.BaseBranch). Jika local copy belum ada (repo baru atau belum pernah ada PR), eksekusi mengambil code path Clone yang menyertakan --end-of-options. Nama branch berbahaya diperlakukan sebagai data, bukan flag. Clone sukses, testPatch tidak mendeteksi konflik, status PR Mergeable.
Kemudian background goroutine TestPullRequests berjalan secara periodik dan re-check PR. Di iterasi berikutnya, local copy sudah ada, sehingga eksekusi mengambil code path Checkout yang tidak menyertakan --end-of-options. Git checkout dengan --exec=... sebagai argumen gagal, error dikembalikan, tapi karena terjadi error, checkAndUpdateStatus() di-skip. PR tidak di-update ke status conflict. dan PR tetap Mergeable.
Jika target repository sudah pernah memiliki PR sebelumnya (local copy sudah ada), kondisi ini terbalik: Checkout path diambil dari awal, checkout gagal, dan PR tidak pernah mencapai Mergeable. Ini membatasi target ke repository yang belum memiliki riwayat PR, atau repository baru.
Attack Surface dan Prerequisites
Gogs by default dikirim dengan:
DISABLE_REGISTRATION = false - registrasi terbuka
MAX_CREATION_LIMIT = -1 - tidak ada batas pembuatan repository
PullsAllowRebase - dinonaktifkan by default per-repo, tapi setiap owner repo bisa mengaktifkannya.
Artinya pada instance default, seseorang yang belum punya akun bisa: registrasi - buat repo - aktifkan rebase merging - eksploit. Semua dalam satu sesi, tanpa interaksi dengan user lain, tanpa privilege admin. DISABLE_REGISTRATION = true memblok jalur paling mudah ini. Tapi tidak memblok attacker yang sudah punya akun dan memiliki write access ke repository yang rebase-nya sudah aktif atau bisa diaktifkan karena privelege adalah owner.
MAX_CREATION_LIMIT = 0 menghilangkan kemampuan membuat repo baru, tapi sekali lagi tidak memblok attacker dengan write access ke repo existing. Tidak ada setting global atau organization-level yang bisa disable rebase merging di semua repository sekaligus.
Hubungan dengan Perbaikan Sebelumnya
Gogs sudah beberapa kali memperbaiki argument injection di code path berbeda:
Library git-module sudah di-harden di v1.8.7 dengan --end-of-options di Clone(), Push(), Fetch(), dan 28 call site lainnya. Merge() di internal/database/pull.go tidak menggunakan safe API ini tetapi memanggil process.ExecDir secara langsung. Seluruh hardening di git-module tidak berlaku di sini karena kode ini tidak pernah dimigrasikan.
Impact di Konteks Multi-Tenant
Ketika Gogs dijalankan sebagai platform shared (universitas, tim, organisasi), semua repository berada di bawah satu REPOSITORY_ROOT dengan satu process user biasanya git. Tidak ada OS-level isolation antar user, Process user ini memiliki read/write access ke seluruh filesystem Git di instance tersebut.
1. Arbitrary command execution sebagai process user ini berarti:
2. Seluruh repository di instance terbaca, termasuk private repo user lain
3. Database yang berisi password hash, API token, SSH key, dan 2FA secret untuk semua user bisa di-dump
4. Commit bisa dimanipulasi langsung di filesystem, melewati audit log Gogs. Tanpa commit signing (jarang dipakai di self-hosted instance), commit palsu sulit dideteksi
5. Lateral movement ke sistem lain yang reachable dari network server
Aspek supply chain yang perlu diperhatikan: modifikasi repository di filesystem level tidak meninggalkan trace di Gogs audit log. Jika instance ini menjadi bagian dari pipeline CI/CD, kode yang dimodifikasi bisa masuk ke production build tanpa melalui review process normal.
Artefak dan Deteksi
Ketika exploit dijalankan menggunakan repository yang dibuat dan langsung dihapus attacker, satu-satunya trace di server log adalah HTTP 500. Entry spesifik yang dihasilkan oleh git checkout yang gagal di Step 5:
[E] ...merge: git checkout '--exec=<...>': exit status 128 - error: unknown option `exec=<...>'
Gogs mencatat ini via c.Error(err, "merge") di ERROR level, termasuk nama branch berbahaya secara lengkap di log message. Tapi exploit yang lebih hati-hati bisa menggunakan pendekatan yang menghasilkan error lebih kecil.
Jika exploit menyasar repository existing (bukan yang dibuat attacker), artifact tambahan tertinggal: nama branch --exec=... muncul di branch listing, PR yang gagal ada di history, dan repository bisa stuck dalam state yang mengembalikan HTTP 500 untuk operasi tertentu. Di Windows, file payload (.abcdef, .abcdef.bat) juga tersimpan di git history.
Metasploit module membuat API token Gogs dengan nama msf_<hex> selama eksploitasi. Gogs tidak memiliki API endpoint untuk delete token, sehingga token ini persisten sampai di-revoke secara manual melalui UI atau database. Token list bisa dicek di /-/user/settings/applications.
Metasploit module sudah tersedia di msf prerequisites minimal: akun yang terdaftar (atau kemampuan registrasi), dan akses ke repository dengan rebase merging aktif atau bisa diaktifkan.
Baca Juga Tentang: Git-Push Git-C2 Git-Repo-Leaked
Benediktus Sava – Security Researcher
Sumber: Rapid7


.png)

.png)
