Contact CTF writeups Notes

[PicoCTF 2018] - web - Secure Logon

This is one of my writeups for PicoCTF 2018

Problem

Uh oh, the login page is more secure... I think. http://2018shell3.picoctf.com:13747

hints:

There are versions of AES that really aren't secure.

Solution

The following source code is provided in the task description :

from flask import Flask, render_template, request, url_for, redirect, make_response, flash
import json
from hashlib import md5
from base64 import b64decode
from base64 import b64encode
from Crypto import Random
from Crypto.Cipher import AES

app = Flask(__name__)
app.secret_key = 'seed removed'
flag_value = 'flag removed'

BLOCK_SIZE = 16  # Bytes
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
                chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]


@app.route("/")
def main():
    return render_template('index.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.form['user'] == 'admin':
        message = "I'm sorry the admin password is super secure. You're not getting in that way."
        category = 'danger'
        flash(message, category)
        return render_template('index.html')
    resp = make_response(redirect("/flag"))

    cookie = {}
    cookie['password'] = request.form['password']
    cookie['username'] = request.form['user']
    cookie['admin'] = 0
    print(cookie)
    cookie_data = json.dumps(cookie, sort_keys=True)
    encrypted = AESCipher(app.secret_key).encrypt(cookie_data)
    print(encrypted)
    resp.set_cookie('cookie', encrypted)
    return resp

@app.route('/logout')
def logout():
    resp = make_response(redirect("/"))
    resp.set_cookie('cookie', '', expires=0)
    return resp

@app.route('/flag', methods=['GET'])
def flag():
  try:
      encrypted = request.cookies['cookie']
  except KeyError:
      flash("Error: Please log-in again.")
      return redirect(url_for('main'))
  data = AESCipher(app.secret_key).decrypt(encrypted)
  data = json.loads(data)

  try:
     check = data['admin']
  except KeyError:
     check = 0
  if check == 1:
      return render_template('flag.html', value=flag_value)
  flash("Success: You logged in! Not sure you'll be able to see the flag though.", "success")
  return render_template('not-flag.html', cookie=data)

class AESCipher:
    """
    Usage:
        c = AESCipher('password').encrypt('message')
        m = AESCipher('password').decrypt(c)
    Tested under Python 3 and PyCrypto 2.6.1.
    """

    def __init__(self, key):
        self.key = md5(key.encode('utf8')).hexdigest()

    def encrypt(self, raw):
        raw = pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + cipher.encrypt(raw))

    def decrypt(self, enc):
        enc = b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(enc[16:])).decode('utf8')

if __name__ == "__main__":
    app.run()

Now I didn't know much about cryptography before this task, but I learned a lot during PicoCTF 2018, starting with this task.

So first things first, we can login to the target app with any username/password. When we do, the cleartext value of our cookie is displayed :

{'admin': 0, 'username': 'iodbh', 'password': ''} 

The goal here is obviously to set the admin value to 1. But the cookie is encrypted, so we have to figure out a way to alter the ciphertext in a way that will lead it to decrypt to the desired value.

Looking at the source code and the app, here are the informations we have :

  • The Cipher is AES in CBC mode with a blocksize of 16
  • The IV is know (it is prepended to the ciphertext in the cookie)
  • We know the plaintext matching the ciphertext (it is returned by the app)

After a frenzied search engine querying session, I found this cryptography stack exchange question regarding a very similar problem, and skimming through the relevant wikipedia pages gave me a superficial understanding of the issue at stake.

The problem here is that we know the IV (Initialization Vector) and that we can alter it.

CBC Mode ?

Wikipedia has a helpful page on the topic.

CBC stands for "Cipher Block Chaining" and refers to a mode of encryption/decryption that works as follow (for decryption) :

  1. The message is split in "blocks" of equal size (the blocksize, here it's 16 bytes)
  2. Each block is decrypted
  3. The decrypted bytes are XOR'd with the previous block's ciphertext (the first block is XOR'd with the IV)

Since we control the IV, we can alter the resulting plaintext in the first block. Since we know the plaintext, a simple way to do that is to construct the new IV by XORing the known plaintext value with the desired plaintext value. That will create a masks that flips the necessary bits on decryption.

Quick and dirty script

After experimenting a bit, I ended up with the following python3 script. It's not pretty, but it does the job. I've added comments for this writeup.

from base64 import b64encode, b64decode
from sys import argv


def xor(a, b):
    """
    return a bytearray constructed by XORing a and b
    """
    out = bytearray()
    for i,c in enumerate(a):
        out.append(c ^ b[i])
    return bytes(out)


def construct_cookie(cookie_value):
    # current and desired plaintext, obtained by observing the cookie
    desired_plaintext = bytearray('{"admin": 0, "pa', 'utf8')
    current_plaintext = bytearray('{"admin": 1, "pa', 'utf8')
    # base64-decode the cookie value
    decoded_cookie = b64decode(cookie_value)
    # split the IV and the message
    original_iv = decoded_cookie[:16]
    # construct the mask for the first block
    desired_iv = xor(xor(desired_plaintext, current_plaintext), bytearray(original_iv))
    # prepend the new IV to the original ciphertext
    altered_ciphertext = desired_iv+decoded_cookie[16:]
    # base64-encode the cookie value
    cookie = b64encode(altered_ciphertext).decode("utf8")
    return cookie


if __name__ == '__main__':
    try:
        cookie = argv[1]
    except IndexError:
        print(f'usage: {argv[0]} [COOKIE]')
    print(construct_cookie(cookie))

Now, if we grab the original cookie value, pass it to this script and request the /flag path with the generated cookie, we get our flag : picoCTF{fl1p_4ll_th3_bit3_7d7c2296}