Configuring an out-of-band callback listener and notification service in under 10 minutes using AWS Lambda function URLs and Discord webhooks

Configuring an out-of-band callback listener and notification service in under 10 minutes using AWS Lambda function URLs and Discord webhooks
Out-of-band Discord notifications

Amazon Web Services (AWS) just released Lambda function URLs which is the ability to provide external urls to invoke Lambda functions. This is a big deal for many use cases because it eliminates the need to front the Lambdas with an additional service such as AWS API Gateway. The additional services add cost and complexity.

There has been speculation that this feature was coming and I have been anxiously awaiting it as I build out my out-of-band testing services to improve vulnerability validation and blind testing. This can be utilized in lieu of or to augment existing services such as Projectdiscovery's interactsh and Burp Collaborator. I still heavily utilize each of these, but am excited to eventually incorporate this into my heavily AWS engineered recon automation.

Overview

This walkthrough will teach you to setup an AWS Lambda function as your HTTPS callback listener and a Discord webhook for real-time notifications containing relevant information to identify the source calling the Lambda. Additionally, I will provide you with a test script to confirm functionality.

Lambda costs are $0.20 USD per 1M requests and this runs on the smallest 128 MB Lambda size, so even with heavy usage, it will cost pennies per month. This is even better than running a $5 persistent droplet in DigitalOcean because with AWS Lambda you only pay for execution time.

Additionally, the Discord webhook setup is free.

Currently, this setup will only support HTTPS although is sufficient for the majority of blind testing scenarios such as injection, cross-site scripting, or server side request forgery (SSRF).

At the completion, you will receive a listener URL in the format of: https://{unique}.lambda-url.us-east-1.on.aws/ that you can add to your payloads. Although this service does not yet support custom domain names, having the AWS generated URL may provide advantage as more companies utilize external Lambdas, they will create allowlists and trust policies for those domains and may be more effective than a custom URL. Additionally, it could blend in with other traffic.

The notifications in Discord will contain three components (the full headers, the querystring if a GET payload, and the body of the request if it is a POST payload) and will look like the following:

Discord webhook notification

Configuring the Discord webhook

In order to configure a Discord webhook, you will need to generate your own Discord server (don't worry, it is not self-hosted).

Open your Discord app and navigate to the bottom left corner and click the green plus circle.

adding a Discord server

From the popup dialogue box, select:

  • Create My Own
  • For Me and My Friends
  • Provide it with a name, an image if desired, and click Create.
  • Once created, you should see it in your list of other Discord servers along the left side.

Next, you will want to create a dedicated channel for the notifications.

  • Select your newly created (or existing) Discord server on the left.
  • Click the + arrow beside "Text Channels" to create a new channel.
  • Provide it with a name.

Once it is created, you will need to configure a webhook for the channel.

  • Click on the notification channel.
  • On the left side, there is a gear wheel. Click it to "Edit Channel"
Edit Discord channel
  • Click on Integrations in the list of options.
  • Click on Webhooks.
  • Click on New Webhook
  • Provide the webhook with a name.
  • Click on the "Copy Webhook URL" and paste it somewhere because you will need this for the AWS Lambda code. Protect this URL as it provides the ability to post data to the Discord server channel.

At this point, the Discord configuration is complete.

Configuring an AWS Lambda Function

For this part of the walkthrough we are just going to leverage the AWS console. You could eventually convert this into infrastructure as code (IaC), but I made sure this method is as straightforward as possible as it does not introduce the need for any special IAM permissions and does not require any Lambda layers (woohoo!).

Login to the AWS Console with an account that has sufficient permissions to create resources.

  • Open the AWS Lambda Service.
  • At the top right corner, click "Create function".
  • Select "Author from scratch"
  • Provide a function name.
  • For the Runtime, select Node.js 14.x. The reason that I selected this runtime over Python is that I could utilize the native Lambda libraries. The "requests" Python library requires being added as a layer if choosing to use Python which requires several additional steps.
  • Architecture: x86_64
Lambda function settings

In the permissions section, you can keep the defaults.

  • Select "Create a new role with basic Lambda permissions"
Lambda settings - permissions

Expand the "Advanced Settings" section.

  • Check the box for "Enable function URL"
  • For the auth type, select "None"
  • There is no need to check the box or configure CORS because it is wide open by default.

The final settings will look like:

Click on "Create function"

Once the function is created, click on the function to see the details. Upon creation, you should now have a generated "Function URL" in the red box below. This is the invocation URL that you can leverage in your payloads.

Lambda function details

Next, you will need to add the code.

Click on the "Code" tab and replace everything within the index.js file with the below code snippet.

*** You will need to replace the first line variable value for "discordWebhook" with the webhook URL you previously generated in Discord. ***

const discordWebhook = 'Copy the Discord webhook URL into here';

exports.handler = async (event, request) => {
    const https = require('https')

    async function post(url, data) {

        const options = {
            method: 'POST',
            headers: {
            'Content-Type': 'application/json',
            'Content-Length': data.length,
            },
            timeout: 1000,
        };

        return new Promise((resolve, reject) => {
            const req = https.request(url, options, (res) => {
            if (res.statusCode < 200 || res.statusCode > 299) {
                return reject(new Error(`HTTP status code ${res.statusCode}`));
            }

            const body = [];
            res.on('data', (chunk) => body.push(chunk));
            res.on('end', () => {
                const resString = Buffer.concat(body).toString();
                resolve(resString);
            });
        });

        req.on('error', (err) => {
            reject(err);
        });

        req.on('timeout', () => {
            req.destroy();
            reject(new Error('Request time out'));
        });

        req.write(data);
        req.end();
    });
    }

    const eventHeaders = JSON.stringify(event['headers'], null, 2);
    const eventQuerystring = JSON.stringify(event['queryStringParameters'], null, 2);
    const eventBody = JSON.stringify(event['body'], null, 2);
    const params = {
        content: 'Callback received! \n' + 
        'The headers are: \n' + eventHeaders + '\n' +
        'The querystring contains: \n' + eventQuerystring + '\n' +
        'If there was a POST message, the body contains: \n' + eventBody
    };
        
    var notificationData = JSON.stringify(params);

    const res = await post(discordWebhook, notificationData);
    
    const response = {
        statusCode: 200,
        body: JSON.stringify(res),
    };
    return response;
};

Also, there are likely a million ways to optimize and clean-up this code. This was the first JavaScript code block that I think I've ever written that does anything beyond just three lines. Some of this came from Stackoverflow and various other sources to find a combination that effectively works. This is also why creating tutorials and trying things like this is a quick way to accelerate, broaden, and sharpen skillsets.

Once the code is added, click File --> Save.

In order for the code updates to be effective, you will need to redeploy the code.

  • Click on the "Deploy" button.

Once the deploy is finished, you can click on the "Test" box. For the test, you can just use all of the defaults because all you need to confirm is the syntax and whether the Discord webhook connection is successful.

Click the "Test" button in the top right corner.

If the test succeeds, you will get a green box and can proceed to Discord. Otherwise, you will see a stacktrace with errors that you can leverage for troubleshooting.

Confirming Discord webhook connectivity

If you open the notification channel, you should see a message that will look like:

All of the fields are blank because the Lambda was not initiated using the external URL so none of the fields that we are passing to the notification were present in the test.

Testing with a valid payload

Next, we can invoke the URL using a valid payload to determine whether it fully works. I have been really enjoying reading Corben Leo's (@hacker_) tweets about hacking stories and this one specifically inspired me to dig in more to creating a set of ready to go payloads. Also when embedding this Tweet, I realized I was not following him although seem to consistently have his content on my feed. Happy to be following him now :) and you should too.

He shared one of his payloads in this Tweet thread and I embedded it into a html file for this proof of concept. Both to validate my understanding of what it is doing and also to see if it would effectively work with this out-of-band Lambda solution. It does work, all credit and thanks goes to Corben on the payload!

To test the Lambda and notification service, copy the following html and code into a text editor and save the file as .html.

In the URL, make sure to change it to match your specific Lambda URL for your listener. You can add parameters to the end or leave the existing.

<html>
<title>
    Brevity In Motion - Payload Tester
</title>
<head>

</head>
<body>
<script>
    x = new XMLHttpRequest
    x.open("GET", "https://YOURCUSTOMIDENTIFIER.lambda-url.us-east-1.on.aws/?payload=testing123");
    x.send();
</script>
</body>

</html>

Run the file by opening the HTML in a browser. It should automatically make a call to the Lambda listener and nearly immediately, you should receive a Discord notification.

You can confirm the information by matching the "x-forwarded-for" IP address and it should be the address of the system that you ran the HTML from.

Great news if this worked for you, and if it did not, feel free to message me on Twitter. You should now be able to incorporate this into your HTTPS callback payloads.

If you enjoyed this article, consider following me on Twitter @ryanelkins for future content or checkout previous blog posts. I try to demonstrate and share opportunities to utilize cloud native services for bug bounty, automation, and general security. Although this is scoped more towards Bug Bounty, I challenge you to think more broadly on how configurations like this can be applied to other areas of your security programs.

Thank you for reading!