
PoC pour une vulnérabilité Use-After-Free (UAF) dans le composant RTCEncodedFrameBase de Firefox, exploitable via l'API WebRTC Encoded Transforms.
CVE-2025-14321 : Le destructeur de RTCEncodedFrameBase n'appelle pas DetachArrayBuffer() lors de la libération de la mémoire native du frame. Par conséquent, les ArrayBuffers JavaScript conservent des pointeurs vers une mémoire déjà libérée (pointeurs sauvages), permettant une lecture et une écriture arbitraires sur le heap du processus content de Firefox.
Ce PoC a été testé sur Mozilla Firefox 145.0.1 et Windows 10.
Firefox expose les frames vidéo/audio codés sous forme d'ArrayBuffers JavaScript via NewArrayBufferWithUserOwnedContents(). Lorsque le frame natif (C++) est détruit, la mémoire sous-jacente de l'ArrayBuffer est libérée, mais l'ArrayBuffer en JavaScript n'est pas détaché (detach). Cela viole l'invariant de propriété et génère un pointeur sauvage.
Code vulnérable (dom/media/webrtc/jsapi/RTCEncodedFrameBase.cpp) :
// ❌ Destructor no desvincula el ArrayBuffer
RTCEncodedFrameBase::~RTCEncodedFrameBase() = default;
Code patché :
// ✅ Desvincula el ArrayBuffer antes de liberar memoria nativa
RTCEncodedFrameBase::~RTCEncodedFrameBase() {
DetachData();
}
new Uint8Array(buf) lit les données de la mémoire réaffectée à d'autres objets du heapnew Uint8Array(buf).fill(0x41) corrompt les objets adjacents dans le heap (vtables, structures DOM, etc.)Ces primitives sont suffisantes pour démontrer la vulnérabilité. Avec un heap shaping supplémentaire, elles pourraient évoluer vers une exécution de code à distance (RCE).
media.peerconnection.scripttransform.enabled = true
media.peerconnection.ice.loopback = true
media.peerconnection.ice.no_host = false
media.peerconnection.ice.relay_only = false
media.peerconnection.ice.proxy_only = false
⚠ Redémarrer Firefox après avoir modifié les préférences.
Le PoC tente de demander l'accès à la caméra via getUserMedia(). Ce n'est pas obligatoire si les préférences ICE sont correctement configurées. Son rôle est de faire passer Firefox en mode RFC IP handling mode 1, qui garantit la génération de candidats ICE host dans les scénarios où les seules prefs ne suffisent pas.
En pratique :
getUserMedia() échoue silencieusement (NotFoundError) sans afficher de prompt. ICE se connecte quand même grâce à media.peerconnection.ice.loopback = true.Les Web Workers ne fonctionnent pas depuis file://. Servir via HTTP :
python3 -m http.server 8080
Ouvrir dans Firefox vulnérable : http://localhost:8080/poc-cve-2025-14321.html
Le PoC tente de demander l'accès à la caméra. Si une caméra est disponible et que l'utilisateur accepte, Firefox fait passer la session en mode RFC IP handling mode 1. Si getUserMedia() échoue (VM sans caméra, permission refusée), le PoC continue avec un canvas comme source de frames — ICE se connecte quand même si media.peerconnection.ice.loopback = true est actif.
Un <canvas> de 320×240 est créé avec une animation à 30fps via captureStream(30). Cela génère des frames vidéo codés de manière prévisible. Une fois qu'ICE se connecte et que la caméra a été utilisée, le PoC remplace le track de la caméra par celui du canvas via replaceTrack() et libère la caméra.
Deux RTCPeerConnection locales sont créées (pc1 ↔ pc2) avec une négociation SDP standard et trickle ICE. Les candidats sont échangés directement entre les pairs.
Un RTCRtpScriptTransform est assigné au sender de pc1 avant la négociation SDP. Le worker intercepte chaque frame codé du pipeline d'envoi.
Pour chacun des 200 premiers frames, le worker :
frame.data (ArrayBuffer pointant vers la mémoire native)leaks[])new Uint8Array(buf).fill(0x41)En ne réenvoyant pas, le GC détruit le wrapper C++ et libère la mémoire native. Mais l'ArrayBuffer JavaScript conserve le pointeur — pointeur sauvage créé.
À partir du frame 201, tous les 3 frames, le worker :
0x41, l'allocateur a réutilisé cette mémoire → UAF confirmé0x41 sur le buffer (write primitive sur un heap étranger)Ce cycle de lecture/écriture sur la mémoire libérée provoque une corruption progressive du heap jusqu'à ce que Firefox fasse crasher l'onglet (équivalent à un SIGSEGV rapporté dans l'advisory).
[0.4s] ✓ Track inicial: canvas (fallback)
[0.4s] ✓ ¡CONEXIÓN ICE ESTABLECIDA!
[0.5s] ▶▶▶ ¡PRIMER FRAME ENCODED RECIBIDO! ◀◀◀
[0.5s] Leak #0: 1219B → 41414141414141414141414141414141
[2.7s] Leak #49: 1497B → 41414141414141414141414141414141
[9.8s] Leak #199: 1870B → 41414141414141414141414141414141
[9.9s] 200 ArrayBuffers retenidos → memoria nativa YA liberada
[9.9s] → Dangling pointers activos, esperando reuso…
[ CRASH DE PESTAÑA — heap corruption]
Remarque : Si une caméra est disponible et que la permission est accordée, le log affichera
✓ getUserMedia concedido — ICE modo 1 activopuis✓ Track reemplazado: cámara → canvasaprès la connexion ICE. Dans les VM sans caméra, le PoC utilise directement le canvas sans prompt.
Le crash de l'onglet confirme la corruption du heap : l'écriture de 0x41 sur la mémoire réaffectée à d'autres objets (vtables, nœuds DOM) provoque un accès mémoire invalide dans le processus content.
✓ Buffer[0] detached correctamente — fix activo
ArrayBuffer detached — versión parcheada ✓
Le DetachData() dans le destructeur invalide l'ArrayBuffer avant de libérer la mémoire native, éliminant le pointeur sauvage.
├── poc-cve-2025-14321.html # PoC funcional (código fuente legible)
└── README.md # Este archivo
Mozilla a corrigé la vulnérabilité dans Firefox 146 en ajoutant DetachData() dans tous les destructeurs et chemins de teardown de RTCEncodedFrameBase.
Commit : https://hg-edge.mozilla.org/mozilla-central/rev/1051067f6e83
⚠ UNIQUEMENT À DES FINS ÉDUCATIVES ET DE RECHERCHE
Ce PoC démontre l'existence de la vulnérabilité jusqu'au point de corruption du heap (crash). Il n'implémente pas de heap shaping, de bypass ASLR, ni de techniques de hijack nécessaires à l'exécution de code. Son but est exclusivement la vérification et la compréhension du CVE dans des environnements contrôlés.