Skip to content

Commit f73c1f5

Browse files
authored
Merge pull request duo-labs#96 from 0xdabbad00/find_admins
Adds find_admins command
2 parents c3bb8a2 + 9203078 commit f73c1f5

File tree

2 files changed

+218
-0
lines changed

2 files changed

+218
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ python cloudmapper.py collect --account-name my_account
146146
- `prepare`/`webserver`: See [Network Visualizations](docs/network_visualizations.md)
147147
- `stats`: Show counts of resources for accounts. More details [here](https://summitroute.com/blog/2018/06/06/cloudmapper_stats/).
148148
- `sg_ips`: Get geoip info on CIDRs trusted in Security Groups. More details [here](https://summitroute.com/blog/2018/06/12/cloudmapper_sg_ips/).
149+
- `find_admins`: Look at IAM policies to identify admin users and roles and spot potential IAM issues. More details [here](https://summitroute.com/blog/2018/06/12/cloudmapper_find_admins/).
149150

150151
Licenses
151152
--------

commands/find_admins.py

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import os.path
2+
import json
3+
import re
4+
from policyuniverse.policy import Policy
5+
from shared.common import parse_arguments, make_list, log_info, log_warning
6+
7+
__description__ = "Find admins in accounts"
8+
9+
def get_current_policy_doc(policy):
10+
for doc in policy['PolicyVersionList']:
11+
if doc['IsDefaultVersion']:
12+
return doc['Document']
13+
raise Exception('No default document version in policy {}'.format(policy['Arn']))
14+
15+
16+
def action_matches(action_from_policy, actions_to_match_against):
17+
action_from_policy = action_from_policy.lower()
18+
action = '^' + action_from_policy.replace('*', '.*') + '$'
19+
for action_to_match_against in actions_to_match_against:
20+
action_to_match_against = action_to_match_against.lower()
21+
if re.match(action, action_to_match_against):
22+
return True
23+
return False
24+
25+
26+
def policy_action_count(policy_doc, location):
27+
# Counts how many unrestricted actions a policy grants
28+
policy = Policy(policy_doc)
29+
actions_count = 0
30+
for stmt in policy.statements:
31+
if (stmt.effect == 'Allow' and
32+
len(stmt.condition_entries) == 0 and
33+
stmt.resources == set('*')):
34+
actions_count += len(stmt.actions_expanded)
35+
return actions_count
36+
37+
38+
def is_admin_policy(policy_doc, location):
39+
# This attempts to identify policies that directly allow admin privs, or indirectly through possible
40+
# privilege escalation (ex. iam:PutRolePolicy to add an admin policy to itself).
41+
# It is a best effort. It will have false negatives, meaning it may not identify an admin policy
42+
# when it should, and may have false positives.
43+
for stmt in make_list(policy_doc['Statement']):
44+
if stmt['Effect'] == 'Allow':
45+
# Check for use of NotAction, if they are allowing everything except a set, with no restrictions,
46+
# this is bad.
47+
not_actions = make_list(stmt.get('NotAction', []))
48+
if not_actions != [] and stmt.get('Resource', '') == '*' and stmt.get('Condition', '') == '':
49+
if 'iam:*' in not_actions:
50+
# This is used for PowerUsers, where they can do everything except IAM actions
51+
return False
52+
log_warning('Use of Allow and NotAction on Resource * is likely unwanted', location, [stmt])
53+
return True
54+
55+
actions = make_list(stmt.get('Action', []))
56+
for action in actions:
57+
if action == '*' or action == '*:*' or action == 'iam:*':
58+
if stmt.get('Resource', '') != '*':
59+
log_warning('Admin policy not using a Resource of *', location, [stmt.get('Resource', '')])
60+
return True
61+
# Look for privilege escalations
62+
if stmt.get('Resource', '') == '*' and stmt.get('Condition', '') == '' and (
63+
action_matches(action, [
64+
'iam:PutRolePolicy',
65+
'iam:AddUserToGroup',
66+
'iam:AddRoleToInstanceProfile',
67+
'iam:AttachGroupPolicy',
68+
'iam:AttachRolePolicy',
69+
'iam:AttachUserPolicy',
70+
'iam:ChangePassword',
71+
'iam:CreateAccessKey',
72+
# Check for the rare possibility that an actor has a Deny policy on themselves,
73+
# so they try to escalate privs by removing that policy
74+
'iam:DeleteUserPolicy',
75+
'iam:DetachGroupPolicy',
76+
'iam:DetachRolePolicy',
77+
'iam:DetachUserPolicy']
78+
)
79+
):
80+
return True
81+
82+
return False
83+
84+
85+
def record_admin(admins, account_name, actor_type, actor_name):
86+
admins.append({'account': account_name, 'type': actor_type, 'name': actor_name})
87+
88+
def find_admins(accounts, config):
89+
admins = []
90+
for account in accounts:
91+
location = {'account': account['name']}
92+
93+
try:
94+
file_name = 'account-data/{}/{}/{}'.format(
95+
account['name'],
96+
'us-east-1',
97+
'iam-get-account-authorization-details.json')
98+
iam = json.load(open(file_name))
99+
except:
100+
if not os.path.exists('account-data/{}/'.format(account['name'])):
101+
# Account has not been collected from, so silently skip it
102+
continue
103+
log_error('Problem opening iam data, skipping account', location, [file_name])
104+
continue
105+
106+
admin_policies = []
107+
policy_action_counts = {}
108+
for policy in iam['Policies']:
109+
location['policy'] = policy['Arn']
110+
policy_doc = get_current_policy_doc(policy)
111+
policy_action_counts[policy['Arn']] = policy_action_count(policy_doc, location)
112+
113+
if is_admin_policy(policy_doc, location):
114+
admin_policies.append(policy['Arn'])
115+
if ('arn:aws:iam::aws:policy/AdministratorAccess' in policy['Arn'] or
116+
'arn:aws:iam::aws:policy/IAMFullAccess' in policy['Arn']):
117+
# Ignore the admin policies that are obviously admin
118+
continue
119+
if 'arn:aws:iam::aws:policy' in policy['Arn']:
120+
# Detects the deprecated `AmazonElasticTranscoderFullAccess`
121+
log_warning('AWS managed policy allows admin', location, [policy_doc])
122+
continue
123+
log_warning('Custom policy allows admin', location, [policy_doc])
124+
location.pop('policy', None)
125+
126+
# Identify roles that allow admin access
127+
for role in iam['RoleDetailList']:
128+
location['role'] = role['Arn']
129+
reasons = []
130+
131+
# Check if this role is an admin
132+
for policy in role['AttachedManagedPolicies']:
133+
if policy['PolicyArn'] in admin_policies:
134+
reasons.append('Attached managed policy: {}'.format(policy['PolicyArn']))
135+
for policy in role['RolePolicyList']:
136+
policy_doc = policy['PolicyDocument']
137+
if is_admin_policy(policy_doc, location):
138+
reasons.append('Custom policy: {}'.format(policy['PolicyName']))
139+
log_warning('Role has custom policy allowing admin', location, [policy_doc])
140+
141+
if len(reasons) != 0:
142+
for stmt in role['AssumeRolePolicyDocument']['Statement']:
143+
if stmt['Effect'] != 'Allow':
144+
log_warning('Unexpected Effect in AssumeRolePolicyDocument', location, [stmt])
145+
continue
146+
147+
if stmt['Action'] == 'sts:AssumeRole':
148+
if 'AWS' not in stmt['Principal'] or len(stmt['Principal']) != 1:
149+
log_warning('Unexpected Principal in AssumeRolePolicyDocument', location, [stmt['Principal']])
150+
elif stmt['Action'] == 'sts:AssumeRoleWithSAML':
151+
continue
152+
else:
153+
log_warning('Unexpected Action in AssumeRolePolicyDocument', location, [stmt])
154+
log_info('Role is admin', location, reasons)
155+
record_admin(admins, account['name'], 'role', role['RoleName'])
156+
# TODO Should check users or other roles allowed to assume this role to show they are admins
157+
location.pop('role', None)
158+
159+
# Identify groups that allow admin access
160+
admin_groups = []
161+
for group in iam['GroupDetailList']:
162+
location['group'] = group['Arn']
163+
is_admin = False
164+
for policy in group['AttachedManagedPolicies']:
165+
if policy['PolicyArn'] in admin_policies:
166+
is_admin = True
167+
if 'admin' not in group['Arn'].lower():
168+
log_warning('Group is admin, but name does not indicate it is', location)
169+
for policy in group['GroupPolicyList']:
170+
policy_doc = policy['PolicyDocument']
171+
if is_admin_policy(policy_doc, location):
172+
is_admin = True
173+
log_warning('Group has custom policy allowing admin', location, [policy_doc])
174+
if is_admin:
175+
admin_groups.append(group['GroupName'])
176+
location.pop('group', None)
177+
178+
# Check users
179+
for user in iam['UserDetailList']:
180+
location['user'] = user['UserName']
181+
reasons = []
182+
183+
# Check the different ways in which the user could be an admin
184+
for policy in user['AttachedManagedPolicies']:
185+
if policy['PolicyArn'] in admin_policies:
186+
reasons.append('Attached managed policy: {}'.format(policy['PolicyArn']))
187+
for policy in user.get('UserPolicyList', []):
188+
policy_doc = policy['PolicyDocument']
189+
if is_admin_policy(policy_doc, location):
190+
reasons.append('Custom user policy: {}'.format(policy['PolicyName']))
191+
log_warning('User has custom policy allowing admin', location)
192+
for group in user['GroupList']:
193+
if group in admin_groups:
194+
reasons.append('In admin group: {}'.format(group))
195+
196+
# Log them if they are an admin
197+
if len(reasons) != 0:
198+
log_info('User is admin', location, reasons)
199+
record_admin(admins, account['name'], 'user', user['UserName'])
200+
201+
location.pop('user', None)
202+
return admins
203+
204+
205+
def get_account_name_from_id(accounts, account_id):
206+
for a in accounts:
207+
if account_id == a['id']:
208+
return a['name']
209+
return None
210+
211+
212+
def run(arguments):
213+
_, accounts, config = parse_arguments(arguments)
214+
admins = find_admins(accounts, config)
215+
216+
for admin in admins:
217+
print("{}\t{}\t{}".format(admin['account'], admin['type'], admin['name']))

0 commit comments

Comments
 (0)