CSAW CTF 2020 QUALS | Crypto

All Crypto tasks for CSAW 2020 QUALS

I played with Fword Team in CSAW CTF Qualification Round 2020 and managed to solve all the crypto tasks 🥳

To be honnest, I think the tasks were poorly designed and guessy. In this article, I’ll provide writeups for:

The first task is just XORing two images together so I’ll pass on that



  1. Length of keys is 25, which (cleaned-up) ramblings would fit that?
  2. If near-perfect ramblings are used to encrypt in order, how should it be decrypted?
  3. The guard want’s a legible message they can read in!

From the challenge’s title we can guess that this is going to be about [bifid cipher](https://en.wikipedia.org/wiki/Bifid_cipher). We will need to decode the message give: snbwmuotwodwvcywfgmruotoozaiwghlabvuzmfobhtywftopmtawyhifqgtsiowetrksrzgrztkfctxnrswnhxshylyehtatssukfvsnztyzlopsv using a key from the ramblings given:

Mr. Jock, TV quiz PhD., bags few lynx
Two driven jocks help fax my big quiz.
Jock nymphs waqf drug vex blitz
Fickle jinx bog dwarves spy math quiz.
Crwth vox zaps qi gym fjeld bunk
Public junk dwarves hug my quartz fox.
Quick fox jumps nightly above wizard.
Hm, fjord waltz, cinq busk, pyx veg
phav fyx bugs tonq milk JZD CREW
Woven silk pyjamas exchanged for blue quartz.
The quick onyx goblin jumps over the lazy dwarf.
Foxy diva Jennifer Lopez wasn’t baking my quiche.
he said 'bcfgjklmnopqrtuvwxyz'
Jen Q. Vahl: bidgum@krw.cfpost.xyz
Brawny gods just flocked up to quiz and vex him.
Emily Q Jung-Schwarzkopf XTV, B.D.
My girl wove six dozen plaid jackets before she quit.
John 'Fez' Camrws Putyx. IG: @kqBlvd
Q-Tip for SUV + NZ Xylem + DC Bag + HW?....JK!
Jumbling vext frowzy hacks pdq
Jim quickly realized that the beautiful gowns are expensive.
J.Q. Vandz struck my big fox whelp
How razorback-jumping frogs can level six piqued gymnasts!
Lumpy Dr. AbcGQVZ jinks fox thew
Fake bugs put in wax jonquils drive him crazy.
The jay, pig, fox, zebra, and my wolves quack!
hey i am nopqrstuvwxzbcdfgjkl
Quiz JV BMW lynx stock derp. Agh! F.
Pled big czar junks my VW Fox THQ
The big plump jowls of zany Dick Nixon quiver.
Waltz GB quick fjords vex nymph
Cozy lummox gives smart squid who asks for job pen.
Few black taxis drive up major roads on quiet hazy nights.
a quick brown fx jmps ve th lzy dg
Bored? Craving a pub quiz fix? Why, just come to the Royal Oak!

For bifid cipher, they key needs to be a perfect pangram with 25 chars long. A perfect pangram is a sentence that uses each letter of the alphabet only one time. We can see in our encrypted message that the letter ‘j’ isn’t used. That will help us deduce that key doesn’t have the letter ‘j’ . From the first hint, we have clean up the ramblings, so first of all we will remove every non-alphabetic char and the letter j. With every thing cleaned-up we will have 20 possible keys:


I’ve tried using each one of them but none worked. Then, I tried using all the keys inverted and successively and got the message.


Cleaning up that message we will have:

just some unnecessary text that holds absolutely no meaning whatsoever and bears no significance to you in any way

Sending that to the server gives us the flag.

from secretpy import Bifid
from secretpy import CryptMachine

def encdec(machine, enc):
    dec = machine.decrypt(enc)
    print (dec)
    return dec

encrypted = "snbwmuotwodwvcywfgmruotoozaiwghlabvuzmfobhtywftopmtawyhifqgtsiowetrksrzgrztkfctxnrswnhxshylyehtatssukfvsnztyzlopsv"
dict = open("plz","r").readlines()[::-1]
for k in dict:
	k = k.strip()
	cipher = Bifid()
	alphabet = []
	for i in k:		
	cm = CryptMachine(Bifid(), 5)
	encrypted = encdec(cm,encrypted)

Flag: flag{t0ld_y4_1t_w4s_3z}


Hint <200

Connecting to the nc service we are asked to find out if the block cipher used is ECB or CBC.

It’s easy to determine if the block cipher used is ECB or CBC from the ciphertext since we control the plaintext. Since ECB mode encrypts every 16 bytes block independently, two equal plaintext blocks shall result in two equal ciphertext blocks. The problem here is when all queries answered correctly the server doesn’t send the flag ☹ . So we have to store our answers and convert them to binary with ECB standing for 0 and CBC standing for 1.

from pwn import remote
from Crypto.Util.number import long_to_bytes

p = remote("crypto.chal.csaw.io",5001)
flag = ""
for _ in range(176):
	line = p.recvline()
	data = p.recvline().strip()
	if "Ciphertext" not in data:

	print p.recvline()
	data=data.replace("Ciphertext is:  ","")
	if data[:32] == data[32:64]:
		print data, "ecb"
		flag += "0"
		print data ," cbc"
		flag += "1"
print long_to_bytes(int(flag,2))

FLAG: flag{ECB_re@lly_sUck$}



#!/usr/bin/env python3
import struct
import hashlib
import base64
import flask

# flag that is to be returned once authenticated
FLAG = ":p"

# secret used to generate HMAC with
SECRET = ":p".encode()

app = flask.Flask(__name__)

@app.route("/", methods=["GET", "POST"])
def home():
    return """
This is a secure and private note-taking app sponsored by your favorite Nation-State.
For citizens' convenience, we offer to encrypt your notes with OUR own password! How awesome is that?
Just give us the ID that we generate for you, and we'll happily decrypt it back for you!

Unfortunately we have prohibited the use of frontend design in our intranet, so the only way you can interact with it is our API.


        Adds a new note and uses our Super Secure Cryptography to encrypt it.

        :author: your full government-issued legal name
        :note: the message body you want to include. We won't read it :)

        🆔 an ID protected by password  that you can use to retrieve and decrypt the note.
        :integrity: make sure you give this to validate your ID, Fraud is a high-level offense!

        View and decrypt the contents of a note stored on our government-sponsored servers.

        🆔 an ID that you can use to retrieve and decrypt the note.
        :integrity: make sure you give this to validate your ID, Fraud is a high-level offense!

        :message: the original unadultered message you stored on our service.

@app.route("/new", methods=["POST"])
def new():
    if flask.request.method == "POST":

        payload = flask.request.form.to_dict()
        if "author" not in payload.keys():
            return ">:(\n"
        if "note" not in payload.keys():
            return ">:(\n"

        if "admin" in payload.keys():
            return ">:(\n>:(\n"
        if "access_sensitive" in payload.keys():
            return ">:(\n>:(\n"

        info = {"admin": "False", "access_sensitive": "False" }
        info["entrynum"] = 783

        infostr = ""
        for pos, (key, val) in enumerate(info.items()):
            infostr += "{}={}".format(key, val)
            if pos != (len(info) - 1):
                infostr += "&"

        infostr = infostr.encode()

        identifier = base64.b64encode(infostr).decode()

        hasher = hashlib.sha1()
        hasher.update(SECRET + infostr)
        return "Successfully added {}:{}\n".format(identifier, hasher.hexdigest())

@app.route("/view", methods=["POST"])
def view():

    info = flask.request.form.to_dict()
    if "id" not in info.keys():
        return ">:(\n"
    if "integrity" not in info.keys():
        return ">:(\n"

    identifier = base64.b64decode(info["id"]).decode()
    checksum = info["integrity"]

    params = identifier.replace('&', ' ').split(" ")
    note_dict = { param.split("=")[0]: param.split("=")[1]  for param in params }

    encode = base64.b64decode(info["id"]).decode('unicode-escape').encode('ISO-8859-1')
    hasher = hashlib.sha1()
    print (encode)
    hasher.update(SECRET + encode)
    gen_checksum = hasher.hexdigest()

    print (checksum)
    print (gen_checksum)
    print (note_dict["entrynum"])

    if checksum != gen_checksum:
        return ">:(\n>:(\n>:(\n"

        entrynum = int(note_dict["entrynum"])
        if 0 <= entrynum <= 10:

            if (note_dict["admin"] not in [True, "True"]):
                return ">:(\n"
            if (note_dict["access_sensitive"] not in [True, "True"]):
                return ">:(\n"

            if (entrynum == 7):
                return "\nAuthor: admin\nNote: You disobeyed our rules, but here's the note: " + FLAG + "\n\n"
                return "Hmmmmm...."

            return """\nAuthor: {}
Note: {}\n\n""".format(note_dict["author"], note_dict["note"])

    except Exception:
        return ">:(\n"

if __name__ == "__main__":

In order to access the flag, we need turn admin=True & access_sensitive=True & entrynum=7.

The first two parameters can be easily exploited with url encoding:

The entrynum is set after the info update so we will use hash_extender to append entrynum=7 in our payload.

        info = {"admin": "False", "access_sensitive": "False" }
        info["entrynum"] = 783

Since we don’t know the secret length the server is using we need to brute force that until we get our flag! We will get a valid signature for secret length = 13 !

FLAG: flag{h4ck_th3_h4sh}


In this task we have 21 ciphertexts encrypted using this code:

#!/usr/bin/env python2

import os

import Crypto.Cipher.AES
import Crypto.Util.Counter

from Messager import send

KEY = os.environ['key']
IV = os.environ['iv']

secrets = open('/tmp/exfil.txt', 'r')

for pt in secrets:
    # initialize our counter
    ctr = Crypto.Util.Counter.new(128, initial_value=long(IV.encode("hex"), 16))

    # create our cipher
    cipher = Crypto.Cipher.AES.new(KEY, Crypto.Cipher.AES.MODE_CTR, counter=ctr)

    # encrypt the plaintext
    ciphertext = cipher.encrypt(pt)

    # send the ciphertext

If you don’t know how AES-CTR works here’s a good screenshot from wikipedia. It encypts the nonce (IV) & the counter for each block with the chosen key and xors the result with the plaintext. And the exact same thing happens in decryption! And that’s what makes AES-CTR a stream cipher. Having many ciphertexts, we only have to guess the key to xor them with to get valid plaintext! We can do this by guessing some possible chars (or words). For example we know that in english texts the most appearing char is the space. That will help is recover parts of the key, then we need to guess some gibberish words to complete our key! I have used a script from github with some modifications to do this. (script bellow) sending 4fb81eac0729a to the provided server will get us the flag.

import string
import collections
import sets, sys
from base64 import b64decode
from pwn import xor

for c in range(len(ciphers)):

# XORs two string
def strxor(a, b):     # xor two strings (trims the longer input)
    return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)])

# To store the final key
final_key = [None]*400
# To store the positions we know are broken
known_key_positions = set()

# For each ciphertext
for current_index, ciphertext in enumerate(ciphers):
	counter = collections.Counter()
	# for each other ciphertext
	for index, ciphertext2 in enumerate(ciphers):
		if current_index != index: # don't xor a ciphertext with itself
			for indexOfChar, char in enumerate(strxor(ciphertext, ciphertext2)): # Xor the two ciphertexts
				# If a character in the xored result is a alphanumeric character, it means there was probably a space character in one of the plaintexts (we don't know which one)
				if char in string.printable and char.isalpha(): counter[indexOfChar] += 1 # Increment the counter at this index
	knownSpaceIndexes = []

	# Loop through all positions where a space character was possible in the current_index cipher
	for ind, val in counter.items():
		# If a space was found at least 7 times at this index out of the 9 possible XORS, then the space character was likely from the current_index cipher!
		if val >= 7: knownSpaceIndexes.append(ind)
	#print knownSpaceIndexes # Shows all the positions where we now know the key!

	# Now Xor the current_index with spaces, and at the knownSpaceIndexes positions we get the key back!
	xor_with_spaces = xor(ciphertext,' ')
	for index in knownSpaceIndexes:
		# Store the key's value at the correct position
		final_key[index] = xor_with_spaces[index]
		# Record that we known the key at this position

# Construct a hex key from the currently known key, adding in '00' hex chars where we do not know (to make a complete hex string)
final_key_hex = ''.join([val if val is not None else '00' for val in final_key])

#After correcting words I would get

part= '\n'
for c in ciphers:
	#print xor(c,final_key_hex)

	output = strxor(target_cipher,final_key_hex)
	print "\nFix this sentence:"
	msg = ''.join([char if index in known_key_positions else '*' for index, char in enumerate(output)])
	print msg + "\n"
	part += msg +"\n"
	# To correct some words and get a valid key
	print final_key_hex.encode('hex')[:64]
	print xor(c,'t there\'s a difference ').encode('hex')

print part

FLAG: flag{m1ss1on_acc00mpl11shheedd!!}


Hint: The administrators are always taught that sharing is caring when first onboarded.

I have found this challenge really guessy and the admins only released the source code after a lot of time. 😩

We had a list of admins in database.txt:


and an encrypted “password” in encrypted.txt


The source code is long so I’ll only mention interesting parts.

First, the server is getting the password of the corresponding user and calculating a server_hmac

       xH = hasher(salt + str(pwd))
       v = modular_pow(g, xH, N)
       B = (k * v + modular_pow(g, b, N)) % N
       u = hasher(str(A) + str(B))
       S = modular_pow(A * modular_pow(v, u, N), b, N)
       K = hashlib.sha256(str(S).encode()).digest()
       flask.session["server_hmac"] = hmac_sha256(K, salt.encode())
       return flask.jsonify(nacl=salt, token2=B)

Then, to connect to the dashboard at /dash/<user> the server checks if the server_hmac equals the hmac provided.

@app.route("/dash/<user>", methods=["POST", "GET"])
def dashboard(user):
    if "hmac" not in flask.request.args:
        flask.flash("Error encountered on server-side.")
        return flask.redirect(flask.url_for("home"))

    hmac = flask.request.args["hmac"]
    servermac = flask.session.get("server_hmac", None)
    print(hmac, servermac)
    if hmac != servermac:
        flask.flash("Incorrect password.")
        return flask.redirect(flask.url_for("home"))

    pwd = DATABASE[user]
    return flask.render_template("dashboard.html", username=user, pwd=pwd)

Trying to login with the first user Jere, with any password we will get the token2 and salt and the session cookie: Using flask-usign we can retrieve back the server_hmac for Jere: With the server_hmac retrieved we can connect to Jere’s dahsbord using this link http://crypto.chal.csaw.io:5005/dash/Jere?hmac=c36cf89934ae475a5bcf1487d7b82ecd14e47e1e55742262087651dee1320e89 Once connected we can get Jere’s password 1:c4ee528d1e7d1931e512ff263297e25c:128 Retrieving all users password we get:

Jere 1:c4ee528d1e7d1931e512ff263297e25c:128
Loraine 3:7180fe06299e1774e0a18f48441efdaf:128
Ingrid 4:48359d52540614247337a5a1191034a7:128
Orlando 5:1fcd4a7279840854989b7ad086354b21:128
Berry 6:f69f8e4ecde704a140705927160751d1:128
Alton 7:b0ca40dc161b1baa61930b6b7c311c30:128
Bryan 8:04ed6f6bf5ec8c8c2a4d18dcce04ae48:128
Kathryn 9:430ad338b7b603d1770f94580f23cb38:128
Brigitte 10:d51669551515b6d31ce3510de343370f:128
Dannie 11:b303ee7908dcbc07b8e9dac7e925a417:128
Jo 12:3c4a692ad1b13e27886e2b4893f8d761:128
Leslie 13:a8e53ef9ee51cf682f621cb4ea0cb398:128
Adrian 14:feb294f9380c462807bb3ea0c7402e12:128
Autumn 15:9b2b15a72430189048dee8e9594c9885:128
Kellie 16:f4d52e11f6f9b2a4bfbe23526160fdfd:128
Alphonso 17:d0f902472175a3f2c47a88b3b3108bb2:128
Joel 18:cc29eb96af9c82ab0ba6263a6e5a3768:128
Alissa 19:913227d2d7e1a01b4ec52ff630053b73:128
Rubin 20:8669dd2b508c2a5dfd24945f8577bd62:128

From the hint shared The administrators are always taught that sharing is caring when first onboarded. or with a bit of guess we will use those passwords as sharings for Shamir’s secret sharing. 20 shares should be enough to retrieve back the secret.

Once that secret retrieved we will used it as a key to decrypt the encrypted.txt with CBC as AES mode, and 254dc5ae7bb063ceaf3c2da953386948 as the IV.

from Crypto.Cipher import AES
from binascii import unhexlify
from Crypto.Protocol.SecretSharing import Shamir

shares = ['c4ee528d1e7d1931e512ff263297e25c','4b58b8b5285d2e8642a983881ed28fc7','7180fe06299e1774e0a18f48441efdaf','48359d52540614247337a5a1191034a7','1fcd4a7279840854989b7ad086354b21','f69f8e4ecde704a140705927160751d1','b0ca40dc161b1baa61930b6b7c311c30','04ed6f6bf5ec8c8c2a4d18dcce04ae48','430ad338b7b603d1770f94580f23cb38','d51669551515b6d31ce3510de343370f','b303ee7908dcbc07b8e9dac7e925a417','3c4a692ad1b13e27886e2b4893f8d761','a8e53ef9ee51cf682f621cb4ea0cb398','feb294f9380c462807bb3ea0c7402e12','9b2b15a72430189048dee8e9594c9885','f4d52e11f6f9b2a4bfbe23526160fdfd','d0f902472175a3f2c47a88b3b3108bb2','cc29eb96af9c82ab0ba6263a6e5a3768','913227d2d7e1a01b4ec52ff630053b73','8669dd2b508c2a5dfd24945f8577bd62']

for i in range(20):

key = Shamir.combine(shares)
print ("secret: "+key)

IV= unhexlify("254dc5ae7bb063ceaf3c2da953386948")
enc= unhexlify("08589c6b40ab64c434064ec4be41c9089eefc599603bc7441898c2e8511d03f6")
cipher = AES.new(key,AES.MODE_CBC,IV)
print (cipher.decrypt(enc))

FLAG: flag{n0t_s0_s3cur3_4ft3r_4ll}

Networking and Telecommunications

Cyber Security Enthusiast