Banka hesabınızdaki parayı çoğaltabilseniz veya hediye kuponlarını birden fazla kez kullanabilseniz nasıl hissederdiniz ? Bu yazıda, tüm bunlara imkan veren Race Condition isimli zafiyeti inceleyeceğiz.
Giriş
Race Condition, eş zamanlı çalışan(multithreaded) birden çok iş parçacığının(process) aynı kaynak üzerinde işlem yapmasıyla meydana gelir. Doğası gereği tespit etmesi zordur, yazılım release edildikten uzun süreler sonra bile bu zafiyet tespit edilemeyebilir. Kritik uygulamalar dışında race condition testi yazılımcılar tarafından genellikle yapılmamaktadır.
Türkçe’ye “Yarış Durumu” olarak çevrilebilir. İsmini “birden çok sürecin aynı kaynakta işlem yapmak için yarışması” benzetmesinden almıştır.

Daha iyi anlayalım: Bankacılık Uygulamasında Race Condition örneği
Ali, arkadaşı Veli’nin banka hesabına 100 lira göndermek istemektedir. Kullandığı bankanın mobil uygulamasına girer, hesabında tam 100 lira olduğunu görür. Transferler sekmesine girerek Veli’nin hesap numarasını yazar ve göndereceği miktar alanına 100 yazıp gönder butonuna tıklar.
Gayet normal bir para transferi işlemi olduğunu görüyoruz. Peki, bu para transferinin arka planında neler dönmekte ?
1- Ali’nin yeteri kadar bakiyesi mevcut mu ? Sistem önce, Ali’nin bakiyesi olup olmadığını kontrol öder. Çünkü Ali, sahip olduğundan daha fazla parayı transfer edememelidir. (Eksi bakiyeye düşebilen hesapları tenzih ederim:))
2- Transfer edilen miktar Ali’nin bakiyesinden düşülür Ali, Veli’ye 100 lira transfer ettiğine göre, Ali’nin bakiyesinden 100 lira düşülmelidir. Ali’nin hesabında 100 lira olduğunu biliyoruz. Veli’ye de 100 lira gönderdiğine göre, Ali’nin mevcut bakiyesi 0 olarak güncellenmelidir.
3- Transfer edilen miktar Veli’nin bakiyesine eklenir Ali’den Veli’ye gönderilen parayı Veli’nin bakiyesine ekliyoruz. Veli’nin hesabında hiç para olmadığını düşünürsek, Veli’nin bakiyesi 100 lira olarak güncellenir.
4- İşlem başarılı mesajı✔️ Sistem Ali’nin bakiyesini kontrol etti, transfer edilen miktarı Ali’nin bakiyesinden düştü ve Veli’nin bakiyesine ekledi. Bir sorun çıkmadığı, her şey yolunda gittiği için işlem başarılı mesajını döndürdü.
Şimdi bu işlemleri sözde kod (pseudocode) yazarak da sadeleştirelim
Eğer (Ali.Bakiye >= transferMiktari)
Ali.Bakiye = Ali.Bakiye - transferMiktari
Veli.Bakiye = Veli.Bakiye + transferMiktari
Mesaj("İşlem Başarılı")
Değilse
Hata("Yetersiz bakiye")
Algoritma hatasız görünüyor değil mi ? Evet, doğru. Algoritmada bir hata yok.
Peki Race Condition zafiyeti nerede ? Modern web uygulamalarında. Aslında algoritma sırayla çalışırsa race condition meydana gelmez. Modern web uygulamaları, aynı anda birden çok kullanıcıya hizmet verebilmek için multithreading (çok iş parçacıklı) prensibi ile çalışmaktadır. Bu durum kullanıcıların işlemleri birbirlerine paralel olarak yapabilmesine olanak sağlar.
Algoritmamızın multithreaded -çok iş parçacıklı- olarak web uygulamamızda çalıştığını ve aynı anda 3 transfer isteği geldiğini düşünelim. Threadler önce Ali’nin bakiyesini kontrol eder ve üçü de 100 cevabına ulaşır. Çünkü, threadler paralel ve hemen hemen aynı anda çalıştığı için hepsi henüz algoritmanın ilk adımındadır ve hiç biri bakiyede güncelleme yapmamıştır. Bu durum, 3 threadın da bakiye kontrolünü geçmesine olanak sağlamakta. Bakiye kontrol edildikten sonra üç thread da Ali’nin bakiyesinden 100 lira düşer ve Veli’nin bakiyesine 100 lira ekler. Bu durumda Ali, hesabında 100 lira olmasına rağmen Veli’ye 300 lira transfer edebilmiştir.
Daha anlaşılır olması için bu durumu adım adım inceleyelim
Adım 1:
Thread(İş Parçacığı) 1 : Ali’nin yeteri kadar bakiyesi mevcut mu ? Evet.
Thread(İş Parçacığı) 2 : Ali’nin yeteri kadar bakiyesi mevcut mu ? Evet.
Thread(İş Parçacığı) 3 : Ali’nin yeteri kadar bakiyesi mevcut mu ? Evet.
Adım 2:
Thread(İş Parçacığı) 1 : Transfer edilen miktar Ali’nin bakiyesinden düşülür. Bakiye = 0
Thread(İş Parçacığı) 2 : Transfer edilen miktar Ali’nin bakiyesinden düşülür? Bakiye = -100
Thread(İş Parçacığı) 3 : Transfer edilen miktar Ali’nin bakiyesinden düşülür? Bakiye = -200
Adım 3:
Thread(İş Parçacığı) 1 : Transfer edilen miktar Veli’nin bakiyesine eklenir. Bakiye = 100
Thread(İş Parçacığı) 2 : Transfer edilen miktar Veli’nin bakiyesine eklenir. Bakiye = 200
Thread(İş Parçacığı) 3 : Transfer edilen miktar Veli’nin bakiyesine eklenir. Bakiye = 300
Test için gerçek bir uygulama yazalım
Teoride ve sözde kodlarda çalışan zafiyetimizi bir de uygulama yazarak test etmek istedim. Go ile küçük bir bankacılık API oluşturdum. Web sunucu kütüphanesi Fiber, ORM kütüphanesi Gorm ve Veritabanı sistemi olarak PostgreSQL kullandım. Web uygulamalarını genellikle bu kütüphaneleri kullanarak geliştirdiğim için özellikle bunları seçtim.
Öncelikle veritabanı yapımıza göz atalım. Hesap tablomuz Hesap No(ID), İsim(Name) ve Bakiye(Balance) alanlarından oluşuyor.
| Hesap No | İsim | Bakiye |
|---|---|---|
| 1 | Ali | 100 |
| 2 | Veli | 0 |
Bu tabloya göre Ali kullanıcısının 100 lira bakiyesi olduğunu görüyoruz.
Aşağıdaki kodla gerçek uygulamanın kodlarının açıklamalı ve sadeleştirilmiş hali. GitHub reposu burada.
//Para transferi için JSON isteğinin modeli
type TransferRequest struct {
From uint `json:"fromID"`
To uint `json:"toID"`
Amount int `json:"amount"`
}
//Hesap veritabanı modeli
type Account struct {
gorm.Model
Name string `json:"name"`
Balance int `json:"status" gorm:"default:1"`
}
func TransferMoney(c *fiber.Ctx) error {
db := database.DBConn // Veritabanı bağlantısı
var transferReq TransferRequest
var fromAccount Account
var toAccount Account
c.BodyParser(&transferReq); // JSON ile gelen isteğimizi BodyParser ile parçalayıp değişkene kaydediyoruz
// Bakiye kontrolü
if !CheckBalance(transferReq.From, transferReq.Amount) {
// Gönderici hesaptaki bakiye, gönderilmek istenen tutardan düşükse hata verir
return c.JSON(fiber.Map{"status": "error", "message": "Yetersiz bakiye!"})
}
db.Take(&fromAccount, transferReq.From) // Gönderici hesap bilgilerini fromAccount isimli değişkene atar
db.Take(&toAccount, transferReq.To) // Alıcı hesap bilgilerini toAccount isimli değişkene atar
fromAccount.Balance = fromAccount.Balance - transferReq.Amount // Gönderilen miktar, gönderici hesabın bakiyesinden düşülür
toAccount.Balance = toAccount.Balance + transferReq.Amount // Gönderilen miktar, alıcı hesabın bakiyesine eklenir
db.Save(&fromAccount) // Düzenlenen gönderici hesap veritabanında güncellenir
db.Save(&toAccount) // Düzenlenen alıcı hesap veritabanında güncellenir
return c.Status(200).JSON(fiber.Map{"status": "success", "message": "Transfer başarılı."})
}
//Bakiye kontrol fonksiyonu
func CheckBalance(userID uint, amount int) bool {
db := database.DBConn // Veritabanı bağlantısı
var account Account // Bakiyesi kontrol edilecek hesap
db.Take(&account, userID) // Bakiyesi kontrol edilecek hesap bilgilerini account isimli değişkene atar
// Hesap bakiyesi miktardan düşükse "false" döner
if account.Balance < amount {
return false
}
//Yeterli bakiye mevcutsa "true" döner
return true
}
Kodları incelediğimizde algoritmamıza uygun bir şekilde önce bakiye kontrolünün yapıldığı, daha sonra gönderici hesap bakiyesinden gönderilen tutarın düşüldüğü ve alıcı hesap bakiyesine bu tutarın eklendiğini görüyoruz. Tüm kodlar ayrıntılı bir şekilde yorum satırları ile açıklanmıştır.
Hesabımızda olmayan parayı gönderelim
Hazırladığımız küçük bankacılık uygulamasındaki zafiyetimizi sömürmenin vakti geldi. Race Condition zafiyetini sömürürken dikkat etmememiz gereken en önemli unsur, HTTP isteklerimizi mümkün olduğunca hızlı ve paralel göndermektir. Çünkü sunucu algoritmayı bitirmeden yeni paketlerimiz sunucuya ulaşmalı ve işleme alınmalıdır. Bu yazıda Burp Suite eklentisi olan Turbo Intruder’i kullanacağız.
Turbo Intruder, çok sayıda HTTP isteğini eş zamanlı göndermek için kullanılan Burp Suite eklentisidir. Aşırı hız ve karmaşıklık gerektiren saldırıları gerçekleştirebilmesi sebebiyle Burp Intruder’a alternatif olarak kullanılmaktadır.
Burp Suite’e Turbo Intruder eklentisini yükleyelim
1 - Burp Suite’i çalıştıralım
2 - Extender sekmesine gidin

3 - BApp Store sekmesine gidin ve listeden Turbo Intruder’i bulun

4 - Install butonuna tıklayarak Burp Intruder kurulumunu yapın
Artık eklentimiz hazır.
Artık örnek uygulamamızdaki transferMoney isimli uç noktamıza JSON formatında “fromID, toID ve amount” alanlarını göndermeliyiz ve Burp Suite ile de bu isteği yakalamalıyız. API testleri için kullandığımız Postman’ı kullanarak verileri göndereceğim.

Postman, Burp Suite Proxy’i kullanacak şekilde ayarlanmış durumda. İsteği gönderdiğimizde Burp Suite ile yakalayıp Turbo Intruder’a gönderiyoruz.

İlgili isteğin üzerinde sağ tıklayıp “Send to Turbo Intruder” seçeneğine tıklayarak isteği Turbo Intruder’a gönderdik.

Örnek kodların Turbo Intruder kodlarının içerisinden race.py olanı seçiyoruz. İstekleri birbirinden ayırmak için User Agent’in sonuna %s ekliyoruz yoksa hata veriyor.

Kodları incelediğimizde eş zamanlı 30 bağlantı oluşturulacağını görüyoruz. Senaryomuz için gayet yeterli rakamlar.
Veri tabanındaki duruma göz atıyoruz

Ali, 100 lira bakiyeye sahip. Veli’nin ise henüz bakiyesi mevcut değil.

İsteğimizi kontrol ediyoruz. Gönderici hesap numarası 1 yani Ali, alıcı ise 2 yani Veli. Gönderilmek istenen miktarı 50 lira yapıyoruz ki daha Race Condition yakalamak için şansımızı arttıralım. Attack butonuna tıkladığımızda bakalım neler yaşanacak :)

Beklediğimiz oluyor ve şaşırtıcı bir şekilde(bu kadar beklemiyordum) 30 isteğimizin tamamı 200 OK kodu ve “Transfer başarılı” mesajı ile bize geri dönüyor. Bu, Race Condition’u yakaladığımızı ve 50 liranın çok üstünde bir rakamı transfer ettiğimizi bize gösteriyor.
Veritabanımıza bakalım ve neler yaşanmış görelim

Biz 50 lira transfer etmiştik, Ali’nin mevcut bakiyesi de 100 liraydı ancak görüyoruz ki Veli’ye 200 lira transfer edebilmişiz. Ali’nin bakiyesi ise -50’ye düşmüş bulunmakta.
Küçük bir bankacılık uygulamasında Race Condition testi yaptık ve hesabımızda olmayan parayı arkadaşımıza gönderebildik. Peki sadece bankacılık uygulamalarında mı karşılaşırız Race Condition ile ? Tabiki hayır.
Gerçek hayattan Race Condition kesitleri

reverb.com - Hediye kuponunun birden fazla kez kullanılabilmesi
Hackerone üzerinden bildirilen #759247 numaralı bug bildiriminde hacker, 25$ tutarında bir hediye kuponu satın alıyor ve Race Condition zafiyetini sömürerek bu kuponu 7 kere kullanıyor. Hesabına 25*7 = 175$ değerinde kupon tanımlamış oluyor. Kendisine bu bildirim için 1,500$ ödül veriliyor.

Forge Of Empires - Ücretli oyun parasının birden fazla kez kazanılması
Hackerone üzerinden bildirilen #509629 numaralı bug bildiriminde hacker, Innogames’e ait Forge of Empires isimli oyunda yeni hesap açıldığında gelen ve tıklanıldığında 50 ücretli oyun parası veren e-posta onay bağlantısında Race Condition keşfediyor. Race Condition’u kullanan hacker, 50 oyun parası kazanması gerekirken 800 oyun parası kazanıyor. Kendisine bildiriminden dolayı 2,000$ ödül veriliyor.
Hacker101 - Gruptan silinemeyen üye
Hackerone üzerinden bildirilen #604534 numaralı bug bildiriminde hacker, Hacker101 üzerindeki bir gruba katılma bağlantısında Race Condition keşfediyor. Bu Race Condition’u kullanarak kendini gruba birden fazla defa ekliyor, grup kurucusu ise hackeri gruptan çıkarması mümkün değil. Farklı tarzı ile ince gören hacker 500$ ödül kazanıyor.

Keybase - Davetiye limitini atlatmak
Hackerone üzerinden bildirilen #115007 numaralı bug bildiriminde hacker, Keybase.io’da 3 davetiyesi bulunmasına rağmen Race Condition zafiyetini sömürerek 7 davetiye gönderebiliyor. Bu bildirimden dolayı 350$ kazanıyor.
Race Condition Nasıl önlenir ?
Race Condition her şeyden önce mimari bir problemdir. Mimarinin doğru inşa edilmesi, race condition için fırsat vermeyecektir.
Web Uygulamalarında Race Condition’u engellemek için farklı teknolojiler için farklı yöntemler bulunmaktadır. Veritabanı kilitleri, izolasyon seviyeleri gibi özellikler kullanılarak Race Condition’a karşı önlem alınabilir. Kullandığınız teknolojilere göre bu konuda araştırma yapabilirsiniz.