Skip to content

Commit bac93ae

Browse files
committed
Updated add-applicationpermission
1 parent 5f6ce91 commit bac93ae

File tree

2 files changed

+175
-64
lines changed

2 files changed

+175
-64
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ Please refer to the [Wiki](https://github.com/mlcsec/Graphpython/wiki) for the f
194194
* **Update-UserProperties** - Update a specific user property of the target user
195195
* **Add-ApplicationPassword** - Add client secret to target application
196196
* **Add-ApplicationCertificate** - Add client certificate to target application
197-
* **Add-ApplicationPermission** - Add permission to target application (application/delegated)
197+
* **Add-ApplicationPermission** - Add permission to target application e.g. Mail.Send and attempt to grant admin consent
198198
* **Add-UserTAP** - Add new Temporary Access Password (TAP) to target user
199199
* **Add-GroupMember** - Add member to target group
200200
* **Create-Application** - Create new enterprise application with default settings
@@ -598,9 +598,9 @@ Graph permission IDs applied to objects can be easily located with detailed expl
598598
- [x] `Spoof-OWAEmailMessage` - add --email option containing formatted message as only accepts one line at the mo...
599599
- [x] `Deploy-MaliciousScript` - add input options to choose runAsAccount, enforceSignatureCheck, etc. and more assignment options
600600
- [x] `Get-DeviceConfigurationPolicies` - tidy up the templateReference and assignmentTarget output
601-
- [ ] `Add-ApplicationPermission` - check logic to ensure existing perms aren't overridden
601+
- [x] `Add-ApplicationPermission` - updated logic and added ability to grant admin consent for admin permissions assigned from the same command - update `Grant-AppAdminConsent` to handle any failures so users don't have to repeat this whole command again
602602
- New:
603-
- [ ] `Grant-AdminConsent` - grant admin consent for requested/applied admin app permissions
603+
- [ ] `Grant-AppAdminConsent` - grant admin consent for requested/applied admin app permissions (if `Add-ApplicationPermission` fails)
604604
- [x] `Backdoor-Script` - first user downloads target script content then adds their malicious code, supply updated script as args, encodes then [patch](https://learn.microsoft.com/en-us/graph/api/intune-shared-devicemanagementscript-update?view=graph-rest-beta)
605605
- [ ] `Deploy-MaliciousWin32App` - use IntuneWinAppUtil.exe to package the EXE/MSI and deploy to devices
606606
- check also [here](https://learn.microsoft.com/en-us/graph/api/resources/intune-app-conceptual?view=graph-rest-1.0) for managing iOS, Android, LOB apps etc. via graph

graphpython.py

Lines changed: 172 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def list_commands():
125125
["Update-UserProperties", "Update the user properties of the target user"],
126126
["Add-ApplicationPassword", "Add client secret to target application"],
127127
["Add-ApplicationCertificate", "Add client certificate to target application"],
128-
["Add-ApplicationPermission", "Add permission to target application (application/delegated)"],
128+
["Add-ApplicationPermission", "Add permission to target application e.g. Mail.Send and attempt to grant admin consent"],
129129
["Add-UserTAP", "Add new Temporary Access Password (TAP) to target user"],
130130
["Add-GroupMember", "Add member to target group"],
131131
["Create-Application", "Create new enterprise application with default settings"],
@@ -2512,7 +2512,8 @@ def base64url_encode(data):
25122512

25132513
print_yellow("\n[*] Get-Application")
25142514
print("=" * 80)
2515-
api_url = f"https://graph.microsoft.com/beta/myorganization/applications(appId='{args.id}')"
2515+
api_url = f"https://graph.microsoft.com/beta/myorganization/applications(appId='{args.id}')" # app id
2516+
#api_url = f"https://graph.microsoft.com/v1.0/applications/{args.id}" # object id
25162517
if args.select:
25172518
api_url += "?$select=" + args.select
25182519

@@ -3689,85 +3690,193 @@ def read_and_encode_cert(cert_path):
36893690
print_red(response.text)
36903691
print("=" * 80)
36913692

3692-
# dump-applicationpermissions
3693-
# - dump permissions for --id app
3694-
# - https://learn.microsoft.com/en-us/graph/permissions-reference
3695-
# - list-applications 'requiredResourceAccess' contains all the permission ids
3696-
# can also check: https://graph.microsoft.com/v1.0/servicePrincipals/9ee251b0-b25e-4562-b62e-611c75387f2b/appRoleAssignments
3697-
# - for the configured permission
3698-
# CHECK
3699-
# - one of above functions^^ kinda does this
3700-
# - need to get the present perms and add to add-applicationpermissions json body like with add-applicationcertificate which checks for current certs so it doesn't override them
3701-
3693+
37023694
# add-applicationpermission
3703-
# - create an new application (add-application) then assign this
3704-
# - trying to assing to existing one will remove all the other perms
3705-
# NEED todo dump-applicationpermissions
3706-
# - then make sure you reassign the existing ones found from that in the following req
37073695
elif args.command and args.command.lower() == "add-applicationpermission":
37083696
if not args.id:
37093697
print_red("[-] Error: --id required for Add-ApplicationPermission command")
37103698
return
3711-
37123699
print_yellow("\n[*] Add-ApplicationPermission")
37133700
print("=" * 80)
3714-
api_url = f"https://graph.microsoft.com/v1.0/myorganization/applications/{args.id}"
3715-
3701+
3702+
# 1. CHECK existing permissions
3703+
api_url = f"https://graph.microsoft.com/beta/myorganization/applications(appId='{args.id}')" # app id
3704+
#api_url = f"https://graph.microsoft.com/v1.0/applications/{args.id}" # object id
3705+
37163706
user_agent = get_user_agent(args)
37173707
headers = {
37183708
'Authorization': f'Bearer {access_token}',
3719-
'Content-Type': 'application/json',
37203709
'User-Agent': user_agent
37213710
}
3711+
response = requests.get(api_url, headers=headers)
3712+
existingperms = []
3713+
if response.status_code == 200:
3714+
response_json = response.json()
3715+
existingperms = response_json.get('requiredResourceAccess', [])
3716+
3717+
# 2. patch
3718+
api_url = f"https://graph.microsoft.com/beta/myorganization/applications(appId='{args.id}')" # app id
3719+
#api_url = f"https://graph.microsoft.com/v1.0/myorganization/applications/{args.id}" # object id
3720+
3721+
print("\033[34m~> API Permissions: https://learn.microsoft.com/en-us/graph/permissions-reference\033[0m")
3722+
3723+
# permission id validation
3724+
def parse_permissionid(content):
3725+
soup = BeautifulSoup(content, 'html.parser')
3726+
permissions = {}
3727+
for h3 in soup.find_all('h3'):
3728+
permission_name = h3.get_text()
3729+
table = h3.find_next('table')
3730+
rows = table.find_all('tr')
3731+
application_id = rows[1].find_all('td')[1].get_text()
3732+
delegated_id = rows[1].find_all('td')[2].get_text()
3733+
application_consent = rows[4].find_all('td')[1].get_text() if len(rows) > 4 else "Unknown"
3734+
delegated_consent = rows[4].find_all('td')[2].get_text() if len(rows) > 4 else "Unknown"
3735+
permissions[application_id] = ('Application', permission_name, application_consent)
3736+
permissions[delegated_id] = ('Delegated', permission_name, delegated_consent)
3737+
return permissions
3738+
3739+
script_dir = os.path.dirname(os.path.abspath(__file__))
3740+
file_path = os.path.join(script_dir, '.github', 'graphpermissions.txt')
3741+
3742+
try:
3743+
with open(file_path, 'r') as file:
3744+
content = file.read()
3745+
except FileNotFoundError:
3746+
print_red(f"\n[-] The file {file_path} does not exist.")
3747+
sys.exit(1)
3748+
except Exception as e:
3749+
print_red(f"\n[-] An error occurred: {e}")
3750+
sys.exit(1)
37223751

3723-
# ADD
3724-
# - table of common permissions we can abuse
3752+
permissions = parse_permissionid(content)
37253753

37263754
try:
37273755
permissionid = input("\nEnter API Permission ID: ").strip()
3728-
permissiontype = input("Enter Permission Type (application/delegated): ").strip().lower()
3729-
except KeyboardInterrupt:
3730-
sys.exit()
3756+
if permissionid not in permissions:
3757+
print_red("\n[-] Invalid permission ID. Not in graphpermissions.txt")
3758+
sys.exit(1)
3759+
3760+
permission_info = permissions[permissionid]
3761+
if len(permission_info) == 3:
3762+
permission_type, permission_name, admin_consent_required = permission_info
3763+
else:
3764+
permission_type, permission_name = permission_info
3765+
admin_consent_required = "Unknown"
3766+
3767+
print(f"\nPermission ID: {permissionid} corresponds to '{permission_name}' with type '{permission_type}'")
3768+
3769+
# grant admin consent option
3770+
print(f"Admin Consent Required: {admin_consent_required}")
3771+
if admin_consent_required.lower() == 'yes':
3772+
grantadminconsent = input(f"\nGrant Admin Consent For: {permission_name}? (yes/no): ").strip().lower()
3773+
else:
3774+
pass
3775+
grantadminconsent = 'no'
37313776

3732-
if permissiontype == "application":
3777+
except KeyboardInterrupt:
3778+
sys.exit(1)
3779+
3780+
if permission_type.lower() == "application":
37333781
typevalue = "Role"
3734-
else:
3782+
elif permission_type.lower() == "delegated":
37353783
typevalue = "Scope"
3784+
else:
3785+
print_red("\n[-] Unexpected error")
3786+
print("=" * 80)
3787+
sys.exit()
37363788

3789+
graphresource = next((resource for resource in existingperms if resource['resourceAppId'] == '00000003-0000-0000-c000-000000000000'), None) # does Microsoft Graph resource already exist
3790+
3791+
if graphresource:
3792+
graphresource['resourceAccess'].append({
3793+
"id": permissionid,
3794+
"type": typevalue
3795+
})
3796+
else:
3797+
existingperms.append({
3798+
"resourceAppId": "00000003-0000-0000-c000-000000000000",
3799+
"resourceAccess": [
3800+
{
3801+
"id": permissionid, # b633e1c5-b582-4048-a93e-9f11b44c7e96 -> Mail.Send (Application perm - admin consent required)
3802+
"type": typevalue
3803+
}
3804+
]
3805+
})
3806+
3807+
# assign perm json
37373808
data = {
3738-
"requiredResourceAccess": [
3809+
"requiredResourceAccess": existingperms
3810+
}
3811+
3812+
clientAppId = args.id
3813+
3814+
# admin consent json
3815+
admin_data = {
3816+
"clientAppId": clientAppId,
3817+
"onBehalfOfAll": True,
3818+
"checkOnly": False,
3819+
"tags": [],
3820+
"constrainToRra": True,
3821+
"dynamicPermissions": [
37393822
{
3740-
"resourceAppId": "00000003-0000-0000-c000-000000000000",
3741-
"resourceAccess": [
3742-
{
3743-
"id": permissionid, #"e383f46e-2787-4529-855e-0e479a3ffac0", # Mail.Send
3744-
"type": typevalue
3745-
}
3746-
]
3823+
"appIdentifier": "00000003-0000-0000-c000-000000000000",
3824+
"appRoles": [permission_name],
3825+
"scopes": []
37473826
}
37483827
]
37493828
}
37503829

37513830
response = requests.patch(api_url, headers=headers, json=data)
3752-
print(response.text)
3831+
if grantadminconsent == "no":
3832+
if response.ok:
3833+
print_green("\n[+] Application permissions updated successfully")
3834+
print("=" * 80)
3835+
sys.exit()
3836+
else:
3837+
print_red(f"\n[-] Failed to update application permissions: {response.status_code}")
3838+
print_red(response.text)
3839+
print("=" * 80)
3840+
sys.exit()
37533841

3754-
if response.ok:
3755-
print_green("\n[+] Application permission updated successfully")
3756-
# graphpython.py --command invoke-customquery --query https://graph.microsoft.com/v1.0/myorganization/applications/<id> --token .\token
3757-
print(response.text)
3758-
else:
3759-
print_red(f"\n[-] Failed to update application permission: {response.status_code}")
3760-
print_red(response.text)
3842+
headers = {
3843+
'Authorization': f'Bearer {access_token}',
3844+
'User-Agent': user_agent,
3845+
'Content-Type': 'application/json',
3846+
}
3847+
3848+
# any failures granting admin consent likely due to token scope/perms
3849+
if grantadminconsent == "yes":
3850+
if response.ok:
3851+
print_green("\n[+] Application permissions updated successfully")
3852+
3853+
print()
3854+
custom_bar = '╢{bar:50}╟'
3855+
for _ in tqdm(range(5), bar_format='{l_bar}'+custom_bar+'{r_bar}', leave=False, colour='yellow'):
3856+
time.sleep(1)
3857+
3858+
granturl = "https://graph.microsoft.com/beta/directory/consentToApp"
3859+
grantreq = requests.post(granturl, headers=headers, json=admin_data)
3860+
3861+
if grantreq.ok:
3862+
print_green(f"[+] Admin consent granted for: '{permission_name}'")
3863+
else:
3864+
print_red(f"\n[-] Failed to grant admin consent: {grantreq.status_code}")
3865+
print_red(grantreq.text)
37613866
print("=" * 80)
37623867

3763-
# grant-adminconsent
3764-
# - grant admin consent to adding api app permission
3765-
# POST https://graph.microsoft.com/beta/directory/consentToApp
3766-
# {"clientAppId":"3d84ebcc-0eef-4f59-ae2a-3fe0e1eb7f51","onBehalfOfAll":true,"checkOnly":false,"tags":[],"constrainToRra":true,"dynamicPermissions":[{"appIdentifier":"00000003-0000-0000-c000-000000000000","appRoles":["Directory.Read.All"],"scopes":[]}]}
3767-
3868+
# grant-appadminconsent
3869+
# - cover for if the grant fails in above likely due to token privs
3870+
# - instead of repeating whole request this can be used if permissions are updated successfully but the admin consent grant fails
3871+
# need:
3872+
# - admin_data payload and granturl/req
3873+
# - client app id
3874+
# - permission name to be granted e.g. 'Mail.Send' - prompt for input?
3875+
37683876
# add-userTAP
37693877
elif args.command and args.command.lower() == "add-usertap":
37703878
if not args.id:
3879+
37713880
print_red("[-] Error: --id required for Add-UserTAP command")
37723881
return
37733882

@@ -5941,19 +6050,18 @@ def read_and_encode_cert(cert_path):
59416050
if not args.id:
59426051
print_red("[-] Error: --id argument is required for Locate-PermissionID command")
59436052
return
5944-
59456053
print_yellow("\n[*] Locate-PermissionID")
59466054
print("=" * 80)
59476055
def parse_html(content):
59486056
soup = BeautifulSoup(content, 'html.parser')
59496057
permissions = {}
5950-
6058+
59516059
for h3 in soup.find_all('h3'):
59526060
title = h3.text
59536061
table = h3.find_next('table')
59546062
headers = [th.text for th in table.find('thead').find_all('th')]
59556063
rows = table.find('tbody').find_all('tr')
5956-
6064+
59576065
permission_data = {}
59586066
for row in rows:
59596067
cells = row.find_all('td')
@@ -5965,14 +6073,12 @@ def parse_html(content):
59656073
headers[2]: delegated
59666074
}
59676075
permissions[title] = permission_data
5968-
6076+
59696077
return permissions
5970-
59716078
def highlight(text, should_highlight):
59726079
if should_highlight:
5973-
return f"\033[92m{text}\033[0m"
6080+
return f"\033[92m{text}\033[0m"
59746081
return text
5975-
59766082
def print_permission(permission, data, app_ids, delegated_ids):
59776083
print_green(f"{permission}")
59786084
for category, values in data.items():
@@ -5982,19 +6088,18 @@ def print_permission(permission, data, app_ids, delegated_ids):
59826088
print(f" Application: {highlight(values['Application'], app_highlight)}")
59836089
print(f" Delegated: {highlight(values['Delegated'], delegated_highlight)}")
59846090
print()
5985-
59866091
identifiers = args.id.split(',')
59876092
script_dir = os.path.dirname(os.path.abspath(__file__))
59886093
file_path = os.path.join(script_dir, '.github', 'graphpermissions.txt')
5989-
59906094
try:
59916095
with open(file_path, 'r') as file:
59926096
content = file.read()
59936097
except FileNotFoundError:
5994-
print(f"The file {file_path} does not exist.")
6098+
print_red(f"[-] The file {file_path} does not exist.")
6099+
return
59956100
except Exception as e:
5996-
print(f"An error occurred: {e}")
5997-
6101+
print_red(f"[-] An error occurred: {e}")
6102+
return
59986103
permissions = parse_html(content)
59996104
app_ids = []
60006105
delegated_ids = []
@@ -6003,10 +6108,16 @@ def print_permission(permission, data, app_ids, delegated_ids):
60036108
app_ids.append(data['Identifier']['Application'])
60046109
if data['Identifier']['Delegated'] in identifiers:
60056110
delegated_ids.append(data['Identifier']['Delegated'])
6006-
6111+
6112+
found_permissions = False
60076113
for permission, data in permissions.items():
60086114
if data['Identifier']['Application'] in app_ids or data['Identifier']['Delegated'] in delegated_ids:
60096115
print_permission(permission, data, app_ids, delegated_ids)
6116+
found_permissions = True
6117+
6118+
if not found_permissions:
6119+
print_red("[-] Permission ID not found")
6120+
60106121
print("=" * 80)
60116122

60126123
if __name__ == "__main__":

0 commit comments

Comments
 (0)