]> git.somenet.org - pub/jan/ctf-seminar.git/blob - writeups/ginkor/hxp36c3.md
Add hxp writeup
[pub/jan/ctf-seminar.git] / writeups / ginkor / hxp36c3.md
1 # HXP 36c3 CTF - WriteUpBin challenge
2
3 The WriteupBin challenge was from HXP 36c3 ctf, which took place from 27. until 29.12.2019.
4 It was a web challenge, and ended with 13 solves.
5 Its given description was:
6 > Finally (again), a minimalistic, open-source social writeup hosting solution.
7
8 ## General information about the challenge
9
10 The web service given for this challenge was, as said, a writeup hosting solution, which offered a text field to put writeups in, and generated URLs for them, to be shared with other people.
11 A docker container, containing the website source including a python script for the admin behaviour, was already given, which enables us to create a local environment to test our solution on, including a docker run command in the first line of the dockerfile, which runs the container as intended for the challenge (including starting the admin script).
12
13 ## My own (failed) attempt and information gathering
14
15 Since I was nowhere with a somewhat decent internet connection at the time of me taking the challenge, I only tried solving the challenge locally using the provided docker environment, and also wasn't able to get any information by others using mattermost.
16 It was still lucky that the docker environment was available, since without it I could not have worked on it at all.
17 So all of the following "my own attempts" where conducted locally, not on the given server.
18 Starting the challenge up locally was easy enough after I found the correct command to start docker in the Dockerfile (just building and starting it with exposed ports does not start the admin.py file).
19
20 ### First analysis of the challenge
21
22 The challenge consists of a website, with a greetings message, identifing me by some 16 character hex string, and a text input field to submit your writeups.
23 After submitting a demo writeup (which had to be at least 140 characters), the writeup was listed under the previously empty "Your writeups" section of the site, under a 16 character long hex id.
24 When you click on it (or right after you submitted it), you will be forwarded to the `/show.php?id=5a79cb3347d78b9d` site, where you can like the writeup, and show it to the admin, which then likes the writeup as well.
25 While guessing that the admin, most likely has the flag either as ID or as writeup, I assumed a good next step would be testing if I can view other writeups as well.
26 You can also see who liked your writeup, and the admin user was called "admin", so the flag would most likely be in a writeup.
27
28 By a look at the cookies saved by the site (to check how the site knows which user you are), I saw a PHPSESSID cookie, which (without yet looking at the code) hinted that it used PHP sessions as "authentication", to be able to list your own writeups.
29 I used the inkognito mode of chrome to get another session, which of course had an empty writeup list then, and tried to access my previously created writeups, with success.
30
31 By looking at the source code of the application, and its db.sql file, the earlier suspicion that the flag was in a writeup was confirmed, and the writeup id gets generated by the command used to start docker, and is a random 16 character hex string. Also, there is only one writeup generated for the admin.
32
33 To wrap it up:
34
35 * Flag is stored under admin user with randomly generated 16 character hex ID
36 * You can access any writeups you want, if you know the ID
37 * Admin likes your writeup upon request, has username "admin"
38 * A list of your own writeups gets displayed on the site even if looking at another writeup
39 * The admin only has one writeup, which contains the flag
40
41 ### Attempting to solve the challenge
42
43 Bruteforcing the value is not feasable, since there are 16^16 possibilities, or with a decimal base, 10^19, which would take way to long and overload the server in any case. My first attempt for an XSS injection was simple, just enter a `<script>alert(0);<\script>` into the writeup field, to attempt a simple stored XSS attack (since no URL parameters where parsed and you cannot give the admin an url, that seemed the natural variant).
44 I did not assume it would work, it was more to show what would happen, and something happened, since immediatly a error message appeared, first because the message was to short, and after I added random characters to make it long enough, "value seems to be invalid" appeared.
45 After looking at the website source, I saw it used two javascript libraries, jquery and parsley, and using parsley this field was validated, on the client side.
46 Now client side verification is not a bad thing in general, if server side verification is in place as well, which it wasn't in this case.
47 So, after removing the verification disallowing "<>" in the chrome developer console, the script tags submitted to the server without further problems.
48 This would theoretically also work with a curl post of course, but I found it way easier just editing the website with the developer tools.
49
50 After submitting the writeup was loaded, and I immediatly got a CSP error in the console.
51 But at least, the code was loading up, and there seemed to be no other server side restrictions in place.
52 To exploit it now, "only" two things have to be done:
53
54 1. Bypass CSP (hard)
55 2. List writeups and leak them (depending on the bypass, possibly easy)
56
57 I spent hours looking to bypass the CSP somehow, which was very strict.
58 First I used the [Google CSP evaluator](https://csp-evaluator.withgoogle.com/) to evaluate the CSP, but it found no obvious issues with it, if no jsonp endpoints or angularjs where hosted by the whitelisted domains, which was not the case (they even specifically locked the csp whitelist to the two files).
59 The jQuery version was slightly outdated at the point of the challenge (it was shipped with version 3.4.0, but version 3.4.1 was current), but I found no known XSS vulnerabilities.
60 I also looked for CSP bypasses using parsley, but no promising results there as well.
61 XSS auditor header was set, but since it was set to block, the CSP comes with headers, and it is removed by recent chromium versions anyway, that also seemed not leading to anything.
62 I also dug into the parsley documentation, but oversaw the possibility for information extraction explained later.
63 After spending those houres googling and trying anything that was even remotely promising, I finally gave up on the challenge, without being able to solve this problem.
64
65 ## How to solve the challenge for real
66
67 This is based on the writeup by "pasten" (the winners of HXP 36c3) from [CTFTime](https://ctftime.org/writeup/17935).
68
69 The XSS injection uses not so much the injection of actual javascript, but uses the parsley verification to leak the id of the admins writeup step by step, by injecting a form field into the site, but lets start slower.
70
71 Parsley is a javascript library for validating form fields, which is based on jQuery.
72 It offers the possibility to validate the field for equality with another field (with `data-parsley-equalto`), and to append error messages into other elements on the same page.
73 This validation can also be triggered by javascript events like blur or focusoff.
74 Also, it uses the jQuery selector options to be able to match other elements on the site, in this case for matching attributes for `data-parsley-equalto`.
75
76 Using this tool, a form can be written, which uses parsley for input validation, with trigger to focusout, which will trigger once the admin presses the like button (but before the form submits, because it is autofocused, and also the admin will automatically click on the like button, which will makes it lose its focus, and thus triggering the validation).
77
78 ```html
79 <form data-parsley-validate>
80     <input type="text"
81         data-parsley-trigger="focusout"
82         data-parsley-equalto='a[href^="/show.php?id=GUESS"]'
83
84         data-parsley-errors-container="form[action='/like.php']"
85         data-parsley-error-message='<input type="input" name="id" value="0000000000000000">'
86
87         value='a[href^="/show.php?id=GUESS"]'
88         autofocus>
89     <input type="submit">
90 </form>
91 ```
92
93 This form works by using this trigger, and checking the equality of its own value to the value in the field specified by `data-parsley-equalto` (which uses a jQuery selector).
94 We use a match for all links to other reports (which are the links under the "Your reports" section).
95 If this element exists, it will compare the .val() of this element to our current value (which will not match, since .val() of a link seems to be an empty string).
96 If the element specified by this selector does not exist, it will be `undefined` and parsley will try to match the value of `data-parsley-equalto`, so in our case `a[href^="/show.php?id=GUESS"]` to our value, which will match.
97
98 We will now try to extract the information, by guessing the id of the admin writeup one character at a time (the jQuery selector also matches partially), to trigger the parsley validity check, or not.
99 So, if our guess is correct, the validity check by parsley will fail, leading to the error message to be injected (more on that in a bit).
100 If our guess is incorrect, the validity check passes, and the admin will like our writeup, as usual.
101
102 So, what happens if the validation fails (because our guess is correct). Parsley will inject our error message into the given form, which is the form for the like button. It appends the error message to this element. In our error message we set the ID parameter to `0000000000000000`, as new ID of the form it shall like. The new element on the bottom will now override the existing ID value, forcing the admin to like our custom ID. The CSRF token in this form will not be affected.
103
104 So, if we guess correctly, the submitted writeup will not be liked (since it will like the most likely non existant `0000000000000000` writeup), if it is incorrect, our report will not be liked.
105
106 Using the information whether the report is liked (and the guess incorrect) or not (and correct) we can get the ID one character at a time.
107 On average-case, this takes 8 * 16 = 128 guesses (256 on worst case), and can quite easily be automated.
108 See [this script by pasten](https://github.com/amelkiy/write-ups/blob/master/CCC/36c3/WriteupBin/solve.py) or [this script by defragment brains](https://ctftime.org/writeup/17891).
109
110 ### The Flag
111
112 > hxp{Petersilie_lol__I_couldn_t_come_up_with_funny_flag_sorry_:/}
113
114 ## Time spent on the challenges
115
116 I totally spent 10 - 15 hours on this challenge (most of it of looking through documentation and known CSP bypasses).
117 I also spent around 10 hours at the RuCTF (on-site), on the locator challenge, and also around 10 - 12 hours on Hack.Lu (numtonce challenge), around 3 hours on site, and the rest off-site.