├── .gitignore ├── BadZure.py ├── Invoke-BadZure.ps1 ├── LICENSE ├── README.md ├── badzure.yml ├── entity_data ├── administrative-units.txt ├── app-core-names.txt ├── app-prefixes.txt ├── app-sufixes.txt ├── first-names.txt ├── group-names.txt └── last-names.txt ├── img ├── BadZure.png └── attack_paths.png ├── requirements.txt ├── src ├── __init__.py └── constants.py └── terraform ├── main.tf ├── outputs.tf └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .ionide/ 4 | project.lock.json 5 | *-tests.xml 6 | /debug/ 7 | /staging/ 8 | /Packages/ 9 | *.nuget.props 10 | 11 | # dotnet cli install/uninstall scripts 12 | dotnet-install.ps1 13 | dotnet-install.sh 14 | dotnet-uninstall-pkgs.sh 15 | dotnet-uninstall-debian-packages.sh 16 | 17 | # VS auto-generated solution files for project.json solutions 18 | *.xproj 19 | *.xproj.user 20 | *.suo 21 | 22 | # VS auto-generated files for csproj files 23 | *.csproj.user 24 | 25 | # Visual Studio IDE directory 26 | .vs/ 27 | 28 | # VSCode directories that are not at the repository root 29 | /**/.vscode/ 30 | 31 | # Project Rider IDE files 32 | .idea.powershell/ 33 | 34 | # Ignore executables 35 | *.exe 36 | *.msi 37 | *.appx 38 | *.msix 39 | 40 | # Ignore binaries and symbols 41 | *.pdb 42 | *.dll 43 | *.wixpdb 44 | 45 | #ignore text files 46 | #*.txt 47 | 48 | # Ignore packages 49 | *.deb 50 | *.tar.gz 51 | *.zip 52 | *.rpm 53 | *.pkg 54 | *.nupkg 55 | *.AppImage 56 | 57 | # default location for produced nuget packages 58 | /nuget-artifacts 59 | 60 | # resgen output 61 | gen 62 | 63 | # Per repo profile 64 | .profile.ps1 65 | 66 | # macOS 67 | .DS_Store 68 | .DocumentRevisions-V100 69 | .fseventsd 70 | .Spotlight-V100 71 | .TemporaryItems 72 | .Trashes 73 | .VolumeIcon.icns 74 | .com.apple.timemachine.donotpresent 75 | .AppleDB 76 | .AppleDesktop 77 | Network Trash Folder 78 | Temporary Items 79 | .apdisk 80 | .AppleDouble 81 | .LSOverride 82 | 83 | # TestsResults 84 | TestsResults*.xml 85 | ParallelXUnitResults.xml 86 | xUnitResults.xml 87 | 88 | # Resharper settings 89 | PowerShell.sln.DotSettings.user 90 | *.msp 91 | StyleCop.Cache 92 | 93 | # Ignore SelfSignedCertificate autogenerated files 94 | test/tools/Modules/SelfSignedCertificate/ 95 | 96 | # BenchmarkDotNet artifacts 97 | test/perf/BenchmarkDotNet.Artifacts/ 98 | 99 | # Test generated module 100 | test/tools/Modules/Microsoft.PowerShell.NamedPipeConnection/ 101 | 102 | # Test generated startup profile 103 | StartupProfileData-NonInteractive 104 | 105 | # Ignore logfiles 106 | logfile/* 107 | 108 | # Ignore Python virtual environment 109 | venv/ 110 | .venv/ 111 | 112 | # Ignore Python cache files 113 | __pycache__/ 114 | *.pyc 115 | *.pyo 116 | *.pyd 117 | 118 | # Ignore environment configuration files 119 | .env 120 | *.env 121 | 122 | # Ignore Terraform state files and backups 123 | *.tfstate 124 | *.tfstate.* 125 | *.backup 126 | 127 | # Ignore Terraform configuration files 128 | .terraform/ 129 | .terraform.lock.hcl 130 | 131 | # Ignore Terraform variable files 132 | *.tfvars 133 | *.tfvars.json 134 | 135 | # Ignore config file 136 | local.yml 137 | 138 | # Ignore users file 139 | users.txt 140 | 141 | # Ignore tokens file 142 | tokens.txt -------------------------------------------------------------------------------- /BadZure.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import yaml 4 | import click 5 | from python_terraform import Terraform 6 | from src.constants import ENTRA_ROLES, GRAPH_API_PERMISSIONS, HIGH_PRIVILEGED_ENTRA_ROLES, HIGH_PRIVILEGED_GRAPH_API_PERMISSIONS 7 | import random 8 | import string 9 | import requests 10 | import time 11 | import logging 12 | 13 | # Ensure AZURE_CONFIG_DIR is set the Azure CLI config directory 14 | os.environ['AZURE_CONFIG_DIR'] = os.path.expanduser('~/.azure') 15 | 16 | 17 | 18 | TERRAFORM_DIR = os.path.join(os.path.dirname(__file__), 'terraform') 19 | tf = Terraform(working_dir=TERRAFORM_DIR) 20 | 21 | banner = """ 22 | 23 | ____ _ ______ 24 | | _ \ | |___ / 25 | | |_) | __ _ __| | / /_ _ _ __ ___ 26 | | _ < / _` |/ _` | / /| | | | '__/ _ \\ 27 | | |_) | (_| | (_| |/ /_| |_| | | | __/ 28 | |____/ \__,_|\__,_/_____\__,_|_| \___| 29 | 30 | 31 | by Mauricio Velazco 32 | @mvelazco 33 | 34 | """ 35 | 36 | extra = {'include_timestamp': False} 37 | 38 | def setup_logging(level, include_timestamp=True): 39 | 40 | custom_formats = { 41 | logging.INFO: "{timestamp}[+] %(message)s", 42 | logging.ERROR: "{timestamp}[!] %(message)s", 43 | "DEFAULT": "{timestamp}[%(levelname)s] - %(message)s", 44 | } 45 | custom_time_format = "%Y-%m-%d %H:%M:%S" 46 | 47 | class CustomFormatter(logging.Formatter): 48 | def __init__(self, fmt=None, datefmt=None, style='%'): 49 | super().__init__(fmt, datefmt=custom_time_format, style=style) 50 | 51 | def format(self, record): 52 | if hasattr(record, 'include_timestamp') and not record.include_timestamp: 53 | timestamp = "" 54 | else: 55 | timestamp = f"{self.formatTime(record, self.datefmt)} " 56 | 57 | # Replace the {timestamp} placeholder in the format with the actual timestamp or empty string 58 | self._style._fmt = custom_formats.get(record.levelno, custom_formats["DEFAULT"]).format(timestamp=timestamp) 59 | return super().format(record) 60 | 61 | root_logger = logging.getLogger() 62 | root_logger.handlers.clear() 63 | console_handler = logging.StreamHandler() 64 | console_handler.setFormatter(CustomFormatter()) 65 | root_logger.addHandler(console_handler) 66 | root_logger.setLevel(level) 67 | 68 | 69 | def parse_terraform_output(output): 70 | # Parse the Terraform state output and extract the essential information 71 | resources = [] 72 | 73 | try: 74 | state = json.loads(output) 75 | for module in state.get('values', {}).get('root_module', {}).get('resources', []): 76 | resource_type = module.get('type') 77 | resource_name = module.get('name') 78 | if resource_type == 'azuread_domains' or resource_type == 'azuread_administrative_unit_member' or resource_type == 'azuread_group_member' or resource_type == 'azuread_directory_role_assignment': 79 | #print(module.get('values', {})) 80 | continue 81 | if resource_type == 'azuread_user': 82 | key_attr = module.get('values', {}).get('user_principal_name') 83 | elif resource_type == 'azuread_group': 84 | key_attr = module.get('values', {}).get('display_name') 85 | elif resource_type == 'azuread_application_registration': 86 | key_attr = module.get('values', {}).get('display_name') 87 | elif resource_type == 'azuread_administrative_unit': 88 | key_attr = module.get('values', {}).get('display_name') 89 | elif resource_type == 'azuread_service_principal': 90 | key_attr = module.get('values', {}).get('id') 91 | #print(module.get('values', {})) 92 | else: 93 | key_attr = "N/A" 94 | 95 | #resources.append(f"Resource Type: {resource_type}, Name: {resource_name}, Key Attribute: {key_attr}") 96 | resources.append(f"Resource Type: {resource_type}, Identifier: {key_attr}") 97 | 98 | except json.JSONDecodeError: 99 | logging.error("Failed to parse Terraform state output") 100 | 101 | return resources 102 | 103 | def write_users_to_file(users, domain, file_path): 104 | with open(file_path, 'w') as file: 105 | for user in users.values(): 106 | file.write(f"{user['user_principal_name']}@{domain}\n") 107 | 108 | def get_ms_token_username_pass(tenant_id, username, password, scope): 109 | 110 | # https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth-ropc 111 | 112 | #logging.info("Using resource owner password OAuth flow to obtain a token") 113 | 114 | token_url = f'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token' 115 | 116 | full_scope = f'{scope} offline_access' 117 | 118 | token_data = { 119 | 120 | 'client_id': '1950a258-227b-4e31-a9cf-717495945fc2', # Microsoft Azure PowerShell 121 | #'client_id': '00b41c95-dab0-4487-9791-b9d2c32c80f2', # Office 365 Management. Works to read emails Graph and EWS. 122 | #'client_id': 'd3590ed6-52b3-4102-aeff-aad2292ab01c', # Microsoft Office. Also works to read emails Graph and EWS. 123 | #'client_id': '00000002-0000-0ff1-ce00-000000000000', # Office 365 Exchange Online 124 | #'client_id': '00000006-0000-0ff1-ce00-000000000000', # Microsoft Office 365 Portal 125 | #'client_id': 'fb78d390-0c51-40cd-8e17-fdbfab77341b', # Microsoft Exchange REST API Based Powershell 126 | # 'client_id': '00000003-0000-0000-c000-000000000000', # Microsoft Graph 127 | #'client_id': 'de8bc8b5-d9f9-48b1-a8ad-b748da725064', # Graph Explorer 128 | #'client_id': '14d82eec-204b-4c2f-b7e8-296a70dab67e', # Microsoft Graph Command Line Tools 129 | 130 | 'grant_type': 'password', 131 | 'username': username, 132 | 'password': password, 133 | 'scope': full_scope 134 | } 135 | 136 | response = requests.post(token_url, data=token_data) 137 | refresh_token = response.json().get('refresh_token') 138 | access_token = response.json().get('access_token') 139 | 140 | if refresh_token and access_token: 141 | return {'access_token': access_token, 'refresh_token': refresh_token} 142 | else: 143 | logging.error (f'Error obtaining token. Http response: {response.status_code}') 144 | logging.error (response.text) 145 | 146 | def create_attack_path(attack_patch_config, users, applications, domain, password): 147 | 148 | app_owner_assignments = {} 149 | user_role_assignments = {} 150 | app_role_assignments = {} 151 | app_api_permission_assignments = {} 152 | 153 | # Pick a random application registration 154 | app_keys = list(applications.keys()) 155 | random_app = random.choice(app_keys) 156 | #app_id = applications[random_app]['display_name'] 157 | 158 | attack_path_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) 159 | key = f"attack-path-{attack_path_id}" 160 | 161 | # Pick a random user 162 | user_keys = list(users.keys()) 163 | random_user = random.choice(user_keys) 164 | user_principal_name = f"{users[random_user]['user_principal_name']}@{domain}" 165 | password = users[random_user]['password'] 166 | 167 | if attack_patch_config['scenario'] == "direct": 168 | 169 | initial_access_user = { 170 | "user_principal_name": user_principal_name, 171 | "password": password 172 | } 173 | 174 | elif attack_patch_config['scenario'] == "helpdesk": 175 | 176 | helpdesk_admin_role_id = "729827e3-9c14-49f7-bb1b-9608f156bbb8" # ID for "Helpdesk Administrator" 177 | second_random_user = random.choice(user_keys) 178 | second_user_principal_name = f"{users[second_random_user]['user_principal_name']}@{domain}" 179 | second_user_password = users[second_random_user]['password'] 180 | 181 | initial_access_user = { 182 | "user_principal_name": second_user_principal_name, 183 | "password": second_user_password 184 | } 185 | 186 | user_role_assignments[key] = { 187 | 'user_name': second_random_user, 188 | 'role_definition_id': helpdesk_admin_role_id 189 | } 190 | 191 | app_owner_assignments[key] = { 192 | 'app_name': random_app, 193 | 'user_principal_name': user_principal_name, 194 | } 195 | 196 | 197 | if attack_patch_config['method'] == "AzureADRole": 198 | 199 | 200 | if attack_patch_config['entra_role'] != 'random': 201 | 202 | privileged_role_id = attack_patch_config['entra_role'] 203 | 204 | else: 205 | privileged_role_id = random.choice(list(HIGH_PRIVILEGED_ENTRA_ROLES.values())) 206 | 207 | # Assign "Privileged Role Administrator" role to the application 208 | #privileged_role_id = "e8611ab8-c189-46e8-94e1-60213ab1f814" # ID for "Privileged Role Administrator" 209 | 210 | app_role_assignments[key] = { 211 | 'app_name': random_app, 212 | 'role_id': privileged_role_id, 213 | } 214 | 215 | elif attack_patch_config['method'] == "GraphAPIPermission": 216 | 217 | if attack_patch_config['app_role'] != 'random': 218 | 219 | api_permission_id = attack_patch_config['app_role'] 220 | 221 | else: 222 | api_permission_id = random.choice([permission["id"] for permission in HIGH_PRIVILEGED_GRAPH_API_PERMISSIONS.values()]) 223 | 224 | # Assign API permission to the application 225 | #api_permission_id = "9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8" # ID for "RoleManagement.ReadWrite.Directory" 226 | 227 | app_api_permission_assignments[key] = { 228 | 'app_name': random_app, 229 | 'api_permission_id': api_permission_id, 230 | } 231 | 232 | return initial_access_user, app_owner_assignments, user_role_assignments, app_role_assignments, app_api_permission_assignments 233 | 234 | def load_config(file_path): 235 | """Load and return the configuration from a YAML file.""" 236 | try: 237 | with open(file_path, 'r') as file: 238 | config = yaml.safe_load(file) 239 | return config 240 | except FileNotFoundError: 241 | #logging.error(f"Configuration file not found at: {file_path}") 242 | exit(1) 243 | except yaml.YAMLError as e: 244 | #logging.error(f"Error parsing the YAML file: {e}") 245 | exit(1) 246 | 247 | def create_random_assignments(users, groups, administrative_units, applications): 248 | 249 | user_group_assignments = {} 250 | user_au_assignments = {} 251 | user_role_assignments = {} 252 | app_role_assignments = {} 253 | app_api_permission_assignments={} 254 | 255 | user_keys = list(users.keys()) 256 | group_keys = list(groups.keys()) 257 | au_keys = list(administrative_units.keys()) 258 | app_keys = list(applications.keys()) 259 | 260 | # Calculate subset size as one-third of the total users 261 | user_subset_size = max(1, len(user_keys) // 3) 262 | 263 | # Randomly select a subset of users for group assignments 264 | logging.info("Creating random user to group assignments") 265 | group_assigned_users = random.sample(user_keys, user_subset_size) 266 | for user in group_assigned_users: 267 | if groups: 268 | group = random.choice(group_keys) 269 | assignment_key = f"{user}-{group}" 270 | user_group_assignments[assignment_key] = { 271 | 'user_name': user, 272 | 'group_name': group 273 | } 274 | 275 | # Randomly select a subset of users for administrative unit assignments 276 | logging.info("Creating random user to administrative unit assignments") 277 | au_assigned_users = random.sample(user_keys, user_subset_size) 278 | for user in au_assigned_users: 279 | if administrative_units: 280 | au = random.choice(au_keys) 281 | assignment_key = f"{user}-{au}" 282 | user_au_assignments[assignment_key] = { 283 | 'user_name': user, 284 | 'administrative_unit_name': au 285 | } 286 | 287 | # Randomly select a subset of users for role assignments 288 | logging.info("Creating random azure ad role assignments to users") 289 | role_assigned_users = random.sample(user_keys, user_subset_size) 290 | for user in role_assigned_users: 291 | if ENTRA_ROLES: 292 | role_name = random.choice(list(ENTRA_ROLES.keys())) 293 | role_id = ENTRA_ROLES[role_name] 294 | assignment_key = f"{user}-{role_name}" 295 | user_role_assignments[assignment_key] = { 296 | 'user_name': user, 297 | 'role_definition_id': role_id 298 | } 299 | 300 | app_subset_size = max(1, len(app_keys) // 2) 301 | role_assigned_apps = random.sample(app_keys, app_subset_size) 302 | 303 | logging.info("Creating random azure ad role assignments to applications") 304 | for app in role_assigned_apps: 305 | if ENTRA_ROLES: 306 | role_name = random.choice(list(ENTRA_ROLES.keys())) 307 | role_id = ENTRA_ROLES[role_name] 308 | assignment_key = f"{app}-{role_name}" 309 | app_role_assignments[assignment_key] = { 310 | 'app_name': app, 311 | 'role_id': role_id 312 | } 313 | 314 | 315 | logging.info("Creating random Graph api permission assignments to applications") 316 | api_assigned_apps = random.sample(app_keys, app_subset_size) 317 | for app in api_assigned_apps: 318 | if GRAPH_API_PERMISSIONS: 319 | api_name = random.choice(list(GRAPH_API_PERMISSIONS.keys())) 320 | api_permission_id = GRAPH_API_PERMISSIONS[api_name]['id'] 321 | assignment_key = f"{app}-{api_name}" 322 | app_api_permission_assignments[assignment_key] = { 323 | 'app_name': app, 324 | 'api_permission_id': api_permission_id, 325 | } 326 | 327 | return user_group_assignments, user_au_assignments, user_role_assignments, app_role_assignments, app_api_permission_assignments 328 | 329 | def generate_random_password(length=15): 330 | if length < 8: 331 | raise ValueError("Password length must be at least 8 characters") 332 | 333 | password = [ 334 | random.choice(string.ascii_uppercase), 335 | random.choice(string.ascii_lowercase), 336 | random.choice(string.digits), 337 | random.choice(string.punctuation) 338 | ] 339 | 340 | password += random.choices(string.ascii_letters + string.digits + string.punctuation, k=length - 4) 341 | random.shuffle(password) 342 | return ''.join(password) 343 | 344 | def read_lines_from_file(file_path): 345 | with open(file_path, mode='r') as file: 346 | return [line.strip() for line in file.readlines()] 347 | 348 | def generate_user_details(first_names_file, last_names_file, number_of_users): 349 | 350 | first_names = read_lines_from_file(first_names_file) 351 | last_names = read_lines_from_file(last_names_file) 352 | users = {} 353 | 354 | for _ in range(number_of_users): 355 | first_name = random.choice(first_names) 356 | last_name = random.choice(last_names) 357 | key = f"{first_name}.{last_name}".lower() 358 | users[key] = { 359 | 'user_principal_name': key, 360 | 'display_name': f"{first_name.capitalize()} {last_name.capitalize()}", 361 | 'mail_nickname': key, 362 | 'password': generate_random_password() 363 | } 364 | 365 | return users 366 | 367 | def generate_group_details(file_path, number_of_groups): 368 | 369 | groups = {} 370 | 371 | group_names = read_lines_from_file(file_path) 372 | 373 | selected_groups = random.sample(group_names, number_of_groups) 374 | 375 | for group_name in selected_groups: 376 | groups[group_name] = { 377 | 'display_name': group_name 378 | } 379 | 380 | return groups 381 | 382 | def generate_app_details(prefix_file, core_file, suffix_file, number_of_names): 383 | 384 | prefixes = read_lines_from_file(prefix_file) 385 | core_names = read_lines_from_file(core_file) 386 | suffixes = read_lines_from_file(suffix_file) 387 | 388 | app_names = set() 389 | 390 | while len(app_names) < number_of_names: 391 | prefix = random.choice(prefixes) 392 | core_name = random.choice(core_names) 393 | suffix = random.choice(suffixes) if random.random() > 0.5 else "" 394 | app_name = f"{prefix}{core_name}{suffix}" 395 | app_names.add(app_name) 396 | 397 | applications = {} 398 | for name in app_names: 399 | applications[name] = { 400 | 'display_name': name 401 | } 402 | 403 | return applications 404 | 405 | def generate_administrative_units_details(file_path, number_of_aunits): 406 | 407 | aunits = {} 408 | 409 | aunit_names = read_lines_from_file(file_path) 410 | 411 | selected_groups = random.sample(aunit_names, number_of_aunits) 412 | 413 | for group_name in selected_groups: 414 | aunits[group_name] = { 415 | 'display_name': group_name 416 | } 417 | 418 | return aunits 419 | 420 | @click.group() 421 | def cli(): 422 | pass 423 | 424 | @cli.command() 425 | @click.option('--config', type=click.Path(exists=True), default='badzure.yml', help="Path to the configuration YAML file") 426 | @click.option('--verbose', is_flag=True, help="Enable verbose output") 427 | def build(config, verbose): 428 | """Create resources and attack paths""" 429 | azure_config_dir = os.path.expanduser('~/.azure') 430 | os.environ['AZURE_CONFIG_DIR'] = azure_config_dir 431 | 432 | # Load configuration 433 | logging.info(f"Loading BadZure configuration from {config}") 434 | config = load_config(config) 435 | tenant_id = config['tenant']['tenant_id'] 436 | domain = config['tenant']['domain'] 437 | 438 | max_users = config['tenant']['users'] 439 | max_groups = config['tenant']['groups'] 440 | max_apps = config['tenant']['applications'] 441 | max_aunits = config['tenant']['administrative_units'] 442 | 443 | 444 | # Generate random users 445 | logging.info(f"Generating {max_users} random users") 446 | users = generate_user_details('entity_data/first-names.txt', 'entity_data/last-names.txt', max_users) 447 | 448 | # Generate random groups 449 | logging.info(f"Generating {max_groups} random groups") 450 | groups = generate_group_details('entity_data/group-names.txt', max_groups) 451 | 452 | # Generate random application registrations 453 | logging.info(f"Generating {max_apps} random application registrations/service principals") 454 | applications = generate_app_details('entity_data/app-prefixes.txt', 'entity_data/app-core-names.txt','entity_data/app-sufixes.txt', max_apps) 455 | 456 | # Generate random administratuve units 457 | logging.info(f"Generating {max_aunits} random administrative units") 458 | administrative_units = generate_administrative_units_details('entity_data/administrative-units.txt', max_aunits) 459 | 460 | # Create random assignments 461 | #logging.info("Creating random assignments for groups, administrative units, azure ad roles and graph api permissions") 462 | 463 | user_group_assignments, user_au_assignments, user_role_assignments, app_role_assignments, app_api_permission_assignments = create_random_assignments(users, groups, administrative_units, applications) 464 | 465 | attack_path_application_owner_assignments, attack_path_user_role_assignments, attack_path_app_role_assignments, attack_path_app_api_permission_assignments = {}, {}, {}, {} 466 | 467 | user_creds = {} 468 | 469 | for attack_path in config['attack_paths']: 470 | 471 | if config['attack_paths'][attack_path]['enabled']: 472 | 473 | #password = config['attack_paths'][attack_path]['password'] 474 | logging.info(f"Creating assignments for attack path '{attack_path}'") 475 | initial_access, ap_app_owner_assignments, ap_user_role_assignments, ap_app_role_assignments, ap_app_api_permission_assignments = create_attack_path(config['attack_paths'][attack_path], users, applications, domain, "test") 476 | attack_path_application_owner_assignments = {**attack_path_application_owner_assignments, **ap_app_owner_assignments} 477 | attack_path_user_role_assignments = {**attack_path_user_role_assignments, **ap_user_role_assignments} 478 | attack_path_app_role_assignments = {**attack_path_app_role_assignments, **ap_app_role_assignments} 479 | attack_path_app_api_permission_assignments = {**attack_path_app_api_permission_assignments, **ap_app_api_permission_assignments} 480 | user_creds[attack_path] = initial_access 481 | #update_password(users, initial_access['user_principal_name'].split('@')[0], password) 482 | 483 | 484 | # Prepare Terraform variables 485 | user_vars = {user['user_principal_name']: user for user in users.values()} 486 | group_vars = {group['display_name']: group for group in groups.values()} 487 | application_vars = {app['display_name']: app for app in applications.values()} 488 | administrative_unit_vars = {au['display_name']: au for au in administrative_units.values()} 489 | 490 | tf_vars = { 491 | 'tenant_id': tenant_id, 492 | 'domain': domain, 493 | 'users': user_vars, 494 | 'azure_config_dir': azure_config_dir, 495 | 'groups': group_vars, 496 | 'applications': application_vars, 497 | 'administrative_units': administrative_unit_vars, 498 | 'user_group_assignments': user_group_assignments, 499 | 'user_au_assignments': user_au_assignments, 500 | 'user_role_assignments': user_role_assignments, 501 | 'app_role_assignments': app_role_assignments, 502 | 'app_api_permission_assignments' : app_api_permission_assignments, 503 | 'attack_path_application_owner_assignments' : attack_path_application_owner_assignments, 504 | 'attack_path_user_role_assignments' : attack_path_user_role_assignments, 505 | 'attack_path_application_role_assignments' : attack_path_app_role_assignments, 506 | 'attack_path_application_api_permission_assignments' : attack_path_app_api_permission_assignments 507 | } 508 | 509 | # Write the Terraform variables to a file 510 | logging.info(f"Creating terraform.tfvars.json") 511 | with open(os.path.join(TERRAFORM_DIR, 'terraform.tfvars.json'), 'w') as f: 512 | json.dump(tf_vars, f, indent=4) 513 | 514 | 515 | 516 | # Initialize and apply the Terraform configuration 517 | logging.info(f"Calling terraform init.") 518 | return_code, stdout, stderr = tf.init() 519 | if return_code != 0: 520 | logging.error(f"Terraform init failed: {stderr}") 521 | if verbose: 522 | logging.error(stdout) 523 | logging.error(stderr) 524 | return 525 | 526 | logging.info(f"Calling terraform apply to create resources, this may take several minutes ...") 527 | return_code, stdout, stderr = tf.apply(skip_plan=True, capture_output=not verbose) 528 | if return_code != 0: 529 | logging.error(f"Terraform apply failed: {stderr}") 530 | if verbose: 531 | logging.error(stdout) 532 | logging.error(stderr) 533 | return 534 | 535 | logging.info("Azure AD tenant setup completed with assigned permissions and configurations!") 536 | write_users_to_file(users, domain, 'users.txt') 537 | logging.info("Created users.txt file.") 538 | logging.info("Attack Path Details") 539 | 540 | for attack_path in config['attack_paths']: 541 | 542 | if config['attack_paths'][attack_path]['enabled']: 543 | logging.info(f"*** {attack_path} ***") 544 | logging.info(f"Initial access user: {user_creds[attack_path]['user_principal_name']}") 545 | 546 | if config['attack_paths'][attack_path]['initial_access'] == "password": 547 | logging.info(f"Password: {user_creds[attack_path]['password']}") 548 | 549 | elif config['attack_paths'][attack_path]['initial_access'] == "token": 550 | logging.info(f"Obtaining tokens...") 551 | #logging.info(f"Will use {user_creds[attack_path]['user_principal_name']} and {user_creds[attack_path]['password']}") 552 | tokens = get_ms_token_username_pass(tenant_id, user_creds[attack_path]['user_principal_name'], user_creds[attack_path]['password'], "https://graph.microsoft.com/.default") 553 | with open("tokens.txt", "a") as file: 554 | file.write(f"Attack Path : {attack_path}\n") 555 | file.write(f"User : {user_creds[attack_path]['user_principal_name']}\n") 556 | file.write(f"Access Token: {tokens['access_token']}\n") 557 | file.write(f"Refresh Token: {tokens['refresh_token']}\n") 558 | logging.info(f"Tokens saved in tokens.txt!.") 559 | 560 | logging.info("Good bye.") 561 | 562 | @cli.command() 563 | @click.option('--verbose', is_flag=True, help="Enable verbose output") 564 | def show(verbose): 565 | """Show all the created resources in the tenant""" 566 | 567 | # Ensure terraform.tfvars.json exists 568 | tfvars_path = os.path.join(TERRAFORM_DIR, 'terraform.tfvars.json') 569 | if not os.path.exists(tfvars_path): 570 | logging.error("Error: terraform.tfvars.json file not found.") 571 | return 572 | 573 | # Initialize the Terraform configuration 574 | return_code, stdout, stderr = tf.init() 575 | if return_code != 0: 576 | logging.error(f"Terraform init failed: {stderr}") 577 | return 578 | 579 | logging.info(f"Calling terraform show to display the current state ...") 580 | 581 | # Execute the terraform show command 582 | return_code, stdout, stderr = tf.show(json=True, capture_output=not verbose) 583 | 584 | if return_code != 0: 585 | logging.error(f"Terraform show failed: {stderr}") 586 | logging.error(stdout) 587 | logging.error(stderr) 588 | return 589 | 590 | if verbose: 591 | print(stdout) 592 | else: 593 | resources = parse_terraform_output(stdout) 594 | for resource in resources: 595 | logging.info(resource) 596 | #logging.info(stdout) 597 | 598 | logging.info("Current state of Azure AD tenant resources displayed successfully.") 599 | 600 | @cli.command() 601 | @click.option('--verbose', is_flag=True, help="Enable verbose output") 602 | def destroy(verbose): 603 | """Destroy all created resources in the tenant""" 604 | 605 | # Ensure terraform.tfvars.json exists 606 | tfvars_path = os.path.join(TERRAFORM_DIR, 'terraform.tfvars.json') 607 | if not os.path.exists(tfvars_path): 608 | logging.error("Error: terraform.tfvars.json file not found.") 609 | return 610 | 611 | # Initialize and destroy the Terraform configuration 612 | return_code, stdout, stderr = tf.init() 613 | if return_code != 0: 614 | logging.error(f"Terraform init failed: {stderr}") 615 | return 616 | 617 | 618 | logging.info(f"Calling terraform destroy, this may take several minutes ...") 619 | #return_code, stdout, stderr = tf.destroy(force=True, input=False, auto_approve=True, capture_output=verbose) 620 | return_code, stdout, stderr = tf.apply(skip_plan=True, destroy=True, auto_approve=True, capture_output=not verbose) 621 | 622 | if return_code != 0: 623 | logging.error(f"Terraform apply failed: {stderr}") 624 | logging.error(stdout) 625 | logging.error(stderr) 626 | return 627 | 628 | logging.info("Azure AD tenant resources have been successfully destroyed!") 629 | 630 | # Remove the state files after destroying the resources 631 | logging.info(f"Deleting terraform state files ") 632 | for file in ["terraform.tfstate", "terraform.tfstate.backup", "terraform.tfvars.json"]: 633 | try: 634 | os.remove(os.path.join(TERRAFORM_DIR, file)) 635 | except FileNotFoundError: 636 | pass 637 | 638 | logging.info("Good bye.") 639 | 640 | if __name__ == '__main__': 641 | 642 | setup_logging(logging.INFO) 643 | print (banner) 644 | time.sleep(2) 645 | cli() 646 | -------------------------------------------------------------------------------- /Invoke-BadZure.ps1: -------------------------------------------------------------------------------- 1 | 2 | 3 | $banner = @" 4 | 5 | 6 | ____ _ _____ 7 | | __ ) __ _ __| ||__ /_ _ _ __ ___ 8 | | _ \ / _` | / _` | / /| | | || '__|/ _ \ 9 | | |_) || (_| || (_| | / /_| |_| || | | __/ 10 | |____/ \__,_| \__,_|/____|\__,_||_| \___| 11 | 12 | 13 | By Mauricio Velazco 14 | @mvelazco 15 | 16 | 17 | "@ 18 | 19 | 20 | Function Invoke-BadZure { 21 | 22 | 23 | <# 24 | 25 | .DESCRIPTION 26 | 27 | BadZure is a PowerShell script that leverages the Microsoft Graph SDK to orchestrate the setup of Azure Active Directory environments, populating them with diverse entities while also introducing common security misconfigurations to create vulnerable Azure AD tenants with multiple attack paths. 28 | 29 | .PARAMETER Build 30 | 31 | Used to populate and configure an Azure AD tenant 32 | 33 | .PARAMETER Destroy 34 | 35 | Used to delete entities created by BadZure on an Azure AD tenant. 36 | 37 | .PARAMETER TenantId 38 | 39 | Used to specify the Tenant ID for the initial authentication with Azure AD 40 | 41 | .PARAMETER NoAttackPaths 42 | 43 | If set, no attack paths are configured. 44 | 45 | .PARAMETER RandomAttackPath 46 | 47 | If set, only one random attack path is configured. 48 | 49 | .PARAMETER Password 50 | 51 | If set, Passwords will be leveraged for initial access simulation. Can be either random or user defined. 52 | 53 | .PARAMETER Token 54 | 55 | If set, Tokens will be leveraged for initial access simulation. 56 | 57 | 58 | .EXAMPLE 59 | 60 | .LINK 61 | 62 | https://github.com/mvelazc0/BadZure/ 63 | 64 | 65 | #> 66 | 67 | [CmdletBinding()] 68 | 69 | param 70 | ( 71 | [Parameter(Mandatory = $false)] 72 | [switch]$Build, 73 | [Parameter(Mandatory = $false)] 74 | [switch]$Destroy, 75 | [Parameter(Mandatory = $false)] 76 | [switch]$NoAttackPaths, 77 | [Parameter(Mandatory = $false)] 78 | [String]$Password, 79 | [Parameter(Mandatory = $false)] 80 | [Switch]$Token, 81 | [Parameter(Mandatory = $false)] 82 | [Switch]$RandomAttackPath, 83 | [Parameter(Mandatory = $true, ValueFromPipeline=$true)] 84 | [string]$TenantId 85 | 86 | ) 87 | $Verbose = $false 88 | if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent){$Verbose = $true} 89 | 90 | Write-Host $banner 91 | 92 | if($Build -eq $true){ 93 | 94 | 95 | 96 | Connect-Graph -Scopes "Application.ReadWrite.All", "Directory.AccessAsUser.All","EntitlementManagement.ReadWrite.All","RoleManagement.ReadWrite.Directory","Group.Read.All" -TenantId $TenantId | Out-Null #Added the -TenantId parameter here 97 | 98 | CheckDomain 99 | # create principals 100 | CreateUsers 101 | CreateGroups 102 | CreateApps 103 | CreateAdministrativeUnits 104 | 105 | # assign random groups and permissions 106 | AssignGroups 107 | AssignUserRoles 108 | AssignAppRoles 109 | AssignAppApiPermissions 110 | 111 | # create attack paths 112 | if($NoAttackPaths -eq $false){ 113 | 114 | # choose a random attack path 115 | if ($RandomAttackPath -eq $true) 116 | { 117 | $path = Get-Random (1,3) 118 | Switch ($path){ 119 | 1 {CreateAttackPath1 $Password $Token} 120 | 2 {CreateAttackPath2 $Password $Token} 121 | 3 {CreateAttackPath3 $Password $Token} 122 | 123 | } 124 | } 125 | else 126 | { 127 | CreateAttackPath1 $Password $Token 128 | CreateAttackPath2 $Password $Token 129 | CreateAttackPath3 $Password $Token 130 | $global:attackpath_apps =@() 131 | } 132 | } 133 | } 134 | elseif($Destroy-eq $true){ 135 | 136 | 137 | 138 | Connect-Graph -Scopes "Application.ReadWrite.All", "Directory.AccessAsUser.All","EntitlementManagement.ReadWrite.All","RoleManagement.ReadWrite.Directory","Group.Read.All" -TenantId $TenantId | Out-Null #Added the -TenantId parameter here 139 | 140 | CheckDomain 141 | # remove principals 142 | DeleteUsers 143 | DeleteGroups 144 | DeleteApps 145 | DeleteeAdministrativeUnits 146 | } 147 | else{ 148 | 149 | Get-Help Invoke-BadZure 150 | 151 | } 152 | 153 | } 154 | 155 | ## Global var 156 | 157 | $global:attackpath_apps = @() 158 | $global:tenantDomain = "" 159 | 160 | ## Create functions 161 | 162 | Function CheckDomain([Boolean]$Verbose) { 163 | 164 | 165 | $account = (Get-MgContext | Select-Object Account).Account 166 | if ([string]::IsNullorEmpty($account) -eq $false){ 167 | $pos=$account.IndexOf('@') 168 | $global:tenantDomain = $account.Substring($pos+1) 169 | } 170 | else { 171 | $global:tenantDomain = Read-Host -Prompt "Enter your tenant's domain name. Example badzure.com" 172 | } 173 | 174 | } 175 | 176 | 177 | Function CreateUsers([Boolean]$Verbose) { 178 | 179 | Write-Host [!] Creating Users 180 | # set a random password for each user 181 | $randomString = -join ((33..47) + (48..57) + (65..90) + (97..122) + (123..126) | Get-Random -Count 15 | % { [char]$_ }) 182 | $PasswordProfile = @{ 183 | Password = $randomString 184 | } 185 | 186 | $users = Import-Csv -Path "Csv\users.csv" 187 | <# 188 | $checkdomain = (Get-MgContext | Select-Object Account).Account 189 | #if $checkdomain has a value, use it as part of the newly created users' email addresses 190 | if ([string]::IsNullorEmpty($checkdomain) -eq $false){ 191 | $checkdomain = $account 192 | } 193 | #if get-mgcontext does not have the .account, ask them to enter their domain in the form of an email. code flow proceeds. 194 | else{ 195 | $account = Read-Host -Prompt "Enter a verified email domain in the format hello@emaildomain" 196 | } 197 | 198 | $pos=$account.IndexOf('@') 199 | $domain=$account.Substring($pos+1) 200 | #> 201 | $upns=@() 202 | 203 | 204 | foreach ($user in $users) { 205 | $displayName = -join($user.FirstName,'.',$user.LastName) 206 | #$upn = -join($displayName,'@',$domain) 207 | $upn = -join($displayName,'@',$global:tenantDomain) 208 | $upns+=$upn 209 | New-MgUser -DisplayName $displayName -PasswordProfile $PasswordProfile -AccountEnabled -MailNickName $displayName -UserPrincipalName $upn | Out-Null 210 | Write-Verbose "`t[+] Created User $upn" 211 | } 212 | $upns | Out-File -FilePath users.txt 213 | } 214 | 215 | Function CreateApps([Boolean]$Verbose){ 216 | 217 | Write-Host [!] Creating application registrations and service principals 218 | $apps = Import-Csv -Path "Csv\apps.csv" 219 | foreach ($app in $apps) { 220 | 221 | $new_app= New-MgApplication -DisplayName $app.DisplayName 222 | $new_sp= New-MgServicePrincipal -AppId $new_app.Appid 223 | Write-Verbose "`t[+] Created application with displayname $($app.DisplayName) and Service Principal $($new_sp.Id)" 224 | 225 | } 226 | } 227 | 228 | Function CreateGroups([Boolean]$Verbose){ 229 | 230 | Write-Host [!] Creating Groups 231 | $groups = Import-Csv -Path "Csv\groups.csv" 232 | foreach ($group in $groups) { 233 | 234 | $nickName= $group.DisplayName -replace (' ','') 235 | #$new_group = New-MgGroup -DisplayName $group.DisplayName -MailEnabled:$False -MailNickName $nickName -SecurityEnabled -IsAssignableToRole 236 | $new_group = New-MgGroup -DisplayName $group.DisplayName -MailEnabled:$False -MailNickName $nickName -SecurityEnabled 237 | Write-Verbose "`t[+] Created group with displayname $($new_group.DisplayName) and Id $($new_group.Id)" 238 | 239 | } 240 | } 241 | 242 | Function CreateAdministrativeUnits([Boolean]$Verbose){ 243 | 244 | Write-Host [!] Creating administrative units 245 | $a_units = Import-Csv -Path "Csv\a_units.csv" 246 | foreach ($a_unit in $a_units) { 247 | 248 | $params = @{ 249 | displayName = $a_unit.DisplayName 250 | } 251 | $new_adunit = New-MgDirectoryAdministrativeUnit -BodyParameter $params 252 | Write-Verbose "`t[+] Created administrative unit with displayname $($new_adunit.DisplayName) and Id $($new_adunit.Id)" 253 | } 254 | 255 | } 256 | 257 | 258 | ## Delete functions 259 | 260 | 261 | Function DeleteGroups([Boolean]$Verbose){ 262 | 263 | Write-Host [!] Removing groups 264 | $groups = Import-Csv -Path "Csv\groups.csv" 265 | foreach ($group in $groups) { 266 | 267 | $displayName = $group.DisplayName 268 | $groups = Get-MgGroup -Filter "DisplayName eq '$displayName'" 269 | 270 | # in case groups were created more than once 271 | if ($groups -is [Array]) { 272 | foreach ($group in $groups){ 273 | 274 | Remove-MgGroup -GroupId $group.Id 275 | Write-Verbose "`t[+] Deleted group with displayname $($group.DisplayName) and Id $($group.Id)" 276 | 277 | } 278 | } 279 | else { 280 | Remove-MgGroup -GroupId $($groups.Id) 281 | Write-Verbose "`t[+] Deleted group with displayname $($groups.DisplayName) and Id $($groups.Id)" 282 | } 283 | 284 | } 285 | } 286 | 287 | 288 | 289 | Function DeleteApps([Boolean]$Verbose){ 290 | 291 | Write-Host [!] Removing application registrations 292 | 293 | $apps = Import-Csv -Path "Csv\apps.csv" 294 | foreach ($app in $apps) { 295 | 296 | $DisplayName = $app.DisplayName 297 | $app_ids= (Get-MgApplication -Filter "DisplayName eq '$DisplayName'").Id 298 | 299 | # in case apps were created more than once 300 | if ($app_ids -is [Array]) { 301 | foreach ($app_id in $app_ids){ 302 | Remove-MgApplication -ApplicationId $app_id | Out-Null 303 | Write-Verbose "`t[+] Deleted application with Id $app_id" 304 | 305 | } 306 | } 307 | else { 308 | Remove-MgApplication -ApplicationId $app_ids | Out-Null 309 | Write-Verbose "`t[+] Deleted application with Id $app_ids" 310 | } 311 | 312 | } 313 | } 314 | 315 | 316 | Function DeleteUsers([Boolean]$Verbose){ 317 | 318 | Write-Host [!] Removing users 319 | 320 | $users = Import-Csv -Path "Csv\users.csv" 321 | <# 322 | $checkdomain = (Get-MgContext | Select-Object Account).Account 323 | #if $checkdomain has a value, use it as part of the newly created users' email addresses 324 | if ([string]::IsNullorEmpty($checkdomain) -eq $false){ 325 | $checkdomain = $account 326 | } 327 | #if get-mgcontext does not have the .account, ask them to enter their domain in the form of an email. code flow proceeds. 328 | else{ 329 | $account = Read-Host -Prompt "Enter a verified email domain in the format hello@emaildomain" 330 | } 331 | 332 | $pos=$account.IndexOf('@') 333 | $domain=$account.Substring($pos+1) 334 | #> 335 | 336 | foreach ($user in $users) { 337 | $displayName = -join($user.FirstName,'.',$user.LastName) 338 | 339 | #$upn = -join($displayName,'@',$domain) 340 | $upn = -join($displayName,'@',$global:tenantDomain) 341 | $user = Get-MgUser -Filter "UserPrincipalName eq '$upn'" 342 | Remove-MgUser -UserId $user.Id 343 | Write-Verbose "`t[+] Deleted user with ObjectId $($user.Id)" 344 | } 345 | } 346 | 347 | 348 | Function DeleteeAdministrativeUnits([Boolean]$Verbose){ 349 | 350 | Write-Host [!] Removing administrative units 351 | $a_units = Import-Csv -Path "Csv\a_units.csv" 352 | foreach ($a_unit in $a_units) { 353 | 354 | $DisplayName = $a_unit.DisplayName 355 | $admunit_ids= (Get-MgDirectoryAdministrativeUnit -Filter "DisplayName eq '$DisplayName'").Id 356 | 357 | # in case adm units were created more than once 358 | if ($admunit_ids -is [Array]) { 359 | foreach ($admunit_id in $admunit_ids){ 360 | 361 | Remove-MgDirectoryAdministrativeUnit -AdministrativeUnitId $admunit_id | Out-Null 362 | Write-Verbose "`t[+] Deleted administrative unit with Id $admunit_id" 363 | 364 | } 365 | } 366 | else { 367 | Remove-MgDirectoryAdministrativeUnit -AdministrativeUnitId $admunit_ids| Out-Null 368 | Write-Verbose "`t[+] Deleted administrative unit with Id $admunit_ids" 369 | } 370 | } 371 | } 372 | 373 | 374 | ## Assign functions 375 | 376 | Function AssignGroups([Boolean]$Verbose){ 377 | 378 | Write-Host [!] Assigning random users to random groups 379 | $users = Import-Csv -Path "Csv/users.csv" 380 | <# 381 | $checkdomain = (Get-MgContext | Select-Object Account).Account 382 | #if $checkdomain has a value, use it as part of the newly created users' email addresses 383 | if ([string]::IsNullorEmpty($checkdomain) -eq $false){ 384 | $checkdomain = $account 385 | } 386 | #if get-mgcontext does not have the .account, ask them to enter their domain in the form of an email. code flow proceeds. 387 | else{ 388 | $account = Read-Host -Prompt "Enter a verified email domain in the format hello@emaildomain" 389 | } 390 | 391 | $pos=$account.IndexOf('@') 392 | $domain=$account.Substring($pos+1) 393 | #> 394 | 395 | $user_ids = @() 396 | 397 | foreach ($user in $users) { 398 | $displayName = -join($user.FirstName,'.',$user.LastName) 399 | #$upn = -join($displayName,'@',$domain) 400 | $upn = -join($displayName,'@',$global:tenantDomain) 401 | $user = Get-MgUser -Filter "UserPrincipalName eq '$upn'" 402 | $user_ids +=$user.Id 403 | } 404 | 405 | $groups = Import-Csv -Path "Csv\groups.csv" 406 | foreach ($group in $groups) { 407 | 408 | $displayName = $group.DisplayName 409 | $group_id = (Get-MgGroup -Filter "DisplayName eq '$displayName'").Id 410 | $used_users = @() 411 | foreach($i in 1..3){ 412 | do 413 | { 414 | $random_user = (Get-Random $user_ids) 415 | } 416 | until ($used_users -notcontains $random_user) 417 | New-MgGroupMember -GroupId $group_id -DirectoryObjectId $random_user 418 | Write-Verbose "`t[+] Added user with Id $random_user to group with id $group_id" 419 | 420 | $used_users += $random_user 421 | } 422 | 423 | } 424 | 425 | } 426 | 427 | 428 | Function AssignAppRoles([Boolean]$Verbose){ 429 | 430 | Write-Host [!] Assigning random Azure Ad roles to applications 431 | $roles = ('Exchange Administrator', 'Security Operator', 'Network Administrator', 'Intune Administrator', 'Attack Simulation Administrator', 'Application Developer') 432 | $apps = Import-Csv -Path "Csv\apps.csv" 433 | $used_apps =@() 434 | foreach ($role in $roles) 435 | { 436 | do 437 | { 438 | $random_app_dn = (Get-Random $apps).DisplayName 439 | } 440 | until ($used_apps -notcontains $random_app_dn) 441 | 442 | $roleDefinitionId = (Get-MgRoleManagementDirectoryRoleDefinition -Filter "DisplayName eq '$role'").Id 443 | $appSpId = (Get-MgServicePrincipal -Filter "DisplayName eq '$random_app_dn'").Id 444 | $appId = (Get-MgApplication -Filter "DisplayName eq '$random_app_dn'").Id 445 | New-MgRoleManagementDirectoryRoleAssignment -PrincipalId $appSpId -RoleDefinitionId $roleDefinitionId -DirectoryScopeId "/" | Out-Null 446 | Write-Verbose "`t[+] Assigned $role to application with displayName $random_app_dn" 447 | $used_apps += $random_app_dn 448 | 449 | } 450 | 451 | } 452 | 453 | Function AssignAppApiPermissions([Boolean]$Verbose){ 454 | 455 | 456 | Write-Host [!] Assigning random Graph API permissions to applications 457 | $apps = Import-Csv -Path "Csv\apps.csv" 458 | 459 | $permissions = ('d07a8cc0-3d51-4b77-b3b0-32704d1f69fa', '134fd756-38ce-4afd-ba33-e9623dbe66c2', '93283d0a-6322-4fa8-966b-8c121624760d', '798ee544-9d2d-430c-a058-570e29e34338', '6b7d71aa-70aa-4810-a8d9-5d9fb2830017', '7e05723c-0bb0-42da-be95-ae9f08a6e53c' ,'4f5ac95f-62fd-472c-b60f-125d24ca0bc5' , 'eedb7fdd-7539-4345-a38b-4839e4a84cbd') 460 | # 06da0dbc-49e2-44d2-8312-53f166ab848a 461 | # eda39fa6-f8cf-4c3c-a909-432c683e4c9b 462 | # eda39fa6-f8cf-4c3c-a909-432c683e4c9b 463 | # 6323133e-1f6e-46d4-9372-ac33a0870636 464 | 465 | 466 | $used_apps =@() 467 | 468 | foreach ($permission in $permissions) 469 | { 470 | do 471 | { 472 | $random_app_dn = (Get-Random $apps).DisplayName 473 | } 474 | until ($used_apps -notcontains $random_app_dn) 475 | 476 | $resourceId = (Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Graph'" -Property "id,displayName,appId,appRoles").Id 477 | $appSpId = (Get-MgServicePrincipal -Filter "DisplayName eq '$random_app_dn'").Id 478 | $appId = (Get-MgApplication -Filter "DisplayName eq '$random_app_dn'").Id 479 | 480 | $params = @{ 481 | PrincipalId = $appSpId 482 | ResourceId = $resourceId 483 | AppRoleId = $permission 484 | } 485 | New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $appSpId -BodyParameter $params | Out-Null 486 | Write-Verbose "`t[+] Assigned API permissions $permission to application with displayName $random_app_dn" 487 | $used_apps += $random_app_dn 488 | 489 | } 490 | } 491 | 492 | 493 | Function AssignUserRoles([string]$Password, [Boolean]$Verbose) { 494 | 495 | 496 | Write-Host [!] Assigning random Azure Ad roles to users 497 | $users = Import-Csv -Path "Csv/users.csv" 498 | <# 499 | $checkdomain = (Get-MgContext | Select-Object Account).Account 500 | #if $checkdomain has a value, use it as part of the newly created users' email addresses 501 | if ([string]::IsNullorEmpty($checkdomain) -eq $false){ 502 | $checkdomain = $account 503 | } 504 | #if get-mgcontext does not have the .account, ask them to enter their domain in the form of an email. code flow proceeds. 505 | else{ 506 | $account = Read-Host -Prompt "Enter a verified email domain in the format hello@emaildomain" 507 | } 508 | 509 | $pos=$account.IndexOf('@') 510 | $domain=$account.Substring($pos+1) 511 | #> 512 | 513 | $user_ids = @() 514 | 515 | foreach ($user in $users) { 516 | $displayName = -join($user.FirstName,'.',$user.LastName) 517 | #$upn = -join($displayName,'@',$domain) 518 | $upn = -join($displayName,'@',$global:tenantDomain) 519 | $user = Get-MgUser -Filter "UserPrincipalName eq '$upn'" 520 | $user_ids +=$user.Id 521 | } 522 | 523 | $used_users = @() 524 | $roles = ('Reports Reader', 'Reports Reader', 'Authentication Administrator', 'Directory Readers', 'Guest Inviter', 'Message Center Reader', 'Groups Administrator', 'Guest Inviter', 'Network Administrator') 525 | foreach ($role in $roles) 526 | { 527 | do 528 | { 529 | $random_user = (Get-Random $user_ids) 530 | } 531 | until ($used_users -notcontains $random_user) 532 | 533 | $roleDefinitionId = (Get-MgRoleManagementDirectoryRoleDefinition -Filter "DisplayName eq '$role'").Id 534 | New-MgRoleManagementDirectoryRoleAssignment -PrincipalId $random_user -RoleDefinitionId $roleDefinitionId -DirectoryScopeId "/" | Out-Null 535 | Write-Verbose "`t[+] Assigned $role to user with id $random_user" 536 | $used_users += $random_user 537 | } 538 | } 539 | 540 | 541 | 542 | 543 | ## Attack path functions 544 | 545 | Function CreateAttackPath1 ([String]$Password, [Boolean]$Token){ 546 | 547 | Write-Host [!] Creating attack path 1 548 | 549 | <# 550 | We have to use the Graph beta based on https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/880 551 | Select-MgProfile beta -Verbose:$false 552 | $directoryRole='Privileged Role Administrator' 553 | $directoryRoleId= (Get-MgDirectoryRole -Filter "DisplayName eq '$directoryRole'").Id 554 | $service_principal_id="" 555 | $service_principals= Get-MgDirectoryRoleMember -DirectoryRoleId $directoryRoleId | where { $_.AdditionalProperties."@odata.type" -eq "#microsoft.graph.servicePrincipal"} 556 | Select-MgProfile v1.0 -Verbose:$false 557 | #> 558 | 559 | $role = "Privileged Role Administrator" 560 | $apps = Import-Csv -Path "Csv\apps.csv" 561 | 562 | Do 563 | { 564 | $random_app_dn = (Get-Random $apps).DisplayName 565 | $appSpId = (Get-MgServicePrincipal -Filter "DisplayName eq '$random_app_dn'").Id 566 | $appId = (Get-MgApplication -Filter "DisplayName eq '$random_app_dn'").Id 567 | } 568 | While ($global:attackpath_apps -contains $appId ) 569 | 570 | $roleDefinitionId = (Get-MgRoleManagementDirectoryRoleDefinition -Filter "DisplayName eq '$role'").Id 571 | New-MgRoleManagementDirectoryRoleAssignment -PrincipalId $appSpId -RoleDefinitionId $roleDefinitionId -DirectoryScopeId "/" | Out-Null 572 | Write-Verbose "`t[+] Assigned $role to application with displayName $random_app_dn" 573 | $random_user_id= GetRandomUser 574 | $NewOwner = @{ 575 | "@odata.id"= "https://graph.microsoft.com/v1.0/directoryObjects/{$random_user_id}" 576 | } 577 | 578 | New-MgApplicationOwnerByRef -ApplicationId $appId -BodyParameter $NewOwner 579 | Write-Verbose "`t[+] Created application owner for $appId" 580 | $global:attackpath_apps+=$appId 581 | UpdatePassword $random_user_id $Password $Token 582 | 583 | 584 | 585 | } 586 | 587 | Function CreateAttackPath2([String]$Password, [Boolean]$Token){ 588 | 589 | Write-Host [!] Creating attack path 2 590 | 591 | $random_user_id = GetRandomUser 592 | $role= 'Helpdesk Administrator' 593 | $roleDefinitionId = (Get-MgRoleManagementDirectoryRoleDefinition -Filter "DisplayName eq '$role'").Id 594 | New-MgRoleManagementDirectoryRoleAssignment -PrincipalId $random_user_id -RoleDefinitionId $roleDefinitionId -DirectoryScopeId "/" | Out-Null 595 | Write-Verbose "`t[+] Assigned $role to user with id $random_user_id" 596 | 597 | $NewOwner = @{ 598 | "@odata.id"= "https://graph.microsoft.com/v1.0/directoryObjects/$random_user_id" 599 | } 600 | 601 | 602 | <# 603 | $applications = Import-Csv -Path "Csv\apps.csv" 604 | $service_principal_ids= @() 605 | foreach ($app in $applications) { 606 | 607 | $DisplayName = $app.DisplayName 608 | $service_principal_ids+=(Get-MgServicePrincipal -Filter "DisplayName eq '$DisplayName'").Id 609 | } 610 | 611 | foreach ($service_principal_id in $service_principal_ids){ 612 | $appRoleId = (Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $service_principal_id).AppRoleId 613 | if ($appRoleId -eq '9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8') 614 | { 615 | $DisplayName = (Get-MgServicePrincipal -ServicePrincipalId $service_principal_id).DisplayName 616 | $appId= (Get-MgApplication -Filter "DisplayName eq '$DisplayName'").Id 617 | New-MgApplicationOwnerByRef -ApplicationId $appId -BodyParameter $NewOwner 618 | Write-Host `t[+] Created application owner for $appId 619 | UpdatePassword $user_id $Password $Token 620 | 621 | } 622 | #> 623 | 624 | # RoleManagement.ReadWrite.Directory 625 | $permission = ('9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8') 626 | $apps = Import-Csv -Path "Csv\apps.csv" 627 | 628 | Do 629 | { 630 | $random_app_dn = (Get-Random $apps).DisplayName 631 | $appSpId = (Get-MgServicePrincipal -Filter "DisplayName eq '$random_app_dn'").Id 632 | $appId = (Get-MgApplication -Filter "DisplayName eq '$random_app_dn'").Id 633 | } 634 | While ($global:attackpath_apps -contains $appId ) 635 | 636 | $resourceId = (Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Graph'" -Property "id,displayName,appId,appRoles").Id 637 | 638 | $params = @{ 639 | PrincipalId = $appSpId 640 | ResourceId = $resourceId 641 | AppRoleId = $permission 642 | } 643 | 644 | New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $appSpId -BodyParameter $params | Out-Null 645 | Write-Verbose "[+] Assigned API permissions $permission to application with displayName $random_app_dn" 646 | New-MgApplicationOwnerByRef -ApplicationId $appId -BodyParameter $NewOwner 647 | Write-Verbose "[+] Created application owner for $appId" 648 | $global:attackpath_apps+=$appId 649 | UpdatePassword $random_user_id $Password $Token 650 | 651 | } 652 | 653 | Function CreateAttackPath3([String]$Password, [Boolean]$Token){ 654 | 655 | Write-Host [!] Creating attack path3 656 | 657 | $random_user_id = GetRandomUser 658 | $role= 'User Administrator' 659 | $roleDefinitionId = (Get-MgRoleManagementDirectoryRoleDefinition -Filter "DisplayName eq '$role'").Id 660 | New-MgRoleManagementDirectoryRoleAssignment -PrincipalId $random_user_id -RoleDefinitionId $roleDefinitionId -DirectoryScopeId "/" | Out-Null 661 | Write-Verbose "`t[+] Assigned $role to user with id $random_user_id" 662 | 663 | 664 | <# 665 | $directoryRole='User Administrator' 666 | $directoryRoleId= (Get-MgDirectoryRole -Filter "DisplayName eq '$directoryRole'").Id 667 | $user_id="" 668 | $users = Get-MgDirectoryRoleMember -DirectoryRoleId $directoryRoleId 669 | 670 | if ($users -is [Array]){ 671 | 672 | $user_id=$users[0].Id 673 | } 674 | 675 | else { 676 | $user_id=$users.Id 677 | } 678 | #> 679 | 680 | $NewOwner = @{ 681 | "@odata.id"= "https://graph.microsoft.com/v1.0/directoryObjects/$random_user_id" 682 | } 683 | 684 | 685 | # AppRoleAssignment.ReadWrite.All 686 | $permission = ('06b708a9-e830-4db3-a914-8e69da51d44f') 687 | $apps = Import-Csv -Path "Csv\apps.csv" 688 | 689 | Do 690 | { 691 | $random_app_dn = (Get-Random $apps).DisplayName 692 | $appSpId = (Get-MgServicePrincipal -Filter "DisplayName eq '$random_app_dn'").Id 693 | $appId = (Get-MgApplication -Filter "DisplayName eq '$random_app_dn'").Id 694 | } 695 | While ($global:attackpath_apps -contains $appId ) 696 | 697 | $resourceId = (Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Graph'" -Property "id,displayName,appId,appRoles").Id 698 | 699 | $params = @{ 700 | PrincipalId = $appSpId 701 | ResourceId = $resourceId 702 | AppRoleId = $permission 703 | } 704 | 705 | New-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $appSpId -BodyParameter $params | Out-Null 706 | Write-Verbose "`t[+] Assigned API permissions $permission to application with displayName $random_app_dn" 707 | New-MgApplicationOwnerByRef -ApplicationId $appId -BodyParameter $NewOwner 708 | Write-Verbose "`t[+] Created application owner for $appId" 709 | $global:attackpath_apps+=$appId 710 | UpdatePassword $random_user_id $Password $Token 711 | 712 | } 713 | 714 | 715 | ## Util functions 716 | 717 | Function GetRandomUser{ 718 | 719 | <# 720 | $checkdomain = (Get-MgContext | Select-Object Account).Account 721 | #if $checkdomain has a value, use it as part of the newly created users' email addresses 722 | if ([string]::IsNullorEmpty($checkdomain) -eq $false){ 723 | $checkdomain = $account 724 | } 725 | #if get-mgcontext does not have the .account, ask them to enter their domain in the form of an email. code flow proceeds. 726 | else{ 727 | $account = Read-Host -Prompt "Enter a verified email domain in the format hello@emaildomain" 728 | } 729 | 730 | $pos=$account.IndexOf('@') 731 | $domain=$account.Substring($pos+1) 732 | #> 733 | 734 | $users = Import-Csv -Path "Csv/users.csv" 735 | $user_ids = @() 736 | 737 | foreach ($user in $users) { 738 | $displayName = -join($user.FirstName,'.',$user.LastName) 739 | #$upn = -join($displayName,'@',$domain) 740 | $upn = -join($displayName,'@',$global:tenantDomain) 741 | $user = Get-MgUser -Filter "UserPrincipalName eq '$upn'" 742 | $user_ids +=$user.Id 743 | } 744 | $random_userid = (Get-Random $user_ids) 745 | return $random_userid 746 | 747 | } 748 | 749 | 750 | Function UpdatePassword ([String]$userId, [String]$Password, [Boolean]$Token) { 751 | 752 | if([string]::IsNullOrEmpty($Password)){ 753 | 754 | $randomString = -join ((33..47) + (48..57) + (65..90) + (97..122) + (123..126) | Get-Random -Count 15 | % { [char]$_ }) 755 | $NewPassword = @{} 756 | $NewPassword["Password"]= $randomString 757 | $NewPassword["ForceChangePasswordNextSignIn"] = $False 758 | Update-Mguser -UserId $userId.Trim() -PasswordProfile $NewPassword 759 | $username = (Get-MgUser -Filter "Id eq '$userId'").UserPrincipalName 760 | 761 | if ($Token -eq $false) 762 | { 763 | Write-Host `t[+] `"$randomString`" assigned as password to random user. 764 | Write-Verbose "t[+] $username" 765 | 766 | } 767 | else{ 768 | GetAccessToken2 $userId $randomString 769 | } 770 | 771 | } 772 | else{ 773 | 774 | $NewPassword = @{} 775 | $NewPassword["Password"]= $Password 776 | $NewPassword["ForceChangePasswordNextSignIn"] = $False 777 | Update-Mguser -UserId $userId.Trim() -PasswordProfile $NewPassword 778 | $username = (Get-MgUser -Filter "Id eq '$userId'").UserPrincipalName 779 | if ($Token -eq $false) 780 | { 781 | Write-Host `t[+] `"$Password`" assigned as password to random user. 782 | Write-Verbose "`t[+] $username" 783 | } 784 | else{ 785 | GetAccessToken2 $userId $Password 786 | } 787 | 788 | } 789 | 790 | } 791 | 792 | Function GetAccessToken ([String]$userId, [String]$Password) { 793 | 794 | Write-Host `t`[!] Obtaining user access token 795 | $username = (Get-MgUser -Filter "Id eq '$userId'").UserPrincipalName 796 | $SecurePassword = ConvertTo-SecureString “$Password” -AsPlainText -Force 797 | $credentials = New-Object System.Management.Automation.PSCredential($username, $SecurePassword) 798 | Connect-AzAccount -Credential $credentials | Out-Null 799 | Write-Host `t`[+] Access token for $username : 800 | Write-Host `t` (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com/").Token 801 | 802 | } 803 | 804 | Function GetAccessToken2 ([String]$userId, [String]$Password) { 805 | 806 | Write-Host `t`[!] Obtaining access and refresh tokens for $username 807 | $username = (Get-MgUser -Filter "Id eq '$userId'").UserPrincipalName 808 | $tenantId = (Get-MgContext).TenantId 809 | $tokens = Get-CKAccessToken -ClientId 1950a258-227b-4e31-a9cf-717495945fc2 -Resource 'https://graph.microsoft.com/' -TenantId $tenantId -GrantType password -Username $username -Password $Password -Verbose:$false 810 | $access_token = $tokens.access_token 811 | $refresh_token = $tokens.refresh_token 812 | Write-Host `t`[+] access_token:$access_token 813 | Write-Host `t`[+] refresh_token:$refresh_token 814 | 815 | } 816 | 817 | 818 | ## External Functions 819 | 820 | ## credtis to Roberto Rodriguez for this function https://github.com/Azure/Cloud-Katana/blob/main/CloudKatanaAbilities/AzureAD/Authentication/Get-CKAccessToken.ps1 821 | function Get-CKAccessToken { 822 | <# 823 | .SYNOPSIS 824 | A PowerShell script to get a MS graph access token with a specific grant type and Azure AD application. 825 | 826 | Author: Roberto Rodriguez (@Cyb3rWard0g) 827 | License: MIT 828 | Required Dependencies: None 829 | Optional Dependencies: None 830 | 831 | .DESCRIPTION 832 | Get-CKAccessToken is a simple PowerShell wrapper around the Microsoft Graph API to get an access token. 833 | 834 | .PARAMETER ClientId 835 | The Application (client) ID assigned to the Azure AD application. 836 | 837 | .PARAMETER TenantId 838 | Tenant ID. Can be /common, /consumers, or /organizations. It can also be the directory tenant that you want to request permission from in GUID or friendly name format. 839 | 840 | .PARAMETER ResourceUrl 841 | Resource url for what you're requesting token. This could be one of the Azure services that support Azure AD authentication or any other resource URI. Example: https://graph.microsoft.com/ 842 | 843 | .PARAMETER GrantType 844 | The type of token request. 845 | 846 | .PARAMETER Username 847 | Username used for Password grant type. 848 | 849 | .PARAMETER Password 850 | Password used for Password grant type. 851 | 852 | .PARAMETER SamlToken 853 | SAML token used for SAML token grant type. 854 | 855 | .PARAMETER DeviceCode 856 | The device_code returned in the device authorization request. 857 | 858 | .PARAMETER AppSecret 859 | if the application requires a client secret, then use this parameter. 860 | 861 | .LINK 862 | https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-overview 863 | 864 | #> 865 | 866 | [cmdletbinding()] 867 | Param( 868 | [Parameter(Mandatory = $true)] 869 | [String] $ClientId, 870 | 871 | [Parameter(Mandatory = $false)] 872 | [string] $TenantId, 873 | 874 | [Parameter(Mandatory = $false)] 875 | [string] $Resource = 'https://graph.microsoft.com/', 876 | 877 | [Parameter(Mandatory=$true)] 878 | [ValidateSet("client_credentials","password","saml_token","device_code","refresh_token")] 879 | [string] $GrantType, 880 | 881 | [Parameter(Mandatory=$false)] 882 | [AllowEmptyString()] 883 | [string] $AppSecret 884 | ) 885 | DynamicParam { 886 | if ($GrantType) { 887 | # Adding Dynamic parameters 888 | if ($GrantType -eq 'password') { 889 | $ParamOptions = @( 890 | @{ 891 | 'Name' = 'Username'; 892 | 'Mandatory' = $true 893 | }, 894 | @{ 895 | 'Name' = 'Password'; 896 | 'Mandatory' = $true 897 | } 898 | ) 899 | } 900 | elseif ($GrantType -eq 'saml_token') { 901 | $ParamOptions = @( 902 | @{ 903 | 'Name' = 'SamlToken'; 904 | 'Mandatory' = $true 905 | } 906 | ) 907 | } 908 | elseif ($GrantType -eq 'device_code') { 909 | $ParamOptions = @( 910 | @{ 911 | 'Name' = 'DeviceCode'; 912 | 'Mandatory' = $true 913 | } 914 | ) 915 | } 916 | elseif ($GrantType -eq 'refresh_token') { 917 | $ParamOptions = @( 918 | @{ 919 | 'Name' = 'RefreshToken'; 920 | 'Mandatory' = $true 921 | } 922 | ) 923 | } 924 | 925 | # Adding Dynamic parameter 926 | $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary 927 | foreach ($Param in $ParamOptions) { 928 | $RuntimeParam = New-DynamicParam @Param 929 | $RuntimeParamDic.Add($Param.Name, $RuntimeParam) 930 | } 931 | return $RuntimeParamDic 932 | } 933 | } 934 | begin { 935 | # Force TLS 1.2 936 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 937 | 938 | # Process Tenant ID 939 | if (!$TenantId) { 940 | $TenantId = 'common' 941 | } 942 | 943 | # Process Dynamic parameters 944 | $PsBoundParameters.GetEnumerator() | ForEach-Object { New-Variable -Name $_.Key -Value $_.Value -ea 'SilentlyContinue'} 945 | } 946 | process { 947 | # Initialize Headers dictionary 948 | $headers = @{} 949 | $headers.Add('Content-Type','application/x-www-form-urlencoded') 950 | 951 | # Initialize Body 952 | $body = @{} 953 | $body.Add('resource',$Resource) 954 | $body.Add('client_id',$ClientId) 955 | 956 | if ($GrantType -eq 'client_credentials') { 957 | $body.Add('grant_type','client_credentials') 958 | } 959 | elseif ($GrantType -eq 'password') { 960 | $body.Add('username',$Username) 961 | $body.Add('password',$Password) 962 | $body.Add('grant_type','password') 963 | } 964 | elseif ($GrantType -eq 'saml_token') { 965 | $encodedSamlToken= [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($SamlToken)) 966 | $body.Add('assertion',$encodedSamlToken) 967 | $body.Add('grant_type','urn:ietf:params:oauth:grant-type:saml1_1-bearer') 968 | $body.Add('scope','openid') 969 | } 970 | elseif ($GrantType -eq 'device_code') { 971 | $body.Add('grant_type','urn:ietf:params:oauth:grant-type:device_code') 972 | $body.Add('code',$DeviceCode) 973 | } 974 | elseif ($GrantType -eq 'refresh_token') { 975 | $body.Add('refresh_token',$RefreshToken) 976 | $body.Add('grant_type','refresh_token') 977 | $body.Add('scope','openid') 978 | } 979 | 980 | if ($AppSecret) 981 | { 982 | $body.Add('client_secret',$AppSecret) 983 | } 984 | 985 | $Params = @{ 986 | Headers = $headers 987 | uri = "https://login.microsoftonline.com/$TenantId/oauth2/token?api-version=1.0" 988 | Body = $body 989 | method = 'Post' 990 | } 991 | $request = Invoke-RestMethod @Params 992 | 993 | # Process authentication request 994 | if($null -eq $request) { 995 | throw "Token never received from AAD" 996 | } 997 | else { 998 | $request 999 | } 1000 | } 1001 | } 1002 | 1003 | function New-DynamicParam { 1004 | [CmdletBinding()] 1005 | [OutputType('System.Management.Automation.RuntimeDefinedParameter')] 1006 | param ( 1007 | [Parameter(Mandatory)] 1008 | [ValidateNotNullOrEmpty()] 1009 | [string]$Name, 1010 | [Parameter(Mandatory=$false)] 1011 | [array]$ValidateSetOptions, 1012 | [Parameter()] 1013 | [switch]$Mandatory = $false, 1014 | [Parameter()] 1015 | [switch]$ValueFromPipeline = $false, 1016 | [Parameter()] 1017 | [switch]$ValueFromPipelineByPropertyName = $false 1018 | ) 1019 | 1020 | $Attrib = New-Object System.Management.Automation.ParameterAttribute 1021 | $Attrib.Mandatory = $Mandatory.IsPresent 1022 | $Attrib.ValueFromPipeline = $ValueFromPipeline.IsPresent 1023 | $Attrib.ValueFromPipelineByPropertyName = $ValueFromPipelineByPropertyName.IsPresent 1024 | 1025 | # Create AttributeCollection object for the attribute 1026 | $Collection = new-object System.Collections.ObjectModel.Collection[System.Attribute] 1027 | # Add our custom attribute 1028 | $Collection.Add($Attrib) 1029 | # Add Validate Set 1030 | if ($ValidateSetOptions) 1031 | { 1032 | $ValidateSet= new-object System.Management.Automation.ValidateSetAttribute($Param.ValidateSetOptions) 1033 | $Collection.Add($ValidateSet) 1034 | } 1035 | 1036 | # Create Runtime Parameter 1037 | $DynParam = New-Object System.Management.Automation.RuntimeDefinedParameter($Param.Name, [string], $Collection) 1038 | $DynParam 1039 | } 1040 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BadZure 2 | [](https://www.blackhat.com/asia-23/arsenal/schedule/index.html#purplesharp-automated-adversary-simulation-31336) 3 | [](https://twitter.com/OTR_Community) 4 | 5 |