6 - Exploiting DOM clobbering to enable XSS

https://www.youtube.com/watch?v=eWD4LH5W2Es

El DOM Clobbering es una vulnerabilidad de seguridad en aplicaciones web que ocurre cuando elementos del Documento Object Model (DOM) de una página web sobrescriben o "clobber" (destruyen o alteran) objetos o variables globales de JavaScript en el entorno del navegador.

¿Cómo ocurre?

En HTML, los elementos pueden tener atributos id o name, y estos nombres pueden crear referencias globales en el entorno JavaScript. Por ejemplo, si un elemento tiene un id llamado form, en JavaScript podría generarse automáticamente una variable global window.form que apunta a ese elemento. Sin embargo, si la aplicación ya usa una variable llamada form, esta referencia puede ser sobrescrita por el elemento DOM, lo que puede causar comportamientos inesperados o vulnerabilidades de seguridad.

Ejemplo básico

Supongamos que una aplicación tiene un código JavaScript que utiliza una variable form para referirse a un objeto que maneja la lógica de formularios:

var form = new FormHandler();
form.submit();

Ahora, si en el HTML se define un elemento con id="form":

<form id="form" action="/submit">
  ...
</form>

En algunos navegadores, la variable form en JavaScript será sobrescrita por la referencia al elemento DOM <form>. Esto se conoce como clobbering. Ahora, si se intenta llamar a form.submit(), en lugar de ejecutar la lógica del objeto FormHandler, se llamará al método submit() del elemento <form>, lo que podría llevar a un comportamiento no deseado.

loadCommentsWithDomClobbering.js

function loadComments(postCommentPath) {
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            let comments = JSON.parse(this.responseText);
            displayComments(comments);
        }
    };
    xhr.open("GET", postCommentPath + window.location.search);
    xhr.send();
    function escapeHTML(data) {
        return data.replace(/[<>'"]/g, function(c){
            return '&#' + c.charCodeAt(0) + ';';
        });
    }
    function displayComments(comments) {
        let userComments = document.getElementById("user-comments");
        for (let i = 0; i < comments.length; ++i) {
            comment = comments[i];
            let commentSection = document.createElement("section");
            commentSection.setAttribute("class", "comment");
            let firstPElement = document.createElement("p");
            let defaultAvatar = window.defaultAvatar || {avatar: '/resources/images/avatarDefault.svg'};
            let avatarImgHTML = '<img class="avatar" src="' + (comment.avatar ? escapeHTML(comment.avatar) : defaultAvatar.avatar) + '">';
            let divImgContainer = document.createElement("div");
            divImgContainer.innerHTML = avatarImgHTML;
            if (comment.author) {
                if (comment.website) {
                    let websiteElement = document.createElement("a");
                    websiteElement.setAttribute("id", "author");
                    websiteElement.setAttribute("href", comment.website);
                    firstPElement.appendChild(websiteElement);
                }
                let newInnerHtml = firstPElement.innerHTML + DOMPurify.sanitize(comment.author);
                firstPElement.innerHTML = newInnerHtml;
            }
            if (comment.date) {
                let dateObj = new Date(comment.date);
                let month = '' + (dateObj.getMonth() + 1);
                let day = '' + dateObj.getDate();
                let year = dateObj.getFullYear();
                if (month.length < 2) month = '0' + month;
                if (day.length < 2) day = '0' + day;
                dateStr = [day, month, year].join('-');
                let newInnerHtml = firstPElement.innerHTML + " | " + dateStr;
                firstPElement.innerHTML = newInnerHtml;
            }
            firstPElement.appendChild(divImgContainer);
            commentSection.appendChild(firstPElement);
            if (comment.body) {
                let commentBodyPElement = document.createElement("p");
                commentBodyPElement.innerHTML = DOMPurify.sanitize(comment.body);
                commentSection.appendChild(commentBodyPElement);
            }
            commentSection.appendChild(document.createElement("p"));
            userComments.appendChild(commentSection);
        }
    }
};

Solución :

<a id=defaultAvatar><a id=defaultAvatar name=avatar href="cid:&quot;onerror=alert(1)//">

Estamos clobbeando default avatar,si nos fijamos en la elección del default avatar (se evalúan los OR de izquierda a derecha):

let defaultAvatar = window.defaultAvatar || {avatar: '/resources/images/avatarDefault.svg'};

¿Existe comment.avatar? No -> el src será el atributo avatar del objeto defaultAvatar:

let avatarImgHTML = '<img class="avatar" src="' + (comment.avatar ? escapeHTML(comment.avatar) : defaultAvatar.avatar) + '">';

¿Porque dos anchor? -> Si el primer elemento con id="defaultAvatar" no sobrescribe completamente la variable global (porque el DOM trata de mantener una referencia a ambos), el segundo elemento podría asegurar que la propiedad avatar sea clobberada, ya que ahora está intentando sobrescribir una referencia a un grupo de elementos en lugar de un único elemento. (Ponerlo por si acaso)

El valor href="cid:&quot;onerror=alert(1)//" es un intento de inyección maliciosa de código en un atributo HTML, con la intención de ejecutar un ataque XSS (Cross-Site Scripting) utilizando el evento onerror de una etiqueta <img>.

Vamos a desglosar lo que hace cada parte:

1. href="cid:&quot;onerror=alert(1)//"

Como podemos ver usar el esquema URI cid: implica que la comilla sea url encodeada o no lo sea.