Imaginary CTF 2021 Writeup
This competition was hosted by the team over at Imaginary CTF and was a ton of fun! I went in with very little knowledge and I ended up impressed with how much I got done. The competition page can be found here.
I learned a ton of random nonsense over the course of the event and hopefully I can explain some of my solutions in a way that makes sense. Anyway, I’m going to quickly skip over the gimmie challenges and get into what I managed to solve.
Chicken Caesar Salad - Crypto - 50pts
qkbn{ePmv_lQL_kIMamZ_kQxpMZa_oMb_aW_pIZl}
The goal of this challenge was to de-obfuscate the flag which was put through the Caesar Shift Cipher. To do this I rammed it into an online decoder which brute forced it to figure out the correct shift.
Hidden - Forensics - 50pts
Oh no, someone hid my flag behind a giant red block! Please help me retrieve it!!
Like the flavour text says, the goal of this challenge was to retrieve the flag hidden behind a red block. The challenge supplied a single .psd file to find the flag in. Here I got a bit lucky, since I didn’t have photoshop on my VM, I just opened it up in an online photo editor which supported .psd files. In hindsight, this would have also been an easy thing to pickup with a utility like strings.
Roos World - Web - 50pts
Somebody hid Roo's flag on his website. Roo really needs some help.
For this challenge we are given a simple website and need to find a flag hidden on it. I started by checking the source code which would have been easy, but, there was only a message saying that “that would be too easy”. Next I opened up the developer tools on my browser and bumped into the flag hiding in the console.
Build-A-Website - Web - 100pts
I made a website where y'all can create your own websites! Should be considerably secure even though I'm a bit rusty with Flask.
This challenge gives us both a website and some python code which it runs on. The basic functionality of the website is to build a website from user supplied html and text.
app = Flask(__name__)
@app.route('/')
def index():
# i dont remember how to return a string in flask so
# here goes nothing :rooNervous:
return render_template_string(open('templates/index.html').read())
@app.route('/backend')
def backend():
website_b64 = b64encode(request.args['content'].encode())
return redirect(url_for('site', content=website_b64))
@app.route('/site')
def site():
content = b64decode(request.args['content']).decode()
#prevent xss
blacklist = ['script', 'iframe', 'cookie', 'document', "las", "bas", "bal", ":roocursion:"] # no roocursion allowed
for word in blacklist:
if word in content:
# this should scare them away
content = "*** stack smashing detected ***: python3 terminated"
csp = '''<head>\n<meta http-equiv="Content-Security-Policy" content="default-src 'none'">\n</head>\n'''
return render_template_string(csp + content)
The comments in the code gave a little hint to what sort of vulnerability I needed to be looking for and after some googling I found Server Side Template Injections(SSTI). The basic idea here is that our data is being processed by a “template engine” which allows user supplied data to be input into a static template and adjust it. However, if this user data is put directly into the template then the user could potentially supply malicious template syntax that will be processed by the engine. The simplest proof of concept to check for SSTI is to input {{7*7}} and if the application displays 49 then it’s vulnerable.
The next step of this, at least for this challenge, was to use the primitive types and built-ins to read flag.txt.
I found a few resources which indicated I could use __class__
to find the class of an object (in this case I used
a string) in conjunction with __mro__
to list all classes it resolves. One of which, is the “object” class. This can
then be used in conjunction with __subclasses__()
to find a list of all objects.
However, back in the Python code, there is a blacklist which bans 3 peculiar things: “las”, “bas”, and “bal”. Which stops the use of keywords like class, subclasses, and global. Luckily, using some annoying concatenation this was easily avoidable. The equivalent injection was
The resulting list of objects was massive and it took some time to find the appropriate one to help me read flag.txt.
Finally, I came across <class '_frozen_importlib_external.FileLoader'>
which let’s me use a method called get_data()
.
The syntax for this is a bit wild but this resource
explained it very well for me. Injecting this final statement output the flag.
Flip Flops - Crypto - 100pts
Yesterday, Roo bought some new flip flops. Let's see how good at flopping you are.
As the title may suggest, this challenge was all about AES. This challenge included python code for a program that would output the flag if given an input that, when decrypted, contained the phrase “gimmeflag”. The program lets you encrypt your chosen phrase as long as it doesn’t have “gimmeflag” in it.
key = os.urandom(16)
iv = os.urandom(16)
flag = open("flag.txt").read().strip()
for _ in range(3):
print("Send me a string that when decrypted contains 'gimmeflag'.")
print("1. Encrypt")
print("2. Check")
choice = input("> ")
if choice == "1":
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = binascii.unhexlify(input("Enter your plaintext (in hex): "))
if b"gimmeflag" in pt:
print("I'm not making it *that* easy for you :kekw:")
else:
print(binascii.hexlify(cipher.encrypt(pad(pt, 16))).decode())
else:
cipher = AES.new(key, AES.MODE_CBC, iv)
ct = binascii.unhexlify(input("Enter ciphertext (in hex): "))
assert len(ct) % 16 == 0
if b"gimmeflag" in cipher.decrypt(ct):
print(flag)
else:
print("Bad")
print("Out of operations!")
At a glance, this appears impossible. However, the trick is the use of CBC mode in AES. This particular mode is vulnerable to a “bit flipping attack”. This attack is a result of how CBC operates.
Since CBC uses the previous block of cipher text to help decrypt the next block of plain text, changing one of the bits in first block of cipher text will affect the next block. This causes the decrypted message to be slightly different in that one specific spot. It is important to keep in mind that the first round of decryption results in garbled output. After learning this, the next step was to create a payload. Keeping in mind the blocks are 16 bytes each, I created this abomination.
ddddddddddddddddgimmeflafddddddd
With this payload, I needed to flip the bit in the first block of encrypted text at index 8. Since ‘f’ is 1 off from ‘g’, this was as simple as counting to the right index and adding or subtracting 1 from the value located there. For example:
9230994fb23be84d7dcc5da638b5f1ddde75d[...] - > 9230994fb23be84d7ccc5da638b5f1ddde75d9a8a[...]
^ ^
Original Index 8 New Index 8
Then all that’s left was to input the new cipher text and receive the flag.
Formatting - Misc - 100pts
Wait, I thought format strings were only in C???
The idea behind this one is right in the description. We are given python code which formats strings in a potentially unsafe way.
art = '''
88
,d 88
88 88
,adPPYba, MM88MMM ,adPPYba, 8b,dPPYba, 88 ,d8 ,adPPYba,
I8[ "" 88 a8" "8a 88P' `"8a 88 ,a8" I8[ ""
`"Y8ba, 88 8b d8 88 88 8888[ `"Y8ba,
aa ]8I 88, "8a, ,a8" 88 88 88`"Yba, aa ]8I
`"YbbdP"' "Y888 `"YbbdP"' 88 88 88 `Y8a `"YbbdP"'
'''
flag = open("flag.txt").read()
class stonkgenerator: # I heard object oriented programming is popular
def __init__(self):
pass
def __str__(self):
return "stonks"
def main():
print(art)
print("Welcome to Stonks as a Service!")
print("Enter any input, and we'll say it back to you with any '{a}' replaced with 'stonks'! Try it out!")
while True:
inp = input("> ")
print(inp.format(a=stonkgenerator()))
if __name__ == "__main__":
main()
First thing to note is flag.txt is opened in the file and read but the data is never printed to us. However, the variable is there, we just need to read it somehow. Going back to format strings, they are most famously known in C programming and are very exploitable if not written correctly. From what I learned, in python they are similarly broken, they just look a bit different. The focus here is exploiting this section of code
inp = input("> ")
print(inp.format(a=stonkgenerator()))
From here, in a similar way to Build-A-Website, I accessed global variables through the attributes of stonksgenerator
to break the format and get the flag.
stonksgenerator.__init__.__globals__[flag]
SaaS - Web - 100pts
Welcome to Sed as a Service! Now you can filter lorem ipsum to your heart's desire!
The challenge here involves sed, a utility to manipulate files, but set on a website! We are given both the site and the python code for the application that sends our demands to sed.
@app.route('/')
def index():
return render_template('index.html')
blacklist = ["flag", "cat", "|", "&", ";", "`", "$"]
@app.route('/backend')
def backend():
for word in blacklist:
if word in request.args['query']:
return "Stop hacking.\n"
return html.escape(os.popen(f"sed {request.args['query']} stuff.txt").read())
The code has a few important things in it, a blacklist, and the following line of code.
return html.escape(os.popen(f"sed {request.args['query']} stuff.txt").read())
This line is where all the magic happens. Our blacklist validated query is formatted into the middle of this os.popen() call and executed on the system. Next I did a bit of experimenting to try to inject into this query and found that printing two single (or double) quotes would print the whole lorem ipsum text found in stuff.txt.
'' stuff.txt #
The above query also has the same result. So this means it is very intuitively injectable. The only thing left to do was create a payload that evades the blacklist and prints from flag.txt instead. Concatenation or starting a new command wouldn’t work since ‘;’ is blacklisted. So the only remaining solution was wildcards. I constructed the following query to read the flag
'' ?lag.txt #
Here the question mark represents any single character to bash so it will search for an appropriate file and read it to me. Sure enough, the flag was printed out to by the application.
ictf{:roocu:roocu:roocu:roocu:roocu:roocursion:rsion:rsion:rsion:rsion:rsion:_473fc2d1}
Spelling Test - Misc - 100pts
I made a spelling test for you, but with a twist. There are several words in words.txt that are misspelled by one letter only.
Find the misspelled words, fix them, and find the letter that I changed. Put the changed letters together, and you get the flag.
This challenge is fairly simple and I chose the most straight-forward and most eye-straining way to solve this. First, I copied the words into a program like google docs and then manually scanned through the list for each misspelling and wrote down the offending letter. The only immediate problem with this method is that the spell check would pickup country/city names that weren’t capitalized (which was all of them) making picking out the right words a bit trickier. Capitalizing the first letter of every word would have solved this easily and made the job quicker.
Vacation - Forensics - 100pts
Roo's cousin was on vacation, but he forgot to tell us where he went! But he posted this image on his social media.
Could you track down his location?
This was another straight-forward challenge. We are given a single picture and need to find the location of the picture and submit the latitude and longitude as the flag. The first step was to take a look at the photo.
From the photo I picked up two important things, the photo was taken in the city of South Lake Tahoe, and there are some easily identifiable shops that we can look up. A quick google search and I had found the rock shop and the weed shop.
The final step was to get the right spot and jot down the latitude and longitude. I did this by going into street view and finding the closest spot to where the photo was taken as possible. Then the coordinates can be found in the url.
Awkward_Bypass - Web - 150pts
This blacklist is so awkward, it will make you wonder if you know how to spell...
I will immediately tell you that this one is an SQL one so, there. Anyway, this one is mostly about avoiding a blacklist with a lot of SQL keywords in it. We are given some python code and a website which runs it.
blacklist = ["ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER",
"ALWAYS", "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT",
"BEFORE", "BEGIN", "BETWEEN", "CASCADE", "CASE", "CAST", "CHECK",
"COLLATE", "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE",
"CROSS", "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP",
"DATABASE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH",
"DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE",
"EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FIRST", "FOLLOWING", "FOR",
"FOREIGN", "FROM", "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF",
[...]
def checkCreds(username, password):
con = sqlite3.connect('database.db')
cur = con.cursor()
for n in blacklist:
regex = re.compile(n, re.IGNORECASE)
username = regex.sub("", username)
for n in blacklist:
regex = re.compile(n, re.IGNORECASE)
password = regex.sub("", password)
print(f"SELECT * FROM users WHERE username='{username}' AND password='{password}'")
try:
content = cur.execute(f"SELECT * FROM users WHERE username='{username}' AND password='{password}'").fetchall()
except:
return False
cur.close()
con.close()
if content == []:
return False
else:
return True
@app.route('/')
def index():
return render_template("index.html")
@app.route('/user', methods=['POST'])
def user():
if request.method == 'POST':
username = request.values['username']
password = request.values['password']
if checkCreds(username, password) == True:
return render_template("user.html")
else:
return "Error"
else:
return render_template("user.html")
The code is fairly straight forward. The script takes in a username and password from our login request, runs them both through the blacklist removing the offending text, inputs the result into an sql query to determine if the right credentials were entered, and logs us in if we did. The query definitely looks vulnerable to SQL injection, the only problem is the blacklist. Luckily, the blacklist doesn’t recurse over the input so we can construct an injection like this
' oorr '1'='1' --
Then the blacklist removes the only ‘or’ it sees and sends the result off to the next step. Here is the statement after the blacklist does its job
' or '1'='1' --
Here is the resulting database query
SELECT * FROM users WHERE username='' or '1'='1' --
So it’s very possible and easy to log in to get the flag!
No, of course it wouldn’t be that easy. Clearly I needed to enumerate a username or password to find the flag. I did this through a blind SQL injection. Basically, if a query I made was successful (evaluated to true) I would see Roo’s mocking face and if it failed (evaluated to false) I would get an error screen. Here is the basic formula (keep in mind the black list evasion)
' oorr '1'='1' aandnd SUBSTR(paassswoorrd,1,1)>'a'--
First off, it’s important to note I was really lucky there was only a single user in the users table or else this would have been a lot more work. Anyway, going back to the query at hand, I set up a SUBSTR statement that checks to see if the first letter in the password has a value greater than ‘a’. If it does then the statement is true and I log in, otherwise, an error pops up. Depending on that result, I check another value in the same way, slowly narrowing down what the actual character could be. Once I have the first character, I do the same process for the next character in the password.
' oorr '1'='1' aandnd SUBSTR(paassswoorrd,2,1)>'a'--
The process continues until I find every letter of the password. Which, in the end, was indeed the flag!
ictf{n1c3_fil73r_byp@ss_7130676d}
Cookie Stream - Web - 150pts
Cookie streaming service? Naaaaaaah. Password protected Rickroll as a Service? YAAAAAAAAAAAAAAAAAAAAAAAAAAAASSSSSSSSSSS!
For this challenge we are given another combo of python code and a website that uses it. The difference here is that this challenge has a crypto challenge vibe.
key = urandom(16)
cnonce = urandom(8)
users = {
'admin' : '240964a7a2f1b057b898ef33c187f2c2412aa4d849ac1a920774fd317000d33ebb8b0064834ed1f8a74763df4e95cd8c8be3a154b46929c3969ce323db69b81f',
'ImaginaryCTFUser' : '87197acc4657e9adcc2e4e24c77268fa5b95dea2867eacd493a0478a0c493420bfb2280c7e4e579a604e0a243f74a36a8931edf71b088add09537e54b11ce326',
'Eth007' : '444c67bb7d9d56580e0a2fd1ad00c535e465fc3ca9558e8333512fe65ff971a3dfb6b08f48ea4f91f8e8b55887ec3f0d7634a8df98e636a4134628c95a8f0ebf',
'just_a_normal_user' : 'b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86',
'firepwny' : '6adee5baa5ad468ac371d40771cf2e83e3033f91076f158d2c8d5d7be299adfce15247067740edd428ef596006d6eaa843b36cc109618e0a1cae843b6eed5c29',
':roocursion:' : '7f5310d2675c09c1b274f7642bf4979b2ce642515551a7617d155033e77ecfd53dede33ee541adde2f1072739696d0138d1b2f90c9ecc596095fa43b759e9baa',
}
def check(username, password):
if username not in users.keys():
return False
if sha512(password.encode()).hexdigest() == users[username]:
return True
@app.route('/')
def index():
return render_template('login.html')
@app.route('/backend', methods=['GET', 'POST'])
def backend():
if request.method == 'POST':
if not check(request.form['username'], request.form['password']):
return 'Wrong username/password.'
resp = make_response(redirect('/home'))
nonce = urandom(8)
cipher = AES.new(key, AES.MODE_CTR, nonce=nonce) # my friend told me that cbc had some weird bit flipping attack? ctr sounds way cooler anyways
cookie = hexlify(nonce + cipher.encrypt(pad(request.form['username'].encode(), 16)))
resp.set_cookie('auth', cookie)
return resp
else:
return make_response(redirect('/home'))
@app.route('/home', methods=['GET'])
def home():
nonce = unhexlify(request.cookies.get('auth')[:16])
cipher = AES.new(key, AES.MODE_CTR, nonce=nonce)
username = unpad(cipher.decrypt(unhexlify(request.cookies.get('auth')[16:])), 16).decode()
if username == 'admin':
flag = open('flag.txt').read()
return render_template('fun.html', username=username, message=f'Your flag: {flag}')
else:
return render_template('fun.html', username=username, message='Only the admin user can view the flag.')
@app.errorhandler(Exception)
def handle_error(e):
return redirect('/')
In this script we have a lot to break down. First, there is a list of usernames and hashed passwords. Second, we see that this is another AES-based challenge except this time it uses CTR mode. Finally, we can determine the basic work flow. The application has two primary functions, check valid log ins, and manage user sessions through cookies. First, I needed to log in. Since I didn’t have a cookie, the only way to do that was to use one of provided sets of usernames and passwords. So, I booted up john and input the password hashes to see if any were weak enough to crack.
The results show that 3 sets of credentials are available ImaginaryCTFUser:idk, just_a_normal_user:password, and firepwny:pwned. I logged in as firepwny and got to see Rick Astley.
Unfortunately, only the admin can see the flag and his hash wasn’t weak. However, I did get a cookie to dissect as a runner-up prize. My next move was to examine how the cookies were made.
def backend():
if request.method == 'POST':
if not check(request.form['username'], request.form['password']):
return 'Wrong username/password.'
resp = make_response(redirect('/home'))
nonce = urandom(8)
cipher = AES.new(key, AES.MODE_CTR, nonce=nonce) # my friend told me that cbc had some weird bit flipping attack? ctr sounds way cooler anyways
cookie = hexlify(nonce + cipher.encrypt(pad(request.form['username'].encode(), 16)))
resp.set_cookie('auth', cookie)
return resp
else:
return make_response(redirect('/home'))
Once a user logs in, a cookie is created for them using AES with CTR mode. The process looks pretty standard, but, we get two pieces of information from it 1) the key AES uses to encrypt is the same each time, 2) the nonce is appended to the front of the cipher text so we know its value.
def home():
nonce = unhexlify(request.cookies.get('auth')[:16])
cipher = AES.new(key, AES.MODE_CTR, nonce=nonce)
username = unpad(cipher.decrypt(unhexlify(request.cookies.get('auth')[16:])), 16).decode()
if username == 'admin':
flag = open('flag.txt').read()
return render_template('fun.html', username=username, message=f'Your flag: {flag}')
else:
return render_template('fun.html', username=username, message='Only the admin user can view the flag.')
This code segment shows how authentication works for the homepage. First, the nonce is stripped from the front of the cookie. Second, the cipher is created from the key and the nonce. Third, the username is decrypted and the padding is removed. Finally, the username is used to determine whose session it was and if it’s admin’s session it prints the flag.
It’s important to note how CTR Mode works. In essence, the nonce is concatenated with a counter and then black magic is used to combine them with a key. The result of the black magic is then XOR’d with the plaintext to get the ciphertext. For all the math people here it is boiled down to an equation
Here C is the ciphertext, P is the plaintext and F(k,n) represents the black magic that AES does to combine the nonce, the key, and the counter. In this situation, we have values for 3 of the 4 variables, C, P, and n. Since, we don’t know the value of the key k and the nonce is random every time, we shouldn’t be able to do anything here. However due to the way the script works, there is no cookie expiry and therefore, the nonce can be forced to be a value we know. This means we can force F(k,n) to essentially be the same value every time.
Our equation becomes way easier with Y being some constant value and we can even manipulate it a bit using XOR rules.
With this, we can use Y to make our own brand new ciphertext and trick the cookie system into thinking we are the admin. First, I grabbed the cookie from earlier and did some surgery on it.
a23d98c011b41eab3b0e8375b7b6ecfc52af4f92105485a2
nonce : a23d98c011b41eab
remainder: 3b0e8375b7b6ecfc52af4f92105485a2
I removed the nonce from the front so I can do some XORing and find Y and converted “admin” to hex.
cookie = hexlify(nonce + cipher.encrypt(pad(request.form['username'].encode(), 16)))
[...]
username = unpad(cipher.decrypt(unhexlify(request.cookies.get('auth')[16:])), 16).decode()
Now another small point about this whole process is that before the username is encrypted, it is padded with some bytes. The way pad(input, number) works is that it will pad the input up with bytes until it is number in length. The byte value used to pad the input is chosen based on how many bytes it needs to pad. So in essence,
pad('admin', 16) = admin\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b
hex('admin')+padding = 61646d696e0b0b0b0b0b0b0b0b0b0b0b
\x0b is chosen because admin needs to be padded with 11 bytes to be 16 bytes in length. With that out of the way, the next step is to XOR our original cookie with its translated plain text.
'firepwny' = 6669726570776e79
'firepwny'+ padding = 6669726570776e790808080808080808
Y = 6669726570776e790808080808080808 XOR 3b0e8375b7b6ecfc52af4f92105485a2
Y = 5d67f110c7c182855aa7479a185c8daa
Next to get the desired plaintext stitched into the cookie I did another XOR
New Ciphertext = 5d67f110c7c182855aa7479a185c8daa XOR 61646d696e0b0b0b0b0b0b0b0b0b0b0b
New Ciphertext = 3c039c79a9ca898e51ac4c91135786a1
Now I re-added the nonce to the front to force the script to use it for decryption
New Cookie = a23d98c011b41eaba23d98c011b41eab3c039c79a9ca898e51ac4c91135786a1
Finally, I changed the cookie in my browser to this new cookie and refreshed the homepage.
Lines - Crypto - 150pts
Try to crack my unbreakable™ encryption! I based it off of the Diffie-Helman key exchange!
Lines was all about modulo and modular arithmetic. We are given a python script and the output from the program.
from Crypto.Util.number import bytes_to_long
import random
flag = bytes_to_long(open("flag.txt", "rb").read())
msg = bytes_to_long(b":roocursion:")
p = 82820875767540480278499859101602250644399117699549694231796720388646919033627
g = 2
a = random.randint(0, p)
b = random.randint(0, p)
s = pow(pow(g, a, p), b, p)
def encrypt(msg):
return (s*msg) % p
print(f"{p = }")
print(f"{encrypt(flag) = }")
print(f"{encrypt(msg) = }")
p = 82820875767540480278499859101602250644399117699549694231796720388646919033627
encrypt(flag) = 26128737736971786465707543446495988011066430691718096828312365072463804029545
encrypt(msg) = 15673067813634207159976639166112349879086089811595176161282638541391245739514
The script loosely follows the Diffie-Hellman key exchange as suggested by the intro text. So to get the basics I looked up some equations and some explanations of the algorithm. I won’t go too deep into it but, the gist of it is that we have two secret components a and b, a big number p, and a generator g. In the case of this code, we use them to get the following equation
The next step the code takes is to use s to modify the plaintext message
Here c is the resulting ciphertext. From all of this, we know the values of g, p, c, and we are given a sample message, “:roocursion:”. The only unknown here is s. That means the goal here was to figure out s and then use it somehow to decode the flag. Again it boiled down to 2 equations
From my research, I learned I could find an inverse to msg_1 such that,
The hope with this, was that I could use the inverse to extract s from the ciphertext. Now after a bunch more searching and trial and error, I stumbled into a solution. I was looking at ELGamal encryption and found the equations
At this point, I figured it was worth a shot so I punched in an adjustment to it.
In the end this worked and I am not quite sure why. I assume its something to do with linear congruence but I’m definitely holding out for a writeup to explain the math to me. Anyway, the way I continued with my experiment was by finding the inverse of s and then using it to decode c_2.
Punching the values in results in the flag for this challenge.
ictf{m0d_4r1th_ftw_1c963241}
Short Story - Forensics - 150pts
Do you like my short story? I feel like it's already too long...
I found this challenge really interesting, but also fairly simple. To solve it I just needed the right idea. First off, we are given a story that is pretty much just nonsense and some big ol’ bookwords. However, the story itself isn’t important what is important is the words.
farmers quantities beneath that stranger stood looking glasses finished supernatural still in summer indiscriminately
befallen tens replied cultivate rear i consider his quadrant white silent undiscriminating scornful tone conveyed
which case i happened she screamed partiality follow apprehensiveness captain langsdorff touching that swayed apprehensiveness
[...]
To analyze this, I started off by looking at the specific words. How many times each word occurred, the starting letter of each word, the final letter of each word, and the length of each word. Most of these weren’t worthwhile but what I did notice was that every word in the story is no more than 16 letters long. With this being a computer competition this was no coincidence. As such, each word translated to a single hexadecimal character, which in turn, paired up and translated to an ascii character. Revealing the flag.
ictf{A_sh0rt_st0ry_is_4_piece_of_pr0s3_f1ct10n_that_typ1cally_can_b3_read[...]}
No Thoughts, Head Empty - Reversing - 200pts
When I was making Roolang, of course I took a look at the mother of all esolangs! So, have some bf code.
For this challenge, I got luckier than lucky. First off, we are given some brainfuck code that we need to interpret. The basics idea of the code is that it prints the flag one letter at a time. However, the catch is that it prints each letter double as many times as the previous.
My “analysis” started with just removing random bits - since I had no hope of ever understanding brainfuck. Luckily for me, and possibly frustratingly for people who solved this properly, deleting random bits caused the interpreter to pop out the flag for me. Another challenged well solved.
Prisoner’s Dilemma - Misc - 200pts
So you thought https://stackoverflow.com/questions/11828270/how-do-i-exit-the-vim-editor could help you? Think again...
The basic premise of this challenge was that you are trapped in VIM with none of the usual escape routes.
From the first line of the file we can gather what happened and why none of the usual commands work. To sum it all up, ‘:’ is unmapped so no using the command mode, ‘Q’ is unmapped so no funny stuff with ‘ZQ’, ‘!’ is unmapped so no ‘!!’ trickery, and finally quit kicks you out of the ssh connection to the challenge machine. This challenge took quite a bit of trial and error and research on my end. I tried a bunch of commands that would be obscure to a lot of users until I came across this cheatsheet A ways down the page, I learned about vim registers. Registers allow for all sorts of read and write commands, but, more importantly give me a way to enter system commands with the expression register.
CTRL+r =
This command invokes the expression register and allows me to enter system commands to “break out” of the file and find the flag. While I don’t necessarily break out I do find the flag through.
CTRL+r = system("ls")
CTRL+r = system("cat 0696b44f21ad9d1f.ext
Both commands neatly print their output to the document and I get the flag without breaking out.
Conclusion
Overall, the competition was a great experience. I learned about web exploits, crypto, and even more stuff from challenges I couldn’t fully complete. I am looking forward to reading other people’s writeups to find out how to solve other challenges, maybe use them to work my way into finally doing some pwn or reversing. Thank you to all the challenge creators and Imaginary CTF staff for working on and hosting this event.
Lessons Learned
- Server Side Template Injections
- CBC Bit Flipping Attacks
- Python Format String Vulnerabilities
- Command Injection with Blacklist Avoidance
- SQLi with Blacklist Avoidance
- Cookie Manipulation
- AES CTR Mode Weaknesses
- Diffie-Hellman Key Exchange
- Modular Arithmetic
- Advanced Vim Commands