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]

Steam, VMWare, Battle.net and other popular applications exposing localhost services

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:

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.

Example crypto-currency user-interface (Sia Client) which connects to a localhost server (Sia Daemon)

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:

An attempt to access Siacoin's wallet seed from the browser

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:

SOP blocking a request to mail.google.com from ret2.io

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.

SOP blocks origins from reading responses from others, but sometimes it also blocks the request

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!

Example of a DNS Rebinding attack on 127.0.0.1

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 the Forbidden 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.

Putting the exploit together and stealing the Siacoin Wallet Seeds from Firefox

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.

Chrome blocking the unsafe User-Agent header

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.