The Crow Flies at Midnight — Exploring Red Team Persistence via AWS Lex Chatbots

Lizzie Moratti
9 min readFeb 8, 2024

--

Today, we will look at using an AWS Lex Service chatbot as a persistence method for a red teamer. This entire blog post is more of a fun exercise in creativity than it is a practical technique. However, it never hurts to get hands-on experience with a service that is being increasingly used by companies in the AI explosion.

The fake text meme I initially sent to a friend as a joke before I spent a day actually building it.

For those unfamiliar with the differences between a red team engagement and a penetration test — allow me to briefly explain.

A penetration test will attempt to uncover as many vulnerabilities/security risks in an environment as possible within a window of time.

While a red team will generally have a higher-level goal, such as:

  • Attempt to access specific sensitive data (customer data, trade secrets, etc.)
  • Attempt to evade detection from defenders while working in the environment
  • Attempt to establish persistence

Why persistence is valuable

Before diving into the technical aspects, let’s describe persistence and why it’s valuable to an adversary.

Once an adversary has access to an environment, they generally want to maintain said access. They might want to take a lovely vacation from December 1st until December 25th, to resume hacking when fewer personnel look at logs. They may wish to routinely exfiltrate future data yet to enter the environment. Additionally, real-world adversaries may also sell their access to others as an initial access broker.

These are some reasons why persistence is valuable and the mindset you must be aware of as a red teamer. Thus, demonstrating persistence might appear in the high-level engagement goals of a red team assessment. So the challenge becomes — how do we ensure we can access the environment at a future date?

The sunglasses and car are not included in this blogpost.

There are quite a few persistence methods that are publicly known for AWS. I’m including them here as they will generally be more practical than using Lex, in all honesty.

https://hackingthe.cloud/aws/post_exploitation/iam_persistence/

  • IAM user login profile
  • IAM Role Assume Role Policy
  • STS (federation token)
  • EC2 Instance Persistence

https://hackingthe.cloud/aws/post_exploitation/lambda_persistence/

  • Lambda Persistence

https://hackingthe.cloud/aws/post_exploitation/user_data_script_persistence/

  • User Data Script Persistence

AWS Lex Use-cases

Amazon Lex is an AWS service that allows developers to build conversation chatbots with which their customers can interact. These days, it’s also often hooked up to AI, so the company doesn’t have to pay a human to provide customer support. I assert that anyone who has been on the internet long enough has seen a chat window pop on a website with something equivalent to “looks like you’ve been browsing our website, need some help?” and is familiar with chatbots.

Chatbots grow up so fast, don’t they?

You can also build a voice chat agent using Lex, but I’ll leave it for someone else to alter this persistence concept to utilize a slide whistle to relive their glory days of phreaking.

The hypothetical situation

For our persistence, we’re going to imagine we’ve been hired by an air travel company. This company worries about cyberattacks after several doors have flown off planes midflight and videos have gone viral (negatively). An utterly unrealistic scenario that would never occur. Either way, they’ve hired a red team to come in and demonstrate if it’s possible to obtain sensitive data, evade detection, and establish persistence. They also have a wonderful sense of humor, as all security professionals do.

How I imagine our client will look as they read the report.

They use AWS Lex as a chat agent to help customers book their flights, check their status, and otherwise use their services. After wrapping up the first two goals, you have extra time and intend to demonstrate persistence and your sense of humor to make sure they keep coming back.

The Game Plan

While it’s possible to build a Lex chatbot from scratch and do everything in a custom manner. We’ll be using the most low-effort method as a proof of concept. We will use the Lex Bot Templates, pick a business vertical, run the CloudFormation that is so helpfully provided by the Lex team, and modify the business logic functions in Lambda to demonstrate.

The Bot Templates page from the AWS console.

Specifically, we will modify a Lambda function from the Lex bot template. If our sleeper phrase is used, it will trigger our malicious business logic to provide credentials for its Lambda role.

A simple diagram of the chain of events leading to a chatbot user obtaining credentials from an external vantage point for persistence.

Modifying the Lambda Function

Once we’ve deployed the CloudFormation stack, it will create a Lambda Application in our account with associated Lambda functions. Viewing the Lambda functions that are now in our account shows there is one function named “AirlinesBusinessLogic.”

We must test our function, which requires sending an “event” as input to the Lambda function. An event is the input that would be made in the API call to invoke the Lambda.

How to configure a test event from the Lambda console.
This is a sample event that is from the LexV2 template for booking a hotel

Unfortunately, there’s no template event for our Airplane company, but its syntax is similar enough to where it works fine.

We can view the function logs after the Lambda was invoked using the test event button with our event. We can also send test events to the function and the logger.debug() output will display. This is also available via CloudWatch logs.

Modifying the lambda_function.py to include a debug message and then running a test event to ensure its working.

We’re now interested in taking the user input and checking if it contains our sleeper phrase; we’ll be using “the crow flies at midnight.” The most promising function in the Python code that’s being used is the “dispatch” function, which is also defined within the same lambda_function.py file.

The “lambda_handler” function handing the event to a function named “dispatch”.

We can see the function is essentially just routing the event to the correct “intent” module. An intent is a Lex-specific terminology and is essentially the goal the chatter is attempting, such as “Book a flight.”

The dispatch function which routes to intent modules.

From the logger.debug, we can see the event being passed to the dispatch() function and stored in a variable now called intent_request. We’re looking for the raw user input; the closest guess we have without reading through the documentation to confirm would be the inputTranscript.

We will need to access that field’s contents using Python and check if it contains our sleeper phrase.

So we’ll modify the event JSON to include that sleeper phrase.

They said the title!

We must then test for that phrase from the raw user input. I wrote a quick check for that with logger.debug() messages.

logger.debug() output showing that we can hit that code path.

To exploit this now requires writing code to steal the Lambda’s role credentials. Luckily, I’ve written that code previously and can reuse it. One of the benefits of saving previous work and tossing it on GitHub.

My code that works but makes my eyes hurt to gaze upon.

https://github.com/lmoratti/moratti-cloud-toolkit/blob/main/index.py

We must use the same imports from the GitHub repo (boto3, base64) to create a one-liner base64 encoded. The base64 encoding is for workflow reasons, so it’s easier to copy and paste into a ~/.aws/credentials file and then use. Optionally, you can return these in clear text by modifying the Lambda response JSON later in this post to return an array of messages.

Adding the imports
Taking the code and then adding a logger.debug for the output we need.

From the logging we can see that it does indeed find credentials and then creates a base64 blob.

Lambda role credentials grabbed and then outputted in a base64 blob.

I pasted that blob into output.txt and confirmed it worked by decoding it on WSL. You can also paste the blob into an online base64 decoder, though I caution to only use a client-side decoder as these will be valid credentials.

Base64 decoding to showcase that we have credentials in the blob above.

This format is for appending the decoded text to a ~/.aws/credentials file as part of my workflow optimization. This makes it easy to confirm the credentials are valid.

This is my workflow to get credentials into my ~/.aws/credentials file for CLI usage.

As you can see, we’ve confirmed the Lambda role credentials are usable. If you have the permissions to do so in a red team engagement, you could also elevate that Lambda role permissions to whatever suits your needs.

Now, we must ensure this output is returned to the chatter no matter what the intent dialogue tree they take. To make this easier, we will move the credential exfil code out of the dispatch() function and into the lambda_handler() function.

Moving the code to the lambda_handler() function so that we don’t need to replicate code in each Lex intent dispatch function.

We had to change the variable “intent_request” to “event” because of where we moved the code. Now that the code is working and returning a response to the bot, we can build it and test it with our user input.

On the console, this is where the build button is located. If you have a giant screen, it’ll likely be off to the right and easy to miss.

If you build it, they will… persist?

We’ll use the Lex console to test the draft bot version.

Error messages just mean you’re closer than you think.

Unsurprisingly, the format is off. To return a valid response, we have to follow the syntax that is dictated by the Lex Service.

https://docs.aws.amazon.com/lexv2/latest/dg/lambda-response-format.html

According to the documentation, a valid response syntax will always require “sessionState” which is the state of the conversation between the user and the LexV2 Bot. Within sessionState, required subfields will depend on the “dialogueAction” taken.

I am closing the session as a dialogueAction because it requires the minimal fields. The intent I am using is BookAFlight since it’s in our Bot Template.

It took me longer than I’d like to admit to get this JSON correct.

What our modified function now looks like with the correct syntax for the response.

Now let’s give it a try by talking to the bot.

The chatbot is now giving us base64 encoded credentials from user input.

When we decode the output

Moving the blob into output.txt, then decoding it to confirm the format is correct.

We can see we have valid credentials for the role. If this bot was built and deployed externally to customers, we would have a valid persistence mechanism.

QED
A cool looking cloud practitioner, much like my readers.

I hope this blog post was helpful or, at the very least, interesting.

If you enjoyed this post, please follow my blog or connect with me on Medium, X (formerly Twitter), Mastodon, or LinkedIN.

--

--

Lizzie Moratti

Security Professional - Pentester, former security project manager & technical program manager.