A Cryptocurrency Heist, Starring Your Web Browser
Abusing well-defined web standards to exploit localhost services
Beneath the surface, the modern web is made possible only through a growing labryinth of technology standards. Standards are designed to govern the interoperability of technology and data. Web standards are both among the most broadly adopted and rapidly evolving, with changes often leading to heated debate amongst browser vendors, web developers, and consumers alike.
In this blogpost, we will detail how a well-defined and universally adopted web standard can be considered harmful when followed blindly. Abusing this issue, we demonstrate practical remote exploitability in a (now fixed) “steal all the money” attack against a popular cryptocurrency service.
Localhost Services
Many modern applications are starting to use localhost “api-servers” as a design pattern that separates program logic from user interfaces. These services quietly listen on 127.0.0.1
(localhost) and headlessly encapsulate the core logic of the application as a platform-agnostic remote programming interface (RPC).
C:\WINDOWS\system32>netstat -a -b
Active Connections
Proto Local Address Foreign Address State
TCP 0.0.0.0:443 DESKTOP:0 LISTENING
[vmware-hostd.exe]
TCP 0.0.0.0:912 DESKTOP:0 LISTENING
[vmware-authd.exe]
TCP 0.0.0.0:5900 DESKTOP:0 LISTENING
[siad.exe]
TCP 0.0.0.0:49664 DESKTOP:0 LISTENING
[Spotify.exe]
TCP 0.0.0.0:57621 DESKTOP:0 LISTENING
[Discord.exe]
TCP 127.0.0.1:8307 DESKTOP:0 LISTENING
[siad.exe]
TCP 127.0.0.1:18171 DESKTOP:0 LISTENING
[Battle.net.exe]
TCP 127.0.0.1:27015 DESKTOP:0 LISTENING
[AppleMobileDeviceProcess.exe]
TCP 127.0.0.1:27060 DESKTOP:0 LISTENING
[Steam.exe]
TCP 127.0.0.1:52094 DESKTOP:0 LISTENING
[NVIDIA Web Helper.exe]
Over the past several years, research into these localhost API services has yielded a number of remotely exploitable issues. Some of the more high profile findings came from Tavis Ormandy of Google’s Project Zero:
All Blizzard games (World of Warcraft, Overwatch, Diablo III, Starcraft II, etc.) were vulnerable to DNS rebinding vulnerability allowing any website to run arbitrary code. 🎮 https://t.co/ssKyxfkuZo
— Tavis Ormandy (@taviso) January 22, 2018
Here is a basket of uTorrent DNS rebinding vulnerabilities that are now fixed, from remote code execution to querying and copying downloaded files, and more. https://t.co/JEvhq1IHGJ
— Tavis Ormandy (@taviso) February 20, 2018
More recent research uncovered some exploitable issues in the popular video conferencing application Zoom
In the cryptocurrency space, this same “api-server” design pattern is very common. A good number of blockchain projects use this architecture in their coin-daemon. These daemons are responsible for managing a user’s crypto wallet, performing transactions, and staying in sync with the blockchain.
Typically, a user-facing GUI application will connect to this local service and translate “high-level” concepts (such as creating a transaction) into “low-level” blockchain operations that the daemon provides via the API it exposes. This model also allows advanced users or third party developers to easily write code that drives, extends, or showcase the core functionality of the daemon as they wish.
Localhost is Relative
Binding these api-servers to only serve on 127.0.0.1
seems like a safe and easy way to prevent the application (such as the coin / wallet daemon) from being exposed to the internet and remote attacks. The problem is that this isn’t always a safe assumption, espescially when co-located with the average web browser.
Surfing the web, your browser downloads and operates upon a lot of ‘untrusted’ data to render your favorite website on your screen. By extension, any JavaScript shipped down a given website is executed by your web browser on the local machine. This means that remotely-originating & maliciously crafted JavaScript could potentially be used to probe at localhost services.
Taking a Look at Siacoin
Let’s take our theoretical “hunch” that code executing locally, inside a browser, should (in principle) be able to interact with local services, and just run with it. Through the remainder of this post, we’ll be attacking Siacoin: a thriving cryptocurrency project built for the purpose of providing cheap, efficient, and decentralized file-storage via blockchain technology.
Our primary goal will be to successfully complete an API call to Sia’s /wallet/seed
endpoint. In cryptocurrency parlance, a “wallet-seed” is a string of words that can be used to reconstruct the private key associated with a particular wallet. If you have this key, you own the funds.
We can test this theory by creating a malicious website that attempts to request the victim’s wallet seed from their localhost daemon:
However our request has been blocked! What happened??
For good reason, it isn’t this simple to attack localhost services through the browser. This is because modern web browsers employ a catch-all protection known as the Same-Origin-Policy (SOP).
Introducing Same Origin Policy
SOP was first introduced in Netscape Navigator 2 (circa 1995) in an effort to regulate access to the Document Object Model (DOM). As websites became more user-oriented and JavaScript became commonplace, SOP placed clear boundaries around which resources code on a particular webpage could interact with or modify.
Without SOP, a malicious website could make a request to another website and read potentially sensitive information from its response. Imagine the malicious pseudo-JavaScript below:
let req = await fetch("https://mail.google.com/") // Request data from current logged in gmail
let mail_content = await req.text(); // Decode the response data
exfil(mail_content); // Exfil the emails to an attacker
Prior to, or without SOP, a malicious website could perform a request like this to read the email of anyone who visits their site! The main idea behind SOP is that a script executing as a result of visiting one particular origin (for example, attacker.com), should not be able to interact with data on another origin (for example, mail.google.com, or localhost).
To enforce this, browsers inspect every outbound request to ensure that it is compliant. When the browser determines that a website is making a request to a different origin (a “cross origin request”), it will first check if the request includes any “unsafe” headers. If it does, the browser will outright block the request as seen below:
Conversely, if the request does not include any unsafe headers, it is forwarded along to the target site. This “target site” now has the option to tell the browser if other origins are allowed to read the response. This functionality is implemented via Cross-Origin Resource Sharing (CORS) headers that can be set by the “target site”.
Normally, websites do not enable CORS, or enable it only for specific domains. This means that the browser will simply block the response from being delivered. As a result, the requesting site cannot read the response data.
An important consequence of this scheme is that if a particular request is marked “safe”, it will be allowed to pass through to the target site. Despite being named “safe,” these requests can still pose significant risk for a given application. Take for example the following request:
fetch('http://localhost:1337/wallet/make_transaction', {
method:'POST',
body:'to=attack_address&amount=100000'
})
Here is the actual data that is sent to the server:
POST /wallet/make_transaction
Host: localhost:1337
Origin: http://attacker.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36
Accept: */*
Referer: http://attacker.com
to=attack_address&amount=100000
In this example, the only pieces of data under the malicious script’s control are the path and the request body, both of which are deemed “safe” under CORS. Therefore, this request is considered “safe” overall despite obviously posing substantial risk! If the server does not perform any additional verification, the request would successfully trigger a transaction.
Now that we have seen how SOP blocks our cross-origin request from reading http://localhost:9980/wallet/seed
, our goal shifts to bypassing SOP such that the browser thinks our malicious request is from localhost (and therefore, same-origin). This can be accomplished via a technique known as DNS rebinding.
In Need of DNS Rebinding
DNS rebinding is a modern technique that confuses the browser into thinking the current origin is associated with a different IP address than it actually is. In turn, this allows a malicious actor to “satisfy” the SOP while interacting with a service on a different origin.
This type of attack can be carried out by controlling a particular domain name, as well as an associated DNS server. When a victim visits the domain, the DNS server responds with the real IP address, but with a very short Time-To-Live (TTL) to prevent caching.
Later, once the TTL has expired, another request to the attacker’s domain is made (e.g via JavaScript). However this time, the DNS server responds with an internal IP address (such as 127.0.0.1
). The browser believes it is still talking to the original attacker.com
, but now the request will go to the target service!
In this example, note how the first time attacker.com resolves to 12.34.56.78
, but the second time it resolves to 127.0.0.1
.
This is a very powerful primitive as it allows a malicious website to sidestep some of the boundaries put in place by SOP. Now, when attacker.com issues a request to itself, the browser will make a same-origin request to 127.0.0.1
, because we’ve “swapped” the associated IP address out from under the browser.
As we’ll see a bit later on, a relaxed set of restrictions applies to same origin requests, a point that will be crucial later on. The key takeaway is that DNS rebinding forces open a non-obvious corner-case that application developers must consider.
Protecting Localhost API Servers
The strongest defense against these attacks is to require a secret on-disk token while making requests to the API: something an attacker could not possibly know from a remote context. However this is sometimes not ideal as it makes using the API more difficult, so often developers try to find alternatives.
Another common technique is to validate request headers with the goal of making sure the request is from a legitimate client application. A common way to do this is to check that the Host header is set to localhost
or some other expected value. Checking for certain headers the browser is expected to send such as the Origin
, User-Agent
, or Referer
is another approach. However, such “header checks” can themselves be problematic, as it can be non-obvious which headers can actually be trusted, and which can be modified by malicious scripts.
Let’s take a look at how the Siacoin Daemon tries to protect itself from unauthorized interaction… Fairly early in the project’s life, the developers of Sia realized that requests originating from the browser could become an issue. To mitigate this risk, they included the following code, which ensures that the daemon only accepts requests that have a User-Agent
with the value of “Sia-Agent”:
if !strings.Contains(req.UserAgent(), "Sia-Agent") {
writeError(w, "Browser access disabled due to security vulnerability. Use Sia-UI or siac.", http.StatusBadRequest)
return
}
To bypass this check, we need to specify the User-Agent
header while performing a cross-origin request. Let’s find out if that’s possible!
Checking the Standards
To figure out which headers we can control in outbound requests we need to dive into some web standards. These standards define two lists of headers. The first is called no-CORS-safe
: it whitelists headers that can safely be set for Cross-Origin requests (e.g headers attacker.com
can send to bank.com
):
`Accept`
`Accept-Language`
`Content-Language`
`Content-Type`
JavaScript can set these, and ONLY these, headers when performing Cross-Origin requests. If others are set, the browser will block the request. This is why the User-Agent
filtering approach described above seems secure. User-Agent
is not in the whitelist, and therefore cannot be set for a Cross-Origin request.
The other list is the Forbidden
list: it explicitly blacklists headers from being set, REGARDLESS of their Cross-Origin status (disallowed even for same origin requests such as bank.com
sending to bank.com
):
`Accept-Charset` | `Accept-Encoding`
`Access-Control-Request-Headers` | `Access-Control-Request-Method`
`Connection` | `Content-Length`
`Cookie` | `Cookie2`
`Date` | `DNT`
`Expect` | `Host`
`Keep-Alive` | `Origin`
`Referer` | `TE`
`Trailer` | `Transfer-Encoding`
`Upgrade` | `Via`
There are tricks to prevent some of these headers from being sent, but they cannot be spoofed: if they exist, the recipient can trust them. To note, we see that Origin
and Referer
make the list, but User-Agent
does not. This implies that for a same-origin request an attacker can spoof the User-Agent
header to any value they want!
Let’s test this out on the Siacoin Daemon!
Completing the Siacoin Exploit
We can start putting the pieces all together:
- The Siacoin Daemon validates requests by validating the
User-Agent
header - Same-Origin requests are allowed to set custom User-Agents, because
User-Agent
is not in theForbidden
list - DNS rebinding allows us to turn a cross-origin request into a same-origin one
To actually exploit this issue, we need to set up a DNS rebinding attack against http://localhost:9980
. We can do this relatively easily by using rbndr.us, a utility created by Tavis during his related research. Rbndr provides a DNS server that will switch between two target’s IPs, perfect for this attack scenario.
First we create a malicious site at 7f000001.<our_ip>.rbndr.us
and then attempt to access /wallet/seeds
. However we still need to spoof the User-Agent
header. This turns out to be very easy and can be done as follows:
var req = new XMLHttpRequest();
req.open("GET","/wallet/seeds",true);
req.setRequestHeader("User-Agent","Sia-Agent");
req.send();
With our attack in place, we just have to wait for the DNS record to be updated. As soon as that happens we will be speaking directly to the Siacoin Daemon, which will happily give us the user’s seed.
There were a number of direct consequences of this, most notably the ability to steal a victim’s wallet-seed as long as the wallet was “unlocked” (the default state assuming a user was running the Sia wallet application). This seed can later be used to irrevocably transfer all of the victim’s funds.
To target users in a watering-hole style attack, a malicious actor could have easily stood up a ‘faucet’ style site for this coin while silently pulling seeds off their visitors. This very website could have been pulling off the attack without you knowing.
Coin Theft to Remote Code Execution
This particular attack has an interesting endgame: Not only is it possible to steal victim’s funds, it is also possible to achieve remote code execution by abusing intended functionality of the Sia daemon.
As stated before, Siacoin is primarily a decentralized system for facilitating cheap & reliable file-storage. An attacker who has access to Sia’s wallet daemon API can use it to upload and download arbitrary files between the decentralized storage network and the victim’s computer. With the ability to write an arbitrary file at an arbitrary path on a target machine, we were able to demonstrate code execution in a number of different ways.
Affected Browsers
From our testing, Google’s Chrome was the only major browser which prevents setting the user agent field in the face of a DNS-rebinding style attack. This is a particularly interesting case, because by consciously electing to go against the defined standard, Chrome must have forsaw the danger in an edge case everyone else missed.
We found that both Apple Safari and Mozilla Firefox users would be vulnerable to the attack demonstrated in this post. This is because both browsers follow the standard correctly. Interestingly, users of Microsoft Edge were safe, despite the browser also following the standard. This is due to the built-in network isolation of Edge’s app-container, which disallows the browser from actually establishing a connection to localhost.
This situation illustrates the dangers that well-defined standards can give way to, especially if they are followed with little discretion to the current ecosystem.
Disclosure Timeline
We responsibly disclosed this issue to Nebulous Labs in September 2018. They acknowledged its severity and addressed the issue in a timely fashion by hardening their service to use an on-disk token.
- 09/12/2018 - Disclosed to Nebulous Labs
- 09/25/2018 - Fix merged to source tree
- 10/16/2018 - Fix released in Sia 1.3.6
At no point did we observe any active exploitation of this issue. From the date of this post, the fix has been in-market for roughly a year.