def gen_overprivileged_instance_profile_findings( graph: Graph) -> List[Finding]: """Generates findings related to risk from EC2 instances being loaded with overprivileged instance profiles.""" result = [] affected_roles = [] for node in graph.nodes: if ':role/' in node.arn and node.is_admin and len( node.instance_profile) > 0: affected_roles.append(node) if len(affected_roles) > 0: description_preamble = 'In AWS, EC2 instances can be given an instance profile. These instance profiles ' \ 'are associated with an IAM Role, and grants access to the permissions of the IAM ' \ 'Role. Because EC2 instances are at a higher risk of exposure and compromise, both ' \ 'to external attackers and authorized users in the AWS account, they should not have ' \ 'access to administrative privileges. The following IAM Roles have administrative ' \ 'permissions and are associated with an instance profile:\n\n' description_body = '' for node in affected_roles: description_body += '* {}\n'.format(node.searchable_name()) result.append( Finding( 'Instance {} Administrator Privileges'.format( 'Profiles Have' if len(affected_roles) > 1 else 'Profile Has'), 'High', 'If an instance with the noted instance profile(s) is compromised, then the AWS account as a whole is at ' 'risk of compromise.', description_preamble + description_body, 'Reduce the scope of permissions attached to the noted instance profile(s).' )) return result
def gen_overprivileged_function_findings(graph: Graph) -> List[Finding]: """Generates findings related to risk from Lambda functions being loaded with overprivileged roles""" result = [] affected_roles = [] for node in graph.nodes: if ':role/' in node.arn and node.is_admin: if query_interface.resource_policy_authorization('lambda.amazonaws.com', arns.get_account_id(node.arn), node.trust_policy, 'sts:AssumeRole', node.arn, {})\ == query_interface.ResourcePolicyEvalResult.SERVICE_MATCH: affected_roles.append(node) if len(affected_roles) > 0: description_preamble = 'In AWS, Lambda functions can be assigned an IAM Role to use during execution. These ' \ 'IAM Roles give the function access to call the AWS API with the permissions of the ' \ 'IAM Role, depending on the policies attached to it. If the Lambda function can be ' \ 'compromised, and the attacker can alter the code it executes, the attacker could ' \ 'make AWS API calls with the IAM Role\'s permissions. The following IAM Roles have ' \ 'administrative privileges, and can be passed to Lambda functions:\n\n' description_body = '' for node in affected_roles: description_body += '* {}\n'.format(node.searchable_name()) result.append(Finding( 'IAM Roles Available to Lambda Functions Have Administrative Privileges' if len(affected_roles) > 1 else 'IAM Role Available to Lambda Functions Has Administrative Privileges', 'Medium', 'If an attacker can inject code or commands into the function, or if a lower-privileged principal can ' 'alter the function, the AWS account as a whole could be compromised.', description_preamble + description_body, 'Reduce the scope of permissions attached to the noted IAM Role(s).' )) return result
def gen_os_lpe_finding(graph: Graph) -> List[Finding]: """Generates findings related to risk of SSM permissions being misconfigured (local priv-esc on the host)""" result = [] affected_roles = [] for node in graph.nodes: if ':role/' in node.arn and node.instance_profile is not None and len( node.instance_profile) > 0: # https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-setting-up-messageAPIs.html if query_interface.local_check_authorization( node, 'ssmmessages:*', '*', {}): if query_interface.local_check_authorization( node, 'ssm:SendCommand', '*', {}): affected_roles.append(node) elif query_interface.local_check_authorization( node, 'ssm:StartSession', '*', {}): affected_roles.append(node) if len(affected_roles) > 0: description_preamble = 'In AWS EC2, instances can be assigned instance profiles. An instance profile is tied ' \ 'to a single IAM Role. The instance profile can be used to access the AWS API with ' \ 'the permissions of the IAM Role. If the IAM Role has permission to call certain SSM ' \ 'actions such as `ssm:SendCommand` or `ssm:StartSession`, the instance profile ' \ 'can be used to invoke commands on other instances or itself.' \ '\n' \ '\n' \ 'Because the SSM Agent runs with the highest permissions on their hosts ' \ '(root or SYSTEM), this can be a way for attackers to pivot and compromise other ' \ 'instances, or escalate privileges on the local machine. The following IAM Roles ' \ 'are attached to at least one instance profile and have permissions with the ' \ 'aforementioned risk:' \ '\n' \ '\n' description_body = '' for node in affected_roles: description_body += '* {}\n'.format(node.searchable_name()) result.append( Finding( 'IAM Roles With Unsafe SSM Permissions' if len(affected_roles) > 1 else 'IAM Role With Unsafe SSM Permissions', 'Medium', 'If an attacker gains access to an instance with the unsafe permissions, they could escalate privileges ' 'on its current host or compromise other hosts.', description_preamble + description_body, 'Reduce the scope of permissions attached to the noted IAM Role(s).' )) return result
def gen_circular_access_finding(graph: Graph) -> List[Finding]: """Generates findings related to the risk of a set of nodes that can circularly access each other. Admins excluded.""" result = [] cycles = [] for node in graph.nodes: if node.is_admin: continue cycle_result = _find_cycle(graph, node) if cycle_result is not None: cycles.append(cycle_result) if len(cycles) > 0: description_preamble = 'In AWS, an IAM Principal with a specific set of permissions can gain access ' \ 'to another principal, such as when an IAM User has permission to call ' \ '`sts:AssumeRole` for an IAM Role. Principal Mapper tracks these connections as ' \ 'Nodes (a.k.a. Vertices) and Edges in a Graph.' \ '\n' \ '\n' \ 'However, there may be instances where nodes can access each other in a circular ' \ 'manner. This presents a risk in the account if an attacker gains access to one ' \ 'of the principals in a cycle. An attacker can abuse that access to pivot between ' \ 'each of the principals in a cycle. This can be used to evade detection or ' \ 'persist access to an AWS account. The following cycles were identified:' \ '\n' \ '\n' description_body = '' for cycle in cycles: description_body += '* {}\n'.format( ' -> '.join([x.searchable_name() for x in cycle] + [cycle[0].searchable_name()])) result.append( Finding( 'IAM Principals with Circular Access', 'Low', 'If an attacker gains access to one of the identified principals, they can potentially evade detections ' 'or persist access.', description_preamble + description_body, 'Break the cycle of access by altering/removing permissions assigned to one of the noted principals.' )) return result
def gen_privesc_findings(graph: Graph) -> List[Finding]: """Generates findings related to privilege escalation risks.""" result = [] node_path_list = [] for node in graph.nodes: if node.is_admin: continue # skip current admins privesc_res, edge_list = can_privesc(graph, node) if privesc_res: node_path_list.append((node, edge_list)) if len(node_path_list) > 0: description_preamble = 'In AWS, IAM Principals such as IAM Users or IAM Roles have their permissions defined ' \ 'using IAM Policies. These policies describe different actions, resources, and ' \ 'conditions where the principal can make a given API call to a service.\n\n' \ 'Administrative principals can call any action with any resource, as in the ' \ 'AdministratorAccess AWS-managed policy. However, some permissions may allow another ' \ 'non-administrative principal to gain access to an administrative principal. ' \ 'This represents a privilege escalation risk.\n\n' \ 'The following principals could escalate privileges:\n\n' description_body = '' for node, edge_list in node_path_list: end_of_list = edge_list[-1].destination description_body += '* {} can escalate privileges by accessing the administrative principal {}:\n'.format( node.searchable_name(), end_of_list.searchable_name()) for edge in edge_list: description_body += ' * {}\n'.format(edge.describe_edge()) description_body += '\n' result.append( Finding( 'IAM {} Can Escalate Privileges'.format( 'Principals' if len(node_path_list) > 1 else 'Principal'), 'High', 'A lower-privilege IAM User or Role is able to gain administrative privileges. This could lead to the ' 'lower-privilege principal being used to compromise the account and its resources.', description_preamble + description_body, 'Review the IAM Policies that are applicable to the affected IAM User(s) or Role(s). Either reduce the ' 'permissions of the administrative principal(s), or reduce the permissions of the principal(s) that can ' 'access the administrative principals.')) return result
def gen_mfa_actions_findings(graph: Graph) -> List[Finding]: """Generates findings related to risk from IAM Users able to call sensitive actions without needing MFA.""" result = [] affected_users = [] for node in graph.nodes: if ':user/' in node.arn and node.is_admin and node.access_keys > 0: # Check if the given admin user with access keys can call sensitive actions without MFA # TODO: Check for other actions in here? actions = [ 'iam:CreateUser', 'iam:CreateRole', 'iam:CreateGroup', 'iam:PutUserPolicy', 'iam:PutRolePolicy', 'iam:PutGroupPolicy', 'iam:AttachUserPolicy', 'iam:AttachRolePolicy', 'iam:AttachGroupPolicy', 'sts:AssumeRole' ] if _can_call_without_mfa(node, actions): affected_users.append(node) if len(affected_users) > 0: description_preamble = 'In AWS, IAM Users can be configured to use an MFA device. When an IAM User has MFA ' \ 'enabled, they are required to provide the second factor of authentication when they ' \ 'log in to the AWS Console. However, unless there is a specific IAM policy attached ' \ 'to the user, they will not need to provide a second factor of authentication when ' \ 'making API calls.\n\nThe following administrative IAM Users have at least one set of ' \ 'access keys, and can call sensitive actions to alter permissions or add users ' \ 'without using a second factor of authentication:\n\n' description_body = '' for node in affected_users: description_body += '* {}\n'.format(node.searchable_name()) result.append( Finding( 'Administrative IAM {} Can Call Sensitive Actions Without MFA'. format('Users' if len(affected_users) > 1 else 'User'), 'Medium', 'An adminstrative IAM User is able to call sensitive actions, such as creating more principals or ' 'modifying permissions, without using MFA.', description_preamble + description_body, 'Implement and attach an IAM Policy to the noted user(s) that rejects requests when MFA is not used.' )) return result
def gen_admin_users_without_mfa_finding(graph: Graph) -> List[Finding]: """Generates findings related to IAM Users that have administrative privileges in an AWS account but no MFA device configured.""" result = [] affected_nodes = [] for node in graph.nodes: if node.searchable_name().startswith( 'user/') and node.is_admin and not node.has_mfa: affected_nodes.append(node) if len(affected_nodes) > 0: description_preamble = 'In AWS, an IAM User can be assigned a device for Multi-Factor Authentication (MFA). ' \ 'When an IAM User is assigned an MFA device, they are required to provide an extra ' \ 'factor of authentication when logging in to the AWS Console. It is also possible to ' \ 'create IAM Policies that impose extra restrictions on the permissions of IAM Users ' \ 'depending on whether or not they have authenticated with MFA when using the AWS API. ' \ 'Any IAM User with administrative privileges should be configured to have an MFA ' \ 'device. \n\n' \ 'The following IAM Users with administrative privileges do not have an MFA ' \ 'device configured:' \ '\n' \ '\n' user_list = [] for node in affected_nodes: user_list.append('* {}'.format(node.searchable_name())) description_body = '\n'.join(user_list) result.append( Finding( 'IAM Users With Administrative Permissions But No MFA Device', 'Medium', 'If an attacker gains access to any of the noted sensitive IAM Users, there is no secondary layer of ' 'protection in place to prevent the AWS from being compromised.', description_preamble + description_body, 'Assign an MFA device to each of the noted IAM Users.')) return result
def gen_overprivileged_stack_findings(graph: Graph) -> List[Finding]: """Generates findings related to risk from CloudFormation stacks being loaded with overprivileged roles""" result = [] affected_roles = [] for node in graph.nodes: if ':role/' in node.arn and node.is_admin: if query_interface.resource_policy_authorization('cloudformation.amazonaws.com', arns.get_account_id(node.arn), node.trust_policy, 'sts:AssumeRole', node.arn, {}) == \ query_interface.ResourcePolicyEvalResult.SERVICE_MATCH: affected_roles.append(node) if len(affected_roles) > 0: description_preamble = 'In AWS, CloudFormation stacks can be given an IAM Role. When a stack has an IAM ' \ 'Role, it can use that IAM Role to make AWS API calls to create the resources ' \ 'defined in the template for that stack. If the IAM Role has administrator access ' \ 'to the account, and an attacker is able to make the right CloudFormation API calls, ' \ 'they would be able to use the IAM Role to escalate privileges and compromise the ' \ 'account as a whole. The following IAM Roles can be used in CloudFormation and ' \ 'have administrative privileges:\n\n' description_body = '' for node in affected_roles: description_body += '* {}\n'.format(node.searchable_name()) result.append( Finding( 'IAM Roles Available to CloudFormation Stacks Have Administrative Privileges' if len(affected_roles) > 1 else 'IAM Role Available to CloudFormation Stacks Has Administrative Privileges', 'Low', 'If an attacker has the right permissions in the AWS Account, they can grant themselves adminstrative ' 'access to the account to compromise the account.', description_preamble + description_body, 'Reduce the scope of permissions attached to the noted IAM Role(s).' )) return result
def gen_resources_with_potential_confused_deputies(graph: Graph) -> List[Finding]: """Generates findings related to AWS resources that allow access to AWS services (via resource policy) that may not correctly verify which AWS account is the true source of a request that affects the given resource. Primarily works by inspecting resource policies and making sure that access is guarded with a condition using aws:SourceAccount.""" result = [] resource_service_action_map = { 's3': { 'serverlessrepo.amazonaws.com': [ 's3:GetObject' ] } } affected_policies = [] # type: List[Tuple[str, str, str]] for resource_type in resource_service_action_map.keys(): for policy in graph.policies: if arns.get_service(policy.arn) == resource_type: for service, action_list in resource_service_action_map[resource_type].items(): available_actions = [] for action in action_list: rpa_result = resource_policy_authorization( service, graph.metadata['account_id'], policy.policy_doc, action, policy.arn, { 'aws:SourceAccount': '000000000000' } ) if rpa_result.SERVICE_MATCH: available_actions.append(action) if len(available_actions) > 0: affected_policies.append( (policy.arn, service, ' | '.join(available_actions)) ) if len(affected_policies) > 0: desc_list_str = '\n'.join(['* With service {}, the resource {} for the action(s): {}'.format(y, x, z) for x, y, z in affected_policies]) result.append( Finding( 'Resources With A Potential Confused-Deputy Risk', 'Medium', 'Depending on the affected resources and services, an attacker may be able to execute read or write ' 'operations on the resources from another AWS account.', 'In AWS, certain services will create and use resources in the customer\'s own AWS account. This may ' 'be controlled using a resource policy that grants access to the service that created the resource ' 'in the customer\'s AWS account. However, some services require customers to use the ' '`${aws:SourceAccount}` condition context key to control access to the account resource from the ' 'service. In other words, to prevent the service from accessing the resource on the behalf of ' 'another customer, the resource needs a resource policy that allow-lists the true "source" of a ' 'request.\n\n' 'The following AWS services and resources could allow an external account to potentially gain ' 'read/write access to the resources:\n\n' + desc_list_str, 'Update the resource policy for all affected resources, and ensure that all statements granting ' 'access to AWS services check against the `${aws:SourceAccount}` condition context key when ' 'appropriate.' ) ) return result