From e6310c32ac50e4455a030b56d27d68f2dbf871f1 Mon Sep 17 00:00:00 2001 From: Sergio Garcia <38561120+sergargar@users.noreply.github.com> Date: Tue, 17 Jan 2023 12:17:37 +0100 Subject: [PATCH] feat(check): add iam_role_cross_service_confused_deputy_prevention check (#1710) Co-authored-by: sergargar --- .../__init__.py | 0 ...e_confused_deputy_prevention.metadata.json | 36 +++++++ ...ross_service_confused_deputy_prevention.py | 66 +++++++++++++ .../providers/aws/services/iam/iam_service.py | 22 ++++- ...service_confused_deputy_prevention_test.py | 99 +++++++++++++++++++ .../aws/services/iam/iam_service_test.py | 40 ++++++-- 6 files changed, 253 insertions(+), 10 deletions(-) create mode 100644 prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/__init__.py create mode 100644 prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.metadata.json create mode 100644 prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.py create mode 100644 tests/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention_test.py diff --git a/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/__init__.py b/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.metadata.json b/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.metadata.json new file mode 100644 index 00000000..6d429b80 --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "aws", + "CheckID": "iam_role_cross_service_confused_deputy_prevention", + "CheckTitle": "Ensure IAM Service Roles prevents against a cross-service confused deputy attack", + "CheckType": [], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "high", + "ResourceType": "AwsIamPolicy", + "Description": "Ensure IAM Service Roles prevents against a cross-service confused deputy attack", + "Risk": "Allow attackers to gain unauthorized access to resources", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Use the aws:SourceArn and aws:SourceAccount global condition context keys in resource-based policies to limit the permissions that a service has to a specific resource", + "Url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html#cross-service-confused-deputy-prevention" + } + }, + "Categories": [ + "trustboundaries" + ], + "Tags": { + "Tag1Key": "value", + "Tag2Key": "value" + }, + "DependsOn": [], + "RelatedTo": [], + "Notes": "CAF Security Epic: IAM" +} diff --git a/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.py b/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.py new file mode 100644 index 00000000..a17cde4d --- /dev/null +++ b/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.py @@ -0,0 +1,66 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.iam_client import iam_client + + +class iam_role_cross_service_confused_deputy_prevention(Check): + def execute(self) -> Check_Report_AWS: + findings = [] + for role in iam_client.roles: + # This check should only be performed against service roles + if role.is_service_role: + report = Check_Report_AWS(self.metadata()) + report.region = iam_client.region + report.resource_arn = role.arn + report.resource_id = role.name + report.status = "FAIL" + report.status_extended = f"IAM Service Role {role.name} prevents against a cross-service confused deputy attack" + for statement in role.assume_role_policy["Statement"]: + if ( + statement["Effect"] == "Allow" + and ( + "sts:AssumeRole" in statement["Action"] + or "sts:*" in statement["Action"] + or "*" in statement["Action"] + ) + # Need to make sure we are checking the part of the assume role policy document that provides a service access + and "Service" in statement["Principal"] + # Check to see if the appropriate condition statements have been implemented + and "Condition" in statement + and ( + ( + "StringEquals" in statement["Condition"] + and "aws:SourceAccount" + in statement["Condition"]["StringEquals"] + and iam_client.account + in str( + statement["Condition"]["StringEquals"][ + "aws:SourceAccount" + ] + ) + ) + or ( + "ArnEquals" in statement["Condition"] + and "aws:SourceArn" + in statement["Condition"]["ArnEquals"] + and iam_client.account + in str( + statement["Condition"]["ArnEquals"]["aws:SourceArn"] + ) + ) + or ( + "ArnLike" in statement["Condition"] + and "aws:SourceArn" in statement["Condition"]["ArnLike"] + and iam_client.account + in str( + statement["Condition"]["ArnEquals"]["aws:SourceArn"] + ) + ) + ) + ): + report.status = "PASS" + report.status_extended = f"IAM Service Role {role.name} does not prevent against a cross-service confused deputy attack" + break + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/iam/iam_service.py b/prowler/providers/aws/services/iam/iam_service.py index 54b9c418..75a3ab9f 100644 --- a/prowler/providers/aws/services/iam/iam_service.py +++ b/prowler/providers/aws/services/iam/iam_service.py @@ -6,6 +6,23 @@ from prowler.lib.logger import logger from prowler.providers.aws.aws_provider import generate_regional_clients +def is_service_role(role): + if "Statement" in role["AssumeRolePolicyDocument"]: + for statement in role["AssumeRolePolicyDocument"]["Statement"]: + if ( + statement["Effect"] == "Allow" + and ( + "sts:AssumeRole" in statement["Action"] + or "sts:*" in statement["Action"] + or "*" in statement["Action"] + ) + # This is what defines a service role + and "Service" in statement["Principal"] + ): + return True + return False + + ################## IAM class IAM: def __init__(self, audit_info): @@ -58,6 +75,7 @@ class IAM: name=role["RoleName"], arn=role["Arn"], assume_role_policy=role["AssumeRolePolicyDocument"], + is_service_role=is_service_role(role), ) ) return roles @@ -436,11 +454,13 @@ class Role: name: str arn: str assume_role_policy: dict + is_service_role: bool - def __init__(self, name, arn, assume_role_policy): + def __init__(self, name, arn, assume_role_policy, is_service_role): self.name = name self.arn = arn self.assume_role_policy = assume_role_policy + self.is_service_role = is_service_role @dataclass diff --git a/tests/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention_test.py b/tests/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention_test.py new file mode 100644 index 00000000..ada5b25e --- /dev/null +++ b/tests/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention_test.py @@ -0,0 +1,99 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_iam + +AWS_REGION = "us-east-1" +AWS_ACCOUNT_ID = "123456789012" + + +class Test_iam_role_cross_service_confused_deputy_prevention: + @mock_iam + def test_iam_service_role_without_cross_service_confused_deputy_prevention(self): + iam_client = client("iam", region_name=AWS_REGION) + policy_document = { + "Version": "2008-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "ec2.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + response = iam_client.create_role( + RoleName="test", + AssumeRolePolicyDocument=dumps(policy_document), + ) + + from prowler.providers.aws.lib.audit_info.audit_info import current_audit_info + from prowler.providers.aws.services.iam.iam_service import IAM + + current_audit_info.audited_partition = "aws" + current_audit_info.audited_account = AWS_ACCOUNT_ID + with mock.patch( + "prowler.providers.aws.services.iam.iam_role_cross_service_confused_deputy_prevention.iam_role_cross_service_confused_deputy_prevention.iam_client", + new=IAM(current_audit_info), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_role_cross_service_confused_deputy_prevention.iam_role_cross_service_confused_deputy_prevention import ( + iam_role_cross_service_confused_deputy_prevention, + ) + + check = iam_role_cross_service_confused_deputy_prevention() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "IAM Service Role test prevents against a cross-service confused deputy attack" + ) + assert result[0].resource_id == "test" + assert result[0].resource_arn == response["Role"]["Arn"] + + @mock_iam + def test_iam_service_role_with_cross_service_confused_deputy_prevention(self): + iam_client = client("iam", region_name=AWS_REGION) + policy_document = { + "Version": "2008-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "workspaces.amazonaws.com"}, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": {"aws:SourceAccount": [AWS_ACCOUNT_ID]} + }, + } + ], + } + response = iam_client.create_role( + RoleName="test", + AssumeRolePolicyDocument=dumps(policy_document), + ) + + from prowler.providers.aws.lib.audit_info.audit_info import current_audit_info + from prowler.providers.aws.services.iam.iam_service import IAM + + current_audit_info.audited_partition = "aws" + current_audit_info.audited_account = AWS_ACCOUNT_ID + with mock.patch( + "prowler.providers.aws.services.iam.iam_role_cross_service_confused_deputy_prevention.iam_role_cross_service_confused_deputy_prevention.iam_client", + new=IAM(current_audit_info), + ): + # Test Check + from prowler.providers.aws.services.iam.iam_role_cross_service_confused_deputy_prevention.iam_role_cross_service_confused_deputy_prevention import ( + iam_role_cross_service_confused_deputy_prevention, + ) + + check = iam_role_cross_service_confused_deputy_prevention() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "IAM Service Role test does not prevent against a cross-service confused deputy attack" + ) + assert result[0].resource_id == "test" + assert result[0].resource_arn == response["Role"]["Arn"] diff --git a/tests/providers/aws/services/iam/iam_service_test.py b/tests/providers/aws/services/iam/iam_service_test.py index 7407d873..2080d371 100644 --- a/tests/providers/aws/services/iam/iam_service_test.py +++ b/tests/providers/aws/services/iam/iam_service_test.py @@ -6,7 +6,7 @@ from freezegun import freeze_time from moto import mock_iam from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info -from prowler.providers.aws.services.iam.iam_service import IAM +from prowler.providers.aws.services.iam.iam_service import IAM, is_service_role AWS_ACCOUNT_NUMBER = 123456789012 TEST_DATETIME = "2023-01-01T12:01:01+00:00" @@ -223,20 +223,42 @@ class Test_IAM_Service: # Generate IAM Client iam_client = client("iam") # Create 2 IAM Roles - iam_client.create_role( - RoleName="role1", - AssumeRolePolicyDocument="string", - ) - iam_client.create_role( - RoleName="role2", - AssumeRolePolicyDocument="string", - ) + service_policy_document = { + "Version": "2008-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "ec2.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"}, + "Action": "sts:AssumeRole", + } + ], + } + service_role = iam_client.create_role( + RoleName="test-1", + AssumeRolePolicyDocument=dumps(service_policy_document), + )["Role"] + role = iam_client.create_role( + RoleName="test-2", + AssumeRolePolicyDocument=dumps(policy_document), + )["Role"] # IAM client for this test class audit_info = self.set_mocked_audit_info() iam = IAM(audit_info) assert len(iam.roles) == len(iam_client.list_roles()["Roles"]) + assert is_service_role(service_role) + assert not is_service_role(role) # Test IAM Get Groups @mock_iam