Rating: 5.0
The following source code is provided in the task description :
```python
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 :
```javascript
{'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](https://crypto.stackexchange.com/questions/30407/alter-plaintext-in-block-cipher-aes-in-cbc-mode)
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](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#CBC)
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](decryption)'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.
```python
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}`
thx what a good WU