Bölüm 19
File Upload Security
Bölüm 4 (beyaz liste), Bölüm 8 (ImageTragick — dolaylı işleme), Bölüm 14 (yetkilendirme). İleri referanslar: Bölüm 20 (path traversal), Bölüm 27 (sunucu sertleştirme), Bölüm 34 (web shell).
Dosya yükleme açığını, "saldırgan çalıştırılabilir içerik yükleyebiliyor ve o dosya çalıştırılabiliyor" koşulunun birleşimi olarak kavrayacak; MIME/uzantı doğrulamasının neden yetersiz olduğunu; ve savunmanın bu iki koşuldan birini kırmak olduğunu göreceksiniz.
1. Giriş
Dosya yükleme, web uygulamalarının en yaygın ve en tehlikeli özelliklerinden biridir. Profil fotoğrafı, belge, ek dosya, içe aktarma — neredeyse her uygulama kullanıcıdan dosya kabul eder. Ama bir dosya, sıradan bir metin girdisinden temelde farklıdır: yalnızca veri değil, potansiyel olarak kod taşır. Ve bu kod, yanlış koşullarda sunucuda çalışırsa, sonuç doğrudan uzaktan kod çalıştırmadır (RCE) — saldırganın en yüksek ödülü.
Dosya yükleme açıkları, PHP dünyasında özellikle yıkıcıdır çünkü PHP, web sunucusu tarafından çalıştırılabilir bir dildir. Bir saldırgan .php uzantılı bir dosyayı web'den erişilebilir ve PHP'nin çalıştığı bir dizine yükleyebilirse, o dosyaya erişerek keyfi kod çalıştırır — buna web shell denir (Bölüm 34). 2020'de WordPress File Manager eklentisindeki açık (CVE-2020-25213, Bölüm 11), tam bu senaryoyla 700.000'den fazla siteyi etkiledi ve kitlesel olarak web shell + cryptominer kurulumuna yol açtı.
Bu bölümün merkezî içgörüsü şudur: dosya yükleme RCE'si iki koşulun birleşimidir — (1) saldırgan çalıştırılabilir içerik yükleyebilmeli ve (2) o dosya çalıştırılabilmeli. Savunma, bu iki koşuldan herhangi birini kırmaktır. En sağlam savunma ikisini birden kırar (derinlemesine savunma, Bölüm 1).
2. Temel Teori
İki RCE koşulu: 1. Çalıştırılabilir içerik yüklenebilir. Saldırgan, PHP kodu içeren bir dosyayı sunucuya koyabilir (yükleme doğrulaması yetersizse). 2. Yüklenen dosya çalıştırılabilir. Dosya, web'den erişilebilir ve PHP'nin işlediği bir dizinde durur (depolama/sunucu yapılandırması yanlışsa).
İkisi birden gerekir. Bir saldırgan .php yükleyebilse ama dosya web-dışı bir dizinde saklansa (veya o dizinde PHP çalıştırılmasa), RCE olmaz. Tersine, çalıştırma açık ama yalnızca güvenli dosyalar yüklenebiliyorsa, yine RCE olmaz. Bu yüzden savunma iki cepheden yürütülür.
Doğrulamanın klasik hataları — neden yetersizler:
| Doğrulama | Neden yetersiz |
|---|---|
İstemci MIME tipi ($_FILES['type']) |
İstemci kontrollü; saldırgan image/jpeg der, .php gönderir |
| Yalnızca uzantı kara listesi | .phtml, .php5, .phar, .pht; büyük/küçük harf (.PhP); atlatılır |
| Çift uzantı gözden kaçırma | shell.php.jpg bazı yapılandırmalarda .php olarak çalışır |
| Sadece uzantı beyaz listesi | İçerik kontrol edilmezse polyglot dosya (geçerli JPEG + geçerli PHP) geçer |
| İçerik (magic bytes) tek başına | Polyglot ile atlatılabilir; ayrıca .htaccess yüklemesi çalıştırmayı açabilir |
Kök neden. Tanıdık kalıp: güvenilmeyen veri (dosya), onu çalıştırabilecek bir bağlama (web-erişilebilir PHP dizini) doğrulama/izolasyon olmadan yerleştiriliyor. Fark, "yorumlayıcının" bu kez web sunucusu + PHP motoru olmasıdır.
Dolaylı riskler. Dosya yükleme yalnızca web shell değildir. Yüklenen dosya bir kütüphaneyle işlenirse (ImageMagick, Bölüm 8), o kütüphanenin açıkları (ImageTragick) tetiklenir. Ayrıca dosya adı path traversal (Bölüm 20), aşırı boyut DoS, ve XXE (Bölüm 10 — .svg/.docx içi XML) taşıyabilir. Dosya yükleme, birçok açık sınıfının kavşağıdır.
3. Mimarisel Bakış
GÜVENSİZ (iki koşul da sağlanıyor):
Saldırgan → shell.php yükler
↓ doğrulama: istemci MIME'ına güvenir → geçer (Koşul 1 ✓)
Dosya → /var/www/html/uploads/shell.php (web-erişilebilir, PHP çalışır)
↓ (Koşul 2 ✓)
Saldırgan → GET /uploads/shell.php?cmd=... → PHP çalışır → RCE (web shell)
GÜVENLİ — Koşul 1 kırılır (içerik doğrulama):
Yükleme → finfo ile GERÇEK tür kontrol + görüntü yeniden kodlanır
→ PHP içeriği taşıyan dosya reddedilir/temizlenir
GÜVENLİ — Koşul 2 kırılır (web-dışı depolama / yürütme kapalı):
Dosya → /var/app/private/uploads/ (web-dışı) → doğrudan erişilemez
Servis → kontrollü bir PHP betiğiyle, Content-Disposition ile indirtilir
VEYA web-içiyse: o dizinde PHP yürütme KAPALI (Böl. 27)
→ dosya çalışamaz → RCE yok
Kritik gözlem: iki koşuldan birini kırmak RCE'yi engeller; ikisini birden kırmak derinlemesine savunmadır. En güvenilir tek karar, dosyaları web kök dizini dışında saklamaktır.
4. Güvensiz Kod
Gerçekçi bir "profil fotoğrafı / belge yükleme" akışı:
<?php
// ⚠️ GÜVENSİZ — çoklu dosya yükleme hatası
declare(strict_types=1);
$file = $_FILES['avatar'];
// 1) İstemci MIME tipine güven
if ($file['type'] !== 'image/jpeg') { // $_FILES['type'] İSTEMCİDEN
exit('Sadece JPEG');
}
// 2) Yalnızca kara liste (atlatılabilir)
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
if ($ext === 'php') { // .phtml/.php5/.PhP geçer
exit('PHP yasak');
}
// 3) Orijinal adla, WEB-ERİŞİLEBİLİR dizine kaydet
$dest = __DIR__ . '/uploads/' . $file['name']; // path traversal + çalıştırılabilir
move_uploaded_file($file['tmp_name'], $dest);
echo 'Yüklendi: /uploads/' . $file['name']; // doğrudan erişilebilir
Ayrıca yükleme uç noktasında kimlik doğrulama/yetki kontrolü ve boyut sınırı da yok.
5. Açığın Analizi
$file['type'] !== 'image/jpeg' — İstemci MIME'ına güven. $_FILES['type'] değeri, tarayıcının (yani istemcinin) gönderdiği Content-Type'tan gelir; saldırgan bunu istediği gibi ayarlar. .php dosyasını image/jpeg MIME'ıyla gönderir, kontrol geçer. MIME tipi asla güvenilmez; içerik sunucuda (finfo) belirlenmeli.
if ($ext === 'php') (kara liste) — Kara listenin klasik kusuru (Bölüm 4). Yalnızca php'yi engelliyor; ama PHP birçok uzantıdan çalışabilir: .phtml, .php3/4/5/7, .phar, .pht, ve sunucu yapılandırmasına göre büyük/küçük harf (.PhP). Ayrıca .htaccess yüklemek, o dizinde çalıştırmayı açabilir. Beyaz liste (yalnızca izinli uzantılar) gerekir.
.../uploads/' . $file['name'] — İki hata: (1) Orijinal dosya adı kullanılıyor → path traversal (../, Bölüm 20) ve üzerine yazma; (2) Web-erişilebilir dizin → yüklenen dosya doğrudan URL'den çağrılabilir. Bu ikisi, "çalıştırılabilirlik" koşulunu (Koşul 2) sağlar.
Eksik kontroller — Yetki (yükleyen kim?), boyut (DoS), içerik doğrulama (polyglot) yok.
Kök sorun: doğrulama istemciye güveniyor ve kara liste temelli (Koşul 1 kırılmıyor); dosya web-erişilebilir ve çalıştırılabilir dizinde orijinal adla saklanıyor (Koşul 2 kırılmıyor). İki koşul da açık → RCE.
6. Hacker Bakış Açısı
Saldırgan dosya yüklemeyi nasıl istismar eder?
Doğrulamayı haritalar. Hangi uzantıların/MIME'ların kabul edildiğini test eder: .php, .phtml, .php5, .phar, büyük/küçük harf, çift uzantı (x.php.jpg), sondaki nokta/boşluk (x.php.), null bayt (eski PHP). MIME kontrolü varsa, .php içeriğini image/jpeg MIME'ıyla gönderir.
Polyglot dener. İçerik (magic bytes) kontrolü varsa, hem geçerli bir görüntü hem geçerli PHP olan bir polyglot dosya oluşturur (ör. bir GIF/JPEG başlığı + gömülü PHP). Uzantı beyaz listesi yoksa, .jpg olarak yükleyip sonra çalıştırmayı dener.
Çalıştırılabilirliği yoklar. Yüklenen dosyanın nereye gittiğini ve doğrudan erişilebilir olup olmadığını arar. Yanıtta dosya yolu dönüyorsa (bizim güvensiz kodda olduğu gibi), doğrudan çağırır. Web-dışı depolanıyorsa, path traversal ile web-içine çıkmayı ya da servis betiğini istismar etmeyi dener.
.htaccess/config yükler. Uzantı beyaz listesi .php'yi engelliyorsa, saldırgan bir .htaccess (Apache) yükleyerek .jpg'yi PHP olarak yürütmeyi açmayı dener — yani çalıştırma koşulunu kendisi kurar.
Dolaylı işlemeyi hedefler. Dosya bir kütüphaneyle işleniyorsa (ImageMagick, Ghostscript), o kütüphanenin açıklarını (ImageTragick, Bölüm 8) tetikleyen bir dosya yükler — web shell yüklemeden RCE.
Örnek/dev dosyaları arar. WP File Manager vakasındaki gibi, uygulamanın/eklentinin içindeki örnek (.dist, connector.minimal.php) veya test dosyalarının yanlışlıkla erişilebilir bırakılıp bırakılmadığını arar.
Savunmacı dersi: Saldırgan iki koşulu birlikte düşünür — "çalıştırılabilir içerik yükleyebilir miyim ve onu çalıştırabilir miyim?". Savunma her iki koşulu da kapatmalıdır.
7. Exploit Mantığı
Bu bölüm çalıştırılabilir web shell/payload içermez. Dosya yükleme RCE'sinin neden bu kadar yıkıcı olduğu kavramsaldır:
- Doğrudan ve kalıcı RCE. Bir web shell yüklendiğinde, saldırgan sunucuda süregelen bir komut yürütme yeteneği kazanır — ilk açık kapatılsa bile shell kalır (kalıcılık, Bölüm 34). Bu, en yüksek etkili sonuçtur.
- İki koşulun çarpımı. Etki, iki bağımsız zayıflığın (yükleme + çalıştırma) birleşmesine bağlıdır. Bu, savunmanın neden iki cepheli olduğunu ve neden "web-dışı depolama"nın bu kadar güçlü olduğunu (Koşul 2'yi tek başına kırar) açıklar.
- Zincirleme. Web shell, genellikle bir sonraki aşamanın (yanal hareket, cryptominer, veri sızdırma, botnet) başlangıç noktasıdır. WP File Manager vakasında web shell'ler Kinsing/cryptominer kurulumuna zincirlendi.
- Dolaylı yollar. Doğrudan
.phpyüklenemese bile, kütüphane işleme (ImageTragick),.htaccess, veya polyglot ile aynı sonuca ulaşılabilir. Bu yüzden tek bir kontrol (uzantı) yetmez.
Araştırmacının çerçevesi: (a) çalıştırılabilir içerik yüklenebiliyor mu, (b) yüklenen dosya çalıştırılabiliyor mu (web-erişilebilir + PHP), (c) dolaylı işleme/çalıştırma yolları var mı. Savunma (a) ve (b)'yi kapatır; en sağlamı (b)'yi web-dışı depolamayla kesin kırar.
8. Güvenli Kod
İki koşulu birden kıran, derinlemesine savunmalı sürüm:
<?php
// ✅ GÜVENLİ — içerik doğrulama + web-dışı depolama + rastgele ad
declare(strict_types=1);
// 0) Yetki + boyut (Böl. 14)
requireAuth();
$file = $_FILES['avatar'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK || $file['size'] > 2_000_000) {
http_response_code(422); exit('Geçersiz yükleme');
}
// 1) İÇERİK doğrulama (finfo — istemci MIME'ına GÜVENME)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']); // gerçek tür, sunucuda
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
if (!isset($allowed[$mime])) {
http_response_code(422); exit('İzin verilmeyen tür');
}
$ext = $allowed[$mime]; // uzantı GERÇEK türe göre atanır
// 2) Görüntüyü YENİDEN KODLA (gömülü kod/polyglot temizlenir)
$img = @imagecreatefromstring(file_get_contents($file['tmp_name']));
if ($img === false) { http_response_code(422); exit('Geçersiz görüntü'); }
// 3) RASTGELE ad + WEB-DIŞI dizin (Koşul 2 kesin kırılır)
$name = bin2hex(random_bytes(16)) . '.' . $ext; // orijinal ad kullanılmaz
$dir = '/var/app/private/uploads'; // web kökü DIŞINDA
$dest = $dir . '/' . $name;
// Yeniden kodlanmış hâli kaydet (orijinal baytlar değil)
switch ($ext) {
case 'jpg': imagejpeg($img, $dest, 90); break;
case 'png': imagepng($img, $dest); break;
case 'webp': imagewebp($img, $dest); break;
}
imagedestroy($img);
// 4) Servis: kontrollü betikle, doğrudan erişim yok
saveAvatarRef($userId, $name); // DB'de referans; dosya URL'den erişilemez
echo 'Yüklendi';
Kontrollü servis betiği (dosyayı yetki kontrolüyle sunar):
// ✅ Dosyayı yalnızca yetkiliye, çalıştırmadan sun
$name = basename($_GET['name'] ?? ''); // path traversal önlemi (Böl. 20)
$path = '/var/app/private/uploads/' . $name;
if (!isOwnedByCurrentUser($name) || !is_file($path)) { http_response_code(404); exit; }
header('Content-Type: image/jpeg');
header('Content-Disposition: inline; filename="avatar.jpg"');
header('X-Content-Type-Options: nosniff');
readfile($path);
Web-içi depolama kaçınılmazsa, o dizinde PHP yürütmeyi kapatmak (Bölüm 27):
# nginx — upload dizininde PHP'yi FPM'e GÖNDERME
location ^~ /uploads/ {
location ~ \.php$ { return 403; } # PHP çalıştırma yok
}
Öne çıkan modern kalıplar:
- İçerik doğrulama (finfo): İstemci MIME'ına asla güvenme; gerçek türü sunucuda belirle.
- Uzantı beyaz listesi + gerçek türe göre atama: Uzantı, kullanıcı adından değil, doğrulanmış içerikten türetilir.
- Görüntü yeniden kodlama: imagecreatefromstring + yeniden kaydetme, gömülü kod/polyglot'u imha eder (güçlü savunma).
- Web-dışı depolama: En sağlam Koşul-2 savunması; dosya URL'den hiç erişilemez, kontrollü betikle sunulur.
- Web-içiyse yürütmeyi kapat: nginx/Apache'de o dizinde PHP-FPM'e gönderme (Bölüm 27).
- Rastgele ad: Orijinal ad kullanılmaz (path traversal, üzerine yazma, tahmin önlenir).
- Boyut + yetki + tür sayısı sınırı: DoS ve yetkisiz yükleme önlenir.
- X-Content-Type-Options: nosniff: Tarayıcının MIME-sniffing ile içeriği yanlış yorumlamasını engeller.
- Örnek/dev dosyalarını üretime koyma (WP File Manager dersi, Bölüm 30).
9. Patch Analizi
Neden işe yarar? İçerik doğrulama + görüntü yeniden kodlama, Koşul 1'i (çalıştırılabilir içerik yükleme) kırar: PHP taşıyan bir dosya ya reddedilir ya yeniden kodlanınca kodu imha edilir. Web-dışı depolama, Koşul 2'yi (çalıştırılabilirlik) kesin olarak kırar: dosya web'den erişilemediğinden, PHP içerse bile çalışamaz. İki koşuldan herhangi biri kapalıysa RCE olmaz; ikisi birden kapalıysa derinlemesine savunma sağlanır (Bölüm 1). Rastgele ad, path traversal ve üzerine yazmayı kapatır.
Savunmaların karşılaştırması:
| Savunma | Kırdığı koşul | Güç |
|---|---|---|
| İstemci MIME kontrolü | — | Değersiz (atlatılır) |
| Uzantı kara listesi | Kısmen Koşul 1 | Zayıf (atlatılır) |
Uzantı beyaz listesi + finfo |
Koşul 1 | İyi |
| Görüntü yeniden kodlama | Koşul 1 | Güçlü (polyglot imha) |
| Web-dışı depolama | Koşul 2 | En güçlü tek karar |
| Dizinde PHP yürütme kapalı | Koşul 2 | Güçlü (web-içi zorunluysa) |
| Rastgele ad | Yan (traversal/overwrite) | Gerekli |
Performans: İçerik doğrulama ve yeniden kodlama küçük bir CPU maliyeti getirir (görüntü işleme), ama güvenlik kazancı büyüktür ve genelde kabul edilebilir. Web-dışı depolama + servis betiği, ek bir istek katmanı getirir ama önbelleklenebilir; maliyeti düşüktür.
10. Gerçek Hayat Senaryosu
Kurgu — bir işe alım (recruiting) platformu. Adaylar CV (PDF/DOCX) ve profil fotoğrafı yüklüyor. Uygulama, dosyaları web-erişilebilir /uploads/ dizinine orijinal adla kaydediyor ve yalnızca uzantı kara listesiyle "kontrol" ediyor. Bir saldırgan, cv.phtml (kara liste .php'yi engelliyor ama .phtml'i değil) adlı bir web shell yükleyip doğrudan çağırarak RCE elde ediyor; sunucuda İK veritabanına (binlerce adayın kimlik/iletişim verisi) erişiyor ve kalıcı bir arka kapı bırakıyor. Dersler: (1) kara liste yetmez, beyaz liste + içerik doğrulama gerekir; (2) dosyalar web-dışı saklansaydı, .phtml yüklense bile çalışamazdı (Koşul 2 kırılırdı); (3) DOCX/PDF gibi "belge"ler bile XXE (Bölüm 10) veya makro/işleme riskleri taşır — tür beyaz listesi ve güvenli işleme şarttır.
11. Gerçek CVE Analizi — WordPress File Manager RCE (CVE-2020-25213)
Wordfence, Seravo, Palo Alto Unit 42, MITRE/NVD ve CISA KEV kaydıyla doğrulanmıştır. Silahlaştırılmış payload verilmez.
Özet. Ağustos–Eylül 2020'de, WordPress'in popüler File Manager (wp-file-manager) eklentisinin 6.0–6.8 sürümlerinde kimlik doğrulaması gerektirmeyen keyfi dosya yükleme → RCE açığı, 0-gün olarak kitlesel biçimde sömürüldü. Yaklaşık 700.000 kurulum etkilendi; saldırganlar web shell'ler kurup ardından Kinsing/cryptominer (H2miner) yerleştirdi. Açık 6.9'da düzeltildi ve CISA'nın "bilinen sömürülen açıklar" (KEV) kataloğuna girdi. Sınıflandırması CWE-434 (Tehlikeli Türde Kısıtlanmamış Dosya Yükleme).
Teknik neden — "iki koşulun" ve "örnek dosya" hatasının birleşimi. Eklenti, içerdiği elFinder dosya yöneticisi kütüphanesinin bir örnek bağlayıcı dosyasını (connector.minimal.php-dist) paketleme sırasında connector.minimal.php olarak yeniden adlandırmıştı — böylece doğrudan çalıştırılabilir hâle geldi. Bu örnek dosya, elFinder'ın güçlü dosya işlemleri API'sini (yükleme, mkfile, put) açığa çıkarıyor ama hiçbir kimlik doğrulama/yetki kontrolü içermiyordu. Sonuç:
- Koşul 1 (yükleme): Kimlik doğrulamasız herhangi biri, elFinder komutlarıyla .php dahil keyfi bir dosyayı wp-content/plugins/wp-file-manager/lib/files/ dizinine yazabiliyordu — çünkü PHP yüklemeye izin veriliyordu.
- Koşul 2 (çalıştırma): Bu dizin web-erişilebilir ve PHP çalıştırılabilir olduğundan, yüklenen dosya doğrudan URL'den çağrılıp çalıştırılabiliyordu.
İki koşul da sağlandığından, tek bir kimlik doğrulamasız istekle web shell → RCE mümkündü. Kök neden çok öğreticidir: üretime sızmış bir örnek/geliştirme dosyası (Bölüm 30) + erişim kontrolü eksikliği (Bölüm 14) + çalıştırılabilir dizine yükleme (Koşul 2). Üç ders bir araya geldi.
Etki. ~700.000 site; kitlesel otomatik sömürü (saldırı kolayca otomatikleştirilebildiğinden), web shell'ler, cryptominer'lar ve kalıcı arka kapılar. Yama öncesi kurulumlar kısa sürede ele geçirildi.
Satıcı düzeltmesi. File Manager 6.9, sorunlu connector.minimal.php dosyasını eklentiden kaldırdı. (Manuel olarak bu dosyayı silmek de geçici mitigation sağlıyordu.)
Çıkarılacak dersler: 1. Örnek/geliştirme dosyaları asla üretime çıkmamalı ve erişim kontrolsüz güçlü uç noktalar açıkta kalmamalı (Bölüm 14, 30). 2. Yüklenen dosyalar çalıştırılabilir bir dizinde durmamalı. Web-dışı depolama veya yürütme kapatma, bu vakayı tümden önlerdi (Koşul 2). 3. Dosya yükleme kimlik doğrulama + içerik doğrulama gerektirir; "PHP yüklemeye izin ver" varsayılanı felakettir.
12. Detection
Kod incelemede: İstemci MIME güveni, kara liste, orijinal ad ve web-içi depolamayı arayın:
grep -rnE "\['type'\]|\['name'\]" src/ | grep -i _FILES # istemci MIME/ad güveni
grep -rn 'move_uploaded_file' src/ # hedef dizin web-içi mi?
grep -rnE 'finfo|imagecreatefrom|getimagesize' src/ # içerik doğrulama var mı?
grep -rn 'pathinfo.*EXTENSION' src/ # uzantı beyaz liste mi kara mı?
Kontroller: içerik doğrulanıyor mu (finfo)? Uzantı beyaz listesi mi? Ad rastgele mi? Depolama web-dışı mı / yürütme kapalı mı? Yetki var mı?
SAST: Kısıtlanmamış dosya yükleme (CWE-434) için kurallar; istemci MIME güvenini ve web-içi hedefi işaretler.
DAST: Farklı uzantı/MIME/polyglot yükleyip yüklenen dosyanın çalıştırılabilir olup olmadığını test eder.
Loglar/SIEM — en güçlü sinyaller: Yükleme dizininde beklenmeyen .php dosyalarının oluşması (dosya bütünlüğü izleme — FIM); yüklenen dosyalara doğrudan HTTP istekleri (özellikle ?cmd= gibi parametrelerle); web shell göstergeleri (eval, system, base64_decode, obfuscation) içeren yeni dosyalar; web sürecinden beklenmeyen alt süreçler ve giden bağlantılar (Bölüm 8, 34). WP File Manager saldırısında connector.minimal.php'ye POST istekleri ve lib/files/ altında yeni PHP dosyaları en net izlerdi.
13. Prevention
- İçerik doğrula (
finfo/yeniden kodlama): İstemci MIME'ına asla güvenme; görüntüleri yeniden kodlayarak gömülü kodu imha et. - Uzantı beyaz listesi: Yalnızca izinli uzantılar; uzantı gerçek türe göre atanır (kara liste değil).
- Web-dışı depolama: En sağlam savunma; dosyalar web kökü dışında, kontrollü betikle sunulur.
- Web-içiyse yürütmeyi kapat: Yükleme dizininde PHP-FPM'e gönderme (Bölüm 27).
- Rastgele ad +
basename: Path traversal (Bölüm 20), üzerine yazma ve tahmin önlenir. - Yetki + boyut + hız sınırı: Yetkisiz yükleme ve DoS önlenir.
- Güvenli işleme: Kütüphane işlemeyi (ImageMagick) sertleştir (Bölüm 8); belge türlerinde XXE'ye dikkat (Bölüm 10).
- Örnek/dev dosyalarını üretimden çıkar (Bölüm 30) ve güçlü uç noktalara yetki koy (Bölüm 14).
14. Mitigation
- Acil: Yükleme uç noktasını sertleştir (içerik doğrulama + beyaz liste); yükleme dizininde PHP yürütmeyi kapat; erişimsiz örnek/dev dosyalarını sil (WP File Manager'da olduğu gibi).
- Geçici: FIM/loglarla web shell taraması yap (Bölüm 34); bulunan shell'leri ve kalıcılık mekanizmalarını temizle; sızmış sırları döndür; giden bağlantıları incele.
- Kalıcı: Depolamayı web-dışına taşı; içerik doğrulama + yeniden kodlamayı standartlaştır; yetki + boyut sınırları ekle; testler + FIM izleme + bağımlılık/örnek-dosya taraması ekle.
15. Checklist
- [ ] Dosya türü içerikten mi (
finfo) doğrulanıyor (istemci MIME'ına güven yok)? - [ ] Uzantı beyaz listesi mi (kara liste değil) ve gerçek türe göre mi atanıyor?
- [ ] Görüntüler yeniden kodlanıyor mu (polyglot/gömülü kod imhası)?
- [ ] Dosyalar web kökü dışında mı saklanıyor (ya da o dizinde PHP yürütme kapalı mı)?
- [ ] Dosya adı rastgele mi (
random_bytes) vebasenameile mi (path traversal)? - [ ] Yükleme uç noktası kimlik doğrulama + yetki + boyut sınırı içeriyor mu?
- [ ] Kütüphane işleme (ImageMagick) sertleştirildi mi (Bölüm 8)?
- [ ] Örnek/dev dosyaları üretimde erişilebilir değil mi (Bölüm 30)?
- [ ] Yükleme dizini FIM ile izleniyor mu (yeni
.phpdosyaları)?
16. Laboratuvar
Lab 19.1 — MIME güveni. İzole ortamda $_FILES['type']'a güvenen bir yükleme yaz; bir .php dosyasını image/jpeg MIME'ıyla göndererek atlatmayı göster. finfo ile içerik doğrulamasına geçip düzelt.
Lab 19.2 — Kara liste atlatma. .php'yi engelleyen ama .phtml/.php5/.PhP geçen bir kara liste yaz; her varyantı dene. Beyaz listeye geçip düzelt.
Lab 19.3 — Çalıştırılabilirlik (Koşul 2). Web-içi bir dizine bir "web shell" (kendi izole test dosyan) yükleyip çalıştığını gözlemle. Dosyayı web-dışına taşı (veya dizinde PHP yürütmeyi kapat) ve artık çalışamadığını doğrula.
Lab 19.4 — Yeniden kodlama. Gömülü metin içeren bir görüntü oluştur; imagecreatefromstring + yeniden kaydetme sonrası gömülü içeriğin imha edildiğini gözlemle.
17. Quiz
- Dosya yükleme RCE'si için gereken iki koşul nedir? Neden ikisi birden gerekir?
$_FILES['type'](istemci MIME) neden güvenilmez? Doğru tür nasıl belirlenir?- Uzantı kara listesi neden yetersizdir? En az üç atlatma yöntemi sayın.
- Polyglot dosya nedir ve içerik (magic bytes) doğrulamasını nasıl atlatır?
- Görüntü yeniden kodlama hangi koşulu ve neden kırar?
- Web-dışı depolama neden "en güçlü tek karar"dır? Hangi koşulu kırar?
- Web-içi depolama zorunluysa hangi savunma uygulanır (Bölüm 27)?
- Rastgele dosya adı hangi ek riskleri (path traversal, üzerine yazma) kapatır?
.htaccessyüklemek bir saldırgana ne sağlayabilir?- WP File Manager vakasında iki RCE koşulu nasıl sağlandı? "Örnek dosya" hatası ne rol oynadı?
- Dosya yükleme hangi diğer açık sınıflarının (ImageTragick, path traversal, XXE) kavşağıdır?
- SIEM'de dosya yükleme sonrası RCE için en güçlü tespit sinyalleri nelerdir?
18. Kaynakça
- OWASP, File Upload Cheat Sheet; Unrestricted File Upload (WSTG).
- MITRE, CWE-434: Unrestricted Upload of File with Dangerous Type.
- MITRE / NVD, CVE-2020-25213; CISA Known Exploited Vulnerabilities (KEV) kaydı.
- Wordfence, 700,000 WordPress Users Affected by Zero-Day in File Manager Plugin; Seravo 0-day analizi; Palo Alto Unit 42, CVE-2020-25213 in the Wild (Kinsing).
- PHP Manual,
finfo,move_uploaded_file,imagecreatefromstring,$_FILESreferansları. - OWASP, Protect FileUpload Against Malicious File ve nginx/Apache yürütme kapatma rehberleri (Bölüm 27).