MCTF Final 2019 Writeup - Back to the Old Ways

  • Tuesday, Jul 30, 2019
blog-image

I played MCTF final 2019 with my team this week and wanted to share a writeup for the ‘Back to the old ways’ crypto challenge.

Challenge description

There is a group of hackers that we’ve been trying to take down for a while, we had a spy among them but his cover was compromised, but he managed to leak a piece of code running on their main socket, try to get admin access.

nc 52.59.5.86 1111

Objective: Get admin access

Solution

I first tried to use the public service

[youben@ym ~]$ ncat 52.59.5.86 1111
[1] register
[2] login
1
[+] name:
youben
[+] password:
password
[+] this is your hex-encoded cookie: 6625205b724457776f2e753f7c3463542a3f584254547d6a77493c0b09714c3a2761476358606a6e45653a5d
[1] register
[2] login
2
[+] cookie(in hex):
6625205b724457776f2e753f7c3463542a3f584254547d6a77493c0b09714c3a2761476358606a6e45653a5d
[-] access is for admins only
[1] register
[2] login

It seems like our information are stored in that cookie and are checked when we try to login to see if we have admin rights. So the goal is to construct a valid admin cookie. Let’s check the provided code

#all what u need is here

from string import printable as alpha

def encrypt(msg,key,iv):
    if len(msg) % 11 != 0:
        msg += alpha[len(msg) % 11] * (11 - len(msg) % 11)
    l = [msg[i:i+11] for i in range(0,len(msg),11)]
    ct = []
    for i in l:
        tmp = viginereEncrypt(i,iv)
        tmp = railFenceEncrypt(tmp,key)
        ct.append(tmp)
        iv = tmp
    return ''.join(ct)

def decrypt(msg,key,iv):
    l = [msg[i:i+11] for i in range(0,len(msg),11)]
    pt = []
    for i in l:
        tmp = railFenceDecrypt(i,key)
        tmp = viginereDecrypt(tmp,iv)
        pt.append(tmp)
        iv = i
    return ''.join(pt)

The encryption can be illustrated in the figure below

It’s using vigenere and railfence in CBC mode, the keyspace for railfence here is computable (only 10 possible keys), so if we know the last plaintext then we can just try decrypting the last ciphertext with the possible keys until we found the correct one that decrypts to our know plaintext, thanks to the padding for that, if len(plaintext) % 11 == 1 then it will be padded with 10 ‘1’, however, we must first understand how it’s constructing the plaintext cookie.

Trying different inputs, I discovered that a name and password of length 6 + 11k gives us a padding of 10 ‘1’, so we can now decrypt the last block with the 10 possible keys and check until we get those 10 ‘1’ and decrypt the whole cookie except the first block cause we don’t have the IV.

Decrypting the cookie gave me ‘](yv$zHX?B)&password=&admin=false&1111111111’, seems like we only need to replace that ‘false’ with a ‘true’ and reconstruct a valid cookie. You can check the complete code for doing that below

from string import printable as alpha
from binascii import unhexlify, hexlify


def encryptRailFence(text, key):
    rail = [['\n' for i in range(len(text))]
                  for j in range(key)]
    dir_down = False
    row, col = 0, 0

    for i in range(len(text)):
        if (row == 0) or (row == key - 1):
            dir_down = not dir_down

        rail[row][col] = text[i]
        col += 1
        if dir_down:
            row += 1
        else:
            row -= 1
    result = []
    for i in range(key):
        for j in range(len(text)):
            if rail[i][j] != '\n':
                result.append(rail[i][j])
    return("" . join(result))


def decryptRailFence(cipher, key):
    rail = [['\n' for i in range(len(cipher))]
                  for j in range(key)]
    dir_down = None
    row, col = 0, 0

    for i in range(len(cipher)):
        if row == 0:
            dir_down = True
        if row == key - 1:
            dir_down = False

        rail[row][col] = '*'
        col += 1

        if dir_down:
            row += 1
        else:
            row -= 1
    index = 0
    for i in range(key):
        for j in range(len(cipher)):
            if ((rail[i][j] == '*') and
               (index < len(cipher))):
                rail[i][j] = cipher[index]
                index += 1
    result = []
    row, col = 0, 0
    for i in range(len(cipher)):
        if row == 0:
            dir_down = True
        if row == key-1:
            dir_down = False
        if (rail[row][col] != '*'):
            result.append(rail[row][col])
            col += 1
        if dir_down:
            row += 1
        else:
            row -= 1
    return("".join(result))


def decryptVig(ciphertext, key):
    key_length = len(key)
    key_as_int = [alpha.find(k) for k in key]
    ciphertext_int = [alpha.find(c) for c in ciphertext]
    plaintext = ''
    for i in range(len(ciphertext_int)):
        p = (ciphertext_int[i] - key_as_int[i % key_length]) % len(alpha)
        plaintext += alpha[p]
    return plaintext


def encryptVig(plaintext, key):
    key_length = len(key)
    key_as_int = [alpha.find(k) for k in key]
    plaintext_int = [alpha.find(p) for p in plaintext]
    ciphertext = ''
    for i in range(len(plaintext_int)):
        c = (plaintext_int[i] + key_as_int[i % key_length]) % len(alpha)
        ciphertext += alpha[c]
    return ciphertext


def decrypt(msg,key,iv):
    l = [msg[i:i+11] for i in range(0,len(msg),11)]
    pt = []
    for i in l:
        tmp = decryptRailFence(i,key)
        tmp = decryptVig(tmp,iv)
        pt.append(tmp)
        iv = i
    return ''.join(pt)


if __name__ == '__main__':
    # name: 'aaaaaa'
    # password: ''
    expected_chars = '1' * 10
    cookie = "2a6f0c58475c0a7c6b3533434e395e29686b6978295c4d2176357e0d7a73530c0b66227736200b4174540c30"
    cipher = unhexlify(cookie).decode()
    ciphers = [cipher[i:i+11] for i in range(0, len(cipher), 11)]
    last_cipher = ciphers[-1]
    rf_key = -1
    for i in range(2, 11):
        p = decryptRailFence(last_cipher, i)
        p = decryptVig(p, ciphers[-2])
        if expected_chars in p:
            print("Found RF key: %d" % i)
            rf_key = i
    print(decrypt(cipher, rf_key, "0"))
    admin_val = "admin=true&"
    # construct the admin cookie
    cons = encryptVig(admin_val, ciphers[-3])
    cons = encryptRailFence(cons, rf_key)
    ciphers[-2] = cons
    # print admin cookie
    print(hexlify(''.join(ciphers[:-1]).encode()))

Finally, I only got a new cookie by registering, constructed a valid admin cookie from it, used it to login and get the flag :)

[youben@ym ~]$ ncat 52.59.5.86 1111
[1] register
[2] login
1
[+] name:
aaaaaa
[+] password:

[+] this is your hex-encoded cookie: 2a6f0c58475c0a7c6b3533434e395e29686b6978295c4d2176357e0d7a73530c0b66227736200b4174540c30
[1] register
[2] login
2
[+] cookie(in hex):
2a6f0c58475c0a7c6b3533434e395e29686b6978295c4d2176357e0d4e4a22515b
MCTF{mochi_mochi,CBC desu?!}
[1] register
[2] login


Thanks to th3jackers for the nice challenge.


comments powered by Disqus