June 11, 2026
Credential Stuffing Saldırısı: Kalabalığın İçinde Kaybolan Tehdit
Siber saldırıların büyük çoğunluğu karmaşık açıklarla ya da zincirleme exploit tekniklerle gerçekleşmiyor. Asıl hasar çoğu zaman çok daha…
İbrahim Yiğit Çetin
12 min read
Siber saldırıların büyük çoğunluğu karmaşık açıklarla ya da zincirleme exploit tekniklerle gerçekleşmiyor. Asıl hasar çoğu zaman çok daha basit bir gerçeğin üstüne inşa ediliyor: insanlar aynı şifreyi birden fazla yerde kullanıyor. Credential stuffing tam olarak bu zafiyeti, endüstriyel ölçekte ve tamamen otomatik biçimde sömürüyor.
Peki Nedir Bu Credential Stuffing?
Credential stuffing, siber saldırganların ellerindeki sızdırılmış kullanıcı adı ve şifre listelerini, otomatik botlar aracılığıyla farklı platformlarda sistematik olarak denedikleri bir siber saldırı türüdür. Bu saldırı, sistemdeki teknik bir açıktan ziyade, kullanıcıların aynı şifreyi birden fazla platformda kullanma alışkanlığını sömürür. Saldırganın amacı, bir platformdan sızan bilgilerle kullanıcının diğer hesaplarına (banka, e-ticaret, sosyal medya vb.) "meşru" bir giriş yaparak yetkisiz erişim sağlamaktır.
Saldırının Anatomisi
Her şey bir veri sızıntısıyla başlıyor. Büyük çaplı ihlallerin ardından e-posta adresi ve şifre çiftlerinden oluşan devasa listeler dark web pazarlarında, Telegram kanallarında ya da ücretsiz forum gönderilerinde dolaşıma giriyor. HaveIBeenPwned'in indekslediği veriler bu sızıntıların boyutunu net ortaya koyuyor: bugün itibarıyla 14 milyarı aşkın ele geçirilmiş hesap kaydı mevcut.
Bu listeler her zaman ham hâlde gelmiyor. En yaygın format email:password çiftlerinden oluşan düz metin dosyaları olmakla beraber bunlara combo list deniyor. Bir adım ileri gidildiğinde stealer log çıkıyor karşına; yalnızca şifre değil, tarayıcı oturumları ve çerezler de var içinde. En değerlisi ise belirli bir platforma ait, orada test edilmiş ve çalıştığı doğrulanmış çiftlerden oluşan listeler. Fiyatlar buna göre şekilleniyor: genel bir 100 milyon satırlık combo list birkaç dolarken, belirli bir bankaya ait doğrulanmış hesap listesi yüzlerce dolara ulaşabiliyor.
Saldırgan listeleri temin ettikten sonra otomatik araçları devreye sokuyor. Piyasada en çok kullanılanlar arasında OpenBullet öne çıkıyor (açık kaynak, ücretsiz, her platforma özel hazır konfigürasyon dosyaları internette serbestçe dolaşıyor). Bu araçlar config adı verilen ayar dosyalarıyla çalışıyor. Her hedef site için ayrı bir config yazılıyor; içinde üç temel şey var: sitenin giriş adresi, gönderilecek kullanıcı adı ve şifre, başarılı girişi anlamak için yanıtta aranacak bir ifade.
URL = https://hedef-site.com/api/login
POSTDATA = username={{USER}}&password={{PASS}}
SUCCESS = "welcome_back"URL = https://hedef-site.com/api/login
POSTDATA = username={{USER}}&password={{PASS}}
SUCCESS = "welcome_back"Bu kadar. Birkaç satır, herhangi bir platforma karşı çalışan bir saldırı altyapısı.
Hız ve anonimlik için saldırı proxy ağları üzerinden yürütülüyor. En ucuzu bulut sağlayıcıların IP'lerinden geçen datacenter proxy'leri (tespiti kolay, çoğu platform bu aralıkları tanıyor). Bir üst katmanda residential proxy var: gerçek kullanıcıların ev internet bağlantıları kiraya verilmiş, gelen trafik sıradan bir kullanıcıdan farksız görünüyor. En gelişmişi mobile proxy: gerçek 4G/5G hatları, IP'ler dinamik atandığı için takip etmek neredeyse imkânsız. Fiyat farkı da büyük (datacenter proxy saniyesi kuruşlarla hesaplanırken kaliteli residential proxy aylık yüzlerce dolara çıkıyor).
Neden Bu Kadar Etkili?
Başarı oranı düşük görünüyor: yüzde 0.1 ile 2 arasında. Ama bu rakam kullanılan listelerin boyutuyla çarpılınca tablo değişiyor. 500 milyon girişimde yüzde 0.2'lik bir oran, 1 milyon ele geçirilmiş hesap demek.
Sorunun kökünde şifre yeniden kullanımı var. Google'ın 2019 araştırması, sızdırılan şifrelerin yüzde 1.5 ile 7'sinin başka hesaplarda da geçerli olduğunu gösteriyor. Kullanıcılar farklı platformlarda aynı şifreyi kullandığı sürece saldırının hammaddesi tükenmiyor.
Saldırgan açısından bakıldığında bu düşük maliyetli ve kolay ölçeklenen bir iş modeli. Üstelik bu saldırı hedef sistemdeki teknik bir açığı değil, kullanıcı davranışındaki bir örüntüyü sömürüyor. Giriş bilgileri "doğru", erişim "meşru" görünüyor. Bu yüzden tespit etmek klasik kaba kuvvet saldırılarından çok daha zor.
Senaryo: Bir Hesabın Sessiz Ölümü
Ahmet, alışkın olduğu şifreyi yıllardır her yerde kullanıyor. 2018'de bir e-ticaret sitesine üye olurken seçtiği ahmet.1987, o günden bu yana bankasında, müzik platformunda, iş e-postasında ve düzinelerce başka hesapta aynı şekilde duruyor. 2019'da o e-ticaret sitesi büyük bir veri ihlali yaşadı. Şirket duyuruyu küçük bir basın açıklamasıyla geçiştirdi, kullanıcıları e-postayla bilgilendirmedi. Ahmet haberi kaçırdı. Şifresi o günden bu yana dark web'de dolaşıyor.
Ocak 2024'te bir saldırgan, çeşitli sızıntı listelerini birleştiren yaklaşık 80 milyon satırlık bir combo list satın aldı (fiyatı 12 dolar). Listeyi açtığında format standart: ahmet@gmail.com:ahmet.1987 gibi milyonlarca satır, düz metin. Hedef olarak Ahmet'in kullandığı müzik platformunu seçti. Giriş sayfasını birkaç dakika inceledi: giriş noktası herkese açık, isteklere herhangi bir bot koruması uygulanmıyor, CAPTCHA yok.
OpenBullet'i açtı, platforma özel config dosyasını yükledi. Config'in içinde üç şey var: giriş URL'si, gönderilecek kullanıcı adı ve şifre parametreleri, başarılı girişi anlamak için yanıtta aranacak ifade. Proxy listesini de ekledi (residential proxy, her biri farklı bir ülkeden gerçek ev bağlantısı). Aracı başlattı.
Araç listeyi işlemeye başladı. Her deneme farklı bir IP üzerinden gidiyordu. İstekler arasındaki bekleme süresi rastgele ayarlanmıştı. Tarayıcı kimliğini temsil eden User-Agent her seferinde değişiyordu. Platform tarafında bunların hepsi dünyanın farklı köşelerinden bağlanan masum kullanıcılar gibi görünüyordu.
Ahmet'in satırına gelindiğinde yanıt farklı geldi. Diğerlerinin döndürdüğü {"error": "invalid_credentials"} yerine bu sefer {"status": "success", "token": "eyJhbGci…"} vardı. Araç bunu "hit" olarak işaretledi, Ahmet'in bilgilerini ayrı bir dosyaya yazdı ve kaldığı yerden devam etti.
Saldırgan dosyayı kontrol etti. Hesapta premium üyelik vardı, son aktivite iki gün önceydi, kayıtlı ödeme yöntemi yoktu. Hesabı hemen satışa çıkarmak yerine bir süre bekledi. Birkaç gün sonra dark web'deki toplu satış listesine ekledi: "müzik platformu premium" kategorisi, birim fiyat 0.40 dolar. Aynı gün iki alıcı hesabı edindi (biri Brezilya'dan, biri Polonya'dan).
Ahmet fark etmedi. Şifre değişmemişti, hesap hâlâ açılıyordu. Üç hafta sonra platformun dolandırıcılık tespit ekibi bir anomali fark etti: aynı hesap Türkiye, Brezilya ve Polonya'dan aynı gün içinde giriş yapmıştı. Coğrafi olarak imkânsız bir örüntü. Otomatik sistem hesabı askıya aldı ve Ahmet'e bir e-posta gönderdi. Ahmet e-postayı spam klasöründe buldu, şifresini değiştirdi ve olayı kapattı. Ama Ahmet'in aynı şifreyi kullandığı diğer onlarca platform hâlâ açıktı; saldırgan da aynı combo listesiyle onlara geçmişti bile.
2FA Varken Saldırgan Ne Yapıyor?
Ahmet senaryosu klasik bir credential stuffing vakasını anlatıyor. Ancak 2025–2026 itibarıyla saldırı yöntemleri genişledi.
Birçok platformda artık 2FA (iki faktörlü doğrulama) zorunlu. Ama saldırganlar durmadı, yöntem değiştirdi.
Session stuffing / Cookie replay: Stealer log'lar artık yalnızca şifre değil, aktif oturum çerezlerini de taşıyor. Kurbanın tarayıcısından çalınan çerez doğrudan tekrar kullanıldığında 2FA devreye girmiyor; çünkü oturum zaten açık sayılıyor. Kimlik doğrulama adımı hiç gerçekleşmiyor.
Adversary-in-the-Middle (AiTM) phishing: Saldırgan, kurban ile gerçek site arasına ters proxy (reverse proxy) kuruyor. Kurban 2FA kodunu kendisi giriyor, saldırgan bunu gerçek zamanlı olarak kendi oturumuna aktarıyor. Evilginx2 gibi araçlar bu akışı otomatik hale getiriyor.
Token theft: Mobil ve masaüstü uygulamalarda oturumu temsil eden JWT ya da OAuth token'ları çalındığında şifre ve 2FA tamamen anlamsız hale geliyor. Uygulama katmanında token geçerliyse giriş de geçerli sayılıyor.
Credential stuffing ile session theft kombinasyonu, saldırıyı artık "şifre çalma"dan "kimlik çalma"ya dönüştürüyor. Savunma bu çerçevede tasarlanmak zorunda.
Gerçek Olay: 2020 Nintendo Hesap İhlali
Nisan 2020'de Nintendo, yaklaşık 160.000 kullanıcı hesabının yetkisiz erişime uğradığını duyurdu. Saldırganlar o dönemde hâlâ aktif olan Nintendo Network ID (NNID) sistemi üzerinden hesaplara girdi ve bu erişimi Nintendo Store'da sahte satın alımlar için kullandı. Etkilenen hesaplarda kayıtlı ödeme bilgileri kullanılmış, bazı kullanıcılar kredi kartı ekstrelerinde beklenmedik harcamalarla karşılaşmıştı.
Soruşturma sonucunda saldırının credential stuffing olduğu doğrulandı. Sızdırılmış listelerden gelen kimlik bilgileri, yıllar önce açılmış ve o tarihten bu yana modern bot koruması eklenmemiş NNID giriş noktası üzerinden sistematik biçimde deneniyordu. NNID ile ana Nintendo hesabı arasındaki entegrasyon da işleri kolaylaştırdı: başarılı bir NNID girişi doğrudan ana hesaba erişim sağlıyordu.
Nintendo'nun yanıtı hızlı geldi. NNID bağlantısını kalıcı olarak kesti, etkilenen hesaplarda iki faktörlü doğrulamayı zorunlu hale getirdi ve tüm kullanıcıları şifrelerini değiştirmeye davet etti. En sert eleştiri ise büyük ölçüde kullanım dışı kalmış bir sistemin neden bu kadar uzun süre korumasız bırakıldığı üzerineydi.
Saldırı Tarafındaki Evrim: Yapay Zeka Destekli Araçlar
2026'da saldırı araçları OpenBullet'in config mantığını çok geride bırakmış durumda.
Klasik araçlar başarılı girişi bir JSON anahtarına ya da HTTP durum koduna göre tanımlıyordu: {"status": "success"} görürse "hit" yazardı. Bu yaklaşım statik ve kırılgandı; platform yanıt formatını değiştirince config de bozulurdu.
Yeni nesil araçlar farklı çalışıyor.
Vision-enabled botlar: Ekran görüntüsü alıp doğrudan sayfayı görsel olarak analiz ediyor. Giriş başarılı olduğunda sayfada ne değiştiğini "görsel olarak" algılıyor. Karşılama mesajı mı çıktı, avatar mı belirdi, menü mü değişti? Bunların hepsini yorumlayabiliyor. JSON formatı değişse bile tespit çalışmaya devam ediyor.
DOM-aware agent sistemleri: Tarayıcıyı bir insan gibi kontrol eden yapay zeka tabanlı ajanlar, giriş formunu otomatik buluyor, alanları dolduruyor, 2FA ekranlarına tepki veriyor. Sayfanın yapısı değişse bile ajan uyum sağlıyor.
Adaptif CAPTCHA bypass: Geleneksel CAPTCHA çözücüler görsel bulmacaları başka insanlara ya da özel modellere havale ediyordu. Yeni sistemler bazı CAPTCHA türlerini artık model üzerinden doğrudan çözebiliyor. Daha da kritik olan şu: modern araçlar CAPTCHA gösterilmemesi için davranışı insan örüntülerine yakınsatıyor. Yani sorun çıkmadan geçmeyi tercih ediyor.
Bu evrim, savunma tarafındaki statik kural setlerini ve imza tabanlı tespiti giderek daha az güvenilir hale getiriyor.
Savunma Tarafı: Teknik Önlemler
Credential stuffing'e karşı tek bir kontrol yeterli değil. Etkili savunma katmanlı olmak zorunda.
1. Kimlik Bilgilerini Sızıntı Veritabanlarıyla Karşılaştırmak
Kullanıcı şifre oluştururken ya da giriş yaparken o şifrenin daha önce sızıp sızmadığını kontrol edebilirsiniz. HaveIBeenPwned'in Pwned Passwords API'si bunu hash üzerinden yapıyor; şifrenin kendisini göndermenize gerek yok. Şifre önce SHA-1 ile hash'leniyor, hash'in yalnızca ilk 5 karakteri API'ye gönderiliyor, geri kalanıyla eşleşme sizin tarafınızda yapılıyor.
import hashlib, requests
def is_pwned(password: str) -> int:
sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
prefix, suffix = sha1[:5], sha1[5:]
response = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}")
hashes = (line.split(":") for line in response.text.splitlines())
return next((int(count) for h, count in hashes if h == suffix), 0)
count = is_pwned("ahmet.1987")
if count:
print(f"Bu şifre {count} sızıntıda görüldü. Değiştirin.")import hashlib, requests
def is_pwned(password: str) -> int:
sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
prefix, suffix = sha1[:5], sha1[5:]
response = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}")
hashes = (line.split(":") for line in response.text.splitlines())
return next((int(count) for h, count in hashes if h == suffix), 0)
count = is_pwned("ahmet.1987")
if count:
print(f"Bu şifre {count} sızıntıda görüldü. Değiştirin.")hashlib ve requests kütüphaneleri içe aktarılıyor. hashlib hash işlemleri için, requests ise HTTP istekleri yapmak için kullanılıyor.
is_pwned fonksiyonu bir şifre alıyor ve o şifrenin kaç farklı sızıntıda göründüğünü döndürüyor. Sıfır dönerse temiz demektir.
sha1 = hashlib.sha1(password.encode()).hexdigest().upper() satırı şifreyi önce byte dizisine çeviriyor (encode()), sonra SHA-1 algoritmasıyla hash'liyor, ardından hexadecimal (16'lık tabanda) bir string'e dönüştürüyor ve tüm harfleri büyük yapıyor. Sonuç 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8 gibi 40 karakterlik bir string oluyor.
prefix, suffix = sha1[:5], sha1[5:] bu hash'i ikiye bölüyor. İlk 5 karakter prefix (örneğin 5BAA6), geri kalan 35 karakter suffix oluyor. API'ye yalnızca prefix gönderiliyor; böylece şifrenin tam hash'i dışarıya hiç çıkmıyor. Buna "k-anonimlik" deniyor.
response = requests.get(…) satırı API'ye o 5 karakterlik prefix'i gönderiyor. API karşılığında aynı prefix'le başlayan tüm hash'lerin kalanını ve her birinin kaç kez sızdığını döndürüyor. Örnek bir yanıt satırı: 1E4C9B93F3F0682250B6CF8331B7EE68FD8:3561 (suffix:kaç_kez_görüldü formatında).
hashes = (line.split(":") for line in response.text.splitlines()) yanıttaki her satırı : karakterinden ikiye bölüyor ve [suffix, count] çiftleri üretiyor.
return next(…) bu çiftler arasında bizim suffix'imizle eşleşen birini arıyor. Bulursa kaç kez göründüğünü döndürüyor, bulamazsa 0 döndürüyor.
Son iki satır fonksiyonu çağırıp sonuca göre uyarı yazıyor.
2. Rate Limiting ve Hesap Kilitleme
Belirli bir sürede çok fazla hatalı giriş denemesini engellemek anlamına geliyor. IP bazlı rate limit tek başına yetersiz; proxy rotasyonuyla kolayca aşılıyor. Hesap bazlı kilitleme çok daha etkili: aynı kullanıcı adına 5 hatalı deneme gelirse hesabı geçici olarak kilitle.
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
server {
location /api/auth/login {
limit_req zone=login burst=5 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
server {
location /api/auth/login {
limit_req zone=login burst=5 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m satırı Nginx'e şunu söylüyor: her IP adresi için ayrı bir sayaç tut, bu sayaçları login adlı bir havuzda sakla, bu havuz için 10 megabayt bellek ayır ve her IP'nin dakikada en fazla 10 istek göndermesine izin ver.
location /api/auth/login bloğu bu kuralın yalnızca giriş endpoint'ine uygulanacağını belirtiyor. Sitenin diğer sayfaları etkilenmiyor.
limit_req zone=login burst=5 nodelay satırı login havuzundaki kuralı devreye sokuyor. burst=5 ani bir artışa tolerans tanıyor: kullanıcı birkaç saniye içinde 5 istek göndermişse hepsini kabul et, ama 6. istek gelirse reddet. nodelay ise bu burst isteklerini kuyruğa almak yerine anında işle demek.
limit_req_status 429 kural aşıldığında döndürülecek HTTP durum kodunu belirtiyor. 429 "Too Many Requests" (çok fazla istek) anlamına geliyor.
proxy_pass http://backend normal koşullarda (kural aşılmadığında) isteği asıl uygulama sunucusuna yönlendiriyor.
Uygulama katmanında hesap bazlı sayaç (Redis ile):
import redis
r = redis.Redis()
def check_login_attempts(username: str) -> bool:
key = f"login_attempts:{username}"
attempts = r.incr(key)
if attempts == 1:
r.expire(key, 900)
if attempts > 5:
return False
return Trueimport redis
r = redis.Redis()
def check_login_attempts(username: str) -> bool:
key = f"login_attempts:{username}"
attempts = r.incr(key)
if attempts == 1:
r.expire(key, 900)
if attempts > 5:
return False
return Trueimport redis ve r = redis.Redis() satırları Redis kütüphanesini içe aktarıp yerel Redis sunucusuna bağlanıyor. Redis, değerleri bellekte tutan ve çok hızlı okuma/yazma yapabilen bir veritabanı. Sayaç tutmak için ideal.
check_login_attempts fonksiyonu bir kullanıcı adı alıyor ve o kullanıcının daha fazla giriş denemesi yapmasına izin verilip verilmeyeceğini (True/False) döndürüyor.
key = f"login_attempts:{username}" Redis'te bu kullanıcıya ait sayacın anahtarını oluşturuyor. Örneğin ahmet@gmail.com için anahtar login_attempts:ahmet@gmail.com olacak.
attempts = r.incr(key) Redis'teki sayacı 1 artırıyor ve yeni değeri döndürüyor. Anahtar daha önce yoksa Redis onu 0'dan başlatıp 1 yapıyor. Yani her hatalı giriş denemesinde bu sayaç bir artıyor.
if attempts == 1: r.expire(key, 900) sayaç ilk kez oluşturulduğunda (yani 1 değerini aldığında) 900 saniyelik (15 dakika) bir süre sonu belirleniyor. Bu sayede kullanıcı 15 dakika hiçbir şey yapmazsa sayaç sıfırlanıyor ve hesap kendiliğinden açılıyor.
if attempts > 5: return False 5'ten fazla deneme yapılmışsa fonksiyon False döndürüyor. Çağıran kod bunu görünce giriş isteğini reddediyor.
return True 5 veya daha az deneme yapılmışsa giriş denemesine izin veriliyor.
3. Davranışsal Anomali Tespiti
Normal trafikte başarılı giriş oranı yüzde 60–80 bandında seyreder. Gerçek kullanıcılar genellikle doğru şifrelerini bilir. Bu oran ani biçimde düşerse geçersiz deneme trafiği geliyordur.
from collections import deque
from datetime import datetime, timedelta
class LoginAnomalyDetector:
def __init__(self, window_minutes=10, threshold=0.15):
self.window = deque()
self.window_size = timedelta(minutes=window_minutes)
self.threshold = threshold
def record(self, success: bool):
now = datetime.utcnow()
self.window.append((now, success))
while self.window and (now - self.window[0][0]) > self.window_size:
self.window.popleft()
def is_attack(self) -> bool:
if len(self.window) < 50:
return False
success_rate = sum(1 for _, s in self.window if s) / len(self.window)
return success_rate < self.thresholdfrom collections import deque
from datetime import datetime, timedelta
class LoginAnomalyDetector:
def __init__(self, window_minutes=10, threshold=0.15):
self.window = deque()
self.window_size = timedelta(minutes=window_minutes)
self.threshold = threshold
def record(self, success: bool):
now = datetime.utcnow()
self.window.append((now, success))
while self.window and (now - self.window[0][0]) > self.window_size:
self.window.popleft()
def is_attack(self) -> bool:
if len(self.window) < 50:
return False
success_rate = sum(1 for _, s in self.window if s) / len(self.window)
return success_rate < self.thresholddeque (çift uçlu kuyruk) içe aktarılıyor. Normal Python listesinden farkı şu: başından eleman silmek çok daha hızlı. Kayan zaman penceresi için ideal bir yapı.
init metodu sınıfı başlatıyor. window_minutes=10 son kaç dakikalık veriyi takip edeceğimizi belirtiyor (varsayılan 10 dakika). threshold=0.15 eşik değeri, yani başarılı giriş oranı yüzde 15'in altına düşerse saldırı var sayılacak. self.window giriş kayıtlarını tutan kuyruk. self.window_size 10 dakikayı bir timedelta nesnesine dönüştürüyor; zaman farkı hesaplamalarında kullanılacak.
record metodu her giriş denemesinde çağrılıyor. now = datetime.utcnow() o anki zamanı alıyor. self.window.append((now, success)) zaman damgası ve başarı durumunu (True/False) bir çift olarak kuyruğa ekliyor. Ardından gelen while döngüsü kuyruğun başındaki kayıtları kontrol ediyor: eğer en eski kayıt 10 dakikadan eskiyse kuyruktan çıkarıyor. Böylece pencere her zaman son 10 dakikayı kapsıyor.
is_attack metodu saldırı olup olmadığını değerlendiriyor. if len(self.window) < 50 kuyruğun en az 50 kayıt içermesini şart koşuyor; az örnekle karar vermek hatalı alarm üretebilir. success_rate satırı kuyruktaki başarılı girişlerin oranını hesaplıyor: sum(1 for _, s in self.window if s) başarılı girişleri sayıyor, bunu toplam kayıt sayısına böldüğümüzde oran çıkıyor. Bu oran eşik değerinin (0.15) altındaysa True döndürüyor, yani "saldırı var" deniyor.
4. TLS Fingerprinting
Saldırı araçları IP adresini her istekte değiştirebilir, ama TLS el sıkışma (handshake) davranışını pek değiştirmiyor. TLS bağlantısı kurulurken tarayıcı ya da araç kendine özgü bir imza bırakıyor; buna JA3 fingerprint deniyor. Aynı JA3 değeri onlarca farklı IP'den geliyorsa büyük ihtimalle aynı araçtan çıkan trafiktir.
tshark -r capture.pcap \
-T fields \
-e ip.src \
-e tls.handshake.type \
-e tls.handshake.ciphersuite \
| python3 compute_ja3.pytshark -r capture.pcap \
-T fields \
-e ip.src \
-e tls.handshake.type \
-e tls.handshake.ciphersuite \
| python3 compute_ja3.pytshark Wireshark'ın komut satırı versiyonu; ağ trafiğini analiz etmeye yarıyor.
-r capture.pcap daha önce kaydedilmiş bir ağ trafiği dosyasını okuyor. .pcap dosyaları ağ paketlerini kayıt altına almanın standart formatı.
-T fields çıktıyı ham tablo formatında ver demek; böylece sonraki betikle parse etmek kolaylaşıyor.
-e ip.src her paketin kaynak IP adresini çıkar.
-e tls.handshake.type TLS el sıkışma paketinin türünü çıkar (Client Hello, Server Hello gibi).
-e tls.handshake.ciphersuite istemcinin desteklediği şifreleme yöntemlerinin listesini çıkar. Bu liste JA3 hesaplamasının temel girdisi; farklı araçlar farklı cipher suite listeleri sunuyor.
| python3 compute_ja3.py ise bu ham veriyi compute_ja3.py betiğine aktarıyor. O betik cipher suite listesi, TLS versiyonu ve uzantılardan bir MD5 hash üretiyor. Çıkan hash o istemcinin parmak izi oluyor.
5. Modern Anti-Bot Sistemleri
Redis sayaçları ve JA3 fingerprinting iyi bir başlangıç katmanı oluşturuyor. Ama büyük ölçekli platformlarda tehdit modeli artık bunun ötesini gerektiriyor.
Cloudflare Turnstile, Akamai Bot Manager ve benzeri sistemler çok daha geniş bir sinyal kümesine bakıyor:
- Cihaz entropy'si: Ekran çözünürlüğü, GPU, font listesi, donanım ivmelenme desteği gibi cihaza özgü özellikler. Gerçek bir tarayıcı ile bot ortamı arasındaki farklılıklar burada kendini gösteriyor.
- Davranış analizi: Mouse hareketi, tuş vuruş ritmi, scroll örüntüsü, girişler arasındaki gecikme. İnsan davranışı ölçülebilir düzensizlikler taşır; bot davranışı genellikle taşımaz.
- Tarayıcı telemetrisi: WebGL render davranışı, Canvas parmak izi, AudioContext özellikleri. Araçlar bu katmanları taklit etmekte giderek daha başarılı olsa da her taklit kendi imzasını bırakıyor.
- "İnsan varlığı" sinyalleri: Sayfada geçirilen süre, etkileşim derinliği, geçmiş oturum örüntüleri.
Temel mantık şu: tek bir sinyali manipüle etmek mümkün, ama yüzlerce sinyali tutarlı biçimde sahte üretmek giderek zorlaşıyor. Savunmanın amacı saldırıyı imkânsız kılmak değil, maliyetini artırmak.
6. MFA (Çok Faktörlü Doğrulama)
Ele geçirilmiş bir şifre, ikinci faktör olmadan işe yaramıyor. En yaygın yöntem TOTP (zaman tabanlı tek kullanımlık şifre): kullanıcının telefon uygulamasında her 30 saniyede yenilenen 6 haneli bir kod.
import pyotp
def generate_totp_secret() -> str:
return pyotp.random_base32()
def verify_totp(secret: str, token: str) -> bool:
totp = pyotp.TOTP(secret)
return totp.verify(token, valid_window=1)import pyotp
def generate_totp_secret() -> str:
return pyotp.random_base32()
def verify_totp(secret: str, token: str) -> bool:
totp = pyotp.TOTP(secret)
return totp.verify(token, valid_window=1)import pyotp TOTP işlemlerini kolaylaştıran bir Python kütüphanesini içe aktarıyor. RFC 6238 standardını uyguluyor; yani Google Authenticator, Authy gibi her standart uygulamayla uyumlu çalışıyor.
generate_totp_secret kullanıcı için bir kez üretilip veritabanında saklanan gizli anahtarı oluşturuyor. pyotp.random_base32() rastgele 32 karakterlik bir Base32 string üretiyor; örneğin JBSWY3DPEHPK3PXP gibi bir şey. Bu anahtar hem sunucuda hem de kullanıcının telefon uygulamasında bulunuyor. QR kod okutulduğunda telefona aktarılan da bu anahtar.
verify_totp kullanıcının girdiği kodu doğruluyor. secret veritabanındaki gizli anahtar, token ise kullanıcının az önce telefonundan okuduğu 6 haneli kod.
totp = pyotp.TOTP(secret) o anahtarla bir TOTP nesnesi oluşturuyor. Bu nesne, Unix zaman damgasını ve gizli anahtarı birleştirerek her 30 saniyede bir yeni kod üretiyor; hem sunucu hem telefon aynı algoritmayı çalıştırdığı için kodlar eşleşiyor.
totp.verify(token, valid_window=1) kullanıcının gönderdiği kodu doğruluyor. valid_window=1 bir önceki ve bir sonraki 30 saniyelik penceredeki kodları da geçerli sayıyor. Bu tolerans ağ gecikmesi ve saat farklılıkları için gerekli; aksi hâlde kullanıcı kodu tam sınırda girerse doğrulama başarısız olabilir.
Şifreyi Ortadan Kaldırmak: Passkeys ve Passwordless Authentication
Buraya kadar anlatılan her şey aynı zemin üstüne inşa edilmiş: şifreler var, ve şifreler başkasına iletilebiliyor. Combo list bu yüzden çalışıyor. Session theft bu yüzden çalışıyor.
2026 itibarıyla sektörün önemli bir kısmı bu zemini ortadan kaldırmayı tercih ediyor.
Passkeys (FIDO2 / WebAuthn): Kullanıcı adı-şifre çiftinin yerini kriptografik anahtar çifti alıyor. Özel anahtar cihazda (güvenli donanımda) saklanıyor, hiçbir zaman sunucuya gitmiyor. Kimlik doğrulama biyometri ya da cihaz PIN'i ile tetikleniyor. Sunucuda saklanan şey yalnızca public key. Bu key sızdırılsa bile başka bir yerde işe yaramıyor; credential stuffing'in hammaddesi hiç oluşmuyor.
// WebAuthn ile kayıt
const credential = await navigator.credentials.create({
publicKey: {
challenge: serverChallenge,
rp: { name: "Hedef Platform", id: "hedef.com" },
user: { id: userId, name: userEmail, displayName: userName },
pubKeyCredParams: [{ type: "public-key", alg: -7 }], // ES256
authenticatorSelection: {
residentKey: "required",
userVerification: "required"
}
}
});
// credential.id ve credential.response sunucuya gönderilir
// Sunucu public key'i kaydeder, şifre asla iletilmez// WebAuthn ile kayıt
const credential = await navigator.credentials.create({
publicKey: {
challenge: serverChallenge,
rp: { name: "Hedef Platform", id: "hedef.com" },
user: { id: userId, name: userEmail, displayName: userName },
pubKeyCredParams: [{ type: "public-key", alg: -7 }], // ES256
authenticatorSelection: {
residentKey: "required",
userVerification: "required"
}
}
});
// credential.id ve credential.response sunucuya gönderilir
// Sunucu public key'i kaydeder, şifre asla iletilmezGoogle, Apple ve Microsoft passkey altyapısını platform düzeyinde entegre etmiş durumda. Kullanıcı passkey destekli bir platforma giriş yaptığında telefonda Face ID ya da parmak izi isteniyor. Şifre yok, kimlik avı yok, replay saldırısı yok.
Neden bu kadar önemli? Çünkü, Credential stuffing'in "şifreyi daha iyi koruyun" ile çözülmesi mümkün değil; sorun korunabilirlikte değil, paylaşılabilirlikte. Passkey bu paylaşılabilirliği mimariden kaldırıyor. Sızdırılamayan bir sırrı başka platformda denemek mümkün olmuyor.
Bu yaklaşımın önündeki en büyük engel geriye dönük uyumluluk: eski sistemleri passkey'e taşımak özellikle büyük kullanıcı tabanlarında ciddi bir maliyet demek. Ama yeni platformlar için passkey-first bir mimari artık makul bir tercih olarak öne çıkıyor.
Şifreyi Korumak mı, Ortadan Kaldırmak mı?
Credential stuffing'e karşı etkili savunma iki katmanda çalışmak zorunda.
Kısa vade: MFA zorunluluğu, davranışsal anomali izleme, sızıntı bildirim entegrasyonu, akıllı rate limiting ve modern anti-bot sistemleri birlikte çalıştığında saldırının maliyet-fayda dengesi bozuluyor. Credential stuffing bir maliyet-fayda hesabıdır; savunma bu hesabı saldırgan aleyhine çevirmeyi hedefler.
Uzun vade: Saldırının kök nedenine bakıldığında çözüm şifreleri daha iyi korumaktan değil, onları gereksiz kılmaktan geçiyor. Passkey/FIDO2 tabanlı passwordless authentication credential stuffing'in hammaddesini ortadan kaldırıyor. "Şifreleri koruyun" yerine "şifreleri kaldırın" yaklaşımı tehdit modelini köktenci biçimde değiştiriyor.
Yapay zeka destekli saldırı araçları, session theft kombinasyonları ve AiTM phishing teknikleri sahneye girdiğinde, şifreye dayalı her savunma konfigürasyonu bir adaptasyon yarışının içinde kalıyor. Passkey bu yarışın dışına çıkmayı mümkün kılıyor.
Şimdilik bu kadar. Daha fazlası için profilimi ziyaret edebilirsiniz.