Gaining Long-Term AWS Access with CodeBuild and GitHub
As I wrote in other blogs, such as Gaining AWS Persistence by Updating a SAML Identity Provider, when an attacker compromises an AWS account, one of the first tactics they will attempt is to gain persistence. This is because access obtained through temporary credentials may expire quickly, or the attacker may want to ensure continued access even if their initial foothold is discovered and removed. That’s why I’m interested in exploring how legitimate AWS services can be abused for persistence. While creating IAM users and assigning them admin privileges is still a common and effective tactic (as we continue to observe in data from TrailDiscover), it’s noisy and easier to detect. In this article, I’ll explore how an attacker could use/abuse AWS CodeBuild for persistence. I haven’t seen this particular approach documented in other research, but let me know if it’s been covered elsewhere. ⚠️ Disclaimer: This is NOT an AWS vulnerability. The technique described assumes the attacker has already compromised the account and has sufficient permissions. The goal is to expose how CodeBuild can be used to establish long-term access. What is AWS CodeBuild? CodeBuild is a fully managed continuous integration service that compiles source code, runs tests, and produces software packages for deployment. It supports custom build environments and integrates with popular tools such as GitHub. In mid-2024, AWS introduced support for running self-hosted GitHub Actions runners using CodeBuild. This functionality allows you to configure a CodeBuild project to act as a runner for GitHub Actions. This GitHub Actions integration is the option we are going to explore to see how an attacker can abuse it to gain persistence. How an Attacker Could Abuse CodeBuild The technique is surprisingly straightforward. Because CodeBuild now supports self-hosted GitHub Actions runners, an attacker can: Configure a CodeBuild project to serve as a GitHub Actions runner. Link that project to a GitHub repository controlled by the attacker. Modify an IAM role to allow CodeBuild to assume it. Then this role will provide access to the AWS environment. Let’s see these steps in more detail. Step 1: Backdoor a Role The attacker first needs to choose an IAM role to abuse. Ideally, this is an existing role with broad/admin privileges. In most cases, backdooring an existing role is often stealthier than creating a new one. To “backdoor” the role, the attacker modifies its trust policy to allow the CodeBuild service to assume it: { "Effect": "Allow", "Principal": { "Service": "codebuild.amazonaws.com" }, "Action": "sts:AssumeRole" } This change allows CodeBuild builds to assume the role and inherit its permissions. Note: Backdooring IAM roles for persistence is a well-known technique. It has been documented in research and observed in real-world incidents. Step 2: Create a CodeBuild Project and Connect it to GitHub Once the attacker has selected and backdoored an IAM role, the next step is to go into AWS CodeBuild and create a new build project linked to a GitHub repository they control. While CodeBuild projects can be created via API or CLI, connecting a GitHub repository often requires going through the AWS Console. Fortunately for the attacker, it’s relatively easy to move from IAM credentials to a web console session, as described here: Create a Console Session from IAM Credentials Selecting a Project name and type The first step when creating a project will be to select the name and the type. In this case, the type will be a “Runner project” Connecting to GitHub To link a GitHub repository to the build project, AWS provides three options: GitHub App OAuth App Personal Access Token (PAT) To minimize noise and complexity, the attacker can choose the simplest route: using a GitHub Personal Access Token (PAT). This PAT would grant access to a repository that the attacker controls (it can even be private). Once the GitHub connection is established, the attacker can select the Repository Using the Backdoored Role In the project settings, under the “Environment” section, the attacker can choose to use an existing service role; this is where they select the backdoored IAM role from Step 1. There is also a checkbox: “Allow AWS CodeBuild to modify this service role so it can be used with this build project.” If this box is checked, CodeBuild will automatically add extra permissions to the role (for things like writing logs to CloudWatch or interacting with CodeBuild resources). If the role already has admin-level permissions, the attacker won’t need to enable this. Note: The attacker will likely disable CloudWatch Logs to reduce visibility of their build runs. Step 3: Set Up the GitHub Action At this point, the attacker has: Created a CodeBuild project Linked it to a GitHub repository they control Configured it to assume a backdoored IAM rol
As I wrote in other blogs, such as Gaining AWS Persistence by Updating a SAML Identity Provider, when an attacker compromises an AWS account, one of the first tactics they will attempt is to gain persistence. This is because access obtained through temporary credentials may expire quickly, or the attacker may want to ensure continued access even if their initial foothold is discovered and removed.
That’s why I’m interested in exploring how legitimate AWS services can be abused for persistence. While creating IAM users and assigning them admin privileges is still a common and effective tactic (as we continue to observe in data from TrailDiscover), it’s noisy and easier to detect.
In this article, I’ll explore how an attacker could use/abuse AWS CodeBuild for persistence. I haven’t seen this particular approach documented in other research, but let me know if it’s been covered elsewhere.
⚠️ Disclaimer: This is NOT an AWS vulnerability. The technique described assumes the attacker has already compromised the account and has sufficient permissions. The goal is to expose how CodeBuild can be used to establish long-term access.
What is AWS CodeBuild?
CodeBuild is a fully managed continuous integration service that compiles source code, runs tests, and produces software packages for deployment. It supports custom build environments and integrates with popular tools such as GitHub.
In mid-2024, AWS introduced support for running self-hosted GitHub Actions runners using CodeBuild. This functionality allows you to configure a CodeBuild project to act as a runner for GitHub Actions.
This GitHub Actions integration is the option we are going to explore to see how an attacker can abuse it to gain persistence.
How an Attacker Could Abuse CodeBuild
The technique is surprisingly straightforward.
Because CodeBuild now supports self-hosted GitHub Actions runners, an attacker can:
- Configure a CodeBuild project to serve as a GitHub Actions runner.
- Link that project to a GitHub repository controlled by the attacker.
- Modify an IAM role to allow CodeBuild to assume it. Then this role will provide access to the AWS environment.
Let’s see these steps in more detail.
Step 1: Backdoor a Role
The attacker first needs to choose an IAM role to abuse. Ideally, this is an existing role with broad/admin privileges. In most cases, backdooring an existing role is often stealthier than creating a new one.
To “backdoor” the role, the attacker modifies its trust policy to allow the CodeBuild service to assume it:
{
"Effect": "Allow",
"Principal": {
"Service": "codebuild.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
This change allows CodeBuild builds to assume the role and inherit its permissions.
Note: Backdooring IAM roles for persistence is a well-known technique. It has been documented in research and observed in real-world incidents.
Step 2: Create a CodeBuild Project and Connect it to GitHub
Once the attacker has selected and backdoored an IAM role, the next step is to go into AWS CodeBuild and create a new build project linked to a GitHub repository they control.
While CodeBuild projects can be created via API or CLI, connecting a GitHub repository often requires going through the AWS Console. Fortunately for the attacker, it’s relatively easy to move from IAM credentials to a web console session, as described here: Create a Console Session from IAM Credentials
Selecting a Project name and type
The first step when creating a project will be to select the name and the type. In this case, the type will be a “Runner project”
Connecting to GitHub
To link a GitHub repository to the build project, AWS provides three options:
- GitHub App
- OAuth App
- Personal Access Token (PAT)
To minimize noise and complexity, the attacker can choose the simplest route: using a GitHub Personal Access Token (PAT). This PAT would grant access to a repository that the attacker controls (it can even be private).
Once the GitHub connection is established, the attacker can select the Repository
Using the Backdoored Role
In the project settings, under the “Environment” section, the attacker can choose to use an existing service role; this is where they select the backdoored IAM role from Step 1.
There is also a checkbox: “Allow AWS CodeBuild to modify this service role so it can be used with this build project.”
If this box is checked, CodeBuild will automatically add extra permissions to the role (for things like writing logs to CloudWatch or interacting with CodeBuild resources). If the role already has admin-level permissions, the attacker won’t need to enable this.
Note: The attacker will likely disable CloudWatch Logs to reduce visibility of their build runs.
Step 3: Set Up the GitHub Action
At this point, the attacker has:
- Created a CodeBuild project
- Linked it to a GitHub repository they control
- Configured it to assume a backdoored IAM role
The final step is to create a GitHub Action workflow that executes commands in the AWS environment using the assumed role.
The workflow can be extremely simple. Here’s an example that will run on every push and call the sts:GetCallerIdentity
API:
name: Backdoor AWS Access
on: [push]
jobs:
persistence-access:
runs-on:
- codebuild-MyGitHubRunner-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- name: Verify AWS Access
run: |
aws sts get-caller-identity
Here’s an example of the execution of this action:
This confirms that the workflow is successfully running in the AWS environment with permission from the backdoored role. From here, the attacker can execute any AWS commands their role allows.
With these three simple steps, the attacker now has persistent access to the AWS environment. Even if their initial credentials are revoked or the original attack path is closed.
Defending Against CodeBuild-Based Persistence
Let’s walk through what defenders can observe (and what they can’t):
CloudTrail Logs
During the setup phase, several key events will appear in CloudTrail, such as:
- UpdateAssumeRolePolicy: This event happens when the role is backdoored. We’ll see in the parameters the policyDocument containing codebuild.amazonaws.com
- ImportSourceCredentials: This event happens when the connection with GitHub is established, but it will depend on the configuration. In the case of a PAT in codebuild we’ll see a requesParameters that will look like:
- CreateProject: This event happens when the project is created.
- CreateWebhook: This event happens when the project is created and is meant to create the webhook that will trigger the build from GitHub.
- ProcessWebhook: This event happens when CodeBuild processes an event from GitHub
IAM Role Abuse Visibility
Once the GitHub Action runs, all actions will be performed under the backdoored IAM role.
- AssumeRole will happen every time the CodeBuild assumes the backdoored role.
- Source IP will appear as AWS infrastructure, or even an IP from your own VPC if the attacker configures CodeBuild to run in VPC mode.
- PrincipalID and arn will contain AWSCodeBuild
Access Analyzer Blind Spot
One crucial point: AWS Access Analyzer does not detect the GitHub connection as external access.
This is a major blind spot. Many security teams rely on Access Analyzer to detect unexpected trust relationships, but in this case, no alerts are triggered, even though the GitHub repo is controlling a role externally.
I confirmed this with the AWS security team, and “it is expected behavior”. But it certainly complicates detection.
Conclusion
This is just another example of how attackers can abuse legitimate AWS services for persistence. With over 200 AWS services available, attackers with highly privileged access have plenty of options to carry out their attacks. As defenders, we should be prepared for the day an attacker gains access to our environment, because a compromised account shouldn’t be game over.
For this reason, it is essential to understand diverse abuse paths, monitor CloudTrail logs, audit IAM trust relationships, and stay aware of service limitations (like the one described here with Access Analyzer). These are key actions that will allow us to detect and stop an attack before it’s too late.