Browsers Are Localhost Gateways: Client Port Scanning Using WebAssembly And Go

Avi Lumelsky
InfoSec Write-ups
Published in
9 min readJul 25, 2021

--

Websites tend to scan the open ports of their users, from the browser, to identify new/returning users better.
Can ‘localhost’ be abused by the browser?
Can it be done through WebAssembly?

Live Demo is available at http://ports.sh or https://ports.sh,
The code is available at https://github.com/avilum/portsscan,
You are welcome to contribute!

In this article, I will demonstrate how browsers can be abused to attack localhost services — to penetrate organizations or to run remote code from the browser.

Photo by Immo Wegmann on Unsplash

It isn’t a secret that each of us has a unique fingerprint when we visit web pages. Client fingerprinting helps websites track our activity across plenty of other websites.
A fingerprint is a combination of many factors regarding your device, browser, screen size, IP address, and many other variables. When combined, they make us highly identifiable by websites.

A Word About Client Code

Websites, such as eBay, run code on our computers endlessly.
They used to do it solely with JavaScript.
Over the years, FrontEnd technologies evolved —
Javascript (ECMAScript), TypeScript, Deno (secure and fast javascript runtime, written in rust), to name a few…

The browsers’ Javascript API is abused by malware all the time since it is the only language that browsers support. With the next-generation web technologies that were under development in recent years, such as WebAssembly, The next-generation malware can be even more sophisticated.

The WebAssembly (WASM) runtime allows languages to be compiled into binary code, which can be used by the browser’s WebAssembly Runtime to execute low-level code in your browser when visiting web pages. Besides being much faster (Compiled code is usually faster than JS, but not always), WASM decouples the programming language and focuses on “what to do”.

WebAssembly’s runtime is amazing — it brings a lot of new APIs and features to the browser, but there’s no free lunch — WASM is a great target for security researchers and hackers. After all, and even though it is public, WASM has plenty of code that was reviewed by very few eyes.

This is why many new languages like Rust, Go, and Deno, provide out-of-the-box support for WebAssembly as target architecture/runtime.
As of today, you can write code in Go, Rust, and many more, instead of using a javascript-derived language. Thus, system programming languages are becoming widely adaptable among developers, who want the ability to reach something beyond what we once treated as “cross-platform”.
Python is not so “cross-platform” now, huh? (Just Kidding — you might find https://pyodide.org/en/stable/console.html interesting).

Researching WebAssembly Runtime (Using Go)

How easy will it be, to map open ports (active, listening services) on a host from a browser context, when a user visits my website?
And can I do it with a low-level language?

As a website owner, Let’s say I would like to identify if a user is a developer.
Port scanning techniques discover assets and servers every second. “Open Ports” literally refer to servers that are bound to a specific port on a NIC device. Some of the services have to be defined manually, but many operating systems run services on startup, exposing many application APIs, available on localhost such as IPC, SMB/Samba, SSH, SMTP, FTP, etc.

Thanks to that, It is super easy to find assets that are vulnerable to 1-day vulnerabilities (public but not disclosed). It’s just a matter of who got there first, and whether is it reported (and fixed).

I have always believed in WASM since I first heard about it years ago. I started playing with it. I chose Go because of its easy Net/Socket and HTTP standard library APIs. By the way, Rust also got vast WebAssembly compilation support out of the box.

Understanding the flow

A user visits the web page

Now, the user’s browser is going to initialize the WebAssembly runtime.
Then, It is going to automatically run my Go Port Scanner, which was compiled into a WebAssembly binary.

Let’s GO: Writing a (not-so-simple) Port Scanner

I followed the Javascript API using Go’s ‘syscalls/js’ bindings.
I discovered the browser liked it when I used the HTTP Go API, and it did not work well with the (maybe not-fully-implemented) ‘net’ Go API.

WebAssembly Raw TCP session and UDP are not supported (yet) in browsers. The community is also working on the new WASI standard — which will eventually require browsers to implement raw TCP/UDP sessions, one day.

After trying several ways to abuse WebAssembly’s raw TCP session, to receive raw TCP responses, I understood that programming using native TCP session scanning won’t work. Chrome proxies (and sometimes blocks) WebAssembly requests and responses for security reasons (like CORS and unsafe ports).

Let’s define the port scanner:

Then, we will define the port scanning function:

Go’s “http” API has some great benefits, like the ‘GotFirstResponseByte’ handler for HTTP tracing. I wish it had worked but it didn’t — Imagine how easy it would have been.

It will be better to leverage Go’s famous “http” package instead, After all — browsers are all about Applications. It should be easier with an application protocol like HTTP.

Understanding responses.

How do you scan ports consistently, using high-level HTTP sessions?
Getting responses is one thing. Understanding them and classifying them correctly is a challenge since we can’t see the response as-is. Port scanners usually classify networks using raw packets and sessions. Inside the WebAssembly runtime, this is impossible at the time of writing this article.

We can aggressively segment the following responses:
- Connection Refused (The port is CLOSED)
- Timeouts (The port can be either Open or CLOSED).
- HTTP Response (Open — And there is a valid HTTP server on the other side!)

I found that relatively small timeouts are outstandingly reliable.

I ran into more problems as I progressed. The browser was blocking valid HTTP responses that lacked CORS Headers, presenting it as a generic “fetch error”, masking the real cause of the error, and marking the request as “insecure”. The browser didn’t even forward the contents of the error to the WASM runtime — it simply failed with no further details.

Iterating over common errors. Treating them as “open ports”.

How about CORS?

localhost HTTP services often miss the “Cross-Origin-****” headers.
Cross-Origin (A combination of host+port that is different from window.location) protection is available by default, so HTTP requests without these headers will fail. You can’t know the request failed because of CORS in the WebAssembly context.

I had to overcome that somehow,
In classic JS one would have simply added a ‘no-cors’ mode to the fetch() request. After researching a bit, and thinking of a way to override JS’s fetch() API call, I found a recipe — a pretty simple one — that works like a charm!

I would have never thought of adding such an HTTP Header to my request. It is such a weird way to modify the browser behavior (syscall) using the Go language.

So CORS is no longer a problem for the Demo, but it doesn't mean that it will work in websites that explicitly specify ‘Access-Control-Allow-Origin’ headers.
We cannot address this case. Still, there are plenty of websites across the world that will run this WASM code successfully.

Supporting ports with TLS/SSL services

without SSL Handshakes, we “skip” many SSL security features that browsers activate by default. This helps me to find any TCP open port, not only services that accept SSL transport.

We have to remember that WebAssembly runtime’s TCP stack is proxied by Chrome’s own TCP stack. The browser terminates our SSL requests if the certificate does not match, without specifying why — it will look like a typical “Connection Refused”. SSL adds complexity and security features, thus I proceeded with plain text HTTP.

Defining The Startup Function:

Please note the JS DOM elements I inject using the syscalls/js library, which enables modifying the DOM using Go code. I was amazed by the simplicity.

Compiling The Code:

Defining a web page that loads the WASM

Now let’s look at the client’s computer. Which ports are currently listening (e.g. “open”)?

Notice the wasm_exec.js script tag, which is the WebAssembly Go runtime bindings.

Starting a local demo:

>>> python3 -m http.server 5000

Running a local port scan upon visit:

What visitors see upon startup (index.html) — The HTML itself is injected as Go loads! how cool is that?

Let’s take a look at the network packets of the visitor

Network packets are captured on the localhost (loopback) network interface. The packets are numbered on the left: packets 10–20, which are the index.html requests and responses, including the scripts. After packet 22, the browser starts port scanning. It fires (TCP SYN) packets, and receives (RST, ACK) packets in response, meaning the port is closed.

Now let’s look at an open port indicator.
If the socket receives HTTP Timeouts, without receiving the ConnectionRefused error, There must be something listening on that port (commonly treated as ‘Filtered’) that accepts the connections, returning a Reset / Ack response.

Notice the valid HTTP / TCP responses (blue/purple). Someone is listening out there!

And, Putting it all together:

The HTML page that runs the port scan on the visitor, through its browser’s WebAssmebly runtime

Comparing The Results To The Ground Truth.

I used netstat and nmap to verify what I saw.
I expected my results to be the same, and so they were.

NetStat output (open ports that are listening on the current OS) vs. NMap Output (TCP scanning these ports from an attacker perspective). Each of the ports I found in the WASM port scanner, was backed by these results.

Localhost is not local, but remote, after all.

When configuring or working on personal computers today, we treat Localhost as a “safe” or “gated” environment.
It seemed once like it was much harder to attack a localhost service.
Persistent apps that we install often allocate and listen on ports on localhost.

Operating systems tend to open ports on the localhost network interface, upon startup to work. Whether it is Windows, Mac, or Linux — your computer listens on localhost for something. As I have shown — Javascript or WebAssembly applications can scan these localhost services with ease. They can also abuse them.

Scenario 1: Denial-Of-Service through RPC vulnerability on a Linux client, using a single visit.

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-8779
RPC-BOMB is an example vulnerability that enables an attacker to cause a Denial-Of-Service attack on an operating system through an outdated Linux built-in RPC service (rpcbind), using malicious TCP packets.

Imagine this: You visit a website.
A second later — Your computer gets stuck or resets, destroying your latest unsaved work.
1.
Find whether that vulnerable port is open (111)
2. Send specially-crafted TCP packet(s) to localhost:111
3. You have successfully shut down your visitor’s computer.

Scenario 2: Windows Print Spooler Remote Code Execution Vulnerability (CVE-2021–34527):

A remote code execution vulnerability exists when the Windows Print Spooler service improperly performs privileged file operations. An attacker who successfully exploited this vulnerability could run arbitrary code with SYSTEM privileges. An attacker could then install programs; view, change, or delete data; or create new accounts with full user rights.

This vulnerability can be used against millions of users, exploiting different devices, thanks to the browser’s access to localhost, 0.0.0.0, 192.168.1.1, and so on.
Many of us use Windows, with this vulnerable service up and running upon startup. Bound on 0.0.0.0, accessible over the network, hackers can use this vulnerability to penetrate organizations.

Developers (such as myself) often run local servers/containers on localhost.
I often forget to shut them down as well (For example, when running a docker container with restart=always). Besides Exploiting vulnerabilities, developers can be identified (by StackOverflow/Linkedin/Facebook for example) to understand which technologies we use in development.

In my humble opinion, The browser WASM attack surface will be abused more over time, as more futures will be implemented.
In this article, I have demonstrated how a web page can communicate and map services on the user’s local host network, regarding the browser’s security features. It can be done in many languages that compile into web assembly, although the usage of raw TCP/UDP sessions is not possible as of today.

I gave an example of a scenario in which Linux hosts can experience DOS by visiting a web page from a browser — and how Microsoft’s new vulnerability CVE-2021–34527 can be used as well.

WASI is amazing.
WebAssembly is great.
Browsers aren’t that great — and they will always pose a serious risk & attack vector for all of us, as they turn into Operating Systems with time, supporting more and more WASI specification features.

Live Demo

Live Demo using WebAssembly:

http://ports.sh or https://ports.sh

Code

The code is available at https://github.com/avilum/portsscan, Feel free to contribute.

--

--

A business-oriented security researcher, who loves Privacy and AI, with deep security insights.