İçeriğe atla
0 XP 0
13 dkderinleşmeyle 14 dk50 XP7 etkinlik

access.log: web sunucusunun hafızası

Bu derste
  • Nginx ve Apache access.log satırlarından dizin taramasını, yol geçişini ve web kabuğunu ayıklamayı, awk ile saymayı ve kaydı zenginleştirmeyi öğren.
  • Bu derste web01'in access.log kaydından dizin taramasını, yol geçişi denemesini ve web kabuğu yüklemesini ayıklayacaksın.
  • Durum kodu tek başına yetmez. Kodu, yolu, boyutu ve hızı birlikte okuyacağız.

Puan bütün etkinlikleri cevaplayınca hesaba yazılır. Bu dersi tamamlayınca +50 XP alacaksın. Giriş yapmadan ilerlersen puan yalnız bu tarayıcıda durur.

Web sunucusu gelen her isteği tek satıra yazar. Bu dosya sunucunun hafızasıdır: kim geldi, ne istedi, ne aldı? Bu derste web01'in access.log kaydından bir gecelik saldırıyı çıkaracaksın.

Önce birleşik biçimi alan alan tanıyacaksın. Sonra tarama, yol geçişi ve web kabuğu izlerini ayıracak, kodlanmış istekleri çözeceksin. awk ile sayım yapacak, saldırının hızını ölçeceksin. En sonda kayıt biçimini zenginleştirmeyi, yük dengeleyici arkasında gerçek adresi bulmayı ve aynı saldırıyı yapılandırmayla durdurmayı göreceksin.

Birleşik biçim ve durum kodları

Nginx ve Apache varsayılan olarak birleşik (combined) biçimi kullanır. Nginx bu biçimi yerleşik olarak tanımlar, Apache'de aynı satır LogFormat yönergesiyle kurulur. Sıra iki sunucuda da aynıdır.

Önce kaynak adres gelir. Sonra iki kimlik alanı gelir; çoğu sitede ikisi de tire olarak kalır. Köşeli parantez içinde zaman ve saat ofseti durur.

Tırnak içindeki istek satırı üç parça taşır: yöntem, yol ve protokol sürümü. Ardından durum kodu ve yanıt gövdesinin bayt cinsinden boyutu gelir. Son iki tırnaklı alan yönlendiren adres (Referer) ve kullanıcı ajanıdır (User-Agent). Aşağıda önce iki sunucunun tanımını, sonra web01'den beş satırı görüyorsun.

# nginx: yerleşik combined tanımı
log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

# Apache: aynı biçim
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" combined
İki sunucu, aynı alan sırası. Nginx'teki tire ve $remote_user, Apache'deki %l ve %u alanlarının karşılığıdır.
198.51.100.77 - - [14/Sep/2026:03:02:11 +0300] "GET /admin HTTP/1.1" 404 162 "-" "gobuster/3.6"
198.51.100.77 - - [14/Sep/2026:03:02:11 +0300] "GET /backup HTTP/1.1" 404 162 "-" "gobuster/3.6"
198.51.100.77 - - [14/Sep/2026:03:02:12 +0300] "GET /uploads HTTP/1.1" 301 178 "-" "gobuster/3.6"
198.51.100.77 - - [14/Sep/2026:03:04:55 +0300] "POST /uploads/up.php HTTP/1.1" 200 34 "-" "Mozilla/5.0"
10.20.30.44 - - [14/Sep/2026:09:11:03 +0300] "GET /urunler?id=12 HTTP/1.1" 200 8841 "https://altaykargo.example/" "Mozilla/5.0"
web01 access.log. İlk üç satır tarama, dördüncü satır yüklenen dosyanın çalıştırılmasıdır.

Durum kodu isteğin sonucunu söyler. RFC 9110 kodları beş aileye ayırır: 1xx bilgi, 2xx başarı, 3xx yönlendirme, 4xx istemci hatası, 5xx sunucu hatası. 200 istek karşılandı, 301 kalıcı yönlendirme, 404 kaynak yok, 500 sunucunun kendi hatası demektir. 401 ile 403 sık karışır.

401 kimliğin doğrulanmadığını söyler. 403 ise sunucunun isteği anladığını ama reddettiğini söyler; kimlik verilmişse yetki yetmemiştir. Bu ayrım bir denemenin kapıda mı yoksa içeride mi takıldığını gösterir.

Aileyi tanımak ilk süzgeçtir. 4xx yığını çoğu zaman keşfi, 5xx yığını ise bozuk girdiye verilen tepkiyi gösterir. Kod tek başına sonuç vermez; yol ve yöntemle birlikte anlam kazanır.

Etkinlik 1 / 7

Durum kodunu anlamıyla eşleştir.

Soldan bir tane seç, sağdan eşini seç. Kullanılmış bir seçeneği yeniden seçersen yeni öğeye taşınır.

Tarama, yol geçişi ve web kabuğu izleri

Dizin taramasının imzası bellidir. Saniyeler içinde onlarca farklı yol gelir, hepsi 404 alır, hepsi aynı adresten çıkar. Kullanıcı ajanında gobuster, ffuf ya da dirsearch yazması işi kolaylaştırır, ama saldırgan bu alanı tek bir seçenekle değiştirir. O yüzden ajan adına güvenme, hıza ve dağılıma bak.

Gerçek kullanıcı bir sayfayı açar, resimleri ve stil dosyalarını çeker, biraz bekler. Tarama aracı ise ardışık ve çoğunlukla başarısız ister. Yönlendiren alanı da ipucu verir; gerçek ziyaretçide çoğu zaman doludur, araçlarda tire olarak boş kalır.

Asıl tehlike 404 yığınının içindeki birkaç 200'dür. Tarama orada bir şey bulmuştur.

Yol geçişi (path traversal) denemesi, web kökünün dışındaki bir dosyayı okumaya çalışır. İz genelde ../ dizisidir. Saldırgan bu diziyi filtreye takılmasın diye kodlar.

%2f eğik çizginin, %2e noktanın kodlanmış hâlidir. Çift kodlamada yüzde işareti de kodlanır: %252f önce %2f'ye, sonra eğik çizgiye döner. Kodlanmış hâliyle yazılan bir kural bu çeşitlemeleri kaçırır.

İlke şudur: önce normalleştir, sonra ara. Aşağıda Python'un standart kütüphanesiyle tek satırlık çözüm var; ikinci komut çift kodlamayı iki turda açar. SIEM tarafında aynı işi ayrıştırma aşamasında yaparsın.

$ python3 -c 'import sys, urllib.parse; print(urllib.parse.unquote(sys.argv[1]))' '/indir?dosya=..%2f..%2fetc%2fpasswd'
/indir?dosya=../../etc/passwd

$ python3 -c 'import sys, urllib.parse as u; print(u.unquote(u.unquote(sys.argv[1])))' '/indir?dosya=%252e%252e%252fetc%252fpasswd'
/indir?dosya=../etc/passwd
Tek ve çift kodlanmış yol geçişi denemesi, çözülmüş hâliyle.

En ağır bulgu web kabuğudur (web shell). Saldırgan sunucuya kendi betiğini yükler, sonra o betiğe komut gönderir. Zincir loglarda sırayla görünür.

Önce 404 yığını gelir. Sonra /uploads dizini 301 ile bulunur. Ardından bir yükleme formuna ya da açığa istek gider.

Sonunda /uploads/up.php adresine tekrarlanan küçük POST istekleri akar. Yanıt boyutu burada sessiz bir ipucudur. Hata sayfası hep aynı küçük boyutta döner, gerçek bir sayfa ise binlerce bayt tutar.

Web kabuğu komut çıktısını kısa metin olarak döndürür ve sayfa şablonu taşımaz; bu yüzden 34, 36, 34 gibi tekdüze boyutlar görürsün. Kullanıcının dosya attığı dizinde kod çalışmamalıdır. /uploads altındaki bir .php dosyasına 200 dönüyorsa kabuğu bulmuşsun demektir.

$ grep -E '(GET|POST) /uploads/[^ ]+[.]php' access.log
198.51.100.77 - - [14/Sep/2026:03:04:55 +0300] "POST /uploads/up.php HTTP/1.1" 200 34 "-" "Mozilla/5.0"
198.51.100.77 - - [14/Sep/2026:03:05:12 +0300] "POST /uploads/up.php HTTP/1.1" 200 36 "-" "Mozilla/5.0"
198.51.100.77 - - [14/Sep/2026:03:05:40 +0300] "POST /uploads/up.php HTTP/1.1" 200 34 "-" "Mozilla/5.0"
Yükleme dizininde çalışan PHP dosyasına giden istekler. Küçük ve birbirine yakın boyutlar komut çıktısıdır.

Etkinlik 2 / 7

access.log parçasında saldırıya ait satırları işaretle.

Şüpheli satırlara tıkla. Birden fazla olabilir.

Çözülmüş yol saldırının türünü de söyler. SQL enjeksiyonunda tek tırnak, OR 1=1, UNION SELECT ve sleep( gibi parçalar görürsün. XSS denemesinde <script>, onerror= ya da javascript: dizileri çıkar. Dosya dahil etme (LFI) denemesinde ../ dizisinin yanında php://filter gibi sarmalayıcılar belirir.

Çözmeyi kaç tur yapacağını da say. Çözdüğün değerde %25 dizisi hâlâ duruyorsa kodlama bir kat daha vardır. Yüzde işareti kalmayana kadar çözmeye devam et, sonra kuralı sonuca uygula.

Etkinlik 3 / 7

access.log'daki sorgu değerini URL çözümüyle aç. Çift kodlama için aracı iki kez uygula. Saldırgan hangi yolu okumak istedi?

Araçları sırayla dene. Yanlış araç bozuk çıktı verir.

%252e%252e%252f%252e%252e%252fetc%252fpasswd

awk ile sayım, hız ve hata kaydı

Birleşik biçimde awk alanları sabittir, ama bir tuzakla. awk satırı boşluklardan böler. $1 kaynak adres olur.

$4 zamanın köşeli parantezli ilk yarısı, $5 ofsettir. İstek satırı üç alana bölünür: $6 başında tırnakla yöntem, $7 yol, $8 protokol. $9 durum kodu, $10 gövde boyutu, $11 yönlendiren adrestir.

Kullanıcı ajanı $12'den başlar ama tek alan değildir. Mozilla/5.0 (Windows NT 10.0; Win64; x64) gibi bir ajan boşluk taşır ve birkaç alana yayılır. $12'den sonrasına numarayla güvenme. Ajanın tamamı gerekiyorsa satırı tırnak işaretinden bölersin; awk -F'"' ile ajan altıncı alana düşer.

$ awk '$9 == 404 {print $1}' access.log | sort | uniq -c | sort -rn | head -3
    340 198.51.100.77
      4 10.20.30.44
      1 10.20.30.51

$ awk '$1 == "198.51.100.77" {print substr($4, 2, 17)}' access.log | uniq -c
    118 14/Sep/2026:03:02
    171 14/Sep/2026:03:03
     54 14/Sep/2026:03:04
      2 14/Sep/2026:03:05
Üstte 404 üreten adresler, altta tek adresin dakika başına istek sayısı.

İlk komut yalnız 404 dönen satırlarda adresi yazar ve adresleri sayar. 198.51.100.77 tek başına 340 hata üretmiştir. İç adreslerin birkaç 404'ü ise kırık bağlantıdır.

İkinci komut bu adresin isteklerini dakikaya göre gruplar. substr($4, 2, 17) köşeli parantezi atlar ve saniyeyi keser. Burada uniq -c sort olmadan çalışır, çünkü log zaten zaman sırasıyla yazılır ve aynı dakika yan yana durur.

Dakikada 171 istek bir insanın tıklama hızı değildir. Bu iki sayı alarm cümlesinin ham maddesidir: kaynak, kod ve hız. Sayımı yaparken HEAD ve OPTIONS isteklerini ayrıca süz; izleme araçları ve tarayıcı ön istekleri bunları bolca üretir.

Erişim kaydı isteğin sonucunu söyler, nedenini söylemez. Nedeni hata kaydı verir. Nginx'te bu dosya error.log'dur ve PHP uyarıları, izin reddi ya da açılamayan dosya mesajları oraya düşer.

İki dosyayı aynı saniyede yan yana koy. Aşağıdaki örnekte erişim kaydı 200 yazıyor, yani saldırgan bir cevap almış görünüyor. Aynı saniyedeki hata satırı ise PHP'nin open_basedir sınırı yüzünden dosyayı açmadığını söylüyor.

Durum kodu 200 olsa da deneme dosyayı okuyamamıştır; dönen 41 bayt uygulamanın kendi hata mesajıdır. Tersi de olur: 500 dönen bir istek bazen asıl açığın göstergesidir. Hata kaydının zamanı ofset taşımaz; bu farkı zaman dersinde ele alacaksın.

# access.log
198.51.100.77 - - [14/Sep/2026:03:02:19 +0300] "GET /indir?dosya=..%2f..%2fetc%2fpasswd HTTP/1.1" 200 41 "-" "curl/8.4.0"
# error.log
2026/09/14 03:02:19 [error] 812#812: *4411 FastCGI sent in stderr: "PHP message: PHP Warning:  file_get_contents(): open_basedir restriction in effect. File(../../etc/passwd) is not within the allowed path(s): (/var/www/html) in /var/www/html/indir.php on line 12" while reading response header from upstream, client: 198.51.100.77, server: web01, request: "GET /indir?dosya=..%2f..%2fetc%2fpasswd HTTP/1.1"
Aynı saniye, iki dosya. Erişim kaydındaki 200, hata kaydındaki open_basedir engeliyle birlikte okunur.

Etkinlik 4 / 7

Aşağıdaki komut yukarıdaki beş satırlık access.log üzerinde ne yazar?

Önce kodu oku, sonra çıktıyı seç.

awk '$9 == 404 {print $1}' access.log | sort | uniq -c

Etkinlik 5 / 7

awk satırı boşluklardan böldüğünde, Nginx birleşik biçimindeki durum kodu kaçıncı alana düşer? ____

Biçimi zenginleştir, gerçek adresi bul

Varsayılan biçim çok şeyi yazmaz. POST gövdesi, çerezler ve başlıkların çoğu kayda girmez. GET ile gelen bir enjeksiyonu görürsün, aynı yükü POST gövdesinde taşıyan denemeyi göremezsin.

Gövdeyi görmek için WAF ya da ters vekil kaydına bakarsın; bu kayıtlar isteğin tamamını ve verilen kararı tutar. Yanıt süresi ve gerçek istemci adresi de eksiktir. Nginx'te $request_time isteğin işlenme süresini milisaniye çözünürlükle, saniye cinsinden verir.

Süre istemciden ilk bayt okunduğu anda başlar, log yazıldığında biter. $upstream_response_time arka uygulamanın ne kadar beklettiğini gösterir. Aniden uzayan süreler, zamana dayalı kör SQL enjeksiyonunu ve kaynak tüketen istekleri görünür kılar.

Nginx 1.11.8'den beri log_format için escape=json seçeneğini tanır. Bu seçenekle her satır geçerli bir JSON nesnesi olur ve SIEM ayrıştırıcısı tahmin yürütmez. $time_iso8601 ise zamanı yıl ve ofsetle yazar. Bu değişiklikleri olaydan önce yapman gerekir.

log_format json_log escape=json '{"zaman":"$time_iso8601","ip":"$remote_addr",'
  '"istek":"$request","kod":$status,"boyut":$body_bytes_sent,'
  '"sure":$request_time,"arka_sure":"$upstream_response_time",'
  '"ajan":"$http_user_agent","xff":"$http_x_forwarded_for"}';
access_log /var/log/nginx/access.json json_log;

{"zaman":"2026-09-14T03:04:55+03:00","ip":"198.51.100.77","istek":"POST /uploads/up.php HTTP/1.1","kod":200,"boyut":34,"sure":0.012,"arka_sure":"0.011","ajan":"Mozilla/5.0","xff":""}
JSON biçimli kayıt ve ürettiği satır. arka_sure metin olarak tutulur, çünkü birden çok arka uç denenirse virgüllü liste gelir.

Yük dengeleyici ya da ters vekil arkasında $remote_addr yanıltır. Bütün istekler dengeleyicinin iç adresinden geliyor görünür ve bütün kuralların tek adrese bakar. Gerçek adres X-Forwarded-For başlığında taşınır.

Nginx'te ngx_http_realip_module bu başlığı okur ve istemci adresini değiştirir. Modül varsayılan derlemede yoktur, kullandığın pakette olup olmadığını kontrol et. set_real_ip_from güvendiğin vekillerin adresini tanımlar.

real_ip_header hangi başlığın okunacağını söyler. real_ip_recursive on ise başlıktaki güvenilmeyen son adresi alır. Tuzak şudur: X-Forwarded-For başlığını istemci de yazabilir. İlk adrese körü körüne güvenirsen saldırgan istediği adresi loga yazdırır.

set_real_ip_from  10.0.0.0/24;      # yalnız yük dengeleyiciler
real_ip_header    X-Forwarded-For;
real_ip_recursive on;
Güvenilir vekil listesi dar tutulur. Liste genişledikçe sahte başlık loga sızar.

Etkinlik 6 / 7

web01'e 10.0.0.5 adresindeki yük dengeleyiciden bir istek geliyor. İstemci başlığa sahte bir adres yazmış, dengeleyici de gerçek adresi sona eklemiş. Nginx $remote_addr olarak ne yazar?

Önce kodu oku, sonra çıktıyı seç.

set_real_ip_from  10.0.0.0/24;
real_ip_header    X-Forwarded-For;
real_ip_recursive on;

# bağlantı 10.0.0.5 adresinden
X-Forwarded-For: 192.0.2.99, 203.0.113.50

Savunma ve alarm cümlesi

Tespitin karşılığı yapılandırmadır. İlk kural, yükleme dizininde kod çalışmamasıdır. Nginx'te /uploads altındaki .php isteklerini reddeden tek bir location bloğu web kabuğunu çalışamaz hâle getirir. Bu blok, PHP'yi işleyen location bloğundan önce yazılır; nginx düzenli ifadeli blokları sırayla dener ve ilk eşleşeni seçer.

Yükleme tarafında uzantıyı izin listesiyle denetle, dosya içeriğinin türünü doğrula ve dosyayı web kökünün dışında sakla. Oran sınırlama (limit_req) dakikada yüzlerce isteği keser. Web uygulama güvenlik duvarı (WAF) bilinen kalıpları normalleştirerek engeller. Dosya bütünlüğü izleme (FIM) ise web kökünde beliren yeni bir .php dosyasını hemen haber verir.

location ~* ^/uploads/.*[.]php$ {
    deny all;      # yükleme dizininde betik çalışmaz, 403 döner
}
Yükleme dizinindeki PHP isteklerini kapatan blok. PHP işleyicisinden önce gelmelidir.

Log bir iz verir, diskteki dosya ise kanıtı. Web kabuğunu bulduğunda aynı saatlerde başka dosya bırakılıp bırakılmadığına bak. find komutu belirli bir andan sonra değişen dosyaları listeler. Çıktıdaki her dosyayı paket listesiyle ya da sürüm deposuyla karşılaştır.

Dosyayı silmeden önce kopyasını al ve sha256sum ile özetini kaydet; delil zinciri bu adımla başlar. Sonra erişimi kapat, dosyayı kaldır ve yükleme açığını düzelt. Yalnız dosyayı silip açığı bırakırsan saldırgan birkaç dakika içinde yenisini yükler. Temizlikten sonra aynı find komutunu bir kez daha çalıştır ve boş çıktı gör.

$ find /var/www -name '*.php' -newermt '2026-09-14 03:00' -ls
  918231      4 -rw-r--r--   1 www-data www-data       61 Sep 14 03:04 /var/www/html/uploads/up.php
Saldırı penceresinden sonra değişen PHP dosyaları. Tek sonuç, logdaki web kabuğudur.

Sık yapılan hatalar birbirine benzer. 404'ü zararsız saymak ilk hatadır; asıl tehlike tarama sırasında 200 dönen yollardır. Kullanıcı ajanına güvenmek ikincisidir.

Kodlanmış yolu çözmeden kural yazmak üçüncüsüdür. Sağlık yoklamalarını ve izleme ajanlarının düzenli isteklerini saldırı sanmak da boş alarm üretir; bu kaynakları önce tanı. ATT&CK eşlemesi raporu ortak dile çevirir: sözlükle dizin taraması T1595.003, açıktan yararlanma T1190, web kabuğu T1505.003 olarak geçer.

Son adım alarm cümlesidir. Kaynak kimi, kod sonucu, yol niyeti, hız ise aracı söyler. Dördü birleşince tek cümle kurulur: 198.51.100.77 adresi iki dakikada 340 tane 404 üretti, /uploads dizinini buldu ve oraya yüklediği up.php dosyasına komut göndermeye başladı.

Derinleş:Common Log Format'tan JSON'a

Bugün kullandığın satır biçimi 1990'ların başındaki NCSA HTTPd sunucusundan kalır. O dönemin Common Log Format'ı (CLF) yedi alan taşıyordu: istemci adresi, ident kimliği, kullanıcı adı, zaman, istek satırı, durum kodu ve boyut. Referer ve User-Agent sonradan eklendi ve bu genişletilmiş biçime birleşik biçim denildi.

İkinci alandaki tire, RFC 1413'teki ident protokolünden kalan bir izdir. İstemcideki ident servisi kullanıcı adını bildirecekti. Bu servis bugün neredeyse hiç çalışmaz ve alan hep tire kalır. Apache bu biçimi korudu, nginx de aynı adla yerleşik olarak sundu.

Biçimin kalıcılığı araçları besledi; yıllar önce yazılmış awk komutları hâlâ çalışır. Bedeli ise ayrıştırmanın kırılganlığıdır. Tırnak içindeki boşluklar alan numaralarını kaydırır, kaçış kuralları sunucudan sunucuya değişir. JSON biçimine geçiş bu kırılganlığı bitirir.

Etkinlik 7 / 7

Bir adres iki dakika içinde 340 farklı yola istek atıp hepsinden 404 alıyorsa bu hangi davranıştır?

Bu derste bir hata mı gördün? Bildir