πWeb - HTB Proxy
Hack the Box Business CTF 2024 - Web - HTB Proxy
Last updated
Hack the Box Business CTF 2024 - Web - HTB Proxy
Last updated
This is a very short write-up of the HTB-Proxy web challenge, as it was another one of the interesting challenges the team and I completed.
Your team is tasked to penetrate the internal networks of a raider base in order to acquire explosives, scanning their ip ranges revealed only one alive host running their own custom implementation of an HTTP proxy, have you got enough wit to get the job done?
This challenge seemed pretty straight forward at first but as you progressed through it seemed to get more and more difficult.
I started off working on this challenge and routing my traffic through the proxy using curl. From reviewing the source code we can see there is a back end I wanted to reach that just as a first goal. This proved to be a lot tougher than I though though.
There are a series of checks each request through the proxy goes through, here they are in order:
Parses headers of the HTTP request
If POST request:
Parses body
Compares body length to Content-Length HTTP header
Checks if the HTTP version is 1.1
Checks if the request is for /
If so it returns /app/proxy/includes/index.html
Checks if the request is for /server-info
Checks if the request contains /flushInterface
If so return "Not Allowed"
Checks if the Host header is set
Checks if it is an IPv4 address or domain address using regex
Does checks on the host header looking at if its empty, checks fro port number, and checks if it is localhost using this function:
Checks if the request is malicious using this function:
And if it makes it though all these checks then it forwards all the bytes to the target host.
So now step 1 was to bypass the localhost checks so we could reach one of the two backend endpoints, /getAddresses. I managed to get by the first few checks after realizing that the /server-status endpoint gave me the hostname and I used <hostname>.local to try to route my requests. This ended up not working fully and I ended that working session a little defeated and went to work on some other challenges.
The following day, while I was still doing other challenges, some of my team was able to find a way around all the checks for localhost using https://nip.io/ and the internal IP gathered from /server-status.
After seeing this I knew that I had to finish out this challenge I had started. As my teammates went to bed, I am in a vastly different timezone as them at the moment, I hopped on and started working on completing the rest of the steps...
The next step was to reach the /flushInterface endpoint it seemed. This was the only endpoint on the backend that could accept user input and actually did stuff with it. The issue was you could reach out to it over the proxy because the url is being parsed to see if it contains flushInterface.
I tried URL encoding but it doesn't get decoded, capitalization didn't work because it uses toLower on the URL before checking, and no amount of random encoding seemed to work. I was stumped...
Until I noticed that the proxy was parsing the HTTP requests in a slightly weird way.
Reading through this you can see that it is parsing the body by just splitting the message into an array where \r\n\r\n is. In a normal HTTP request that is OK as the body is located after \r\n\r\n usually, but as an attacker we can use this to our advantage and attach a second request to the end and perform and HTTP request smuggling attack. This was easier said than done though as we had to now bypass a check on content-length to avoid the checkMaliciousBody function (which would ruin us because it checks for /n and /r).
After some messing around I found a way to successfully smuggle a request through and avoid the checks, you'll find it in the exploit script at the bottom.
Now we could reach the /flushInterface endpoint and send out data to it but it seemed to be just sending the user supplied interface to ipWrapper.addr.flush.
This wasn't anything custom or interesting... Unless you go to the package, ip-wrapper, on NPM and review the code.
From doing this I found that the interface is just being passed into an exec call raw, no filtering. Now we just had to exploit this command injection vulnerability.
This was straight forward and I escaped the command it was running using a semi-colon and then added my command with ${IFS} as spaces, as spaces were filtered out when validating the interface in this function:
My payload:
Testing this locally, and with more verbose output, you can see whats happening and the exploit chain working:
And with that I had the flag!