From fea3d63e177cfd206b22e2b4198769c0ed46322f Mon Sep 17 00:00:00 2001 From: TeneBrae93 Date: Wed, 5 Jun 2024 14:37:16 -0500 Subject: [PATCH 1/8] Updated the iam__bruteforce_permissions module --- pacu/core/enumerate_iam/__init__.py | 0 pacu/core/enumerate_iam/bruteforce_tests.py | 1148 +++++++++++++++++ .../generate_bruteforce_tests.py | 135 ++ pacu/core/enumerate_iam/main.py | 435 +++++++ pacu/core/enumerate_iam/utils/__init__.py | 0 pacu/core/enumerate_iam/utils/json_utils.py | 49 + .../enumerate_iam/utils/remove_metadata.py | 5 + .../ReadOnlyAccessPolicy.json | 317 ----- .../iam__bruteforce_permissions/main.py | 417 +----- .../param_generator.py | 60 - .../preload_actions.json | 81 -- 11 files changed, 1810 insertions(+), 837 deletions(-) create mode 100644 pacu/core/enumerate_iam/__init__.py create mode 100644 pacu/core/enumerate_iam/bruteforce_tests.py create mode 100644 pacu/core/enumerate_iam/generate_bruteforce_tests.py create mode 100644 pacu/core/enumerate_iam/main.py create mode 100644 pacu/core/enumerate_iam/utils/__init__.py create mode 100644 pacu/core/enumerate_iam/utils/json_utils.py create mode 100644 pacu/core/enumerate_iam/utils/remove_metadata.py delete mode 100644 pacu/modules/iam__bruteforce_permissions/ReadOnlyAccessPolicy.json delete mode 100644 pacu/modules/iam__bruteforce_permissions/param_generator.py delete mode 100644 pacu/modules/iam__bruteforce_permissions/preload_actions.json diff --git a/pacu/core/enumerate_iam/__init__.py b/pacu/core/enumerate_iam/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pacu/core/enumerate_iam/bruteforce_tests.py b/pacu/core/enumerate_iam/bruteforce_tests.py new file mode 100644 index 00000000..2fb87416 --- /dev/null +++ b/pacu/core/enumerate_iam/bruteforce_tests.py @@ -0,0 +1,1148 @@ +BRUTEFORCE_TESTS = { + "a4b": [ + "get_conference_preference", + "get_device", + "get_invitation_configuration", + "get_profile", + "get_room", + "get_skill_group", + "list_business_report_schedules", + "list_conference_providers", + "list_skills", + "list_skills_store_categories" + ], + "acm": [ + "list_certificates" + ], + "amplify": [ + "list_apps" + ], + "apigateway": [ + "get_account", + "get_api_keys", + "get_client_certificates", + "get_domain_names", + "get_rest_apis", + "get_sdk_types", + "get_usage_plans", + "get_vpc_links", + "get_domain_names" + ], + "appmesh": [ + "list_meshes", + "list_meshes" + ], + "appstream2": [ + "describe_directory_configs", + "describe_fleets", + "describe_image_builders", + "describe_images", + "describe_user_stack_associations" + ], + "appsync": [ + "list_graphql_apis" + ], + "athena": [ + "list_named_queries", + "list_query_executions", + "list_work_groups" + ], + "autoscaling": [ + "describe_account_limits", + "describe_adjustment_types", + "describe_auto_scaling_groups", + "describe_auto_scaling_instances", + "describe_auto_scaling_notification_types", + "describe_launch_configurations", + "describe_lifecycle_hook_types", + "describe_metric_collection_types", + "describe_notification_configurations", + "describe_policies", + "describe_scaling_activities", + "describe_scaling_process_types", + "describe_scheduled_actions", + "describe_tags", + "describe_termination_policy_types" + ], + "backup": [ + "get_supported_resource_types", + "list_backup_jobs", + "list_backup_plan_templates", + "list_backup_plans", + "list_backup_vaults", + "list_protected_resources", + "list_restore_jobs" + ], + "batch": [ + "describe_compute_environments", + "describe_job_definitions", + "describe_job_queues", + "list_jobs" + ], + "chime": [ + "list_accounts" + ], + "cloud9": [ + "describe_environment_memberships", + "list_environments" + ], + "clouddirectory": [ + "list_development_schema_arns", + "list_directories", + "list_published_schema_arns", + "list_development_schema_arns", + "list_directories", + "list_managed_schema_arns", + "list_published_schema_arns" + ], + "cloudformation": [ + "describe_account_limits", + "describe_stack_events", + "describe_stack_resources", + "get_template", + "get_template_summary", + "list_exports", + "list_stack_sets", + "list_stacks" + ], + "cloudfront": [ + "list_cloud_front_origin_access_identities", + "list_distributions", + "list_field_level_encryption_configs", + "list_field_level_encryption_profiles", + "list_streaming_distributions", + "list_cloud_front_origin_access_identities", + "list_distributions", + "list_streaming_distributions", + "list_cloud_front_origin_access_identities", + "list_distributions", + "list_field_level_encryption_configs", + "list_field_level_encryption_profiles", + "list_streaming_distributions", + "list_cloud_front_origin_access_identities", + "list_distributions", + "list_field_level_encryption_configs", + "list_field_level_encryption_profiles", + "list_streaming_distributions", + "list_cloud_front_origin_access_identities", + "list_distributions", + "list_streaming_distributions" + ], + "cloudhsm": [ + "describe_hsm", + "describe_luna_client", + "list_available_zones", + "list_hapgs", + "list_hsms", + "list_luna_clients" + ], + "cloudhsmv2": [ + "describe_backups", + "describe_clusters" + ], + "cloudsearch": [ + "describe_domains", + "list_domain_names", + "describe_domains" + ], + "cloudtrail": [ + "describe_trails" + ], + "codebuild": [ + "list_builds", + "list_curated_environment_images", + "list_projects", + "list_source_credentials" + ], + "codecommit": [ + "get_branch", + "list_repositories" + ], + "codedeploy": [ + "batch_get_deployment_targets", + "get_deployment_target", + "list_applications", + "list_deployment_configs", + "list_deployment_targets", + "list_deployments", + "list_git_hub_account_token_names", + "list_on_premises_instances" + ], + "codepipeline": [ + "list_action_types", + "list_pipelines", + "list_webhooks" + ], + "codestar": [ + "list_projects", + "list_user_profiles" + ], + "cognito-sync": [ + "list_identity_pool_usage" + ], + "comprehend": [ + "list_document_classification_jobs", + "list_document_classifiers", + "list_dominant_language_detection_jobs", + "list_entities_detection_jobs", + "list_entity_recognizers", + "list_key_phrases_detection_jobs", + "list_sentiment_detection_jobs", + "list_topics_detection_jobs" + ], + "config": [ + "describe_aggregation_authorizations", + "describe_compliance_by_config_rule", + "describe_compliance_by_resource", + "describe_config_rule_evaluation_status", + "describe_config_rules", + "describe_configuration_aggregators", + "describe_configuration_recorder_status", + "describe_configuration_recorders", + "describe_delivery_channel_status", + "describe_delivery_channels", + "describe_pending_aggregation_requests", + "describe_retention_configurations", + "get_compliance_summary_by_config_rule", + "get_compliance_summary_by_resource_type", + "get_discovered_resource_counts" + ], + "cur": [ + "describe_report_definitions" + ], + "data.mediastore": [ + "list_items" + ], + "datapipeline": [ + "list_pipelines" + ], + "datasync": [ + "list_agents", + "list_locations", + "list_task_executions", + "list_tasks" + ], + "dax": [ + "describe_clusters", + "describe_default_parameters", + "describe_parameter_groups", + "describe_subnet_groups" + ], + "devicefarm": [ + "get_account_settings", + "get_offering_status", + "list_device_instances", + "list_devices", + "list_instance_profiles", + "list_offering_promotions", + "list_offering_transactions", + "list_offerings", + "list_projects", + "list_vpce_configurations" + ], + "devices.iot1click": [ + "list_devices" + ], + "directconnect": [ + "describe_connections", + "describe_direct_connect_gateway_associations", + "describe_direct_connect_gateway_attachments", + "describe_direct_connect_gateways", + "describe_interconnects", + "describe_lags", + "describe_locations", + "describe_virtual_gateways", + "describe_virtual_interfaces" + ], + "discovery": [ + "describe_agents", + "describe_continuous_exports", + "describe_export_configurations", + "describe_export_tasks", + "describe_import_tasks", + "get_discovery_summary" + ], + "dlm": [ + "get_lifecycle_policies" + ], + "dms": [ + "describe_account_attributes", + "describe_certificates", + "describe_connections", + "describe_endpoint_types", + "describe_endpoints", + "describe_event_categories", + "describe_event_subscriptions", + "describe_orderable_replication_instances", + "describe_replication_instances", + "describe_replication_subnet_groups", + "describe_replication_task_assessment_results", + "describe_replication_tasks" + ], + "ds": [ + "describe_directories", + "describe_event_topics", + "describe_snapshots", + "describe_trusts", + "get_directory_limits", + "list_log_subscriptions" + ], + "dynamodb": [ + "describe_endpoints", + "describe_limits", + "list_backups", + "list_global_tables", + "list_tables", + "list_tables" + ], + "ec2": [ + "describe_account_attributes", + "describe_addresses", + "describe_aggregate_id_format", + "describe_availability_zones", + "describe_bundle_tasks", + "describe_capacity_reservations", + "describe_classic_link_instances", + "describe_client_vpn_endpoints", + "describe_conversion_tasks", + "describe_customer_gateways", + "describe_dhcp_options", + "describe_egress_only_internet_gateways", + "describe_elastic_gpus", + "describe_export_tasks", + "describe_fleets", + "describe_flow_logs", + "describe_fpga_images", + "describe_host_reservation_offerings", + "describe_host_reservations", + "describe_hosts", + "describe_iam_instance_profile_associations", + "describe_id_format", + "describe_images", + "describe_import_image_tasks", + "describe_import_snapshot_tasks", + "describe_instance_credit_specifications", + "describe_instance_status", + "describe_instances", + "describe_internet_gateways", + "describe_key_pairs", + "describe_launch_template_versions", + "describe_launch_templates", + "describe_moving_addresses", + "describe_nat_gateways", + "describe_network_acls", + "describe_network_interface_permissions", + "describe_network_interfaces", + "describe_placement_groups", + "describe_prefix_lists", + "describe_principal_id_format", + "describe_public_ipv4_pools", + "describe_regions", + "describe_reserved_instances", + "describe_reserved_instances_listings", + "describe_reserved_instances_modifications", + "describe_reserved_instances_offerings", + "describe_route_tables", + "describe_scheduled_instances", + "describe_security_groups", + "describe_snapshots", + "describe_spot_datafeed_subscription", + "describe_spot_fleet_requests", + "describe_spot_instance_requests", + "describe_spot_price_history", + "describe_subnets", + "describe_tags", + "describe_transit_gateway_attachments", + "describe_transit_gateway_route_tables", + "describe_transit_gateway_vpc_attachments", + "describe_transit_gateways", + "describe_volume_status", + "describe_volumes", + "describe_volumes_modifications", + "describe_vpc_classic_link", + "describe_vpc_classic_link_dns_support", + "describe_vpc_endpoint_connection_notifications", + "describe_vpc_endpoint_connections", + "describe_vpc_endpoint_service_configurations", + "describe_vpc_endpoint_services", + "describe_vpc_endpoints", + "describe_vpc_peering_connections", + "describe_vpcs", + "describe_vpn_connections", + "describe_vpn_gateways" + ], + "ecr": [ + "describe_repositories", + "get_authorization_token" + ], + "ecs": [ + "describe_clusters", + "list_account_settings", + "list_clusters", + "list_container_instances", + "list_services", + "list_task_definition_families", + "list_task_definitions", + "list_tasks" + ], + "eks": [ + "list_clusters" + ], + "elasticache": [ + "describe_cache_clusters", + "describe_cache_engine_versions", + "describe_cache_parameter_groups", + "describe_cache_security_groups", + "describe_cache_subnet_groups", + "describe_replication_groups", + "describe_reserved_cache_nodes", + "describe_reserved_cache_nodes_offerings", + "describe_snapshots", + "list_allowed_node_type_modifications" + ], + "elasticbeanstalk": [ + "describe_account_attributes", + "describe_configuration_options", + "describe_environment_health", + "describe_environment_managed_action_history", + "describe_environment_managed_actions", + "describe_environment_resources", + "describe_instances_health", + "describe_platform_version" + ], + "elasticfilesystem": [ + "describe_file_systems", + "describe_mount_targets" + ], + "elasticloadbalancing": [ + "describe_account_limits", + "describe_load_balancer_policies", + "describe_load_balancer_policy_types", + "describe_load_balancers", + "describe_account_limits", + "describe_listeners", + "describe_load_balancers", + "describe_rules", + "describe_ssl_policies", + "describe_target_groups" + ], + "elasticmapreduce": [ + "describe_job_flows", + "list_clusters", + "list_security_configurations" + ], + "elastictranscoder": [ + "list_pipelines", + "list_presets" + ], + "email": [ + "get_account", + "get_dedicated_ips", + "get_deliverability_dashboard_options", + "list_configuration_sets", + "list_dedicated_ip_pools", + "list_deliverability_test_reports", + "list_email_identities", + "describe_active_receipt_rule_set", + "get_account_sending_enabled", + "get_send_quota", + "get_send_statistics", + "list_configuration_sets", + "list_custom_verification_email_templates", + "list_identities", + "list_receipt_filters", + "list_receipt_rule_sets", + "list_templates", + "list_verified_email_addresses" + ], + "es": [ + "describe_reserved_elasticsearch_instance_offerings", + "describe_reserved_elasticsearch_instances", + "get_compatible_elasticsearch_versions", + "list_domain_names", + "list_elasticsearch_versions" + ], + "events": [ + "describe_event_bus", + "list_rules" + ], + "firehose": [ + "list_delivery_streams" + ], + "fms": [ + "get_admin_account", + "get_notification_channel", + "list_member_accounts", + "list_policies" + ], + "fsx": [ + "describe_backups", + "describe_file_systems" + ], + "gamelift": [ + "describe_ec2_instance_limits", + "describe_fleet_attributes", + "describe_fleet_capacity", + "describe_fleet_utilization", + "describe_game_session_details", + "describe_game_session_queues", + "describe_game_sessions", + "describe_matchmaking_configurations", + "describe_matchmaking_rule_sets", + "describe_player_sessions", + "describe_vpc_peering_authorizations", + "describe_vpc_peering_connections", + "list_aliases", + "list_builds", + "list_fleets" + ], + "globalaccelerator": [ + "describe_accelerator_attributes", + "list_accelerators" + ], + "glue": [ + "get_catalog_import_status", + "get_classifiers", + "get_connections", + "get_crawler_metrics", + "get_crawlers", + "get_data_catalog_encryption_settings", + "get_databases", + "get_dataflow_graph", + "get_dev_endpoints", + "get_jobs", + "get_resource_policy", + "get_security_configurations", + "get_triggers", + "list_crawlers", + "list_dev_endpoints", + "list_jobs", + "list_triggers" + ], + "greengrass": [ + "get_service_role_for_account", + "list_bulk_deployments", + "list_connector_definitions", + "list_core_definitions", + "list_device_definitions", + "list_function_definitions", + "list_groups", + "list_logger_definitions", + "list_resource_definitions", + "list_subscription_definitions" + ], + "guardduty": [ + "get_invitations_count", + "list_detectors", + "list_invitations" + ], + "health": [ + "describe_entity_aggregates", + "describe_event_types" + ], + "iam": [ + "get_account_authorization_details", + "get_account_password_policy", + "get_account_summary", + "get_credential_report", + "get_user", + "list_access_keys", + "list_account_aliases", + "list_groups", + "list_instance_profiles", + "list_mfa_devices", + "list_open_id_connect_providers", + "list_policies", + "list_roles", + "list_saml_providers", + "list_server_certificates", + "list_service_specific_credentials", + "list_signing_certificates", + "list_ssh_public_keys", + "list_users", + "list_virtual_mfa_devices" + ], + "importexport": [ + "list_jobs" + ], + "inspector": [ + "describe_cross_account_access_role", + "list_assessment_runs", + "list_assessment_targets", + "list_assessment_templates", + "list_event_subscriptions", + "list_findings", + "list_rules_packages" + ], + "iot": [ + "describe_account_audit_configuration", + "describe_default_authorizer", + "describe_endpoint", + "describe_event_configurations", + "get_effective_policies", + "get_indexing_configuration", + "get_logging_options", + "get_registration_code", + "get_v2_logging_options", + "list_active_violations", + "list_audit_findings", + "list_authorizers", + "list_billing_groups", + "list_ca_certificates", + "list_certificates", + "list_indices", + "list_jobs", + "list_ota_updates", + "list_outgoing_certificates", + "list_policies", + "list_role_aliases", + "list_scheduled_audits", + "list_security_profiles", + "list_streams", + "list_thing_groups", + "list_thing_registration_tasks", + "list_thing_types", + "list_things", + "list_topic_rules", + "list_v2_logging_levels" + ], + "iotanalytics": [ + "describe_logging_options", + "list_channels", + "list_datasets", + "list_datastores", + "list_pipelines" + ], + "kafka": [ + "list_clusters" + ], + "kinesis": [ + "describe_limits", + "describe_stream_consumer", + "list_shards", + "list_streams" + ], + "kinesisanalytics": [ + "list_applications", + "list_applications" + ], + "kinesisvideo": [ + "describe_stream", + "list_streams", + "list_tags_for_stream" + ], + "kms": [ + "describe_custom_key_stores", + "list_aliases", + "list_keys" + ], + "lambda": [ + "list_functions", + "get_account_settings", + "list_event_source_mappings", + "list_functions", + "list_layers" + ], + "license-manager": [ + "get_service_settings", + "list_license_configurations" + ], + "lightsail": [ + "get_active_names", + "get_blueprints", + "get_bundles", + "get_cloud_formation_stack_records", + "get_disk_snapshots", + "get_disks", + "get_domains", + "get_export_snapshot_records", + "get_instance_snapshots", + "get_instances", + "get_key_pairs", + "get_load_balancers", + "get_operations", + "get_regions", + "get_relational_database_blueprints", + "get_relational_database_bundles", + "get_relational_database_snapshots", + "get_relational_databases", + "get_static_ips" + ], + "logs": [ + "describe_destinations", + "describe_export_tasks", + "describe_log_groups", + "describe_metric_filters", + "describe_queries", + "describe_resource_policies" + ], + "machinelearning": [ + "describe_batch_predictions", + "describe_data_sources", + "describe_evaluations", + "describe_ml_models" + ], + "macie": [ + "list_member_accounts", + "list_s3_resources" + ], + "mediaconnect": [ + "list_entitlements", + "list_flows" + ], + "mediaconvert": [ + "describe_endpoints", + "list_job_templates", + "list_jobs", + "list_presets", + "list_queues" + ], + "medialive": [ + "list_channels", + "list_input_security_groups", + "list_inputs", + "list_offerings", + "list_reservations" + ], + "mediapackage": [ + "list_channels", + "list_origin_endpoints" + ], + "mediastore": [ + "describe_container", + "list_containers" + ], + "mediatailor": [ + "list_playback_configurations" + ], + "mgh": [ + "list_migration_tasks", + "list_progress_update_streams" + ], + "mobile": [ + "list_bundles", + "list_projects" + ], + "models.lex": [ + "get_bots", + "get_builtin_intents", + "get_builtin_slot_types", + "get_intents", + "get_slot_types" + ], + "monitoring": [ + "describe_alarm_history", + "describe_alarms", + "list_dashboards" + ], + "mq": [ + "list_brokers", + "list_configurations" + ], + "mturk-requester": [ + "get_account_balance", + "list_bonus_payments", + "list_hi_ts", + "list_qualification_requests", + "list_reviewable_hi_ts", + "list_worker_blocks" + ], + "opsworks": [ + "describe_agent_versions", + "describe_apps", + "describe_commands", + "describe_deployments", + "describe_ecs_clusters", + "describe_elastic_ips", + "describe_elastic_load_balancers", + "describe_instances", + "describe_layers", + "describe_my_user_profile", + "describe_operating_systems", + "describe_permissions", + "describe_raid_arrays", + "describe_user_profiles", + "describe_volumes" + ], + "opworks": [ + "describe_account_attributes", + "describe_backups", + "describe_servers" + ], + "organizations": [ + "describe_organization", + "list_accounts", + "list_aws_service_access_for_organization", + "list_create_account_status", + "list_handshakes_for_account", + "list_handshakes_for_organization", + "list_roots" + ], + "pinpoint": [ + "get_apps" + ], + "polly": [ + "describe_voices", + "list_lexicons", + "list_speech_synthesis_tasks" + ], + "pricing": [ + "describe_services" + ], + "projects.iot1click": [ + "list_projects" + ], + "ram": [ + "get_resource_share_invitations" + ], + "rds": [ + "describe_db_engine_versions", + "describe_db_instances", + "describe_db_parameter_groups", + "describe_db_security_groups", + "describe_db_snapshots", + "describe_db_subnet_groups", + "describe_event_categories", + "describe_event_subscriptions", + "describe_option_groups", + "describe_reserved_db_instances", + "describe_reserved_db_instances_offerings", + "describe_db_engine_versions", + "describe_db_instances", + "describe_db_parameter_groups", + "describe_db_security_groups", + "describe_db_snapshots", + "describe_db_subnet_groups", + "describe_event_categories", + "describe_event_subscriptions", + "describe_option_groups", + "describe_reserved_db_instances", + "describe_reserved_db_instances_offerings", + "describe_account_attributes", + "describe_certificates", + "describe_db_cluster_endpoints", + "describe_db_cluster_parameter_groups", + "describe_db_cluster_snapshots", + "describe_db_clusters", + "describe_db_engine_versions", + "describe_db_instance_automated_backups", + "describe_db_instances", + "describe_db_parameter_groups", + "describe_db_security_groups", + "describe_db_snapshots", + "describe_db_subnet_groups", + "describe_event_categories", + "describe_event_subscriptions", + "describe_global_clusters", + "describe_option_groups", + "describe_pending_maintenance_actions", + "describe_reserved_db_instances", + "describe_reserved_db_instances_offerings", + "describe_source_regions", + "describe_db_cluster_parameter_groups", + "describe_db_cluster_snapshots", + "describe_db_clusters", + "describe_db_engine_versions", + "describe_db_instances", + "describe_db_parameter_groups", + "describe_db_subnet_groups", + "describe_event_categories", + "describe_event_subscriptions", + "describe_pending_maintenance_actions", + "describe_db_engine_versions", + "describe_db_instances", + "describe_db_parameter_groups", + "describe_db_security_groups", + "describe_db_snapshots", + "describe_db_subnet_groups", + "describe_event_categories", + "describe_event_subscriptions", + "describe_option_groups", + "describe_reserved_db_instances", + "describe_reserved_db_instances_offerings", + "describe_db_engine_versions", + "describe_db_instances", + "describe_db_parameter_groups", + "describe_db_security_groups", + "describe_db_snapshots", + "describe_db_subnet_groups", + "describe_event_categories", + "describe_event_subscriptions", + "describe_option_groups", + "describe_reserved_db_instances", + "describe_reserved_db_instances_offerings", + "describe_db_cluster_parameter_groups", + "describe_db_cluster_snapshots", + "describe_db_clusters", + "describe_db_engine_versions", + "describe_db_instances", + "describe_db_subnet_groups", + "describe_event_categories", + "describe_pending_maintenance_actions" + ], + "redshift": [ + "describe_account_attributes", + "describe_cluster_db_revisions", + "describe_cluster_parameter_groups", + "describe_cluster_security_groups", + "describe_cluster_subnet_groups", + "describe_cluster_tracks", + "describe_cluster_versions", + "describe_clusters", + "describe_event_categories", + "describe_event_subscriptions", + "describe_hsm_client_certificates", + "describe_hsm_configurations", + "describe_orderable_cluster_options", + "describe_reserved_node_offerings", + "describe_reserved_nodes", + "describe_snapshot_copy_grants", + "describe_snapshot_schedules", + "describe_storage", + "describe_table_restore_status", + "describe_tags" + ], + "rekognition": [ + "list_collections", + "list_stream_processors" + ], + "robomaker": [ + "list_deployment_jobs", + "list_fleets", + "list_robot_applications", + "list_robots", + "list_simulation_applications", + "list_simulation_jobs" + ], + "route53": [ + "get_health_check_count", + "get_hosted_zone_count", + "get_traffic_policy_instance_count", + "list_health_checks", + "list_hosted_zones", + "list_hosted_zones_by_name", + "list_query_logging_configs", + "list_reusable_delegation_sets", + "list_traffic_policies", + "list_traffic_policy_instances" + ], + "route53domains": [ + "get_contact_reachability_status", + "list_domains", + "list_operations" + ], + "route53resolver": [ + "list_resolver_endpoints", + "list_resolver_rule_associations", + "list_resolver_rules" + ], + "s3": [ + "list_buckets" + ], + "sagemaker": [ + "list_algorithms", + "list_code_repositories", + "list_compilation_jobs", + "list_endpoint_configs", + "list_endpoints", + "list_hyper_parameter_tuning_jobs", + "list_labeling_jobs", + "list_model_packages", + "list_models", + "list_notebook_instance_lifecycle_configs", + "list_notebook_instances", + "list_subscribed_workteams", + "list_training_jobs", + "list_transform_jobs", + "list_workteams" + ], + "sdb": [ + "list_domains" + ], + "secretsmanager": [ + "get_random_password", + "list_secrets" + ], + "securityhub": [ + "get_enabled_standards", + "get_findings", + "get_insights", + "get_invitations_count", + "get_master_account", + "list_enabled_products_for_import", + "list_invitations", + "list_members" + ], + "serverlessrepo": [ + "list_applications" + ], + "servicecatalog": [ + "get_aws_organizations_access_status", + "list_accepted_portfolio_shares", + "list_portfolios", + "list_provisioned_product_plans", + "list_record_history", + "list_service_actions", + "list_tag_options" + ], + "shield": [ + "describe_drt_access", + "describe_emergency_contact_settings", + "describe_protection", + "describe_subscription", + "get_subscription_state", + "list_attacks", + "list_protections" + ], + "signer": [ + "list_signing_jobs", + "list_signing_platforms", + "list_signing_profiles" + ], + "sms": [ + "get_app", + "get_app_launch_configuration", + "get_app_replication_configuration", + "get_connectors", + "get_replication_jobs", + "get_servers", + "list_apps" + ], + "sms-voice.pinpoint": [ + "list_configuration_sets" + ], + "snowball": [ + "describe_addresses", + "get_snowball_usage", + "list_clusters", + "list_compatible_images", + "list_jobs" + ], + "sns": [ + "get_sms_attributes", + "list_phone_numbers_opted_out", + "list_platform_applications", + "list_subscriptions", + "list_topics" + ], + "sqs": [ + "list_queues" + ], + "ssm": [ + "describe_activations", + "describe_association", + "describe_available_patches", + "describe_inventory_deletions", + "describe_maintenance_window_schedule", + "describe_maintenance_windows", + "describe_patch_baselines", + "describe_patch_groups", + "get_default_patch_baseline", + "get_inventory_schema", + "list_command_invocations", + "list_commands", + "list_compliance_items", + "list_compliance_summaries", + "list_resource_compliance_summaries", + "list_resource_data_sync" + ], + "states": [ + "list_activities", + "list_state_machines" + ], + "storagegateway": [ + "describe_tape_archives", + "list_file_shares", + "list_gateways", + "list_tapes", + "list_volumes" + ], + "streams.dynamodb": [ + "list_streams" + ], + "sts": [ + "get_caller_identity", + "get_session_token" + ], + "support": [ + "describe_cases", + "describe_services", + "describe_severity_levels" + ], + "tagging": [ + "get_resources", + "get_tag_keys" + ], + "transcribe": [ + "list_transcription_jobs", + "list_vocabularies" + ], + "transfer": [ + "list_servers" + ], + "translate": [ + "list_terminologies" + ], + "waf": [ + "get_change_token", + "list_activated_rules_in_rule_group", + "list_byte_match_sets", + "list_geo_match_sets", + "list_ip_sets", + "list_logging_configurations", + "list_rate_based_rules", + "list_regex_match_sets", + "list_regex_pattern_sets", + "list_rule_groups", + "list_rules", + "list_size_constraint_sets", + "list_sql_injection_match_sets", + "list_subscribed_rule_groups", + "list_xss_match_sets" + ], + "waf-regional": [ + "get_change_token", + "list_activated_rules_in_rule_group", + "list_byte_match_sets", + "list_geo_match_sets", + "list_ip_sets", + "list_logging_configurations", + "list_rate_based_rules", + "list_regex_match_sets", + "list_regex_pattern_sets", + "list_rule_groups", + "list_rules", + "list_size_constraint_sets", + "list_sql_injection_match_sets", + "list_subscribed_rule_groups", + "list_xss_match_sets" + ], + "workdocs": [ + "describe_activities", + "describe_users", + "get_resources" + ], + "worklink": [ + "list_fleets" + ], + "workmail": [ + "list_organizations" + ], + "workspaces": [ + "describe_account", + "describe_account_modifications", + "describe_ip_groups", + "describe_workspace_bundles", + "describe_workspace_directories", + "describe_workspace_images", + "describe_workspaces", + "describe_workspaces_connection_status" + ], + "xray": [ + "get_encryption_config", + "get_group", + "get_groups", + "get_sampling_rules", + "get_sampling_statistic_summaries" + ] +} \ No newline at end of file diff --git a/pacu/core/enumerate_iam/generate_bruteforce_tests.py b/pacu/core/enumerate_iam/generate_bruteforce_tests.py new file mode 100644 index 00000000..c4cef31f --- /dev/null +++ b/pacu/core/enumerate_iam/generate_bruteforce_tests.py @@ -0,0 +1,135 @@ +import re +import os +import json + +OUTPUT_FMT = 'BRUTEFORCE_TESTS = %s' +OUTPUT_FILE = 'bruteforce_tests.py' + +API_DEFINITIONS = 'aws-sdk-js/apis/' + +OPERATION_CONTAINS = { + 'list_', + 'describe_', + 'get_', +} + +BLACKLIST_OPERATIONS = { + 'get_apis', + 'get_bucket_notification', + 'get_bucket_notification_configuration', + 'list_web_ac_ls', + 'get_hls_streaming_session_url', + 'describe_scaling_plans', + 'list_certificate_authorities', + 'list_event_sources', + 'get_geo_location', + 'get_checker_ip_ranges', + 'list_geo_locations', + 'list_public_keys', + + # https://twitter.com/AndresRiancho/status/1106680434442809350 + 'describe_stacks', + 'describe_service_errors', + 'describe_application_versions', + 'describe_applications', + 'describe_environments', + 'describe_events', + 'list_available_solution_stacks', + 'list_platform_versions', +} + + +def extract_service_name(filename, api_json): + try: + endpoint = api_json['metadata']['endpointPrefix'] + except: + return None + + endpoint = endpoint.replace('api.', '') + endpoint = endpoint.replace('opsworks-cm', 'opworks') + endpoint = endpoint.replace('acm-pca', 'acm') + + return endpoint + + +def is_dangerous(operation_name): + for safe in OPERATION_CONTAINS: + if safe in operation_name: + return False + + return True + + +def extract_operations(api_json): + operations = [] + + items = api_json['operations'].items() + + for operation_name, operation_data in items: + operation_name = to_underscore(operation_name) + + if is_dangerous(operation_name): + continue + + if operation_name in BLACKLIST_OPERATIONS: + continue + + inputs = operation_data.get('input', None) + + if inputs is None: + operations.append(operation_name) + continue + + inputs = str(inputs) + + if "required" not in inputs: + operations.append(operation_name) + continue + + operations = list(set(operations)) + operations.sort() + return operations + + +def to_underscore(name): + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + +def main(): + bruteforce_tests = dict() + + for filename in os.listdir(API_DEFINITIONS): + + if not filename.endswith('.min.json'): + continue + + api_json_data = open(os.path.join(API_DEFINITIONS, filename)).read() + + api_json = json.loads(api_json_data) + + service_name = extract_service_name(filename, api_json) + + if service_name is None: + print('%s does not define a service name' % filename) + continue + + operations = extract_operations(api_json) + + if not operations: + continue + + if service_name in bruteforce_tests: + bruteforce_tests[service_name].extend(operations) + else: + bruteforce_tests[service_name] = operations + + output = OUTPUT_FMT % json.dumps(bruteforce_tests, + indent=4, + sort_keys=True) + + open(OUTPUT_FILE, 'w').write(output) + + +if __name__ == '__main__': + main() diff --git a/pacu/core/enumerate_iam/main.py b/pacu/core/enumerate_iam/main.py new file mode 100644 index 00000000..4807268d --- /dev/null +++ b/pacu/core/enumerate_iam/main.py @@ -0,0 +1,435 @@ +""" +IAM Account Enumerator + +This code provides a mechanism to attempt to validate the permissions assigned +to a given set of AWS tokens. + +Initial code from: + + https://gist.github.com/darkarnium/1df59865f503355ef30672168063da4e + +Improvements: + * Complete refactoring + * Results returned in a programmatic way + * Threads + * Improved logging + * Increased API call coverage + * Export as a library +""" +import re +import json +import logging +import boto3 +import botocore +import random + +from botocore.client import Config +from botocore.endpoint import MAX_POOL_CONNECTIONS +from multiprocessing.dummy import Pool as ThreadPool + +from .utils.remove_metadata import remove_metadata +from .utils.json_utils import json_encoder +from .bruteforce_tests import BRUTEFORCE_TESTS + +MAX_THREADS = 25 +CLIENT_POOL = {} + + +def report_arn(candidate): + """ + Attempt to extract and slice up an ARN from the input string + """ + logger = logging.getLogger() + + arn_search = re.search(r'.*(arn:aws:.*?) .*', candidate) + + if arn_search: + arn = arn_search.group(1) + + arn_id = arn.split(':')[4] + arn_path = arn.split(':')[5] + + logger.info('-- Account ARN : %s', arn) + logger.info('-- Account Id : %s', arn.split(':')[4]) + logger.info('-- Account Path: %s', arn.split(':')[5]) + + return arn, arn_id, arn_path + + return None, None, None + + +def enumerate_using_bruteforce(access_key, secret_key, session_token, region): + """ + Attempt to brute-force common describe calls. + """ + output = dict() + + logger = logging.getLogger() + logger.info('Attempting common-service describe / list brute force.') + + pool = ThreadPool(MAX_THREADS) + args_generator = generate_args(access_key, secret_key, session_token, region) + + try: + results = pool.map(check_one_permission, args_generator) + except KeyboardInterrupt: + print('') + + results = [] + + logger.info('Ctrl+C received, stopping all threads.') + logger.info('Hit Ctrl+C again to force exit.') + + try: + pool.close() + pool.join() + except KeyboardInterrupt: + print('') + return output + + for thread_result in results: + if thread_result is None: + continue + + key, action_result = thread_result + output[key] = action_result + + pool.close() + pool.join() + + return output + + +def generate_args(access_key, secret_key, session_token, region): + + service_names = list(BRUTEFORCE_TESTS.keys()) + + random.shuffle(service_names) + + for service_name in service_names: + actions = list(BRUTEFORCE_TESTS[service_name]) + random.shuffle(actions) + + for action in actions: + yield access_key, secret_key, session_token, region, service_name, action + + +def get_client(access_key, secret_key, session_token, service_name, region): + key = '%s-%s-%s-%s-%s' % (access_key, secret_key, session_token, service_name, region) + + client = CLIENT_POOL.get(key, None) + if client is not None: + return client + + logger = logging.getLogger() + logger.debug('Getting client for %s in region %s' % (service_name, region)) + + config = Config(connect_timeout=5, + read_timeout=5, + retries={'max_attempts': 3}, + max_pool_connections=MAX_POOL_CONNECTIONS * 2) + + try: + client = boto3.client( + service_name, + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + aws_session_token=session_token, + region_name=region, + verify=False, + config=config, + ) + except: + # The service might not be available in this region + return + + CLIENT_POOL[key] = client + + return client + + +def check_one_permission(arg_tuple): + access_key, secret_key, session_token, region, service_name, operation_name = arg_tuple + logger = logging.getLogger() + + service_client = get_client(access_key, secret_key, session_token, service_name, region) + if service_client is None: + return + + try: + action_function = getattr(service_client, operation_name) + except AttributeError: + # The service might not have this action (this is most likely + # an error with generate_bruteforce_tests.py) + logger.error('Remove %s.%s action' % (service_name, operation_name)) + return + + logger.debug('Testing %s.%s() in region %s' % (service_name, operation_name, region)) + + try: + action_response = action_function() + except (botocore.exceptions.ClientError, + botocore.exceptions.EndpointConnectionError, + botocore.exceptions.ConnectTimeoutError, + botocore.exceptions.ReadTimeoutError): + return + except botocore.exceptions.ParamValidationError: + logger.error('Remove %s.%s action' % (service_name, operation_name)) + return + + msg = '-- %s.%s() worked!' + args = (service_name, operation_name) + logger.info(msg % args) + + key = '%s.%s' % (service_name, operation_name) + + return key, remove_metadata(action_response) + + +def configure_logging(): + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(process)d - [%(levelname)s] %(message)s', + ) + + # Suppress boto INFO. + logging.getLogger('boto3').setLevel(logging.WARNING) + logging.getLogger('botocore').setLevel(logging.WARNING) + logging.getLogger('nose').setLevel(logging.WARNING) + + logging.getLogger("requests").setLevel(logging.WARNING) + logging.getLogger("urllib3").setLevel(logging.WARNING) + + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + # import botocore.vendored.requests.packages.urllib3 as urllib3 + urllib3.disable_warnings(botocore.vendored.requests.packages.urllib3.exceptions.InsecureRequestWarning) + + +def enumerate_iam(access_key, secret_key, session_token, region): + """IAM Account Enumerator. + + This code provides a mechanism to attempt to validate the permissions assigned + to a given set of AWS tokens. + """ + output = dict() + configure_logging() + + output['iam'] = enumerate_using_iam(access_key, secret_key, session_token, region) + output['bruteforce'] = enumerate_using_bruteforce(access_key, secret_key, session_token, region) + + return output + + +def enumerate_using_iam(access_key, secret_key, session_token, region): + output = dict() + logger = logging.getLogger() + + # Connect to the IAM API and start testing. + logger.info('Starting permission enumeration for access-key-id "%s"', access_key) + iam_client = boto3.client( + 'iam', + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + aws_session_token=session_token + ) + + # Try for the kitchen sink. + try: + everything = iam_client.get_account_authorization_details() + except (botocore.exceptions.ClientError, + botocore.exceptions.EndpointConnectionError, + botocore.exceptions.ReadTimeoutError): + pass + else: + logger.info('Run for the hills, get_account_authorization_details worked!') + logger.info('-- %s', json.dumps(everything, indent=4, default=json_encoder)) + + output['iam.get_account_authorization_details'] = remove_metadata(everything) + + enumerate_user(iam_client, output) + enumerate_role(iam_client, output) + + return output + + +def enumerate_role(iam_client, output): + logger = logging.getLogger() + + # This is the closest thing we have to a role ARN + user_or_role_arn = output.get('arn', None) + + if user_or_role_arn is None: + # The checks which follow all required the user name to run, if we were + # unable to get that piece of information just return + return + + # Attempt to get role to start. + try: + role = iam_client.get_role(RoleName=user_or_role_arn) + except botocore.exceptions.ClientError as err: + arn, arn_id, arn_path = report_arn(str(err)) + + if arn is not None: + output['arn'] = arn + output['arn_id'] = arn_id + output['arn_path'] = arn_path + + if 'role' not in user_or_role_arn: + # We did out best, but we got nothing from iam + return + else: + role_name = user_or_role_arn + + else: + output['iam.get_role'] = remove_metadata(role) + role_name = role['Role']['RoleName'] + + # Attempt to get policies attached to this user. + try: + role_policies = iam_client.list_attached_role_policies(RoleName=role_name) + except botocore.exceptions.ClientError as err: + pass + else: + output['iam.list_attached_role_policies'] = remove_metadata(role_policies) + + logger.info( + 'Role "%s" has %0d attached policies', + role['Role']['RoleName'], + len(role_policies['AttachedPolicies']) + ) + + # List all policies, if present. + for policy in role_policies['AttachedPolicies']: + logger.info('-- Policy "%s" (%s)', policy['PolicyName'], policy['PolicyArn']) + + # Attempt to get inline policies for this user. + try: + role_policies = iam_client.list_role_policies(RoleName=role_name) + except botocore.exceptions.ClientError as err: + pass + else: + output['iam.list_role_policies'] = remove_metadata(role_policies) + + logger.info( + 'User "%s" has %0d inline policies', + role['Role']['RoleName'], + len(role_policies['PolicyNames']) + ) + + # List all policies, if present. + for policy in role_policies['PolicyNames']: + logger.info('-- Policy "%s"', policy) + + return output + + +def enumerate_user(iam_client, output): + logger = logging.getLogger() + output['root_account'] = False + + # Attempt to get user to start. + try: + user = iam_client.get_user() + except botocore.exceptions.ClientError as err: + arn, arn_id, arn_path = report_arn(str(err)) + + output['arn'] = arn + output['arn_id'] = arn_id + output['arn_path'] = arn_path + + # The checks which follow all required the user name to run, if we were + # unable to get that piece of information just return + return + else: + output['iam.get_user'] = remove_metadata(user) + + if 'UserName' not in user['User']: + if user['User']['Arn'].endswith(':root'): + # OMG + logger.warn('Found root credentials!') + output['root_account'] = True + return + else: + logger.error('Unexpected iam.get_user() response: %s' % user) + return + else: + user_name = user['User']['UserName'] + + # Attempt to get policies attached to this user. + try: + user_policies = iam_client.list_attached_user_policies(UserName=user_name) + except botocore.exceptions.ClientError as err: + pass + else: + output['iam.list_attached_user_policies'] = remove_metadata(user_policies) + + logger.info( + 'User "%s" has %0d attached policies', + user_name, + len(user_policies['AttachedPolicies']) + ) + + # List all policies, if present. + for policy in user_policies['AttachedPolicies']: + logger.info('-- Policy "%s" (%s)', policy['PolicyName'], policy['PolicyArn']) + + # Attempt to get inline policies for this user. + try: + user_policies = iam_client.list_user_policies(UserName=user_name) + except botocore.exceptions.ClientError as err: + pass + else: + output['iam.list_user_policies'] = remove_metadata(user_policies) + + logger.info( + 'User "%s" has %0d inline policies', + user_name, + len(user_policies['PolicyNames']) + ) + + # List all policies, if present. + for policy in user_policies['PolicyNames']: + logger.info('-- Policy "%s"', policy) + + # Attempt to get the groups attached to this user. + user_groups = dict() + user_groups['Groups'] = [] + + try: + user_groups = iam_client.list_groups_for_user(UserName=user_name) + except botocore.exceptions.ClientError as err: + pass + else: + output['iam.list_groups_for_user'] = remove_metadata(user_groups) + + logger.info( + 'User "%s" has %0d groups associated', + user_name, + len(user_groups['Groups']) + ) + + # Attempt to get the group policies + output['iam.list_group_policies'] = dict() + + for group in user_groups['Groups']: + try: + group_policy = iam_client.list_group_policies(GroupName=group['GroupName']) + + output['iam.list_group_policies'][group['GroupName']] = remove_metadata(group_policy) + + logger.info( + '-- Group "%s" has %0d inline policies', + group['GroupName'], + len(group_policy['PolicyNames']) + ) + + # List all group policy names. + for policy in group_policy['PolicyNames']: + logger.info('---- Policy "%s"', policy) + except botocore.exceptions.ClientError as err: + pass + + return output + diff --git a/pacu/core/enumerate_iam/utils/__init__.py b/pacu/core/enumerate_iam/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pacu/core/enumerate_iam/utils/json_utils.py b/pacu/core/enumerate_iam/utils/json_utils.py new file mode 100644 index 00000000..783a61f8 --- /dev/null +++ b/pacu/core/enumerate_iam/utils/json_utils.py @@ -0,0 +1,49 @@ +import datetime +import json +import collections + + +DEFAULT_ENCODING = 'utf-8' + + +def map_nested_dicts(ob, func): + if isinstance(ob, collections.Mapping): + return {k: map_nested_dicts(v, func) for k, v in ob.iteritems()} + else: + return func(ob) + + +def json_encoder(o): + if type(o) is datetime.date or type(o) is datetime.datetime: + return o.isoformat() + + if isinstance(o, unicode): + return o.encode('utf-8', errors='ignore') + + if isinstance(o, str): + return o.encode('utf-8', errors='ignore') + + +def smart_str(s, encoding=DEFAULT_ENCODING, errors='ignore'): + """ + Return a byte-string version of 's', encoded as specified in 'encoding'. + """ + if isinstance(s, unicode): + return s.encode(encoding, errors) + + # Already a byte-string, nothing to do here + if isinstance(s, str): + return s + + return s + + +def json_write(filename, data): + data = map_nested_dicts(data, smart_str) + + data_str = json.dumps(data, + indent=4, + sort_keys=True, + default=json_encoder) + + file(filename, 'wb').write(data_str) diff --git a/pacu/core/enumerate_iam/utils/remove_metadata.py b/pacu/core/enumerate_iam/utils/remove_metadata.py new file mode 100644 index 00000000..9542f975 --- /dev/null +++ b/pacu/core/enumerate_iam/utils/remove_metadata.py @@ -0,0 +1,5 @@ +def remove_metadata(boto_response): + if isinstance(boto_response, dict): + boto_response.pop('ResponseMetadata', None) + + return boto_response diff --git a/pacu/modules/iam__bruteforce_permissions/ReadOnlyAccessPolicy.json b/pacu/modules/iam__bruteforce_permissions/ReadOnlyAccessPolicy.json deleted file mode 100644 index d44ffb27..00000000 --- a/pacu/modules/iam__bruteforce_permissions/ReadOnlyAccessPolicy.json +++ /dev/null @@ -1,317 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Action": [ - "a4b:Get*", - "a4b:List*", - "a4b:Describe*", - "a4b:Search*", - "acm:Describe*", - "acm:Get*", - "acm:List*", - "apigateway:GET", - "application-autoscaling:Describe*", - "autoscaling-plans:Describe*", - "appstream:Describe*", - "appstream:Get*", - "appstream:List*", - "athena:List*", - "athena:Batch*", - "athena:Get*", - "autoscaling:Describe*", - "batch:List*", - "batch:Describe*", - "cloud9:Describe*", - "cloud9:List*", - "clouddirectory:List*", - "clouddirectory:BatchRead", - "clouddirectory:Get*", - "clouddirectory:LookupPolicy", - "cloudformation:Describe*", - "cloudformation:Get*", - "cloudformation:List*", - "cloudformation:Estimate*", - "cloudformation:Preview*", - "cloudfront:Get*", - "cloudfront:List*", - "cloudhsm:List*", - "cloudhsm:Describe*", - "cloudhsm:Get*", - "cloudsearch:Describe*", - "cloudsearch:List*", - "cloudtrail:Describe*", - "cloudtrail:Get*", - "cloudtrail:List*", - "cloudtrail:LookupEvents", - "cloudwatch:Describe*", - "cloudwatch:Get*", - "cloudwatch:List*", - "codebuild:BatchGet*", - "codebuild:List*", - "codecommit:BatchGet*", - "codecommit:Get*", - "codecommit:GitPull", - "codecommit:List*", - "codedeploy:BatchGet*", - "codedeploy:Get*", - "codedeploy:List*", - "codepipeline:List*", - "codepipeline:Get*", - "codestar:List*", - "codestar:Describe*", - "codestar:Get*", - "codestar:Verify*", - "cognito-identity:List*", - "cognito-identity:Describe*", - "cognito-identity:Lookup*", - "cognito-sync:List*", - "cognito-sync:Describe*", - "cognito-sync:Get*", - "cognito-sync:QueryRecords", - "cognito-idp:AdminGet*", - "cognito-idp:AdminList*", - "cognito-idp:List*", - "cognito-idp:Describe*", - "cognito-idp:Get*", - "config:Deliver*", - "config:Describe*", - "config:Get*", - "config:List*", - "connect:List*", - "connect:Describe*", - "connect:GetFederationToken", - "datapipeline:Describe*", - "datapipeline:EvaluateExpression", - "datapipeline:Get*", - "datapipeline:List*", - "datapipeline:QueryObjects", - "datapipeline:Validate*", - "dax:BatchGetItem", - "dax:DescribeClusters", - "dax:DescribeDefaultParameters", - "dax:DescribeEvents", - "dax:DescribeParameterGroups", - "dax:DescribeSubnetGroups", - "dax:GetItem", - "dax:ListTags", - "dax:Query", - "dax:Scan", - "directconnect:Describe*", - "devicefarm:List*", - "devicefarm:Get*", - "discovery:Describe*", - "discovery:List*", - "discovery:Get*", - "dms:Describe*", - "dms:List*", - "dms:Test*", - "ds:Check*", - "ds:Describe*", - "ds:Get*", - "ds:List*", - "ds:Verify*", - "dynamodb:BatchGet*", - "dynamodb:Describe*", - "dynamodb:Get*", - "dynamodb:List*", - "dynamodb:Query", - "dynamodb:Scan", - "ec2:Describe*", - "ec2:Get*", - "ec2messages:Get*", - "ecr:BatchCheck*", - "ecr:BatchGet*", - "ecr:Describe*", - "ecr:Get*", - "ecr:List*", - "ecs:Describe*", - "ecs:List*", - "elasticache:Describe*", - "elasticache:List*", - "elasticbeanstalk:Check*", - "elasticbeanstalk:Describe*", - "elasticbeanstalk:List*", - "elasticbeanstalk:Request*", - "elasticbeanstalk:Retrieve*", - "elasticbeanstalk:Validate*", - "elasticfilesystem:Describe*", - "elasticloadbalancing:Describe*", - "elasticmapreduce:Describe*", - "elasticmapreduce:List*", - "elasticmapreduce:View*", - "elastictranscoder:List*", - "elastictranscoder:Read*", - "es:Describe*", - "es:List*", - "es:ESHttpGet", - "es:ESHttpHead", - "events:Describe*", - "events:List*", - "events:Test*", - "firehose:Describe*", - "firehose:List*", - "gamelift:List*", - "gamelift:Get*", - "gamelift:Describe*", - "gamelift:RequestUploadCredentials", - "gamelift:ResolveAlias", - "gamelift:Search*", - "glacier:List*", - "glacier:Describe*", - "glacier:Get*", - "guardduty:Get*", - "guardduty:List*", - "health:Describe*", - "health:Get*", - "health:List*", - "iam:Generate*", - "iam:Get*", - "iam:List*", - "iam:Simulate*", - "importexport:Get*", - "importexport:List*", - "inspector:Describe*", - "inspector:Get*", - "inspector:List*", - "inspector:Preview*", - "inspector:LocalizeText", - "iot:Describe*", - "iot:Get*", - "iot:List*", - "iotanalytics:Describe*", - "iotanalytics:List*", - "iotanalytics:Get*", - "iotanalytics:SampleChannelData", - "kinesisanalytics:Describe*", - "kinesisanalytics:Discover*", - "kinesisanalytics:Get*", - "kinesisanalytics:List*", - "kinesisvideo:Describe*", - "kinesisvideo:Get*", - "kinesisvideo:List*", - "kinesis:Describe*", - "kinesis:Get*", - "kinesis:List*", - "kms:Describe*", - "kms:Get*", - "kms:List*", - "lambda:List*", - "lambda:Get*", - "lex:Get*", - "lightsail:Get*", - "lightsail:Is*", - "lightsail:Download*", - "logs:Describe*", - "logs:Get*", - "logs:FilterLogEvents", - "logs:ListTagsLogGroup", - "logs:TestMetricFilter", - "machinelearning:Describe*", - "machinelearning:Get*", - "mobileanalytics:Get*", - "mobilehub:Describe*", - "mobilehub:Export*", - "mobilehub:Generate*", - "mobilehub:Get*", - "mobilehub:List*", - "mobilehub:Validate*", - "mobilehub:Verify*", - "mobiletargeting:Get*", - "mq:Describe*", - "mq:List*", - "opsworks:Describe*", - "opsworks:Get*", - "opsworks-cm:Describe*", - "organizations:Describe*", - "organizations:List*", - "polly:Describe*", - "polly:Get*", - "polly:List*", - "polly:SynthesizeSpeech", - "rekognition:CompareFaces", - "rekognition:Detect*", - "rekognition:List*", - "rekognition:Search*", - "rds:Describe*", - "rds:List*", - "rds:Download*", - "redshift:Describe*", - "redshift:GetReservedNodeExchangeOfferings", - "redshift:View*", - "resource-groups:Describe*", - "resource-groups:Get*", - "resource-groups:List*", - "resource-groups:Search*", - "route53:Get*", - "route53:List*", - "route53:Test*", - "route53domains:Check*", - "route53domains:Get*", - "route53domains:List*", - "route53domains:View*", - "s3:Get*", - "s3:List*", - "s3:Head*", - "sagemaker:Describe*", - "sagemaker:List*", - "sdb:Get*", - "sdb:List*", - "sdb:Select*", - "serverlessrepo:List*", - "serverlessrepo:Get*", - "serverlessrepo:SearchApplications", - "servicecatalog:List*", - "servicecatalog:Scan*", - "servicecatalog:Search*", - "servicecatalog:Describe*", - "ses:Get*", - "ses:List*", - "ses:Describe*", - "ses:Verify*", - "shield:Describe*", - "shield:List*", - "snowball:Get*", - "snowball:Describe*", - "snowball:List*", - "sns:Get*", - "sns:List*", - "sns:Check*", - "sqs:Get*", - "sqs:List*", - "sqs:Receive*", - "ssm:Describe*", - "ssm:Get*", - "ssm:List*", - "states:List*", - "states:Describe*", - "states:GetExecutionHistory", - "storagegateway:Describe*", - "storagegateway:List*", - "sts:Get*", - "swf:Count*", - "swf:Describe*", - "swf:Get*", - "swf:List*", - "tag:Get*", - "trustedadvisor:Describe*", - "waf:Get*", - "waf:List*", - "waf-regional:List*", - "waf-regional:Get*", - "workdocs:Describe*", - "workdocs:Get*", - "workdocs:CheckAlias", - "workmail:Describe*", - "workmail:Get*", - "workmail:List*", - "workmail:Search*", - "workspaces:Describe*", - "xray:BatchGet*", - "xray:Get*" - ], - "Effect": "Allow", - "Resource": "*" - } - ] -} \ No newline at end of file diff --git a/pacu/modules/iam__bruteforce_permissions/main.py b/pacu/modules/iam__bruteforce_permissions/main.py index 6be3d325..41054b7f 100644 --- a/pacu/modules/iam__bruteforce_permissions/main.py +++ b/pacu/modules/iam__bruteforce_permissions/main.py @@ -1,278 +1,22 @@ #!/usr/bin/env python3 import argparse -from datetime import datetime -import json import os -import re - -import boto3 - -from . import param_generator +import sys +from copy import deepcopy +from pacu.core.enumerate_iam.main import enumerate_iam module_info = { 'name': 'iam__bruteforce_permissions', - 'author': 'Alexander Morgenstern at RhinoSecurityLabs', + 'author': 'Rhino Security Labs', 'category': 'ENUM', 'one_liner': 'Enumerates permissions using brute force', - 'description': "This module will automatically run through all possible API calls of supported services in order to enumerate permissions without the use of the IAM API.", + 'description': "This module will automatically run through all possible API calls of supported services in order to enumerate permissions. This uses the 'enumerate-iam' library by Andres Riancho.", 'services': ['all'], 'prerequisite_modules': [], 'external_dependencies': [], - 'arguments_to_autocomplete': ['--services'], } - parser = argparse.ArgumentParser(add_help=False, description=module_info['description']) -parser.add_argument( - '--services', - required=False, - default=None, - help='A comma separated list of services to brute force permissions' -) - -SUPPORTED_SERVICES = [ - 'ec2', - 's3', - 'logs' -] - -client = None -current_region = None -current_service = None - -summary_data = { - 'unsupported': [], - 'unknown': [], - 'services': [], - 'allow': [], - 'deny': [], -} - - -def complete_service_list(): - """Returns a list of all supported boto3 services""" - session = boto3.session.Session() - return session.get_available_services() - - -def missing_param(param): - """Sets param to 'dummydata'""" - # Don't use an underscore here (or change this in general) since it can result in different and possibly - # incorrect error codes - out = {param: 'dummydata'} - return out - - -def invalid_param(valid_type): - """Returns an object matching the requested valid type.""" - print('Checking for invalid types') - types = { - 'datetime.datetime': datetime(2015, 1, 1), - 'list': ['test'], - 'int': 1, - 'dict': {}, - 'bool': True - } - return types[valid_type] - - -def error_delegator(error): - """Processes the complete error message. Trims the error response to not overwrite missing data with a valid type error""" - kwargs = {} - # Ignore first line of error message and process in reverse order. - for line in str(error).split('\n')[::-1][:-1]: - if 'Missing required parameter in input' in line: - if line[line.find('"') + 1:-1] not in kwargs.keys(): - kwargs = {**kwargs, **missing_param(line.split()[-1][1:-1])} - elif 'Missing required parameter in' in line: - # Grabs the parameter to build a dictionary of - dict_name = line.split(':')[0].split()[-1] - if '[' in dict_name: - # Need to populate missing parameters for a sub type - param = dict_name[:dict_name.find('.')] - sub_param = dict_name[dict_name.find('.') + 1:dict_name.find('[')] - missing_parameter = line[line.find('"') + 1:-1] - kwargs.update({param: {sub_param: [missing_param(missing_parameter)]}}) - else: - param = line.split(':')[1].strip()[1:-1] - if dict_name not in kwargs: - kwargs = {dict_name: {param: ''}} - else: - kwargs[dict_name].update({param: ''}) - - elif 'Invalid type for parameter' in line: - param_name = line.split()[4][:-1] - if '.' in param_name: - # This invalid type is a sub type within a parameter - dict_name = param_name.split('.')[0] - param_name = param_name.split('.')[1] - if '[' in param_name: - # The invalid parameter is a list within a dict within a dict - param_name = param_name[:param_name.find('[')] - valid_type = line.split("'")[3] - temp_dict = {param_name: [invalid_param(valid_type)]} - else: - # The invalid parameter is a basic key value - valid_type = line.split("'")[-2] - temp_dict = {param_name: invalid_param(valid_type)} - if dict_name not in kwargs: - kwargs.update({dict_name: temp_dict}) - else: - kwargs[dict_name].update(temp_dict) - else: - # Convert list of strings to list of dicts of invalid list subtype found. - if param_name[:-3] == '[0]': - kwargs[param_name] = [{'DryRun': True}] - else: - valid_type = line.split("'")[3] - kwargs[param_name] = invalid_param(valid_type) - return kwargs - - -def generate_preload_actions(): - """Certain actions require parameters that cannot be easily discerned from the - error message provided by preloading kwargs for those actions. - """ - module_dir = os.path.dirname(__file__) - path = os.path.join(module_dir, 'preload_actions.json') - with open(path) as actions_file: - data = actions_file.read() - return json.loads(data) - - -def read_only_function(service, func): - """Verifies that actions being ran are ReadOnlyAccess to minimize unexpected - changes to the AWS environment. - """ - module_dir = os.path.dirname(__file__) - path = os.path.join(module_dir, 'ReadOnlyAccessPolicy.json') - with open(path) as file: - data = json.load(file) - formatted_func = service + ':' + camel_case(func) - for action in data['Statement'][0]['Action']: - if re.match(action, formatted_func) is not None: - return True - return False - - -def valid_func(service, func): - """Returns False for service functions that don't correspond to an AWS API action""" - if func[0] == '_': - return False - BAD_FUNCTIONS = [ - # Common boto3 methods. - 'can_paginate', - 'get_waiter', - 'waiter_names', - 'get_paginator', - 'generate_presigned_url', - 'generate_presigned_post', - 'exceptions', - 'meta', - - # S3 Function to manage multipart uploads. - 'list_parts', - ] - if func in BAD_FUNCTIONS: - return False - return read_only_function(service, func) - - -def convert_special_params(func, kwargs): - """Certain actions go through additional argument parsing. If such a case exists, the dummy_data will - be filled with valid data so that the action can successfully pass validation and reach and query - correctly determine authorization. - """ - SPECIAL_PARAMS = [ - 'Bucket', - 'Attribute', - 'Key', - ] - for param in list(filter(lambda p: kwargs[p] == 'dummydata', kwargs)): - if param in SPECIAL_PARAMS: - v = param_generator.get_special_param(client, func, param) - if v is None: - return False - else: - kwargs[param] = v - return True - return False - - -def build_service_list(services=None): - """Returns a list of valid services. """ - if not services: - return SUPPORTED_SERVICES - - unsupported_services = [s for s in services if s not in SUPPORTED_SERVICES] - summary_data['unsupported'] = unsupported_services - - unknown_services = [service for service in unsupported_services if service not in complete_service_list()] - summary_data['unknown'] = unknown_services - service_list = [service for service in services if service in SUPPORTED_SERVICES] - return service_list - - -def error_permissions(error): - """There are certain Exceptions raised that indicate successful authorization. This method will return 'allowed', - 'unknown', or 'denied' based on the whether the error indicates access is allowed or not. - """ - VALID_EXCEPTIONS = [ - 'DryRunOperation', - # S3 - 'NoSuchCORSConfiguration', - 'ServerSideEncryptionConfigurationNotFoundError', - 'NoSuchConfiguration', - 'NoSuchLifecycleConfiguration', - 'ReplicationConfigurationNotFoundError', - 'NoSuchTagSet', - 'NoSuchWebsiteConfiguration', - 'NoSuchKey', - 'NoSuchBucket', - 'NoSuchBucketPolicy', - 'OwnershipControlsNotFoundError', - 'MethodNotAllowed', - '(403) when calling the HeadBucket operation', - '(404) when calling the HeadObject operation', - '(InvalidRequest) when calling the GetObjectLegalHold operation', - '(InvalidRequest) when calling the GetObjectRetention operation', - '(ObjectLockConfigurationNotFoundError) when calling the GetObjectLockConfiguration operation', - '(NoSuchPublicAccessBlockConfiguration) when calling the GetPublicAccessBlock operation', - - # EC2 - 'InvalidTargetArn.Unknown', - 'Invalid type for parameter ReservedInstanceIds', - 'Invalid type for parameter HostIdSet', - '(InvalidCertificateArn.Malformed) when calling the GetAssociatedEnclaveCertificateIamRoles operation', - '(InvalidInstanceID.Malformed) when calling the GetConsoleScreenshot', - '(InvalidParameterValue) when calling the GetFlowLogsIntegrationTemplate operation', - - - # Logs - '(ResourceNotFoundException) when calling the DescribeLogStreams operation', - '(ResourceNotFoundException) when calling the DescribeSubscriptionFilters operation', - '(ResourceNotFoundException) when calling the FilterLogEvents operation', - '(ResourceNotFoundException) when calling the GetLogEvents operation', - '(InvalidParameterException) when calling the GetLogRecord operation', - '(ResourceNotFoundException) when calling the GetQueryResults operation', - '(ResourceNotFoundException) when calling the ListTagsLogGroup operation', - ] - - UNKNOWN_EXCEPTIONS = [ - # EC2 - '(InvalidAction) when calling the DescribeAddressesAttribute operation' - '(InvalidHostId.Malformed) when calling the GetHostReservationPurchasePreview operation:' - ] - - for exception in VALID_EXCEPTIONS: - if exception in str(error): - return 'allowed' - - for exception in UNKNOWN_EXCEPTIONS: - if exception in str(error): - return 'unknown' - - return 'denied' def main(args, pacu_main): @@ -280,129 +24,44 @@ def main(args, pacu_main): args = parser.parse_args(args) print = pacu_main.print - service_list = build_service_list(args.services.lower().split(',')) if args.services else build_service_list() - if not service_list: - return summary_data - summary_data['services'] = service_list - - preload_actions = generate_preload_actions() - - allow_permissions = {} - unknown_permissions = {} - deny_permissions = {} - - for service in service_list: - global current_service - current_service = service - allow_permissions[service] = [] - unknown_permissions[service] = [] - deny_permissions[service] = [] - - # Only checking against 'us-east-1'. To store more granular permissions the DB needs to be changed. - regions = ['us-east-1','us-east-2','us-west-2','us-west-1'] - for region in regions: - global current_region, client - - current_region = region - client = pacu_main.get_boto3_client(service, region) - - functions = [func for func in dir(client) if valid_func(service, func)] - index = 1 - - for func in functions: - index += 1 + aws_key = session.get_active_aws_key(pacu_main.database) - op = client.meta.service_model.operation_model(operation_name=client._PY_TO_OP_NAME[func]) + access_key = aws_key.access_key_id + secret_key = aws_key.secret_access_key + session_token = aws_key.session_token if aws_key.session_token else None + region = 'us-east-1' # You can change this to the desired region - if func in preload_actions: - kwargs = preload_actions[func] if func in preload_actions else {} - else: - kwargs = dict(((arg, 'dummydata') for arg in getattr(op.input_shape, 'required_members', []))) - - members = getattr(op.input_shape, 'members', {}) - if members.get('DryRun'): - kwargs['DryRun'] = True - - if members.get('AvailabilityZone'): - kwargs['AvailabilityZone'] = current_region - - if members.get('MaxResults'): - kwargs['MaxResults'] = 10 - - if members.get('GroupId'): - kwargs['GroupId'] = 1 - - if members.get('StartTime'): - kwargs['StartTime'] = datetime.now() - - convert_special_params(func, kwargs) - - print('Trying {} -- kwargs: {}'.format(func, kwargs)) - caller = getattr(client, func) - try: - caller(**kwargs) - allow_permissions[service].append(func) - print(' Authorization exists for: {}'.format(func)) - continue - except Exception as error: - if error_permissions(error) == 'allowed': - allow_permissions[service].append(func) - print(' Authorization exists for: {}'.format(func)) - continue - elif error_permissions(error) == 'unknown': - unknown_permissions[service].append(func) - continue - print(error) - deny_permissions[service].append(func) - - print('Allowed Permissions: \n') - print_permissions(allow_permissions) - print('Denied Permissions: \n') - print_permissions(deny_permissions) - - # Condenses the following dicts to a list that fits the standard service:action format. - if allow_permissions: - full_allow = [service + ':' + camel_case(perm) for perm in allow_permissions[service] for service in allow_permissions] - if deny_permissions: - full_deny = [service + ':' + camel_case(perm) for perm in deny_permissions[service] for service in deny_permissions] - - active_aws_key = session.get_active_aws_key(pacu_main.database) - active_aws_key.update( - pacu_main.database, - allow_permissions=full_allow, - deny_permissions=full_deny + # Call the enumerate_iam function from the enumerate-iam library + results = enumerate_iam( + access_key=access_key, + secret_key=secret_key, + session_token=session_token, + region=region ) - summary_data['allow'] = sum([len(allow_permissions[region]) for region in allow_permissions]) - summary_data['unknown'] = sum([len(allow_permissions[region]) for region in unknown_permissions]) - summary_data['deny'] = sum([len(deny_permissions[region]) for region in deny_permissions]) - - return summary_data - + # Process and print the results + print('Enumerated IAM Permissions:') + for service, actions in results.items(): + print(f'{service}:') + for action, status in actions.items(): + print(f' {action}: {status}') -def print_permissions(permission_dict): - """Helper function to print permissions.""" - for service in permission_dict: - print(' {}:'.format(service)) - for action in permission_dict[service]: - print(' {}'.format(action)) - print('') - - -def camel_case(name): - """Helper function to convert snake_case to CamelCase.""" - split_name = name.split('_') - return ''.join([name[0].upper() + name[1:] for name in split_name]) + # Write all the data to the Pacu DB for storage + iam_data = deepcopy(session.IAM) + for key, value in results.items(): + if key in iam_data: + iam_data[key].update(value) + else: + iam_data[key] = value + session.update(pacu_main.database, IAM=iam_data) + return results def summary(data, pacu_main): - out = 'Services: \n' - out += ' Supported: {}.\n'.format(data['services']) - if 'unsupported' in data: - out += ' Unsupported: {}.\n'.format(data['unsupported']) - if 'unknown' in data: - out += ' Unknown: {}.\n'.format(data['unknown']) - out += '{} allow permissions found.\n'.format(data['allow']) - out += '{} unknown permissions found.\n'.format(data['unknown']) - out += '{} deny permissions found.\n'.format(data['deny']) - return out + out = "" + + total_permissions = 0 + for service in data['bruteforce']: + total_permissions += len(data['bruteforce'][service]) + out += "Num of IAM permissions found: {} \n".format(total_permissions) + return out \ No newline at end of file diff --git a/pacu/modules/iam__bruteforce_permissions/param_generator.py b/pacu/modules/iam__bruteforce_permissions/param_generator.py deleted file mode 100644 index 1a34d792..00000000 --- a/pacu/modules/iam__bruteforce_permissions/param_generator.py +++ /dev/null @@ -1,60 +0,0 @@ -from botocore.exceptions import ClientError - -# Stores found values to minimize AWS calls -PARAM_CACHE = {} - -current_region = None - - -def get_special_param(client, func, param): - print('Getting info for func: {}, param: {}'.format(func, param)) - if param in PARAM_CACHE: - return PARAM_CACHE[param] - - if param == 'Bucket': - PARAM_CACHE[param] = get_bucket(client) - elif param == 'Attribute': - # Return 'Attribute directly because it doesn't need to reach out to AWS - return get_attribute(func) - elif param == 'Key': - PARAM_CACHE[param] = get_key(client) - return PARAM_CACHE[param] - - -def get_key(client, i=0): - try: - bucket = client.list_buckets()['Buckets'][i]['Name'] - try: - key = client.list_objects_v2( - Bucket=bucket, - MaxKeys=1 - ).get('Contents', [{}])[0].get('Key') - return key - except KeyError: - get_key(client, i+1) # If this bucket is empty try the next one - except ClientError as error: - if error.response['Error']['Code'] == 'AccessDeniedException': - return None - return None - - -def get_bucket(client): - try: - return client.list_buckets()['Buckets'][0]['Name'] - except ClientError as error: - if error.response['Error']['Code'] == 'AccessDeniedException': - return None - return None - - -def get_attribute(func): - FUNC_ATTRIBUTES = { - 'reset_image_attribute': 'launchPermission', - 'reset_instance_attribute': 'kernel', - 'reset_snapshot_attribute': 'createVolumePermission', - 'describe_instance_attribute': 'instanceType', - 'describe_image_attribute': 'description', - 'describe_snapshot_attribute': 'productCodes', - 'describe_vpc_attribute': 'enableDnsSupport', - } - return FUNC_ATTRIBUTES.get(func, None) diff --git a/pacu/modules/iam__bruteforce_permissions/preload_actions.json b/pacu/modules/iam__bruteforce_permissions/preload_actions.json deleted file mode 100644 index 326b52eb..00000000 --- a/pacu/modules/iam__bruteforce_permissions/preload_actions.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "associate_address": { - "InstanceId": "dummydata", - "AllocationId": "dummydata" - }, - "create_launch_template_version": { - "LaunchTemplateId": "test" - }, - "create_network_interface_permission": { - "AwsAccountId": "test" - }, - "create_volume": { - "SnapshotId": "test" - }, - "delete_launch_template": { - "LaunchTemplateId": "test" - }, - "delete_launch_template_versions": { - "LaunchTemplateId": "test" - }, - "disassociate_address": { - "AssociationId": "dummydata" - }, - "modify_launch_template": { - "LaunchTemplateId": "test" - }, - "modify_image_attribute": { - "Description": { - "Value": "string" - } - }, - "modify_snapshot_attribute": { - "CreateVolumePermission": { - "Add": [ - { - "Group": "all", - "UserId": "string" - } - ] - } - }, - "modify_subnet_attribute": { - "AssignIpv6AddressOnCreation": { - "Value": true - } - }, - "release_address": { - "AllocationId": "test" - }, - "describe_security_group_references": { - "GroupId": ["asdf"] - }, - "describe_scheduled_instance_availability": { - "FirstSlotStartTimeRange": { - "EarliestTime": "1", - "LatestTime": "2" - }, - "Recurrence": { - "Interval": 1 - } - }, - "get_reserved_instances_exchange_quote": { - "ReservedInstanceIds": ["dummydata"] - }, - "get_flow_logs_integration_template": { - "FlowLogId": "fl-11223344556677889", - "ConfigDeliveryS3DestinationArn": "asdf", - "IntegrateServices": { - "AthenaIntegrations": [ - { - "IntegrationResultS3DestinationArn": "asdf", - "PartitionLoadFrequency": "asdf" - } - ] - } - }, - "test_metric_filter": { - "filterPattern": "dummydata", - "logEventMessages": ["dummydata"] - } -} \ No newline at end of file From bbb015d33b154b8beb4c3bf4fa4bc80b8c7090ee Mon Sep 17 00:00:00 2001 From: TeneBrae93 Date: Wed, 5 Jun 2024 15:41:31 -0500 Subject: [PATCH 2/8] Correctly adding permissions to the user's session so they can be queried with 'whoami' --- .../iam__bruteforce_permissions/main.py | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/pacu/modules/iam__bruteforce_permissions/main.py b/pacu/modules/iam__bruteforce_permissions/main.py index 41054b7f..033ab759 100644 --- a/pacu/modules/iam__bruteforce_permissions/main.py +++ b/pacu/modules/iam__bruteforce_permissions/main.py @@ -18,7 +18,6 @@ parser = argparse.ArgumentParser(add_help=False, description=module_info['description']) - def main(args, pacu_main): session = pacu_main.get_active_session() args = parser.parse_args(args) @@ -40,28 +39,48 @@ def main(args, pacu_main): ) # Process and print the results + allow_permissions = [] + deny_permissions = [] + print('Enumerated IAM Permissions:') for service, actions in results.items(): print(f'{service}:') - for action, status in actions.items(): - print(f' {action}: {status}') + for action, result in actions.items(): + print(f' {action}: {result}') + if result: # If result is not empty or False, consider it allowed + allow_permissions.append(f'{service}:{action}') + else: + deny_permissions.append(f'{service}:{action}') + + # Update the active AWS key with the new permissions + active_aws_key = session.get_active_aws_key(pacu_main.database) + active_aws_key.update( + pacu_main.database, + allow_permissions=allow_permissions, + deny_permissions=deny_permissions + ) # Write all the data to the Pacu DB for storage iam_data = deepcopy(session.IAM) - for key, value in results.items(): - if key in iam_data: - iam_data[key].update(value) - else: - iam_data[key] = value + if 'permissions' not in iam_data: + iam_data['permissions'] = {} + + iam_data['permissions']['allow'] = allow_permissions + iam_data['permissions']['deny'] = deny_permissions + session.update(pacu_main.database, IAM=iam_data) - return results + # Prepare the summary data + summary_data = { + 'allow': allow_permissions, + 'deny': deny_permissions, + } + + return summary_data def summary(data, pacu_main): out = "" - total_permissions = 0 - for service in data['bruteforce']: - total_permissions += len(data['bruteforce'][service]) + total_permissions = len(data['allow']) out += "Num of IAM permissions found: {} \n".format(total_permissions) - return out \ No newline at end of file + return out From b09702854243238f4ae7fe3d8d9356810bdf1fb6 Mon Sep 17 00:00:00 2001 From: TeneBrae93 Date: Wed, 5 Jun 2024 16:18:40 -0500 Subject: [PATCH 3/8] Have permissions formatted properly in the Pacu session --- .../iam__bruteforce_permissions/main.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/pacu/modules/iam__bruteforce_permissions/main.py b/pacu/modules/iam__bruteforce_permissions/main.py index 033ab759..d61cd589 100644 --- a/pacu/modules/iam__bruteforce_permissions/main.py +++ b/pacu/modules/iam__bruteforce_permissions/main.py @@ -18,6 +18,25 @@ parser = argparse.ArgumentParser(add_help=False, description=module_info['description']) +# List of attributes to exclude from permissions +EXCLUDED_ATTRIBUTES = ['arn', 'arn_id', 'arn_path', 'root_account'] + +def format_permission(action): + """ + Format the permission to match AWS IAM action format. + Converts dots to colons and snake_case to camelCase for the action part. + Removes the "bruteforce:" prefix if present. + """ + if action.startswith('bruteforce:'): + action = action[len('bruteforce:'):] + parts = action.split('.') + if len(parts) > 1: + service = parts[0] + action_part = parts[1].split('_') + formatted_action = action_part[0].capitalize() + ''.join(word.capitalize() for word in action_part[1:]) + return f'{service}:{formatted_action}' + return action + def main(args, pacu_main): session = pacu_main.get_active_session() args = parser.parse_args(args) @@ -47,10 +66,15 @@ def main(args, pacu_main): print(f'{service}:') for action, result in actions.items(): print(f' {action}: {result}') + formatted_perm = format_permission(action) if result: # If result is not empty or False, consider it allowed - allow_permissions.append(f'{service}:{action}') + allow_permissions.append(formatted_perm) else: - deny_permissions.append(f'{service}:{action}') + deny_permissions.append(formatted_perm) + + # Remove non-permission attributes + allow_permissions = [perm for perm in allow_permissions if ':' in perm and perm.split(':', 1)[1] not in EXCLUDED_ATTRIBUTES] + deny_permissions = [perm for perm in deny_permissions if ':' in perm and perm.split(':', 1)[1] not in EXCLUDED_ATTRIBUTES] # Update the active AWS key with the new permissions active_aws_key = session.get_active_aws_key(pacu_main.database) From cf53546b528f57ae29baea868b7095e2ab01bde2 Mon Sep 17 00:00:00 2001 From: TeneBrae93 Date: Wed, 5 Jun 2024 16:31:47 -0500 Subject: [PATCH 4/8] Added support for all regions --- .../iam__bruteforce_permissions/main.py | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/pacu/modules/iam__bruteforce_permissions/main.py b/pacu/modules/iam__bruteforce_permissions/main.py index d61cd589..ed1105f7 100644 --- a/pacu/modules/iam__bruteforce_permissions/main.py +++ b/pacu/modules/iam__bruteforce_permissions/main.py @@ -17,6 +17,12 @@ } parser = argparse.ArgumentParser(add_help=False, description=module_info['description']) +parser.add_argument( + '--region', + required=False, + default=None, + help='The region to run the enumeration in (default: all regions)' +) # List of attributes to exclude from permissions EXCLUDED_ATTRIBUTES = ['arn', 'arn_id', 'arn_path', 'root_account'] @@ -47,30 +53,37 @@ def main(args, pacu_main): access_key = aws_key.access_key_id secret_key = aws_key.secret_access_key session_token = aws_key.session_token if aws_key.session_token else None - region = 'us-east-1' # You can change this to the desired region - - # Call the enumerate_iam function from the enumerate-iam library - results = enumerate_iam( - access_key=access_key, - secret_key=secret_key, - session_token=session_token, - region=region - ) + regions = args.region.split(',') if args.region else ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2'] # You can add more regions as needed # Process and print the results allow_permissions = [] deny_permissions = [] print('Enumerated IAM Permissions:') - for service, actions in results.items(): - print(f'{service}:') - for action, result in actions.items(): - print(f' {action}: {result}') - formatted_perm = format_permission(action) - if result: # If result is not empty or False, consider it allowed - allow_permissions.append(formatted_perm) - else: - deny_permissions.append(formatted_perm) + for region in regions: + client = pacu_main.get_boto3_client('apigateway', region) + print(f"Enumerating {region}") + + try: + results = enumerate_iam( + access_key=access_key, + secret_key=secret_key, + session_token=session_token, + region=region + ) + except Exception as e: + print(f"Failed to enumerate IAM permissions in {region}: {e}") + continue + + for service, actions in results.items(): + print(f'{service}:') + for action, result in actions.items(): + print(f' {action}: {result}') + formatted_perm = format_permission(action) + if result: # If result is not empty or False, consider it allowed + allow_permissions.append(formatted_perm) + else: + deny_permissions.append(formatted_perm) # Remove non-permission attributes allow_permissions = [perm for perm in allow_permissions if ':' in perm and perm.split(':', 1)[1] not in EXCLUDED_ATTRIBUTES] From 1b9d33d5ac7d73a5d9dac574ca6b470482acdc64 Mon Sep 17 00:00:00 2001 From: Tyler Ramsbey <86263907+TeneBrae93@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:49:10 -0500 Subject: [PATCH 5/8] Update main.py --- pacu/core/enumerate_iam/main.py | 48 +++++++++++++++------------------ 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/pacu/core/enumerate_iam/main.py b/pacu/core/enumerate_iam/main.py index 4807268d..2e275f9b 100644 --- a/pacu/core/enumerate_iam/main.py +++ b/pacu/core/enumerate_iam/main.py @@ -22,6 +22,7 @@ import boto3 import botocore import random +from typing import Dict, Tuple, Generator, Any from botocore.client import Config from botocore.endpoint import MAX_POOL_CONNECTIONS @@ -32,10 +33,10 @@ from .bruteforce_tests import BRUTEFORCE_TESTS MAX_THREADS = 25 -CLIENT_POOL = {} +CLIENT_POOL: Dict[str, boto3.client] = {} -def report_arn(candidate): +def report_arn(candidate: str) -> Tuple[str, str, str]: """ Attempt to extract and slice up an ARN from the input string """ @@ -58,7 +59,7 @@ def report_arn(candidate): return None, None, None -def enumerate_using_bruteforce(access_key, secret_key, session_token, region): +def enumerate_using_bruteforce(access_key: str, secret_key: str, session_token: str, region: str) -> Dict[str, Any]: """ Attempt to brute-force common describe calls. """ @@ -100,8 +101,7 @@ def enumerate_using_bruteforce(access_key, secret_key, session_token, region): return output -def generate_args(access_key, secret_key, session_token, region): - +def generate_args(access_key: str, secret_key: str, session_token: str, region: str) -> Generator[Tuple[str, str, str, str, str, str], None, None]: service_names = list(BRUTEFORCE_TESTS.keys()) random.shuffle(service_names) @@ -114,7 +114,7 @@ def generate_args(access_key, secret_key, session_token, region): yield access_key, secret_key, session_token, region, service_name, action -def get_client(access_key, secret_key, session_token, service_name, region): +def get_client(access_key: str, secret_key: str, session_token: str, service_name: str, region: str) -> boto3.client: key = '%s-%s-%s-%s-%s' % (access_key, secret_key, session_token, service_name, region) client = CLIENT_POOL.get(key, None) @@ -141,20 +141,20 @@ def get_client(access_key, secret_key, session_token, service_name, region): ) except: # The service might not be available in this region - return + return None CLIENT_POOL[key] = client return client -def check_one_permission(arg_tuple): +def check_one_permission(arg_tuple: Tuple[str, str, str, str, str, str]) -> Tuple[str, Any]: access_key, secret_key, session_token, region, service_name, operation_name = arg_tuple logger = logging.getLogger() service_client = get_client(access_key, secret_key, session_token, service_name, region) if service_client is None: - return + return None try: action_function = getattr(service_client, operation_name) @@ -162,7 +162,7 @@ def check_one_permission(arg_tuple): # The service might not have this action (this is most likely # an error with generate_bruteforce_tests.py) logger.error('Remove %s.%s action' % (service_name, operation_name)) - return + return None logger.debug('Testing %s.%s() in region %s' % (service_name, operation_name, region)) @@ -172,10 +172,10 @@ def check_one_permission(arg_tuple): botocore.exceptions.EndpointConnectionError, botocore.exceptions.ConnectTimeoutError, botocore.exceptions.ReadTimeoutError): - return + return None except botocore.exceptions.ParamValidationError: logger.error('Remove %s.%s action' % (service_name, operation_name)) - return + return None msg = '-- %s.%s() worked!' args = (service_name, operation_name) @@ -203,11 +203,8 @@ def configure_logging(): import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - # import botocore.vendored.requests.packages.urllib3 as urllib3 - urllib3.disable_warnings(botocore.vendored.requests.packages.urllib3.exceptions.InsecureRequestWarning) - -def enumerate_iam(access_key, secret_key, session_token, region): +def enumerate_iam(access_key: str, secret_key: str, session_token: str, region: str) -> Dict[str, Any]: """IAM Account Enumerator. This code provides a mechanism to attempt to validate the permissions assigned @@ -222,7 +219,7 @@ def enumerate_iam(access_key, secret_key, session_token, region): return output -def enumerate_using_iam(access_key, secret_key, session_token, region): +def enumerate_using_iam(access_key: str, secret_key: str, session_token: str, region: str) -> Dict[str, Any]: output = dict() logger = logging.getLogger() @@ -254,7 +251,7 @@ def enumerate_using_iam(access_key, secret_key, session_token, region): return output -def enumerate_role(iam_client, output): +def enumerate_role(iam_client: boto3.client, output: Dict[str, Any]) -> Dict[str, Any]: logger = logging.getLogger() # This is the closest thing we have to a role ARN @@ -263,7 +260,7 @@ def enumerate_role(iam_client, output): if user_or_role_arn is None: # The checks which follow all required the user name to run, if we were # unable to get that piece of information just return - return + return output # Attempt to get role to start. try: @@ -277,8 +274,8 @@ def enumerate_role(iam_client, output): output['arn_path'] = arn_path if 'role' not in user_or_role_arn: - # We did out best, but we got nothing from iam - return + # We did our best, but we got nothing from iam + return output else: role_name = user_or_role_arn @@ -325,7 +322,7 @@ def enumerate_role(iam_client, output): return output -def enumerate_user(iam_client, output): +def enumerate_user(iam_client: boto3.client, output: Dict[str, Any]) -> Dict[str, Any]: logger = logging.getLogger() output['root_account'] = False @@ -341,7 +338,7 @@ def enumerate_user(iam_client, output): # The checks which follow all required the user name to run, if we were # unable to get that piece of information just return - return + return output else: output['iam.get_user'] = remove_metadata(user) @@ -350,10 +347,10 @@ def enumerate_user(iam_client, output): # OMG logger.warn('Found root credentials!') output['root_account'] = True - return + return output else: logger.error('Unexpected iam.get_user() response: %s' % user) - return + return output else: user_name = user['User']['UserName'] @@ -432,4 +429,3 @@ def enumerate_user(iam_client, output): pass return output - From 34beac7e92e1a4eafe339079561af74d5b3ff79a Mon Sep 17 00:00:00 2001 From: Tyler Ramsbey <86263907+TeneBrae93@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:01:48 -0500 Subject: [PATCH 6/8] Update poetry.lock --- poetry.lock | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index f3f1a7a0..bc6e9694 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. [[package]] name = "ansicon" @@ -1516,6 +1516,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1869,6 +1870,17 @@ files = [ {file = "types_s3transfer-0.6.2.tar.gz", hash = "sha256:4ba9b483796fdcd026aa162ee03bdcedd2bf7d08e9387c820dcdd158b0102057"}, ] +[[package]] +name = "types-urllib3" +version = "1.26.25.14" +description = "Typing stubs for urllib3" +optional = false +python-versions = "*" +files = [ + {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, + {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, +] + [[package]] name = "typing-extensions" version = "4.7.1" @@ -1967,4 +1979,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "968b278fcb484b40e5c854fefaa1df02224e8e2406628e4d4ab786372063324b" +content-hash = "60b756b9087fb303595791b7276f7ec62172da191cc002949ca9a6df7e93e244" From 157a23ddbc14a5ab46ba3bad278506bdfceba804 Mon Sep 17 00:00:00 2001 From: Tyler Ramsbey <86263907+TeneBrae93@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:02:00 -0500 Subject: [PATCH 7/8] Update pyproject.toml --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 94f58a65..80ea3dfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ qrcode = "^7.4.2" jq = "^1.4.1" pyyaml = "^6.0.1" toml = "^0.10.2" +types-urllib3 = "^1.26.25.14" [tool.poetry.dev-dependencies] flake8 = "^3.9.1" @@ -39,6 +40,9 @@ boto3-stubs = {extras = ["iam", "s3", "lambda"], version = "^1.17.54"} moto = "^2.2.1" importlib-metadata = "4.13.0" +[tool.poetry.group.dev.dependencies] +types-urllib3 = "^1.26.25.14" + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" From a7a7d3034ee7f0954d2ff569a83d0f5437f60cc7 Mon Sep 17 00:00:00 2001 From: Tyler Ramsbey <86263907+TeneBrae93@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:11:04 -0500 Subject: [PATCH 8/8] Update main.py --- pacu/core/enumerate_iam/main.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pacu/core/enumerate_iam/main.py b/pacu/core/enumerate_iam/main.py index 2e275f9b..334e7f98 100644 --- a/pacu/core/enumerate_iam/main.py +++ b/pacu/core/enumerate_iam/main.py @@ -22,7 +22,7 @@ import boto3 import botocore import random -from typing import Dict, Tuple, Generator, Any +from typing import Dict, Tuple, Generator, Any, Optional from botocore.client import Config from botocore.endpoint import MAX_POOL_CONNECTIONS @@ -33,10 +33,10 @@ from .bruteforce_tests import BRUTEFORCE_TESTS MAX_THREADS = 25 -CLIENT_POOL: Dict[str, boto3.client] = {} +CLIENT_POOL: Dict[str, botocore.client.BaseClient] = {} -def report_arn(candidate: str) -> Tuple[str, str, str]: +def report_arn(candidate: str) -> Tuple[Optional[str], Optional[str], Optional[str]]: """ Attempt to extract and slice up an ARN from the input string """ @@ -63,7 +63,7 @@ def enumerate_using_bruteforce(access_key: str, secret_key: str, session_token: """ Attempt to brute-force common describe calls. """ - output = dict() + output: Dict[str, Any] = {} logger = logging.getLogger() logger.info('Attempting common-service describe / list brute force.') @@ -114,7 +114,7 @@ def generate_args(access_key: str, secret_key: str, session_token: str, region: yield access_key, secret_key, session_token, region, service_name, action -def get_client(access_key: str, secret_key: str, session_token: str, service_name: str, region: str) -> boto3.client: +def get_client(access_key: str, secret_key: str, session_token: str, service_name: str, region: str) -> Optional[botocore.client.BaseClient]: key = '%s-%s-%s-%s-%s' % (access_key, secret_key, session_token, service_name, region) client = CLIENT_POOL.get(key, None) @@ -148,7 +148,7 @@ def get_client(access_key: str, secret_key: str, session_token: str, service_nam return client -def check_one_permission(arg_tuple: Tuple[str, str, str, str, str, str]) -> Tuple[str, Any]: +def check_one_permission(arg_tuple: Tuple[str, str, str, str, str, str]) -> Optional[Tuple[str, Any]]: access_key, secret_key, session_token, region, service_name, operation_name = arg_tuple logger = logging.getLogger() @@ -210,7 +210,7 @@ def enumerate_iam(access_key: str, secret_key: str, session_token: str, region: This code provides a mechanism to attempt to validate the permissions assigned to a given set of AWS tokens. """ - output = dict() + output: Dict[str, Any] = {} configure_logging() output['iam'] = enumerate_using_iam(access_key, secret_key, session_token, region) @@ -220,7 +220,7 @@ def enumerate_iam(access_key: str, secret_key: str, session_token: str, region: def enumerate_using_iam(access_key: str, secret_key: str, session_token: str, region: str) -> Dict[str, Any]: - output = dict() + output: Dict[str, Any] = {} logger = logging.getLogger() # Connect to the IAM API and start testing. @@ -251,7 +251,7 @@ def enumerate_using_iam(access_key: str, secret_key: str, session_token: str, re return output -def enumerate_role(iam_client: boto3.client, output: Dict[str, Any]) -> Dict[str, Any]: +def enumerate_role(iam_client: botocore.client.BaseClient, output: Dict[str, Any]) -> Dict[str, Any]: logger = logging.getLogger() # This is the closest thing we have to a role ARN @@ -322,7 +322,7 @@ def enumerate_role(iam_client: boto3.client, output: Dict[str, Any]) -> Dict[str return output -def enumerate_user(iam_client: boto3.client, output: Dict[str, Any]) -> Dict[str, Any]: +def enumerate_user(iam_client: botocore.client.BaseClient, output: Dict[str, Any]) -> Dict[str, Any]: logger = logging.getLogger() output['root_account'] = False @@ -391,7 +391,7 @@ def enumerate_user(iam_client: boto3.client, output: Dict[str, Any]) -> Dict[str logger.info('-- Policy "%s"', policy) # Attempt to get the groups attached to this user. - user_groups = dict() + user_groups: Dict[str, Any] = {} user_groups['Groups'] = [] try: