AttackProbes ve ThreatScore: .NET 10'da Saldırı Probe'larını Puanlamak

0 yorum 440 görüntülenme

Siber Güvenlik dotnet ASP.NET Core SQL Server Middleware Telemetri Güvenlik

11 dk okuma 2046 kelime

TL;DR

Origin classifier'ımız her saldırı denemesini dbo.AttackProbes tablosuna yazıyor; ThreatScore PERSISTED computed kolonu Severity × Confidence / 20 formülüyle 0-25 arası bir puan üretiyor. 49 path pattern, 13 tarama aracı User-Agent imzası ve 12 query string pattern üç ayrı static registry'de tutuluyor. Puanı 11 eşiğini aşan probe, IP'yi otomatik bloklatıyor; aşmayanlar için 15 dakikalık burst sayacı devreye giriyor. Tam şema ve sınıflandırma kodu — canlı sistemden.

Yazar notu: Bu yazıdaki şema, kod ve eşik değerleri bilalkose.com.tr canlı sisteminden alındı. AttackProbes tablosu + üç pattern registry'si + RecordAttackAsync sınıflandırma akışı 2026-05 itibarıyla prod'da çalışıyor. Pillar 2 H2-3'te AttackProbes ve ThreatScore'u yüzeysel geçmiştik; bu yazıda tablo şeması, puanlama formülünün gerekçesi ve üç katmanlı pattern eşleştirme detaylı.


İçindekiler

  1. AttackProbes tablosu: 23 kolon + PERSISTED ThreatScore
  2. ThreatScore formülü: neden Severity × Confidence / 20
  3. Path tabanlı tespit: 49 pattern, 14 kategori
  4. User-Agent tabanlı tespit: 13 tarama aracı imzası
  5. Query string pattern: SQLi, XSS, path traversal, Log4Shell
  6. Sınıflandırma kodu: RecordAttackAsync + dedup
  7. Sıkça Sorulan Sorular
  8. İlgili Yazılar

ThreatScore ≥ 11 ThreatScore < 11 Incoming Request Path Registry /wp-login, /.env, /.git User-Agent Registry sqlmap, nikto, scanners Query Registry SQLi / XSS signatures Match produces category + severity (1–5) + confidence (1–100) AttackProbes — INSERT row DB computes ThreatScore IpBlocklist IP auto-blocked at the edge Burst Counter reported, watched for repeats AttackProbes → ThreatScore: every request scored, only real threats blocked
AttackProbes ThreatScore boru hattı: gelen istek üç registry'den geçer (path, User-Agent, query string) → eşleşme kategori + severity + confidence üretir → AttackProbes INSERT → DB ThreatScore hesaplar → eşik 11 aşılırsa IpBlocklist, aşılmazsa burst sayacı


AttackProbes Tablosu: 23 Kolon + PERSISTED ThreatScore

Pillar 2'de "saldırı denemelerini ayrı bir tabloda topluyoruz" demiştik. O tablo dbo.AttackProbes — her satır bir saldırı probe'u, request bağlamı ve tehdit puanıyla birlikte:

CREATE TABLE dbo.AttackProbes (
    Id              BIGINT          IDENTITY(1,1) NOT NULL,
    HitTime         DATETIME2(0)    NOT NULL CONSTRAINT DF_AttackProbes_HitTime DEFAULT SYSUTCDATETIME(),
    DayIso          CHAR(10)        NOT NULL,
    Host            NVARCHAR(80)    NULL,
    Source          VARCHAR(20)     NOT NULL,
    RawIp           NVARCHAR(45)    NOT NULL,
    Country         CHAR(2)         NULL,
    City            NVARCHAR(80)    NULL,
    Asn             NVARCHAR(120)   NULL,
    AsnNumber       INT             NULL,
    Method          VARCHAR(10)     NOT NULL,
    Path            NVARCHAR(512)   NOT NULL,
    QueryString     NVARCHAR(1024)  NULL,
    UserAgent       NVARCHAR(512)   NULL,
    Referer         NVARCHAR(512)   NULL,
    StatusCode      SMALLINT        NULL,
    Category        VARCHAR(40)     NOT NULL,
    Severity        TINYINT         NOT NULL,
    Confidence      TINYINT         NOT NULL,
    ThreatScore     AS (CAST(Severity AS INT) * Confidence / 20) PERSISTED,
    PatternMatched  NVARCHAR(120)   NULL,
    CloudflareRayId VARCHAR(20)     NULL,
    CONSTRAINT PK_AttackProbes PRIMARY KEY CLUSTERED (Id),
    CONSTRAINT CK_AttackProbes_Severity   CHECK (Severity   BETWEEN 1 AND 5),
    CONSTRAINT CK_AttackProbes_Confidence CHECK (Confidence BETWEEN 1 AND 100)
);

Üç tasarım kararı dikkat çeker. Birincisi ThreatScore bir PERSISTED computed columnSeverity × Confidence / 20 her INSERT'te bir kez hesaplanır, diske yazılır, sorguda yeniden hesaplanmaz. Index'lenebilir; uygulama kodu bu kolonu asla elle set etmez, edemez.

İkincisi CHECK constraint'leri: Severity 1-5, Confidence 1-100 aralığında olmak zorunda. Sınıflandırma kodunda bir bug bu aralığı bozarsa INSERT veritabanı seviyesinde reddedilir — bozuk bir puan tabloya hiç giremez.

Üçüncüsü DayIso CHAR(10)"2026-05-12" formatında bir gün damgası. Günlük raporlama WHERE DayIso = @today ile, HitTime üzerinde tarih fonksiyonu çağırmadan, index dostu çalışır.

Tablo dört non-clustered index taşır: DayIso + Severity (günlük rapor), RawIp + HitTime (tek IP'nin geçmişi), Category + DayIso (kategori dağılımı), ThreatScore + HitTime (en tehlikeli probe'lar). Hepsi INCLUDE ile kapsayıcı — admin dashboard sorguları clustered index'e key lookup yapmaz.


ThreatScore Formülü: Neden Severity × Confidence / 20

Puan iki girdiden türer:

  • Severity (1-5) — saldırının teknik ciddiyeti. /.env dosyası sızdırma denemesi 5; /api-docs taraması 2.
  • Confidence (1-100) — pattern eşleşmesinin ne kadar kesin bir saldırı olduğu. /.git/ neredeyse her zaman saldırıdır (95); /console meşru bir sayfa da olabilir (70).

İkisini ayrı tutmak önemli. Yüksek ciddiyetli ama düşük güvenli bir sinyali (meşru olabilecek bir admin path'i) ile düşük ciddiyetli ama kesin bir sinyali aynı kovaya atmak istemiyoruz. Çarpım ikisini birleştirir, /20 bölme sonucu 0-25 aralığına oturtur.

Path Kategori Severity Confidence ThreatScore
/.env secret_disclosure 5 95 23
/xmlrpc.php wordpress_probe 4 90 18
/wp-admin wordpress_probe 3 85 12
/console admin_panel_probe 3 70 10
/api-docs api_doc_probe 2 65 6

Otomatik blok eşiği 11. /wp-admin (12) eşiği geçer, /console (10) geçmez. Burada bir integer-division inceliği var: 3 × 70 / 20 matematiksel olarak 10.5, ama SQL tamsayı aritmetiğinde 10. Eşiği 11 seçmek bu kesirli sınır bölgesini bilinçli olarak blok-dışı bırakır — tek bir /console isteği IP'yi bloklamaz. Eşiği 12'ye koysaydık 3 × 80 / 20 = 12 gibi gerçek saldırı kalıpları sınırda kaçabilirdi; 11 doğru kesme noktası. Eşiği geçemeyen probe'lar yok sayılmaz — 15 dakikalık burst sayacına düşer (cluster C3 konusu).

ThreatScore = (Severity × Confidence) / 20 integer division — result clamped to the 0–25 range report only auto-block BLOCK THRESHOLD = 11 0 5 10 15 20 25 10–11 border zone — the integer-division edge Worked examples Severity 5 × Confidence 80 = 400 → 400 / 20 = 20BLOCK Severity 3 × Confidence 70 = 210 → 210 / 20 = 10report only (border) Severity 2 × Confidence 50 = 100 → 100 / 20 = 5report only
ThreatScore ölçeği: Severity 1-5 ekseni × Confidence 1-100 ekseni, çarpım /20 ile 0-25 aralığına iner; 11 eşiğinin altı yalnız raporlanır, üstü otomatik blok; sınır bölgesi 10-11 integer division ile vurgulanır


Path Tabanlı Tespit: 49 Pattern, 14 Kategori

Tespitin ilk hattı, istenen path'i bilinen saldırı kalıplarına karşı eşlemek. Registry middleware'de static bir array — yani uygulama ömrü boyunca tek allocation, her istekte yeniden kurulmaz:

private static readonly (string Pattern, string Category, byte Severity, byte Confidence)[] AttackPaths =
[
    // Sır / dosya açığa çıkarma — kritik
    ("/.env",        "secret_disclosure", 5, 95),
    ("/.aws",        "cloud_creds_probe", 5, 95),
    ("/.ssh",        "secret_disclosure", 5, 95),
    ("/.htpasswd",   "secret_disclosure", 5, 90),
    // VCS açığa çıkarma
    ("/.git/",       "vcs_disclosure",    4, 95),
    ("/.svn/",       "vcs_disclosure",    4, 90),
    // WordPress probe
    ("/wp-admin",    "wordpress_probe",   3, 85),
    ("/wp-login",    "wordpress_probe",   3, 85),
    ("/xmlrpc.php",  "wordpress_probe",   4, 90),
    // PhpMyAdmin / Exchange
    ("/phpmyadmin",  "phpmyadmin_probe",  4, 90),
    ("/owa/",        "exchange_probe",    4, 85),
    ("/autodiscover","exchange_probe",    4, 85),
    // RCE — en yüksek ciddiyet
    ("/cmd.exe",     "rce_pattern",       5, 95),
    // ... toplam 49 pattern, 14 kategori
];

Kategoriler 14 aileye ayrılır: secret_disclosure, cloud_creds_probe, vcs_disclosure, wordpress_probe, phpmyadmin_probe, admin_panel_probe, exchange_probe, backup_probe, vendor_probe, rce_pattern, cgi_probe, oidc_probe, actuator_probe, api_doc_probe. Her aile aynı tip varlığı hedefler — wordpress_probe, WordPress hiç kurmadığımız halde gelen /wp-admin, /wp-login, /xmlrpc.php isteklerini; exchange_probe, ortada bir Exchange sunucusu sanan /owa/ ve /autodiscover taramalarını toplar.

Confidence değerlerinin neden farklı olduğu kritik. /.git/ 95 puan alır çünkü meşru bir tarayıcı bu path'i hiç istemez; /wp-admin 85 çünkü teorik olarak yanlış bir linkten gelinebilir; /console ise yalnızca 70 çünkü gerçekten bir konsol sayfası olabilir. Confidence, false-positive maliyetini formülün içine gömer — şüphe payı yüksek olan path daha düşük puan üretir.


User-Agent Tabanlı Tespit: 13 Tarama Aracı İmzası

Path temiz olsa bile User-Agent saldırganı ele verebilir. İkinci registry, tarama araçlarının UA imzalarını tutar:

private static readonly (string UaPattern, string Category, byte Severity, byte Confidence)[] AttackTools =
[
    ("sqlmap",    "tool_sqli_scan",   5, 98),
    ("nuclei",    "tool_nuclei_scan", 4, 95),
    ("nmap",      "tool_port_scan",   3, 90),
    ("masscan",   "tool_port_scan",   3, 90),
    ("nikto",     "tool_dir_brute",   4, 95),
    ("dirbuster", "tool_dir_brute",   4, 95),
    ("gobuster",  "tool_dir_brute",   4, 92),
    ("ffuf",      "tool_dir_brute",   4, 90),
    ("wpscan",    "tool_wp_scan",     3, 88),
    ("censys",    "tool_recon",       2, 75),
    ("shodan",    "tool_recon",       2, 75),
    // ... toplam 13 araç
];

sqlmap 98 confidence ile listenin en kesini — kimse bir SQL injection aracının adını User-Agent'ına kaza eseri yazmaz. nikto, dirbuster, gobuster, ffuf dizin brute-force araçları, hepsi 90+ confidence. Diğer uçta censys ve shodan: sadece 75 confidence ve severity 2. Bunlar internet-geneli envanter tarama servisleri — doğrudan saldırı değil keşif. Düşük puanla işaretlenir ama tek başına blok tetiklemezler.

Burada bir incelik var: UA imzası contains mantığıyla aranır ve karşılaştırma kasıtlı olarak küçük harfe indirilir. B1'de Cloudflare WAF DSL'inin contains operatörünün case-sensitive olduğu tuzağını anlatmıştık — Sqlmap yazan bir UA'nın sqlmap imzasını kaçırmaması için origin tarafında aynı hataya düşmüyoruz.


Query String Pattern: SQLi, XSS, Path Traversal, Log4Shell

Üçüncü registry payload'a bakar — query string'in içindeki saldırı imzaları:

private static readonly (string Pattern, string Category, byte Severity, byte Confidence)[] QueryPatterns =
[
    ("union select", "sqli_pattern",   5, 92),
    ("' or '1'='1",  "sqli_pattern",   5, 95),
    ("'; drop ",     "sqli_pattern",   5, 98),
    ("../../",       "path_traversal", 5, 92),
    ("/etc/passwd",  "path_traversal", 5, 98),
    ("<script",      "xss_in_url",     4, 88),
    ("javascript:",  "xss_in_url",     4, 80),
    ("${jndi:",      "log4shell",      5, 99),
    ("system(",      "rce_pattern",    5, 90),
    ("eval(",        "rce_pattern",    5, 88),
    // ... toplam 12 pattern
];

${jndi: 99 confidence ile tüm tablonun en yüksek puanı — 2021 Log4Shell imzası, meşru hiçbir trafik bu string'i taşımaz. SQLi pattern'leri (union select, ' or '1'='1, '; drop ) klasik injection denemeleri; ../../ ve /etc/passwd directory traversal kalıpları. Hepsi severity 4-5 alır, çünkü payload seviyesinde saldırı niyeti açıkça görünür — path tahmininden farklı olarak burada şüphe payı yoktur.

Üç registry birlikte katmanlı bir filtre kurar: path temizse User-Agent'a, UA da temizse query string'e bakılır. Bir istek üç katmandan da temiz geçerse meşru sayılır ve PageHits tablosuna (cluster C1 konusu) yazılır — saldırı değil, ziyaretçi.


Sınıflandırma Kodu: RecordAttackAsync + Dedup

Üç registry bir eşleşme verdiğinde RecordAttackAsync devreye girer. İlk işi dedup:

private async Task RecordAttackAsync(HttpContext ctx, string ip, Classification c)
{
    var path = ctx.Request.Path.Value ?? "/";
    var category = c.Category ?? "unknown";

    // 5 dakikada aynı IP+path+category bir kez yazılır
    // (saldırı tarayıcılarında binlerce DB satırı oluşmasını engeller)
    var dedupKey = $"{AttackDedupPrefix}{ip}:{path}:{category}";
    if (_cache.TryGetValue(dedupKey, out _)) return;
    _cache.Set(dedupKey, true, AttackDedupTtl);
    // ... request bağlamı toplanır: GeoIP ülke/şehir/ASN, UA, referer, CF-Ray
    // ... parametreli INSERT INTO dbo.AttackProbes (...)

Bir tarama aracı saniyede onlarca path dener; dedup olmasa tek bir saldırgan dakikada binlerce satır üretirdi. IP + path + category üçlüsü 5 dakika cache'lenir — aynı kombinasyon penceresi içinde yalnız tek satır yazılır. Dedup'tan sonra request bağlamı toplanır (GeoIP'den ülke/şehir/ASN, User-Agent, referer, Cloudflare'in CF-Ray header'ı) ve satır parametreli bir sorguyla INSERT edilir. Category, Severity, Confidence sınıflandırmadan gelir; ThreatScore'u veritabanı hesaplar.

INSERT'ten sonra otomatik blok kararı verilir:

    // Auto-block — ThreatScore eşiği aşıldıysa IpBlocklist'e ekle
    var threatScore = c.Severity * c.Confidence / 20;
    if (threatScore >= AutoBlockThreshold)
    {
        await UpsertBlocklistAsync(ip, threatScore, category, c.PatternMatched);
        return;
    }

    // Burst detector — düşük skorlu probe'lar bile 15 dk'da 3+ olursa otomatik block
    var burstCount = await IncrementBurstCounterAsync(ip, category);
    if (burstCount >= AttackBurstThreshold)
    {
        await UpsertBlocklistAsync(ip, BurstFakeThreatScore, "attack_burst",
            $"{burstCount} probes/{(int)BurstWindow.TotalMinutes}min, last={category}");
        await ResetBurstCounterAsync(ip);
    }
}

İki yol vardır. ThreatScore tek başına 11'i aşarsa IP doğrudan bloklanır. Aşmazsa burst sayacı artar — düşük puanlı ama ısrarlı bir tarayıcı 15 dakikada üç probe yaparsa, tek tek hiçbiri eşiği geçmese bile toplu davranış bloklatır. UpsertBlocklistAsync IP'yi dbo.IpBlocklist'e yazar ve B2 cluster'ında anlattığımız fire-and-forget Cloudflare push'unu tetikler — yani tespit origin'de, blok edge'de gerçekleşir. Buradaki threatScore hesabı kodda tekrar görünür; bu yalnızca blok kararını INSERT tamamlanmadan verebilmek içindir — tabloya yazılan değer her zaman DB'nin PERSISTED hesabıdır.


Sıkça Sorulan Sorular

ThreatScore neden uygulama kodunda değil veritabanında hesaplanıyor?

PERSISTED computed column, formülü tek bir yere — şema tanımına — sabitler. İster middleware'in canlı INSERT'i, ister bir ETL backfill, ister elle bir kayıt; hangi yoldan girerse girsin ThreatScore aynı formülle üretilir. Kod tarafında ayrıca c.Severity * c.Confidence / 20 hesabını görürsün, ama bu yalnızca blok kararını INSERT'ten önce verebilmek içindir.

Severity 1-5, Confidence 1-100 — neden farklı aralıklar?

Severity kaba bir sınıf; beş kademe yeter (bilgi sızdırma mı, RCE mi). Confidence ince ayar ister — /wp-admin ile /wp-login ikisi de 85, ama /console 70 farkı önemli. 100'lük ölçek bu nüansı taşıyabilir, 5'lik ölçek taşıyamazdı. /20 böleni ikisini ortak 0-25 ölçeğine indirir.

49 path pattern saldırıların hepsini yakalar mı?

Hayır — yakalaması amaç da değil. Registry, en sık görülen otomatik tarama kalıplarını hedefler; gerçek prod'da gelen probe'ların büyük çoğunluğu bu listede. Hedefli, elle yapılan bir saldırı pattern'e uymayabilir. Onun için ThreatScore'a ek olarak burst sayacı ve (Pillar 2'de anlatılan) IsSuspectNetwork katmanı var — tek registry değil, katmanlı savunma.

Censys ve Shodan gibi tarayıcılar neden bloklanmıyor?

Severity 2, Confidence 75 → ThreatScore 7, yani 11 eşiğinin altında. Bunlar internet-geneli envanter servisleri; doğrudan bir saldırı değil keşif. İşaretlenir ve raporlanır ama tek başına blok tetiklemezler. Israrla tararlarsa 15 dakikalık burst sayacı yine de devreye girer.


İlgili Yazılar

Yorumlar (0)

Yorum ve puan bırakın

Henüz yorum yapılmamış. İlk yorumu sen bırak.