diff --git a/aws/shared.go b/aws/shared.go index 566c2934..e47f8097 100644 --- a/aws/shared.go +++ b/aws/shared.go @@ -19,7 +19,7 @@ var yellow = color.New(color.FgRed).SprintFunc() var blue = color.New(color.FgBlue).SprintFunc() var magenta = color.New(color.FgMagenta).SprintFunc() var green = color.New(color.FgGreen).SprintFunc() -var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "me-south-1", "sa-east-1"} +var AWSRegions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-northeast-3", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ca-central-1", "eu-central-1", "eu-west-1", "eu-west-2", "eu-south-1", "eu-west-3", "eu-north-1", "sa-east-1"} var sharedLogger = internal.TxtLogger() func GetIamSimResult(SkipAdminCheck bool, roleArnPtr *string, iamSimulatorMod IamSimulatorModule, localAdminMap map[string]bool) (string, string) { @@ -54,12 +54,6 @@ func GetIamSimResult(SkipAdminCheck bool, roleArnPtr *string, iamSimulatorMod Ia } } - if isRoleAdminBool { - adminRole = "YES" - } else { - adminRole = "No" - } - } else { adminRole = "Skipped" canRolePrivEsc = "Skipped" diff --git a/cli/aws.go b/cli/aws.go index d1646708..3c07466a 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -92,8 +92,9 @@ var ( AWSUseCache bool AWSMFAToken string - Goroutines int - Verbosity int + Goroutines int + Verbosity int + AWSMaxRetries int AWSCommands = &cobra.Command{ Use: "aws", @@ -546,6 +547,9 @@ type OrgAccounts struct { func awsPreRun(cmd *cobra.Command, args []string) { gob.Register(&types.Organization{}) + // Set the max retries before any AWS config loading happens + internal.MaxRetries = AWSMaxRetries + // if multiple profiles were used, ensure the management account is first // if AWSProfilesList != "" || AWSAllProfiles { // AWSProfiles = FindOrgMgmtAccountAndReorderAccounts(AWSProfiles, cmd.Root().Version, AWSMFAToken) @@ -2553,6 +2557,7 @@ func init() { AWSCommands.PersistentFlags().BoolVarP(&AWSUseCache, "cached", "c", false, "Load cached data from disk. Faster, but if changes have been recently made you'll miss them") AWSCommands.PersistentFlags().StringVarP(&AWSTableCols, "cols", "t", "", "Comma separated list of columns to display in table output") AWSCommands.PersistentFlags().StringVar(&AWSMFAToken, "mfa-token", "", "MFA Token") + AWSCommands.PersistentFlags().IntVar(&AWSMaxRetries, "max-retries", 3, "Maximum number of AWS API retry attempts. Set to 0 for no retries (faster for black-box/limited-permission enumeration)") AWSCommands.PersistentFlags().StringVar(&PmapperDataBasePath, "pmapper-data-basepath", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)\nPoint to the parent directory that contains all of the pmapper data by account numbers. \n\tExample: /path/to/com.nccgroup.principalmapper/\n\tExample: ./pmapperdata/") AWSCommands.AddCommand( diff --git a/globals/utils.go b/globals/utils.go index 8b9d2c2f..d0020d6a 100644 --- a/globals/utils.go +++ b/globals/utils.go @@ -4,4 +4,4 @@ const CLOUDFOX_USER_AGENT = "cloudfox" const CLOUDFOX_LOG_FILE_DIR_NAME = ".cloudfox" const CLOUDFOX_BASE_DIRECTORY = "cloudfox-output" const LOOT_DIRECTORY_NAME = "loot" -const CLOUDFOX_VERSION = "2.0.5" +const CLOUDFOX_VERSION = "2.0.6" diff --git a/internal/aws.go b/internal/aws.go index c240504e..711aad95 100644 --- a/internal/aws.go +++ b/internal/aws.go @@ -7,10 +7,12 @@ import ( "encoding/json" "fmt" "io" + "net" "os" "path/filepath" "regexp" "strings" + "sync" "sync/atomic" "time" @@ -34,6 +36,23 @@ var ( UtilsFs = afero.NewOsFs() credsMap = map[string]aws.Credentials{} ConfigMap = map[string]aws.Config{} + + // MaxRetries controls the maximum number of retry attempts for AWS API calls. + // The default AWS SDK behavior is 3 attempts. Set to 0 for no retries, which + // is useful when running with limited-permission credentials to avoid slow + // timeouts on access-denied errors. + MaxRetries = 3 + + // unreachableRegions tracks regions that failed a TCP connectivity check so + // they can be skipped for the remainder of the run. Protected by a mutex + // since multiple goroutines may probe regions concurrently. + unreachableRegions = make(map[string]bool) + unreachableRegionsMu sync.RWMutex + + // RegionReachabilityTimeout controls how long the TCP probe waits before + // declaring a region unreachable. Default 3 seconds is enough for any + // healthy AWS endpoint. + RegionReachabilityTimeout = 3 * time.Second ) type CloudFoxRunData struct { @@ -132,7 +151,7 @@ func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string) if AwsMfaToken != "" { cfg, err = config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(AWSProfile), config.WithDefaultRegion("us-east-1"), config.WithRetryer( func() aws.Retryer { - return retry.AddWithMaxAttempts(retry.NewStandard(), 3) + return retry.AddWithMaxAttempts(retry.NewStandard(), MaxRetries) }), config.WithAssumeRoleCredentialOptions(func(options *stscreds.AssumeRoleOptions) { options.TokenProvider = func() (string, error) { return AwsMfaToken, nil @@ -142,7 +161,7 @@ func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string) } else { cfg, err = config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile(AWSProfile), config.WithDefaultRegion("us-east-1"), config.WithRetryer( func() aws.Retryer { - return retry.AddWithMaxAttempts(retry.NewStandard(), 3) + return retry.AddWithMaxAttempts(retry.NewStandard(), MaxRetries) }), config.WithAssumeRoleCredentialOptions(func(options *stscreds.AssumeRoleOptions) { options.TokenProvider = stscreds.StdinTokenProvider }), @@ -210,6 +229,58 @@ func AWSWhoami(awsProfile string, version string, AwsMfaToken string) (*sts.GetC return CallerIdentity, err } +// isRegionReachable does a fast TCP dial to the STS endpoint in the given region. +// STS is available in every region and is a lightweight endpoint to probe. If the +// connection succeeds, the region is reachable. Results are cached so each region +// is only probed once per run. +func isRegionReachable(region string) bool { + unreachableRegionsMu.RLock() + alreadyUnreachable := unreachableRegions[region] + unreachableRegionsMu.RUnlock() + if alreadyUnreachable { + return false + } + + endpoint := fmt.Sprintf("sts.%s.amazonaws.com:443", region) + conn, err := net.DialTimeout("tcp", endpoint, RegionReachabilityTimeout) + if err != nil { + unreachableRegionsMu.Lock() + unreachableRegions[region] = true + unreachableRegionsMu.Unlock() + TxtLog.Warnf("Region %s is unreachable (probe to %s failed: %v) - skipping for this run", region, endpoint, err) + return false + } + conn.Close() + return true +} + +// FilterReachableRegions takes a list of regions and returns only the ones that +// pass a TCP connectivity probe. Unreachable regions are probed in parallel and +// cached so subsequent calls return immediately. +func FilterReachableRegions(regions []string) []string { + // Probe all regions concurrently for speed + type probeResult struct { + region string + reachable bool + } + resultsChan := make(chan probeResult, len(regions)) + + for _, region := range regions { + go func(r string) { + resultsChan <- probeResult{region: r, reachable: isRegionReachable(r)} + }(region) + } + + var reachableRegions []string + for range regions { + result := <-resultsChan + if result.reachable { + reachableRegions = append(reachableRegions, result.region) + } + } + return reachableRegions +} + func GetEnabledRegions(awsProfile string, version string, AwsMfaToken string) []string { cacheKey := fmt.Sprintf("GetEnabledRegions-%s", awsProfile) cached, found := Cache.Get(cacheKey) @@ -234,12 +305,15 @@ func GetEnabledRegions(awsProfile string, version string, AwsMfaToken string) [] if err != nil { TxtLog.Println(err) } - return AWSRegions + reachableRegions := FilterReachableRegions(AWSRegions) + Cache.Set(cacheKey, reachableRegions, cache.DefaultExpiration) + return reachableRegions } for _, region := range regions.Regions { enabledRegions = append(enabledRegions, *region.RegionName) } + enabledRegions = FilterReachableRegions(enabledRegions) Cache.Set(cacheKey, enabledRegions, cache.DefaultExpiration) return enabledRegions