reddit hackernews mail facebook facebook linkedin

Fail at CTF h1-212 - The Evil Job

Hackerone recently released a CTF created by Jobert Abma. Even if I didn’t complete the challenge, it was so exciting and I was so close from the solution that I wanted to share a writeup. Here is the tweet that tiggered the war:

Hackers, hack your way to NYC this December for h1-212! An engineer of launched a new server for a new admin panel. He is completely confident that the server can’t be hacked, so he hid a flag. Details: …. #TogetherWeHitHarder

And here is the full description:

An engineer of 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!

Below the different steps that led me to The Holy Flag.

Step 1, first things first: find an attack surface

As usual when I approach a new target, I immediately fired up my Burp Suite and started a directory scan with several tools like Cansina, Dirb and Dirsearch, each time with a different wordlist: SVNDigger, Robots Disallowed and Raft ones. Below the urls discovered:

At the end of the scan I tried to compare the discovered items with my local Apache using diff to check if any modification occured but they were all identical. While /index.html is the default Apache index page, /icons/README the default Apache icons list and of course it would be too easy if this /flag file would be the good one…
ctf h1-212 fake flag

At the same time I ran a Nmap scan on all TCP ports and top 500 UDP. This result to only two open ports: 22 and 80. I quickly tried a brute force against SSH with Patator but it failed.

I then decided to use Burp Suite to try another type of issue called Virtual Host Enumeration. I jumped on this because I recently read an article about that and I also sent a similar report to a private program a week before. Since the brilliant engineer works for, I configured the attack for this domain. And it found something interesting at ctf h1-212 host attack

Accessing Localhost via Vhost | VIRTUAL HOST ENUMERATION
Jobert tool: virtual host scanner
vhost-brute: a PHP tool I wrote right after the CTF

Step 2, bargain with the server

I was supposed to have access to an hidden admin now so what’s next ? Let’s check the response of the server: ctf h1-212 host found

Looks like we have a cookie to play with! Obviously the first thing you would try is to change the value from admin=no to admin=yes… Here we go, and the new response is:
ctf h1-212 admin yes

The message is clear enough, the GET method is not allowed, so switching to POST would return:
ctf h1-212 method post

According to the official documentation, aka Wikipedia xD
"406 Not Acceptable: The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request."

Ok then… Altering the header Accept will not change anything, however if I also change the Content-Type in the request…
ctf h1-212 content type json

Server says that I am supposed to provide a body now, let’s say parameters, in JSON format. Since I have no idea about the name of the parameters, I supplied an empty string:
ctf h1-212 body param

The server requires a domain so let’s feed him with that! I won’t show you all the errors returned by the server (.com required, 212 required etc…) but here is the final string I got:
ctf h1-212 good domain

List of HTTP status codes

Step 3, start crying 1/2

At this point here is the request I had:
ctf h1-212 read php

I firstly tried SQLi by injecting single quote, double quotes, encoded values, using SQLmap blablabla, nothing.
I then tried XSS by injecting single quote, double quotes, less-than symbol, encoded values again and still nothing.
I tried Local File Inclusion with Jhaddix worlists, nothing.
I also tried to inject extra parameters and extra headers as well, nothing.
I had a hard time to find the next step, I stayed stuck here 1 full day, trying figure out what kind of attack would work.

After a night break, I read a comment from another hacker that had scripted the 2 requests to be able to run them automatically in a row. At this moment I understood that there was probably a link between the POST request and the result of read.php. To be honest I would not had realize that by myself. I simply thought that the previous step was done and jumped to the next one without even thinking there could be a relation… errrrrr that was my first mistake.

So I did it too. I PHP curly scripted the requests, and from here I only used my script to find the flag, that was my second mistake, this is probably the reason why I didn’t get it.

So basically, altering the domain provided in the JSON param in the body of the POST request would lead to a different response when calling read.php with the corresponding id returned by the server. After playing a little bit with the value, injecting stupid characters and so on, I finally was able to retrieve datas and I quickly realized that I was in front of a SSRF issue.
ctf h1-212 ssrf 80

The final response is the base64 encoded result of the HTTP request performed by the server. In this case: http://localhost:80/index.html the default Apache page.

Important note: at each HTTP POST request performed by the server, id is incremented by 1. Sometimes it get back to 0 but I didn’t figure out why/when/how. It doesn’t seem to be shared between the hackers.

Step 3’ or 4 or 5, port scanning

I’m pretty sure that this step was supposed to be the last one but when I’m able to trigger a SSRF issue, I always perform a port scan first.

I simply moved my 2 curl requests into a for loop that iterates from 1 to 65535, because why not ? Ok, Jobert said that brute force is not the key but you know… I’m still a kid in my mind, I’m a geek, I’m a hacker so… when someone tell me to NOT do something, then I feel really bad if I don’t give it a single try!

After few minutes and some false positive, my script displayed a different response corresponding to the port 1337:
ctf h1-212 ssrf 1337

Confirmed with Burp: ctf h1-212 ssrf 1337 burp

Easy peasy.
I was pretty sure this was the key so I stopped my script at port 19187, enough spam.

Hackerone article about SSRF
Server-side browsing considered harmful by Nicolas Grégoire

Step x, try harder, cry stronger 2/2

Ok I have the good parameter, a good domain, good Content-Type, good cookie, the l33t port, I can decode base64, so far so good! Now I simply need to request a single file like /index.html or /flag, that would be great! Hopefully this is the final step. Unfortunately this is where things gone wrong :/

I read many documentation and articles about SSRF but the most useful was the SSRF bible and the presentation from Orange Tsai. Simply awesome. The idea: &@ @
Since &, % and # are forbidden characters in this challenge, from this two papers I understood that I should combine the SSRF with a CRLF. Dealing with \r and \n I quickly got a strange behavior:
ctf h1-212 ssrf 1337 400

Yes, yes, yes, it’s not Apache anymore, I am now dealing with a Nginx install. Does it matter ? Well I don’t know, maybe those servers don’t react to CRLF the same way…

Since the first part of the domain must contain 212 and the last part must end by .com, I had configure a dns A record of my own domain to point to, that was useless but hey, how could I know! Based on the “3 parts payload” mentionned by Orange Tsai, here are some stuff I tried:\r\r\r\\r\\\\r\\r\ @\r\ @\r\ @/flag @/

After some hours (…) I decided to use the SSRF against my own server, that way I could see what kind of requests the CTF server send. Installing and configuring Nginx. Configuring a .com domain I have and the vhost that will be accepted by Jobert’s code as well. Same same but different:\r\r\r\\r\\\\r\\r\ @\r\ @\r\ @/flag @/

Checking the log, again and again and still got this f*****g error 400: ctf h1-212 nginx 400

Better use Netcat to see the headers received:
ctf h1-212 netcat 400

Well not so much information here… Let’s try with tcdump then: ctf h1-212 tcpdump

Still nothing… Let’s try something a bit more tricky. What about CRLF combined with header injection ? My new payload looked like: HTTP/1.0\rHost:\rCookie: admin=yes\r

ctf h1-212 header injection

Hey but what about encoding ? Yeah yeah yeah I tried that too, since % is not allowed, I had to try something else: \x0A 0x0A \u000A and the whole family, of course it didn’t work…

At the end of the day, the great @EdOverflow gave me a hint, kudos to him. He told me that I have gone too far with header injection, the solution was much simpler, I should focus on the first part of the payload. That said, after a full day on this shit (and some violent outrage against Jobert ^^), I gave up for a good night.

Next day, let’s recap what I learned the day before: the first part and the third part of the three part payload (found in Orange Tsai doc) are injectable and trigger the SSRF to a domain I can control and depending of the characters I use for the CRLF (\t \s \r \s \b 💩) the server acts different, which seems to be logic…

Ok! New payloads on the way:


No more luck, I continued to alter it with different characters, different encoding. I also tried different protocols found in the SSRF bible: http, https, file, gopher, dict, mailto and so on… With a good wordlist and a for loop it was easy to test them all but unfortunately no matter what I tried, my script always returned empty data.

One more day wasted for nothing, I didn’t move forward :/ I won’t show you all the payloads I tried, thousands and thousands, the only thing you need to know is that they all failed! But the real question is: did they really failed?

PayloadsAllTheThings resources on GitHub
SSRF bible
SSRF tips by xl7dev
A New Era of SSRF - Exploiting URL Parser in Trending Programming Languages! by Orange Tsai

The answer, free your mind…

After 5 full days working on the challenge, I finally spent the 2 last days with my childrens, trying to get over the headhash. Since the challenge was ended, I decided to ask the answer to @EdOverflow, knowing that I would probably cry… and guess what? I did!

Remember that the id returned by the POST request is incremented by 1 for each request made by the server ? But what if I inject a CRLF and the server does 2 requests ? Then the id returned will be incremented by 2, because 1+1=2! Holy cow!!!!

Running my script like a moron, I only focused my mind on the result of /read.php and I didn’t even notice that the id jump the steps 2 by 2…
ctf h1-212 moron

So I need to decrease the id by 1 to get the result of the previous request. Which is supposed to be the content of the file I requested aka http://0:1337/flag. ctf h1-212 answer

Et voilà ! Decoded value:
FLAG: CF,2dsV\/]fRAYQ.TDEp`w"M(%mU;p9+………


Pay attention to all details, stupid idiot!

Jobert’s answer in 1 single line:

D=;eval echo $(C=admin=yes;H='Host: ';N=$(curl -H'Content-Type:application/json' -H$H -b$C -d'{"domain":"0:1337/flag\"}' "$D"|egrep -o '[0-9]+');curl -H$H -b$C "$D"read.php?id=`expr $N - 1`|jq .[])|base64 --decode


Congrats to these 3 people, congrats to all others that successfully got the flag and congrats to everyone that at least gave a try!

External resources