ふるつき

v(*'='*)v かに

RCTF 2019 writeup

RCTF 2019 has been held. We participated in it as team zer0pts and were the 59th place with 791 points. I'm writing up about the baby crypto challenge.

f:id:Furutsuki:20190520100734p:plain

[Crypto 400pts(Solved by 31 teams)] baby crypto

nc 45.76.208.70 20000 nc 207.148.68.109 20000

First, it was very similar to the decrypto challenge of BSidesSF 2019 CTF. Luckily I knew it, so I could get the second solve.

We were given the server side script file: crypto.py.

#!/usr/bin/python3 -u
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from hashlib import sha1
import binascii
import json
import os
import re
import sys

def pad(s):
    padder = padding.PKCS7(128).padder()
    return padder.update(s) + padder.finalize()

def unpad(s):
    unpadder = padding.PKCS7(128).unpadder()
    return unpadder.update(s) + unpadder.finalize()

key = os.urandom(16)
iv = os.urandom(16)
salt = key

iv_len = 16

print("Input username:")
username = sys.stdin.readline().strip()
if not re.match("^[a-z]{5,10}$", username):
    print("Invalid username")
    exit()

print("Input password:")
password = sys.stdin.readline().strip()
if not re.match("^[a-z]{5,10}$", password):
    print("Invalid password")
    exit()

cookie = b"admin:0;username:%s;password:%s" %(username.encode(), password.encode())

h = sha1()
h.update(salt)
h.update(cookie)
hv = h.digest()
hv_hex = h.hexdigest()
hash_len = len(hv)

cookie_padded = pad(cookie)

backend = default_backend()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
encryptor = cipher.encryptor()
cookie_padded_encrypted = encryptor.update(cookie_padded) + encryptor.finalize()
print("Your cookie:")
print(binascii.hexlify(iv).decode() + binascii.hexlify(cookie_padded_encrypted).decode() + hv_hex)

def is_valid_hash(cookie, hv):
    h = sha1()
    h.update(salt)
    h.update(cookie)
    return hv == h.digest()

while True:
    try:
        print("Input your cookie:")
        data_hex = sys.stdin.readline().strip()
        data = binascii.unhexlify(data_hex)
        assert(len(data) > iv_len + hash_len)
        iv, cookie_padded_encrypted, hv = data[:iv_len], data[iv_len: -hash_len], data[-hash_len:]
        cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
        decryptor = cipher.decryptor()
        cookie_padded = decryptor.update(cookie_padded_encrypted) + decryptor.finalize()
        try:
            cookie = unpad(cookie_padded)
        except Exception as e:
            print("Invalid padding")
            continue
        if not is_valid_hash(cookie, hv):
            print("Invalid hash")
            continue
        info = {}
        for _ in cookie.split(b";"):
            k, v = _.split(b":")
            info[k] = v
        if info[b"admin"] == b"1":
            with open("flag") as f:
                flag = f.read()
                print("Your flag: %s" %flag)
        else:
            print("Goodbye nobody")
        exit()
    except Exception as e:
        print("Invalid cookie %s" %e)

The application manages a login credential encrypted with AES-CBC PCKS#7. As we can get the decryption oracle, the padding oracle encryption attack works.

However, a salted SHA-1 hash value of the login credential was also used for checking the consistency of the credential value. It prevents us from a naive padding oracle attack.

How can we avoid the check? The length extension attack is also available since we know the length of the salt and the heading several bytes of the plaintext.

Thus, to get the flag, we just have to create a new login credential which is known_plaintext || paddings || ;admin:1, get its SHA-1 value by length extension attack, and then encrypt it using padding oracle encryption attack.

The following script does this.

from ptrlib import *
from binascii import hexlify, unhexlify

iv_len = 16
hash_len = 20
salt_len = 16

username = b"takoyaki"
password = b"hogepiyo"

sock = Socket("207.148.68.109", 20000)
# sock = Socket("localhost", 8765)
sock.recvuntil("Input username:\n")
sock.sendline(username)

sock.recvuntil("Input password:\n")
sock.sendline(password)


sock.recvuntil("Your cookie:\n")
hex_cookie = sock.recvline().decode().strip()
data = unhexlify(hex_cookie)
iv, cookie, checksum = data[:iv_len], data[iv_len:-hash_len], data[-hash_len:]
print("[+]cookie, checksum = {}, {}".format(cookie, checksum))


def oracle(x):
    sock.recvuntil("Input your cookie:\n")
    my_cookie = hexlify(x + checksum).decode()
    sock.sendline(my_cookie)
    result = sock.recvline().decode().strip()
    if "padding" in result:
        return False
    if "hash" in result:
        return True
    raise Exception(result)


def pad(x):
    l = (16 - len(x)) % 16
    return x + bytes([l] * l)


known_msg = b"admin:0;username:%s;password:%s" % (username, password)
append_msg = b";admin:1"

new_checksum, new_data = lenext(SHA1, salt_len, checksum, known_msg, append_msg)
attack_data = padding_oracle_encrypt(oracle, plain=pad(new_data), bs=16, unknown=b"A")

print("[+]new_cookie, new_checksum = {}, {}".format(repr(new_data), repr(new_checksum)))
print("[+]attack data = {}".format(hexlify(new_data)))

sock.recvuntil("Input your cookie:\n")
attack_cookie = hexlify(attack_data).decode() + new_checksum
sock.sendline(attack_cookie)

print(new_checksum)
print(sock.recvline())
print(sock.recvline())
print(sock.recvline())
print(sock.recvline())