Selam arkadaşlar,
İnternete açık bir VPS kiraladıysanız ilk gün şunu fark etmişsinizdir: sunucu daha hiçbir yerde duyurulmadan SSH portuna dakikada onlarca giriş denemesi gelmeye başlar. Bunların çoğu otomatik tarayıcılar ve botlardır. Ben kendi sunucularımda bu trafiği hem anlamak hem de raporlamak için küçük bir Python scripti kullanıyorum. Bu konuda o scripti satır satır anlatıyorum; tamamen savunma amaçlıdır ve sadece kendi sunucunuzun loglarında kullanılmalıdır.
Konuda Anlatılacaklar
- SSH giriş denemeleri loglarda nasıl görünür?
- auth.log mu, journalctl mi? (Debian/Ubuntu farkı)
- Python scripti: regex, Counter ve argparse ile analiz
- Çıktıyı yorumlamak: asıl tehlikeli olan satır hangisi?
- Scripti otomatikleştirmek ve sonraki adımlar
1) Loglarda SSH Denemeleri Nasıl Görünür?
OpenSSH sunucusu (sshd) her kimlik doğrulama olayını syslog'a yazar. Bizi ilgilendiren üç tip satır var:
Failed password for root from 198.51.100.7 port 52122 ssh2
Invalid user admin from 203.0.113.45 port 40022
Accepted publickey for bilge from 192.168.1.20 port 51000 ssh2- Failed password / Failed publickey: Var olan (veya "invalid user" olarak işaretlenen) bir hesaba hatalı deneme.
- Invalid user: Sistemde hiç olmayan bir kullanıcı adı denenmiş. Botlar admin, test, oracle, pi gibi isimleri sırayla dener.
- Accepted: Başarılı giriş. Brute force yapan bir IP'den bu satırı görüyorsanız alarm zili çalmalı.
2) auth.log mu, journalctl mi?
Ubuntu'da rsyslog varsayılan olarak geldiği için kayıtlar /var/log/auth.log dosyasında durur. Debian 12 ve sonrasında ise rsyslog varsayılan kurulumda gelmez, loglar sadece systemd journal içindedir. Script iki durumu da desteklesin diye dosya verilmezse standart girdiden (stdin) okuyacak şekilde yazdım:
# Ubuntu (rsyslog varsa)
sudo python3 ssh_tespit.py /var/log/auth.log
# Debian 12+ veya journal kullanan sistemler (servis adı dağıtıma göre ssh ya da sshd)
sudo journalctl -u ssh --since today -o cat | python3 ssh_tespit.py
sudo journalctl -u sshd --since "2 days ago" -o cat | python3 ssh_tespit.py --esik 53) Script
Python 3.8 ve üstü yeterli (walrus operatörü := kullandım). Harici paket yok, sadece standart kütüphane:
#!/usr/bin/env python3
"""auth.log / journalctl çıktısından SSH brute force denemelerini özetler."""
import argparse, ipaddress, json, re, sys
from collections import Counter, defaultdict
FAILED = re.compile(r"Failed (?:password|publickey) for (?:invalid user )?(?P<user>\S+) from (?P<ip>[0-9a-fA-F:.]+)")
INVALID = re.compile(r"Invalid user (?P<user>\S*) from (?P<ip>[0-9a-fA-F:.]+)")
ACCEPTED = re.compile(r"Accepted (?P<method>\S+) for (?P<user>\S+) from (?P<ip>[0-9a-fA-F:.]+)")
def analiz(satirlar):
hata = Counter() # "Failed password/publickey" satırları
gecersiz = Counter() # "Invalid user" satırları (olmayan kullanıcı)
kullanicilar = defaultdict(set)
basarili = defaultdict(list)
for satir in satirlar:
if m := FAILED.search(satir):
hata[m["ip"]] += 1
kullanicilar[m["ip"]].add(m["user"])
elif m := INVALID.search(satir):
gecersiz[m["ip"]] += 1
kullanicilar[m["ip"]].add(m["user"] or "(bos)")
elif m := ACCEPTED.search(satir):
basarili[m["ip"]].append(f'{m["user"]}/{m["method"]}')
return hata, gecersiz, kullanicilar, basarili
def main():
p = argparse.ArgumentParser(description="SSH brute force özet raporu")
p.add_argument("dosya", nargs="?", help="auth.log yolu (boşsa stdin okunur)")
p.add_argument("--esik", type=int, default=10, help="şüpheli sayılacak minimum hatalı deneme")
p.add_argument("--json", action="store_true", help="çıktıyı JSON ver")
a = p.parse_args()
kaynak = open(a.dosya, encoding="utf-8", errors="replace") if a.dosya else sys.stdin
with kaynak:
hata, gecersiz, kullanicilar, basarili = analiz(kaynak)
rapor = []
toplam = hata + gecersiz
for ip, adet in toplam.most_common():
if adet < a.esik:
break
rapor.append({
"ip": ip,
"hatali_deneme": hata[ip],
"gecersiz_kullanici": gecersiz[ip],
"yerel_veya_ayrilmis": ipaddress.ip_address(ip).is_private,
"denenen_kullanicilar": sorted(kullanicilar[ip])[:10],
"basarili_giris": basarili.get(ip, []),
})
if a.json:
print(json.dumps(rapor, ensure_ascii=False, indent=2))
return
print(f"{'IP':<40}{'HATA':>6}{'GECERSIZ':>10} KULLANICILAR")
for r in rapor:
uyari = " <-- DIKKAT: basarili giris var!" if r["basarili_giris"] else ""
print(f"{r['ip']:<40}{r['hatali_deneme']:>6}{r['gecersiz_kullanici']:>10} {','.join(r['denenen_kullanicilar'])}{uyari}")
print(f"\nToplam {len(rapor)} şüpheli IP (eşik: {a.esik})")
if __name__ == "__main__":
main()Ne yapıyor, kısaca:
- Üç ayrı regex ile satırları sınıflandırıyor. IPv6 adresleri de yakalansın diye IP desenini [0-9a-fA-F:.] olarak geniş tuttum.
- "Failed" ve "Invalid user" sayılarını ayrı tutuyorum. Bunun bir sebebi var: sshd olmayan bir kullanıcı için çoğu zaman önce "Invalid user" sonra "Failed password for invalid user" yazar. Tek sayaçta toplarsanız aynı denemeyi iki kez saymış olursunuz.
- Eşik değeri (--esik) altında kalan IP'leri raporlamıyor; gürültü azalıyor.
- ipaddress modülü ile adresin yerel/ayrılmış bir blokta olup olmadığını işaretliyor. Kendi ağınızdan gelen hatalı denemeler genelde şifresini unutan bir kullanıcıdır, farklı değerlendirin.
- --json ile çıktıyı başka bir araca (SIEM, Telegram botu, cron maili) aktarabilirsiniz.
4) Çıktıyı Yorumlamak
Test için oluşturduğum örnek logda (dokümantasyon IP blokları kullanıldı) çıktı şöyle:
IP HATA GECERSIZ KULLANICILAR
203.0.113.45 25 0 admin,oracle,test
198.51.100.7 12 0 root <-- DIKKAT: basarili giris var!
192.0.2.9 0 3 pi
Toplam 3 şüpheli IP (eşik: 3)Burada asıl dikkat edilmesi gereken ikinci satır: 12 hatalı root denemesinin ardından aynı IP'den başarılı giriş var. Gerçek bir sunucuda bunu görürseniz yapılacaklar sırasıyla şunlar:
- O oturumu ve kullanıcıyı tespit edin: last -i, who ve journal kayıtları.
- Şifreyi değiştirin, authorized_keys dosyasına sizin eklemediğiniz bir anahtar eklenmiş mi bakın.
- crontab, systemd servisleri ve /tmp altında tanımadığınız dosya var mı kontrol edin.
- Kalıcı çözüm: parola ile girişi kapatın, sadece anahtar kullanın.
5) Otomatikleştirme ve Sonraki Adımlar
Ben scripti günde bir kez çalıştırıp JSON çıktıyı bir dosyaya ekliyorum. Basit bir cron satırı:
# her gün 07:00'de dünün özetini kaydet
0 7 * * * journalctl -u ssh --since yesterday --until today -o cat | /usr/bin/python3 /opt/ssh_tespit.py --json >> /var/log/ssh_ozet.jsonlTespit tek başına koruma değildir. Asıl koruma için sırasıyla:
- /etc/ssh/sshd_config içinde PasswordAuthentication no ve PermitRootLogin no (veya prohibit-password).
- Fail2ban gibi bir araçla tekrarlayan denemeleri otomatik engelleme.
- Mümkünse SSH'ı sadece VPN veya belirli IP'lerden erişilebilir yapmak.
Bu scripti geliştirmek isteyenlere fikir: saat bazlı dağılım (botlar genelde gece yoğunlaşıyor), ülke bilgisi için yerel bir GeoIP veritabanı, ya da sonuçları SQLite'a yazıp haftalık trend çıkarmak. Geliştiren olursa konunun altına eklesin, birlikte toparlarız.
Kaynaklar
- Python re modülü dokümantasyonu
- collections.Counter ve ipaddress modülü
- OpenSSH sshd_config kılavuz sayfası
- journalctl kılavuzu
Alıntı:Yasal not: Bu içerik savunma amaçlıdır. Scripti yalnızca yöneticisi olduğunuz veya yazılı izin aldığınız sistemlerin loglarında kullanın. Başkasına ait sistemlere izinsiz erişim TCK 243-245 kapsamında suçtur.
Kapak görseli: TurkTeam için hazırlanmış özgün görsel.