Dosya yükleme güvenliği
- Dosya yükleme, sunucu gerçek içerik türünü ve depolama yerini denetlemezse, saldırgana sunucuda kod çalıştırmaya kadar uzanan bir kapı açar.
- Bu derste uzantı sahteciliğini, sihirli bayt doğrulamasını ve çalıştırılamayan depoyu çözümlü örnekle öğrenirsin.
Puan bütün etkinlikleri cevaplayınca hesaba yazılır. Bu dersi tamamlayınca +55 XP alacaksın. Giriş yapmadan ilerlersen puan yalnız bu tarayıcıda durur.
Yükleme ucu, kullanıcıdan bir dosya alır ve onu diskte ya da nesne depoda saklar. Denetim yalnız dosya uzantısına bakıyorsa kolayca atlatılır: kurye hasar fotoğrafı yükleme ucu yalnız .jpg uzantısını kabul ediyor olsun. Saldırgan, içeriği çalıştırılabilir bir betik olan dosyayı çift uzantıyla ya da araya boş karakter sıkıştırarak adlandırıp yükler.
Bazı sunucular adın son noktasına değil, aradaki başka bir işarete göre karar verir. Dosya, web kökünün altına ve çalıştırılabilir bir dizine kaydedildiyse, saldırgan doğrudan o adrese giderek betiği tarayıcıdan tetikler. Buna web kabuğu (web shell) yüklemesi denir ve sunucuda kod çalıştırmaya kadar uzanır.
Doğrulama türleri: neye güvenilir
Dosya yükleme, modern uygulamalarda her gün kullanılan bir özelliktir: profil fotoğrafı, fatura eki, hasar kanıtı, destek talebine iliştirilen ekran görüntüsü. Her yükleme ucu, dışarıdan gelen ham baytları sunucunun diskine ya da deposuna yazar. Tehlike de buradan doğar.
Bir metin kutusuna girilen veriyi doğrulamak kolaydır; bir dosyanın gerçekte ne olduğunu anlamak çok daha zordur. Saldırgan bunu bilir ve dosyanın kılığıyla içeriğini birbirinden ayırır. Güvenli bir uç, dosyayı asla göründüğü gibi kabul etmez; her zaman gerçek içeriğini sorgular.
Bir yükleme ucu dört farklı sinyale bakabilir, ama hepsi aynı güveni hak etmez. Dosya adının uzantısı ve isteğin Content-Type başlığı, ikisi de istemciden gelir; saldırgan bir araçla ikisini de dilediği değere yazar. Bu iki sinyal güvenilmezdir. Dosyanın ilk baytlarındaki tür imzası (magic bytes) ise dosyanın kendi içeriğinden okunur ve sunucu tarafında doğrulanır.
Dördüncü ve en güçlü adım, dosyayı bir kütüphaneyle gerçekten açıp yeniden kaydetmektir; bu, içeriğin gerçekten iddia edilen tür olduğunu kanıtlar. Güvenli bir uç, kararını yalnız son iki sinyale dayandırır: baytların kendisine ve dosyanın yeniden işlenebilmesine.
Uzantı yine de tamamen işe yaramaz değildir. Kararı tek başına vermez ama ucuz bir ilk eleme olur. Burada da izin listesi kuralı geçerlidir.
Yasak listesi .php uzantısını engellese bile, aynı betik işleyicisine bağlı başka uzantılar ya da büyük harfli yazımlar arada kalır. İzin listesi ise yalnız .jpg, .jpeg ve .png gibi işin gerçekten istediği birkaç uzantıyı kabul eder. Karşılaştırmadan önce adı küçük harfe çevirir ve yalnız son uzantıya bakar. Böylece uzantı denetimi, imza ve yeniden kodlama gibi güvenilir sinyallerin önünde duran hızlı bir süzgece dönüşür.
Etkinlik 1 / 6
Yükleme ucuna gelen her sinyali, sunucunun neye güvenerek karar verdiğine göre ayır.
Önce bir seçeneğe, sonra aşağıdaki uygun kutuya bas. İstersen seçeneği kutuya sürükleyip bırak. Yerini değiştirmek için aynı işlemi tekrarla.
0/4 seçenek yerleşti.
Atlatma: çift uzantı ve gerçek tür
Uzantıya güvenen bir uç birkaç yolla atlatılır. Çift uzantının iki yüzü vardır. Adın içinde .jpg geçiyor mu diye bakan gevşek bir filtre foto.jpg.php adını geçirir, sunucu ise son uzantıya bakıp dosyayı betik sayar. Tersine foto.php.jpg adında, bazı sunucu yapılandırmaları adın herhangi bir yerindeki betik uzantısını görüp dosyayı betik işleyicisine verir.
Boş karakter hilesinde ad, foto.php%00.jpg gibi yazılır; bazı eski işleyiciler boş karakterden sonrasını yok sayar. Büyük-küçük harf oynaması bazı filtreleri şaşırtır. En sinsisi polyglot dosyadır: belge hem geçerli bir JPEG imzasıyla başlar hem de içine betik gömer, böylece imza kontrolünü bile geçebilir. Bu yüzden tek bir imza kontrolü değil, imza artı yeniden kodlama birlikte gerekir.
CWE bu sınıfı CWE-434, Sınırsız Tehlikeli Dosya Türü Yüklemesi olarak kaydeder. Kök neden, sunucunun dosyanın gerçek türünü değil, istemcinin bildirdiği ada ve Content-Type başlığına güvenmesidir. İkisi de saldırganın elindedir; isteğini bir araçla yeniden yazan biri her iki alanı da istediği gibi doldurur.
Gerçek tür, dosyanın ilk baytlarında (magic bytes) yazar: PNG dosyası her zaman aynı sekiz baytla başlar, gerçek bir JPEG farklı bir imzayla. Sunucu bu imzayı okumadan karar verirse, ad ve başlık tek başına hiçbir şey kanıtlamaz.
Sihirli baytlar her biçim için sabittir ve bir imza tablosu oluşturur. PNG dosyası 89 50 4E 47 baytlarıyla, GIF 47 49 46 ile başlar, PDF ise %PDF dizisiyle açılır. JPEG her zaman FF D8 FF ile başlar.
Sunucu bu ilk baytları okuyup beklenen imzayla karşılaştırırsa, dosya adının ne olduğu önemini yitirir. Ama tek başına imza kontrolü de son söz değildir; polyglot bir dosya geçerli imzayla başlayıp içine betik gömebilir. Bu yüzden imza kontrolünü, dosyayı yeniden kodlayan ikinci bir katmanla desteklemek gerekir. İki katman birlikte, hem yanlış türü hem gizlenmiş içeriği yakalar.
# file komutu dosyanın gerçek türünü ilk baytlardan okur $ file teslimat-kaniti.jpg teslimat-kaniti.jpg: PHP script, ASCII text # uzantı .jpg, içerik betik # Gerçek bir JPEG dosyasının ilk baytları her zaman ff d8 ff ile başlar $ xxd gercek-foto.jpg | head -1 00000000: ffd8 ffe0 0010 4a46 4946 0001 0101 0048 ......JFIF.....H # geçerli JPEG imzası
Etkinlik 2 / 6
Bir yükleme ucu dosya adının .jpg ile bitmesini şart koşuyor. Saldırgan dosyayı neden hâlâ betik olarak çalıştırabilir?
Tespit: web kabuğu izi
Web kabuğu, yüklenen bir dosyanın sunucuda komut çalıştıran bir arka kapıya dönüşmesidir. Saldırgan dosyayı yükler, sonra adresini tarayıcıdan açar ve dosya artık gelen parametreyi sunucuda komut gibi işler. ATT&CK bu tekniği T1505.003 Web Shell olarak sınıflar.
Başarılı bir web kabuğu yüklemesi loglarda kendini belli eder. Yükleme isteğinin hemen ardından, yüklenen dosyanın adresine gelen bir GET isteği görürsün; normal bir kullanıcı kendi yüklediği dosyayı hemen çağırıp parametre eklemez. Depolama alanında beklenmedik uzantılı dosyaların birikmesi de bir tarama girişiminin izidir.
Tespiti iki kaynağı birleştirerek kurarsın. Web erişim kaydı, yükleme isteğini ve hemen ardından gelen çağrıyı gösterir. Sunucu üstündeki uç nokta izleme aracı ise web sunucusu sürecinin bir kabuk ya da sistem komutu başlattığını kaydeder. Web sunucusu süreci normalde sh, cmd ya da powershell başlatmaz; bu üst süreç ile alt süreç ilişkisi, kabuğun kullanıldığı anın en güvenilir izidir.
Dosya bütünlüğü izleme de web kökünde yeni ya da değişmiş betik dosyalarını raporlar. Sık yapılan hata, yalnız yükleme anına bakıp sonrasını izlememektir. Tehlike yüklemede değil, dosyanın ilk çağrıldığı anda başlar; bu ikiliyi gören bir uyarı kuralı erken müdahale penceresi açar.
Etkinlik 3 / 6
Erişim kaydında web kabuğu yüklemesine ait satırları işaretle.
Şüpheli satırlara tıkla. Birden fazla olabilir.
Savunma: imza, yeniden kodlama, izin listesi
Savunmanın ilk katmanı içerik doğrulamadır. Sunucu, dosyanın ilk baytlarını okuyup beklenen imzayla karşılaştırır; uyuşmuyorsa dosya reddedilir.
Görsel yüklemede daha güçlü bir adım, dosyayı bir görsel kütüphanesiyle gerçekten açıp yeniden kodlamaktır. Bu işlem, resmin içine gizlenmiş her ek veriyi ve gömülü betiği siler, çünkü kütüphane yalnız piksel verisini okur ve yeni, temiz bir dosya yazar. Dosya türü izin listesi de burada devreye girer: yalnız iş için gerekli birkaç tür kabul edilir, geri kalan her şey reddedilir.
// Güvensiz: yalnız uzantıya bakılıyor
if (dosyaAdi.endsWith(".jpg")) kaydet(dosyaAdi, icerik);
// Güvenli: gerçek tür imzadan okunuyor, dosya yeniden kodlanıyor
const tur = dosyaTuruTespitEt(icerik);
if (tur !== "image/jpeg") return res.status(400).end();
const temiz = gorselKutuphanesi.yenidenKodla(icerik);
kaydet(rastgeleAd(), temiz);Doğrulamanın yanına birkaç sınır daha koyarsın. Yükleme boyutuna bir tavan koyarsın; sıkıştırılmış arşivlerde açılmış boyutu da hesaba katarsın. Dosya adının uzunluğunu ve kullanılabilecek karakterleri sınırlarsın. Dosyayı geri sunarken Content-Disposition: attachment başlığıyla tarayıcıyı indirmeye yönlendirirsin.
X-Content-Type-Options: nosniff başlığı da tarayıcının içeriği tahminle HTML ya da betik saymasını önler. Bu sınırların hiçbiri tek başına yetmez. Ama doğrulamadan kaçan bir dosyanın etkisini küçültür ve katmanlar birbirini tamamlar.
Bir yükleme savunmasını sınarken birkaç dosya denersin. Adı .jpg ama içeriği betik olan bir dosya reddedilmeli. Geçerli bir görsel kabul edilip yeniden kodlanmış hâliyle saklanmalı.
Çok büyük bir dosya boyut sınırında durmalı. Bu sınamaları otomatik testlere koyarsan, ileride biri kontrolü gevşettiğinde hemen fark edilir. Savunmayı yazmak kadar, çalıştığını sürekli doğrulamak da işin parçasıdır.
Etkinlik 4 / 6
Yukarıdaki güvenli sürüme, içeriği bir betik olan ama adı foto.jpg olan bir dosya gönderilirse ne olur?
Önce kodu oku, sonra çıktıyı seç.
const tur = dosyaTuruTespitEt(icerik); if (tur !== "image/jpeg") return res.status(400).end();
Güvenli karar bu yüzden hep aynı yerden gelir: sunucunun kendi okuduğu baytlardan, istemcinin söylediğinden değil. Başlık ve ad birer iddiadır; bayt ise kanıttır.
Etkinlik 5 / 6
Bir yükleme ucu Content-Type başlığının image/jpeg olup olmadığına bakarak karar veriyor. Bu denetimin asıl zayıflığı nedir?
Depolama ve ad: zararı sınırlamak
İçerik doğrulaması atlatılsa bile ikinci katman zararı sınırlar: depolama yeri. Yüklenen dosyalar, web sunucusunun kod çalıştırdığı dizinin dışında, mümkünse tamamen ayrı bir nesne depoda (object storage) tutulur. Bu depo dosyayı yalnız veri olarak sunar, hiçbir betik motorunu çalıştırmaz. Dosya adı da kullanıcının verdiği adla değil, sunucunun ürettiği rastgele bir kimlikle kaydedilir; böylece adın içine gizlenmiş yol karakterleri ya da ikinci bir uzantı hiçbir anlam taşımaz.
Gerçek bir mimaride bu ayrım bir adım daha ileri gider. OWASP, yüklenen dosyaları uygulamadan ayrı bir sunucuda tutmayı ilk sıraya koyar. Dosyaları istemciye ana siteden değil, ayrı bir alan adından ve sınırlı yetkiyle sunarsın. Böylece yüklenen bir SVG ya da HTML dosyası tarayıcıda açılsa bile, ana sitenin çerezlerine ve oturumuna ulaşamaz.
Saldırgan içine betik gömülü bir görsel yüklese bile, o dosya sunucuda kod olarak işlenmez. Depolama kararı, içerik doğrulaması atlatıldığında devreye giren son emniyet kemeridir.
Derinleş:Polyglot dosyalar neden tek imza kontrolünü aşar
Polyglot, aynı anda iki biçimin kurallarına uyan bir dosyadır. Saldırgan bir dosyayı hem geçerli bir JPEG imzasıyla başlatır hem de içine çalışabilir bir betik gömer.
İlk baytlara bakan basit bir imza kontrolü dosyayı geçerli görsel sayar, çünkü başlangıç gerçekten JPEG'dir. Oysa betik işleyici, dosyanın ilerleyen baytlarını çalıştırabilir. Bu yüzden imza kontrolü tek başına yetmez.
Dosyayı bir görsel kütüphanesiyle açıp yeniden kodlamak, polyglot'u etkisiz kılar: kütüphane yalnız piksel verisini okur, gömülü betiği değil; ürettiği yeni dosya sadece görseldir. Çalıştırılamayan bir depoda saklamak da bu dosyanın hiçbir koşulda betik olarak işlenmemesini garanti eder. İki savunma birlikte, polyglot'un hem içeriğini temizler hem de çalışma zeminini elinden alır.
Aynı ilkeler kurye uygulamasının teslimat kanıtı fotoğrafında da geçerlidir. Uygulama, imza doğrulaması ve yeniden kodlamayı atlayıp dosyayı doğrudan istemcinin adıyla web sunucusunun altına kaydediyorsa, teslimat kanıtı özelliği de aynı web kabuğu riskini taşır.
Teslimat kanıtı ucunu denetlerken üç soru sorarsın: dosya gerçekten iddia ettiği tür mü, içine bir şey gizlenmiş mi, en kötü ihtimalde nerede saklanıyor? İmza kontrolü ilk soruyu, yeniden kodlama ikincisini, çalıştırılamayan depo ve rastgele ad üçüncüsünü yanıtlar. Üçü birlikte, yüklemeyi bir kod çalıştırma kapısı olmaktan çıkarır.
Etkinlik 6 / 6
Yüklenen dosyanın adı kullanıcıdan gelen adla değil, sunucunun ürettiği ____ bir kimlikle kaydedilirse, ad içine gizlenmiş ikinci uzantı ya da yol karakteri anlamını yitirir.
Okumayı istediğin sırayla sürdürebilirsin. Ders, bütün etkinlikleri cevapladığında tamamlanır.