def is_admin_policy(policy_doc): # TODO Use find_admin.py code instead of copy pasting it here. for stmt in make_list(policy_doc['Statement']): if stmt['Effect'] == 'Allow': actions = make_list(stmt.get('Action', [])) for action in actions: if action == '*' or action == '*:*' or action == 'iam:*': return True return False
def is_admin_policy(policy_doc): # TODO Use find_admin.py code instead of copy pasting it here. for stmt in make_list(policy_doc["Statement"]): if stmt["Effect"] == "Allow": actions = make_list(stmt.get("Action", [])) for action in actions: if action == "*" or action == "*:*" or action == "iam:*": return True return False
def is_admin_policy( policy_doc, location, findings, region, privs_to_look_for, include_retricted ): # This attempts to identify policies that directly allow admin privs, or indirectly through possible # privilege escalation (ex. iam:PutRolePolicy to add an admin policy to itself). # It is a best effort. It will have false negatives, meaning it may not identify an admin policy # when it should, and may have false positives. for stmt in make_list(policy_doc["Statement"]): if stmt["Effect"] == "Allow": # Check for use of NotAction, if they are allowing everything except a set, with no restrictions, # this is bad. not_actions = make_list(stmt.get("NotAction", [])) if ( not_actions != [] and stmt.get("Resource", "") == "*" and stmt.get("Condition", "") == "" ): if "iam:*" in not_actions: # This is used for PowerUsers, where they can do everything except IAM actions return False findings.add( Finding( region, "IAM_NOTACTION_ALLOW", location, resource_details={"Statement": stmt}, ) ) return True actions = make_list(stmt.get("Action", [])) for action in actions: if action == "*" or action == "*:*" or action == "iam:*": if stmt.get("Resource", "") != "*": findings.add( Finding( region, "IAM_UNEXPECTED_FORMAT", location, resource_details={ "comment": "This policy is oddly allowing all actions, but is restricted to a specific resource. This is a confusing way of restricting access that may be more privileged than expected.", "statement": stmt, }, ) ) return True # Look for privilege escalations if action_matches(action, privs_to_look_for): if include_retricted: return True elif ( stmt.get("Resource", "") == "*" and stmt.get("Condition", "") == "" ): return True return False
def is_admin_policy(policy_doc, location): # This attempts to identify policies that directly allow admin privs, or indirectly through possible # privilege escalation (ex. iam:PutRolePolicy to add an admin policy to itself). # It is a best effort. It will have false negatives, meaning it may not identify an admin policy # when it should, and may have false positives. for stmt in make_list(policy_doc['Statement']): if stmt['Effect'] == 'Allow': # Check for use of NotAction, if they are allowing everything except a set, with no restrictions, # this is bad. not_actions = make_list(stmt.get('NotAction', [])) if not_actions != [] and stmt.get( 'Resource', '') == '*' and stmt.get('Condition', '') == '': if 'iam:*' in not_actions: # This is used for PowerUsers, where they can do everything except IAM actions return False log_warning( 'Use of Allow and NotAction on Resource * is likely unwanted', location, [stmt]) return True actions = make_list(stmt.get('Action', [])) for action in actions: if action == '*' or action == '*:*' or action == 'iam:*': if stmt.get('Resource', '') != '*': log_warning('Admin policy not using a Resource of *', location, [stmt.get('Resource', '')]) return True # Look for privilege escalations if stmt.get('Resource', '') == '*' and stmt.get( 'Condition', '' ) == '' and (action_matches( action, [ 'iam:PutRolePolicy', 'iam:AddUserToGroup', 'iam:AddRoleToInstanceProfile', 'iam:AttachGroupPolicy', 'iam:AttachRolePolicy', 'iam:AttachUserPolicy', 'iam:ChangePassword', 'iam:CreateAccessKey', # Check for the rare possibility that an actor has a Deny policy on themselves, # so they try to escalate privs by removing that policy 'iam:DeleteUserPolicy', 'iam:DetachGroupPolicy', 'iam:DetachRolePolicy', 'iam:DetachUserPolicy' ])): return True return False
def audit_iam(findings, region): # By calling the code to find the admins, we'll exercise the code that finds problems. find_admins_in_account(region, findings) # By default we get the findings for the admins, but we can also look for specific # privileges, so we'll look for who has s3:ListAllMyBuckets and then only use those # findings that are for a compute resource having this privilege s3_listing_findings = Findings() s3_get_findings = Findings() # TODO Running find_admins_in_account is really slow, and now we're running it 3 times. # So figure out a way to run it once. find_admins_in_account(region, s3_listing_findings, privs_to_look_for=["s3:ListAllMyBuckets"]) find_admins_in_account(region, s3_get_findings, privs_to_look_for=["s3:GetObject"]) for flist in s3_listing_findings: if flist.issue_id != "IAM_UNEXPECTED_ADMIN_PRINCIPAL": continue services = make_list( flist.resource_details.get("Principal", {}).get("Service", "")) for service in services: if service in [ "config.amazonaws.com", "trustedadvisor.amazonaws.com", "macie.amazonaws.com", ]: continue # If we are here then we have a principal that can list S3 buckets, # and is associated with an unexpected service, # so check if they can read data from them as well for fget in s3_get_findings: if (fget.issue_id == "IAM_UNEXPECTED_ADMIN_PRINCIPAL" and fget.resource_id == flist.resource_id): # If we are here, then the principal can list S3 buckets and get objects # from them, and is not an unexpected service. Ensure we haven't already # recorded this as an unexpected admin. already_recorded = False for f in findings: if (f.resource_id == fget.resource_id and f.issue_id == "IAM_UNEXPECTED_ADMIN_PRINCIPAL"): already_recorded = True break if not already_recorded: flist.issue_id = "IAM_UNEXPECTED_S3_EXFIL_PRINCIPAL" findings.add(flist) # Don't record this multiple times if multiple services are listed break
def check_for_bad_policy(findings, region, arn, policy_text): for statement in make_list(policy_text["Statement"]): # Checking for signatures of the bad MFA policy from # https://web.archive.org/web/20170602002425/https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_users-self-manage-mfa-and-creds.html # and # https://github.com/awsdocs/iam-user-guide/blob/cfe14c674c494d07ba0ab952fe546fdd587da65d/doc_source/id_credentials_mfa_enable_virtual.md#permissions-required if ( statement.get("Sid", "") == "AllowIndividualUserToManageTheirOwnMFA" or statement.get("Sid", "") == "AllowIndividualUserToViewAndManageTheirOwnMFA" ): if "iam:DeactivateMFADevice" in make_list(statement.get("Action", [])): findings.add(Finding(region, "IAM_BAD_MFA_POLICY", arn, policy_text)) return elif ( statement.get("Sid", "") == "BlockAnyAccessOtherThanAboveUnlessSignedInWithMFA" ): if "iam:*" in make_list(statement.get("NotAction", [])): findings.add(Finding(region, "IAM_BAD_MFA_POLICY", arn, policy_text)) return
def is_admin_policy(policy_doc, location, findings, region): # This attempts to identify policies that directly allow admin privs, or indirectly through possible # privilege escalation (ex. iam:PutRolePolicy to add an admin policy to itself). # It is a best effort. It will have false negatives, meaning it may not identify an admin policy # when it should, and may have false positives. for stmt in make_list(policy_doc['Statement']): if stmt['Effect'] == 'Allow': # Check for use of NotAction, if they are allowing everything except a set, with no restrictions, # this is bad. not_actions = make_list(stmt.get('NotAction', [])) if not_actions != [] and stmt.get( 'Resource', '') == '*' and stmt.get('Condition', '') == '': if 'iam:*' in not_actions: # This is used for PowerUsers, where they can do everything except IAM actions return False findings.add( Finding(region, 'IAM_NOTACTION_ALLOW', location, resource_details={'Statement': stmt})) return True actions = make_list(stmt.get('Action', [])) for action in actions: if action == '*' or action == '*:*' or action == 'iam:*': if stmt.get('Resource', '') != '*': findings.add( Finding( region, 'IAM_UNEXPECTED_FORMAT', location, resource_details={ 'comment': 'This policy is oddly allowing all actions, but is restricted to a specific resource. This is a confusing way of restricting access that may be more privileged than expected.', 'statement': stmt })) return True # Look for privilege escalations if stmt.get('Resource', '') == '*' and stmt.get( 'Condition', '' ) == '' and (action_matches( action, [ 'iam:PutRolePolicy', 'iam:AddUserToGroup', 'iam:AddRoleToInstanceProfile', 'iam:AttachGroupPolicy', 'iam:AttachRolePolicy', 'iam:AttachUserPolicy', 'iam:ChangePassword', 'iam:CreateAccessKey', # Check for the rare possibility that an actor has a Deny policy on themselves, # so they try to escalate privs by removing that policy 'iam:DeleteUserPolicy', 'iam:DetachGroupPolicy', 'iam:DetachRolePolicy', 'iam:DetachUserPolicy' ])): return True return False
def is_admin_policy(policy_doc, location, findings, region): # This attempts to identify policies that directly allow admin privs, or indirectly through possible # privilege escalation (ex. iam:PutRolePolicy to add an admin policy to itself). # It is a best effort. It will have false negatives, meaning it may not identify an admin policy # when it should, and may have false positives. for stmt in make_list(policy_doc["Statement"]): if stmt["Effect"] == "Allow": # Check for use of NotAction, if they are allowing everything except a set, with no restrictions, # this is bad. not_actions = make_list(stmt.get("NotAction", [])) if ( not_actions != [] and stmt.get("Resource", "") == "*" and stmt.get("Condition", "") == "" ): if "iam:*" in not_actions: # This is used for PowerUsers, where they can do everything except IAM actions return False findings.add( Finding( region, "IAM_NOTACTION_ALLOW", location, resource_details={"Statement": stmt}, ) ) return True actions = make_list(stmt.get("Action", [])) for action in actions: if action == "*" or action == "*:*" or action == "iam:*": if stmt.get("Resource", "") != "*": findings.add( Finding( region, "IAM_UNEXPECTED_FORMAT", location, resource_details={ "comment": "This policy is oddly allowing all actions, but is restricted to a specific resource. This is a confusing way of restricting access that may be more privileged than expected.", "statement": stmt, }, ) ) return True # Look for privilege escalations if ( stmt.get("Resource", "") == "*" and stmt.get("Condition", "") == "" and ( action_matches( action, [ "iam:PutRolePolicy", "iam:AddUserToGroup", "iam:AddRoleToInstanceProfile", "iam:AttachGroupPolicy", "iam:AttachRolePolicy", "iam:AttachUserPolicy", "iam:ChangePassword", "iam:CreateAccessKey", # Check for the rare possibility that an actor has a Deny policy on themselves, # so they try to escalate privs by removing that policy "iam:DeleteUserPolicy", "iam:DetachGroupPolicy", "iam:DetachRolePolicy", "iam:DetachUserPolicy", ], ) ) ): return True return False
def test_make_list(self): assert_equal(["hello"], make_list("hello")) assert_equal(["hello"], make_list(["hello"]))