How Pirate World Cup Streams Actually Work

I wanted to figure out how that trick hiding in plain sight actually worked. Pirate streaming sites have been around forever, but during the World Cup you couldn’t open a sports forum without someone dropping a link to a live match — full HD, no buffering, no login. I’d always assumed there was something exotic going on under the hood. A friend sent me a link before the England-Croatia match. “Watch this,” he said. So I opened DevTools.

I expected a scraped HLS playlist or a re-streamed feed. Instead I found a <video> tag, a Shaka Player instance pointing at a legitimate Akamai CDN, and two hex strings in a <script> block that were doing all the work. Those hex strings were the DRM decryption keys. In the page source. Not behind an API. Not encrypted. Just wrapped in JavaScript that looked scary and did nothing.

Then someone showed me an Android app doing the same thing for every World Cup match — but “properly,” with encrypted configs, Firebase Remote Config, a license server, the works. A dedicated “MUNDIAL FIFA 2026” section with 28 channels including multicam, player cam, and manager cam for every game. It took about an hour to realize it was the same vulnerability wearing a nicer suit.

What started as thirty minutes of curiosity turned into a weekend. By the end I had mapped 670 live streams across 169 CDNs in 33 countries from the web side, and 525 more channels from a single Android app — all protected by a DRM scheme the W3C itself calls a “testing” tool. During a World Cup where England is putting four past Croatia and every goal is simultaneously available on hundreds of unauthorized streams.

Disclosure
This is a vulnerability analysis. No real keys, stream URLs, or working tools are published. App names and domains are redacted. The goal is to document a systemic weakness in how platforms deploy DRM — not to enable piracy.

Every browser that plays encrypted video uses the W3C’s Encrypted Media Extensions (EME). EME supports four key systems. Three of them — Widevine, FairPlay, PlayReady — use license servers, encrypted key exchanges, and hardware-backed decryption. The fourth is ClearKey.

ClearKey sends the decryption key to the browser in plaintext.

That’s it. Same AES-128 encryption as Widevine. Same MPEG-DASH delivery. But where Widevine negotiates keys through a secure channel and decrypts video inside a hardware sandbox the browser can’t peek into, ClearKey hands the raw key to JavaScript and says “here you go.”

text

    WIDEVINE L1                          CLEARKEY
    ───────────────────────              ───────────────────────
    License server (HTTPS)               No license server
    Encrypted key exchange               Key in plaintext JS
    Hardware TEE decryption              Software decryption
    Key never in JS memory               Key IS the JS
    Per-device, per-session policy       No policy at all

The DASH Industry Forum says ClearKey is “recommended only for testing purposes.” Some platforms decided to use it in production anyway. Both the websites and the app I analyzed depend on this decision.

I examined dozens of pirate streaming sites. They all follow the same pattern.

text

    ┌──────────────────────────────────────────────────────────────┐
                        HOW IT ACTUALLY WORKS                     
    └──────────────────────────────────────────────────────────────┘

    STEP 1                    STEP 2                    STEP 3
    User visits               Clicks a channel          Page loads an <iframe>
    pirate-site.co            (e.g. "Sky Sports")       from a different domain

    ┌──────────────┐          ┌──────────────┐          ┌──────────────────┐
                              ┌────┐┌────┐│            player-host.co  
      Channel       click     ESPN││DAZN││  iframe                    
      Grid Page    ───────>   ├────┤├────┤│ ───────>   Shaka Player    
                              Sky ││beIN││            + DRM keys      
      (static                 └────┘└────┘│            + manifest URL  
       HTML)                └──────────────┘          └────────┬─────────┘
    └──────────────┘                                             
                                                                  fetches
                                                                 
                                                    ┌──────────────────────┐
                                                      LEGITIMATE CDN      
                                                      (akamaized.net,     
                                                       skycdp.com,        
                                                       indazn.com)        
                                                                          
                                                      Encrypted DASH      
                                                      segments            
                                                    └──────────────────────┘

    The pirate site serves ZERO video.
    The CDN bill belongs to the legitimate provider.
    The pirate site hosts ~50KB of HTML.

The pirate site is just a middleman that knows two hex strings. It points the viewer’s browser at a real CDN, hands it the decryption key, and the browser does the rest. The CDN never knows the viewer isn’t a paying subscriber.

The player host page contains a <script> block with the keys, wrapped in obfuscation that a 30-line Node script undoes in under a second:

javascript

// Before: 40 lines of _0x4a2f, IIFEs, string-array lookups
// After: this is what it actually does

var drmKeyId = 'a1b2c3d4e5f6a7b80000000000000000';
var drmKey   = '0123456789abcdef0123456789abcdef';

player.configure({
    drm: { clearKeys: { [drmKeyId]: drmKey } }
});
player.load('https://cdn.legitimate-provider.com/manifest.mpd');

Two variables. Two hex strings. That is the entire “DRM protection.” The obfuscation — variable renaming, string arrays, control-flow flattening — is undone by eval(), because the browser has to run it too. If the browser can execute it, a script can execute it.

Once extracted, keys spread through M3U playlists on GitHub and Telegram. I found a single M3U file with 545 ClearKey entries across 92 CDNs. Public, searchable, indexed by Google. Time from extraction to global distribution: minutes. Time for the key to be revoked: usually never.

Not every pirate operation is a static webpage. Some ship a full Android app — with login screens, Firebase analytics, encrypted configs, and a professional UI. One app I analyzed during the World Cup takes this approach. 525 channels. 36 categories. A dedicated “MUNDIAL FIFA 2026” section with multicam angles for every match.

The app’s encrypted channel config — 525 channels across 36 categories, with AES-encrypted URLs and DRM credentials. [URLs redacted]

The app fetches its channel list as an AES-encrypted JSON from a remote server. Every field — stream URLs, DRM license URIs, HTTP headers — is base64-encoded AES ciphertext. The decryption key is a 16-character string stored in Firebase Remote Config, fetched at launch.

Sounds serious. Then you look at the ciphertext:

text

    236 URLs share the same first 35 encrypted blocks.

    Block 0: f91b7f273abccc6e...  ← identical across all 236
    Block 1: 932b23560f1d43ba...  ← identical
    Block 2: a7d788a399cea962...  ← identical
    ...

    ECB mode. Same plaintext block = same ciphertext block.
    The URLs all start with the same CDN domain prefix.

AES-ECB is the textbook example of how not to use AES. Every cryptography course teaches this with the ECB penguin — encrypt a bitmap with ECB and the image is still recognizable because identical plaintext blocks produce identical ciphertext blocks. This app encrypts 236 URLs that start with the same CDN domain, producing 236 ciphertexts with an identical 560-byte prefix. Pattern analysis alone reveals the structure before you even find the key.

Once decrypted, the ClearKey credentials aren’t in a separate key exchange. They’re in the license URI itself, as plaintext query parameters:

text

    drm_license_uri (after decryption):

    https://[redacted]/?keyid=49eb924b...&key=6e131b04...
                              ^^^^^^^^        ^^^^^^^^
                              ClearKey ID     ClearKey Key
                              (in the URL)    (in the URL)

The app’s “license server” is a URL that contains the key. There is no challenge-response. No session binding. The app fetches the URL, the URL is the key, and the key decrypts the stream.

Three layers of indirection — Firebase Remote Config, AES encryption, a “license server” endpoint — all collapsing into the same failure: the decryption key ends up in the client, in plaintext, because ClearKey requires it to.

text

    ┌──────────────────────────────────────────────────────────────┐
    │           ANDROID APP ARCHITECTURE                           │
    └──────────────────────────────────────────────────────────────┘

    1. App launches
       └──> Firebase Remote Config
            └──> Fetches AES key ("claveapp") + config URLs

    2. App fetches encrypted JSON (525 channels)
       └──> Base64 decode ──> AES-128-ECB decrypt
            └──> Stream URLs, DRM license URIs, headers (plaintext)

    3. For CLEARKEY channels (348 of 525):
       └──> "License URI" = https://[redacted]/?keyid=XXX&key=YYY
            └──> Key ID and Key are IN THE URL

    4. App configures ExoPlayer with ClearKey
       └──> Fetches DASH manifest from legitimate CDN
            └──> Decrypts video with the key from step 3

    Package name: com.example.myapplication
    Encryption:   AES/ECB/PKCS5Padding (textbook insecure)
    Key storage:  Firebase Remote Config (single API call to extract)
    Key length:   16 characters, static, never rotated

The package name com.example.myapplication tells you everything about the development rigor behind this operation. The default Android Studio template. Not even renamed.

The most quietly damning part — and the part that shows the real sophistication of the operation, which is mostly logistics — is where the encrypted config is hosted. The app fetches its channel JSON from a public GitHub repository. Anyone can browse it. Anyone can git clone it. Anyone can watch the commit history.

And the commit history is the interesting bit:

The public GitHub repo that hosts the app’s encrypted channel list. A commit lands every 4–5 minutes — automated edits to the channel JSON as upstream streams get rotated, killed, or replaced. Username and repo owner redacted.

A commit lands every 4–5 minutes. The diff is always against tv (10).json or bearer.json — the encrypted channel list, and the auth credentials used to fetch new streams from upstream. The commit messages are all "Actualizar tv (10).json desde tv.json" or "Actualizar bearer.json desde panel - <timestamp>". Translation: an automated job, somewhere, is rewriting the channel config every few minutes and pushing the update so phones running the app pick up new stream URLs and rotated DRM credentials within one polling interval.

This is the actual moat of the operation. Not the AES-ECB (broken). Not the Firebase Remote Config (one API call). Not the “license server” (a URL with the key in it). The moat is operational: a piece of automation that keeps the channel JSON aligned with whichever upstream feeds are alive at any given minute, hosted in plain sight on a free CDN that they did not have to build. When a legitimate provider rotates a key, the bot notices, fetches the new one, re-encrypts the config, and commits. When a CDN blocks the source IP, the bot finds another one. The customers’ phones poll, see the new commit, fetch the new config, and continue watching the match. The user does not even see the rotation happen.

The defence everyone reaches for is “revoke the key.” But you cannot revoke a key faster than a script can push a new one to a GitHub repo. The mismatch in cadence is the whole game.

Web auditAndroid app
Streams670525 (348 ClearKey)
CDNs16934
Countries33~8 (LATAM-focused)
Key storageJavaScript <script> blockFirebase Remote Config
ObfuscationJS variable renaming + IIFEsAES-128-ECB (broken)
Key in plaintext at clientYesYes
World Cup coverageDozens of sports channels28 dedicated channels + multicam

Both approaches — the static website and the full Android app — end at the same place: a ClearKey key pair in the client’s memory, with no mechanism to prevent extraction, no session binding, no revocation, and no key rotation.

The economics explain everything. Pirate sites host ~50KB of static HTML and point your browser at someone else’s CDN. The app is a thin ExoPlayer wrapper around someone else’s infrastructure. Neither serves video. Both monetize through ad networks.

StreamEast served 1.6 billion visits before being raided in September 2025. The IPTV ring in “Operation Takendown” had 22 million users and EUR 250 million per month. A mid-tier site during the World Cup — a million visitors on match day, six ad impressions each, $1.50 CPM — clears $9,000 a day against hosting costs of essentially zero.

Enforcement keeps escalating — prison sentences in Spain, $18.75 million judgments in Texas, 27,000 feeds taken down the week before the World Cup — and new sites keep appearing. Because the vulnerability is architectural. You cannot arrest your way out of a spec that puts the decryption key in the browser. You cannot encrypt your way out of it either, as the Android app demonstrates: three layers of encryption, and the key still ends up in plaintext on the client, because ClearKey demands it.

The fix is migrating off ClearKey entirely — to Widevine, FairPlay, or PlayReady. Until then, the combination is written on the back of the padlock, and the World Cup is making sure everyone knows where to look.

Related Content