H1-212 CTF - Writeup

"Hack your way to NYC this December for h1-212"

Posted by 0xacb on November 20, 2017 · 7 mins read


Hackerone launched the H1212 CTF challenge on November 13. I’m going to show how I solved it in this post. Thanks @jobertabma and @NahamSec for this awesome challenge! It was fun!

It seems that 25 ~50 hackers solved it:

Cool thing: As far as I know, 5 are portuguese 🇵🇹
Congrats @jllis, @prcabral, Luís Maia and @tvmpt! Well done!

The Writeup

An engineer of acme.org launched a new server for a new admin panel at He is completely confident that the server can’t be hacked. He added a tripwire that notifies him when the flag file is read. He also noticed that the default Apache page is still there, but according to him that’s intentional and doesn’t hurt anyone. Your goal? Read the flag!

I opened the link and there it is, the default Apache page!

Nothing special on this page… The 404 page gives us the following information: Apache/2.4.18 (Ubuntu) Server at Port 80. I tried the most common directories and files, such as “/admin”, with the “admin panel” reference from the challenge description in mind, but I couldn’t find anything. Then, I remembered to try “/flag”:

Well, not so easy! I tried DirBuster and dirsearch and I found “/index.html” and “/icons/”, but the icons directory is not interesting because it is enabled by default and I didn’t find any modified files.

I tried everything, I even installed Tripwire and started looking for potential vulnerabilities in configurations or default files. Too many dead ends… Four or five hours later, I decided to take a break and read the challenge description again. I realized that the domain acme.org must be in the description for a reason. I started Burp and started sending HTTP requests to the same IP address and modified the Host HTTP header while playing with subdomains of acme.org. Then, I tried the following request and received a different response:

Finally! The VirtualHost admin.acme.org is configured on the Apache server, most likely with a different DocumentRoot. I also found later that there are tools to bruteforce VirtualHosts, such as vhostbrute. So, I instantly noticed the cookie “admin=no”. Thus, I started sending requests with “admin=yes”. I noticed that the server started responding with the code 405 Method Not Allowed. Every method returned this code, except POST, which returned a 406 Not Acceptable. Then I tried to play with the Content-Type and received a very interesting response:

I sent {"domain": ""} and received the response {"error":{"domain":"incorrect value, .com domain expected"}}. Then, I sent {"domain": "google.com"} and received the same exact response, which is weird. By adding a subdomain {"domain": "mail.google.com"} we get a different response: {"domain":"incorrect value, sub domain should contain 212"}. Then, I used a random korean domain that I found on Google: 212.bamsuny.com 😆

After following the “next” URL we get base64 encoded data. After decoding it, I realized it was the HTML code of 212.bamsuny.com. So, this service sends a HTTP request to the domain that we specify and returns the response. Then, I decided to host a website on 000webhost.com that basically redirects the request to http://localhost using the HTTP location header.

header("Location: http://localhost");
Redirect, please!

However, it didn’t work. The service returned the contents of https://2120xacb.000webhostapp.com/:

Decoded -> "Redirect, please!"

So, my idea was to get contents from the localhost (SSRF), but this technique didn’t work. Then I started playing with the domain parser regex. First, I noticed that by sending an invalid ID to read.php we obtain {"error":{"row":"incorrect row"}}. And then I realized something juicy, what if the requested domains are stored in a text file and the ID is the index of a row? It would make sense…

I tried to send multiple new line characters (“\n”) in a middle of a domain starting with “212.” and ending in “.com”: {"domain": "2120xacb.000webhostapp.com\n127.0.0.1/flag\n2120xacb.000webhostapp.com"}. In fact, the ID was incremented by 3 after sending this request. The returned ID shows the content of https://2120xacb.000webhostapp.com/, but ID-1 returns “You really thought it would be that easy? Keep digging!”. Now, I can send requests to arbitrary URLs. My first idea was to get, which was forbidden while accessing I was then able to view the server status, including requests from the people that were trying to solve the challenge, the total traffic of the server, among other usage metrics.

Then, I thought about scanning for local services. I sent a request to and obtained the SSH server response “SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2 Protocol mismatch”. Let’s code a simple port scanner in python:

import requests, json, base64

# Note: Add " admin.acme.org" in "/etc/hosts"

def scan(port):
    result = requests.post("http://admin.acme.org/", cookies={"admin":"yes"}, json={"domain": "2120xacb.000webhostapp.com\n127.0.0.1:%s\n2120xacb.000webhostapp.com" % port}).text
    next_endpoint = json.loads(result)["next"]
    index = next_endpoint.find("=")+1
    current_row_id = int(next_endpoint[index:])
    endpoint = next_endpoint[:index] + str(current_row_id-1)
    result = requests.get("http://admin.acme.org%s" % endpoint, cookies={"admin":"yes"}).text
    return base64.b64decode(json.loads(result)["data"])

for port in range(80, 10000):
    if port % 10 == 0:
        print("Scanning: %s" % port)
    result = scan(port)
    if len(result):
        print("OPEN: %s" % port)
Scanning: 1300
Scanning: 1310
Scanning: 1320
Scanning: 1330
OPEN: 1337
Hmm, where would it be?

Scanning: 1340
Scanning: 1350

I was about to doubt my faith, but the port 1337 was open! Where would it be? I know it!

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

I finished the CTF ~5 AM UTC here in Portugal, about 12 hours after launch, so…

Time to sleep!