Insomni'hack Teaser 2018 CTF

I saw that the Insomni'hack Teaser 2018 CTF was announced and I thought that would be an opportunity to progress and learn something new. As we already participated in a CTF with a group from our company, I thought we could use the same group and participate here. Unfortunately, of all the ~10 team members, only two thought this had enough priority in their calendar. The challenge started at Saturday 11am local time and ended Sunday night 11pm. I started with 90 minutes delay and my colleague told me that the first and easy challenge is a trap, where it says something like "wall of shame" if you copy something. I didn't want to hear much before it started, so I tried myself. There were 12 challenges and the site was broken in IE, but Chrome works fine:
overview screen, solved challenge in green

Welcome Challenge

The first (welcome) challenge presented itself like this:
warmup challenge
So it says there to "nc welcome.teaser.insomnihack.ch 42315". In the meantime (see previous blog post from HACKvent), I know that this is a Linux command, but on Windows we can also simply use the telnet client. But before doing so, I had to reconfigure my hardware firewall to allow outgoing connections with this nonstandard port to this server. So I connected to the server with "telnet welcome.teaser.insomnihack.ch 42315" and immediately got the flag:
warmup challenge solved
So the flag here is INS{YOU SHALL NOT PASTE}. Not sure what that means, but the flag was accepted and the challenge turned green.
Now I wanted to know where my colleague found this "wall of shame" thing. He told me that this is what happens when you copy the nc command from the challenge window. There seems to be some Javascript that changes the copy data. On Windows, probably nobody would copy an nc command, but it contained bash and PowerShell commands. This was the script. Out of curiosity, I looked what is there (connecting to port 42351 instead), but not much to see, just some message with a "hall of shame" site.
Of the total 759 registered "teams", only 427 solved this simple warmup challenge. No idea what the others have thought this is. To be fair, there were 6 teams that have solved at least one other challenge, but not this warmup one.

VulnShop Challenge

As mentioned above, my colleague started 90 minutes earlier and he was already working on this one, so I thought I look at this one too, in order to help each other.
The Challenge was presented like this:
VulnShop Task
So we had to connect to some website and exploit it somehow.
The website presented itself like a small unfinished prototype, written in PHP:
VulnShop start page
Clicking through the four links at the top showed a similar page, but there were two interesting things: the link to the server configuration (backup as .mht here) and a link where you could view the source code of the application. If we just look at the central code, we already see a few interesting things:
  • There are two pages (captcha and captcha-verify) that are not linked to.
  • On the contactus page, a session variable "challenge" will be created and filled with a PRNG number between 100000 and 999999.
  • On the captcha page, that random number will be printed out, so we can verify what is set.
  • On the captcha-verify page, there are two functions defined and a two-liner of code.
  • This two-liner checks if all variables are set (answer and method) besides the page of course, checks if the function in the parameter method is defined and if yes, calls that function with two parameters: the session variable challenge (6-digit random number) and an attacker-controlled variable
central code of VulnShop, code for captcha-verify

So that's all we have. First I thought that we somehow have to exploit the eval in the verifyFromMath function, but that one only executes something with the challenge parameter, which is not under our control, so that won't work. Then I saw the check function_exists and I immediately thought, that this surely also verifies internal functions as true. It later turned out this is the case. This means that we essentially can execute any built-in PHP function as long as it takes two parameters. The first parameter is a file name. As I'm not familiar with PHP, I had a look at the list of functions. There are thousands of functions and it is not possible to look through all of them. But I stumbled over the function var_dump, which dumps (prints) everything from the passed parameters and the number of parameters is variable. So we could easily test that this works:
var_dump call
So this confirmed that we're on the right track and that function_exists can be exploited that way. After searching for a while among these thousands of build-in functions, I couldn't find anything and I went to other challenges and left my colleague searching for more. He found the function file_put_contents and rename. Using these we could write arbitrary data to some file and even rename it. So I uploaded an evil PHP script <? php @eval($_REQUEST['p']); ?> and tried to execute it, but without success. First, it seems that at the beginning of the page, there is some Anti-XSS filtering being done, that potentially filters out our evil script. But even saving 123456 didn't work. My colleague noticed that at the top there is a chdir("tmp"); command, which means that the written files go into that directory. That directory cannot be accessed directly, even if we know the page name - we get an access denied error in the browser. As we have the configuration, we know where the site is located on the server, so we simply tried to move it back with the rename command. That also didn't work, because we got a 404 error trying to access that page. Probably the web directory is read-only or something. We got stuck there and couldn't solve it.

Bitcoin Challenge

I thought I know something about crypto and bitcoins, so I try this challenge. It gets presented like this.
Bitcoin challenge
So we see here that only a link to a web page is given. Let's open that web page:
Bitcoin page, top part
Bitcoin page, bottom part
So there's not much to see here. No buttons to click, no input, nothing. There was also no robots.txt file present. I noticed the hint at the bottom which says that they use vanity addresses. I remembered that people were using that to create short keys (for hardware coins and other reasons), but they were still pretty safe, although not that long. I thought I try a tool to create a vanity addresses, because with some luck I might get the same key like of one of these addresses., but then I decided not to do so, because the only reliable tool was last updated in 2012 and in bitcoin world, you have to assume that everything is backdoored to pwn your computer and more. And all these addresses had some money in it (around $800 in total I think). If there was any vulnerability to get the private key, it would already have been stolen, for sure. So no way to get the private key. So I decided to have a closer look at the previous transactions to these addresses.
If you look at any of the addresses on the blockchain, you can find that they were all filled with one single transaction to all five accounts. Nothing special there.
transaction to all five addresses (plus the return-address)
So I took a look at the five addresses individually. Most of them had the money still there, but the last one looked a bit strange:
last address, with transactions
So this one had some activity. It not only has two donations, but also one outgoing transaction. And the outgoing one is really tiny, with even smaller transactions fees, so that it hasn't even been confirmed yet, although sent over two months ago. But wait, the destination address looks odd! The destination address is 1PasteBinXXXXXdWzkucb1XXXXXXdY9fcu. So that's something we can continue with. We have the hint "pastebin" and some characters (dWzkucb1 and dY9fcu). The first one goes to a real pastebin post:
Pastebin post
To examine all hints, I also looked at the second pastebin link:
second Pastebin post - deleted
I have a Pastebin Pro account, so I logged in to see if I see more, but that's not the case - still shown as "deleted". I also tried the Wayback Machine to look at old archived content, but nothing there. I was later told by one of the organizers, that there was never anything and that Pastebin always shows this same message when there's no paste. So nothing to continue here. Let's continue with the first paste.
Ok, so we have this message:
-----BEGIN BITCOIN SIGNED MESSAGE-----
Hi dude, I added a login form to our website so that we can remotely share sensitive files :)
It's at /super_secret_Hax4B_admin_login.
-----BEGIN SIGNATURE-----
1Hax4B2j9FC3c73jHhfxrPQmv2zKuiSngv
HMBMSfQJiE68/9x0qeyIiEkf8T3N8Zt26d6vVBECVNZNJWcdIBk06sCcVNKkETaYDFcKwycnz6eDeAPwQyo9w0w=
-----END BITCOIN SIGNED MESSAGE-----
I first thought that this "BITCOIN SIGNED MESSAGE" is a joke, but this actually exists. If if you have the private key of an address, you can sign a message with it, that has then this format. The nice thing is that you can verify the message even without possessing the public key; it's sufficient to have the address. That only works due to some crypto property that I don't understand. But there's also a tool that you can use to verify that the message is indeed signed by this address:
message verified
Ok, so we now know that this is a valid message, signed by the owner of the private key of that address. So what do we get from there? It mentions a login page /super_secret_Hax4B_admin_login. So let's have a look:
Bitcoin login page
I tried to enter some random data, but I always got a "wrong format" error:
Bitcoin login page, wrong format
When looking at the beautified page source, we can see this:
Bitcoin login page source code
You can see there that the input field is defined as having a size of 96 characters and has the attribute "pattern". Looking up what that pattern attribute means, I found that this is a regular expression. So it starts with [iVBOR and has hundreds of characters and ends like this:
Bitcoin login page source code, scrolled to the right
So the regular expression ends these many characters with YII]{87}=. I looked up how these regular expressions work in JavaScript context, as I haven't done JS in many years and the {87} simply means to repeat the previous thing 87 times. I used some online regex tools to verify possible inputs that satisfy this. I thought I had to copy the entire long string iVBOR...YII 87 times and potentially add an '=' to the end. The online tools said I didn't get a match, but none explained why. I also noticed this entire string looks like base64, so I ran that through a base64 to binary converter and got a tiny image file:
tiny image
Not much to detect there. I tried a reverse image search, but all tools said that the image is too small to do a reverse image search. The next day (after I continued, which is explained later) I had the idea to have another look at that image and enlarge it, so I got this:
tiny image enlarged

So you can see that there's a person or something. Doing a reverse image search on this bigger image actually found the original image:
original of tiny image
It looks like this image is from reddit and is Peyton Manning. I have no idea what this means or who this guy is, but I found this reddit post that explains that this image is used in context of a rickroll. So this image is only there for confusion! Luckily, I didn't spend too much time with it.
Ok, after having spent some time with the regular expression, I understood that this entire expression [xxx]{87}= is written only in a confusing way. Anything in the first square brackets means that it's a list of characters to choose from; it doesn't have to be the same order or anything. And it's also allowed to repeat characters, so that's why they've put an entire image (base64-encoded) there. But it does mean that we need 87 characters of something base64 encoded (plus the '='). I noticed that the signature from the Pastebin had exactly this length, so I tried it:
login failed
So it seemed to me that I have to sign a message, probably the one in the Challenge given with the private key. But as we don't have any private key, I didn't know what to do. Stuck again.
I asked the organizers if I really have to find out the private key there. The answer was interesting: "Yes, I need to know some private key." (emphasis mine) and later even more: "Certainly not the key from the bitcoin addresses, otherwise it would have been stolen long ago." and also "There's a hint on the main page at the bottom."
More answers to questions than I even asked. But as I already got this far, it would've been a pity not to solve this one.
Ok, with this knowledge, the rest was easy then:
We just needed an address that starts with "1Hax4B". Ok, so let's do use this vanity tool:
vanity tool, showing created private key
So with this newly created private key I went to the signing tool:
successful signing with new private key
First I thought that as the Challenge seemed to be a hex-string and not a message, I might need some binary signing tool. But let's try anyway:
Login successful
So yes, this worked! No binary tool needed. And here's the flag too:
flag
and the flag INS{v4n1ty_k1ll3d_th3_c4t} worked:
flag accepted

Rule58

This one was marked as a crypto challenge, so I thought crypto is interesting and usually not too complicated, so let's try it. This is the challenge:
Rule58 task
The link there provides a ZIP file that you can now download here as base64. In the ZIP you find three encoded files (hint.gif, rule86.txt, super_cipher.py) plus the decrypted rule86.txt file. We know that it's a synchronous stream cipher and that the key has been reused. First I had to google a bit to find out that this is essentially something like a one-time pad where some random stream gets generated from some initialization vector and then XOR'd with the data. When you reuse the key, you can simply XOR the two encrypted messages against each other and you would get the two clear-text messages XOR'd against each other. But as we have one file once encrypted and once decrypted, we can even get the full key (or better, the stream). So I opened the two files in HxD, copied out the hex values and used an online tool to create the XOR. Then with this key, I applied this same key against the encrypted super_cipher.py.enc file and got just garbage. Back to Google.
After some trying around, it turned out that the online XOR tool was broken. Using another tool worked and with that I could decode the super_cipher.py.enc script. But unfortunately, only up to the length of the key I had and this file was larger. So I had only the beginning of this file. Luckily this file is the encryption algorithm, so we can learn a lot from it. I did the same with the beginning of the image file, but I couldn't get anything useful from it. Even repairing the image didn't help. So we had only the start of the Python script. The beginning looked like this:
partial Python code
It turned out that my Python knowledge was not sufficient. I didn't know what range means, what // means, what int.from_bytes does, what str.encode() does, what "& 1 for i in" does, what RULE = [..] is, and so on, so I had to spend quite some time on this first. I do know a lot of other languages though, but Python is a bit special. Also I only had Python 2.7 installed and the code above requires Python 3.x. And Python has no debugger, so debug with print statements like 30 years ago (unless I'm missing something).
Anyway, after some analysis, I found that it's a simple algorithm in the next(x) calculation: First, x gets its bits shifted around. All bits 0...255 are shifted one bit to the left. This fills up bit 256 and leaves bit 0 empty. The old bit 0 also goes to bit 257 and the bits 255, 256, 257 go to bits 0, 1, 2. So the bits 0, 1 get filled twice (or statement). This is what the first statement does. I've drawn this down, but it's not that important. The only important thing here is that the bits get shuffled around.
Next thing to notice that RULE=[86...] can be simplified to RULE=[0,1,1,0,1,0,1,0].
Next in our next(x) function is that we create a new variable y that we fill up with an or statement, looping from bits 0 to 255. The bit will be set depending on this table:
logic table
Where i means that when this bit from x is set, then this bit from y will get set. The destination bit in y therefore depends on the same bit in x, but also on the two next higher bits.
This logic table is implemented by getting the three bits from x, shifting them to bits 0...2 and ANDing them with 7. Then we have the three input bits. We use this as index to the array RULE and look up the result. Then we shift it back to the original place and OR it into y.

So how do we decrypt further data if the files are longer? First I thought that the big number (256 bits) is only used for holding the state and only the lowest 8 bits return the next value. But after trying around with this (and finding the correct endianness), it turns out that the entire large 256-bit number is used as the next 256/8=32 bytes. When doing this correctly, the next row of the existing key could be reproduced. Therefore, I could just continue this calculation and find out the key stream that is required to decrypt the files completely.
This worked nicely and we now have the full Python script, but we already know how it works, so we didn't really need it. But what is in the hint.gif image?
hint.gif
This is the hint.gif image. So the hint is that the IV (the entered password) is the flag. This means we not only have to decrypt the data, but also find out the password, which was used to initialize the first line for the key stream.
In order to get that, we would have to calculate backwards line by line. But there is no easy way to do this, as every bit set depends on three bits and there's no way doing this in reverse.
I also asked the organizers if the idea is to brute-force the password. The answer was "I don't think you can brute-force the password." My idea was actually to do some character-wise trying and see if the first known line of the stream matches partially. As every three bits go into one bit in the output, I assumed that the result of changing input would be very local distributed, even if we do this 128 times (the bootstrap loop). So I wrote a brute-forcer. From the image above, I concluded that it must be exactly 29 characters in length. So the brute-forcer takes a random character position, tries out all possible characters, and lists the ones that result in the best matching end-result. With best-matching I mean where the number of bits matching the target value is the highest. All possible characters go into an array and we'll just take a random character from that array and replace the character at our position to improve the number of bits matching. Running this for a while, it turned out that after some time, no improvements were made anymore. I thought that maybe the reason for that is that adjacent characters need to be improved always together, because otherwise we might miss some bits. So I rewrote this to try all 2-char combinations for all random positions and do this again. Trying all 2-char combination significantly slowed down the process, but it was still usable. I optimized it a bit by not trying to change the areas of the flag that were known and also to not randomly chose the position, but to go through all positions repeatedly. It turned out that after half an hour running or so, no more improvements were made and the number of correct bits were stuck at 180 or so (from 256 max). The found "best" characters for each position was always only one combination, so it couldn't find anything. Lost approach. Here's my brute-forcer, in case you want to play around with it.

So the only logical way was to use a SAT solver. I've never used that before, so I had to leave my comfort zone and learn something new. I actually didn't go down the rabbit hole and started with Microsoft's Z3 with Python bindings. Z3 is an SMT solver, so you are not limited to binary, but can do more things. In our case bit fields would have been enough, but this SMT solver has a lot of usability advantages. I had to read a lot and actually it was already past the CTF end, but I really wanted to solve this one. The main goal of participating here was to learn something.
I wrote this code, which works fine when doing one level reverse stepping. Unfortunately, it returns three results for the value I used. The reason there is that the bit vector for the "next" function has to be not only 256 bits, but two more. If we define the bit vector with a size of 258 bits, it works, but it returns three possible results and we don't know which one to use. If we just define 256 bits, then the "next" function doesn't work correctly.
So I thought if it does work for 1 step backwards, then it might also work for going back the required 128 steps too, it might just take a bit longer. I went sleeping and the next morning it was still running. I left it running the entire day while being at work, but still no result on Monday evening.
So I thought I need to improve the single-stepping backwards and just repeat that 128 times. I could get rid of the additional results by adding another condition, defining that the bits 256 and 257 have to be 0. That helped not only to give only one result, but also to speed it up. Now I just needed to add a loop 128 times and it was working. Here's my code:
final code
You might notice that I removed the array-lookup, which didn't work with Z3. I replaced the array-lookup from the logic-table with the expression res=i0^(i1|i2). That's much nicer anyway.

And this is the result:
result of final code
So here we have it, our final flag: INS{Rule86_is_W0lfr4m_Cha0s}
Unfortunately, it's much too late for the CTF, but anyway, I learned a lot.

Comments

Popular posts from this blog

Capture The Flag Challenges from Cyber Security Base with F-Secure 2017/2018

Capture The Flag Challenges from Cyber Security Base with F-Secure