How your ethereum can be stolen through DNS rebinding

With the new buzz around exploiting unauthenticated JSON-RPC services on localhost ignited by Tavis Ormandy, The first thing that came to my mind was ethereum clients(Geth, Mist and Parity).

Most of the ethereum clients run a JSON-RPC service on port 8545 on localhost, but since it’s on localhost, we can’t access it directly from user’s browser due to SOP. Thisissue in the electrum wallet exploited the CORS headers to take control over the user’s electrum wallet through JSON-RPC on localhost.

Geth’s JSON-RPC looked pretty secure as it didn’t return any CORS header, but then cpacia commented something on the half-patch of the electrum wallet which sparked my interest. Here are the exact words

Just disabling CORS is still vulnerable to a DNS rebinding attack. It needs to be authenticated. ~ cpacia

I had heard about DNS rebinding but never tried to look into it much. Since Geth’s JSON-RPC is also unauthenticated, it might be vulnerable to DNS rebinding attacks too?

Here is basic definition of DNS rebinding.

I started looking into DNS Rebinding, but all the articles were mainly pretty old. Then I asked about it in Bug Bounty Forum and Luke Young linked me his awesome Defcon talk about modern DNS rebinding exploitation. It also included an automated tool to make DNS rebinding achievable on most of the modern browsers.

But I was more curious and I didn’t want to use any pre-made tool, So I began writing my own DNS server. Python has a pretty nice library called dnslib which handled most of the stuff for me. I registered a domain, setup some glue records pointing towards my server and used them as the nameservers.

I wanted to see how different browsers behaved with very low TTLs, so I made my DNS server return TTLs < 5. Chrome, Firefox and Safari cached the DNS responses for 60 seconds, even though the TTL was less than 5.

60 seconds isn’t such a long time, and I think I can make a user stay on my webpage for at least 60 seconds. Now the only thing left was to actually try it.

I ran geth with the --rpc flag(in testnet of-course)

geth --rpc --testnet

Now was the time for some javascript, and this was the hardest part. I’m not a good web-dev and it was pretty hard for me to wrap my head around the every time Javascript behaviour. I hacked together a pretty bad but working javascript in 3 hours. The initial results were successful.

Now to make it work with geth, I had to run my web server and domain on port 8545 as SOP also follows the port. But this would look kinda sketchy if I sent the link to anyone with port 8545 in it.

The solution was iframes. I made apache listen on both 8545 and 80, and set up a virtual host for each port. Now I could iframe the port 8545 and run all the javascript in the hidden iframe.

Another issue was about multiple users, what if multiple users at the same time visited my domain? The DNS server would get confused as I was using a counter based system and there was no way to differentiate between requests from individual users. This had me stumped for a while until I remembered subdomains.

I could iframe a random subdomain each time a user visits the main domain and use it as an identification. I know I may not be explaining it well but here’s an example.

Let’s assume that my domain is attacker.com and my server’s IP is 87.87.87.87 Here is how the flow goes.

  • The victim opens attacker.com in his browser.
  • First, a DNS request for attacker.com is sent to my server and it responds with the real IP 87.87.87.87
  • Next, attacker.com is loaded into the user’s browser, which then creates a hidden iframe with a random subdomain randomrsub.attacker.com:8545 and appends it to the body
  • Now, a DNS request is sent to my server for the subdomain randomrsub.attacker.com, and the DNS server again responds with the real IP 87.87.87.87. But this time, since it’s on port 8545, the apache responds with a different virtual host, which begins the DNS rebinding.
  • The javascript on randomrsub.attacker.com:8545 waits 60 seconds, and then sends an XmlHttpRequest to randomsub.attacker.com:8545/test.
  • Since the DNS cache has expired, the browser resolves the DNS again. This time, my server responds with an IP of 127.0.0.1.
  • Now the request is actually sent to 127.0.0.1:8545/test instead of my server, and since it’s under the origin of randomrr.attacker.com:8545, we are able to read the response.
  • Since we are generating a random subdomain everytime, we can now even accommodate multiple users as the subdomain can act their identification token.

I also had to optimize the javascript a little to make sure it works 95% of the time. I added some fake DNS lookups before the real ones so that it doesn’t respond with the wrong IP at the wrong time.

This could basically be exploited with a stored XSS too. Just point a script src to the js file which adds the iframe and TADA!!

So now we can read responses from JSON-RPC service, which means we can read their ethereum addresses, their balance and potentially steal their ether if their account is unlocked. The JSON-RPC API has pretty decent method called eth_sendTransaction which can be basically used to send ethereum from the user’s account.

I have set up a Proof-of-Concept on http://rebinddns.ml. If you stay on it for more than 60 seconds and you’re running Geth(or any other ethereum client) with JSON-RPC, you will see an alert() which will contain your ethereum addresses and their balance.

All the files used in the PoC can be found on my github.

  • min.js - The Js file which generates a hidden iframe of a subdomain on port 8545
  • main.js - The Js file which executes the DNS Rebinding
  • server.py - The DNS server written in python

I have verified that Geth, the C++ ethereum client and the python client are vulnerable. The PoC has been tested on Firefox, Chrome and Safari.


PS: This has been reported to the ethereum foundation and is fixed. They also awarded me a nice bounty.

For any questions, you can hit me up on my twitter @ret2jazzy