← Research

Touching Grass: A Browser Extension That Enrolls You as a Proxy Node

In the first post in this series I described the residential proxy economy in the abstract: "free" tools that hand the operator a tunnel in, a supply chain where the exit node's owner rarely understands what they agreed to. That post promised the next installment would dig into the data.

So let's dig. This one is not abstract. We took a browser extension with millions of installs, loaded it in a browser we controlled completely, and watched, in decrypted, timestamped traffic, a logged-out user get drafted into a proxy network and handed somebody else's traffic to relay.

No account. No login. No "click here to start earning." Install, and you're inventory.

The setup (or: how to watch without becoming a node)

You do not get to study proxyware by running it on your own machine and hoping. Run it for real and your connection genuinely becomes a rentable exit. You would be reselling strangers' traffic through your own IP, which is exactly the thing we are documenting, not doing.

So the rig is built to observe without participating:

  • A disposable VM. The browser runs as a locked-down, non-privileged user whose only permitted network path is a local intercepting proxy. Everything else, every attempt to reach out directly over any protocol, is dropped at the kernel and logged.
  • That proxy holds a certificate the browser trusts, so TLS and WebSocket traffic is decrypted and recorded. We see what the extension says, not just who it calls.
  • A hard sinkhole. The moment the extension is told to relay to some third party, that connection is blocked. We capture the instruction. We never carry the freight.

Net effect: the extension behaves as if it's on a normal home connection, we read every word of the conversation, and not one byte of anyone else's traffic ever leaves the box. The subject here is Grass, specifically the "Grass Lite Node" Chrome extension, ilehaonighjijnmpnagapkhpcdbhclfg. Let's read the tape.

Enrollment in a dozen calls, no login

Load the extension, do nothing, and within seconds it starts talking. The opening move is a guest handshake:

POST https://api.grass.io/registerGuestProcedure        200
POST https://api.grass.io/devices/register              200
GET  https://prism-cf.grass.io/ip                        200

There is no signup here. registerGuestProcedure is exactly what it sounds like: a procedure for enrolling a guest, someone who never made an account. The response to that register call carries an access token, and if you decode it (JWTs are just base64, the signature is the only part you can't read), the interesting field is the scope:

{ "scope": "SELLER", "iss": "wynd", ... }

SELLER. Not "user," not "trial." The moment the extension loads, an anonymous browser is classified as a seller of bandwidth. prism-cf.grass.io/ip is the network taking down your public address, the thing it's actually selling.

One aside worth its own footnote: the extension's manifest and our static signatures still pointed at the old infrastructure, getgrass.io and wss://proxy2.wynd.network. The live traffic goes to api.grass.io and director.grass.io. Static analysis had the network a rebrand behind. This is the recurring lesson of the whole series: you cannot map operators by the strings they leave in a binary. You have to watch them run.

The tasking: here is who to relay for

Enrollment is just the interview. The job assignment comes from the director:

POST https://director.grass.io/devices/checkin           201
  ← { "bridge_ip": "45.38.56.225", "token": "eyJ..." }
POST https://director.grass.io/devices/checkin           201
  ← { "bridge_ip": "45.38.56.208", "token": "eyJ..." }
POST https://director.grass.io/devices/checkin           201
  ← { "bridge_ip": "45.38.56.255", "token": "eyJ..." }

Every check-in comes back 201 Created with a bridge (an IP the node is expected to connect through) and a signed token authorizing the relay. Across our runs the bridges all lived in 45.38.56.0/24, and that block isn't some rented corner of a cloud: it's registered to AS23511, "Grass DataCo (BVI) Ltd.," Grass's own autonomous system. The relay infrastructure your browser is pointed at is the operator's, end to end.

The token is where it stops being theoretical. Decode the bridge token's claims and one field answers the only question that matters: relay to whom?

{ "aud": ["162.158.78.94"], "deviceType": "extension",
  "iss": "director-server", "exp": 1784749139 }

aud, audience, is 162.158.78.94. That is a third party the browser has just been authorized to carry traffic for. Not Grass. Not you. Someone who paid, somewhere upstream, to look like a house in your neighborhood. This is the entire product, captured in a single claim: your address, their request. (That target, 162.158.78.94, resolves into Cloudflare's range, AS13335, so the destination itself sits behind a CDN. We captured the authorization to relay, not whatever it fronts.)

In our sandbox the node dutifully tried to open those bridge connections. Every one hit the sinkhole and died there. On a real install, they don't.

Flow diagram: a fresh guest install of Grass is issued a SELLER-scope token by api.grass.io, tasked a relay bridge (45.38.56.0/24) and a third-party target by director.grass.io, then attempts to relay; the relay leg is blocked at our sinkhole.

What you actually agreed to

Here is the part that should bother you, whether or not you have ever heard of Grass.

A guest (no account, no cash-out, nobody who was paid a cent) was enrolled as a SELLER and tasked to relay for a stranger, in under a minute, from a fresh install. Whatever consent exists for the people who sign up to sell their bandwidth, there was none here. The device is the product by default.

And the traffic you'd be carrying is not yours to answer for. Residential exits exist because they inherit the trust of a real household. When something abusive rides that relay (credential stuffing, ad fraud, scraping that trips someone's abuse alarms), the IP in the logs is yours. You are the plausible deniability in someone else's operation, and you volunteered for it by installing a VPN that promised to make you more private.

That is the trade the residential proxy economy runs on, and it does not advertise the second half.

Indicators

If you hunt this stuff, here is the current, dynamically-confirmed indicator set for this family: the live infrastructure, not the stale manifest strings.

# Grass — browser exit-node enrollment (confirmed 2026-07)
sample         Grass Lite Node v7.5.2 (Chrome)
  ext_id       ilehaonighjijnmpnagapkhpcdbhclfg
  sha256       8ea2dd825e27e17a4a673bcd946d4af3cef0fa45f7c33754ea33d44bfcbc450b
  sha1         dda1d8dd3db61700cf24f9d1c8438e006b5470fb
  md5          4df3ed45deca334abb3a5bf725f7d7ee
  permissions  background, webRequest, declarativeNetRequestWithHostAccess, 

# C2 (Cloudflare-fronted, AS13335; all share TLS CN=grass.io, sha256 244e1a3f3677c90b…)
host   api[.]grass[.]io         # POST /registerGuestProcedure, /devices/register  (SELLER token)
host   director[.]grass[.]io    # POST /devices/checkin -> {bridge_ip, token}       (relay tasking)
host   prism-cf[.]grass[.]io    # GET /ip                                           (public-address takedown)

# Relay bridges: operator-owned (NOT third-party hosting)
net    45[.]38[.]56[.]0/24      # AS23511 "Grass DataCo (BVI) Ltd.", Grass's own ASN
                                #   e.g. .208 / .225 / .255, handed out on rotation

# Legacy markers: still in the manifest, no longer the live C2
legacy getgrass[.]io, proxy2[.]wynd[.]network

Two operational notes for anyone reproducing this:

  • Hunt by behavior, not by string. The manifest lies by omission: the live C2 had moved. The enrollment pattern (registerGuestProcedureSELLER token → checkin → bridge) is the durable signal.
  • They rate-limit their own supply. After enough enrollments from one address, registerGuestProcedure starts returning 429. The network runs abuse controls on the people it's recruiting. Rotate egress if you're doing this at any scale.

The honest part

Some housekeeping, because good threat intel says what it didn't prove:

  • We confirmed enrollment and tasking. We did not complete a relay. By design, the sinkhole made sure of it. We are documenting the instruction to resell your connection, not reselling anyone's.
  • This is one extension in a large market. In the same pass we ran the big names through the same rig (VeePN, NordVPN's proxy extension, 1clickVPN, Browsec), and on a cold load, none of them reached for a catalogued bandwidth-reseller SDK. Worth stating plainly: on this test, they didn't. That is not a clean bill of health. We didn't log in or hit "connect," and an SDK that only wakes up mid-session wouldn't have shown, but it's what the traffic said. Not everything sketchy is a proxy node. The ones that are deserve evidence, not vibes.
  • One of the ones that was is Hola, the "free VPN" that's been shipping for over a decade, and it registers your IP straight into Bright Data's commercial residential pool (Bright Data being the rebranded Luminati, of 2015-botnet-scandal fame). We didn't have to guess at the parent company: Bright Data's own code loads right off Hola's welcome page. That's the next post.

The supply chain in the anatomy post wasn't a diagram. It was a to-do list. We're working through it, one confirmed node at a time.

Every indicator in this series is compiled in the defanged IOC appendix: hashes, live C2, ASNs, and TLS fingerprints for each confirmed family, formatted for detection and blocking (CSV).