Jean Sébastien Bash

INS'HACK 2019 - Cryptography (182 pts).

INS’HACK 2019: Jean Sébastien Bash

Challenge details

Event Challenge Category Points Solves
INS’HACK 2019 Jean Sébastien Bash Cryptography 182 21

Description

I’ve found a revolutionary way to securely expose my server!

ssh -i <your_keyfile> -p 2227 user@jean-sebastien-bash.ctf.insecurity-insa.fr To find your keyfile, look into your profile on this website.

TL;DR

Use the decryption oracle to decrypt a random valid ciphertext. Modify the first block to produce a valid command in the second block.

Solution

We have to connect to the challenge using our private key. Then we are getting an interface where we can potentially execute commands.

___           _   _            _      ____   ___  _  ___
|_ _|_ __  ___| | | | __ _  ___| | __ |___ \ / _ \/ |/ _ \
| || '_ \/ __| |_| |/ _` |/ __| |/ /   __) | | | | | (_) |
| || | | \__ \  _  | (_| | (__|   <   / __/| |_| | |\__, |
|___|_| |_|___/_| |_|\__,_|\___|_|\_\ |_____|\___/|_|  /_/

===========================================================

     You are accessing a sandbox challenge over SSH
       This sandbox will be killed soon enough.
      Please wait while we launch your sandbox...

===========================================================
Welcome on my server. /help for help  

>/help
This is a tool so that only me can execute commands on my server
(without all the GNU/Linux mess around users and rights).

- /help for help
- /exit to quit
- /cmd <encrypted> to execute a command

Notes (TODO REMOVE THAT) ---------------------------
Ex:
/cmd AES(key, CBC, iv).encrypt(my_command)
/cmd 7bcfab368dc137d4628dcf45d41f8885

Thanks to the help command we know that AES CBC is used to encrypt the commands. Let’s see what the example does.

>/cmd 7bcfab368dc137d4628dcf45d41f8885
Running b'ls -l'
total 8
-rw-r--r-- 1 root root   21 Apr 25 21:18 flag.txt
-rwxr-xr-x 1 root root 2066 Apr 25 21:50 server.py

The server is kind enough to print out the decrypted content, at least after striping padding. That’s very useful and it’s the only thing we’ll be using to solve the challenge. Forget about the given example command, it’s useless. Also note that the key and iv doesn’t change upon reconnection to the service.

We will craft our command by manipulating the first cipher block to produce the desired output in the second block. A third block will be used as padding. For that we need to write a small script but that’s the most complicated part !

Pwntools’ ssh() function doesn’t work correctly because the prompt is not /bin/sh but a custom script. We used process() instead :

conn = process(["ssh","-t","-i", "key", "-p", "2227", "user@jean-sebastien-bash.ctf.insecurity-insa.fr"])

Sadly this is still not sufficient, the service throws an error message and exists :

the input device is not a TTY

Thankfully we can use a PTY for stdin, by default a PIPE is used :

conn = process(["ssh","-t","-i", "key", "-p", "2227", "user@jean-sebastien-bash.ctf.insecurity-insa.fr"], stdin=PTY)

To make things simple, we’ll use 3 blocks of null bytes. We just need to search for a valid third block by changing one of it’s bytes :

from pwn import *

def test(m):
    conn.sendline("/cmd "+m.encode("hex"))
    data = conn.recvuntil(">", drop=True, timeout=1)
    return data

conn = process(["ssh","-t","-i", "key", "-p", "2227", "user@jean-sebastien-bash.ctf.insecurity-insa.fr"], stdin=PTY)
conn.recvuntil(">")

for i in range(256):
    r = test("\x00"*47+chr(i))
    if r == '' or 'Running' in r:
        print r

conn.close()

And we find :

/cmd 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000055
Running b'\x0e6u4\xae\xa0\x9d\'8\xf1B\x8b\x11\x87\xc3\xe1/lDr\x95\x8d\xad~S\xa1\'\xe8-\xe1\x8c\xec\x93\xd6{\xb0\x15\x8c\xbbU\xba!{"\xe4gr'
sh: 1: Syntax error: Unterminated quoted string

Thanks to the server, we know our decrypted output and thus the second block of plaintext :

out = '\x0e6u4\xae\xa0\x9d\'8\xf1B\x8b\x11\x87\xc3\xe1/lDr\x95\x8d\xad~S\xa1\'\xe8-\xe1\x8c\xec\x93\xd6{\xb0\x15\x8c\xbbU\xba!{"\xe4gr'[16:32]

In AES CBC, when decrypting the first block, the result is xored with the iv (which we don’t know). But for the second block, it’s the first encrypted block that is used as an iv and we control it. It’s just like a bit flipping attack.

We know how to craft a second block, we can now include valid commands separated from the garbage with newlines (semicolons also work). Also, sometimes you have to add random garbage after the command so the previous block changes because sometimes it contains characters that interfere with our command.

Let’s pop a shell !

out = '\x0e6u4\xae\xa0\x9d\'8\xf1B\x8b\x11\x87\xc3\xe1/lDr\x95\x8d\xad~S\xa1\'\xe8-\xe1\x8c\xec\x93\xd6{\xb0\x15\x8c\xbbU\xba!{"\xe4gr'[16:32]
out = strxor(out, "bash\n".rjust(16, '\n'))
cmd = out + '\x00'*31 + chr(0x55)
print cmd.encode('hex')

This gives us the following command :

25664e789f87a77459ab2d8a4c92e4e60000000000000000000000000000000000000000000000000000000000000055

Time to get the flag :

>/cmd 25664e789f87a77459ab2d8a4c92e4e60000000000000000000000000000000000000000000000000000000000000055
Running b'\n\x8f\x11MY\x9c\x19\xbb\xa5\xbc\xeb\xe8\xeaZ9_\n\n\n\n\n\n\n\n\n\n\nbash\n\x93\xd6{\xb0\x15\x8c\xbbU\xba!{"\xe4gr'
sh: 2: ?MY???????Z9_: not found
sandbox@a405e1f21f7d:/sandbox$ id
uid=1000(sandbox) gid=1000(sandbox) groups=1000(sandbox)
sandbox@a405e1f21f7d:/sandbox$ ls -la
total 16
drwxr-xr-x 1 root root 4096 Apr 25 21:53 .
drwxr-xr-x 1 root root 4096 May  5 10:39 ..
-rw-r--r-- 1 root root   21 Apr 25 21:18 flag.txt
-rwxr-xr-x 1 root root 2066 Apr 25 21:50 server.py
sandbox@a405e1f21f7d:/sandbox$ cat flag.txt
INSA{or4cle_P4dd1ng}
sandbox@a405e1f21f7d:/sandbox$ cat server.py
#!/bin/python3
import os
import random
import string
from Crypto.Cipher import AES
from binascii import hexlify, unhexlify
import signal

random.seed(';384lg;pf')
IV = ''.join(random.choice(string.printable) for _ in range(16)).encode()
key = ''.join(random.choice(string.printable) for _ in range(32)).encode()

def handler(sig, frame):
    pass

signal.signal(signal.SIGTSTP, handler)

def pad(s):
    return s + (16 - len(s) % 16) * bytes([16 - len(s) % 16])

def unpad(s):
    p = ord(s[len(s)-1:])

    if s[-p:] == p * bytes([p]):
        return s[:-ord(s[len(s)-1:])]
    else:
        raise Exception("Bad padding")

def create_aes():
    return AES.new(key, AES.MODE_CBC, IV)

def encrypt(plain):
    return hexlify(create_aes().encrypt(pad(plain))).decode('utf-8')

def decrypt(enc):
    cipher = unhexlify(enc.encode('utf-8'))
    return unpad(create_aes().decrypt(cipher))

def main():
    instr_welcome = """Welcome on my server. /help for help  """

    instr_help = """This is a tool so that only me can execute commands on my server
(without all the GNU/Linux mess around users and rights).

- /help for help
- /exit to quit
- /cmd <encrypted> to execute a command

Notes (TODO REMOVE THAT) ---------------------------
Ex:
/cmd AES(key, CBC, iv).encrypt(my_command)
/cmd {}
""".format(encrypt(b'ls -l'))

    prompt="\n>"

    print(instr_welcome)
    while True:
        try:
            cmd = input(prompt).strip()
        except EOFError:
            exit(1)

        if cmd == '/exit':
            print("Good bye!")
            exit(0)
        elif cmd == '/help':
            print(instr_help)
        elif cmd.startswith('/cmd '):
            try:
                enc = cmd[4:].strip()
                bash_cmd = decrypt(enc)
                print("Running", bash_cmd)
                os.system(bash_cmd)
            except Exception:
                print("What do you mean?!")
        else:
            print("What do you mean?!")


if __name__ == '__main__':
    try:
        main()
    except:
        print("Bye.")
        exit(0)

Judging by the flag, the intended solution was to use padding oracle but I think in this case the decryption oracle is much simpler.

Flag

INSA{or4cle_P4dd1ng}

ENOENT