Blind callback environment

Hello my dear technically interested friends, It’s been a long time, but today we’re looking at an exciting topic.

You may be familiar with Canary Tokens or similar services that allow you to detect when a link is called manually or by processing. Burp Suite Pro also offers this capability with its “Collaborater” feature. But what if you either don’t have a Burp Suite Pro license, or for some reason can’t outsource such a function to external services? This is exactly what we are going to cover in this blog.

Here is an example of what this can look like if you specify these as header fields in Burp with every request and hope that somewhere in the application they are either directly processed/reflected or logged in a backend system and triggered by a later call.

As you can see, in my default Burp settings, I’ve set it to override or add the Referer, X-Forwarded-For, X-Host, X-Forwarded-Server, X-HTTP-Host-Override & Forwarded headers with a simple XSS payload, which then triggers my token. This can be triggered in two ways, either when the web page for some reason reflects the values in the HTML (of course, specific adjustments to the payload must be made), or when the application writes them to a log and someone reads them later (i.e. blind).

This can of course be extended as desired, as an example with a form submission with the mentioned XSS payload, or when using the pingback functionality in WordPress (you can find enough online 😉 ) but briefly and concisely here in a graphic

As you can see, both the reflected (in blue) and the true blind (in red) cases are shown. The red one could also be that the payload is submitted in a contact form and then the URL is called later by a service desk agent.

Now that we have the basics, let’s build a simple endpoint that accepts the token calls and then notifies us on Discord when they are loaded. I promise it’s pretty easy and as always with me, in PHP 🙂

Let’s start with the token structure. Here it is very important that we can assign the tokens exactly to the target. It makes no sense to use the same token all the time and 2 weeks later it gets triggered for some reason and we have absolutely no idea where and why (it has all happened before). So we use this simple structure: {destination URL};{usage}; {timestamp} and we just encode all of this with base64, here is an example.

In this example, we are testing the comment function of my blog (which does not exist) for a stored XSS attack, the payload could look like this: https://www.collfuse.com;blog comment;1706004352 and in base64 this would be aHR0cHM6Ly93d3cuY29sbGZ1c2UuY29tO2Jsb2cgY29tbWVudDsxNzA2MDA0MzUy. We will now use this token for the rest of the blog, and yes, of course you can come up with your own (more secure!) structure for your tokens, this is just for demonstration purposes.

Now we need a publicly accessible endpoint to accept the token, process it, and notify us when it is opened. We want the token to be base64 decoded and all important information (including the request headers) to be sent to us via Discord message. Uffffffff… no worries, we approach the result one step at a time.

First, we need to set up a Discord channel that we can notify via webhook when the token is invoked. This is very easy, click on the settings dialog next to the channel and create a webhook under “Integrations”. It is important to copy the URL for future use.

Step one completed 🙂 Now we need the public endpoint, which we can make accessible with the following code.

<?php
# get params 
    if(!isset($_GET['can_value'])) { echo "nope, try again"; exit; } else { $can_value = explode(";", base64_decode($_GET['can_value'])); }
# function 
    function discord_webhook($content) {
        $curl = curl_init();
        $api_url = "[Discord Webhook URL]";
        $api_body = '{ "embeds": [{ "title": "token triggert", "description": "' . $content . '","color": 14177041 }]}';
        curl_setopt_array($curl, array(
            CURLOPT_CUSTOMREQUEST => "POST",
            CURLOPT_POSTFIELDS => "$api_body",
            CURLOPT_URL => $api_url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30, 
            CURLOPT_HTTPHEADER => array(
                "Content-Type: application/json"
            ),
        ),
        );
        $api_response = curl_exec($curl);
    }
# get headers
    $http_headers = base64_encode(json_encode(getallheaders()));
# get token data 
    $can_data['can_url'] = $can_value[0];       # the target URL
    $can_data['can_res'] = $can_value[1];       # the reason
    $can_data['can_time'] = $can_value[1];
    $discrd_desc = "reason: " . $can_data['can_res'] . "\\n\\nURL: " . $can_data['can_url'] . "\\n\\ntimestamp:" . $can_data['can_time'] . "\\n\\nheaders:" . $http_headers;
# discord messages
    discord_webhook($discrd_desc);
    echo "may the force be with you"; exit;
?>

your trained eyes have already noticed three things in the code, the first being that a GET variable called “can_value” is used. You can easily do this with URL rewrites, everything after the last “/” (i.e., the token) needs to be written to this variable. Second, you need to replace [Discord Webhook URL] with your Discord Webhook URL. And last but not least, that all request headers are transferred as a Base64-encoded string. This is due to the Discord structure, otherwise our body will not be transferred.

We are through and ready for the first test. Assuming that the token URL is “https://collfuse.com/token/aHR0cHM6Ly93d3cuY29sbGZ1c2UuY29tO2Jsb2cgY29tbWVudDsxNzA2MDA0MzUy“, we should now receive a Discord notification when someone calls this URL. Let’s test it.

It works like a charm and we never miss a blind XSS again. Happy hunting to all of you

** midjourney string “detective reads a note on a computer with a bright smile in his face because he solved the case immediately“