From 5c40ef45df8156baa3ffd6ef4ff849242d854604 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Thu, 9 Nov 2023 08:23:37 -0500 Subject: [PATCH 01/29] initial ideas for graph command --- aws/client-initializers.go | 23 +- aws/codebuild.go | 2 +- aws/ecs-tasks.go | 2 +- aws/eks.go | 2 +- aws/graph.go | 107 ++++++ aws/graph/ingester/ingestor.go | 304 ++++++++++++++++++ aws/graph/ingester/schema/models/account.go | 45 +++ aws/graph/ingester/schema/models/constants.go | 10 + aws/graph/ingester/schema/models/org.go | 19 ++ aws/graph/ingester/schema/models/resource.go | 5 + aws/graph/ingester/schema/models/roles.go | 11 + aws/graph/ingester/schema/schema.go | 98 ++++++ aws/instances.go | 2 +- aws/lambda.go | 2 +- aws/resource-trusts.go | 14 +- aws/role-trusts.go | 2 +- cli/aws.go | 42 ++- go.mod | 3 + go.sum | 6 + 19 files changed, 673 insertions(+), 26 deletions(-) create mode 100644 aws/graph.go create mode 100644 aws/graph/ingester/ingestor.go create mode 100644 aws/graph/ingester/schema/models/account.go create mode 100644 aws/graph/ingester/schema/models/constants.go create mode 100644 aws/graph/ingester/schema/models/org.go create mode 100644 aws/graph/ingester/schema/models/resource.go create mode 100644 aws/graph/ingester/schema/models/roles.go create mode 100644 aws/graph/ingester/schema/schema.go diff --git a/aws/client-initializers.go b/aws/client-initializers.go index 0256c86..46086e3 100644 --- a/aws/client-initializers.go +++ b/aws/client-initializers.go @@ -3,6 +3,7 @@ package aws import ( "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/codebuild" "github.com/aws/aws-sdk-go-v2/service/ecr" "github.com/aws/aws-sdk-go-v2/service/efs" @@ -17,7 +18,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sts" ) -func initIAMSimClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule { +func InitIamCommandClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule { iamSimMod := IamSimulatorModule{ IAMClient: iamSimPPClient, @@ -30,7 +31,7 @@ func initIAMSimClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCa } -func InitCloudFoxSNSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSWrapTable bool) SNSModule { +func InitSNSCommandClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSWrapTable bool) SNSModule { var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) cloudFoxSNSClient := SNSModule{ SNSClient: sns.NewFromConfig(AWSConfig), @@ -44,7 +45,7 @@ func InitCloudFoxSNSClient(caller sts.GetCallerIdentityOutput, AWSProfile string } -func initCloudFoxS3Client(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string) BucketsModule { +func InitS3CommandClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string) BucketsModule { var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) cloudFoxS3Client := BucketsModule{ S3Client: s3.NewFromConfig(AWSConfig), @@ -56,7 +57,7 @@ func initCloudFoxS3Client(caller sts.GetCallerIdentityOutput, AWSProfile string, } -func InitSQSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) SQSModule { +func InitSQSCommandClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) SQSModule { var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) sqsClient := SQSModule{ SQSClient: sqs.NewFromConfig(AWSConfig), @@ -71,7 +72,7 @@ func InitSQSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVers } -func InitLambdaClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) LambdasModule { +func InitLambdaCommandClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) LambdasModule { var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) lambdaClient := LambdasModule{ LambdaClient: lambda.NewFromConfig(AWSConfig), @@ -82,7 +83,7 @@ func InitLambdaClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfV return lambdaClient } -func InitCodeBuildClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) CodeBuildModule { +func InitCodeBuildCommandClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) CodeBuildModule { var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) codeBuildClient := CodeBuildModule{ CodeBuildClient: codebuild.NewFromConfig(AWSConfig), @@ -93,7 +94,7 @@ func InitCodeBuildClient(caller sts.GetCallerIdentityOutput, AWSProfile string, return codeBuildClient } -func InitECRClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) ECRModule { +func InitECRCommandClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) ECRModule { var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) ecrClient := ECRModule{ ECRClient: ecr.NewFromConfig(AWSConfig), @@ -104,7 +105,7 @@ func InitECRClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVers return ecrClient } -func InitFileSystemsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) FilesystemsModule { +func InitFileSystemsCommandClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) FilesystemsModule { var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) fileSystemsClient := FilesystemsModule{ EFSClient: efs.NewFromConfig(AWSConfig), @@ -116,7 +117,7 @@ func InitFileSystemsClient(caller sts.GetCallerIdentityOutput, AWSProfile string return fileSystemsClient } -func InitOrgClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) OrgModule { +func InitOrgCommandClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) OrgModule { var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) orgClient := OrgModule{ OrganizationsClient: organizations.NewFromConfig(AWSConfig), @@ -136,3 +137,7 @@ func InitGlueClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVer var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) return glue.NewFromConfig(AWSConfig) } + +func InitOrgClient(AWSConfig aws.Config) *organizations.Client { + return organizations.NewFromConfig(AWSConfig) +} diff --git a/aws/codebuild.go b/aws/codebuild.go index 5bb7697..9975745 100644 --- a/aws/codebuild.go +++ b/aws/codebuild.go @@ -65,7 +65,7 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosi fmt.Printf("[%s][%s] Enumerating CodeBuild projects for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) - m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) + m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) wg := new(sync.WaitGroup) semaphore := make(chan struct{}, m.Goroutines) diff --git a/aws/ecs-tasks.go b/aws/ecs-tasks.go index f583cf0..4e78f02 100644 --- a/aws/ecs-tasks.go +++ b/aws/ecs-tasks.go @@ -74,7 +74,7 @@ func (m *ECSTasksModule) ECSTasks(outputDirectory string, verbosity int) { // Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) - m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) + m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { // fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) diff --git a/aws/eks.go b/aws/eks.go index 61563a7..7086ae2 100644 --- a/aws/eks.go +++ b/aws/eks.go @@ -73,7 +73,7 @@ func (m *EKSModule) EKS(outputDirectory string, verbosity int) { // Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) - m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) + m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { // fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) diff --git a/aws/graph.go b/aws/graph.go new file mode 100644 index 0000000..b8d6af4 --- /dev/null +++ b/aws/graph.go @@ -0,0 +1,107 @@ +package aws + +import ( + "fmt" + + "github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models" + "github.com/BishopFox/cloudfox/aws/sdk" + "github.com/BishopFox/cloudfox/internal" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/sirupsen/logrus" +) + +type GraphCommand struct { + + // General configuration data + Caller sts.GetCallerIdentityOutput + AWSRegions []string + Goroutines int + AWSProfile string + WrapTable bool + AWSOutputType string + AWSTableCols string + Verbosity int + AWSOutputDirectory string + AWSConfig aws.Config + + // Main module data + // Used to store output data for pretty printing + output internal.OutputData2 + + modLog *logrus.Entry +} + +func init() { + // initialize org client for graph command + +} + +func (m *GraphCommand) RunGraphCommand() { + + // These struct values are used by the output module + m.output.Verbosity = m.Verbosity + m.output.Directory = m.AWSOutputDirectory + m.output.CallingModule = "graph" + m.modLog = internal.TxtLog.WithFields(logrus.Fields{ + "module": m.output.CallingModule, + }) + if m.AWSProfile == "" { + m.AWSProfile = internal.BuildAWSPath(m.Caller) + } + + m.modLog.Info("Generating graph") + + m.collectDataForGraph() + +} + +func (m *GraphCommand) collectDataForGraph() { + //OrganizationsCommandClient := InitOrgClient(m.AWSConfig) + OrganizationsCommandClient := InitOrgClient(m.AWSConfig) + DescribeOrgOutput, err := sdk.CachedOrganizationsDescribeOrganization(OrganizationsCommandClient, aws.ToString(m.Caller.Account)) + if err != nil { + m.modLog.Fatal(err) + } + if DescribeOrgOutput.MasterAccountId == nil { + m.modLog.Fatal("Organization is not configured") + } + if aws.ToString(DescribeOrgOutput.MasterAccountId) != aws.ToString(m.Caller.Account) { + m.modLog.Fatal("You must run this command from the master account") + } + ListAccounts, err := sdk.CachedOrganizationsListAccounts(OrganizationsCommandClient, aws.ToString(DescribeOrgOutput.MasterAccountId)) + if err != nil { + m.modLog.Fatal(err) + } + for _, account := range ListAccounts { + var isMgmtAccount bool + var isChildAccount bool + m.modLog.Info("Account: ", aws.ToString(account.Name)) + if aws.ToString(DescribeOrgOutput.MasterAccountId) == aws.ToString(m.Caller.Account) { + isMgmtAccount = false + } else { + isMgmtAccount = true + } + if DescribeOrgOutput.MasterAccountId == nil { + isChildAccount = false + } else { + isChildAccount = true + } + + //create new object of type models.Account + account := models.Account{ + Id: aws.ToString(account.Id), + Arn: aws.ToString(account.Arn), + Name: aws.ToString(account.Name), + Email: aws.ToString(account.Email), + Status: string(account.Status), + JoinedMethod: string(account.JoinedMethod), + JoinedTimestamp: account.JoinedTimestamp.String(), + IsOrgMgmt: isMgmtAccount, + IsChildAccount: isChildAccount, + OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId), + } + fmt.Println(account) + } + +} diff --git a/aws/graph/ingester/ingestor.go b/aws/graph/ingester/ingestor.go new file mode 100644 index 0000000..dd6574d --- /dev/null +++ b/aws/graph/ingester/ingestor.go @@ -0,0 +1,304 @@ +package ingestor + +import ( + "archive/zip" + "bufio" + "context" + "encoding/json" + "io" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/BishopFox/cloudfox/aws/graph/ingester/schema" + "github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + log "github.com/sirupsen/logrus" +) + +const ( + // Neo4j + MergeNodeQueryTemplate = `CALL apoc.merge.node([$labels[0]], {id: $id}, $properties, $properties) YIELD node as obj + CALL apoc.create.setLabels(obj, $labels) YIELD node as labeledObj + RETURN labeledObj` + + MergeRelationQueryTemplate = `UNWIND $batch as row + CALL apoc.merge.node([row.sourceLabel], apoc.map.fromValues([row.sourceProperty, row.sourceNodeId])) YIELD node as from + CALL apoc.merge.node([row.targetLabel], apoc.map.fromValues([row.targetProperty, row.targetNodeId])) YIELD node as to + CALL apoc.merge.relationship(from, row.relationshipType, {}, row.properties, to) YIELD rel + RETURN rel` + + // Using sprintf to insert the label name since the driver doesn't support parameters for labels here + // %[1]s is a nice way to say "insert the first parameter here" + CreateConstraintQueryTemplate = "CREATE CONSTRAINT IF NOT EXISTS FOR (n: %s) REQUIRE n.id IS UNIQUE" + CreateIndexQueryTemplate = "CREATE INDEX %[1]s_id IF NOT EXISTS FOR (n: %[1]s) ON (n.id)" + + PostProcessMergeQueryTemplate = `MATCH (n) + WITH n.id AS id, COLLECT(n) AS nodesToMerge + WHERE size(nodesToMerge) > 1 + CALL apoc.refactor.mergeNodes(nodesToMerge, {properties: 'combine', mergeRels:true}) + YIELD node + RETURN count(*);` +) + +type Neo4jConfig struct { + Uri string + Username string + Password string +} + +type CloudFoxIngestor struct { + Neo4jConfig + ResultsFile string + Driver neo4j.DriverWithContext + TmpDir string +} + +func NewCloudFoxIngestor(resultsFile string, username string, password string, server string) (*CloudFoxIngestor, error) { + config := Neo4jConfig{ + Uri: server, + Username: username, + Password: password, + } + driver, err := neo4j.NewDriverWithContext(config.Uri, neo4j.BasicAuth(config.Username, config.Password, "")) + if err != nil { + return nil, err + } + return &CloudFoxIngestor{ + Neo4jConfig: config, + ResultsFile: resultsFile, + Driver: driver, + }, nil +} + +func unzipToTemp(zipFilePath string) (string, error) { + // Create a temporary directory to extract the zip file to + tempDir, err := os.MkdirTemp("", "cirro") + if err != nil { + return "", err + } + + // Open the zip file and extract to a temporary directory + zipfile, err := zip.OpenReader(zipFilePath) + if err != nil { + return "", err + } + defer zipfile.Close() + + for _, file := range zipfile.File { + path := filepath.Join(tempDir, file.Name) + log.Debugf("Extracting file: %s", path) + + fileData, err := file.Open() + if err != nil { + return "", err + } + defer fileData.Close() + + newFile, err := os.Create(path) + if err != nil { + return "", err + } + defer newFile.Close() + + if _, err := io.Copy(newFile, fileData); err != nil { + return "", err + } + } + return tempDir, nil +} + +// func (i *CloudFoxIngestor) ProcessFile(path string, info os.FileInfo) error { +// log.Infof("Processing file: %s", info.Name()) + +// switch info.Name() { +// case "users.jsonl": +// return i.ProcessFileObjects(path, schema.GraphUser, schema.GraphObject) +// case "groups.jsonl": +// return i.ProcessFileObjects(path, schema.GraphGroup, schema.GraphObject) +// case "servicePrincipals.jsonl": +// return i.ProcessFileObjects(path, schema.GraphServicePrincipal, schema.GraphObject) +// case "applications.jsonl": +// return i.ProcessFileObjects(path, schema.GraphApplication, schema.GraphObject) +// case "devices.jsonl": +// return i.ProcessFileObjects(path, schema.GraphDevice, schema.GraphObject) +// case "directoryRoles.jsonl": +// return i.ProcessFileObjects(path, schema.GraphRole, schema.GraphObject) +// case "subscriptions.jsonl": +// return i.ProcessFileObjects(path, schema.Subscription, schema.ArmResource) +// case "tenants.jsonl": +// return i.ProcessFileObjects(path, schema.Tenant, schema.ArmResource) +// case "rbac.jsonl": +// return i.ProcessFileObjects(path, schema.AzureRbac, "") +// default: +// return nil +// } +// } + +// function that takes objects, object type and then makes relationships + +// func (i *CloudFoxIngestor) ProcessObjectsFromSlice(objects []interface{}, objectType schema.NodeLabel, generalType schema.NodeLabel) error { +// func (i *CloudFoxIngestor) ProcessObjectsFromSlice(accounts []models.Account, objectType schema.NodeLabel, generalType schema.NodeLabel) error { +// var object = models.NodeLabelToNodeMap[objectType] + +// //Iterate over the lines and create the nodes +// for _, account := range accounts { +// relationships := object.MakeRelationships() +// if err := i.InsertDBObjects(account, relationships, []schema.NodeLabel{generalType, objectType}); err != nil { +// log.Error(err) +// continue +// } + +// } +// return nil +// } + +func (i *CloudFoxIngestor) ProcessFileObjects(path string, objectType schema.NodeLabel, generalType schema.NodeLabel) error { + + var object = models.NodeLabelToNodeMap[objectType] + + // Open the file + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + // Read the file line by line + scanner := bufio.NewScanner(file) + + //Iterate over the lines and create the nodes + for scanner.Scan() { + line := strings.TrimSuffix(scanner.Text(), "\n") + + // Skip empty lines + if len(line) > 0 { + if err := json.Unmarshal([]byte(line), &object); err != nil { + log.Errorf("%s : %s", err, line) + continue + } + } + relationships := object.MakeRelationships() + if err := i.InsertDBObjects(object, relationships, []schema.NodeLabel{generalType, objectType}); err != nil { + log.Error(err) + continue + } + + } + return nil +} + +func (i *CloudFoxIngestor) InsertDBObjects(object schema.Node, relationships []schema.Relationship, labels []schema.NodeLabel) error { + goCtx := context.Background() + var err error + + // Insert the node + if object != nil { + nodeMap := schema.AsNeo4j(&object) + nodeQueryParams := map[string]interface{}{ + "id": nodeMap["id"], + "labels": labels, + "properties": nodeMap, + } + _, err := neo4j.ExecuteQuery(goCtx, i.Driver, MergeNodeQueryTemplate, nodeQueryParams, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + if err != nil { + log.Errorf("Error inserting node: %s -- %v", err, nodeQueryParams) + return err + } + } + + // Insert the relationships + if len(relationships) > 0 { + var relationshipInterface []map[string]interface{} + + // Check the default SourceProperty and TargetProperty values + for _, relationship := range relationships { + var currentRelationship map[string]interface{} + + if relationship.SourceProperty == "" { + relationship.SourceProperty = "id" + } + if relationship.TargetProperty == "" { + relationship.TargetProperty = "id" + } + relationshipBytes, err := json.Marshal(relationship) + if err != nil { + return err + } + if err := json.Unmarshal(relationshipBytes, ¤tRelationship); err != nil { + return err + } + relationshipInterface = append(relationshipInterface, currentRelationship) + } + + _, err = neo4j.ExecuteQuery(goCtx, i.Driver, MergeRelationQueryTemplate, map[string]interface{}{"batch": relationshipInterface}, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + if err != nil { + log.Errorf("Error inserting relationships: %s -- %v", err, relationshipInterface) + return err + } + } + + return nil +} + +func (i *CloudFoxIngestor) Run() error { + goCtx := context.Background() + log.Infof("Verifying connectivity to Neo4J at %s", i.Uri) + if err := i.Driver.VerifyConnectivity(goCtx); err != nil { + return err + } + defer i.Driver.Close(goCtx) + + // Get the label to model map + + // Create constraints and indexes + // log.Info("Creating constraints and indexes for labels") + // for label := range models.NodeLabelToNodeMap { + // for _, query := range []string{CreateConstraintQueryTemplate, CreateIndexQueryTemplate} { + // _, err := neo4j.ExecuteQuery(goCtx, i.Driver, fmt.Sprintf(query, label), nil, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + // if err != nil { + // log.Error(err) + // continue + // } + // } + // } + + // Unzip the results file to a temporary directory + tempDir, err := unzipToTemp(i.ResultsFile) + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + log.Debugf("Using temp dir: %s", tempDir) + + // Open the temporary directory and iterate over the files + fileWg := new(sync.WaitGroup) + filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if tempDir == path { + return nil + } + + fileWg.Add(1) + go func(path string) { + defer fileWg.Done() + //i.ProcessFile(path, info) + log.Infof("Finished processing file: %s", info.Name()) + }(path) + return nil + }) + fileWg.Wait() + log.Info("Finished processing files") + + // Run the post processing merge query + log.Info("Running post processing merge query") + _, err = neo4j.ExecuteQuery(goCtx, i.Driver, PostProcessMergeQueryTemplate, nil, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + if err != nil { + log.Error(err) + return err + } + return nil +} diff --git a/aws/graph/ingester/schema/models/account.go b/aws/graph/ingester/schema/models/account.go new file mode 100644 index 0000000..93bffcb --- /dev/null +++ b/aws/graph/ingester/schema/models/account.go @@ -0,0 +1,45 @@ +package models + +import ( + "github.com/BishopFox/cloudfox/aws/graph/ingester/schema" +) + +type Account struct { + Id string + Arn string + Email string + Name string + Status string + JoinedMethod string + JoinedTimestamp string + IsOrgMgmt bool + IsChildAccount bool + OrgMgmtAccountID string +} + +func (a *Account) MakeRelationships() []schema.Relationship { + var relationships []schema.Relationship + + // make relationship from children accounts to parent org + if a.IsChildAccount { + // make relationship from child to org mgmt account + relationships = append(relationships, schema.Relationship{ + SourceNodeID: a.OrgMgmtAccountID, + TargetNodeID: a.Id, + SourceLabel: schema.Organization, + TargetLabel: schema.Account, + RelationshipType: schema.Manages, + }) + // make relationship from parent org mgmt account to child account + relationships = append(relationships, schema.Relationship{ + SourceNodeID: a.Id, + TargetNodeID: a.OrgMgmtAccountID, + SourceLabel: schema.Account, + TargetLabel: schema.Organization, + RelationshipType: schema.MemberOf, + }) + + } + + return relationships +} diff --git a/aws/graph/ingester/schema/models/constants.go b/aws/graph/ingester/schema/models/constants.go new file mode 100644 index 0000000..bb391c8 --- /dev/null +++ b/aws/graph/ingester/schema/models/constants.go @@ -0,0 +1,10 @@ +package models + +import ( + "github.com/BishopFox/cloudfox/aws/graph/ingester/schema" +) + +var NodeLabelToNodeMap = map[schema.NodeLabel]schema.Node{ + //schema.Organization: &Organization{}, + schema.Account: &Account{}, +} diff --git a/aws/graph/ingester/schema/models/org.go b/aws/graph/ingester/schema/models/org.go new file mode 100644 index 0000000..6e8cee4 --- /dev/null +++ b/aws/graph/ingester/schema/models/org.go @@ -0,0 +1,19 @@ +package models + +import ( + "github.com/BishopFox/cloudfox/aws/graph/ingester/schema" +) + +type Organization struct { + Id string + Arn string + MasterAccountArn string + MasterAccountId string + MasterAccountEmail string + ChildAccounts []Account + MgmtAccount Account +} + +func (o *Organization) MakeRelationships() []schema.Relationship { + return []schema.Relationship{} +} diff --git a/aws/graph/ingester/schema/models/resource.go b/aws/graph/ingester/schema/models/resource.go new file mode 100644 index 0000000..db5af6a --- /dev/null +++ b/aws/graph/ingester/schema/models/resource.go @@ -0,0 +1,5 @@ +package models + +type Resource struct { + ARN string +} diff --git a/aws/graph/ingester/schema/models/roles.go b/aws/graph/ingester/schema/models/roles.go new file mode 100644 index 0000000..9816be9 --- /dev/null +++ b/aws/graph/ingester/schema/models/roles.go @@ -0,0 +1,11 @@ +package models + +type Role struct { + RoleARN string + RoleName string + TrustDocument string + TrustedPrincipal string + TrustedService string + TrustedFederatedProvider string + TrustedFederatedSubject string +} diff --git a/aws/graph/ingester/schema/schema.go b/aws/graph/ingester/schema/schema.go new file mode 100644 index 0000000..e7bb569 --- /dev/null +++ b/aws/graph/ingester/schema/schema.go @@ -0,0 +1,98 @@ +package schema + +import ( + "github.com/goccy/go-json" + "golang.org/x/exp/slices" +) + +type RelationshipType string +type NodeLabel string + +type Node interface { + MakeRelationships() []Relationship +} + +type Relationship struct { + SourceNodeID string `json:"sourceNodeId"` + TargetNodeID string `json:"targetNodeId"` + SourceLabel NodeLabel `json:"sourceLabel"` + TargetLabel NodeLabel `json:"targetLabel"` + RelationshipType RelationshipType `json:"relationshipType"` + Properties map[string]interface{} `json:"properties"` + SourceProperty string `json:"sourceProperty"` + TargetProperty string `json:"targetProperty"` +} + +const ( + // Relationships + + AssociatedTo RelationshipType = "AssociatedTo" + AttachedTo RelationshipType = "AttachedTo" + Authenticates RelationshipType = "Authenticates" + ConnectedTo RelationshipType = "ConnectedTo" + Contains RelationshipType = "Contains" + Exposes RelationshipType = "Exposes" + HasAccess RelationshipType = "HasAccess" + HasConfig RelationshipType = "HasConfig" + HasDisk RelationshipType = "HasDisk" + HasInstance RelationshipType = "HasInstance" + HasRbac RelationshipType = "HasRbac" + HasRole RelationshipType = "HasRole" + Manages RelationshipType = "Manages" + MemberOf RelationshipType = "MemberOf" + Owns RelationshipType = "Owns" + Represents RelationshipType = "Represents" + Trusts RelationshipType = "Trusts" +) + +const ( + // Node labels + Resource NodeLabel = "Resource" + Account NodeLabel = "Account" + Organization NodeLabel = "Org" + Service NodeLabel = "Service" + Role NodeLabel = "Role" + Group NodeLabel = "Group" + User NodeLabel = "User" + FederatedIdentity NodeLabel = "FederatedIdentity" +) + +func AsNeo4j(object *Node) map[string]interface{} { + + // We don't want to include these fields in the map + fieldsToExclude := []string{"members", "owners", "appRoles", "registeredUsers"} + objectMap, err := json.Marshal(object) + if err != nil { + return nil + } + + var objectMapInterface map[string]interface{} + json.Unmarshal(objectMap, &objectMapInterface) + for _, field := range fieldsToExclude { + delete(objectMapInterface, field) + } + + // We need to convert flatten maps to an array + // We'll want to keep order of the keys for things like extensionAttributes + for key, value := range objectMapInterface { + _, isMap := value.(map[string]interface{}) + if isMap { + var valueArray []string + var keys []string + + for k := range value.(map[string]interface{}) { + keys = append(keys, k) + } + slices.Sort(keys) + + for _, k := range keys { + valueString := value.(map[string]interface{})[k].(string) + if valueString != "" { + valueArray = append(valueArray, k, valueString) + } + } + objectMapInterface[key] = valueArray + } + } + return objectMapInterface +} diff --git a/aws/instances.go b/aws/instances.go index 8ce1e89..e3e8419 100644 --- a/aws/instances.go +++ b/aws/instances.go @@ -94,7 +94,7 @@ func (m *InstancesModule) Instances(filter string, outputDirectory string, verbo // Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) - m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) + m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { // fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) diff --git a/aws/lambda.go b/aws/lambda.go index e3e590e..b4d1c7d 100644 --- a/aws/lambda.go +++ b/aws/lambda.go @@ -76,7 +76,7 @@ func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) { fmt.Printf("[%s][%s] Enumerating lambdas for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) - m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) + m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { // fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) diff --git a/aws/resource-trusts.go b/aws/resource-trusts.go index 67dfc19..dde2e6f 100644 --- a/aws/resource-trusts.go +++ b/aws/resource-trusts.go @@ -291,7 +291,7 @@ func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGrou semaphore <- struct{}{} defer func() { <-semaphore }() - cloudFoxSNSClient := InitCloudFoxSNSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.WrapTable) + cloudFoxSNSClient := InitSNSCommandClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines, m.WrapTable) ListTopics, err := cloudFoxSNSClient.listTopics(r) if err != nil { @@ -367,7 +367,7 @@ func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan s semaphore <- struct{}{} defer func() { <-semaphore }() - cloudFoxS3Client := initCloudFoxS3Client(m.Caller, m.AWSProfile, m.CloudFoxVersion) + cloudFoxS3Client := InitS3CommandClient(m.Caller, m.AWSProfile, m.CloudFoxVersion) ListBuckets, err := sdk.CachedListBuckets(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account)) if err != nil { @@ -450,7 +450,7 @@ func (m *ResourceTrustsModule) getSQSQueuesPerRegion(r string, wg *sync.WaitGrou semaphore <- struct{}{} defer func() { <-semaphore }() - cloudFoxSQSClient := InitSQSClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) + cloudFoxSQSClient := InitSQSCommandClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) ListQueues, err := cloudFoxSQSClient.listQueues(r) if err != nil { @@ -512,7 +512,7 @@ func (m *ResourceTrustsModule) getECRRecordsPerRegion(r string, wg *sync.WaitGro semaphore <- struct{}{} defer func() { <-semaphore }() - cloudFoxECRClient := InitECRClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) + cloudFoxECRClient := InitECRCommandClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) DescribeRepositories, err := sdk.CachedECRDescribeRepositories(cloudFoxECRClient.ECRClient, aws.ToString(m.Caller.Account), r) if err != nil { @@ -576,7 +576,7 @@ func (m *ResourceTrustsModule) getCodeBuildResourcePoliciesPerRegion(r string, w semaphore <- struct{}{} defer func() { <-semaphore }() - cloudFoxCodeBuildClient := InitCodeBuildClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) + cloudFoxCodeBuildClient := InitCodeBuildCommandClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) ListProjects, err := sdk.CachedCodeBuildListProjects(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r) if err != nil { @@ -650,7 +650,7 @@ func (m *ResourceTrustsModule) getLambdaPolicyPerRegion(r string, wg *sync.WaitG semaphore <- struct{}{} defer func() { <-semaphore }() - cloudFoxLambdaClient := InitLambdaClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) + cloudFoxLambdaClient := InitLambdaCommandClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) ListFunctions, err := cloudFoxLambdaClient.listFunctions(r) if err != nil { @@ -715,7 +715,7 @@ func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *s semaphore <- struct{}{} defer func() { <-semaphore }() - cloudFoxEFSClient := InitFileSystemsClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) + cloudFoxEFSClient := InitFileSystemsCommandClient(m.Caller, m.AWSProfile, m.CloudFoxVersion, m.Goroutines) ListFileSystems, err := sdk.CachedDescribeFileSystems(cloudFoxEFSClient.EFSClient, aws.ToString(m.Caller.Account), r) if err != nil { diff --git a/aws/role-trusts.go b/aws/role-trusts.go index 73dafad..ccc2fce 100644 --- a/aws/role-trusts.go +++ b/aws/role-trusts.go @@ -116,7 +116,7 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int fmt.Printf("[%s][%s] Enumerating role trusts for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) //fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) - m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) + m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { // fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) // } else { diff --git a/cli/aws.go b/cli/aws.go index 2cf06bd..312da6d 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -444,6 +444,16 @@ var ( PostRun: awsPostRun, } + GraphCommand = &cobra.Command{ + Use: "graph", + Short: "Graph the relationships between resources", + Long: "\nUse case examples:\n" + + os.Args[0] + " aws graph --profile readonly_profile", + PreRun: awsPreRun, + Run: runGraphCommand, + PostRun: awsPostRun, + } + AllChecksCommand = &cobra.Command{ Use: "all-checks", @@ -510,7 +520,7 @@ func awsPreRun(cmd *cobra.Command, args []string) { } } - orgModuleClient := aws.InitOrgClient(*caller, profile, cmd.Root().Version, Goroutines) + orgModuleClient := aws.InitOrgCommandClient(*caller, profile, cmd.Root().Version, Goroutines) isPartOfOrg := orgModuleClient.IsCallerAccountPartOfAnOrg() if isPartOfOrg { isMgmtAccount := orgModuleClient.IsManagementAccount(orgModuleClient.DescribeOrgOutput, ptr.ToString(caller.Account)) @@ -570,7 +580,7 @@ func FindOrgMgmtAccountAndReorderAccounts(AWSProfiles []string, version string) fmt.Printf("[%s] Loaded cached AWS data for to %s\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), ptr.ToString(caller.Account)) } } - orgModuleClient := aws.InitOrgClient(*caller, profile, version, Goroutines) + orgModuleClient := aws.InitOrgCommandClient(*caller, profile, version, Goroutines) orgModuleClient.DescribeOrgOutput, err = sdk.CachedOrganizationsDescribeOrganization(orgModuleClient.OrganizationsClient, ptr.ToString(caller.Account)) if err != nil { continue @@ -751,7 +761,7 @@ func runSNSCommand(cmd *cobra.Command, args []string) { if err != nil { continue } - cloudFoxSNSClient := aws.InitCloudFoxSNSClient(*caller, profile, cmd.Root().Version, Goroutines, AWSWrapTable) + cloudFoxSNSClient := aws.InitSNSCommandClient(*caller, profile, cmd.Root().Version, Goroutines, AWSWrapTable) cloudFoxSNSClient.PrintSNS(AWSOutputDirectory, Verbosity) } } @@ -866,6 +876,29 @@ func runFilesystemsCommand(cmd *cobra.Command, args []string) { } } +func runGraphCommand(cmd *cobra.Command, args []string) { + for _, profile := range AWSProfiles { + var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version) + caller, err := internal.AWSWhoami(profile, cmd.Root().Version) + if err != nil { + continue + } + graphCommandClient := aws.GraphCommand{ + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version), + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + AWSOutputDirectory: AWSOutputDirectory, + Verbosity: Verbosity, + AWSConfig: AWSConfig, + } + graphCommandClient.RunGraphCommand() + } +} + func runIamSimulatorCommand(cmd *cobra.Command, args []string) { for _, profile := range AWSProfiles { var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version) @@ -1703,7 +1736,7 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { } sqsMod.PrintSQS(AWSOutputDirectory, Verbosity) - cloudFoxSNSClient := aws.InitCloudFoxSNSClient(*caller, profile, cmd.Root().Version, Goroutines, AWSWrapTable) + cloudFoxSNSClient := aws.InitSNSCommandClient(*caller, profile, cmd.Root().Version, Goroutines, AWSWrapTable) cloudFoxSNSClient.PrintSNS(AWSOutputDirectory, Verbosity) resourceTrustsCommand := aws.ResourceTrustsModule{ @@ -1884,6 +1917,7 @@ func init() { ResourceTrustsCommand, OrgsCommand, DatabasesCommand, + GraphCommand, ) } diff --git a/go.mod b/go.mod index d4c8d1e..2510798 100644 --- a/go.mod +++ b/go.mod @@ -105,6 +105,7 @@ require ( github.com/dimchansky/utfbom v1.1.1 // indirect github.com/go-openapi/errors v0.20.4 // indirect github.com/go-openapi/strfmt v0.21.7 // indirect + github.com/goccy/go-json v0.10.2 github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/google/uuid v1.3.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -115,11 +116,13 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/neo4j/neo4j-go-driver/v5 v5.14.0 github.com/oklog/ulid v1.3.1 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/pflag v1.0.5 // indirect go.mongodb.org/mongo-driver v1.12.1 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect diff --git a/go.sum b/go.sum index 5708efa..e7394d8 100644 --- a/go.sum +++ b/go.sum @@ -251,6 +251,8 @@ github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWL github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= @@ -353,6 +355,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/neo4j/neo4j-go-driver/v5 v5.14.0 h1:5x3vD4HkXQIktlG63jSG8v9iweGjmObIPU7Y9U0ThUI= +github.com/neo4j/neo4j-go-driver/v5 v5.14.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -430,6 +434,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= From 4edbd3326c3415f7b990767e0e2f04c423a45568 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:52:49 -0500 Subject: [PATCH 02/29] neo4j cross-account stuff kind of working, pmapper stuff not working --- aws/client-initializers.go | 16 + aws/graph.go | 336 ++++++++++++++++-- aws/graph/collectors/accounts.go | 1 + aws/graph/ingester/ingestor.go | 196 +++++----- aws/graph/ingester/schema/models/account.go | 9 +- aws/graph/ingester/schema/models/constants.go | 5 +- aws/graph/ingester/schema/models/org.go | 1 + aws/graph/ingester/schema/models/roles.go | 309 +++++++++++++++- aws/graph/ingester/schema/schema.go | 82 ++++- aws/permissions.go | 35 +- aws/pmapper.go | 103 ++++++ aws/role-trusts.go | 88 +---- cli/aws.go | 25 +- internal/aws.go | 8 +- internal/aws/policy/role-trust-policies.go | 82 +++++ internal/common/common.go | 17 + internal/output2.go | 12 + 17 files changed, 1042 insertions(+), 283 deletions(-) create mode 100644 aws/graph/collectors/accounts.go create mode 100644 internal/aws/policy/role-trust-policies.go create mode 100644 internal/common/common.go diff --git a/aws/client-initializers.go b/aws/client-initializers.go index 46086e3..4b105b3 100644 --- a/aws/client-initializers.go +++ b/aws/client-initializers.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/efs" "github.com/aws/aws-sdk-go-v2/service/fsx" "github.com/aws/aws-sdk-go-v2/service/glue" + "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/organizations" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -128,6 +129,17 @@ func InitOrgCommandClient(caller sts.GetCallerIdentityOutput, AWSProfile string, return orgClient } +func InitPermissionsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) IamPermissionsModule { + var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) + permissionsClient := IamPermissionsModule{ + IAMClient: iam.NewFromConfig(AWSConfig), + Caller: caller, + AWSProfile: AWSProfile, + AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion), + } + return permissionsClient +} + func InitSecretsManagerClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int) *secretsmanager.Client { var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion) return secretsmanager.NewFromConfig(AWSConfig) @@ -141,3 +153,7 @@ func InitGlueClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVer func InitOrgClient(AWSConfig aws.Config) *organizations.Client { return organizations.NewFromConfig(AWSConfig) } + +func InitIAMClient(AWSConfig aws.Config) *iam.Client { + return iam.NewFromConfig(AWSConfig) +} diff --git a/aws/graph.go b/aws/graph.go index b8d6af4..d42eafb 100644 --- a/aws/graph.go +++ b/aws/graph.go @@ -1,11 +1,16 @@ package aws import ( + "context" "fmt" + "os" + "strings" + ingestor "github.com/BishopFox/cloudfox/aws/graph/ingester" "github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models" "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" + "github.com/BishopFox/cloudfox/internal/aws/policy" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/sirupsen/logrus" @@ -24,6 +29,11 @@ type GraphCommand struct { Verbosity int AWSOutputDirectory string AWSConfig aws.Config + Version string + SkipAdminCheck bool + + pmapperMod PmapperModule + pmapperError error // Main module data // Used to store output data for pretty printing @@ -32,11 +42,6 @@ type GraphCommand struct { modLog *logrus.Entry } -func init() { - // initialize org client for graph command - -} - func (m *GraphCommand) RunGraphCommand() { // These struct values are used by the output module @@ -50,58 +55,315 @@ func (m *GraphCommand) RunGraphCommand() { m.AWSProfile = internal.BuildAWSPath(m.Caller) } - m.modLog.Info("Generating graph") + m.modLog.Info("Collecting data for graph ingestor...") + + m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + // Accounts + + accounts := m.collectAccountDataForGraph() + // write data to jsonl file for ingestor to read + fileName := fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "accounts") + // create file and directory if it doesnt exist + if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil { + m.modLog.Error(err) + return + } + + outputFile, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + m.modLog.Error(err) + return + } + defer outputFile.Close() + + for _, account := range accounts { + if err := internal.WriteJsonlFile(outputFile, account); err != nil { + m.modLog.Error(err) + return + } + } + + // Roles + + roles := m.collectRoleDataForGraph() + // write data to jsonl file for ingestor to read + fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "roles") + // create file and directory if it doesnt exist + if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil { + m.modLog.Error(err) + return + } + + outputFile, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + m.modLog.Error(err) + return + } + defer outputFile.Close() - m.collectDataForGraph() + for _, role := range roles { + if err := internal.WriteJsonlFile(outputFile, role); err != nil { + m.modLog.Error(err) + return + } + } + + ingestor, err := ingestor.NewCloudFoxIngestor() + if err != nil { + return + } + + // Pmapper hack + + goCtx := context.Background() + sharedLogger.Infof("Verifying connectivity to Neo4J at %s", ingestor.Uri) + if err := ingestor.Driver.VerifyConnectivity(goCtx); err != nil { + sharedLogger.Error(err) + } + defer ingestor.Driver.Close(goCtx) + m.pmapperMod.GenerateCypherStatements(goCtx, ingestor.Driver) + + // back to regular stuff + + ingestor.Run(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account))) } -func (m *GraphCommand) collectDataForGraph() { +func (m *GraphCommand) collectAccountDataForGraph() []models.Account { //OrganizationsCommandClient := InitOrgClient(m.AWSConfig) + var accounts []models.Account OrganizationsCommandClient := InitOrgClient(m.AWSConfig) DescribeOrgOutput, err := sdk.CachedOrganizationsDescribeOrganization(OrganizationsCommandClient, aws.ToString(m.Caller.Account)) if err != nil { m.modLog.Fatal(err) } if DescribeOrgOutput.MasterAccountId == nil { - m.modLog.Fatal("Organization is not configured") + m.modLog.Error("Organization is not configured") } + // If the account is not the org mgmt account, it can only see some info about itself and some info about the org mgmt account. + // populate both of them here. if aws.ToString(DescribeOrgOutput.MasterAccountId) != aws.ToString(m.Caller.Account) { - m.modLog.Fatal("You must run this command from the master account") + + //create new object of type models.Account for this account + thisAccount := models.Account{ + Id: aws.ToString(m.Caller.Account), + Arn: fmt.Sprintf("arn:aws:iam::%s:root", aws.ToString(m.Caller.Account)), + //Name: aws.ToString(m.Caller.Account), + IsOrgMgmt: false, + IsChildAccount: true, + OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId), + OrganizationID: aws.ToString(DescribeOrgOutput.Id), + } + accounts = append(accounts, thisAccount) + + //create new object of type models.Account for the mgmt account + mgmtAccount := models.Account{ + Id: aws.ToString(DescribeOrgOutput.MasterAccountId), + Arn: aws.ToString(DescribeOrgOutput.MasterAccountArn), + Email: aws.ToString(DescribeOrgOutput.MasterAccountEmail), + //Status: string(account.Status), + //JoinedMethod: string(account.JoinedMethod), + //JoinedTimestamp: account.JoinedTimestamp.String(), + IsOrgMgmt: true, + IsChildAccount: false, + OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId), + OrganizationID: aws.ToString(DescribeOrgOutput.Id), + } + accounts = append(accounts, mgmtAccount) + return accounts + + } else { + // In this case we are the org mgmt account, so we can see all the accounts in the org. + ListAccounts, err := sdk.CachedOrganizationsListAccounts(OrganizationsCommandClient, aws.ToString(DescribeOrgOutput.MasterAccountId)) + if err != nil { + m.modLog.Fatal(err) + } + for _, account := range ListAccounts { + var isMgmtAccount bool + var isChildAccount bool + m.modLog.Info("Account: ", aws.ToString(account.Name)) + if aws.ToString(DescribeOrgOutput.MasterAccountId) == aws.ToString(account.Id) { + // this is the org mgmt account + isMgmtAccount = true + isChildAccount = false + } else if DescribeOrgOutput.MasterAccountId == nil { + // this is a standalone account + isChildAccount = false + isMgmtAccount = false + } else { + // this is a child account + isChildAccount = true + isMgmtAccount = false + } + + //create new object of type models.Account + account := models.Account{ + Id: aws.ToString(account.Id), + Arn: aws.ToString(account.Arn), + Name: aws.ToString(account.Name), + Email: aws.ToString(account.Email), + Status: string(account.Status), + JoinedMethod: string(account.JoinedMethod), + JoinedTimestamp: account.JoinedTimestamp.String(), + IsOrgMgmt: isMgmtAccount, + IsChildAccount: isChildAccount, + OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId), + OrganizationID: aws.ToString(DescribeOrgOutput.Id), + } + accounts = append(accounts, account) + } + return accounts } - ListAccounts, err := sdk.CachedOrganizationsListAccounts(OrganizationsCommandClient, aws.ToString(DescribeOrgOutput.MasterAccountId)) +} + +func (m *GraphCommand) collectRoleDataForGraph() []models.Role { + var isAdmin, canPrivEscToAdmin string + + iamClient := InitIAMClient(m.AWSConfig) + iamSimClient := InitIamCommandClient(iamClient, m.Caller, m.AWSProfile, m.Goroutines) + localAdminMap := make(map[string]bool) + + var roles []models.Role + IAMCommandClient := InitIAMClient(m.AWSConfig) + ListRolesOutput, err := sdk.CachedIamListRoles(IAMCommandClient, aws.ToString(m.Caller.Account)) if err != nil { - m.modLog.Fatal(err) + m.modLog.Error(err) } - for _, account := range ListAccounts { - var isMgmtAccount bool - var isChildAccount bool - m.modLog.Info("Account: ", aws.ToString(account.Name)) - if aws.ToString(DescribeOrgOutput.MasterAccountId) == aws.ToString(m.Caller.Account) { - isMgmtAccount = false - } else { - isMgmtAccount = true + + for _, role := range ListRolesOutput { + accountId := strings.Split(aws.ToString(role.Arn), ":")[4] + trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role) + if err != nil { + m.modLog.Error(err.Error()) + break } - if DescribeOrgOutput.MasterAccountId == nil { - isChildAccount = false + + if m.pmapperError == nil { + isAdmin, canPrivEscToAdmin = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, role.Arn) } else { - isChildAccount = true + isAdmin, canPrivEscToAdmin = GetIamSimResult(m.SkipAdminCheck, role.Arn, iamSimClient, localAdminMap) } - //create new object of type models.Account - account := models.Account{ - Id: aws.ToString(account.Id), - Arn: aws.ToString(account.Arn), - Name: aws.ToString(account.Name), - Email: aws.ToString(account.Email), - Status: string(account.Status), - JoinedMethod: string(account.JoinedMethod), - JoinedTimestamp: account.JoinedTimestamp.String(), - IsOrgMgmt: isMgmtAccount, - IsChildAccount: isChildAccount, - OrgMgmtAccountID: aws.ToString(DescribeOrgOutput.MasterAccountId), + // for _, row := range m.PermissionRowsFromAllProfiles { + // if row.Arn == aws.ToString(role.Arn) { + + // // look for cases where there is a permission that allows sts:assumeRole or * + // // lowercase the action and compare it against the list of checks below + // if strings.EqualFold(row.Action, "sts:AssumeRole") || + // strings.EqualFold(row.Action, "*") || + // strings.EqualFold(row.Action, "sts:Assume*") || + // strings.EqualFold(row.Action, "sts:*") { + + // if row.Effect == "Allow" { + // //PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole") + // if row.Resource == "*" { + // PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole") + // } else if strings.EqualFold(row.Resource, aws.ToString(role.Arn)) { + // PrivEscPermissions = append(PrivEscPermissions, "sts:AssumeRole") + // } + // } + // } + // if row.Effect == "Deny" { + // // Remove the string sts:AssumeRole from the PrivEscPermissions slice + // for i, v := range PrivEscPermissions { + // if v == "sts:AssumeRole" { + // PrivEscPermissions = append(PrivEscPermissions[:i], PrivEscPermissions[i+1:]...) + // } + // } + // } + // } + // } + + var TrustedPrincipals []models.TrustedPrincipal + var TrustedServices []models.TrustedService + var TrustedFederatedProviders []models.TrustedFederatedProvider + //var TrustedFederatedSubjects string + var trustedProvider string + var trustedSubjects string + + for _, statement := range trustsdoc.Statement { + for _, principal := range statement.Principal.AWS { + TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{ + TrustedPrincipal: principal, + ExternalID: statement.Condition.StringEquals.StsExternalID, + //IsAdmin: false, + //CanPrivEscToAdmin: false, + }) + + } + for _, service := range statement.Principal.Service { + TrustedServices = append(TrustedServices, models.TrustedService{ + TrustedService: service, + //IsAdmin: false, + //CanPrivEscToAdmin: false, + }) + + } + for _, federated := range statement.Principal.Federated { + if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 { + trustedProvider = "GitHub" + trustedSubjects := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",") + if trustedSubjects == "" { + trustedSubjects = "ALL REPOS!!!" + } else { + trustedSubjects = "Repos: " + trustedSubjects + } + + } else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" { + if strings.Contains(statement.Principal.Federated[0], "AWSSSO") { + trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" + } else if strings.Contains(statement.Principal.Federated[0], "Okta") { + trustedProvider = "Okta" // (" + statement.Principal.Federated[0] + ")" + } + trustedSubjects = "Not applicable" + } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" { + trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" + if statement.Condition.StringEquals.OidcEksSub != "" { + trustedSubjects = statement.Condition.StringEquals.OidcEksSub + } else if statement.Condition.StringLike.OidcEksSub != "" { + trustedSubjects = statement.Condition.StringLike.OidcEksSub + } else { + trustedSubjects = "ALL SERVICE ACCOUNTS!" + } + } else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" { + trustedProvider = "Cognito" // (" + statement.Principal.Federated[0] + ")" + if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" { + trustedSubjects = statement.Condition.ForAnyValueStringLike.CognitoAMR + } + } else { + if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") { + trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" + trustedSubjects = "ALL SERVICE ACCOUNTS!" + } else if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") { + trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" + } + trustedSubjects = "Not applicable" + } + + TrustedFederatedProviders = append(TrustedFederatedProviders, models.TrustedFederatedProvider{ + TrustedFederatedProvider: federated, + ProviderShortName: trustedProvider, + TrustedSubjects: trustedSubjects, + //IsAdmin: false, + //CanPrivEscToAdmin: false, + }) + } } - fmt.Println(account) - } + //create new object of type models.Role + role := models.Role{ + Id: aws.ToString(role.RoleId), + AccountID: accountId, + RoleARN: aws.ToString(role.Arn), + RoleName: aws.ToString(role.RoleName), + TrustsDoc: trustsdoc, + TrustedPrincipals: TrustedPrincipals, + TrustedServices: TrustedServices, + TrustedFederatedProviders: TrustedFederatedProviders, + CanPrivEscToAdmin: canPrivEscToAdmin, + IsAdmin: isAdmin, + } + roles = append(roles, role) + } + return roles } diff --git a/aws/graph/collectors/accounts.go b/aws/graph/collectors/accounts.go new file mode 100644 index 0000000..9f1676b --- /dev/null +++ b/aws/graph/collectors/accounts.go @@ -0,0 +1 @@ +package collectors diff --git a/aws/graph/ingester/ingestor.go b/aws/graph/ingester/ingestor.go index dd6574d..e8c7dcd 100644 --- a/aws/graph/ingester/ingestor.go +++ b/aws/graph/ingester/ingestor.go @@ -1,11 +1,10 @@ package ingestor import ( - "archive/zip" "bufio" "context" "encoding/json" - "io" + "fmt" "os" "path/filepath" "strings" @@ -50,16 +49,16 @@ type Neo4jConfig struct { type CloudFoxIngestor struct { Neo4jConfig - ResultsFile string - Driver neo4j.DriverWithContext - TmpDir string + //ResultsFile string + Driver neo4j.DriverWithContext + TmpDir string } -func NewCloudFoxIngestor(resultsFile string, username string, password string, server string) (*CloudFoxIngestor, error) { +func NewCloudFoxIngestor() (*CloudFoxIngestor, error) { config := Neo4jConfig{ - Uri: server, - Username: username, - Password: password, + Uri: "neo4j://localhost:7687", + Username: "neo4j", + Password: "cloudfox", } driver, err := neo4j.NewDriverWithContext(config.Uri, neo4j.BasicAuth(config.Username, config.Password, "")) if err != nil { @@ -67,93 +66,75 @@ func NewCloudFoxIngestor(resultsFile string, username string, password string, s } return &CloudFoxIngestor{ Neo4jConfig: config, - ResultsFile: resultsFile, - Driver: driver, + //ResultsFile: resultsFile, + Driver: driver, }, nil } -func unzipToTemp(zipFilePath string) (string, error) { - // Create a temporary directory to extract the zip file to - tempDir, err := os.MkdirTemp("", "cirro") - if err != nil { - return "", err - } - - // Open the zip file and extract to a temporary directory - zipfile, err := zip.OpenReader(zipFilePath) - if err != nil { - return "", err - } - defer zipfile.Close() - - for _, file := range zipfile.File { - path := filepath.Join(tempDir, file.Name) - log.Debugf("Extracting file: %s", path) - - fileData, err := file.Open() - if err != nil { - return "", err - } - defer fileData.Close() - - newFile, err := os.Create(path) - if err != nil { - return "", err - } - defer newFile.Close() - - if _, err := io.Copy(newFile, fileData); err != nil { - return "", err - } - } - return tempDir, nil -} +// func unzipToTemp(zipFilePath string) (string, error) { +// // Create a temporary directory to extract the zip file to +// tempDir, err := os.MkdirTemp("", "cloudfox-graph") +// if err != nil { +// return "", err +// } -// func (i *CloudFoxIngestor) ProcessFile(path string, info os.FileInfo) error { -// log.Infof("Processing file: %s", info.Name()) - -// switch info.Name() { -// case "users.jsonl": -// return i.ProcessFileObjects(path, schema.GraphUser, schema.GraphObject) -// case "groups.jsonl": -// return i.ProcessFileObjects(path, schema.GraphGroup, schema.GraphObject) -// case "servicePrincipals.jsonl": -// return i.ProcessFileObjects(path, schema.GraphServicePrincipal, schema.GraphObject) -// case "applications.jsonl": -// return i.ProcessFileObjects(path, schema.GraphApplication, schema.GraphObject) -// case "devices.jsonl": -// return i.ProcessFileObjects(path, schema.GraphDevice, schema.GraphObject) -// case "directoryRoles.jsonl": -// return i.ProcessFileObjects(path, schema.GraphRole, schema.GraphObject) -// case "subscriptions.jsonl": -// return i.ProcessFileObjects(path, schema.Subscription, schema.ArmResource) -// case "tenants.jsonl": -// return i.ProcessFileObjects(path, schema.Tenant, schema.ArmResource) -// case "rbac.jsonl": -// return i.ProcessFileObjects(path, schema.AzureRbac, "") -// default: -// return nil +// // Open the zip file and extract to a temporary directory +// zipfile, err := zip.OpenReader(zipFilePath) +// if err != nil { +// return "", err // } -// } +// defer zipfile.Close() -// function that takes objects, object type and then makes relationships +// for _, file := range zipfile.File { +// path := filepath.Join(tempDir, file.Name) +// log.Debugf("Extracting file: %s", path) -// func (i *CloudFoxIngestor) ProcessObjectsFromSlice(objects []interface{}, objectType schema.NodeLabel, generalType schema.NodeLabel) error { -// func (i *CloudFoxIngestor) ProcessObjectsFromSlice(accounts []models.Account, objectType schema.NodeLabel, generalType schema.NodeLabel) error { -// var object = models.NodeLabelToNodeMap[objectType] +// fileData, err := file.Open() +// if err != nil { +// return "", err +// } +// defer fileData.Close() -// //Iterate over the lines and create the nodes -// for _, account := range accounts { -// relationships := object.MakeRelationships() -// if err := i.InsertDBObjects(account, relationships, []schema.NodeLabel{generalType, objectType}); err != nil { -// log.Error(err) -// continue +// newFile, err := os.Create(path) +// if err != nil { +// return "", err // } +// defer newFile.Close() +// if _, err := io.Copy(newFile, fileData); err != nil { +// return "", err +// } // } -// return nil +// return tempDir, nil // } +func (i *CloudFoxIngestor) ProcessFile(path string, info os.FileInfo) error { + log.Infof("Processing file: %s", info.Name()) + + switch info.Name() { + // case "accounts.jsonl": + // return i.ProcessFileObjects(path, schema.Account, schema.Account) + // case "roles.jsonl": + // return i.ProcessFileObjects(path, schema.Role, schema.Role) + // case "servicePrincipals.jsonl": + // return i.ProcessFileObjects(path, schema.GraphServicePrincipal, schema.GraphObject) + // case "applications.jsonl": + // return i.ProcessFileObjects(path, schema.GraphApplication, schema.GraphObject) + // case "devices.jsonl": + // return i.ProcessFileObjects(path, schema.GraphDevice, schema.GraphObject) + // case "directoryRoles.jsonl": + // return i.ProcessFileObjects(path, schema.GraphRole, schema.GraphObject) + // case "subscriptions.jsonl": + // return i.ProcessFileObjects(path, schema.Subscription, schema.ArmResource) + // case "tenants.jsonl": + // return i.ProcessFileObjects(path, schema.Tenant, schema.ArmResource) + // case "rbac.jsonl": + // return i.ProcessFileObjects(path, schema.AzureRbac, "") + default: + return nil + } +} + func (i *CloudFoxIngestor) ProcessFileObjects(path string, objectType schema.NodeLabel, generalType schema.NodeLabel) error { var object = models.NodeLabelToNodeMap[objectType] @@ -195,13 +176,19 @@ func (i *CloudFoxIngestor) InsertDBObjects(object schema.Node, relationships []s // Insert the node if object != nil { - nodeMap := schema.AsNeo4j(&object) + nodeMap, err := schema.ConvertCustomTypesToNeo4j(&object) + if err != nil { + log.Errorf("Error converting custom types to neo4j: %s -- %v", err, object) + return err + } + + //nodeMap := schema.AsNeo4j(&object) nodeQueryParams := map[string]interface{}{ - "id": nodeMap["id"], + "id": nodeMap["Id"], "labels": labels, "properties": nodeMap, } - _, err := neo4j.ExecuteQuery(goCtx, i.Driver, MergeNodeQueryTemplate, nodeQueryParams, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + _, err = neo4j.ExecuteQuery(goCtx, i.Driver, MergeNodeQueryTemplate, nodeQueryParams, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) if err != nil { log.Errorf("Error inserting node: %s -- %v", err, nodeQueryParams) return err @@ -242,50 +229,43 @@ func (i *CloudFoxIngestor) InsertDBObjects(object schema.Node, relationships []s return nil } -func (i *CloudFoxIngestor) Run() error { +func (i *CloudFoxIngestor) Run(graphOutputDir string) error { goCtx := context.Background() log.Infof("Verifying connectivity to Neo4J at %s", i.Uri) if err := i.Driver.VerifyConnectivity(goCtx); err != nil { return err } defer i.Driver.Close(goCtx) + var err error // Get the label to model map // Create constraints and indexes - // log.Info("Creating constraints and indexes for labels") - // for label := range models.NodeLabelToNodeMap { - // for _, query := range []string{CreateConstraintQueryTemplate, CreateIndexQueryTemplate} { - // _, err := neo4j.ExecuteQuery(goCtx, i.Driver, fmt.Sprintf(query, label), nil, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) - // if err != nil { - // log.Error(err) - // continue - // } - // } - // } - - // Unzip the results file to a temporary directory - tempDir, err := unzipToTemp(i.ResultsFile) - if err != nil { - return err + log.Info("Creating constraints and indexes for labels") + for label := range models.NodeLabelToNodeMap { + for _, query := range []string{CreateConstraintQueryTemplate, CreateIndexQueryTemplate} { + _, err := neo4j.ExecuteQuery(goCtx, i.Driver, fmt.Sprintf(query, label), nil, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + if err != nil { + log.Error(err) + continue + } + } } - defer os.RemoveAll(tempDir) - log.Debugf("Using temp dir: %s", tempDir) - // Open the temporary directory and iterate over the files + // Process the files in the output directory fileWg := new(sync.WaitGroup) - filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error { + filepath.Walk(graphOutputDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - if tempDir == path { + if graphOutputDir == path { return nil } fileWg.Add(1) go func(path string) { defer fileWg.Done() - //i.ProcessFile(path, info) + i.ProcessFile(path, info) log.Infof("Finished processing file: %s", info.Name()) }(path) return nil diff --git a/aws/graph/ingester/schema/models/account.go b/aws/graph/ingester/schema/models/account.go index 93bffcb..07b76d6 100644 --- a/aws/graph/ingester/schema/models/account.go +++ b/aws/graph/ingester/schema/models/account.go @@ -1,8 +1,6 @@ package models -import ( - "github.com/BishopFox/cloudfox/aws/graph/ingester/schema" -) +import "github.com/BishopFox/cloudfox/aws/graph/ingester/schema" type Account struct { Id string @@ -15,6 +13,7 @@ type Account struct { IsOrgMgmt bool IsChildAccount bool OrgMgmtAccountID string + OrganizationID string } func (a *Account) MakeRelationships() []schema.Relationship { @@ -24,7 +23,7 @@ func (a *Account) MakeRelationships() []schema.Relationship { if a.IsChildAccount { // make relationship from child to org mgmt account relationships = append(relationships, schema.Relationship{ - SourceNodeID: a.OrgMgmtAccountID, + SourceNodeID: a.OrganizationID, TargetNodeID: a.Id, SourceLabel: schema.Organization, TargetLabel: schema.Account, @@ -33,7 +32,7 @@ func (a *Account) MakeRelationships() []schema.Relationship { // make relationship from parent org mgmt account to child account relationships = append(relationships, schema.Relationship{ SourceNodeID: a.Id, - TargetNodeID: a.OrgMgmtAccountID, + TargetNodeID: a.OrganizationID, SourceLabel: schema.Account, TargetLabel: schema.Organization, RelationshipType: schema.MemberOf, diff --git a/aws/graph/ingester/schema/models/constants.go b/aws/graph/ingester/schema/models/constants.go index bb391c8..377c48c 100644 --- a/aws/graph/ingester/schema/models/constants.go +++ b/aws/graph/ingester/schema/models/constants.go @@ -5,6 +5,7 @@ import ( ) var NodeLabelToNodeMap = map[schema.NodeLabel]schema.Node{ - //schema.Organization: &Organization{}, - schema.Account: &Account{}, + schema.Organization: &Organization{}, + schema.Account: &Account{}, + schema.Role: &Role{}, } diff --git a/aws/graph/ingester/schema/models/org.go b/aws/graph/ingester/schema/models/org.go index 6e8cee4..589c25a 100644 --- a/aws/graph/ingester/schema/models/org.go +++ b/aws/graph/ingester/schema/models/org.go @@ -6,6 +6,7 @@ import ( type Organization struct { Id string + OrgId string Arn string MasterAccountArn string MasterAccountId string diff --git a/aws/graph/ingester/schema/models/roles.go b/aws/graph/ingester/schema/models/roles.go index 9816be9..7290916 100644 --- a/aws/graph/ingester/schema/models/roles.go +++ b/aws/graph/ingester/schema/models/roles.go @@ -1,11 +1,308 @@ package models +import ( + "fmt" + "strings" + + "github.com/BishopFox/cloudfox/aws/graph/ingester/schema" + "github.com/BishopFox/cloudfox/internal/aws/policy" + "github.com/BishopFox/cloudfox/internal/common" +) + +type User struct { + Id string + UserArn string + UserName string + IsAdmin string + CanPrivEscToAdmin string +} + type Role struct { - RoleARN string - RoleName string - TrustDocument string - TrustedPrincipal string - TrustedService string + Id string + AccountID string + RoleARN string + RoleName string + TrustsDoc policy.TrustPolicyDocument + TrustedPrincipals []TrustedPrincipal + TrustedServices []TrustedService + TrustedFederatedProviders []TrustedFederatedProvider + CanPrivEscToAdmin string + IsAdmin string +} + +type TrustedPrincipal struct { + TrustedPrincipal string + ExternalID string + //IsAdmin bool + //CanPrivEscToAdmin bool +} + +type TrustedService struct { + TrustedService string + //IsAdmin bool + //CanPrivEscToAdmin bool +} + +type TrustedFederatedProvider struct { TrustedFederatedProvider string - TrustedFederatedSubject string + ProviderShortName string + TrustedSubjects string + //IsAdmin bool + //CanPrivEscToAdmin bool +} + +func (a *Role) MakeRelationships() []schema.Relationship { + var relationships []schema.Relationship + //instance := singleton.GetInstance() + + // get thisAccount id from role arn + var thisAccount string + if len(a.RoleARN) >= 25 { + thisAccount = a.RoleARN[13:25] + } else { + fmt.Sprintf("Could not get account number from this role arn%s", a.RoleARN) + } + + // make a relationship between each role and the account it belongs to + relationships = append(relationships, schema.Relationship{ + SourceNodeID: a.Id, + TargetNodeID: thisAccount, + SourceLabel: schema.Role, + TargetLabel: schema.Account, + RelationshipType: schema.MemberOf, + }) + + for _, TrustedPrincipal := range a.TrustedPrincipals { + //get account id from the trusted principal arn + var trustedPrincipalAccount string + if len(TrustedPrincipal.TrustedPrincipal) >= 25 { + trustedPrincipalAccount = TrustedPrincipal.TrustedPrincipal[13:25] + } else { + fmt.Sprintf("Could not get account number from this TrustedPrincipal%s", TrustedPrincipal.TrustedPrincipal) + } + var PermissionsRowAccount string + // make a TRUSTED_BY relationship between the role and the trusted principal. This does not mean the principal can assume this role, we need more logic to determine that (see below) + // relationships = append(relationships, schema.Relationship{ + // SourceNodeID: TrustedPrincipal.TrustedPrincipal, + // TargetNodeID: a.Id, + // SourceLabel: schema.Principal, + // TargetLabel: schema.Role, + // RelationshipType: schema.IsTrustedBy, + // }) + // // make a TRUSTS relationship between the trusted principal and this role. This does not mean the principal can assume this role, we need more logic to determine that (see below) + // relationships = append(relationships, schema.Relationship{ + // SourceNodeID: a.Id, + // TargetNodeID: TrustedPrincipal.TrustedPrincipal, + // SourceLabel: schema.Role, + // TargetLabel: schema.Principal, + // RelationshipType: schema.Trusts, + // }) + // // make a MEMBER_OF relationship between the role and the account + // relationships = append(relationships, schema.Relationship{ + // SourceNodeID: TrustedPrincipal.TrustedPrincipal, + // TargetNodeID: trustedPrincipalAccount, + // SourceLabel: schema.Principal, + // TargetLabel: schema.Account, + // RelationshipType: schema.MemberOf, + // }) + + // if the role trusts a principal in this same account explicitly, then the principal can assume the role + if thisAccount == trustedPrincipalAccount { + // make a CAN_ASSUME relationship between the trusted principal and this role + relationships = append(relationships, schema.Relationship{ + SourceNodeID: TrustedPrincipal.TrustedPrincipal, + TargetNodeID: a.Id, + SourceLabel: schema.Principal, + TargetLabel: schema.Role, + RelationshipType: schema.CanAssume, + }) + // make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal + // relationships = append(relationships, schema.Relationship{ + // SourceNodeID: a.Id, + // TargetNodeID: TrustedPrincipal.TrustedPrincipal, + // SourceLabel: schema.Role, + // TargetLabel: schema.Principal, + // RelationshipType: schema.CanBeAssumedBy, + // }) + } + + // If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role + // if the role we are looking at trusts root in it's own account + + if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) { + // iterate over all rows in AllPermissionsRows + for _, PermissionsRow := range common.PermissionRowsFromAllProfiles { + // but we only care about the rows that have arns that are in this account + + if len(PermissionsRow.Arn) >= 25 { + PermissionsRowAccount = PermissionsRow.Arn[13:25] + } else { + fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn) + } + + if PermissionsRowAccount == thisAccount { + // lets only look for rows that have sts:AssumeRole permissions + if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || + strings.EqualFold(PermissionsRow.Action, "*") || + strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || + strings.EqualFold(PermissionsRow.Action, "sts:*") { + // lets only focus on rows that have an effect of Allow + if strings.EqualFold(PermissionsRow.Effect, "Allow") { + // if the resource is * or the resource is this role arn, then this principal can assume this role + if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.RoleARN) { + // make a CAN_ASSUME relationship between the trusted principal and this role + //evalutate if the princiapl is a user or a role and set a variable accordingly + //var principalType schema.NodeLabel + if strings.EqualFold(PermissionsRow.Type, "User") { + relationships = append(relationships, schema.Relationship{ + SourceNodeID: PermissionsRow.Arn, + TargetNodeID: a.Id, + SourceLabel: schema.User, + TargetLabel: schema.Role, + RelationshipType: schema.CanAssume, + }) + } else if strings.EqualFold(PermissionsRow.Type, "Role") { + relationships = append(relationships, schema.Relationship{ + SourceNodeID: PermissionsRow.Arn, + TargetNodeID: a.Id, + SourceLabel: schema.Role, + TargetLabel: schema.Role, + RelationshipType: schema.CanAssume, + }) + } + + // relationships = append(relationships, schema.Relationship{ + // SourceNodeID: PermissionsRow.Arn, + // TargetNodeID: a.Id, + // SourceLabel: principalType, + // TargetLabel: schema.Role, + // RelationshipType: schema.CanAssumeTest, + // }) + // make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal + // relationships = append(relationships, schema.Relationship{ + // SourceNodeID: a.Id, + // TargetNodeID: PermissionsRow.Arn, + // SourceLabel: schema.Role, + // TargetLabel: schema.Principal, + // RelationshipType: schema.CanBeAssumedByTest, + // }) + + } + } + } + + } + } + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) { + // iterate over all rows in AllPermissionsRows + for _, PermissionsRow := range common.PermissionRowsFromAllProfiles { + // but we only care about the rows that have arns that are in this other account + if len(PermissionsRow.Arn) >= 25 { + PermissionsRowAccount = PermissionsRow.Arn[13:25] + } else { + fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn) + } + if PermissionsRowAccount == trustedPrincipalAccount { + // lets only look for rows that have sts:AssumeRole permissions + if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || + strings.EqualFold(PermissionsRow.Action, "*") || + strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || + strings.EqualFold(PermissionsRow.Action, "sts:*") { + // lets only focus on rows that have an effect of Allow + if strings.EqualFold(PermissionsRow.Effect, "Allow") { + // if the resource is * or the resource is this role arn, then this principal can assume this role + if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.RoleARN) { + // make a CAN_ASSUME relationship between the trusted principal and this role + + if strings.EqualFold(PermissionsRow.Type, "User") { + relationships = append(relationships, schema.Relationship{ + SourceNodeID: PermissionsRow.Arn, + TargetNodeID: a.Id, + SourceLabel: schema.User, + TargetLabel: schema.Role, + RelationshipType: schema.CanAssumeCrossAccount, + }) + } else if strings.EqualFold(PermissionsRow.Type, "Role") { + relationships = append(relationships, schema.Relationship{ + SourceNodeID: PermissionsRow.Arn, + TargetNodeID: a.Id, + SourceLabel: schema.Role, + TargetLabel: schema.Role, + RelationshipType: schema.CanAssumeCrossAccount, + }) + } + // // make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal + // relationships = append(relationships, schema.Relationship{ + // SourceNodeID: a.Id, + // TargetNodeID: PermissionsRow.Arn, + // SourceLabel: schema.Role, + // TargetLabel: schema.Principal, + // RelationshipType: schema.CanBeAssumedByTest, + // }) + + } + } + } + + } + } + } + + } + + for _, TrustedService := range a.TrustedServices { + // make relationship from trusted service to this role of type can assume + relationships = append(relationships, schema.Relationship{ + SourceNodeID: TrustedService.TrustedService, + TargetNodeID: a.Id, + SourceLabel: schema.Service, + TargetLabel: schema.Role, + RelationshipType: schema.IsTrustedBy, + }) + // make relationship from this role to trusted service of type can be assumed by + relationships = append(relationships, schema.Relationship{ + SourceNodeID: a.Id, + TargetNodeID: TrustedService.TrustedService, + SourceLabel: schema.Role, + TargetLabel: schema.Service, + RelationshipType: schema.Trusts, + }) + } + + for _, TrustedFederatedProvider := range a.TrustedFederatedProviders { + // make relationship from trusted federated provider to this role of type can assume + relationships = append(relationships, schema.Relationship{ + SourceNodeID: TrustedFederatedProvider.TrustedFederatedProvider, + TargetNodeID: a.Id, + SourceLabel: schema.FederatedIdentity, + TargetLabel: schema.Role, + RelationshipType: schema.CanAssume, + }) + // make relationship from this role to trusted federated provider of type can be assumed by + relationships = append(relationships, schema.Relationship{ + SourceNodeID: a.Id, + TargetNodeID: TrustedFederatedProvider.TrustedFederatedProvider, + SourceLabel: schema.Role, + TargetLabel: schema.FederatedIdentity, + RelationshipType: schema.CanBeAssumedBy, + }) + // make relationship from trusted federated provider to this role of type can assume + relationships = append(relationships, schema.Relationship{ + SourceNodeID: TrustedFederatedProvider.TrustedFederatedProvider, + TargetNodeID: a.Id, + SourceLabel: schema.FederatedIdentity, + TargetLabel: schema.Role, + RelationshipType: schema.IsTrustedBy, + }) + // make relationship from this role to trusted federated provider of type can be assumed by + relationships = append(relationships, schema.Relationship{ + SourceNodeID: a.Id, + TargetNodeID: TrustedFederatedProvider.TrustedFederatedProvider, + SourceLabel: schema.Role, + TargetLabel: schema.FederatedIdentity, + RelationshipType: schema.Trusts, + }) + } + + return relationships } diff --git a/aws/graph/ingester/schema/schema.go b/aws/graph/ingester/schema/schema.go index e7bb569..2e85075 100644 --- a/aws/graph/ingester/schema/schema.go +++ b/aws/graph/ingester/schema/schema.go @@ -1,6 +1,9 @@ package schema import ( + "fmt" + "reflect" + "github.com/goccy/go-json" "golang.org/x/exp/slices" ) @@ -26,23 +29,29 @@ type Relationship struct { const ( // Relationships - AssociatedTo RelationshipType = "AssociatedTo" - AttachedTo RelationshipType = "AttachedTo" - Authenticates RelationshipType = "Authenticates" - ConnectedTo RelationshipType = "ConnectedTo" - Contains RelationshipType = "Contains" - Exposes RelationshipType = "Exposes" - HasAccess RelationshipType = "HasAccess" - HasConfig RelationshipType = "HasConfig" - HasDisk RelationshipType = "HasDisk" - HasInstance RelationshipType = "HasInstance" - HasRbac RelationshipType = "HasRbac" - HasRole RelationshipType = "HasRole" - Manages RelationshipType = "Manages" - MemberOf RelationshipType = "MemberOf" - Owns RelationshipType = "Owns" - Represents RelationshipType = "Represents" - Trusts RelationshipType = "Trusts" + AssociatedTo RelationshipType = "AssociatedTo" + AttachedTo RelationshipType = "AttachedTo" + Authenticates RelationshipType = "Authenticates" + ConnectedTo RelationshipType = "ConnectedTo" + Contains RelationshipType = "Contains" + Exposes RelationshipType = "Exposes" + HasAccess RelationshipType = "HasAccess" + HasConfig RelationshipType = "HasConfig" + HasDisk RelationshipType = "HasDisk" + HasInstance RelationshipType = "HasInstance" + HasRbac RelationshipType = "HasRbac" + HasRole RelationshipType = "HasRole" + Manages RelationshipType = "Manages" + MemberOf RelationshipType = "MemberOf" + Owns RelationshipType = "Owns" + Represents RelationshipType = "Represents" + Trusts RelationshipType = "Trusts" + IsTrustedBy RelationshipType = "IsTrustedBy" + CanAssume RelationshipType = "CanAssume" + CanAssumeCrossAccount RelationshipType = "CanAssumeCrossAccount" + CanBeAssumedBy RelationshipType = "CanBeAssumedBy" + CanBeAssumedByTest RelationshipType = "CanBeAssumedByTest" + CanAssumeTest RelationshipType = "CanAssumeTest" ) const ( @@ -51,6 +60,7 @@ const ( Account NodeLabel = "Account" Organization NodeLabel = "Org" Service NodeLabel = "Service" + Principal NodeLabel = "Principal" Role NodeLabel = "Role" Group NodeLabel = "Group" User NodeLabel = "User" @@ -96,3 +106,41 @@ func AsNeo4j(object *Node) map[string]interface{} { } return objectMapInterface } + +func ConvertCustomTypesToNeo4j(node interface{}) (map[string]interface{}, error) { + result := make(map[string]interface{}) + val := reflect.ValueOf(node) + + // Handling pointers to structs or interfaces + for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface { + val = val.Elem() + } + + // Check if the value is valid and if it's a struct + if !val.IsValid() || val.Kind() != reflect.Struct { + return nil, fmt.Errorf("invalid input: not a struct or a pointer to a struct") + } + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := val.Type().Field(i) // Get the StructField + + // Check if the field is a struct or slice of structs and not one of the basic types + if (fieldType.Type.Kind() == reflect.Struct || + (fieldType.Type.Kind() == reflect.Slice && fieldType.Type.Elem().Kind() == reflect.Struct)) && + fieldType.Type != reflect.TypeOf([]string{}) && + fieldType.Type != reflect.TypeOf([]int{}) && + fieldType.Type != reflect.TypeOf([]bool{}) { + // Convert complex field to JSON string + jsonStr, err := json.Marshal(field.Interface()) + if err != nil { + return nil, err + } + result[fieldType.Name] = string(jsonStr) + } else { + // Directly use the field for primitive types + result[fieldType.Name] = field.Interface() + } + } + return result, nil +} diff --git a/aws/permissions.go b/aws/permissions.go index b1e7dcd..0793b1f 100644 --- a/aws/permissions.go +++ b/aws/permissions.go @@ -11,6 +11,7 @@ import ( "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/BishopFox/cloudfox/internal/aws/policy" + "github.com/BishopFox/cloudfox/internal/common" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/aws/aws-sdk-go-v2/service/sts" @@ -36,7 +37,7 @@ type IamPermissionsModule struct { Users []GAADUser Roles []GAADRole Groups []GAADGroup - Rows []PermissionsRow + Rows []common.PermissionsRow CommandCounter internal.CommandCounter // Used to store output data for pretty printing output internal.OutputData2 @@ -71,20 +72,6 @@ type GAADGroup struct { InlinePolicies []types.PolicyDetail } -type PermissionsRow struct { - AWSService string - Type string - Name string - Arn string - PolicyType string - PolicyName string - PolicyArn string - Effect string - Action string - Resource string - Condition string -} - func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbosity int, principal string) { // These struct values are used by the output module m.output.Verbosity = verbosity @@ -104,8 +91,8 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbo m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10))) } - m.getGAAD() - m.parsePermissions(principal) + m.GetGAAD() + m.ParsePermissions(principal) m.output.Headers = []string{ "Account", @@ -206,10 +193,10 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbo fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule) } -func (m *IamPermissionsModule) getGAAD() { +func (m *IamPermissionsModule) GetGAAD() { GAAD, err := sdk.CachedIAMGetAccountAuthorizationDetails(m.IAMClient, aws.ToString(m.Caller.Account)) if err != nil { - m.modLog.Error(err.Error()) + TxtLogger.Error(err.Error()) m.CommandCounter.Error++ return } @@ -297,7 +284,7 @@ func (m *IamPermissionsModule) getPrincipalArn(principal string) string { return arn } -func (m *IamPermissionsModule) parsePermissions(principal string) { +func (m *IamPermissionsModule) ParsePermissions(principal string) { var inputArn string for i := range m.Roles { if principal == "" { @@ -435,7 +422,7 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta } m.Rows = append( m.Rows, - PermissionsRow{ + common.PermissionsRow{ AWSService: AWSService, Arn: arn, Name: name, @@ -462,7 +449,7 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta } m.Rows = append( m.Rows, - PermissionsRow{ + common.PermissionsRow{ AWSService: AWSService, Arn: arn, Name: name, @@ -508,7 +495,7 @@ func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inline } m.Rows = append( m.Rows, - PermissionsRow{ + common.PermissionsRow{ AWSService: AWSService, Arn: arn, Name: name, @@ -534,7 +521,7 @@ func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inline } m.Rows = append( m.Rows, - PermissionsRow{ + common.PermissionsRow{ AWSService: AWSService, Arn: arn, Name: name, diff --git a/aws/pmapper.go b/aws/pmapper.go index 34302fd..7eac29d 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -1,6 +1,7 @@ package aws import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -13,9 +14,12 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/dominikbraun/graph" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" "github.com/sirupsen/logrus" ) +var GlobalPmapperEdges []Edge + type PmapperModule struct { // General configuration data Caller sts.GetCallerIdentityOutput @@ -301,3 +305,102 @@ func (m *PmapperModule) readPmapperData(accountID *string) error { return nil } + +func (m *PmapperModule) GenerateCypherStatements(goCtx context.Context, driver neo4j.DriverWithContext) error { + // Insert nodes + for i, node := range m.Nodes { + query, params := m.generateNodeCreateStatement(node, i) + if err := m.executeCypherQuery(goCtx, driver, query, params); err != nil { + return err + } + } + + // Insert edges + for i, edge := range m.Edges { + query, params := m.generateEdgeCreateStatement(edge, i) + if err := m.executeCypherQuery(goCtx, driver, query, params); err != nil { + return err + } + } + + return nil +} + +func (m *PmapperModule) generateNodeCreateStatement(node Node, i int) (string, map[string]interface{}) { + var ptype string + + if strings.Contains(node.Arn, "role") { + ptype = "Role" + } else if strings.Contains(node.Arn, "user") { + ptype = "User" + node.TrustPolicy = "" + } else if strings.Contains(node.Arn, "group") { + ptype = "Group" + } + + query := `MERGE (n:%s {arn: $arn, idValue: $idValue, isAdmin: $isAdmin, name: $name, principalType: $principalType})` + params := map[string]interface{}{ + "arn": node.Arn, + "idValue": node.IDValue, + "isAdmin": node.IsAdmin, + "name": node.Arn, + "principalType": ptype, + } + + return fmt.Sprintf(query, ptype), params +} + +func (m *PmapperModule) generateEdgeCreateStatement(edge Edge, i int) (string, map[string]interface{}) { + // Sanitize ARNs for matching nodes + srcArnSanitized := sanitizeArnForNeo4jLabel(edge.Source) + destArnSanitized := sanitizeArnForNeo4jLabel(edge.Destination) + + query := `MATCH (a {arn: $srcArn}), (b {arn: $destArn}) CREATE (a)-[:CAN_ACCESS {reason: $reason, shortReason: $shortReason}]->(b)` + params := map[string]interface{}{ + "srcArn": srcArnSanitized, + "destArn": destArnSanitized, + "reason": edge.Reason, + "shortReason": edge.ShortReason, + } + + return query, params +} + +func (m *PmapperModule) executeCypherQuery(ctx context.Context, driver neo4j.DriverWithContext, query string, params map[string]interface{}) error { + _, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + if err != nil { + sharedLogger.Errorf("Error executing query: %s -- %v", err, params) + return err + } + return nil +} + +func sanitizeArnForNeo4jLabel(arn string) string { + // Replace non-allowed characters with underscores or other allowed characters + sanitized := strings.ReplaceAll(arn, ":", "_") + sanitized = strings.ReplaceAll(sanitized, "-", "_") + // Add more replacements if needed + return sanitized +} + +// func GetRelationshipsForRole(roleArn string) []schema.Relationship { +// var relationships []schema.Relationship +// if strings.Contains(node.Arn, "role") { +// ptype = "Role" +// } else if strings.Contains(node.Arn, "user") { +// ptype = "User" +// node.TrustPolicy = "" +// } else if strings.Contains(node.Arn, "group") { +// ptype = "Group" +// } +// for _, edge := range m.Edges { +// if edge.Source == roleArn { +// relationships = append(relationships, schema.Relationship{ +// Source: roleArn, +// SourceProperty: "arn", +// Target: edge.Destination, +// TargetProperty: "arn", +// Type: "CAN_ACCESS", +// }) +// } +// } diff --git a/aws/role-trusts.go b/aws/role-trusts.go index ccc2fce..361a788 100644 --- a/aws/role-trusts.go +++ b/aws/role-trusts.go @@ -1,21 +1,17 @@ package aws import ( - "encoding/json" - "errors" "fmt" - "net/url" "path/filepath" - "regexp" "sort" "strconv" "strings" "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" + "github.com/BishopFox/cloudfox/internal/aws/policy" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" - "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/sirupsen/logrus" ) @@ -62,46 +58,12 @@ type RoleTrustRow struct { type AnalyzedRole struct { roleARN *string - trustsDoc trustPolicyDocument + trustsDoc policy.TrustPolicyDocument // trustType string // UNUSED FIELD, PLEASE REVIEW Admin string CanPrivEsc string } -type trustPolicyDocument struct { - Version string `json:"Version"` - Statement []RoleTrustStatementEntry `json:"Statement"` -} - -type RoleTrustStatementEntry struct { - Sid string `json:"Sid"` - Effect string `json:"Effect"` - Principal struct { - AWS ListOfPrincipals `json:"AWS"` - Service ListOfPrincipals `json:"Service"` - Federated ListOfPrincipals `json:"Federated"` - } `json:"Principal"` - Action string `json:"Action"` - Condition struct { - StringEquals struct { - StsExternalID string `json:"sts:ExternalId"` - SAMLAud string `json:"SAML:aud"` - OidcEksSub string `json:"OidcEksSub"` - OidcEksAud string `json:"OidcEksAud"` - CognitoAud string `json:"cognito-identity.amazonaws.com:aud"` - } `json:"StringEquals"` - StringLike struct { - TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"` - TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"` - OidcEksSub string `json:"OidcEksSub"` - OidcEksAud string `json:"OidcEksAud"` - } `json:"StringLike"` - ForAnyValueStringLike struct { - CognitoAMR string `json:"cognito-identity.amazonaws.com:amr"` - } `json:"ForAnyValue:StringLike"` - } `json:"Condition"` -} - func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int) { m.output.Verbosity = verbosity m.output.Directory = outputDirectory @@ -134,6 +96,7 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int } } + o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, @@ -279,7 +242,7 @@ func (m *RoleTrustsModule) printServiceTrusts(outputDirectory string) ([]string, tableCols = strings.Split(m.AWSTableCols, ",") // If the user specified wide as the output format, use these columns. } else if m.AWSOutputType == "wide" { - tableCols = []string{"Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"} + tableCols = []string{"Account", "Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"} // Otherwise, use the default columns for this module (brief) } else { tableCols = []string{"Role Name", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"} @@ -347,7 +310,7 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin tableCols = strings.Split(m.AWSTableCols, ",") // If the user specified wide as the output format, use these columns. } else if m.AWSOutputType == "wide" { - tableCols = []string{"Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"} + tableCols = []string{"Account", "Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"} // Otherwise, use the default columns for this module (brief) } else { tableCols = []string{"Role Name", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"} @@ -387,7 +350,7 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin } -func parseFederatedTrustPolicy(statement RoleTrustStatementEntry) (string, string) { +func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string, string) { var column2, column3 string if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 { column2 = "GitHub Actions" // (" + statement.Principal.Federated[0] + ")" @@ -447,7 +410,7 @@ func (m *RoleTrustsModule) getAllRoleTrusts() { } for _, role := range ListRoles { - trustsdoc, err := parseRoleTrustPolicyDocument(role) + trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ @@ -467,40 +430,3 @@ func (m *RoleTrustsModule) getAllRoleTrusts() { } } - -func parseRoleTrustPolicyDocument(role types.Role) (trustPolicyDocument, error) { - document, _ := url.QueryUnescape(aws.ToString(role.AssumeRolePolicyDocument)) - - // These next six lines are a hack, needed because the EKS OIDC json field name is dynamic - // and therefore can't be used to unmarshall in a predictable way. The hack involves replacing - // the random pattern with a predictable one so that we can add the predictable one in the struct - // used to unmarshall. - pattern := `(\w+)\:` - pattern2 := `".[a-zA-Z0-9\-\.]+/id/` - var reEKSSub = regexp.MustCompile(pattern2 + pattern + "sub") - var reEKSAud = regexp.MustCompile(pattern2 + pattern + "aud") - document = reEKSSub.ReplaceAllString(document, "\"OidcEksSub") - document = reEKSAud.ReplaceAllString(document, "\"OidcEksAud") - - var parsedDocumentToJSON trustPolicyDocument - _ = json.Unmarshal([]byte(document), &parsedDocumentToJSON) - return parsedDocumentToJSON, nil -} - -// A custom unmarshaller is necessary because the list of principals can be an array of strings or a string. -// https://stackoverflow.com/questions/65854778/parsing-arn-from-iam-policy-using-regex -type ListOfPrincipals []string - -func (r *ListOfPrincipals) UnmarshalJSON(b []byte) error { - var s string - if err := json.Unmarshal(b, &s); err == nil { - *r = append(*r, s) - return nil - } - var ss []string - if err := json.Unmarshal(b, &ss); err == nil { - *r = ss - return nil - } - return errors.New("cannot unmarshal neither to a string nor a slice of strings") -} diff --git a/cli/aws.go b/cli/aws.go index 312da6d..5e1ca70 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -10,6 +10,7 @@ import ( "github.com/BishopFox/cloudfox/aws" "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" + "github.com/BishopFox/cloudfox/internal/common" "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigatewayv2" "github.com/aws/aws-sdk-go-v2/service/apprunner" @@ -83,8 +84,9 @@ var ( AWSWrapTable bool AWSUseCache bool - Goroutines int - Verbosity int + Goroutines int + Verbosity int + AWSCommands = &cobra.Command{ Use: "aws", Short: "See \"Available Commands\" for AWS Modules", @@ -877,12 +879,29 @@ func runFilesystemsCommand(cmd *cobra.Command, args []string) { } func runGraphCommand(cmd *cobra.Command, args []string) { + //gaadWg := new(sync.WaitGroup) + for _, profile := range AWSProfiles { + //var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version) + caller, err := internal.AWSWhoami(profile, cmd.Root().Version) + if err != nil { + continue + } + + //instantiate a permissions client and populate the permissions data + fmt.Println("Getting GAAD for " + profile) + PermissionsCommandClient := aws.InitPermissionsClient(*caller, profile, cmd.Root().Version, Goroutines) + PermissionsCommandClient.GetGAAD() + PermissionsCommandClient.ParsePermissions("") + common.PermissionRowsFromAllProfiles = append(common.PermissionRowsFromAllProfiles, PermissionsCommandClient.Rows...) + } + for _, profile := range AWSProfiles { var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version) caller, err := internal.AWSWhoami(profile, cmd.Root().Version) if err != nil { continue } + graphCommandClient := aws.GraphCommand{ Caller: *caller, AWSProfile: profile, @@ -894,6 +913,8 @@ func runGraphCommand(cmd *cobra.Command, args []string) { AWSOutputDirectory: AWSOutputDirectory, Verbosity: Verbosity, AWSConfig: AWSConfig, + Version: cmd.Root().Version, + SkipAdminCheck: AWSSkipAdminCheck, } graphCommandClient.RunGraphCommand() } diff --git a/internal/aws.go b/internal/aws.go index c22d88f..8d28245 100644 --- a/internal/aws.go +++ b/internal/aws.go @@ -12,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/aws/smithy-go/ptr" @@ -32,7 +33,12 @@ func AWSConfigFileLoader(AWSProfile string, version string) aws.Config { 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) - })) + }), config.WithAssumeRoleCredentialOptions(func(options *stscreds.AssumeRoleOptions) { + options.TokenProvider = func() (string, error) { + return "theTokenCode", nil + } + })) + if err != nil { fmt.Println(err) TxtLog.Println(err) diff --git a/internal/aws/policy/role-trust-policies.go b/internal/aws/policy/role-trust-policies.go new file mode 100644 index 0000000..ec3295a --- /dev/null +++ b/internal/aws/policy/role-trust-policies.go @@ -0,0 +1,82 @@ +package policy + +import ( + "encoding/json" + "errors" + "net/url" + "regexp" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam/types" +) + +type TrustPolicyDocument struct { + Version string `json:"Version"` + Statement []RoleTrustStatementEntry `json:"Statement"` +} + +type RoleTrustStatementEntry struct { + Sid string `json:"Sid"` + Effect string `json:"Effect"` + Principal struct { + AWS ListOfPrincipals `json:"AWS"` + Service ListOfPrincipals `json:"Service"` + Federated ListOfPrincipals `json:"Federated"` + } `json:"Principal"` + Action string `json:"Action"` + Condition struct { + StringEquals struct { + StsExternalID string `json:"sts:ExternalId"` + SAMLAud string `json:"SAML:aud"` + OidcEksSub string `json:"OidcEksSub"` + OidcEksAud string `json:"OidcEksAud"` + CognitoAud string `json:"cognito-identity.amazonaws.com:aud"` + } `json:"StringEquals"` + StringLike struct { + TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"` + TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"` + OidcEksSub string `json:"OidcEksSub"` + OidcEksAud string `json:"OidcEksAud"` + } `json:"StringLike"` + ForAnyValueStringLike struct { + CognitoAMR string `json:"cognito-identity.amazonaws.com:amr"` + } `json:"ForAnyValue:StringLike"` + } `json:"Condition"` +} + +// A custom unmarshaller is necessary because the list of principals can be an array of strings or a string. +// https://stackoverflow.com/questions/65854778/parsing-arn-from-iam-policy-using-regex +type ListOfPrincipals []string + +func (r *ListOfPrincipals) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err == nil { + *r = append(*r, s) + return nil + } + var ss []string + if err := json.Unmarshal(b, &ss); err == nil { + *r = ss + return nil + } + return errors.New("cannot unmarshal neither to a string nor a slice of strings") +} + +func ParseRoleTrustPolicyDocument(role types.Role) (TrustPolicyDocument, error) { + document, _ := url.QueryUnescape(aws.ToString(role.AssumeRolePolicyDocument)) + + // These next six lines are a hack, needed because the EKS OIDC json field name is dynamic + // and therefore can't be used to unmarshall in a predictable way. The hack involves replacing + // the random pattern with a predictable one so that we can add the predictable one in the struct + // used to unmarshall. + pattern := `(\w+)\:` + pattern2 := `".[a-zA-Z0-9\-\.]+/id/` + var reEKSSub = regexp.MustCompile(pattern2 + pattern + "sub") + var reEKSAud = regexp.MustCompile(pattern2 + pattern + "aud") + document = reEKSSub.ReplaceAllString(document, "\"OidcEksSub") + document = reEKSAud.ReplaceAllString(document, "\"OidcEksAud") + + var parsedDocumentToJSON TrustPolicyDocument + _ = json.Unmarshal([]byte(document), &parsedDocumentToJSON) + return parsedDocumentToJSON, nil +} diff --git a/internal/common/common.go b/internal/common/common.go new file mode 100644 index 0000000..56c5530 --- /dev/null +++ b/internal/common/common.go @@ -0,0 +1,17 @@ +package common + +type PermissionsRow struct { + AWSService string + Type string + Name string + Arn string + PolicyType string + PolicyName string + PolicyArn string + Effect string + Action string + Resource string + Condition string +} + +var PermissionRowsFromAllProfiles []PermissionsRow diff --git a/internal/output2.go b/internal/output2.go index a0cad52..d29ae60 100644 --- a/internal/output2.go +++ b/internal/output2.go @@ -455,3 +455,15 @@ func adjustBodyForTable(tableHeaders []string, fullHeaders []string, fullBody [] return adjustedBody, selectedHeaders } + +func WriteJsonlFile(file *os.File, data interface{}) error { + bytes, err := json.Marshal(data) + if err != nil { + return err + } + + if _, err := file.Write(append(bytes, "\n"...)); err != nil { + return err + } + return nil +} From c4a276e4be83b36ef1821ab2d39675df755dbe2e Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Mon, 11 Dec 2023 18:24:34 -0500 Subject: [PATCH 03/29] graph/neo4j functionali working - detecting cross account attack paths --- aws/graph.go | 7 +- aws/graph/ingester/ingestor.go | 43 ++++++------- aws/graph/ingester/schema/models/roles.go | 43 +++++++++---- aws/graph/ingester/schema/schema.go | 1 + aws/pmapper.go | 78 +++++++++++++++++------ aws/sdk/glue.go | 1 + cli/aws.go | 1 - internal/cache.go | 2 +- 8 files changed, 118 insertions(+), 58 deletions(-) diff --git a/aws/graph.go b/aws/graph.go index d42eafb..2a9028f 100644 --- a/aws/graph.go +++ b/aws/graph.go @@ -294,6 +294,7 @@ func (m *GraphCommand) collectRoleDataForGraph() []models.Role { for _, service := range statement.Principal.Service { TrustedServices = append(TrustedServices, models.TrustedService{ TrustedService: service, + AccountID: accountId, //IsAdmin: false, //CanPrivEscToAdmin: false, }) @@ -352,10 +353,10 @@ func (m *GraphCommand) collectRoleDataForGraph() []models.Role { //create new object of type models.Role role := models.Role{ - Id: aws.ToString(role.RoleId), + Id: aws.ToString(role.Arn), AccountID: accountId, - RoleARN: aws.ToString(role.Arn), - RoleName: aws.ToString(role.RoleName), + ARN: aws.ToString(role.Arn), + Name: aws.ToString(role.RoleName), TrustsDoc: trustsdoc, TrustedPrincipals: TrustedPrincipals, TrustedServices: TrustedServices, diff --git a/aws/graph/ingester/ingestor.go b/aws/graph/ingester/ingestor.go index e8c7dcd..5fb932a 100644 --- a/aws/graph/ingester/ingestor.go +++ b/aws/graph/ingester/ingestor.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "encoding/json" - "fmt" "os" "path/filepath" "strings" @@ -18,7 +17,7 @@ import ( const ( // Neo4j - MergeNodeQueryTemplate = `CALL apoc.merge.node([$labels[0]], {id: $id}, $properties, $properties) YIELD node as obj + MergeNodeQueryTemplate = `CALL apoc.merge.node([$labels[0]], {Id: $Id}, $properties, $properties) YIELD node as obj CALL apoc.create.setLabels(obj, $labels) YIELD node as labeledObj RETURN labeledObj` @@ -30,11 +29,11 @@ const ( // Using sprintf to insert the label name since the driver doesn't support parameters for labels here // %[1]s is a nice way to say "insert the first parameter here" - CreateConstraintQueryTemplate = "CREATE CONSTRAINT IF NOT EXISTS FOR (n: %s) REQUIRE n.id IS UNIQUE" - CreateIndexQueryTemplate = "CREATE INDEX %[1]s_id IF NOT EXISTS FOR (n: %[1]s) ON (n.id)" + CreateConstraintQueryTemplate = "CREATE CONSTRAINT IF NOT EXISTS FOR (n: %s) REQUIRE n.Id IS UNIQUE" + CreateIndexQueryTemplate = "CREATE INDEX %[1]s_Id IF NOT EXISTS FOR (n: %[1]s) ON (n.Id)" PostProcessMergeQueryTemplate = `MATCH (n) - WITH n.id AS id, COLLECT(n) AS nodesToMerge + WITH n.Id AS Id, COLLECT(n) AS nodesToMerge WHERE size(nodesToMerge) > 1 CALL apoc.refactor.mergeNodes(nodesToMerge, {properties: 'combine', mergeRels:true}) YIELD node @@ -112,10 +111,10 @@ func (i *CloudFoxIngestor) ProcessFile(path string, info os.FileInfo) error { log.Infof("Processing file: %s", info.Name()) switch info.Name() { - // case "accounts.jsonl": - // return i.ProcessFileObjects(path, schema.Account, schema.Account) - // case "roles.jsonl": - // return i.ProcessFileObjects(path, schema.Role, schema.Role) + case "accounts.jsonl": + return i.ProcessFileObjects(path, schema.Account, schema.Account) + case "roles.jsonl": + return i.ProcessFileObjects(path, schema.Role, schema.Role) // case "servicePrincipals.jsonl": // return i.ProcessFileObjects(path, schema.GraphServicePrincipal, schema.GraphObject) // case "applications.jsonl": @@ -184,7 +183,7 @@ func (i *CloudFoxIngestor) InsertDBObjects(object schema.Node, relationships []s //nodeMap := schema.AsNeo4j(&object) nodeQueryParams := map[string]interface{}{ - "id": nodeMap["Id"], + "Id": nodeMap["Id"], "labels": labels, "properties": nodeMap, } @@ -204,10 +203,10 @@ func (i *CloudFoxIngestor) InsertDBObjects(object schema.Node, relationships []s var currentRelationship map[string]interface{} if relationship.SourceProperty == "" { - relationship.SourceProperty = "id" + relationship.SourceProperty = "Id" } if relationship.TargetProperty == "" { - relationship.TargetProperty = "id" + relationship.TargetProperty = "Id" } relationshipBytes, err := json.Marshal(relationship) if err != nil { @@ -241,16 +240,16 @@ func (i *CloudFoxIngestor) Run(graphOutputDir string) error { // Get the label to model map // Create constraints and indexes - log.Info("Creating constraints and indexes for labels") - for label := range models.NodeLabelToNodeMap { - for _, query := range []string{CreateConstraintQueryTemplate, CreateIndexQueryTemplate} { - _, err := neo4j.ExecuteQuery(goCtx, i.Driver, fmt.Sprintf(query, label), nil, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) - if err != nil { - log.Error(err) - continue - } - } - } + // log.Info("Creating constraints and indexes for labels") + // for label := range models.NodeLabelToNodeMap { + // for _, query := range []string{CreateConstraintQueryTemplate, CreateIndexQueryTemplate} { + // _, err := neo4j.ExecuteQuery(goCtx, i.Driver, fmt.Sprintf(query, label), nil, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase("neo4j")) + // if err != nil { + // log.Error(err) + // continue + // } + // } + // } // Process the files in the output directory fileWg := new(sync.WaitGroup) diff --git a/aws/graph/ingester/schema/models/roles.go b/aws/graph/ingester/schema/models/roles.go index 7290916..95ac919 100644 --- a/aws/graph/ingester/schema/models/roles.go +++ b/aws/graph/ingester/schema/models/roles.go @@ -11,23 +11,29 @@ import ( type User struct { Id string - UserArn string - UserName string + ARN string + Name string IsAdmin string CanPrivEscToAdmin string + IdValue string + IsAdminP bool + PathToAdmin bool } type Role struct { Id string AccountID string - RoleARN string - RoleName string + ARN string + Name string TrustsDoc policy.TrustPolicyDocument TrustedPrincipals []TrustedPrincipal TrustedServices []TrustedService TrustedFederatedProviders []TrustedFederatedProvider CanPrivEscToAdmin string IsAdmin string + IdValue string + IsAdminP bool + PathToAdmin bool } type TrustedPrincipal struct { @@ -39,6 +45,7 @@ type TrustedPrincipal struct { type TrustedService struct { TrustedService string + AccountID string //IsAdmin bool //CanPrivEscToAdmin bool } @@ -57,10 +64,10 @@ func (a *Role) MakeRelationships() []schema.Relationship { // get thisAccount id from role arn var thisAccount string - if len(a.RoleARN) >= 25 { - thisAccount = a.RoleARN[13:25] + if len(a.ARN) >= 25 { + thisAccount = a.ARN[13:25] } else { - fmt.Sprintf("Could not get account number from this role arn%s", a.RoleARN) + fmt.Sprintf("Could not get account number from this role arn%s", a.ARN) } // make a relationship between each role and the account it belongs to @@ -149,7 +156,7 @@ func (a *Role) MakeRelationships() []schema.Relationship { // lets only focus on rows that have an effect of Allow if strings.EqualFold(PermissionsRow.Effect, "Allow") { // if the resource is * or the resource is this role arn, then this principal can assume this role - if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.RoleARN) { + if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) { // make a CAN_ASSUME relationship between the trusted principal and this role //evalutate if the princiapl is a user or a role and set a variable accordingly //var principalType schema.NodeLabel @@ -211,7 +218,7 @@ func (a *Role) MakeRelationships() []schema.Relationship { // lets only focus on rows that have an effect of Allow if strings.EqualFold(PermissionsRow.Effect, "Allow") { // if the resource is * or the resource is this role arn, then this principal can assume this role - if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.RoleARN) { + if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) { // make a CAN_ASSUME relationship between the trusted principal and this role if strings.EqualFold(PermissionsRow.Type, "User") { @@ -222,6 +229,13 @@ func (a *Role) MakeRelationships() []schema.Relationship { TargetLabel: schema.Role, RelationshipType: schema.CanAssumeCrossAccount, }) + relationships = append(relationships, schema.Relationship{ + SourceNodeID: PermissionsRow.Arn, + TargetNodeID: a.Id, + SourceLabel: schema.User, + TargetLabel: schema.Role, + RelationshipType: schema.CanAccess, + }) } else if strings.EqualFold(PermissionsRow.Type, "Role") { relationships = append(relationships, schema.Relationship{ SourceNodeID: PermissionsRow.Arn, @@ -230,6 +244,13 @@ func (a *Role) MakeRelationships() []schema.Relationship { TargetLabel: schema.Role, RelationshipType: schema.CanAssumeCrossAccount, }) + relationships = append(relationships, schema.Relationship{ + SourceNodeID: PermissionsRow.Arn, + TargetNodeID: a.Id, + SourceLabel: schema.Role, + TargetLabel: schema.Role, + RelationshipType: schema.CanAccess, + }) } // // make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal // relationships = append(relationships, schema.Relationship{ @@ -253,7 +274,7 @@ func (a *Role) MakeRelationships() []schema.Relationship { for _, TrustedService := range a.TrustedServices { // make relationship from trusted service to this role of type can assume relationships = append(relationships, schema.Relationship{ - SourceNodeID: TrustedService.TrustedService, + SourceNodeID: TrustedService.TrustedService + "_" + TrustedService.AccountID, TargetNodeID: a.Id, SourceLabel: schema.Service, TargetLabel: schema.Role, @@ -262,7 +283,7 @@ func (a *Role) MakeRelationships() []schema.Relationship { // make relationship from this role to trusted service of type can be assumed by relationships = append(relationships, schema.Relationship{ SourceNodeID: a.Id, - TargetNodeID: TrustedService.TrustedService, + TargetNodeID: TrustedService.TrustedService + "_" + TrustedService.AccountID, SourceLabel: schema.Role, TargetLabel: schema.Service, RelationshipType: schema.Trusts, diff --git a/aws/graph/ingester/schema/schema.go b/aws/graph/ingester/schema/schema.go index 2e85075..294c3af 100644 --- a/aws/graph/ingester/schema/schema.go +++ b/aws/graph/ingester/schema/schema.go @@ -52,6 +52,7 @@ const ( CanBeAssumedBy RelationshipType = "CanBeAssumedBy" CanBeAssumedByTest RelationshipType = "CanBeAssumedByTest" CanAssumeTest RelationshipType = "CanAssumeTest" + CanAccess RelationshipType = "CAN_ACCESS" ) const ( diff --git a/aws/pmapper.go b/aws/pmapper.go index 7eac29d..26f9d1d 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -269,8 +269,25 @@ func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool { if p != "" { if startNode.Arn != destNode.Arn { // if we got here there is a path - //fmt.Printf("%s has a path %s who is an admin.\n", startNode.Arn, destNode.Arn) - //fmt.Println(path) + fmt.Printf("%s has a path %s who is an admin.\n", startNode.Arn, destNode.Arn) + fmt.Println(path) + // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen + for i := 0; i < len(path)-1; i++ { + for _, edge := range m.Edges { + if edge.Source == path[i] && edge.Destination == path[i+1] { + // print it like this: [start node] [reason] [end node] + fmt.Printf(" %s %s %s\n", path[i], edge.Reason, path[i+1]) + } + // shortest path only finds the shortest path. We want to find all paths. So we need to find all paths that have the same start and end nodes from the path, but going back to the main edges slice + for _, edge := range GlobalPmapperEdges { + if edge.Source == path[i] && edge.Destination == path[i+1] { + // print it like this: [start node] [reason] [end node] + fmt.Printf(" %s %s %s\n", path[i], edge.Reason, path[i+1]) + } + } + } + } + return true } @@ -327,42 +344,63 @@ func (m *PmapperModule) GenerateCypherStatements(goCtx context.Context, driver n } func (m *PmapperModule) generateNodeCreateStatement(node Node, i int) (string, map[string]interface{}) { - var ptype string + var ptype, label, query string + var params map[string]any if strings.Contains(node.Arn, "role") { + label = GetResourceNameFromArn(node.Arn) ptype = "Role" + params = map[string]any{ + "Id": node.Arn, + "ARN": node.Arn, + "Name": GetResourceNameFromArn(node.Arn), + "IdValue": node.IDValue, + "IsAdminP": node.IsAdmin, + "PathToAdmin": node.PathToAdmin, + } + } else if strings.Contains(node.Arn, "user") { + label = GetResourceNameFromArn(node.Arn) ptype = "User" - node.TrustPolicy = "" + //node.TrustPolicy = "" + params = map[string]any{ + "Id": node.Arn, + "ARN": node.Arn, + "Name": GetResourceNameFromArn(node.Arn), + "IdValue": node.IDValue, + "IsAdminP": node.IsAdmin, + "PathToAdmin": node.PathToAdmin, + } + } else if strings.Contains(node.Arn, "group") { + label = GetResourceNameFromArn(node.Arn) ptype = "Group" } + label = strings.ReplaceAll(label, "-", "_") + label = strings.ReplaceAll(label, ".", "_") - query := `MERGE (n:%s {arn: $arn, idValue: $idValue, isAdmin: $isAdmin, name: $name, principalType: $principalType})` - params := map[string]interface{}{ - "arn": node.Arn, - "idValue": node.IDValue, - "isAdmin": node.IsAdmin, - "name": node.Arn, - "principalType": ptype, - } + query = `MERGE (%s:%s {Id: $Id, ARN: $ARN, Name: $Name, IdValue: $IdValue, IsAdminP: $IsAdminP, PathToAdmin: $PathToAdmin})` - return fmt.Sprintf(query, ptype), params + //sanitizedArn := sanitizeArnForNeo4jLabel(node.Arn) + //id := fmt.Sprintf("%s_%s", sanitizedArn, ptype) + + fmt.Println(fmt.Sprintf(query, label, ptype), params) + return fmt.Sprintf(query, label, ptype), params } func (m *PmapperModule) generateEdgeCreateStatement(edge Edge, i int) (string, map[string]interface{}) { // Sanitize ARNs for matching nodes - srcArnSanitized := sanitizeArnForNeo4jLabel(edge.Source) - destArnSanitized := sanitizeArnForNeo4jLabel(edge.Destination) + //srcArnSanitized := sanitizeArnForNeo4jLabel(edge.Source) + //destArnSanitized := sanitizeArnForNeo4jLabel(edge.Destination) - query := `MATCH (a {arn: $srcArn}), (b {arn: $destArn}) CREATE (a)-[:CAN_ACCESS {reason: $reason, shortReason: $shortReason}]->(b)` - params := map[string]interface{}{ - "srcArn": srcArnSanitized, - "destArn": destArnSanitized, + query := `MATCH (a {ARN: $srcArn}), (b {ARN: $destArn}) CREATE (a)-[:CAN_ACCESS {reason: $reason, shortReason: $shortReason}]->(b)` + params := map[string]any{ + "srcArn": edge.Source, + "destArn": edge.Destination, "reason": edge.Reason, "shortReason": edge.ShortReason, } - + fmt.Println(query, params) return query, params } diff --git a/aws/sdk/glue.go b/aws/sdk/glue.go index aca4c44..386ba6a 100644 --- a/aws/sdk/glue.go +++ b/aws/sdk/glue.go @@ -27,6 +27,7 @@ func init() { gob.Register(glueTypes.Job{}) gob.Register([]glueTypes.Table{}) gob.Register([]glueTypes.Database{}) + gob.Register([]policy.Policy{}) } func CachedGlueListDevEndpoints(GlueClient AWSGlueClientInterface, accountID string, region string) ([]string, error) { diff --git a/cli/aws.go b/cli/aws.go index 5e1ca70..7aa4375 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -879,7 +879,6 @@ func runFilesystemsCommand(cmd *cobra.Command, args []string) { } func runGraphCommand(cmd *cobra.Command, args []string) { - //gaadWg := new(sync.WaitGroup) for _, profile := range AWSProfiles { //var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version) caller, err := internal.AWSWhoami(profile, cmd.Root().Version) diff --git a/internal/cache.go b/internal/cache.go index 75c2e97..adb0dbc 100644 --- a/internal/cache.go +++ b/internal/cache.go @@ -159,7 +159,7 @@ func LoadCacheFromGobFiles(directory string) error { err = decoder.Decode(&entry) if err != nil { sharedLogger.Errorf("Could not decode the following file: %s", filename) - return err + continue } // the key should remove the directory and the .json suffix from the filename and also trim the first slash From 66ffc574381447cedc740a39339a021ad5bf1c3d Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Tue, 19 Dec 2023 16:15:13 -0500 Subject: [PATCH 04/29] go mod tidy --- go.mod | 3 ++- go.sum | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2510798..efa6336 100644 --- a/go.mod +++ b/go.mod @@ -79,7 +79,7 @@ require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.14 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.13.43 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.43 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 // indirect @@ -127,4 +127,5 @@ require ( golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect + gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index e7394d8..28588d2 100644 --- a/go.sum +++ b/go.sum @@ -338,6 +338,7 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= @@ -357,6 +358,7 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/neo4j/neo4j-go-driver/v5 v5.14.0 h1:5x3vD4HkXQIktlG63jSG8v9iweGjmObIPU7Y9U0ThUI= github.com/neo4j/neo4j-go-driver/v5 v5.14.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -722,11 +724,13 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From f5437b7afbc190ab119c8368a82e15abcfd9adf3 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Wed, 17 Jan 2024 09:03:48 -0500 Subject: [PATCH 05/29] Started to add knownvendoraccounts info --- aws/graph.go | 14 ++++++++++++++ aws/graph/ingester/schema/models/roles.go | 10 ++++++++++ aws/graph/ingester/schema/schema.go | 1 + 3 files changed, 25 insertions(+) diff --git a/aws/graph.go b/aws/graph.go index 2a9028f..0ec1789 100644 --- a/aws/graph.go +++ b/aws/graph.go @@ -13,6 +13,7 @@ import ( "github.com/BishopFox/cloudfox/internal/aws/policy" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/bishopfox/knownawsaccountslookup" "github.com/sirupsen/logrus" ) @@ -35,6 +36,8 @@ type GraphCommand struct { pmapperMod PmapperModule pmapperError error + vendors *knownawsaccountslookup.Vendors + // Main module data // Used to store output data for pretty printing output internal.OutputData2 @@ -55,6 +58,9 @@ func (m *GraphCommand) RunGraphCommand() { m.AWSProfile = internal.BuildAWSPath(m.Caller) } + m.vendors = knownawsaccountslookup.NewVendorMap() + m.vendors.PopulateKnownAWSAccounts() + m.modLog.Info("Collecting data for graph ingestor...") m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) @@ -280,12 +286,20 @@ func (m *GraphCommand) collectRoleDataForGraph() []models.Role { //var TrustedFederatedSubjects string var trustedProvider string var trustedSubjects string + var vendorName string for _, statement := range trustsdoc.Statement { for _, principal := range statement.Principal.AWS { + if strings.Contains(principal, ":root") { + //check to see if the accountID is known + accountID := strings.Split(principal, ":")[4] + vendorName = m.vendors.GetVendorNameFromAccountID(accountID) + } + TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{ TrustedPrincipal: principal, ExternalID: statement.Condition.StringEquals.StsExternalID, + VendorName: vendorName, //IsAdmin: false, //CanPrivEscToAdmin: false, }) diff --git a/aws/graph/ingester/schema/models/roles.go b/aws/graph/ingester/schema/models/roles.go index 95ac919..ee6afaa 100644 --- a/aws/graph/ingester/schema/models/roles.go +++ b/aws/graph/ingester/schema/models/roles.go @@ -39,6 +39,7 @@ type Role struct { type TrustedPrincipal struct { TrustedPrincipal string ExternalID string + VendorName string //IsAdmin bool //CanPrivEscToAdmin bool } @@ -267,6 +268,15 @@ func (a *Role) MakeRelationships() []schema.Relationship { } } + // If the role trusts :root in another account and the trusted principal is a vendor, we will make a relationship between our role and a vendor node instead of a principal node + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" { + relationships = append(relationships, schema.Relationship{ + SourceNodeID: TrustedPrincipal.TrustedPrincipal, + TargetNodeID: a.Id, + SourceLabel: schema.Vendor, + TargetLabel: schema.Role, + RelationshipType: schema.CanAssume, + }) } } diff --git a/aws/graph/ingester/schema/schema.go b/aws/graph/ingester/schema/schema.go index 294c3af..4de803d 100644 --- a/aws/graph/ingester/schema/schema.go +++ b/aws/graph/ingester/schema/schema.go @@ -62,6 +62,7 @@ const ( Organization NodeLabel = "Org" Service NodeLabel = "Service" Principal NodeLabel = "Principal" + Vendor NodeLabel = "Vendor" Role NodeLabel = "Role" Group NodeLabel = "Group" User NodeLabel = "User" From 4cf58a3479ab912faf28e60813e06ffe8542f8e5 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Thu, 18 Jan 2024 10:06:41 -0500 Subject: [PATCH 06/29] Created loot file for pmapper --- aws/pmapper.go | 86 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/aws/pmapper.go b/aws/pmapper.go index 26f9d1d..e1bd7dc 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -238,6 +238,9 @@ func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int) Table: internal.TableClient{ Wrap: m.WrapTable, }, + Loot: internal.LootClient{ + DirectoryName: m.output.FilePath, + }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ Header: m.output.Headers, @@ -247,7 +250,12 @@ func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int) }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - o.WriteFullOutput(o.Table.TableFiles, nil) + loot := m.writeLoot(o.Table.DirectoryName, verbosity) + o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{ + Name: m.output.CallingModule, + Contents: loot, + }) + o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles) //m.writeLoot(o.Table.DirectoryName, verbosity) fmt.Printf("[%s][%s] %s principals who are admin or have a path to admin identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body))) @@ -268,35 +276,79 @@ func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool { for _, p := range path { if p != "" { if startNode.Arn != destNode.Arn { - // if we got here there is a path - fmt.Printf("%s has a path %s who is an admin.\n", startNode.Arn, destNode.Arn) - fmt.Println(path) - // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen - for i := 0; i < len(path)-1; i++ { - for _, edge := range m.Edges { - if edge.Source == path[i] && edge.Destination == path[i+1] { - // print it like this: [start node] [reason] [end node] - fmt.Printf(" %s %s %s\n", path[i], edge.Reason, path[i+1]) - } - // shortest path only finds the shortest path. We want to find all paths. So we need to find all paths that have the same start and end nodes from the path, but going back to the main edges slice - for _, edge := range GlobalPmapperEdges { + return true + } + + } + } + } + } + } + return false +} + +func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string { + path := filepath.Join(outputDirectory, "loot") + err := os.MkdirAll(path, os.ModePerm) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + panic(err.Error()) + } + f := filepath.Join(path, "pmapper-privesc-paths-enhanced.txt") + + var admins, out string + + for _, startNode := range m.Nodes { + if startNode.IsAdmin { + admins += fmt.Sprintf("ADMIN FOUND: %s\n", startNode.Arn) + } else { + for _, destNode := range m.Nodes { + if destNode.IsAdmin { + path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn) + for _, p := range path { + if p != "" { + if startNode.Arn != destNode.Arn { + // if we got here there is a path + out += fmt.Sprintf("PATH TO ADMIN FOUND\n Start: %s\n End: %s\n Path(s):\n", startNode.Arn, destNode.Arn) + //fmt.Println(path) + // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen + for i := 0; i < len(path)-1; i++ { + for _, edge := range m.Edges { if edge.Source == path[i] && edge.Destination == path[i+1] { // print it like this: [start node] [reason] [end node] - fmt.Printf(" %s %s %s\n", path[i], edge.Reason, path[i+1]) + out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1]) } + // shortest path only finds the shortest path. We want to find all paths. So we need to find all paths that have the same start and end nodes from the path, but going back to the main edges slice + //for _, edge := range GlobalPmapperEdges { + // if edge.Source == path[i] && edge.Destination == path[i+1] { + // // print it like this: [start node] [reason] [end node] + // out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1]) + // } + // } } } + out += fmt.Sprintf("\n") + } - return true } - } } } } } - return false + out = admins + "\n\n" + out + + if verbosity > 2 { + fmt.Println() + fmt.Println("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Beginning of loot file")) + fmt.Print(out) + fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file")) + } + fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f) + return out + } func (m *PmapperModule) readPmapperData(accountID *string) error { From 009321e7cb7ba66a64570cfb786fdaac29d57be3 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:44:43 -0500 Subject: [PATCH 07/29] Updated pmapper output files --- aws/codebuild.go | 2 +- aws/ecs-tasks.go | 2 +- aws/eks.go | 2 +- aws/graph.go | 34 ++++++- aws/graph/ingester/schema/models/roles.go | 11 -- aws/instances.go | 2 +- aws/lambda.go | 2 +- aws/pmapper.go | 117 +++++++++++++++++----- aws/role-trusts.go | 2 +- aws/shared.go | 2 +- aws/workloads.go | 2 +- cli/aws.go | 10 ++ internal/common/common.go | 4 + 13 files changed, 148 insertions(+), 44 deletions(-) diff --git a/aws/codebuild.go b/aws/codebuild.go index 9975745..7b373eb 100644 --- a/aws/codebuild.go +++ b/aws/codebuild.go @@ -64,7 +64,7 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosi } fmt.Printf("[%s][%s] Enumerating CodeBuild projects for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) - m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) wg := new(sync.WaitGroup) diff --git a/aws/ecs-tasks.go b/aws/ecs-tasks.go index 4e78f02..366bc1f 100644 --- a/aws/ecs-tasks.go +++ b/aws/ecs-tasks.go @@ -73,7 +73,7 @@ func (m *ECSTasksModule) ECSTasks(outputDirectory string, verbosity int) { fmt.Printf("[%s][%s] Enumerating ECS tasks in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) // Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { diff --git a/aws/eks.go b/aws/eks.go index 7086ae2..6b50460 100644 --- a/aws/eks.go +++ b/aws/eks.go @@ -72,7 +72,7 @@ func (m *EKSModule) EKS(outputDirectory string, verbosity int) { fmt.Printf("[%s][%s] Enumerating EKS clusters for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) // Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { diff --git a/aws/graph.go b/aws/graph.go index 0ec1789..76feaed 100644 --- a/aws/graph.go +++ b/aws/graph.go @@ -63,8 +63,11 @@ func (m *GraphCommand) RunGraphCommand() { m.modLog.Info("Collecting data for graph ingestor...") - m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + + //////////////// // Accounts + //////////////// accounts := m.collectAccountDataForGraph() // write data to jsonl file for ingestor to read @@ -89,7 +92,36 @@ func (m *GraphCommand) RunGraphCommand() { } } + //////////////// + // Users + //////////////// + + // users := m.collectUserDataForGraph() + // // write data to jsonl file for ingestor to read + // fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "users") + // // create file and directory if it doesnt exist + // if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil { + // m.modLog.Error(err) + // return + // } + + // outputFile, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + // if err != nil { + // m.modLog.Error(err) + // return + // } + // defer outputFile.Close() + + // for _, user := range users { + // if err := internal.WriteJsonlFile(outputFile, user); err != nil { + // m.modLog.Error(err) + // return + // } + // } + + //////////////// // Roles + //////////////// roles := m.collectRoleDataForGraph() // write data to jsonl file for ingestor to read diff --git a/aws/graph/ingester/schema/models/roles.go b/aws/graph/ingester/schema/models/roles.go index ee6afaa..dd0cdbb 100644 --- a/aws/graph/ingester/schema/models/roles.go +++ b/aws/graph/ingester/schema/models/roles.go @@ -9,17 +9,6 @@ import ( "github.com/BishopFox/cloudfox/internal/common" ) -type User struct { - Id string - ARN string - Name string - IsAdmin string - CanPrivEscToAdmin string - IdValue string - IsAdminP bool - PathToAdmin bool -} - type Role struct { Id string AccountID string diff --git a/aws/instances.go b/aws/instances.go index a0ca607..1ea073c 100644 --- a/aws/instances.go +++ b/aws/instances.go @@ -94,7 +94,7 @@ func (m *InstancesModule) Instances(filter string, outputDirectory string, verbo // Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { diff --git a/aws/lambda.go b/aws/lambda.go index b4d1c7d..b6f5aa5 100644 --- a/aws/lambda.go +++ b/aws/lambda.go @@ -75,7 +75,7 @@ func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) { fmt.Printf("[%s][%s] Enumerating lambdas for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { diff --git a/aws/pmapper.go b/aws/pmapper.go index e1bd7dc..c0929d8 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -18,8 +18,6 @@ import ( "github.com/sirupsen/logrus" ) -var GlobalPmapperEdges []Edge - type PmapperModule struct { // General configuration data Caller sts.GetCallerIdentityOutput @@ -41,6 +39,12 @@ type PmapperModule struct { modLog *logrus.Entry } +type PmapperOutputRow struct { + Start string + End string + Paths []string +} + type Edge struct { Source string `json:"source"` Destination string `json:"destination"` @@ -250,6 +254,14 @@ func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int) }) o.PrefixIdentifier = m.AWSProfile o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) + + header, body := m.createPmapperTableData(outputDirectory) + o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ + Header: header, + Body: body, + Name: "pmapper-privesc-paths-enhanced", + }) + loot := m.writeLoot(o.Table.DirectoryName, verbosity) o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{ Name: m.output.CallingModule, @@ -287,6 +299,63 @@ func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool { return false } +func (m *PmapperModule) createPmapperTableData(outputDirectory string) ([]string, [][]string) { + var header []string + var body [][]string + + header = []string{ + "Start", + "End", + "Path(s)", + } + + var paths string + var admins, privescPathsBody [][]string + + for _, startNode := range m.Nodes { + if startNode.IsAdmin { + admins = append(admins, []string{startNode.Arn, "", "ADMIN"}) + + } else { + for _, destNode := range m.Nodes { + if destNode.IsAdmin { + path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn) + // if we have a path, + + if len(path) > 0 { + if startNode.Arn != destNode.Arn { + paths = "" + // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen + for i := 0; i < len(path)-1; i++ { + for _, edge := range m.Edges { + if edge.Source == path[i] && edge.Destination == path[i+1] { + + //Some pmapper reasons have commas in them so lets get rid of them in the csvOutputdata + edge.Reason = strings.ReplaceAll(edge.Reason, ",", " and") + paths += fmt.Sprintf("%s %s %s\n", path[i], edge.Reason, path[i+1]) + } + + } + } + //trim the last newline from csvPaths + paths = strings.TrimSuffix(paths, "\n") + privescPathsBody = append(privescPathsBody, []string{startNode.Arn, destNode.Arn, paths}) + + } + + } + } + } + } + } + + // create body by first adding the admins and then the privesc paths + body = append(body, admins...) + body = append(body, privescPathsBody...) + return header, body + +} + func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string { path := filepath.Join(outputDirectory, "loot") err := os.MkdirAll(path, os.ModePerm) @@ -306,33 +375,33 @@ func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string for _, destNode := range m.Nodes { if destNode.IsAdmin { path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn) - for _, p := range path { - if p != "" { - if startNode.Arn != destNode.Arn { - // if we got here there is a path - out += fmt.Sprintf("PATH TO ADMIN FOUND\n Start: %s\n End: %s\n Path(s):\n", startNode.Arn, destNode.Arn) - //fmt.Println(path) - // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen - for i := 0; i < len(path)-1; i++ { - for _, edge := range m.Edges { - if edge.Source == path[i] && edge.Destination == path[i+1] { - // print it like this: [start node] [reason] [end node] - out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1]) - } - // shortest path only finds the shortest path. We want to find all paths. So we need to find all paths that have the same start and end nodes from the path, but going back to the main edges slice - //for _, edge := range GlobalPmapperEdges { - // if edge.Source == path[i] && edge.Destination == path[i+1] { - // // print it like this: [start node] [reason] [end node] - // out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1]) - // } - // } + // if we have a path, + + if len(path) > 0 { + if startNode.Arn != destNode.Arn { + // if we got here there is a path + out += fmt.Sprintf("PATH TO ADMIN FOUND\n Start: %s\n End: %s\n Path(s):\n", startNode.Arn, destNode.Arn) + //fmt.Println(path) + // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen + for i := 0; i < len(path)-1; i++ { + for _, edge := range m.Edges { + if edge.Source == path[i] && edge.Destination == path[i+1] { + // print it like this: [start node] [reason] [end node] + out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1]) } + // shortest path only finds the shortest path. We want to find all paths. So we need to find all paths that have the same start and end nodes from the path, but going back to the main edges slice + //for _, edge := range GlobalPmapperEdges { + // if edge.Source == path[i] && edge.Destination == path[i+1] { + // // print it like this: [start node] [reason] [end node] + // out += fmt.Sprintf(" %s %s %s\n", path[i], edge.Reason, path[i+1]) + // } + // } } - out += fmt.Sprintf("\n") - } + out += fmt.Sprintf("\n") } + } } } diff --git a/aws/role-trusts.go b/aws/role-trusts.go index c15be08..dedba08 100644 --- a/aws/role-trusts.go +++ b/aws/role-trusts.go @@ -83,7 +83,7 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int fmt.Printf("[%s][%s] Enumerating role trusts for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) //fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { // fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) diff --git a/aws/shared.go b/aws/shared.go index d0f16f7..d2b207a 100644 --- a/aws/shared.go +++ b/aws/shared.go @@ -77,7 +77,7 @@ func isRoleAdmin(iamSimMod IamSimulatorModule, principal *string) bool { } -func initPmapperGraph(Caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) (PmapperModule, error) { +func InitPmapperGraph(Caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) (PmapperModule, error) { pmapperMod := PmapperModule{ Caller: Caller, AWSProfile: AWSProfile, diff --git a/aws/workloads.go b/aws/workloads.go index 80749c5..ae7c977 100644 --- a/aws/workloads.go +++ b/aws/workloads.go @@ -82,7 +82,7 @@ func (m *WorkloadsModule) PrintWorkloads(outputDirectory string, verbosity int) fmt.Printf("[%s][%s] Enumerating compute workloads in all regions for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) fmt.Printf("[%s][%s] Supported Services: App Runner, EC2, ECS, Lambda \n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) wg := new(sync.WaitGroup) diff --git a/cli/aws.go b/cli/aws.go index 2471c1c..b64212a 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -947,6 +947,16 @@ func runGraphCommand(cmd *cobra.Command, args []string) { common.PermissionRowsFromAllProfiles = append(common.PermissionRowsFromAllProfiles, PermissionsCommandClient.Rows...) } + // for _, profile := range AWSProfiles { + // caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) + // if err != nil { + // continue + // } + + // fmt.PrintLn("Importing Pmapper for " + profile) + + // } + for _, profile := range AWSProfiles { var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) diff --git a/internal/common/common.go b/internal/common/common.go index 56c5530..8b7aab0 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -1,5 +1,7 @@ package common +import "github.com/dominikbraun/graph" + type PermissionsRow struct { AWSService string Type string @@ -15,3 +17,5 @@ type PermissionsRow struct { } var PermissionRowsFromAllProfiles []PermissionsRow + +var GlobalGraph graph.Graph[string, string] From ac1125d47eb379ba5de9261a30d755b6b1b21722 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:46:19 -0500 Subject: [PATCH 08/29] added users model --- aws/graph/ingester/schema/models/users.go | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 aws/graph/ingester/schema/models/users.go diff --git a/aws/graph/ingester/schema/models/users.go b/aws/graph/ingester/schema/models/users.go new file mode 100644 index 0000000..82d5eab --- /dev/null +++ b/aws/graph/ingester/schema/models/users.go @@ -0,0 +1,41 @@ +package models + +import ( + "fmt" + + "github.com/BishopFox/cloudfox/aws/graph/ingester/schema" +) + +type User struct { + Id string + ARN string + Name string + IsAdmin string + CanPrivEscToAdmin string + IdValue string + IsAdminP bool + PathToAdmin bool +} + +func (a *User) MakeRelationships() []schema.Relationship { + // make a relationship between each role and the account it belongs to + relationships := []schema.Relationship{} + + // get thisAccount id from user arn + var thisAccount string + if len(a.ARN) >= 25 { + thisAccount = a.ARN[13:25] + } else { + fmt.Sprintf("Could not get account number from this user arn%s", a.ARN) + } + + relationships = append(relationships, schema.Relationship{ + SourceNodeID: a.Id, + TargetNodeID: thisAccount, + SourceLabel: schema.User, + TargetLabel: schema.Account, + RelationshipType: schema.MemberOf, + Properties: map[string]interface{}{}, + }) + return relationships +} From aeccb6db6de97c2f4cac6a834a30575cf4ca0cb3 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:24:49 -0500 Subject: [PATCH 09/29] Have global data in dom's graph format now. just need to write the table creation code --- aws/graph.go | 205 +++++++++++++++++- aws/graph/ingester/schema/models/roles.go | 248 +++++++++++++++++++++- aws/pmapper.go | 34 ++- cli/aws.go | 143 ++++++++++--- internal/common/common.go | 4 - 5 files changed, 591 insertions(+), 43 deletions(-) diff --git a/aws/graph.go b/aws/graph.go index 76feaed..7b2c61a 100644 --- a/aws/graph.go +++ b/aws/graph.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path/filepath" "strings" ingestor "github.com/BishopFox/cloudfox/aws/graph/ingester" @@ -12,8 +13,10 @@ import ( "github.com/BishopFox/cloudfox/internal" "github.com/BishopFox/cloudfox/internal/aws/policy" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/bishopfox/knownawsaccountslookup" + "github.com/dominikbraun/graph" "github.com/sirupsen/logrus" ) @@ -45,6 +48,28 @@ type GraphCommand struct { modLog *logrus.Entry } +type GraphCommand2 struct { + + // General configuration data + Caller sts.GetCallerIdentityOutput + AWSRegions []string + Goroutines int + AWSProfile string + WrapTable bool + AWSOutputType string + AWSTableCols string + Verbosity int + AWSOutputDirectory string + AWSConfig aws.Config + Version string + SkipAdminCheck bool + + GlobalGraph graph.Graph[string, string] + + output internal.OutputData2 + modLog *logrus.Entry +} + func (m *GraphCommand) RunGraphCommand() { // These struct values are used by the output module @@ -167,6 +192,57 @@ func (m *GraphCommand) RunGraphCommand() { } +func (m *GraphCommand2) RunGraphCommand2() { + + // These struct values are used by the output module + m.output.Verbosity = m.Verbosity + m.output.Directory = m.AWSOutputDirectory + m.output.CallingModule = "graph" + m.modLog = internal.TxtLog.WithFields(logrus.Fields{ + "module": m.output.CallingModule, + }) + if m.AWSProfile == "" { + m.AWSProfile = internal.BuildAWSPath(m.Caller) + } + m.output.FilePath = filepath.Join(m.AWSOutputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) + + m.output.Headers = []string{ + "Account", + "Principal Arn", + "IsAdmin?", + "CanPrivEscToAdmin?", + } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + var tableCols []string + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + // If the user specified wide as the output format, use these columns. + // remove any spaces between any commas and the first letter after the commas + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") + tableCols = strings.Split(m.AWSTableCols, ",") + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Principal Arn", + "IsAdmin?", + "CanPrivEscToAdmin?", + } + // Otherwise, use the default columns. + } else { + tableCols = []string{ + "Principal Arn", + "IsAdmin?", + "CanPrivEscToAdmin?", + } + } + internal.TxtLog.Debug("tableCols: ", tableCols) + +} + func (m *GraphCommand) collectAccountDataForGraph() []models.Account { //OrganizationsCommandClient := InitOrgClient(m.AWSConfig) var accounts []models.Account @@ -257,9 +333,9 @@ func (m *GraphCommand) collectAccountDataForGraph() []models.Account { func (m *GraphCommand) collectRoleDataForGraph() []models.Role { var isAdmin, canPrivEscToAdmin string - iamClient := InitIAMClient(m.AWSConfig) - iamSimClient := InitIamCommandClient(iamClient, m.Caller, m.AWSProfile, m.Goroutines) - localAdminMap := make(map[string]bool) + // iamClient := InitIAMClient(m.AWSConfig) + // iamSimClient := InitIamCommandClient(iamClient, m.Caller, m.AWSProfile, m.Goroutines) + // localAdminMap := make(map[string]bool) var roles []models.Role IAMCommandClient := InitIAMClient(m.AWSConfig) @@ -276,11 +352,11 @@ func (m *GraphCommand) collectRoleDataForGraph() []models.Role { break } - if m.pmapperError == nil { - isAdmin, canPrivEscToAdmin = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, role.Arn) - } else { - isAdmin, canPrivEscToAdmin = GetIamSimResult(m.SkipAdminCheck, role.Arn, iamSimClient, localAdminMap) - } + // if m.pmapperError == nil { + // isAdmin, canPrivEscToAdmin = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, role.Arn) + // } else { + // isAdmin, canPrivEscToAdmin = GetIamSimResult(m.SkipAdminCheck, role.Arn, iamSimClient, localAdminMap) + // } // for _, row := range m.PermissionRowsFromAllProfiles { // if row.Arn == aws.ToString(role.Arn) { @@ -414,3 +490,116 @@ func (m *GraphCommand) collectRoleDataForGraph() []models.Role { } return roles } + +func ConvertIAMRoleToModelRole(role types.Role, vendors *knownawsaccountslookup.Vendors) models.Role { + var isAdmin, canPrivEscToAdmin string + + accountId := strings.Split(aws.ToString(role.Arn), ":")[4] + trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role) + if err != nil { + internal.TxtLog.Error(err.Error()) + return models.Role{} + } + + var TrustedPrincipals []models.TrustedPrincipal + var TrustedServices []models.TrustedService + var TrustedFederatedProviders []models.TrustedFederatedProvider + //var TrustedFederatedSubjects string + var trustedProvider string + var trustedSubjects string + var vendorName string + + for _, statement := range trustsdoc.Statement { + for _, principal := range statement.Principal.AWS { + if strings.Contains(principal, ":root") { + //check to see if the accountID is known + accountID := strings.Split(principal, ":")[4] + vendorName = vendors.GetVendorNameFromAccountID(accountID) + } + + TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{ + TrustedPrincipal: principal, + ExternalID: statement.Condition.StringEquals.StsExternalID, + VendorName: vendorName, + //IsAdmin: false, + //CanPrivEscToAdmin: false, + }) + + } + for _, service := range statement.Principal.Service { + TrustedServices = append(TrustedServices, models.TrustedService{ + TrustedService: service, + AccountID: accountId, + //IsAdmin: false, + //CanPrivEscToAdmin: false, + }) + + } + for _, federated := range statement.Principal.Federated { + if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 { + trustedProvider = "GitHub" + trustedSubjects := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",") + if trustedSubjects == "" { + trustedSubjects = "ALL REPOS!!!" + } else { + trustedSubjects = "Repos: " + trustedSubjects + } + + } else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" { + if strings.Contains(statement.Principal.Federated[0], "AWSSSO") { + trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" + } else if strings.Contains(statement.Principal.Federated[0], "Okta") { + trustedProvider = "Okta" // (" + statement.Principal.Federated[0] + ")" + } + trustedSubjects = "Not applicable" + } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" { + trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" + if statement.Condition.StringEquals.OidcEksSub != "" { + trustedSubjects = statement.Condition.StringEquals.OidcEksSub + } else if statement.Condition.StringLike.OidcEksSub != "" { + trustedSubjects = statement.Condition.StringLike.OidcEksSub + } else { + trustedSubjects = "ALL SERVICE ACCOUNTS!" + } + } else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" { + trustedProvider = "Cognito" // (" + statement.Principal.Federated[0] + ")" + if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" { + trustedSubjects = statement.Condition.ForAnyValueStringLike.CognitoAMR + } + } else { + if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") { + trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" + trustedSubjects = "ALL SERVICE ACCOUNTS!" + } else if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") { + trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" + } + trustedSubjects = "Not applicable" + } + + TrustedFederatedProviders = append(TrustedFederatedProviders, models.TrustedFederatedProvider{ + TrustedFederatedProvider: federated, + ProviderShortName: trustedProvider, + TrustedSubjects: trustedSubjects, + //IsAdmin: false, + //CanPrivEscToAdmin: false, + }) + } + } + + //create new object of type models.Role + cfRole := models.Role{ + Id: aws.ToString(role.Arn), + AccountID: accountId, + ARN: aws.ToString(role.Arn), + Name: aws.ToString(role.RoleName), + TrustsDoc: trustsdoc, + TrustedPrincipals: TrustedPrincipals, + TrustedServices: TrustedServices, + TrustedFederatedProviders: TrustedFederatedProviders, + CanPrivEscToAdmin: canPrivEscToAdmin, + IsAdmin: isAdmin, + } + //roles = append(roles, role) + + return cfRole +} diff --git a/aws/graph/ingester/schema/models/roles.go b/aws/graph/ingester/schema/models/roles.go index dd0cdbb..09ad1f0 100644 --- a/aws/graph/ingester/schema/models/roles.go +++ b/aws/graph/ingester/schema/models/roles.go @@ -7,6 +7,7 @@ import ( "github.com/BishopFox/cloudfox/aws/graph/ingester/schema" "github.com/BishopFox/cloudfox/internal/aws/policy" "github.com/BishopFox/cloudfox/internal/common" + "github.com/dominikbraun/graph" ) type Role struct { @@ -22,7 +23,8 @@ type Role struct { IsAdmin string IdValue string IsAdminP bool - PathToAdmin bool + PathToAdminSameAccount bool + PathToAdminCrossAccount bool } type TrustedPrincipal struct { @@ -113,6 +115,7 @@ func (a *Role) MakeRelationships() []schema.Relationship { TargetLabel: schema.Role, RelationshipType: schema.CanAssume, }) + // make a CAN_BE_ASSUMED_BY relationship between this role and the trusted principal // relationships = append(relationships, schema.Relationship{ // SourceNodeID: a.Id, @@ -326,3 +329,246 @@ func (a *Role) MakeRelationships() []schema.Relationship { return relationships } + +// func (a *Role) MakeVertex() { + +// // make a vertex for this role as populate all of the data in the Role struct as attributes +// err := GlobalGraph.AddVertex( +// a.Id, +// graph.VertexAttribute("Name", a.Name), +// graph.VertexAttribute("Type", "Role"), +// graph.VertexAttribute("AccountID", a.AccountID), +// graph.VertexAttribute("ARN", a.ARN), +// graph.VertexAttribute("CanPrivEscToAdmin", a.CanPrivEscToAdmin), +// graph.VertexAttribute("IsAdmin", a.IsAdmin), +// graph.VertexAttribute("IdValue", a.IdValue), +// graph.VertexAttribute("IsAdminP", a.IsAdminP), +// graph.VertexAttribute("PathToAdminSameAccount", a.PathToAdminSameAccount), +// graph.VertexAttribute("PathToAdminCrossAccount", a.PathToAdminCrossAccount), +// ) + +// } + +func (a *Role) MakeEdges(GlobalGraph graph.Graph[string, string]) []schema.Relationship { + var relationships []schema.Relationship + + // get thisAccount id from role arn + var thisAccount string + if len(a.ARN) >= 25 { + thisAccount = a.ARN[13:25] + } else { + fmt.Sprintf("Could not get account number from this role arn%s", a.ARN) + } + + for _, TrustedPrincipal := range a.TrustedPrincipals { + //get account id from the trusted principal arn + var trustedPrincipalAccount string + if len(TrustedPrincipal.TrustedPrincipal) >= 25 { + trustedPrincipalAccount = TrustedPrincipal.TrustedPrincipal[13:25] + } else { + fmt.Sprintf("Could not get account number from this TrustedPrincipal%s", TrustedPrincipal.TrustedPrincipal) + } + var PermissionsRowAccount string + + // if the role trusts a principal in this same account explicitly, then the principal can assume the role + if thisAccount == trustedPrincipalAccount { + // make a CAN_ASSUME relationship between the trusted principal and this role + + err := GlobalGraph.AddEdge( + TrustedPrincipal.TrustedPrincipal, + a.Id, + graph.EdgeAttribute("AssumeRole", "Same account explicit trust"), + ) + if err != nil { + fmt.Println(err) + fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Id + "Same account explicit trust") + } + } + + // If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role + // if the role we are looking at trusts root in it's own account + + if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) { + err := GlobalGraph.AddVertex( + a.Id, + graph.VertexAttribute("Name", a.Name), + graph.VertexAttribute("Type", "Account"), + graph.VertexAttribute("AccountID", a.AccountID), + ) + if err != nil { + if err == graph.ErrVertexAlreadyExists { + fmt.Println(a.Id + " already exists") + } + } + // iterate over all rows in AllPermissionsRows + for _, PermissionsRow := range common.PermissionRowsFromAllProfiles { + // but we only care about the rows that have arns that are in this account + + if len(PermissionsRow.Arn) >= 25 { + PermissionsRowAccount = PermissionsRow.Arn[13:25] + } else { + fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn) + } + + if PermissionsRowAccount == thisAccount { + // lets only look for rows that have sts:AssumeRole permissions + if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || + strings.EqualFold(PermissionsRow.Action, "*") || + strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || + strings.EqualFold(PermissionsRow.Action, "sts:*") { + // lets only focus on rows that have an effect of Allow + if strings.EqualFold(PermissionsRow.Effect, "Allow") { + // if the resource is * or the resource is this role arn, then this principal can assume this role + if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) { + // make a CAN_ASSUME relationship between the trusted principal and this role + //evalutate if the princiapl is a user or a role and set a variable accordingly + //var principalType schema.NodeLabel + if strings.EqualFold(PermissionsRow.Type, "User") { + err := GlobalGraph.AddEdge( + PermissionsRow.Arn, + a.Id, + graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"), + ) + if err != nil { + fmt.Println(err) + fmt.Println(PermissionsRow.Arn + a.Id + "Same account root trust and trusted principal has permission to assume role") + } + } else if strings.EqualFold(PermissionsRow.Type, "Role") { + err := GlobalGraph.AddEdge( + PermissionsRow.Arn, + a.Id, + graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"), + ) + if err != nil { + fmt.Println(err) + fmt.Println(PermissionsRow.Arn + a.Id + "Same account root trust and trusted principal has permission to assume role") + } + } + } + } + } + } + } + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" { + err := GlobalGraph.AddVertex( + a.Id, + graph.VertexAttribute("Name", a.Name), + graph.VertexAttribute("Type", "Account"), + graph.VertexAttribute("AccountID", a.AccountID), + graph.VertexAttribute("VendorName", TrustedPrincipal.VendorName), + ) + if err != nil { + if err == graph.ErrVertexAlreadyExists { + fmt.Println(a.Id + " already exists") + } + } + // If the role trusts :root in another account and the trusted principal is a vendor, we will make a relationship between our role and a vendor node instead of a principal node + relationships = append(relationships, schema.Relationship{ + SourceNodeID: TrustedPrincipal.TrustedPrincipal, + TargetNodeID: a.Id, + SourceLabel: schema.Vendor, + TargetLabel: schema.Role, + RelationshipType: schema.CanAssume, + }) + err = GlobalGraph.AddEdge( + //TrustedPrincipal.TrustedPrincipal, + TrustedPrincipal.VendorName, + a.Id, + graph.EdgeAttribute("VendorAssumeRole", "Cross account root trust and trusted principal is a vendor"), + ) + if err != nil { + fmt.Println(err) + fmt.Println(TrustedPrincipal.VendorName + a.Id + "Cross account root trust and trusted principal is a vendor") + + } + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) { + err := GlobalGraph.AddVertex( + a.Id, + graph.VertexAttribute("Name", a.Name), + graph.VertexAttribute("Type", "Account"), + graph.VertexAttribute("AccountID", a.AccountID), + ) + if err != nil { + if err == graph.ErrVertexAlreadyExists { + fmt.Println(a.Id + " already exists") + } + } + // iterate over all rows in AllPermissionsRows + for _, PermissionsRow := range common.PermissionRowsFromAllProfiles { + // but we only care about the rows that have arns that are in this other account + if len(PermissionsRow.Arn) >= 25 { + PermissionsRowAccount = PermissionsRow.Arn[13:25] + } else { + fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn) + } + if PermissionsRowAccount == trustedPrincipalAccount { + // lets only look for rows that have sts:AssumeRole permissions + if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || + strings.EqualFold(PermissionsRow.Action, "*") || + strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || + strings.EqualFold(PermissionsRow.Action, "sts:*") { + // lets only focus on rows that have an effect of Allow + if strings.EqualFold(PermissionsRow.Effect, "Allow") { + // if the resource is * or the resource is this role arn, then this principal can assume this role + if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) { + // make a CAN_ASSUME relationship between the trusted principal and this role + + if strings.EqualFold(PermissionsRow.Type, "User") { + err := GlobalGraph.AddEdge( + PermissionsRow.Arn, + a.Id, + graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"), + ) + if err != nil { + fmt.Println(err) + fmt.Println(PermissionsRow.Arn + a.Id + "Cross account root trust and trusted principal has permission to assume role") + } + + } else if strings.EqualFold(PermissionsRow.Type, "Role") { + err := GlobalGraph.AddEdge( + PermissionsRow.Arn, + a.Id, + graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"), + ) + if err != nil { + fmt.Println(err) + fmt.Println(PermissionsRow.Arn + a.Id + "Cross account root trust and trusted principal has permission to assume role") + } + } + } + } + } + + } + } + + } + } + // pmapper takes care of this part so commenting out for now - but leaving as a placeholder + + // for _, TrustedService := range a.TrustedServices { + // // make relationship from trusted service to this role of type can assume + // // make relationship from this role to trusted service of type can be assumed by + // } + + for _, TrustedFederatedProvider := range a.TrustedFederatedProviders { + // make relationship from trusted federated provider to this role of type can assume + + GlobalGraph.AddVertex( + TrustedFederatedProvider.TrustedFederatedProvider, + graph.VertexAttribute("Type", "FederatedIdentity"), + ) + err := GlobalGraph.AddEdge( + TrustedFederatedProvider.TrustedFederatedProvider, + a.Id, + graph.EdgeAttribute("FederatedAssumeRole", "Trusted federated provider"), + ) + if err != nil { + fmt.Println(err) + fmt.Println(TrustedFederatedProvider.TrustedFederatedProvider + a.Id + "Trusted federated provider") + } + + } + + return relationships +} diff --git a/aws/pmapper.go b/aws/pmapper.go index c0929d8..d3e4d10 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -115,7 +115,39 @@ func (m *PmapperModule) createAndPopulateGraph() graph.Graph[string, string] { } for _, edge := range m.Edges { - _ = pmapperGraph.AddEdge(edge.Source, edge.Destination) + err := pmapperGraph.AddEdge( + edge.Source, + edge.Destination, + graph.EdgeAttribute(edge.ShortReason, edge.Reason), + ) + if err != nil { + if err == graph.ErrEdgeAlreadyExists { + // update the ege by copying the existing graph.Edge with attributes and add the new attributes + //fmt.Println("Edge already exists, but adding a new one!") + + // get the existing edge + existingEdge, _ := pmapperGraph.Edge(edge.Source, edge.Destination) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } + + // Add or update the attribute + existingProperties.Attributes[edge.ShortReason] = edge.Reason + //Update the edge + pmapperGraph.UpdateEdge( + edge.Source, + edge.Destination, + graph.EdgeAttributes(existingProperties.Attributes), + ) + + } + //fmt.Println(edge.Reason) + } + } //internal.Cache.Set(cacheKey, pmapperGraph, cache.DefaultExpiration) diff --git a/cli/aws.go b/cli/aws.go index eaff261..b4c69f7 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -8,6 +8,7 @@ import ( "path/filepath" "github.com/BishopFox/cloudfox/aws" + "github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models" "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/BishopFox/cloudfox/internal/common" @@ -60,6 +61,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/smithy-go/ptr" + "github.com/bishopfox/knownawsaccountslookup" + "github.com/dominikbraun/graph" "github.com/fatih/color" "github.com/kyokomi/emoji" "github.com/spf13/cobra" @@ -932,54 +935,136 @@ func runFilesystemsCommand(cmd *cobra.Command, args []string) { } func runGraphCommand(cmd *cobra.Command, args []string) { + GlobalGraph := graph.New(graph.StringHash, graph.Directed()) + //var PermissionRowsFromAllProfiles []common.PermissionsRow + var GlobalRoles []models.Role + + vendors := knownawsaccountslookup.NewVendorMap() + vendors.PopulateKnownAWSAccounts() for _, profile := range AWSProfiles { - //var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version) + var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) if err != nil { continue } - //instantiate a permissions client and populate the permissions data + //Gather all Permissions data fmt.Println("Getting GAAD for " + profile) PermissionsCommandClient := aws.InitPermissionsClient(*caller, profile, cmd.Root().Version, Goroutines, AWSMFAToken) PermissionsCommandClient.GetGAAD() PermissionsCommandClient.ParsePermissions("") common.PermissionRowsFromAllProfiles = append(common.PermissionRowsFromAllProfiles, PermissionsCommandClient.Rows...) - } - // for _, profile := range AWSProfiles { - // caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) - // if err != nil { - // continue - // } - - // fmt.PrintLn("Importing Pmapper for " + profile) + // Gather all Pmapper data + fmt.Println("Importing Pmapper for " + profile) + pmapperMod, pmapperError := aws.InitPmapperGraph(*caller, AWSProfile, Goroutines) + if pmapperError != nil { + fmt.Println("Error importing pmapper data: " + pmapperError.Error()) + } + for _, node := range pmapperMod.Nodes { + GlobalGraph.AddVertex(node.Arn) + } + for _, edge := range pmapperMod.Edges { + err := GlobalGraph.AddEdge( + edge.Source, + edge.Destination, + graph.EdgeAttribute(edge.ShortReason, edge.Reason), + ) + if err != nil { + if err == graph.ErrEdgeAlreadyExists { + // update the ege by copying the existing graph.Edge with attributes and add the new attributes + //fmt.Println("Edge already exists") + + // get the existing edge + existingEdge, _ := GlobalGraph.Edge(edge.Source, edge.Destination) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } + + // Add or update the attribute + existingProperties.Attributes[edge.ShortReason] = edge.Reason + GlobalGraph.UpdateEdge( + edge.Source, + edge.Destination, + graph.EdgeAttributes(existingProperties.Attributes), + ) + } + } - // } + } - for _, profile := range AWSProfiles { - var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) - caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) + //Gather all role data + fmt.Println("Getting Roles for " + profile) + IAMCommandClient := aws.InitIAMClient(AWSConfig) + ListRolesOutput, err := sdk.CachedIamListRoles(IAMCommandClient, ptr.ToString(caller.Account)) if err != nil { - continue + internal.TxtLog.Error(err) + } + for _, role := range ListRolesOutput { + modelRole := aws.ConvertIAMRoleToModelRole(role, vendors) + GlobalRoles = append(GlobalRoles, modelRole) + } + //making edges + fmt.Println("Making edges for " + profile) + for _, role := range GlobalRoles { + role.MakeEdges(GlobalGraph) } - graphCommandClient := aws.GraphCommand{ - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, - AWSOutputDirectory: AWSOutputDirectory, - Verbosity: Verbosity, - AWSConfig: AWSConfig, - Version: cmd.Root().Version, - SkipAdminCheck: AWSSkipAdminCheck, + // print how many nodes and edges are in the graph to the screen and exit + + for _, profile := range AWSProfiles { + var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) + caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) + if err != nil { + continue + } + + // graphCommandClient := aws.GraphCommand{ + // Caller: *caller, + // AWSProfile: profile, + // Goroutines: Goroutines, + // AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + // WrapTable: AWSWrapTable, + // AWSOutputType: AWSOutputType, + // AWSTableCols: AWSTableCols, + // AWSOutputDirectory: AWSOutputDirectory, + // Verbosity: Verbosity, + // AWSConfig: AWSConfig, + // Version: cmd.Root().Version, + // SkipAdminCheck: AWSSkipAdminCheck, + // } + // graphCommandClient.RunGraphCommand() + + graphCommandClient := aws.GraphCommand2{ + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + AWSOutputDirectory: AWSOutputDirectory, + Verbosity: Verbosity, + AWSConfig: AWSConfig, + Version: cmd.Root().Version, + SkipAdminCheck: AWSSkipAdminCheck, + GlobalGraph: GlobalGraph, + } + + graphCommandClient.RunGraphCommand2() } - graphCommandClient.RunGraphCommand() + } + // // print the edges to the screen + // edges, _ := GlobalGraph.Edges() + // for _, edge := range edges { + // fmt.Println(edge) + // } + } func runIamSimulatorCommand(cmd *cobra.Command, args []string) { diff --git a/internal/common/common.go b/internal/common/common.go index 8b7aab0..56c5530 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -1,7 +1,5 @@ package common -import "github.com/dominikbraun/graph" - type PermissionsRow struct { AWSService string Type string @@ -17,5 +15,3 @@ type PermissionsRow struct { } var PermissionRowsFromAllProfiles []PermissionsRow - -var GlobalGraph graph.Graph[string, string] From a13527a7a98e63c0f596b0695a58e742149e7585 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:25:11 -0500 Subject: [PATCH 10/29] Added MakeVertices method for type Role --- aws/graph/ingester/schema/models/roles.go | 38 ++++++++++++----------- cli/aws.go | 8 +++++ 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/aws/graph/ingester/schema/models/roles.go b/aws/graph/ingester/schema/models/roles.go index 09ad1f0..455d22e 100644 --- a/aws/graph/ingester/schema/models/roles.go +++ b/aws/graph/ingester/schema/models/roles.go @@ -330,24 +330,26 @@ func (a *Role) MakeRelationships() []schema.Relationship { return relationships } -// func (a *Role) MakeVertex() { - -// // make a vertex for this role as populate all of the data in the Role struct as attributes -// err := GlobalGraph.AddVertex( -// a.Id, -// graph.VertexAttribute("Name", a.Name), -// graph.VertexAttribute("Type", "Role"), -// graph.VertexAttribute("AccountID", a.AccountID), -// graph.VertexAttribute("ARN", a.ARN), -// graph.VertexAttribute("CanPrivEscToAdmin", a.CanPrivEscToAdmin), -// graph.VertexAttribute("IsAdmin", a.IsAdmin), -// graph.VertexAttribute("IdValue", a.IdValue), -// graph.VertexAttribute("IsAdminP", a.IsAdminP), -// graph.VertexAttribute("PathToAdminSameAccount", a.PathToAdminSameAccount), -// graph.VertexAttribute("PathToAdminCrossAccount", a.PathToAdminCrossAccount), -// ) - -// } +func (a *Role) MakeVertices(GlobalGraph graph.Graph[string, string]) { + + // make a vertex for this role as populate all of the data in the Role struct as attributes + err := GlobalGraph.AddVertex( + a.Id, + graph.VertexAttribute("Name", a.Name), + graph.VertexAttribute("Type", "Role"), + graph.VertexAttribute("AccountID", a.AccountID), + graph.VertexAttribute("ARN", a.ARN), + graph.VertexAttribute("CanPrivEscToAdmin", a.CanPrivEscToAdmin), + graph.VertexAttribute("IsAdmin", a.IsAdmin), + graph.VertexAttribute("IdValue", a.IdValue), + ) + if err != nil { + if err == graph.ErrVertexAlreadyExists { + fmt.Println(a.Id + " already exists") + } + } + +} func (a *Role) MakeEdges(GlobalGraph graph.Graph[string, string]) []schema.Relationship { var relationships []schema.Relationship diff --git a/cli/aws.go b/cli/aws.go index b4c69f7..161847b 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -1008,6 +1008,14 @@ func runGraphCommand(cmd *cobra.Command, args []string) { modelRole := aws.ConvertIAMRoleToModelRole(role, vendors) GlobalRoles = append(GlobalRoles, modelRole) } + // make vertices + // you can't update verticies - so we need to make all of the vertices that are roles in the in-scope accounts + // all at once to make sure they have the most information possible + fmt.Println("Making vertices for " + profile) + for _, role := range GlobalRoles { + role.MakeVertices(GlobalGraph) + } + //making edges fmt.Println("Making edges for " + profile) for _, role := range GlobalRoles { From e61a4017338ddaf4acd88f866e2bdaa69b2f6be6 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Wed, 31 Jan 2024 16:47:29 -0500 Subject: [PATCH 11/29] Kept first draft as the graph command. Moved second take to the caper command. --- .gitignore | 6 +- aws/caper.go | 774 ++++++++++++++++++++++ aws/graph.go | 189 ------ aws/graph/ingester/schema/models/roles.go | 71 ++ aws/graph/ingester/schema/models/users.go | 34 +- aws/pmapper.go | 75 ++- cli/aws.go | 356 ++++++---- 7 files changed, 1188 insertions(+), 317 deletions(-) create mode 100644 aws/caper.go diff --git a/.gitignore b/.gitignore index 08bf462..00dd9c9 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,8 @@ cloudfox *.json *.csv *.log -dist/ \ No newline at end of file +dist/ + +# graphvis files +*.gv +*.svg \ No newline at end of file diff --git a/aws/caper.go b/aws/caper.go new file mode 100644 index 0000000..31a71b1 --- /dev/null +++ b/aws/caper.go @@ -0,0 +1,774 @@ +package aws + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/BishopFox/cloudfox/internal" + "github.com/BishopFox/cloudfox/internal/aws/policy" + "github.com/BishopFox/cloudfox/internal/common" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/bishopfox/knownawsaccountslookup" + "github.com/dominikbraun/graph" + "github.com/sirupsen/logrus" +) + +type CaperCommand struct { + + // General configuration data + Caller sts.GetCallerIdentityOutput + AWSRegions []string + Goroutines int + AWSProfile string + WrapTable bool + AWSOutputType string + AWSTableCols string + Verbosity int + AWSOutputDirectory string + AWSConfig aws.Config + Version string + SkipAdminCheck bool + GlobalGraph graph.Graph[string, string] + + output internal.OutputData2 + modLog *logrus.Entry +} + +func (m *CaperCommand) RunCaperCommand() { + + // These struct values are used by the output module + m.output.Verbosity = m.Verbosity + m.output.Directory = m.AWSOutputDirectory + m.output.CallingModule = "caper" + m.modLog = internal.TxtLog.WithFields(logrus.Fields{ + "module": m.output.CallingModule, + }) + if m.AWSProfile == "" { + m.AWSProfile = internal.BuildAWSPath(m.Caller) + } + m.output.FilePath = filepath.Join(m.AWSOutputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) + + o := internal.OutputClient{ + Verbosity: m.Verbosity, + CallingModule: m.output.CallingModule, + Table: internal.TableClient{ + Wrap: m.WrapTable, + }, + } + + o.PrefixIdentifier = m.AWSProfile + o.Table.DirectoryName = filepath.Join(m.AWSOutputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) + + // Table #1: Inbound Privilege Escalation Paths + fmt.Println("Printing inbound privesc paths for account: ", aws.ToString(m.Caller.Account)) + header, body, _ := m.generateInboundPrivEscTableData() + o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ + Header: header, + Body: body, + //TableCols: tableCols, + Name: "inbound-privesc-paths", + SkipPrintToScreen: false, + }) + + // // Table #2: Outbound Privilege Escalation Paths + // header, body, tableCols := m.generateOutBoundPrivEscTable() + // o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ + // Header: header, + // Body: body, + // TableCols: tableCols, + // Name: "outbound-privesc-paths", + // SkipPrintToScreen: false, + // }) + + o.WriteFullOutput(o.Table.TableFiles, nil) + +} + +func (m *CaperCommand) generateInboundPrivEscTableData() ([]string, [][]string, []string) { + var body [][]string + var tableCols []string + var header []string + header = []string{ + "Account", + "Source", + "Target", + "Summary", + } + + // If the user specified table columns, use those. + // If the user specified -o wide, use the wide default cols for this module. + // Otherwise, use the hardcoded default cols for this module. + + // If the user specified table columns, use those. + if m.AWSTableCols != "" { + tableCols = strings.Split(m.AWSTableCols, ",") + // If the user specified wide as the output format, use these columns. + } else if m.AWSOutputType == "wide" { + tableCols = []string{ + "Account", + "Source", + "Target", + "Summary", + } + // Otherwise, use these columns. + } else { + tableCols = []string{ + "Source", + "Target", + "Summary", + } + } + + //var reason string + var paths string + var privescPathsBody [][]string + //var vertices map[string]map[string]graph.Edge[string] + //vertices, err := graph.TopologicalSort(m.GlobalGraph) + //vertices, err := m.GlobalGraph.AdjacencyMap() + + // if err != nil { + // m.modLog.Error(err) + // fmt.Println("Error sorting graph: ", err) + // } + //edges, _ := m.GlobalGraph.Edges() + //var reason string + adjacencyMap, _ := m.GlobalGraph.AdjacencyMap() + for destination := range adjacencyMap { + d, destinationVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(destination) + //for the destination vertex, we only want to deal with the ones that are in this account + if destinationVertexWithProperties.Attributes["AccountID"] == aws.ToString(m.Caller.Account) { + // now let's look at every other vertex and see if it has a path to this destination + for source := range adjacencyMap { + s, sourceVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(source) + //for the source vertex, we only want to deal with the ones that are NOT in this account + if sourceVertexWithProperties.Attributes["AccountID"] != aws.ToString(m.Caller.Account) { + // now let's see if there is a path from this source to our destination + path, _ := graph.ShortestPath(m.GlobalGraph, s, d) + // if we have a path, then lets document this source as having a path to our destination + if path != nil { + if s != d { + paths = "" + // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen + // and then lets print the full path to the screen + for i := 0; i < len(path)-1; i++ { + thisEdge, _ := m.GlobalGraph.Edge(path[i], path[i+1]) + + for _, value := range thisEdge.Properties.Attributes { + value = strings.ReplaceAll(value, ",", " and") + paths += fmt.Sprintf("%s %s %s\n", thisEdge.Source, value, thisEdge.Target) + } + } + + //trim the last newline from csvPaths + paths = strings.TrimSuffix(paths, "\n") + privescPathsBody = append(privescPathsBody, []string{aws.ToString(m.Caller.Account), s, d, paths}) + + } + } + } + } + } + + } + + // if destinationVertexWithProperties.Attributes["IsAdminString"] == "Yes" { + // fmt.Println("Admin: ", d) + // } + // if destinationVertexWithProperties.Attributes["CanPrivEscToAdminString"] == "Yes" { + // fmt.Println("Has Path to admin: ", d) + // } + + body = append(body, privescPathsBody...) + return header, body, tableCols + +} + +func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendors) Node { + //var isAdmin, canPrivEscToAdmin string + + accountId := strings.Split(aws.ToString(role.Arn), ":")[4] + trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role) + if err != nil { + internal.TxtLog.Error(err.Error()) + return Node{} + } + + var TrustedPrincipals []TrustedPrincipal + var TrustedServices []TrustedService + var TrustedFederatedProviders []TrustedFederatedProvider + //var TrustedFederatedSubjects string + var trustedProvider string + var trustedSubjects string + var vendorName string + + for _, statement := range trustsdoc.Statement { + for _, principal := range statement.Principal.AWS { + if strings.Contains(principal, ":root") { + //check to see if the accountID is known + accountID := strings.Split(principal, ":")[4] + vendorName = vendors.GetVendorNameFromAccountID(accountID) + } + + TrustedPrincipals = append(TrustedPrincipals, TrustedPrincipal{ + TrustedPrincipal: principal, + ExternalID: statement.Condition.StringEquals.StsExternalID, + VendorName: vendorName, + //IsAdmin: false, + //CanPrivEscToAdmin: false, + }) + + } + for _, service := range statement.Principal.Service { + TrustedServices = append(TrustedServices, TrustedService{ + TrustedService: service, + AccountID: accountId, + //IsAdmin: false, + //CanPrivEscToAdmin: false, + }) + + } + for _, federated := range statement.Principal.Federated { + if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 { + trustedProvider = "GitHub" + trustedSubjects := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",") + if trustedSubjects == "" { + trustedSubjects = "ALL REPOS!!!" + } else { + trustedSubjects = "Repos: " + trustedSubjects + } + + } else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" { + if strings.Contains(statement.Principal.Federated[0], "AWSSSO") { + trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" + } else if strings.Contains(statement.Principal.Federated[0], "Okta") { + trustedProvider = "Okta" // (" + statement.Principal.Federated[0] + ")" + } + trustedSubjects = "Not applicable" + } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" { + trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" + if statement.Condition.StringEquals.OidcEksSub != "" { + trustedSubjects = statement.Condition.StringEquals.OidcEksSub + } else if statement.Condition.StringLike.OidcEksSub != "" { + trustedSubjects = statement.Condition.StringLike.OidcEksSub + } else { + trustedSubjects = "ALL SERVICE ACCOUNTS!" + } + } else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" { + trustedProvider = "Cognito" // (" + statement.Principal.Federated[0] + ")" + if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" { + trustedSubjects = statement.Condition.ForAnyValueStringLike.CognitoAMR + } + } else { + if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") { + trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" + trustedSubjects = "ALL SERVICE ACCOUNTS!" + } else if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") { + trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" + } + trustedSubjects = "Not applicable" + } + + TrustedFederatedProviders = append(TrustedFederatedProviders, TrustedFederatedProvider{ + TrustedFederatedProvider: federated, + ProviderShortName: trustedProvider, + TrustedSubjects: trustedSubjects, + //IsAdmin: false, + //CanPrivEscToAdmin: false, + }) + } + } + + node := Node{ + Arn: aws.ToString(role.Arn), + Type: "Role", + AccountID: accountId, + Name: aws.ToString(role.RoleName), + TrustsDoc: trustsdoc, + TrustedPrincipals: TrustedPrincipals, + TrustedServices: TrustedServices, + TrustedFederatedProviders: TrustedFederatedProviders, + } + + return node +} + +func ConvertIAMUserToNode(user types.User) Node { + accountId := strings.Split(aws.ToString(user.Arn), ":")[4] + + //create new object of type models.User + // cfUser := models.User{ + // Id: aws.ToString(user.UserId), + // ARN: aws.ToString(user.Arn), + // Name: aws.ToString(user.UserName), + // IsAdmin: false, + // CanPrivEscToAdmin: false, + // } + + node := Node{ + Arn: aws.ToString(user.Arn), + Type: "User", + AccountID: accountId, + Name: aws.ToString(user.UserName), + } + + return node +} + +func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) []Node { + + var newNodes []Node + + // get thisAccount id from role arn + var thisAccount string + if len(a.Arn) >= 25 { + thisAccount = a.Arn[13:25] + } else { + fmt.Sprintf("Could not get account number from this role arn%s", a.Arn) + } + + for _, TrustedPrincipal := range a.TrustedPrincipals { + //get account id from the trusted principal arn + var trustedPrincipalAccount string + if len(TrustedPrincipal.TrustedPrincipal) >= 25 { + trustedPrincipalAccount = TrustedPrincipal.TrustedPrincipal[13:25] + } else { + fmt.Sprintf("Could not get account number from this TrustedPrincipal%s", TrustedPrincipal.TrustedPrincipal) + } + + // If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role + // if the role we are looking at trusts root in it's own account + + if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) { + + newNodes = append(newNodes, Node{ + Arn: a.Arn, + Type: "Account", + AccountID: a.AccountID, + Name: a.Name, + }) + + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" { + newNodes = append(newNodes, Node{ + Arn: a.Arn, + Type: "Account", + AccountID: a.AccountID, + Name: a.Name, + VendorName: TrustedPrincipal.VendorName, + }) + + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) { + newNodes = append(newNodes, Node{ + Arn: TrustedPrincipal.TrustedPrincipal, + Type: "Account", + AccountID: trustedPrincipalAccount, + }) + + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":user")) { + newNodes = append(newNodes, Node{ + Arn: TrustedPrincipal.TrustedPrincipal, + Type: "User", + AccountID: trustedPrincipalAccount, + }) + + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":role")) { + newNodes = append(newNodes, Node{ + Arn: TrustedPrincipal.TrustedPrincipal, + Type: "Role", + AccountID: trustedPrincipalAccount, + }) + + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":group")) { + newNodes = append(newNodes, Node{ + Arn: TrustedPrincipal.TrustedPrincipal, + Type: "Group", + AccountID: trustedPrincipalAccount, + }) + + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":assumed-role")) { + newNodes = append(newNodes, Node{ + Arn: TrustedPrincipal.TrustedPrincipal, + Type: "AssumedRole", + AccountID: trustedPrincipalAccount, + }) + + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":assumed-role")) { + newNodes = append(newNodes, Node{ + Arn: TrustedPrincipal.TrustedPrincipal, + Type: "AssumedRole", + AccountID: trustedPrincipalAccount, + }) + + } + } + // pmapper takes care of this part so commenting out for now - but leaving as a placeholder + // for _, TrustedService := range a.TrustedServices { + // // make relationship from trusted service to this role of type can assume + // // make relationship from this role to trusted service of type can be assumed by + // } + + for _, TrustedFederatedProvider := range a.TrustedFederatedProviders { + // make relationship from trusted federated provider to this role of type can assume + + newNodes = append(newNodes, Node{ + Arn: TrustedFederatedProvider.TrustedFederatedProvider, + Type: "FederatedIdentity", + AccountID: a.AccountID, + }) + + } + + return newNodes +} + +func MergeNodes(nodes []Node) []Node { + nodeMap := make(map[string]Node) + + for _, node := range nodes { + existingNode, exists := nodeMap[node.Arn] + if exists { + // fmt.Println("Found a duplicate node: %s, merging", node.Arn) + // fmt.Println("Existing node: %v", existingNode) + // fmt.Println("New node: %v", node) + + mergedNode := mergeNodeData(existingNode, node) + nodeMap[node.Arn] = mergedNode + //fmt.Println("Merged node: %v", mergedNode) + } else { + nodeMap[node.Arn] = node + } + } + + var mergedNodes []Node + for _, node := range nodeMap { + mergedNodes = append(mergedNodes, node) + } + + return mergedNodes +} + +func mergeNodeData(existingNode Node, newNode Node) Node { + if existingNode.Arn == "" { + existingNode.Arn = newNode.Arn + } else { + existingNode.Arn = existingNode.Arn + } + if existingNode.Name == "" { + existingNode.Name = newNode.Name + } else { + existingNode.Name = existingNode.Name + } + if existingNode.Type == "" { + existingNode.Type = newNode.Type + } else { + existingNode.Type = existingNode.Type + } + if existingNode.AccountID == "" { + existingNode.AccountID = newNode.AccountID + } else { + existingNode.AccountID = existingNode.AccountID + } + if existingNode.CanPrivEscToAdminString == "" { + existingNode.CanPrivEscToAdminString = newNode.CanPrivEscToAdminString + } else { + existingNode.CanPrivEscToAdminString = existingNode.CanPrivEscToAdminString + } + if existingNode.IsAdminString == "" { + existingNode.IsAdminString = newNode.IsAdminString + } else { + existingNode.IsAdminString = existingNode.IsAdminString + } + if existingNode.VendorName == "" { + existingNode.VendorName = newNode.VendorName + } else { + existingNode.VendorName = existingNode.VendorName + } + if existingNode.AccessKeys == 0 { + existingNode.AccessKeys = newNode.AccessKeys + } else { + existingNode.AccessKeys = existingNode.AccessKeys + } + if existingNode.ActivePassword == false { + existingNode.ActivePassword = newNode.ActivePassword + } else { + existingNode.ActivePassword = existingNode.ActivePassword + } + if existingNode.HasMfa == false { + existingNode.HasMfa = newNode.HasMfa + } else { + existingNode.HasMfa = existingNode.HasMfa + } + if existingNode.PathToAdmin == false { + existingNode.PathToAdmin = newNode.PathToAdmin + } else { + existingNode.PathToAdmin = existingNode.PathToAdmin + } + if existingNode.AttachedPolicies == nil { + existingNode.AttachedPolicies = newNode.AttachedPolicies + } else { + existingNode.AttachedPolicies = existingNode.AttachedPolicies + } + if existingNode.TrustedFederatedProviders == nil { + existingNode.TrustedFederatedProviders = newNode.TrustedFederatedProviders + } else { + existingNode.TrustedFederatedProviders = existingNode.TrustedFederatedProviders + } + if existingNode.TrustedPrincipals == nil { + existingNode.TrustedPrincipals = newNode.TrustedPrincipals + } else { + existingNode.TrustedPrincipals = existingNode.TrustedPrincipals + } + if existingNode.TrustedServices == nil { + existingNode.TrustedServices = newNode.TrustedServices + } else { + existingNode.TrustedServices = existingNode.TrustedServices + } + // if existingNode.TrustsDoc.Statement == nil { + // existingNode.TrustsDoc = newNode.TrustsDoc + // } else { + // } + return existingNode +} + +func (a *Node) mergeAttributes(newAttributes map[string]any) { + if a.Arn == "" { + a.Arn = newAttributes["Arn"].(string) + } + if a.Name == "" { + a.Name = newAttributes["Name"].(string) + } + if a.Type == "" { + a.Type = newAttributes["Type"].(string) + } + if a.AccountID == "" { + a.AccountID = newAttributes["AccountID"].(string) + } + if a.CanPrivEscToAdminString == "" { + a.CanPrivEscToAdminString = newAttributes["CanPrivEscToAdminString"].(string) + } + if a.IsAdminString == "" { + a.IsAdminString = newAttributes["IsAdminString"].(string) + } + if a.VendorName == "" { + a.VendorName = newAttributes["VendorName"].(string) + } + // if a.AccessKeys does not exist, set it to the value in newAttributes + if a.AccessKeys == 0 { + a.AccessKeys = newAttributes["AccessKeys"].(int) + } + if a.ActivePassword == false { + a.ActivePassword = newAttributes["ActivePassword"].(bool) + } + if a.HasMfa == false { + a.HasMfa = newAttributes["HasMfa"].(bool) + } + if a.PathToAdmin == false { + a.PathToAdmin = newAttributes["PathToAdmin"].(bool) + } + if a.AttachedPolicies == nil { + a.AttachedPolicies = newAttributes["AttachedPolicies"].([]AttachedPolicies) + } + if a.TrustedFederatedProviders == nil { + a.TrustedFederatedProviders = newAttributes["TrustedFederatedProviders"].([]TrustedFederatedProvider) + } + if a.TrustedPrincipals == nil { + a.TrustedPrincipals = newAttributes["TrustedPrincipals"].([]TrustedPrincipal) + } + if a.TrustedServices == nil { + a.TrustedServices = newAttributes["TrustedServices"].([]TrustedService) + } + // if a.TrustsDoc.Statement == nil { + // a.TrustsDoc = newAttributes["TrustsDoc"].(policy.TrustPolicyDocument) + // } + +} + +func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { + + // get thisAccount id from role arn + var thisAccount string + if len(a.Arn) >= 25 { + thisAccount = a.Arn[13:25] + } else { + fmt.Sprintf("Could not get account number from this role arn%s", a.Arn) + } + + for _, TrustedPrincipal := range a.TrustedPrincipals { + //get account id from the trusted principal arn + var trustedPrincipalAccount string + if len(TrustedPrincipal.TrustedPrincipal) >= 25 { + trustedPrincipalAccount = TrustedPrincipal.TrustedPrincipal[13:25] + } else { + fmt.Sprintf("Could not get account number from this TrustedPrincipal%s", TrustedPrincipal.TrustedPrincipal) + } + var PermissionsRowAccount string + + // if the role trusts a principal in this same account explicitly, then the principal can assume the role + if thisAccount == trustedPrincipalAccount { + // make a CAN_ASSUME relationship between the trusted principal and this role + + err := GlobalGraph.AddEdge( + TrustedPrincipal.TrustedPrincipal, + a.Arn, + //graph.EdgeAttribute("AssumeRole", "Same account explicit trust"), + graph.EdgeAttribute("AssumeRole", "can assume (because of an explicit same account trust) "), + ) + if err != nil { + fmt.Println(err) + fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Same account explicit trust") + } + } + + // If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role + // if the role we are looking at trusts root in it's own account + + if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) { + + // iterate over all rows in AllPermissionsRows + for _, PermissionsRow := range common.PermissionRowsFromAllProfiles { + // but we only care about the rows that have arns that are in this account + + if len(PermissionsRow.Arn) >= 25 { + PermissionsRowAccount = PermissionsRow.Arn[13:25] + } else { + fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn) + } + + if PermissionsRowAccount == thisAccount { + // lets only look for rows that have sts:AssumeRole permissions + if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || + strings.EqualFold(PermissionsRow.Action, "*") || + strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || + strings.EqualFold(PermissionsRow.Action, "sts:*") { + // lets only focus on rows that have an effect of Allow + if strings.EqualFold(PermissionsRow.Effect, "Allow") { + // if the resource is * or the resource is this role arn, then this principal can assume this role + if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.Arn) { + // make a CAN_ASSUME relationship between the trusted principal and this role + //evalutate if the princiapl is a user or a role and set a variable accordingly + //var principalType schema.NodeLabel + if strings.EqualFold(PermissionsRow.Type, "User") { + err := GlobalGraph.AddEdge( + PermissionsRow.Arn, + a.Arn, + //graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"), + graph.EdgeAttribute("AssumeRole", "can assume (because of a same account root trust and trusted principal has permission to assume role) "), + ) + if err != nil { + fmt.Println(err) + fmt.Println(PermissionsRow.Arn + a.Arn + "Same account root trust and trusted principal has permission to assume role") + } + } else if strings.EqualFold(PermissionsRow.Type, "Role") { + err := GlobalGraph.AddEdge( + PermissionsRow.Arn, + a.Arn, + //graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"), + graph.EdgeAttribute("AssumeRole", "can assume (because of a same account root trust and trusted principal has permission to assume role) "), + ) + if err != nil { + fmt.Println(err) + fmt.Println(PermissionsRow.Arn + a.Arn + "Same account root trust and trusted principal has permission to assume role") + } + } + } + } + } + } + } + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" { + + // If the role trusts :root in another account and the trusted principal is a vendor, we will make a relationship between our role and a vendor node instead of a principal node + + err := GlobalGraph.AddEdge( + //TrustedPrincipal.TrustedPrincipal, + TrustedPrincipal.VendorName, + a.Arn, + //graph.EdgeAttribute("VendorAssumeRole", "Cross account root trust and trusted principal is a vendor"), + graph.EdgeAttribute("VendorAssumeRole", "can assume (because of a cross account root trust and trusted principal is a vendor) "), + ) + if err != nil { + fmt.Println(err) + fmt.Println(TrustedPrincipal.VendorName + a.Arn + "Cross account root trust and trusted principal is a vendor") + + } + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) { + + // iterate over all rows in AllPermissionsRows + for _, PermissionsRow := range common.PermissionRowsFromAllProfiles { + // but we only care about the rows that have arns that are in this other account + if len(PermissionsRow.Arn) >= 25 { + PermissionsRowAccount = PermissionsRow.Arn[13:25] + } else { + fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn) + } + if PermissionsRowAccount == trustedPrincipalAccount { + // lets only look for rows that have sts:AssumeRole permissions + if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || + strings.EqualFold(PermissionsRow.Action, "*") || + strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || + strings.EqualFold(PermissionsRow.Action, "sts:*") { + // lets only focus on rows that have an effect of Allow + if strings.EqualFold(PermissionsRow.Effect, "Allow") { + // if the resource is * or the resource is this role arn, then this principal can assume this role + if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.Arn) { + // make a CAN_ASSUME relationship between the trusted principal and this role + + if strings.EqualFold(PermissionsRow.Type, "User") { + err := GlobalGraph.AddEdge( + PermissionsRow.Arn, + a.Arn, + //graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"), + graph.EdgeAttribute("CrossAccountAssumeRole", "can assume (because of a cross account root trust and trusted principal has permission to assume role) "), + ) + if err != nil { + fmt.Println(err) + fmt.Println(PermissionsRow.Arn + a.Arn + "Cross account root trust and trusted principal has permission to assume role") + } + + } else if strings.EqualFold(PermissionsRow.Type, "Role") { + err := GlobalGraph.AddEdge( + PermissionsRow.Arn, + a.Arn, + //graph.EdgeAttribute("CrossAccountAssumeRole", "Cross account root trust and trusted principal has permission to assume role"), + graph.EdgeAttribute("CrossAccountAssumeRole", "can assume (because of a cross account root trust and trusted principal has permission to assume role) "), + ) + if err != nil { + fmt.Println(err) + fmt.Println(PermissionsRow.Arn + a.Arn + "Cross account root trust and trusted principal has permission to assume role") + } + } + } + } + } + + } + } + + } + } + // pmapper takes care of this part so commenting out for now - but leaving as a placeholder + + // for _, TrustedService := range a.TrustedServices { + // // make relationship from trusted service to this role of type can assume + // // make relationship from this role to trusted service of type can be assumed by + // } + + for _, TrustedFederatedProvider := range a.TrustedFederatedProviders { + // make relationship from trusted federated provider to this role of type can assume + + err := GlobalGraph.AddEdge( + TrustedFederatedProvider.TrustedFederatedProvider, + a.Arn, + //graph.EdgeAttribute("FederatedAssumeRole", "Trusted federated provider"), + graph.EdgeAttribute("FederatedAssumeRole", "can assume (because of a trusted federated provider) "), + ) + if err != nil { + fmt.Println(err) + fmt.Println(TrustedFederatedProvider.TrustedFederatedProvider + a.Arn + "Trusted federated provider") + } + + } + +} diff --git a/aws/graph.go b/aws/graph.go index 7b2c61a..dd6555c 100644 --- a/aws/graph.go +++ b/aws/graph.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "path/filepath" "strings" ingestor "github.com/BishopFox/cloudfox/aws/graph/ingester" @@ -13,10 +12,8 @@ import ( "github.com/BishopFox/cloudfox/internal" "github.com/BishopFox/cloudfox/internal/aws/policy" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/bishopfox/knownawsaccountslookup" - "github.com/dominikbraun/graph" "github.com/sirupsen/logrus" ) @@ -48,28 +45,6 @@ type GraphCommand struct { modLog *logrus.Entry } -type GraphCommand2 struct { - - // General configuration data - Caller sts.GetCallerIdentityOutput - AWSRegions []string - Goroutines int - AWSProfile string - WrapTable bool - AWSOutputType string - AWSTableCols string - Verbosity int - AWSOutputDirectory string - AWSConfig aws.Config - Version string - SkipAdminCheck bool - - GlobalGraph graph.Graph[string, string] - - output internal.OutputData2 - modLog *logrus.Entry -} - func (m *GraphCommand) RunGraphCommand() { // These struct values are used by the output module @@ -192,57 +167,6 @@ func (m *GraphCommand) RunGraphCommand() { } -func (m *GraphCommand2) RunGraphCommand2() { - - // These struct values are used by the output module - m.output.Verbosity = m.Verbosity - m.output.Directory = m.AWSOutputDirectory - m.output.CallingModule = "graph" - m.modLog = internal.TxtLog.WithFields(logrus.Fields{ - "module": m.output.CallingModule, - }) - if m.AWSProfile == "" { - m.AWSProfile = internal.BuildAWSPath(m.Caller) - } - m.output.FilePath = filepath.Join(m.AWSOutputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) - - m.output.Headers = []string{ - "Account", - "Principal Arn", - "IsAdmin?", - "CanPrivEscToAdmin?", - } - - // If the user specified table columns, use those. - // If the user specified -o wide, use the wide default cols for this module. - // Otherwise, use the hardcoded default cols for this module. - var tableCols []string - // If the user specified table columns, use those. - if m.AWSTableCols != "" { - // If the user specified wide as the output format, use these columns. - // remove any spaces between any commas and the first letter after the commas - m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") - m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",") - tableCols = strings.Split(m.AWSTableCols, ",") - } else if m.AWSOutputType == "wide" { - tableCols = []string{ - "Account", - "Principal Arn", - "IsAdmin?", - "CanPrivEscToAdmin?", - } - // Otherwise, use the default columns. - } else { - tableCols = []string{ - "Principal Arn", - "IsAdmin?", - "CanPrivEscToAdmin?", - } - } - internal.TxtLog.Debug("tableCols: ", tableCols) - -} - func (m *GraphCommand) collectAccountDataForGraph() []models.Account { //OrganizationsCommandClient := InitOrgClient(m.AWSConfig) var accounts []models.Account @@ -490,116 +414,3 @@ func (m *GraphCommand) collectRoleDataForGraph() []models.Role { } return roles } - -func ConvertIAMRoleToModelRole(role types.Role, vendors *knownawsaccountslookup.Vendors) models.Role { - var isAdmin, canPrivEscToAdmin string - - accountId := strings.Split(aws.ToString(role.Arn), ":")[4] - trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role) - if err != nil { - internal.TxtLog.Error(err.Error()) - return models.Role{} - } - - var TrustedPrincipals []models.TrustedPrincipal - var TrustedServices []models.TrustedService - var TrustedFederatedProviders []models.TrustedFederatedProvider - //var TrustedFederatedSubjects string - var trustedProvider string - var trustedSubjects string - var vendorName string - - for _, statement := range trustsdoc.Statement { - for _, principal := range statement.Principal.AWS { - if strings.Contains(principal, ":root") { - //check to see if the accountID is known - accountID := strings.Split(principal, ":")[4] - vendorName = vendors.GetVendorNameFromAccountID(accountID) - } - - TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{ - TrustedPrincipal: principal, - ExternalID: statement.Condition.StringEquals.StsExternalID, - VendorName: vendorName, - //IsAdmin: false, - //CanPrivEscToAdmin: false, - }) - - } - for _, service := range statement.Principal.Service { - TrustedServices = append(TrustedServices, models.TrustedService{ - TrustedService: service, - AccountID: accountId, - //IsAdmin: false, - //CanPrivEscToAdmin: false, - }) - - } - for _, federated := range statement.Principal.Federated { - if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 { - trustedProvider = "GitHub" - trustedSubjects := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",") - if trustedSubjects == "" { - trustedSubjects = "ALL REPOS!!!" - } else { - trustedSubjects = "Repos: " + trustedSubjects - } - - } else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" { - if strings.Contains(statement.Principal.Federated[0], "AWSSSO") { - trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" - } else if strings.Contains(statement.Principal.Federated[0], "Okta") { - trustedProvider = "Okta" // (" + statement.Principal.Federated[0] + ")" - } - trustedSubjects = "Not applicable" - } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" { - trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" - if statement.Condition.StringEquals.OidcEksSub != "" { - trustedSubjects = statement.Condition.StringEquals.OidcEksSub - } else if statement.Condition.StringLike.OidcEksSub != "" { - trustedSubjects = statement.Condition.StringLike.OidcEksSub - } else { - trustedSubjects = "ALL SERVICE ACCOUNTS!" - } - } else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" { - trustedProvider = "Cognito" // (" + statement.Principal.Federated[0] + ")" - if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" { - trustedSubjects = statement.Condition.ForAnyValueStringLike.CognitoAMR - } - } else { - if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") { - trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" - trustedSubjects = "ALL SERVICE ACCOUNTS!" - } else if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") { - trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" - } - trustedSubjects = "Not applicable" - } - - TrustedFederatedProviders = append(TrustedFederatedProviders, models.TrustedFederatedProvider{ - TrustedFederatedProvider: federated, - ProviderShortName: trustedProvider, - TrustedSubjects: trustedSubjects, - //IsAdmin: false, - //CanPrivEscToAdmin: false, - }) - } - } - - //create new object of type models.Role - cfRole := models.Role{ - Id: aws.ToString(role.Arn), - AccountID: accountId, - ARN: aws.ToString(role.Arn), - Name: aws.ToString(role.RoleName), - TrustsDoc: trustsdoc, - TrustedPrincipals: TrustedPrincipals, - TrustedServices: TrustedServices, - TrustedFederatedProviders: TrustedFederatedProviders, - CanPrivEscToAdmin: canPrivEscToAdmin, - IsAdmin: isAdmin, - } - //roles = append(roles, role) - - return cfRole -} diff --git a/aws/graph/ingester/schema/models/roles.go b/aws/graph/ingester/schema/models/roles.go index 455d22e..4ff7a96 100644 --- a/aws/graph/ingester/schema/models/roles.go +++ b/aws/graph/ingester/schema/models/roles.go @@ -330,6 +330,46 @@ func (a *Role) MakeRelationships() []schema.Relationship { return relationships } +// func (a *Node) GenerateAttributes() map[string]string { +// attributes := make(map[string]string) +// attributes["Id"] = a.Id +// attributes["Name"] = a.Name +// attributes["Type"] = "Role" +// attributes["AccountID"] = a.AccountID +// attributes["ARN"] = a.ARN +// attributes["CanPrivEscToAdmin"] = a.CanPrivEscToAdmin +// attributes["IsAdmin"] = a.IsAdmin +// attributes["IdValue"] = a.IdValue +// return attributes +// } + +// func (a *Role) MergeAttributes(newAttributes map[string]string) { +// if a.Id == "" { +// a.Id = newAttributes["Id"] +// } +// if a.Name == "" { +// a.Name = newAttributes["Name"] +// } +// if a.Type == "" { +// a.Type = newAttributes["Type"] +// } +// if a.AccountID == "" { +// a.AccountID = newAttributes["AccountID"] +// } +// if a.ARN == "" { +// a.ARN = newAttributes["ARN"] +// } +// if a.CanPrivEscToAdmin == "" { +// a.CanPrivEscToAdmin = newAttributes["CanPrivEscToAdmin"] +// } +// if a.IsAdmin == "" { +// a.IsAdmin = newAttributes["IsAdmin"] +// } +// if a.IdValue == "" { +// a.IdValue = newAttributes["IdValue"] +// } +// } + func (a *Role) MakeVertices(GlobalGraph graph.Graph[string, string]) { // make a vertex for this role as populate all of the data in the Role struct as attributes @@ -351,6 +391,37 @@ func (a *Role) MakeVertices(GlobalGraph graph.Graph[string, string]) { } +// func MakeAllVertices(GlobalRoles []Role, GlobalPmapperGraph aws.PmapperModule) (GlobalGraph graph.Graph[string, string]) { + +// // for all nodes in the GlobalPmapperGraph, check to see if they exist in the GlobalRoles slice. If they do, then update the node with the new data. If they don't, then add the node to the GlobalRoles slice +// for _, node := range GlobalPmapperGraph.Nodes { +// // check to see if the node exists in the GlobalRoles slice +// var nodeExists bool +// for _, role := range GlobalRoles { +// if node.Id == role.Id { +// nodeExists = true +// } +// } + +// // // make a vertex for this role as populate all of the data in the Role struct as attributes +// // err := GlobalGraph.AddVertex( +// // a.Id, +// // graph.VertexAttribute("Name", a.Name), +// // graph.VertexAttribute("Type", "Role"), +// // graph.VertexAttribute("AccountID", a.AccountID), +// // graph.VertexAttribute("ARN", a.ARN), +// // graph.VertexAttribute("CanPrivEscToAdmin", a.CanPrivEscToAdmin), +// // graph.VertexAttribute("IsAdmin", a.IsAdmin), +// // graph.VertexAttribute("IdValue", a.IdValue), +// // ) +// // if err != nil { +// // if err == graph.ErrVertexAlreadyExists { +// // fmt.Println(a.Id + " already exists") +// // } +// // } + +// } + func (a *Role) MakeEdges(GlobalGraph graph.Graph[string, string]) []schema.Relationship { var relationships []schema.Relationship diff --git a/aws/graph/ingester/schema/models/users.go b/aws/graph/ingester/schema/models/users.go index 82d5eab..2ed3f40 100644 --- a/aws/graph/ingester/schema/models/users.go +++ b/aws/graph/ingester/schema/models/users.go @@ -13,8 +13,6 @@ type User struct { IsAdmin string CanPrivEscToAdmin string IdValue string - IsAdminP bool - PathToAdmin bool } func (a *User) MakeRelationships() []schema.Relationship { @@ -39,3 +37,35 @@ func (a *User) MakeRelationships() []schema.Relationship { }) return relationships } + +func (a *User) GenerateAttributes() map[string]string { + return map[string]string{ + "Id": a.Id, + "ARN": a.ARN, + "Name": a.Name, + "IsAdmin": a.IsAdmin, + "CanPrivEscToAdmin": a.CanPrivEscToAdmin, + "IdValue": a.IdValue, + } +} + +func (a *User) MergeAttributes(other map[string]string) { + if other["Id"] != "" { + a.Id = other["Id"] + } + if other["ARN"] != "" { + a.ARN = other["ARN"] + } + if other["Name"] != "" { + a.Name = other["Name"] + } + if other["IsAdmin"] != "" { + a.IsAdmin = other["IsAdmin"] + } + if other["CanPrivEscToAdmin"] != "" { + a.CanPrivEscToAdmin = other["CanPrivEscToAdmin"] + } + if other["IdValue"] != "" { + a.IdValue = other["IdValue"] + } +} diff --git a/aws/pmapper.go b/aws/pmapper.go index d3e4d10..fb98940 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/BishopFox/cloudfox/internal" + "github.com/BishopFox/cloudfox/internal/aws/policy" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/dominikbraun/graph" @@ -53,19 +54,52 @@ type Edge struct { } type Node struct { - Arn string `json:"arn"` - IDValue string `json:"id_value"` - AttachedPolicies []AttachedPolicies `json:"attached_policies"` - GroupMemberships []interface{} `json:"group_memberships"` - TrustPolicy interface{} `json:"trust_policy"` - InstanceProfile interface{} `json:"instance_profile"` - ActivePassword bool `json:"active_password"` - AccessKeys int `json:"access_keys"` - IsAdmin bool `json:"is_admin"` - PermissionsBoundary interface{} `json:"permissions_boundary"` - HasMfa bool `json:"has_mfa"` - Tags Tags `json:"tags"` - PathToAdmin bool + Arn string `json:"arn"` + Type string + AccountID string + Name string + IDValue string `json:"id_value"` + AttachedPolicies []AttachedPolicies `json:"attached_policies"` + GroupMemberships []interface{} `json:"group_memberships"` + TrustPolicy interface{} `json:"trust_policy"` + TrustsDoc policy.TrustPolicyDocument + TrustedPrincipals []TrustedPrincipal + TrustedServices []TrustedService + TrustedFederatedProviders []TrustedFederatedProvider + InstanceProfile interface{} `json:"instance_profile"` + ActivePassword bool `json:"active_password"` + AccessKeys int `json:"access_keys"` + IsAdmin bool `json:"is_admin"` + PathToAdmin bool + PermissionsBoundary interface{} `json:"permissions_boundary"` + HasMfa bool `json:"has_mfa"` + Tags Tags `json:"tags"` + CanPrivEscToAdminString string + IsAdminString string + VendorName string +} + +type TrustedPrincipal struct { + TrustedPrincipal string + ExternalID string + VendorName string + //IsAdmin bool + //CanPrivEscToAdmin bool +} + +type TrustedService struct { + TrustedService string + AccountID string + //IsAdmin bool + //CanPrivEscToAdmin bool +} + +type TrustedFederatedProvider struct { + TrustedFederatedProvider string + ProviderShortName string + TrustedSubjects string + //IsAdmin bool + //CanPrivEscToAdmin bool } type AttachedPolicies struct { @@ -87,9 +121,16 @@ func (m *PmapperModule) initPmapperGraph() error { for i := range m.Nodes { if m.doesNodeHavePathToAdmin(m.Nodes[i]) { m.Nodes[i].PathToAdmin = true + m.Nodes[i].CanPrivEscToAdminString = "Yes" //fmt.Println(m.Nodes[i].Arn, m.Nodes[i].IsAdmin, m.Nodes[i].PathToAdmin) } else { m.Nodes[i].PathToAdmin = false + m.Nodes[i].CanPrivEscToAdminString = "No" + } + if m.Nodes[i].IsAdmin { + m.Nodes[i].IsAdminString = "Yes" + } else { + m.Nodes[i].IsAdminString = "No" } } @@ -159,7 +200,11 @@ func (m *PmapperModule) DoesPrincipalHavePathToAdmin(principal string) bool { for i := range m.Nodes { if m.Nodes[i].Arn == principal { if m.Nodes[i].PathToAdmin { + m.Nodes[i].CanPrivEscToAdminString = "Yes" return true + } else { + m.Nodes[i].CanPrivEscToAdminString = "No" + return false } } @@ -171,7 +216,11 @@ func (m *PmapperModule) DoesPrincipalHaveAdmin(principal string) bool { for i := range m.Nodes { if m.Nodes[i].Arn == principal { if m.Nodes[i].IsAdmin { + m.Nodes[i].IsAdminString = "Yes" return true + } else { + m.Nodes[i].IsAdminString = "No" + return false } } diff --git a/cli/aws.go b/cli/aws.go index 161847b..874d2ac 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -6,9 +6,9 @@ import ( "log" "os" "path/filepath" + "time" "github.com/BishopFox/cloudfox/aws" - "github.com/BishopFox/cloudfox/aws/graph/ingester/schema/models" "github.com/BishopFox/cloudfox/aws/sdk" "github.com/BishopFox/cloudfox/internal" "github.com/BishopFox/cloudfox/internal/common" @@ -63,6 +63,7 @@ import ( "github.com/aws/smithy-go/ptr" "github.com/bishopfox/knownawsaccountslookup" "github.com/dominikbraun/graph" + "github.com/dominikbraun/graph/draw" "github.com/fatih/color" "github.com/kyokomi/emoji" "github.com/spf13/cobra" @@ -137,6 +138,17 @@ var ( PostRun: awsPostRun, } + CaperCommand = &cobra.Command{ + Use: "caper", + Aliases: []string{"caperParse"}, + Short: "Cross-Account Privilege Escalation Route finder. Best run with multiple profiles, ideally the -l flag", + Long: "\nUse case examples:\n" + + os.Args[0] + " aws caper -l file_with_profile_names.txt", + PreRun: awsPreRun, + Run: runCaperCommand, + PostRun: awsPostRun, + } + CloudformationCommand = &cobra.Command{ Use: "cloudformation", Aliases: []string{"cf", "cfstacks", "stacks"}, @@ -935,9 +947,51 @@ func runFilesystemsCommand(cmd *cobra.Command, args []string) { } func runGraphCommand(cmd *cobra.Command, args []string) { + for _, profile := range AWSProfiles { + //var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version) + caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) + if err != nil { + continue + } + + //instantiate a permissions client and populate the permissions data + fmt.Println("Getting GAAD for " + profile) + PermissionsCommandClient := aws.InitPermissionsClient(*caller, profile, cmd.Root().Version, Goroutines, AWSMFAToken) + PermissionsCommandClient.GetGAAD() + PermissionsCommandClient.ParsePermissions("") + common.PermissionRowsFromAllProfiles = append(common.PermissionRowsFromAllProfiles, PermissionsCommandClient.Rows...) + } + + for _, profile := range AWSProfiles { + var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) + caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) + if err != nil { + continue + } + + graphCommandClient := aws.GraphCommand{ + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + AWSOutputDirectory: AWSOutputDirectory, + Verbosity: Verbosity, + AWSConfig: AWSConfig, + Version: cmd.Root().Version, + SkipAdminCheck: AWSSkipAdminCheck, + } + graphCommandClient.RunGraphCommand() + } +} + +func runCaperCommand(cmd *cobra.Command, args []string) { GlobalGraph := graph.New(graph.StringHash, graph.Directed()) //var PermissionRowsFromAllProfiles []common.PermissionsRow - var GlobalRoles []models.Role + var GlobalPmapperData aws.PmapperModule + var GlobalNodes []aws.Node vendors := knownawsaccountslookup.NewVendorMap() vendors.PopulateKnownAWSAccounts() @@ -955,47 +1009,74 @@ func runGraphCommand(cmd *cobra.Command, args []string) { PermissionsCommandClient.ParsePermissions("") common.PermissionRowsFromAllProfiles = append(common.PermissionRowsFromAllProfiles, PermissionsCommandClient.Rows...) - // Gather all Pmapper data + // Gather all Pmapper data. fmt.Println("Importing Pmapper for " + profile) pmapperMod, pmapperError := aws.InitPmapperGraph(*caller, AWSProfile, Goroutines) if pmapperError != nil { fmt.Println("Error importing pmapper data: " + pmapperError.Error()) } + for _, node := range pmapperMod.Nodes { - GlobalGraph.AddVertex(node.Arn) + // add node to GlobalPmapperData + //GlobalPmapperData.Nodes = append(GlobalPmapperData.Nodes, node) + GlobalNodes = append(GlobalNodes, node) } + for _, edge := range pmapperMod.Edges { - err := GlobalGraph.AddEdge( - edge.Source, - edge.Destination, - graph.EdgeAttribute(edge.ShortReason, edge.Reason), - ) - if err != nil { - if err == graph.ErrEdgeAlreadyExists { - // update the ege by copying the existing graph.Edge with attributes and add the new attributes - //fmt.Println("Edge already exists") - - // get the existing edge - existingEdge, _ := GlobalGraph.Edge(edge.Source, edge.Destination) - // get the map of attributes - existingProperties := existingEdge.Properties - // add the new attributes to attributes map within the properties struct - // Check if the Attributes map is initialized, if not, initialize it - if existingProperties.Attributes == nil { - existingProperties.Attributes = make(map[string]string) - } - - // Add or update the attribute - existingProperties.Attributes[edge.ShortReason] = edge.Reason - GlobalGraph.UpdateEdge( - edge.Source, - edge.Destination, - graph.EdgeAttributes(existingProperties.Attributes), - ) - } - } + // add edge to GlobalPmapperData + GlobalPmapperData.Edges = append(GlobalPmapperData.Edges, edge) + } + + // for _, node := range pmapperMod.Nodes { + // var admin, pathToAdmin string + // if node.IsAdmin { + // admin = "yes" + // } else { + // admin = "no" + // } + // if node.PathToAdmin { + // pathToAdmin = "yes" + // } else { + // pathToAdmin = "no" + // } + + // GlobalGraph.AddVertex(node.Arn, + // graph.VertexAttribute("IsAdmin", admin), + // graph.VertexAttribute("PathToAdmin", pathToAdmin), + // ) + //} + // for _, edge := range pmapperMod.Edges { + // err := GlobalGraph.AddEdge( + // edge.Source, + // edge.Destination, + // graph.EdgeAttribute(edge.ShortReason, edge.Reason), + // ) + // if err != nil { + // if err == graph.ErrEdgeAlreadyExists { + // // update the ege by copying the existing graph.Edge with attributes and add the new attributes + // //fmt.Println("Edge already exists") + + // // get the existing edge + // existingEdge, _ := GlobalGraph.Edge(edge.Source, edge.Destination) + // // get the map of attributes + // existingProperties := existingEdge.Properties + // // add the new attributes to attributes map within the properties struct + // // Check if the Attributes map is initialized, if not, initialize it + // if existingProperties.Attributes == nil { + // existingProperties.Attributes = make(map[string]string) + // } + + // // Add or update the attribute + // existingProperties.Attributes[edge.ShortReason] = edge.Reason + // GlobalGraph.UpdateEdge( + // edge.Source, + // edge.Destination, + // graph.EdgeAttributes(existingProperties.Attributes), + // ) + // } + // } - } + // } //Gather all role data fmt.Println("Getting Roles for " + profile) @@ -1005,74 +1086,124 @@ func runGraphCommand(cmd *cobra.Command, args []string) { internal.TxtLog.Error(err) } for _, role := range ListRolesOutput { - modelRole := aws.ConvertIAMRoleToModelRole(role, vendors) - GlobalRoles = append(GlobalRoles, modelRole) + node := aws.ConvertIAMRoleToNode(role, vendors) + + // First insert the role itself into the Nodes slice + GlobalNodes = append(GlobalNodes, node) + // Then insert all of the vertices that are in the role's trust policy + GlobalNodes = append(GlobalNodes, aws.FindVerticesInRoleTrust(node, vendors)...) + } - // make vertices - // you can't update verticies - so we need to make all of the vertices that are roles in the in-scope accounts - // all at once to make sure they have the most information possible - fmt.Println("Making vertices for " + profile) - for _, role := range GlobalRoles { - role.MakeVertices(GlobalGraph) + + //Gather all user data + fmt.Println("Getting Users for " + profile) + ListUsersOutput, err := sdk.CachedIamListUsers(IAMCommandClient, ptr.ToString(caller.Account)) + if err != nil { + internal.TxtLog.Error(err) } + for _, user := range ListUsersOutput { + GlobalNodes = append(GlobalNodes, aws.ConvertIAMUserToNode(user)) - //making edges - fmt.Println("Making edges for " + profile) - for _, role := range GlobalRoles { - role.MakeEdges(GlobalGraph) } - // print how many nodes and edges are in the graph to the screen and exit + } - for _, profile := range AWSProfiles { - var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) - caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) - if err != nil { - continue - } + //GlobalGraph := models.MakeAllVertices(GlobalRoles, GlobalPmapperData) - // graphCommandClient := aws.GraphCommand{ - // Caller: *caller, - // AWSProfile: profile, - // Goroutines: Goroutines, - // AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - // WrapTable: AWSWrapTable, - // AWSOutputType: AWSOutputType, - // AWSTableCols: AWSTableCols, - // AWSOutputDirectory: AWSOutputDirectory, - // Verbosity: Verbosity, - // AWSConfig: AWSConfig, - // Version: cmd.Root().Version, - // SkipAdminCheck: AWSSkipAdminCheck, - // } - // graphCommandClient.RunGraphCommand() - - graphCommandClient := aws.GraphCommand2{ - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, - AWSOutputDirectory: AWSOutputDirectory, - Verbosity: Verbosity, - AWSConfig: AWSConfig, - Version: cmd.Root().Version, - SkipAdminCheck: AWSSkipAdminCheck, - GlobalGraph: GlobalGraph, - } + // make vertices + // you can't update verticies - so we need to make all of the vertices that are roles in the in-scope accounts + // all at once to make sure they have the most information possible + fmt.Println("Making vertices for all profiles") + // for _, role := range GlobalRoles { + // role.MakeVertices(GlobalGraph) + // } + mergedNodes := aws.MergeNodes(GlobalNodes) + for _, node := range mergedNodes { + GlobalGraph.AddVertex( + node.Arn, + graph.VertexAttribute("Type", node.Type), + graph.VertexAttribute("Name", node.Name), + graph.VertexAttribute("VendorName", node.VendorName), + graph.VertexAttribute("IsAdminString", node.IsAdminString), + graph.VertexAttribute("CanPrivEscToAdminString", node.CanPrivEscToAdminString), + graph.VertexAttribute("AccountID", node.AccountID), + ) + + } + + // make pmapper edges + for _, edge := range GlobalPmapperData.Edges { + err := GlobalGraph.AddEdge( + edge.Source, + edge.Destination, + graph.EdgeAttribute(edge.ShortReason, edge.Reason), + ) + if err != nil { + if err == graph.ErrEdgeAlreadyExists { + // update the ege by copying the existing graph.Edge with attributes and add the new attributes + //fmt.Println("Edge already exists") + + // get the existing edge + existingEdge, _ := GlobalGraph.Edge(edge.Source, edge.Destination) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } - graphCommandClient.RunGraphCommand2() + // Add or update the attribute + existingProperties.Attributes[edge.ShortReason] = edge.Reason + GlobalGraph.UpdateEdge( + edge.Source, + edge.Destination, + graph.EdgeAttributes(existingProperties.Attributes), + ) + } } + } + //making edges + fmt.Println("Making edges for all profiles") + for _, node := range mergedNodes { + if node.Type == "Role" { + node.MakeRoleEdges(GlobalGraph) + } } - // // print the edges to the screen - // edges, _ := GlobalGraph.Edges() - // for _, edge := range edges { - // fmt.Println(edge) - // } + // print how many nodes and edges are in the graph to the screen and exit + + for _, profile := range AWSProfiles { + var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) + caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) + if err != nil { + continue + } + + caperCommandClient := aws.CaperCommand{ + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + AWSOutputDirectory: AWSOutputDirectory, + Verbosity: Verbosity, + AWSConfig: AWSConfig, + Version: cmd.Root().Version, + SkipAdminCheck: AWSSkipAdminCheck, + GlobalGraph: GlobalGraph, + } + + caperCommandClient.RunCaperCommand() + filename := fmt.Sprintf("./mygraph-%s-%s.gv", ptr.ToString(caller.Account), time.Now().Format("2006-01-02-15-04-05")) + file, _ := os.Create(filename) + _ = draw.DOT(GlobalGraph, file, draw.GraphAttribute( + "ranksep", "3", + )) + } } func runIamSimulatorCommand(cmd *cobra.Command, args []string) { @@ -2123,39 +2254,40 @@ func init() { AWSCommands.PersistentFlags().StringVar(&AWSMFAToken, "mfa-token", "", "MFA Token") AWSCommands.AddCommand( + AccessKeysCommand, AllChecksCommand, ApiGwCommand, - RoleTrustCommand, - AccessKeysCommand, - InstancesCommand, + BucketsCommand, + CaperCommand, + CloudformationCommand, + CodeBuildCommand, + DatabasesCommand, ECSTasksCommand, - ElasticNetworkInterfacesCommand, - InventoryCommand, - EndpointsCommand, - SecretsCommand, - Route53Command, ECRCommand, - SQSCommand, - SNSCommand, EKSCommand, - OutboundAssumedRolesCommand, + ElasticNetworkInterfacesCommand, + EndpointsCommand, EnvsCommand, - PrincipalsCommand, - IamSimulatorCommand, FilesystemsCommand, - BucketsCommand, - PermissionsCommand, - CloudformationCommand, - CodeBuildCommand, - RAMCommand, - TagsCommand, + GraphCommand, + IamSimulatorCommand, + InstancesCommand, + InventoryCommand, LambdasCommand, NetworkPortsCommand, + OrgsCommand, + OutboundAssumedRolesCommand, + PermissionsCommand, + PrincipalsCommand, PmapperCommand, + RAMCommand, ResourceTrustsCommand, - OrgsCommand, - DatabasesCommand, - GraphCommand, + RoleTrustCommand, + Route53Command, + SQSCommand, + SNSCommand, + SecretsCommand, + TagsCommand, WorkloadsCommand, ) From e4f3421ea1e05125419854a2074a255410e92bf2 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Fri, 2 Feb 2024 13:23:49 -0500 Subject: [PATCH 12/29] Added functionailty to hightlight admins in caper command --- aws/caper.go | 324 +++++++++++++++++++++++++++++++------------------ aws/pmapper.go | 9 +- cli/aws.go | 66 +++++----- go.mod | 14 ++- go.sum | 29 +++++ 5 files changed, 288 insertions(+), 154 deletions(-) diff --git a/aws/caper.go b/aws/caper.go index 31a71b1..27189fd 100644 --- a/aws/caper.go +++ b/aws/caper.go @@ -19,19 +19,20 @@ import ( type CaperCommand struct { // General configuration data - Caller sts.GetCallerIdentityOutput - AWSRegions []string - Goroutines int - AWSProfile string - WrapTable bool - AWSOutputType string - AWSTableCols string - Verbosity int - AWSOutputDirectory string - AWSConfig aws.Config - Version string - SkipAdminCheck bool - GlobalGraph graph.Graph[string, string] + Caller sts.GetCallerIdentityOutput + AWSRegions []string + Goroutines int + AWSProfile string + WrapTable bool + AWSOutputType string + AWSTableCols string + Verbosity int + AWSOutputDirectory string + AWSConfig aws.Config + Version string + SkipAdminCheck bool + GlobalGraph graph.Graph[string, string] + PmapperDataBasePath string output internal.OutputData2 modLog *logrus.Entry @@ -164,7 +165,11 @@ func (m *CaperCommand) generateInboundPrivEscTableData() ([]string, [][]string, //trim the last newline from csvPaths paths = strings.TrimSuffix(paths, "\n") - privescPathsBody = append(privescPathsBody, []string{aws.ToString(m.Caller.Account), s, d, paths}) + if destinationVertexWithProperties.Attributes["IsAdminString"] == "Yes" { + privescPathsBody = append(privescPathsBody, []string{aws.ToString(m.Caller.Account), s, magenta(d), paths}) + } else { + privescPathsBody = append(privescPathsBody, []string{aws.ToString(m.Caller.Account), s, d, paths}) + } } } @@ -322,12 +327,12 @@ func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) [] var newNodes []Node // get thisAccount id from role arn - var thisAccount string - if len(a.Arn) >= 25 { - thisAccount = a.Arn[13:25] - } else { - fmt.Sprintf("Could not get account number from this role arn%s", a.Arn) - } + // var thisAccount string + // if len(a.Arn) >= 25 { + // thisAccount = a.Arn[13:25] + // } else { + // fmt.Sprintf("Could not get account number from this role arn%s", a.Arn) + // } for _, TrustedPrincipal := range a.TrustedPrincipals { //get account id from the trusted principal arn @@ -341,30 +346,32 @@ func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) [] // If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role // if the role we are looking at trusts root in it's own account - if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) { + // if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) { - newNodes = append(newNodes, Node{ - Arn: a.Arn, - Type: "Account", - AccountID: a.AccountID, - Name: a.Name, - }) + // newNodes = append(newNodes, Node{ + // Arn: a.Arn, + // Type: "Account", + // AccountID: a.AccountID, + // Name: a.Name, + // }) - } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" { + // } else + + if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" { newNodes = append(newNodes, Node{ - Arn: a.Arn, + Arn: fmt.Sprintf("%s-%s", a.Arn, TrustedPrincipal.VendorName), Type: "Account", AccountID: a.AccountID, Name: a.Name, VendorName: TrustedPrincipal.VendorName, }) - } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) { - newNodes = append(newNodes, Node{ - Arn: TrustedPrincipal.TrustedPrincipal, - Type: "Account", - AccountID: trustedPrincipalAccount, - }) + // } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) { + // newNodes = append(newNodes, Node{ + // Arn: TrustedPrincipal.TrustedPrincipal, + // Type: "Account", + // AccountID: trustedPrincipalAccount, + // }) } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":user")) { newNodes = append(newNodes, Node{ @@ -393,14 +400,6 @@ func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) [] Type: "AssumedRole", AccountID: trustedPrincipalAccount, }) - - } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":assumed-role")) { - newNodes = append(newNodes, Node{ - Arn: TrustedPrincipal.TrustedPrincipal, - Type: "AssumedRole", - AccountID: trustedPrincipalAccount, - }) - } } // pmapper takes care of this part so commenting out for now - but leaving as a placeholder @@ -532,59 +531,6 @@ func mergeNodeData(existingNode Node, newNode Node) Node { return existingNode } -func (a *Node) mergeAttributes(newAttributes map[string]any) { - if a.Arn == "" { - a.Arn = newAttributes["Arn"].(string) - } - if a.Name == "" { - a.Name = newAttributes["Name"].(string) - } - if a.Type == "" { - a.Type = newAttributes["Type"].(string) - } - if a.AccountID == "" { - a.AccountID = newAttributes["AccountID"].(string) - } - if a.CanPrivEscToAdminString == "" { - a.CanPrivEscToAdminString = newAttributes["CanPrivEscToAdminString"].(string) - } - if a.IsAdminString == "" { - a.IsAdminString = newAttributes["IsAdminString"].(string) - } - if a.VendorName == "" { - a.VendorName = newAttributes["VendorName"].(string) - } - // if a.AccessKeys does not exist, set it to the value in newAttributes - if a.AccessKeys == 0 { - a.AccessKeys = newAttributes["AccessKeys"].(int) - } - if a.ActivePassword == false { - a.ActivePassword = newAttributes["ActivePassword"].(bool) - } - if a.HasMfa == false { - a.HasMfa = newAttributes["HasMfa"].(bool) - } - if a.PathToAdmin == false { - a.PathToAdmin = newAttributes["PathToAdmin"].(bool) - } - if a.AttachedPolicies == nil { - a.AttachedPolicies = newAttributes["AttachedPolicies"].([]AttachedPolicies) - } - if a.TrustedFederatedProviders == nil { - a.TrustedFederatedProviders = newAttributes["TrustedFederatedProviders"].([]TrustedFederatedProvider) - } - if a.TrustedPrincipals == nil { - a.TrustedPrincipals = newAttributes["TrustedPrincipals"].([]TrustedPrincipal) - } - if a.TrustedServices == nil { - a.TrustedServices = newAttributes["TrustedServices"].([]TrustedService) - } - // if a.TrustsDoc.Statement == nil { - // a.TrustsDoc = newAttributes["TrustsDoc"].(policy.TrustPolicyDocument) - // } - -} - func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { // get thisAccount id from role arn @@ -616,8 +562,34 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { graph.EdgeAttribute("AssumeRole", "can assume (because of an explicit same account trust) "), ) if err != nil { - fmt.Println(err) - fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Same account explicit trust") + //fmt.Println(err) + //fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Same account explicit trust") + if err == graph.ErrEdgeAlreadyExists { + // update the ege by copying the existing graph.Edge with attributes and add the new attributes + //fmt.Println("Edge already exists") + + // get the existing edge + existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.TrustedPrincipal, a.Arn) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } + + // Add or update the attribute + existingProperties.Attributes["AssumeRole"] = "can assume (because of an explicit same account trust) " + err = GlobalGraph.UpdateEdge( + TrustedPrincipal.TrustedPrincipal, + a.Arn, + graph.EdgeAttributes(existingProperties.Attributes), + ) + if err != nil { + fmt.Println(err) + } + } + } } @@ -647,9 +619,9 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { // if the resource is * or the resource is this role arn, then this principal can assume this role if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.Arn) { // make a CAN_ASSUME relationship between the trusted principal and this role - //evalutate if the princiapl is a user or a role and set a variable accordingly + //evaluate if the principal is a user or a role and set a variable accordingly //var principalType schema.NodeLabel - if strings.EqualFold(PermissionsRow.Type, "User") { + if strings.EqualFold(PermissionsRow.Type, "User") || strings.EqualFold(PermissionsRow.Type, "Role") { err := GlobalGraph.AddEdge( PermissionsRow.Arn, a.Arn, @@ -657,19 +629,33 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { graph.EdgeAttribute("AssumeRole", "can assume (because of a same account root trust and trusted principal has permission to assume role) "), ) if err != nil { - fmt.Println(err) - fmt.Println(PermissionsRow.Arn + a.Arn + "Same account root trust and trusted principal has permission to assume role") - } - } else if strings.EqualFold(PermissionsRow.Type, "Role") { - err := GlobalGraph.AddEdge( - PermissionsRow.Arn, - a.Arn, - //graph.EdgeAttribute("AssumeRole", "Same account root trust and trusted principal has permission to assume role"), - graph.EdgeAttribute("AssumeRole", "can assume (because of a same account root trust and trusted principal has permission to assume role) "), - ) - if err != nil { - fmt.Println(err) - fmt.Println(PermissionsRow.Arn + a.Arn + "Same account root trust and trusted principal has permission to assume role") + // fmt.Println(err) + // fmt.Println(PermissionsRow.Arn + a.Arn + "Same account root trust and trusted principal has permission to assume role") + if err == graph.ErrEdgeAlreadyExists { + // update the ege by copying the existing graph.Edge with attributes and add the new attributes + + // get the existing edge + existingEdge, _ := GlobalGraph.Edge(PermissionsRow.Arn, a.Arn) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } + + // Add or update the attribute + existingProperties.Attributes["AssumeRole"] = "can assume (because of a same account root trust and trusted principal has permission to assume role) " + err = GlobalGraph.UpdateEdge( + PermissionsRow.Arn, + a.Arn, + graph.EdgeAttributes(existingProperties.Attributes), + ) + if err != nil { + fmt.Println(err) + } + } + } } } @@ -689,8 +675,32 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { graph.EdgeAttribute("VendorAssumeRole", "can assume (because of a cross account root trust and trusted principal is a vendor) "), ) if err != nil { - fmt.Println(err) - fmt.Println(TrustedPrincipal.VendorName + a.Arn + "Cross account root trust and trusted principal is a vendor") + // fmt.Println(err) + // fmt.Println(TrustedPrincipal.VendorName + a.Arn + "Cross account root trust and trusted principal is a vendor") + if err == graph.ErrEdgeAlreadyExists { + // update the ege by copying the existing graph.Edge with attributes and add the new attributes + + // get the existing edge + existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.VendorName, a.Arn) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } + + // Add or update the attribute + existingProperties.Attributes["VendorAssumeRole"] = "can assume (because of a cross account root trust and trusted principal is a vendor) " + err := GlobalGraph.UpdateEdge( + fmt.Sprintf("%s-%s", a.Arn, TrustedPrincipal.VendorName), + a.Arn, + graph.EdgeAttributes(existingProperties.Attributes), + ) + if err != nil { + fmt.Println(err) + } + } } } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) { @@ -723,8 +733,32 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { graph.EdgeAttribute("CrossAccountAssumeRole", "can assume (because of a cross account root trust and trusted principal has permission to assume role) "), ) if err != nil { - fmt.Println(err) - fmt.Println(PermissionsRow.Arn + a.Arn + "Cross account root trust and trusted principal has permission to assume role") + //fmt.Println(err) + //fmt.Println(PermissionsRow.Arn + a.Arn + "Cross account root trust and trusted principal has permission to assume role") + if err == graph.ErrEdgeAlreadyExists { + // update the ege by copying the existing graph.Edge with attributes and add the new attributes + + // get the existing edge + existingEdge, _ := GlobalGraph.Edge(PermissionsRow.Arn, a.Arn) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } + + // Add or update the attribute + existingProperties.Attributes["CrossAccountAssumeRole"] = "can assume (because of a cross account root trust and trusted principal has permission to assume role) " + err = GlobalGraph.UpdateEdge( + PermissionsRow.Arn, + a.Arn, + graph.EdgeAttributes(existingProperties.Attributes), + ) + if err != nil { + fmt.Println(err) + } + } } } else if strings.EqualFold(PermissionsRow.Type, "Role") { @@ -735,8 +769,32 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { graph.EdgeAttribute("CrossAccountAssumeRole", "can assume (because of a cross account root trust and trusted principal has permission to assume role) "), ) if err != nil { - fmt.Println(err) - fmt.Println(PermissionsRow.Arn + a.Arn + "Cross account root trust and trusted principal has permission to assume role") + //fmt.Println(err) + //fmt.Println(PermissionsRow.Arn + a.Arn + "Cross account root trust and trusted principal has permission to assume role") + if err == graph.ErrEdgeAlreadyExists { + // update the ege by copying the existing graph.Edge with attributes and add the new attributes + + // get the existing edge + existingEdge, _ := GlobalGraph.Edge(PermissionsRow.Arn, a.Arn) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } + + // Add or update the attribute + existingProperties.Attributes["CrossAccountAssumeRole"] = "can assume (because of a cross account root trust and trusted principal has permission to assume role) " + err = GlobalGraph.UpdateEdge( + PermissionsRow.Arn, + a.Arn, + graph.EdgeAttributes(existingProperties.Attributes), + ) + if err != nil { + fmt.Println(err) + } + } } } } @@ -765,8 +823,32 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { graph.EdgeAttribute("FederatedAssumeRole", "can assume (because of a trusted federated provider) "), ) if err != nil { - fmt.Println(err) - fmt.Println(TrustedFederatedProvider.TrustedFederatedProvider + a.Arn + "Trusted federated provider") + //fmt.Println(err) + //fmt.Println(TrustedFederatedProvider.TrustedFederatedProvider + a.Arn + "Trusted federated provider") + if err == graph.ErrEdgeAlreadyExists { + // update the ege by copying the existing graph.Edge with attributes and add the new attributes + + // get the existing edge + existingEdge, _ := GlobalGraph.Edge(TrustedFederatedProvider.TrustedFederatedProvider, a.Arn) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } + + // Add or update the attribute + existingProperties.Attributes["FederatedAssumeRole"] = "can assume (because of a trusted federated provider) " + err = GlobalGraph.UpdateEdge( + TrustedFederatedProvider.TrustedFederatedProvider, + a.Arn, + graph.EdgeAttributes(existingProperties.Attributes), + ) + if err != nil { + fmt.Println(err) + } + } } } diff --git a/aws/pmapper.go b/aws/pmapper.go index fb98940..9bb4f58 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -31,10 +31,11 @@ type PmapperModule struct { WrapTable bool // Main module data - pmapperGraph graph.Graph[string, string] - Nodes []Node - Edges []Edge - CommandCounter internal.CommandCounter + PmapperDataBasePath string + pmapperGraph graph.Graph[string, string] + Nodes []Node + Edges []Edge + CommandCounter internal.CommandCounter // Used to store output data for pretty printing output internal.OutputData2 modLog *logrus.Entry diff --git a/cli/aws.go b/cli/aws.go index 874d2ac..61a7b37 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -75,13 +75,14 @@ var ( red = color.New(color.FgRed).SprintFunc() defaultOutputDir = ptr.ToString(internal.GetLogDirPath()) - AWSProfile string - AWSProfilesList string - AWSAllProfiles bool - AWSProfiles []string - AWSConfirm bool - AWSOutputType string - AWSTableCols string + AWSProfile string + AWSProfilesList string + AWSAllProfiles bool + AWSProfiles []string + AWSConfirm bool + AWSOutputType string + AWSTableCols string + PmapperDataBasePath string AWSOutputDirectory string AWSSkipAdminCheck bool @@ -141,7 +142,10 @@ var ( CaperCommand = &cobra.Command{ Use: "caper", Aliases: []string{"caperParse"}, - Short: "Cross-Account Privilege Escalation Route finder. Best run with multiple profiles, ideally the -l flag", + Short: "Cross-Account Privilege Escalation Route finder.\n" + + "Needs to be run with multiple profiles using -l or -a flag\n" + + "Needs pmapper data to be present", + Long: "\nUse case examples:\n" + os.Args[0] + " aws caper -l file_with_profile_names.txt", PreRun: awsPreRun, @@ -460,7 +464,6 @@ var ( Run: runTagsCommand, PostRun: awsPostRun, } - PmapperCommand = &cobra.Command{ Use: "pmapper", @@ -1182,19 +1185,20 @@ func runCaperCommand(cmd *cobra.Command, args []string) { } caperCommandClient := aws.CaperCommand{ - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, - AWSOutputDirectory: AWSOutputDirectory, - Verbosity: Verbosity, - AWSConfig: AWSConfig, - Version: cmd.Root().Version, - SkipAdminCheck: AWSSkipAdminCheck, - GlobalGraph: GlobalGraph, + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + AWSOutputDirectory: AWSOutputDirectory, + Verbosity: Verbosity, + AWSConfig: AWSConfig, + Version: cmd.Root().Version, + SkipAdminCheck: AWSSkipAdminCheck, + GlobalGraph: GlobalGraph, + PmapperDataBasePath: PmapperDataBasePath, } caperCommandClient.RunCaperCommand() @@ -1401,12 +1405,13 @@ func runPmapperCommand(cmd *cobra.Command, args []string) { continue } m := aws.PmapperModule{ - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } m.PrintPmapperData(AWSOutputDirectory, Verbosity) } @@ -2238,6 +2243,10 @@ func init() { // buckets command flags (for bucket policies) BucketsCommand.Flags().BoolVarP(&CheckBucketPolicies, "with-policies", "", false, "Analyze bucket policies (this is already done in the resource-trusts command)") + // pmapper flag for pmapper and caper commands + //PmapperCommand.Flags().StringVarP(&PmapperDataBasePath, "pmapper-data-basepath", "pdata", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)") + //CaperCommand.Flags().StringVarP(&PmapperDataBasePath, "pmapper-data-basepath", "pdata", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)") + // Global flags for the AWS modules AWSCommands.PersistentFlags().StringVarP(&AWSProfile, "profile", "p", "", "AWS CLI Profile Name") AWSCommands.PersistentFlags().StringVarP(&AWSProfilesList, "profiles-list", "l", "", "File containing a AWS CLI profile names separated by newlines") @@ -2252,6 +2261,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().StringVar(&PmapperDataBasePath, "pmapper-data-basepath", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)") AWSCommands.AddCommand( AccessKeysCommand, diff --git a/go.mod b/go.mod index ad3ab06..508fd11 100644 --- a/go.mod +++ b/go.mod @@ -67,7 +67,16 @@ require ( ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/golang-jwt/jwt/v5 v5.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + golang.org/x/sync v0.5.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -108,6 +117,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/sfn v1.24.6 github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect + github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v0.9.1 github.com/dimchansky/utfbom v1.1.1 // indirect github.com/go-openapi/errors v0.21.0 // indirect github.com/go-openapi/strfmt v0.21.10 // indirect @@ -124,7 +136,7 @@ require ( github.com/neo4j/neo4j-go-driver/v5 v5.14.0 github.com/oklog/ulid v1.3.1 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect - github.com/rivo/uniseg v0.4.4 // indirect + github.com/rivo/uniseg v0.4.6 // indirect github.com/spf13/pflag v1.0.5 // indirect go.mongodb.org/mongo-driver v1.13.1 // indirect golang.org/x/net v0.19.0 // indirect diff --git a/go.sum b/go.sum index c3e6aaf..cf73eba 100644 --- a/go.sum +++ b/go.sum @@ -179,10 +179,20 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.26.6 h1:HJeiuZ2fldpd0WqngyMR6KW7ofkX github.com/aws/aws-sdk-go-v2/service/sts v1.26.6/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bishopfox/awsservicemap v1.0.3 h1:0T+mJLwG+vQV9+o3dzwzxhWJWE40VpoCLWtaPBwixYc= github.com/bishopfox/awsservicemap v1.0.3/go.mod h1:oy9Fyqh6AozQjShSx+zRNouTlp7k3z3YEMoFkN8rquc= github.com/bishopfox/knownawsaccountslookup v0.0.0-20231228165844-c37ef8df33cb h1:ot96tC/kdm0GKV1kl+aXJorqJbyx92R9bjRQvbBmLKU= github.com/bishopfox/knownawsaccountslookup v0.0.0-20231228165844-c37ef8df33cb/go.mod h1:2OnSqu4B86+2xGSIE5D4z3Rze9yJ/LNNjNXHhwMR+vY= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -228,11 +238,16 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4= github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -240,6 +255,14 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/neo4j/neo4j-go-driver/v5 v5.14.0 h1:5x3vD4HkXQIktlG63jSG8v9iweGjmObIPU7Y9U0ThUI= github.com/neo4j/neo4j-go-driver/v5 v5.14.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -252,9 +275,12 @@ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= +github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -301,6 +327,8 @@ golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -310,6 +338,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= From 24b7076bc33ba08299f0afa9f72009246bb27be2 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Thu, 8 Feb 2024 16:32:15 -0500 Subject: [PATCH 13/29] saving place in caper command --- aws/caper.go | 151 ++++++++++++++++++++-------------- aws/outbound-assumed-roles.go | 133 +++++++++++++++++++++++++++++- aws/pmapper.go | 2 +- aws/role-trusts.go | 2 + aws/shared.go | 28 +++++++ cli/aws.go | 2 +- internal/aws.go | 36 +++++++- internal/cache.go | 34 ++++++++ 8 files changed, 321 insertions(+), 67 deletions(-) diff --git a/aws/caper.go b/aws/caper.go index 27189fd..5fb12b2 100644 --- a/aws/caper.go +++ b/aws/caper.go @@ -206,7 +206,7 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo var TrustedFederatedProviders []TrustedFederatedProvider //var TrustedFederatedSubjects string var trustedProvider string - var trustedSubjects string + var trustedSubjects []string var vendorName string for _, statement := range trustsdoc.Statement { @@ -238,12 +238,14 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo for _, federated := range statement.Principal.Federated { if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 { trustedProvider = "GitHub" - trustedSubjects := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",") - if trustedSubjects == "" { - trustedSubjects = "ALL REPOS!!!" - } else { - trustedSubjects = "Repos: " + trustedSubjects + //trustedSubjects = strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",") + trustedSubjects = statement.Condition.StringLike.TokenActionsGithubusercontentComSub + if strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",") == "" { + trustedSubjects = append(trustedSubjects, "ALL REPOS") } + // } else { + // trustedSubjects = "Repos: " + trustedSubjects + // } } else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" { if strings.Contains(statement.Principal.Federated[0], "AWSSSO") { @@ -251,29 +253,29 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo } else if strings.Contains(statement.Principal.Federated[0], "Okta") { trustedProvider = "Okta" // (" + statement.Principal.Federated[0] + ")" } - trustedSubjects = "Not applicable" + trustedSubjects = append(trustedSubjects, "Not applicable") } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" { trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" if statement.Condition.StringEquals.OidcEksSub != "" { - trustedSubjects = statement.Condition.StringEquals.OidcEksSub + trustedSubjects = append(trustedSubjects, statement.Condition.StringEquals.OidcEksSub) } else if statement.Condition.StringLike.OidcEksSub != "" { - trustedSubjects = statement.Condition.StringLike.OidcEksSub + trustedSubjects = append(trustedSubjects, statement.Condition.StringLike.OidcEksSub) } else { - trustedSubjects = "ALL SERVICE ACCOUNTS!" + trustedSubjects = append(trustedSubjects, "ALL SERVICE ACCOUNTS!") } } else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" { trustedProvider = "Cognito" // (" + statement.Principal.Federated[0] + ")" if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" { - trustedSubjects = statement.Condition.ForAnyValueStringLike.CognitoAMR + trustedSubjects = append(trustedSubjects, statement.Condition.ForAnyValueStringLike.CognitoAMR) } } else { if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") { trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" - trustedSubjects = "ALL SERVICE ACCOUNTS!" + trustedSubjects = append(trustedSubjects, "ALL SERVICE ACCOUNTS!") } else if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") { trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" } - trustedSubjects = "Not applicable" + trustedSubjects = append(trustedSubjects, "Not applicable") } TrustedFederatedProviders = append(TrustedFederatedProviders, TrustedFederatedProvider{ @@ -359,10 +361,11 @@ func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) [] if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" { newNodes = append(newNodes, Node{ - Arn: fmt.Sprintf("%s-%s", a.Arn, TrustedPrincipal.VendorName), + Arn: fmt.Sprintf("%s [%s]", TrustedPrincipal.TrustedPrincipal, TrustedPrincipal.VendorName), + //Arn: TrustedPrincipal.VendorName, Type: "Account", - AccountID: a.AccountID, - Name: a.Name, + AccountID: trustedPrincipalAccount, + Name: TrustedPrincipal.VendorName, VendorName: TrustedPrincipal.VendorName, }) @@ -411,11 +414,20 @@ func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) [] for _, TrustedFederatedProvider := range a.TrustedFederatedProviders { // make relationship from trusted federated provider to this role of type can assume - newNodes = append(newNodes, Node{ - Arn: TrustedFederatedProvider.TrustedFederatedProvider, - Type: "FederatedIdentity", - AccountID: a.AccountID, - }) + var providerAndSubject string + for _, trustedSubject := range TrustedFederatedProvider.TrustedSubjects { + if trustedSubject == "Not applicable" { + providerAndSubject = TrustedFederatedProvider.ProviderShortName + } else { + providerAndSubject = TrustedFederatedProvider.ProviderShortName + ":" + trustedSubject + } + newNodes = append(newNodes, Node{ + Arn: providerAndSubject, + Name: TrustedFederatedProvider.ProviderShortName, + Type: "FederatedIdentity", + AccountID: a.AccountID, + }) + } } @@ -609,11 +621,12 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } if PermissionsRowAccount == thisAccount { - // lets only look for rows that have sts:AssumeRole permissions - if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || - strings.EqualFold(PermissionsRow.Action, "*") || - strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || - strings.EqualFold(PermissionsRow.Action, "sts:*") { + if matchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") { + // // lets only look for rows that have sts:AssumeRole permissions + // if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || + // strings.EqualFold(PermissionsRow.Action, "*") || + // strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || + // strings.EqualFold(PermissionsRow.Action, "sts:*") { // lets only focus on rows that have an effect of Allow if strings.EqualFold(PermissionsRow.Effect, "Allow") { // if the resource is * or the resource is this role arn, then this principal can assume this role @@ -669,7 +682,8 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { err := GlobalGraph.AddEdge( //TrustedPrincipal.TrustedPrincipal, - TrustedPrincipal.VendorName, + //TrustedPrincipal.VendorName, + fmt.Sprintf("%s [%s]", TrustedPrincipal.TrustedPrincipal, TrustedPrincipal.VendorName), a.Arn, //graph.EdgeAttribute("VendorAssumeRole", "Cross account root trust and trusted principal is a vendor"), graph.EdgeAttribute("VendorAssumeRole", "can assume (because of a cross account root trust and trusted principal is a vendor) "), @@ -693,7 +707,8 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { // Add or update the attribute existingProperties.Attributes["VendorAssumeRole"] = "can assume (because of a cross account root trust and trusted principal is a vendor) " err := GlobalGraph.UpdateEdge( - fmt.Sprintf("%s-%s", a.Arn, TrustedPrincipal.VendorName), + //fmt.Sprintf("%s-%s", a.Arn, TrustedPrincipal.VendorName), + TrustedPrincipal.TrustedPrincipal, a.Arn, graph.EdgeAttributes(existingProperties.Attributes), ) @@ -715,10 +730,11 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } if PermissionsRowAccount == trustedPrincipalAccount { // lets only look for rows that have sts:AssumeRole permissions - if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || - strings.EqualFold(PermissionsRow.Action, "*") || - strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || - strings.EqualFold(PermissionsRow.Action, "sts:*") { + if matchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") { + // if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || + // strings.EqualFold(PermissionsRow.Action, "*") || + // strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || + // strings.EqualFold(PermissionsRow.Action, "sts:*") { // lets only focus on rows that have an effect of Allow if strings.EqualFold(PermissionsRow.Effect, "Allow") { // if the resource is * or the resource is this role arn, then this principal can assume this role @@ -816,41 +832,50 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { for _, TrustedFederatedProvider := range a.TrustedFederatedProviders { // make relationship from trusted federated provider to this role of type can assume - err := GlobalGraph.AddEdge( - TrustedFederatedProvider.TrustedFederatedProvider, - a.Arn, - //graph.EdgeAttribute("FederatedAssumeRole", "Trusted federated provider"), - graph.EdgeAttribute("FederatedAssumeRole", "can assume (because of a trusted federated provider) "), - ) - if err != nil { - //fmt.Println(err) - //fmt.Println(TrustedFederatedProvider.TrustedFederatedProvider + a.Arn + "Trusted federated provider") - if err == graph.ErrEdgeAlreadyExists { - // update the ege by copying the existing graph.Edge with attributes and add the new attributes - - // get the existing edge - existingEdge, _ := GlobalGraph.Edge(TrustedFederatedProvider.TrustedFederatedProvider, a.Arn) - // get the map of attributes - existingProperties := existingEdge.Properties - // add the new attributes to attributes map within the properties struct - // Check if the Attributes map is initialized, if not, initialize it - if existingProperties.Attributes == nil { - existingProperties.Attributes = make(map[string]string) - } + var providerAndSubject string + for _, trustedSubject := range TrustedFederatedProvider.TrustedSubjects { + if trustedSubject == "Not applicable" { + providerAndSubject = TrustedFederatedProvider.ProviderShortName + } else { + providerAndSubject = TrustedFederatedProvider.ProviderShortName + ":" + trustedSubject + } + + err := GlobalGraph.AddEdge( + providerAndSubject, + a.Arn, + //graph.EdgeAttribute("FederatedAssumeRole", "Trusted federated provider"), + graph.EdgeAttribute("FederatedAssumeRole", "can assume (because of a trusted federated provider) "), + ) + if err != nil { + //fmt.Println(err) + //fmt.Println(TrustedFederatedProvider.TrustedFederatedProvider + a.Arn + "Trusted federated provider") + if err == graph.ErrEdgeAlreadyExists { + // update the ege by copying the existing graph.Edge with attributes and add the new attributes - // Add or update the attribute - existingProperties.Attributes["FederatedAssumeRole"] = "can assume (because of a trusted federated provider) " - err = GlobalGraph.UpdateEdge( - TrustedFederatedProvider.TrustedFederatedProvider, - a.Arn, - graph.EdgeAttributes(existingProperties.Attributes), - ) - if err != nil { - fmt.Println(err) + // get the existing edge + existingEdge, _ := GlobalGraph.Edge(TrustedFederatedProvider.TrustedFederatedProvider, a.Arn) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } + + // Add or update the attribute + existingProperties.Attributes["FederatedAssumeRole"] = "can assume (because of a trusted federated provider) " + err = GlobalGraph.UpdateEdge( + providerAndSubject, + a.Arn, + graph.EdgeAttributes(existingProperties.Attributes), + ) + if err != nil { + fmt.Println(err) + } } } - } + } } } diff --git a/aws/outbound-assumed-roles.go b/aws/outbound-assumed-roles.go index a65009f..d6acebd 100644 --- a/aws/outbound-assumed-roles.go +++ b/aws/outbound-assumed-roles.go @@ -50,6 +50,7 @@ type OutboundAssumeRoleEntry struct { SourcePrincipal string DestinationAccount string DestinationPrincipal string + Action string LogTimestamp string } @@ -117,6 +118,24 @@ type CloudTrailEvent struct { } `json:"tlsDetails"` } +var interestingCrossAccountEventNames = []string{ + "AssumeRole", + "AssumeRoleWithSAML", + "AssumeRoleWithWebIdentity", + "GetObject", + "ListBuckets", + "BatchGetImage", + "GetDownloadUrlForLayer", + "SendMessage", + "GetQueueUrl", + "Invoke20150331", + "RunInstances", + "RunTask", + "StartTask", + "CreateTask", + "CreateTaskSet", +} + func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDirectory string, verbosity int) { // These struct values are used by the output module m.output.Verbosity = verbosity @@ -171,6 +190,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir "Source Principal", //"Destination Account", "Destination Principal", + "Action", "Log Entry Timestamp", } @@ -194,6 +214,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir "Source Principal", //"Destination Account", "Destination Principal", + "Action", "Log Entry Timestamp", } // Otherwise, use the default columns. @@ -206,6 +227,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir "Source Principal", //"Destination Account", "Destination Principal", + "Action", "Log Entry Timestamp", } } @@ -222,6 +244,7 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDir m.OutboundAssumeRoleEntries[i].SourcePrincipal, //m.OutboundAssumeRoleEntries[i].DestinationAccount, m.OutboundAssumeRoleEntries[i].DestinationPrincipal, + m.OutboundAssumeRoleEntries[i].Action, m.OutboundAssumeRoleEntries[i].LogTimestamp, }, ) @@ -279,9 +302,12 @@ func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup, m.modLog.Error(err) } if res { + // wg.Add(1) + // m.CommandCounter.Total++ + // m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver) wg.Add(1) m.CommandCounter.Total++ - m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver) + m.getCrossAccountBatchGetImageEntriesPerRegion(r, wg, semaphore, dataReceiver) } } @@ -359,6 +385,7 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string, SourcePrincipal: sourcePrincipal, DestinationAccount: destinationAccount, DestinationPrincipal: destinationPrincipal, + Action: "sts:AssumeRole", LogTimestamp: logTimestamp, } } @@ -376,3 +403,107 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string, } } + +// get cross account batch get image entries +func (m *OutboundAssumedRolesModule) getCrossAccountBatchGetImageEntriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) { + defer func() { + m.CommandCounter.Executing-- + m.CommandCounter.Complete++ + wg.Done() + + }() + semaphore <- struct{}{} + defer func() { + <-semaphore + }() + // "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages. + var PaginationControl *string + //var LookupAttributes []types.LookupAttributes + //var LookupAttribute types.LookupAttribute + var pages int + + days := 0 - m.Days + endTime := aws.Time(time.Now()) + startTime := endTime.AddDate(0, 0, days) + for { + LookupEvents, err := m.CloudTrailClient.LookupEvents( + context.TODO(), + &cloudtrail.LookupEventsInput{ + EndTime: endTime, + StartTime: &startTime, + // LookupAttributes: []cloudtrailTypes.LookupAttribute{ + // { + // AttributeKey: cloudtrailTypes.LookupAttributeKeyEventName, + // AttributeValue: aws.String("BatchGetImage"), + // }, + // }, + NextToken: PaginationControl, + }, + + func(o *cloudtrail.Options) { + o.Region = r + }, + ) + if err != nil { + m.modLog.Error(err.Error()) + m.CommandCounter.Error++ + break + } + + for _, event := range LookupEvents.Events { + //eventData := *event.CloudTrailEvent + //fmt.Println(eventData) + var sourceAccount, sourcePrincipal, destinationAccount, destinationPrincipal, userType string + cloudtrailEvent := CloudTrailEvent{} + json.Unmarshal([]byte(*event.CloudTrailEvent), &cloudtrailEvent) + + for _, eventName := range interestingCrossAccountEventNames { + if aws.ToString(event.EventName) == eventName { + + // extract the source account and principal + if cloudtrailEvent.UserIdentity.Type == "AssumedRole" || cloudtrailEvent.UserIdentity.Type == "IAMUser" || cloudtrailEvent.UserIdentity.Type == "Role" { + if cloudtrailEvent.UserIdentity.Type == "AssumedRole" { + sourcePrincipal = cloudtrailEvent.UserIdentity.SessionContext.SessionIssuer.Arn + } else { + sourcePrincipal = cloudtrailEvent.UserIdentity.Arn + } + userType = cloudtrailEvent.UserIdentity.Type + sourceAccount = cloudtrailEvent.UserIdentity.AccountID + + //fmt.Printf("%s,%s,%s,%s\n", sourceAccount, sourcePrincipal, destinationAccount, destinationPrincipal) + + if cloudtrailEvent.Resources != nil { + destinationAccount = cloudtrailEvent.Resources[0].AccountID + destinationPrincipal = cloudtrailEvent.Resources[0].Arn + if sourceAccount != destinationAccount { + logTimestamp := cloudtrailEvent.EventTime.Format("2006-01-02 15:04:05") + dataReceiver <- OutboundAssumeRoleEntry{ + AWSService: "CloudTrail", + Region: r, + Type: userType, + SourceAccount: sourceAccount, + SourcePrincipal: sourcePrincipal, + DestinationAccount: destinationAccount, + DestinationPrincipal: destinationPrincipal, + Action: aws.ToString(event.EventName), + LogTimestamp: logTimestamp, + } + } + } + + } + } + } + } + + // The "NextToken" value is nil when there's no more data to return. + if LookupEvents.NextToken != nil { + PaginationControl = LookupEvents.NextToken + pages++ + } else { + PaginationControl = nil + break + } + } + +} diff --git a/aws/pmapper.go b/aws/pmapper.go index 9bb4f58..9148e8f 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -98,7 +98,7 @@ type TrustedService struct { type TrustedFederatedProvider struct { TrustedFederatedProvider string ProviderShortName string - TrustedSubjects string + TrustedSubjects []string //IsAdmin bool //CanPrivEscToAdmin bool } diff --git a/aws/role-trusts.go b/aws/role-trusts.go index dedba08..5dea60a 100644 --- a/aws/role-trusts.go +++ b/aws/role-trusts.go @@ -206,6 +206,7 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]strin for _, principal := range statement.Principal.AWS { if strings.Contains(principal, ":root") { //check to see if the accountID is known + fmt.Println(principal) accountID := strings.Split(principal, ":")[4] vendorName := m.vendors.GetVendorNameFromAccountID(accountID) if vendorName != "" { @@ -279,6 +280,7 @@ func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string) for _, statement := range role.trustsDoc.Statement { for _, principal := range statement.Principal.AWS { if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == "" { + fmt.Println(principal) accountID := strings.Split(principal, ":")[4] vendorName := m.vendors.GetVendorNameFromAccountID(accountID) if vendorName != "" { diff --git a/aws/shared.go b/aws/shared.go index d2b207a..b99eeeb 100644 --- a/aws/shared.go +++ b/aws/shared.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "os" + "regexp" "runtime" "strings" @@ -186,3 +187,30 @@ func removeStringFromSlice(slice []string, element string) []string { } return slice } + +// source: https://github.com/nccgroup/PMapper/blob/master/principalmapper/querying/local_policy_simulation.py +func composePattern(stringToTransform string) *regexp.Regexp { + // Escape special characters and replace wildcards + escaped := strings.ReplaceAll(stringToTransform, ".", "\\.") + escaped = strings.ReplaceAll(escaped, "*", ".*") + escaped = strings.ReplaceAll(escaped, "?", ".") + escaped = strings.ReplaceAll(escaped, "$", "\\$") + escaped = strings.ReplaceAll(escaped, "^", "\\^") + + // Compile the regular expression, ignoring case + pattern, err := regexp.Compile("(?i)^" + escaped + "$") + if err != nil { + panic("regexp compile error: " + err.Error()) + } + return pattern +} + +// source: https://github.com/nccgroup/PMapper/blob/master/principalmapper/querying/local_policy_simulation.py +// matchesAfterExpansion checks the stringToCheck against stringToCheckAgainst. +func matchesAfterExpansion(stringFromPolicyToCheck, stringToCheckAgainst string) bool { + // Transform the stringToCheckAgainst into a regex pattern + pattern := composePattern(stringToCheckAgainst) + + // Check if the pattern matches stringToCheck + return pattern.MatchString(stringFromPolicyToCheck) +} diff --git a/cli/aws.go b/cli/aws.go index 61a7b37..8c12b4a 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -2227,7 +2227,7 @@ func init() { SNSCommand.Flags().BoolVarP(&StoreSNSAccessPolicies, "policies", "", false, "Store all flagged access policies along with the output") // outbound-assumed-roles module flags - OutboundAssumedRolesCommand.Flags().IntVarP(&OutboundAssumedRolesDays, "days", "d", 7, "How many days of CloudTrail events should we go back and look at.") + OutboundAssumedRolesCommand.Flags().IntVarP(&OutboundAssumedRolesDays, "days", "d", -7, "How many days of CloudTrail events should we go back and look at.") // iam-simulator module flags IamSimulatorCommand.Flags().StringVar(&SimulatorPrincipal, "principal", "", "Principal Arn") diff --git a/internal/aws.go b/internal/aws.go index 77956b8..49cb173 100644 --- a/internal/aws.go +++ b/internal/aws.go @@ -3,6 +3,7 @@ package internal import ( "bufio" "context" + "encoding/gob" "fmt" "os" "regexp" @@ -18,6 +19,7 @@ import ( "github.com/aws/smithy-go/ptr" "github.com/bishopfox/awsservicemap" "github.com/kyokomi/emoji" + "github.com/patrickmn/go-cache" "github.com/sirupsen/logrus" "github.com/spf13/afero" ) @@ -30,11 +32,23 @@ var ( ConfigMap = map[string]aws.Config{} ) +func init() { + gob.Register(aws.Config{}) + gob.Register(sts.GetCallerIdentityOutput{}) +} + func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string) aws.Config { // Loads the AWS config file and returns a config object var cfg aws.Config var err error + // cacheKey := fmt.Sprintf("AWSConfigFileLoader-%s", AWSProfile) + // cached, found := Cache.Get(cacheKey) + // if found { + // cfg = cached.(aws.Config) + // return cfg + // } + // Check if the profile is already in the config map. If not, load it and retrieve the credentials. If it is, return the cached config object // The AssumeRoleOptions below are used to pass the MFA token to the AssumeRole call (when applicable) if _, ok := ConfigMap[AWSProfile]; !ok { @@ -81,6 +95,7 @@ func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string) // update the config map with the new config for future lookups ConfigMap[AWSProfile] = cfg //return the config object for this first iteration + //Cache.Set(cacheKey, cfg, cache.DefaultExpiration) return cfg } @@ -89,10 +104,21 @@ func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string) cfg = ConfigMap[AWSProfile] return cfg } + //Cache.Set(cacheKey, cfg, cache.DefaultExpiration) return cfg } func AWSWhoami(awsProfile string, version string, AwsMfaToken string) (*sts.GetCallerIdentityOutput, error) { + + cacheKey := fmt.Sprintf("sts-getCallerIdentity-%s", awsProfile) + if cached, found := Cache.Get(cacheKey); found { + // Correct type assertion: assert the type, not a variable. + if cachedValue, ok := cached.(*sts.GetCallerIdentityOutput); ok { + return cachedValue, nil + } + // Handle the case where type assertion fails, if necessary. + } + // Connects to STS and checks caller identity. Same as running "aws sts get-caller-identity" //fmt.Printf("[%s] Retrieving caller's identity\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version))) STSService := sts.NewFromConfig(AWSConfigFileLoader(awsProfile, version, AwsMfaToken)) @@ -103,10 +129,18 @@ func AWSWhoami(awsProfile string, version string, AwsMfaToken string) (*sts.GetC return CallerIdentity, err } + // Convert CallerIdentity to something i can store using the cache + Cache.Set(cacheKey, CallerIdentity, cache.DefaultExpiration) return CallerIdentity, err } func GetEnabledRegions(awsProfile string, version string, AwsMfaToken string) []string { + cacheKey := fmt.Sprintf("GetEnabledRegions-%s", awsProfile) + cached, found := Cache.Get(cacheKey) + if found { + return cached.([]string) + } + var enabledRegions []string ec2Client := ec2.NewFromConfig(ConfigMap[awsProfile]) regions, err := ec2Client.DescribeRegions( @@ -130,7 +164,7 @@ func GetEnabledRegions(awsProfile string, version string, AwsMfaToken string) [] for _, region := range regions.Regions { enabledRegions = append(enabledRegions, *region.RegionName) } - + Cache.Set(cacheKey, enabledRegions, cache.DefaultExpiration) return enabledRegions } diff --git a/internal/cache.go b/internal/cache.go index adb0dbc..4477cf2 100644 --- a/internal/cache.go +++ b/internal/cache.go @@ -89,6 +89,12 @@ type cacheEntry struct { Exp int64 } +type CacheableAWSConfig struct { + Region string + //Credentials aws.CredentialsProvider + //ConfigSources []interface{} +} + func SaveCacheToGobFiles(directory string, accountID string) error { err := os.MkdirAll(directory, 0755) if err != nil { @@ -96,6 +102,15 @@ func SaveCacheToGobFiles(directory string, accountID string) error { } for key, item := range Cache.Items() { + // if accountID != "" && strings.Contains(key, accountID) || + // strings.Contains(key, "AWSConfigFileLoader") || + // strings.Contains(key, "GetEnabledRegions") || + // strings.Contains(key, "GetCallerIdentity") { + // entry := cacheEntry{ + // Value: item.Object, + // Exp: item.Expiration, + // } + // only if the key contains the accountID if accountID != "" && strings.Contains(key, accountID) { entry := cacheEntry{ @@ -110,17 +125,36 @@ func SaveCacheToGobFiles(directory string, accountID string) error { } defer file.Close() + // if config, ok := item.Object.(aws.Config); ok { + // cacheableConfig := converAWSConfigToCacheableAWSConfig(config) + // encoder := gob.NewEncoder(file) + // err = encoder.Encode(cacheableConfig) + // if err != nil { + // sharedLogger.Errorf("Could not encode the following key: %s", key) + // return err + // } + + // } else { encoder := gob.NewEncoder(file) err = encoder.Encode(entry) if err != nil { sharedLogger.Errorf("Could not encode the following key: %s", key) return err } + // } } } return nil } +// func converAWSConfigToCacheableAWSConfig(config aws.Config) CacheableAWSConfig { +// return CacheableAWSConfig{ +// Region: config.Region, +// //Credentials: config.Credentials, +// //ConfigSources: config.ConfigSources, +// } +// } + var ErrDirectoryDoesNotExist = errors.New("directory does not exist") func LoadCacheFromGobFiles(directory string) error { From e4e548012fc555ed29702fe1b8b60cee804c1575 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Thu, 8 Feb 2024 16:56:51 -0500 Subject: [PATCH 14/29] revert test --- aws/role-trusts.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/aws/role-trusts.go b/aws/role-trusts.go index 5dea60a..dedba08 100644 --- a/aws/role-trusts.go +++ b/aws/role-trusts.go @@ -206,7 +206,6 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]strin for _, principal := range statement.Principal.AWS { if strings.Contains(principal, ":root") { //check to see if the accountID is known - fmt.Println(principal) accountID := strings.Split(principal, ":")[4] vendorName := m.vendors.GetVendorNameFromAccountID(accountID) if vendorName != "" { @@ -280,7 +279,6 @@ func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string) for _, statement := range role.trustsDoc.Statement { for _, principal := range statement.Principal.AWS { if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == "" { - fmt.Println(principal) accountID := strings.Split(principal, ":")[4] vendorName := m.vendors.GetVendorNameFromAccountID(accountID) if vendorName != "" { From dd6dd291c5fa404979647d65a9ba78eab2262f71 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:20:37 -0500 Subject: [PATCH 15/29] playing around with saving graph state between runs --- aws/caper.go | 21 ++++++-- aws/permissions.go | 4 ++ aws/shared.go | 28 ---------- cli/aws.go | 52 +----------------- internal/aws/policy/policy.go | 87 ++++++++++++++++++++++++++++++ internal/aws/policy/policy_test.go | 72 +++++++++++++++++++++++++ internal/aws/policy/statement.go | 15 +++--- internal/cache.go | 24 +++++++++ 8 files changed, 213 insertions(+), 90 deletions(-) diff --git a/aws/caper.go b/aws/caper.go index 5fb12b2..03545d2 100644 --- a/aws/caper.go +++ b/aws/caper.go @@ -96,6 +96,7 @@ func (m *CaperCommand) generateInboundPrivEscTableData() ([]string, [][]string, "Account", "Source", "Target", + "isTargetAdmin", "Summary", } @@ -112,6 +113,7 @@ func (m *CaperCommand) generateInboundPrivEscTableData() ([]string, [][]string, "Account", "Source", "Target", + "isTargetAdmin", "Summary", } // Otherwise, use these columns. @@ -119,6 +121,7 @@ func (m *CaperCommand) generateInboundPrivEscTableData() ([]string, [][]string, tableCols = []string{ "Source", "Target", + "isTargetAdmin", "Summary", } } @@ -166,9 +169,19 @@ func (m *CaperCommand) generateInboundPrivEscTableData() ([]string, [][]string, //trim the last newline from csvPaths paths = strings.TrimSuffix(paths, "\n") if destinationVertexWithProperties.Attributes["IsAdminString"] == "Yes" { - privescPathsBody = append(privescPathsBody, []string{aws.ToString(m.Caller.Account), s, magenta(d), paths}) + privescPathsBody = append(privescPathsBody, []string{ + aws.ToString(m.Caller.Account), + s, + magenta(d), + magenta(destinationVertexWithProperties.Attributes["IsAdminString"]), + paths}) } else { - privescPathsBody = append(privescPathsBody, []string{aws.ToString(m.Caller.Account), s, d, paths}) + privescPathsBody = append(privescPathsBody, []string{ + aws.ToString(m.Caller.Account), + s, + d, + destinationVertexWithProperties.Attributes["IsAdminString"], + paths}) } } @@ -621,7 +634,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } if PermissionsRowAccount == thisAccount { - if matchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") { + if policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") { // // lets only look for rows that have sts:AssumeRole permissions // if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || // strings.EqualFold(PermissionsRow.Action, "*") || @@ -730,7 +743,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } if PermissionsRowAccount == trustedPrincipalAccount { // lets only look for rows that have sts:AssumeRole permissions - if matchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") { + if policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") { // if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || // strings.EqualFold(PermissionsRow.Action, "*") || // strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || diff --git a/aws/permissions.go b/aws/permissions.go index c8241e1..0364835 100644 --- a/aws/permissions.go +++ b/aws/permissions.go @@ -409,6 +409,10 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta //parsedPolicyDocument, _ := parsePolicyDocument(d.Document) document, _ := url.QueryUnescape(aws.ToString(d.Document)) parsedPolicyDocument, _ := policy.ParseJSONPolicy([]byte(document)) + + // hasStsAssumeRole := parsedPolicyDocument.DoesPolicyHaveMatchingStatement("Allow", "sts:AssumeRole", "*") + // fmt.Println(hasStsAssumeRole) + for _, s = range parsedPolicyDocument.Statement { //version := parsedPolicyDocument.Version effect := s.Effect diff --git a/aws/shared.go b/aws/shared.go index b99eeeb..d2b207a 100644 --- a/aws/shared.go +++ b/aws/shared.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "os" - "regexp" "runtime" "strings" @@ -187,30 +186,3 @@ func removeStringFromSlice(slice []string, element string) []string { } return slice } - -// source: https://github.com/nccgroup/PMapper/blob/master/principalmapper/querying/local_policy_simulation.py -func composePattern(stringToTransform string) *regexp.Regexp { - // Escape special characters and replace wildcards - escaped := strings.ReplaceAll(stringToTransform, ".", "\\.") - escaped = strings.ReplaceAll(escaped, "*", ".*") - escaped = strings.ReplaceAll(escaped, "?", ".") - escaped = strings.ReplaceAll(escaped, "$", "\\$") - escaped = strings.ReplaceAll(escaped, "^", "\\^") - - // Compile the regular expression, ignoring case - pattern, err := regexp.Compile("(?i)^" + escaped + "$") - if err != nil { - panic("regexp compile error: " + err.Error()) - } - return pattern -} - -// source: https://github.com/nccgroup/PMapper/blob/master/principalmapper/querying/local_policy_simulation.py -// matchesAfterExpansion checks the stringToCheck against stringToCheckAgainst. -func matchesAfterExpansion(stringFromPolicyToCheck, stringToCheckAgainst string) bool { - // Transform the stringToCheckAgainst into a regex pattern - pattern := composePattern(stringToCheckAgainst) - - // Check if the pattern matches stringToCheck - return pattern.MatchString(stringFromPolicyToCheck) -} diff --git a/cli/aws.go b/cli/aws.go index a1e9709..fc3afd4 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -991,6 +991,7 @@ func runGraphCommand(cmd *cobra.Command, args []string) { } func runCaperCommand(cmd *cobra.Command, args []string) { + GlobalGraph := graph.New(graph.StringHash, graph.Directed()) //var PermissionRowsFromAllProfiles []common.PermissionsRow var GlobalPmapperData aws.PmapperModule @@ -1030,57 +1031,6 @@ func runCaperCommand(cmd *cobra.Command, args []string) { GlobalPmapperData.Edges = append(GlobalPmapperData.Edges, edge) } - // for _, node := range pmapperMod.Nodes { - // var admin, pathToAdmin string - // if node.IsAdmin { - // admin = "yes" - // } else { - // admin = "no" - // } - // if node.PathToAdmin { - // pathToAdmin = "yes" - // } else { - // pathToAdmin = "no" - // } - - // GlobalGraph.AddVertex(node.Arn, - // graph.VertexAttribute("IsAdmin", admin), - // graph.VertexAttribute("PathToAdmin", pathToAdmin), - // ) - //} - // for _, edge := range pmapperMod.Edges { - // err := GlobalGraph.AddEdge( - // edge.Source, - // edge.Destination, - // graph.EdgeAttribute(edge.ShortReason, edge.Reason), - // ) - // if err != nil { - // if err == graph.ErrEdgeAlreadyExists { - // // update the ege by copying the existing graph.Edge with attributes and add the new attributes - // //fmt.Println("Edge already exists") - - // // get the existing edge - // existingEdge, _ := GlobalGraph.Edge(edge.Source, edge.Destination) - // // get the map of attributes - // existingProperties := existingEdge.Properties - // // add the new attributes to attributes map within the properties struct - // // Check if the Attributes map is initialized, if not, initialize it - // if existingProperties.Attributes == nil { - // existingProperties.Attributes = make(map[string]string) - // } - - // // Add or update the attribute - // existingProperties.Attributes[edge.ShortReason] = edge.Reason - // GlobalGraph.UpdateEdge( - // edge.Source, - // edge.Destination, - // graph.EdgeAttributes(existingProperties.Attributes), - // ) - // } - // } - - // } - //Gather all role data fmt.Println("Getting Roles for " + profile) IAMCommandClient := aws.InitIAMClient(AWSConfig) diff --git a/internal/aws/policy/policy.go b/internal/aws/policy/policy.go index 1c2caa7..9a629c1 100644 --- a/internal/aws/policy/policy.go +++ b/internal/aws/policy/policy.go @@ -3,6 +3,8 @@ package policy import ( "encoding/json" "fmt" + "regexp" + "strings" ) type Policy struct { @@ -79,3 +81,88 @@ func contains(list []string, elem string) bool { return false } + +// source: https://github.com/nccgroup/PMapper/blob/master/principalmapper/querying/local_policy_simulation.py +func composePattern(stringToTransform string) *regexp.Regexp { + // Escape special characters and replace wildcards + escaped := strings.ReplaceAll(stringToTransform, ".", "\\.") + escaped = strings.ReplaceAll(escaped, "*", ".*") + escaped = strings.ReplaceAll(escaped, "?", ".") + escaped = strings.ReplaceAll(escaped, "$", "\\$") + escaped = strings.ReplaceAll(escaped, "^", "\\^") + + // Compile the regular expression, ignoring case + pattern, err := regexp.Compile("(?i)^" + escaped + "$") + if err != nil { + panic("regexp compile error: " + err.Error()) + } + return pattern +} + +// source: https://github.com/nccgroup/PMapper/blob/master/principalmapper/querying/local_policy_simulation.py +// MatchesAfterExpansion checks the stringToCheck against stringToCheckAgainst. +func MatchesAfterExpansion(stringFromPolicyToCheck, stringToCheckAgainst string) bool { + // Transform the stringToCheckAgainst into a regex pattern + pattern := composePattern(stringToCheckAgainst) + + // Check if the pattern matches stringToCheck + return pattern.MatchString(stringFromPolicyToCheck) +} + +func (p *Policy) DoesPolicyHaveMatchingStatement(effect string, actionToCheck string, resourceToCheck string) bool { + + for _, statement := range p.Statement { + if statement.Effect == effect { + matchesAction, matchesResource := false, false + for _, action := range statement.Action { + if MatchesAfterExpansion(actionToCheck, action) { + matchesAction = true + if resourceToCheck != "" { + for _, resource := range statement.Resource { + if MatchesAfterExpansion(resourceToCheck, resource) { + matchesResource = true + } + } + for _, notResource := range statement.NotResource { + if MatchesAfterExpansion(resourceToCheck, notResource) { + matchesResource = false + } + } + } + } + } + for _, notAction := range statement.NotAction { + matchesAction = true + if notAction == "*" { + matchesAction = false + } + if notAction == actionToCheck { + matchesAction = false + } + + if MatchesAfterExpansion(actionToCheck, notAction) { + matchesAction = false + } + if resourceToCheck != "" { + for _, resource := range statement.Resource { + if MatchesAfterExpansion(resourceToCheck, resource) { + matchesResource = true + } + } + for _, notResource := range statement.NotResource { + if MatchesAfterExpansion(resourceToCheck, notResource) { + matchesResource = false + } + } + } + + } + if matchesAction && matchesResource { + return true + } + } + + } + + return false +} diff --git a/internal/aws/policy/policy_test.go b/internal/aws/policy/policy_test.go index 5746b89..3870a46 100644 --- a/internal/aws/policy/policy_test.go +++ b/internal/aws/policy/policy_test.go @@ -150,3 +150,75 @@ func getTestFixure(filename string) (*Policy, error) { return &policy, nil } +func TestDoesPolicyHaveMatchingStatement(t *testing.T) { + p := &Policy{ + Statement: []PolicyStatement{ + { + Effect: "Allow", + Action: []string{"ec2:*"}, + Resource: []string{"*"}, + }, + { + Effect: "Allow", + Action: []string{"s3:GetObject"}, + Resource: []string{"arn:aws:s3:::bucket/*"}, + }, + { + Effect: "Deny", + Action: []string{"s3:*"}, + Resource: []string{"arn:aws:s3:::bucket2/*"}, + }, + }, + } + + tests := []struct { + effect string + actionToCheck string + resourceToCheck string + want bool + }{ + { + effect: "Allow", + actionToCheck: "ec2:DescribeInstances", + resourceToCheck: "arn:aws:ec2:us-west-2:123456789012:instance/*", + want: true, + }, + { + effect: "Allow", + actionToCheck: "s3:GetObject", + resourceToCheck: "arn:aws:s3:::bucket/file.txt", + want: true, + }, + { + effect: "Allow", + actionToCheck: "s3:PutObject", + resourceToCheck: "arn:aws:s3:::bucket/file.txt", + want: false, + }, + { + effect: "Deny", + actionToCheck: "s3:GetObject", + resourceToCheck: "arn:aws:s3:::bucket2/file.txt", + want: true, + }, + { + effect: "Deny", + actionToCheck: "s3:PutObject", + resourceToCheck: "arn:aws:s3:::bucket2/file.txt", + want: true, + }, + { + effect: "Deny", + actionToCheck: "s3:GetObject", + resourceToCheck: "arn:aws:s3:::bucket/file.txt", + want: false, + }, + } + + for _, tt := range tests { + actual := p.DoesPolicyHaveMatchingStatement(tt.effect, tt.actionToCheck, tt.resourceToCheck) + if tt.want != actual { + t.Errorf("DoesPolicyHaveMatchingStatement(%s, %s, %s) is %v but should be %v", tt.effect, tt.actionToCheck, tt.resourceToCheck, actual, tt.want) + } + } +} diff --git a/internal/aws/policy/statement.go b/internal/aws/policy/statement.go index b68d97c..94b23d1 100644 --- a/internal/aws/policy/statement.go +++ b/internal/aws/policy/statement.go @@ -6,13 +6,14 @@ import ( ) type PolicyStatement struct { - Sid string `json:"Sid,omitempty"` - Effect string `json:"Effect"` - Principal PolicyStatementPrincipal `json:"Principal,omitempty"` - Action ListOrString `json:"Action"` - NotAction ListOrString `json:"NotAction,omitempty"` - Resource ListOrString `json:"Resource,omitempty"` - Condition PolicyStatementCondition `json:"Condition,omitempty"` + Sid string `json:"Sid,omitempty"` + Effect string `json:"Effect"` + Principal PolicyStatementPrincipal `json:"Principal,omitempty"` + Action ListOrString `json:"Action"` + NotAction ListOrString `json:"NotAction,omitempty"` + Resource ListOrString `json:"Resource,omitempty"` + NotResource ListOrString `json:"NotResource,omitempty"` + Condition PolicyStatementCondition `json:"Condition,omitempty"` } func (ps *PolicyStatement) IsEmpty() bool { diff --git a/internal/cache.go b/internal/cache.go index 4477cf2..458c3a6 100644 --- a/internal/cache.go +++ b/internal/cache.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/dominikbraun/graph" "github.com/patrickmn/go-cache" ) @@ -205,3 +206,26 @@ func LoadCacheFromGobFiles(directory string) error { //fmt.Println("Cache loaded from files.") return nil } + +func SaveGraphToGob[K comparable, T any](directory string, name string, g *graph.Graph[K, T]) error { + err := os.MkdirAll(directory, 0755) + if err != nil { + return err + } + + filename := filepath.Join(directory, name+".gob") + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + encoder := gob.NewEncoder(file) + err = encoder.Encode(g) + if err != nil { + sharedLogger.Errorf("Could not encode the following graph: %s", name) + return err + + } + return nil +} From 05fd899219055133b9e6af9502270c374d4fda0c Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Thu, 7 Mar 2024 15:56:13 -0500 Subject: [PATCH 16/29] Fixed bug in federeated role trust poclies where multiple subjects are trusted --- aws/caper.go | 46 ++++++---- aws/graph.go | 17 ++-- aws/role-trusts.go | 100 ++++++++++++--------- cli/aws.go | 36 +++++--- go.mod | 2 + internal/aws/policy/role-trust-policies.go | 12 +-- 6 files changed, 126 insertions(+), 87 deletions(-) diff --git a/aws/caper.go b/aws/caper.go index 03545d2..cacbe56 100644 --- a/aws/caper.go +++ b/aws/caper.go @@ -139,13 +139,13 @@ func (m *CaperCommand) generateInboundPrivEscTableData() ([]string, [][]string, // } //edges, _ := m.GlobalGraph.Edges() //var reason string - adjacencyMap, _ := m.GlobalGraph.AdjacencyMap() - for destination := range adjacencyMap { + allGlobalNodes, _ := m.GlobalGraph.AdjacencyMap() + for destination := range allGlobalNodes { d, destinationVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(destination) //for the destination vertex, we only want to deal with the ones that are in this account if destinationVertexWithProperties.Attributes["AccountID"] == aws.ToString(m.Caller.Account) { // now let's look at every other vertex and see if it has a path to this destination - for source := range adjacencyMap { + for source := range allGlobalNodes { s, sourceVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(source) //for the source vertex, we only want to deal with the ones that are NOT in this account if sourceVertexWithProperties.Attributes["AccountID"] != aws.ToString(m.Caller.Account) { @@ -225,9 +225,9 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo for _, statement := range trustsdoc.Statement { for _, principal := range statement.Principal.AWS { if strings.Contains(principal, ":root") { - //check to see if the accountID is known - accountID := strings.Split(principal, ":")[4] - vendorName = vendors.GetVendorNameFromAccountID(accountID) + //check to see if the vendorAccountID is known + vendorAccountID := strings.Split(principal, ":")[4] + vendorName = vendors.GetVendorNameFromAccountID(vendorAccountID) } TrustedPrincipals = append(TrustedPrincipals, TrustedPrincipal{ @@ -267,15 +267,27 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo trustedProvider = "Okta" // (" + statement.Principal.Federated[0] + ")" } trustedSubjects = append(trustedSubjects, "Not applicable") - } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" { - trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" - if statement.Condition.StringEquals.OidcEksSub != "" { - trustedSubjects = append(trustedSubjects, statement.Condition.StringEquals.OidcEksSub) - } else if statement.Condition.StringLike.OidcEksSub != "" { - trustedSubjects = append(trustedSubjects, statement.Condition.StringLike.OidcEksSub) - } else { + } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != nil || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != nil { + trustedProvider = statement.Principal.Federated[0] + //providerAccountId := strings.Split(statement.Principal.Federated[0], ":")[4] + // we only care about cross account trusts here, so we only care if the OIDC provider is from another account. + //if providerAccountId != accountId { + //trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" + if statement.Condition.StringEquals.OidcEksSub != nil { + if len(statement.Condition.StringEquals.OidcEksSub) > 0 { + trustedSubjects = append(trustedSubjects, statement.Condition.StringEquals.OidcEksSub...) + } + } + if statement.Condition.StringLike.OidcEksSub != nil { + if len(statement.Condition.StringLike.OidcEksSub) > 0 { + trustedSubjects = append(trustedSubjects, statement.Condition.StringLike.OidcEksSub...) + } + + } + if len(trustedSubjects) == 0 { trustedSubjects = append(trustedSubjects, "ALL SERVICE ACCOUNTS!") } + //} } else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" { trustedProvider = "Cognito" // (" + statement.Principal.Federated[0] + ")" if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" { @@ -634,12 +646,8 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } if PermissionsRowAccount == thisAccount { + // lets only look for rows that have sts:AssumeRole permissions if policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") { - // // lets only look for rows that have sts:AssumeRole permissions - // if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") || - // strings.EqualFold(PermissionsRow.Action, "*") || - // strings.EqualFold(PermissionsRow.Action, "sts:Assume*") || - // strings.EqualFold(PermissionsRow.Action, "sts:*") { // lets only focus on rows that have an effect of Allow if strings.EqualFold(PermissionsRow.Effect, "Allow") { // if the resource is * or the resource is this role arn, then this principal can assume this role @@ -892,3 +900,5 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } } + +// func (a *Node) MakeUserEdges(GlobalGraph graph.Graph[string, string]) { diff --git a/aws/graph.go b/aws/graph.go index dd6555c..866e79a 100644 --- a/aws/graph.go +++ b/aws/graph.go @@ -363,15 +363,16 @@ func (m *GraphCommand) collectRoleDataForGraph() []models.Role { trustedProvider = "Okta" // (" + statement.Principal.Federated[0] + ")" } trustedSubjects = "Not applicable" - } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" { + } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != nil || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != nil { trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" - if statement.Condition.StringEquals.OidcEksSub != "" { - trustedSubjects = statement.Condition.StringEquals.OidcEksSub - } else if statement.Condition.StringLike.OidcEksSub != "" { - trustedSubjects = statement.Condition.StringLike.OidcEksSub - } else { - trustedSubjects = "ALL SERVICE ACCOUNTS!" - } + // if statement.Condition.StringEquals.OidcEksSub != "" { + // trustedSubjects = statement.Condition.StringEquals.OidcEksSub + // } else if statement.Condition.StringLike.OidcEksSub != "" { + // trustedSubjects = statement.Condition.StringLike.OidcEksSub + // } else { + // trustedSubjects = "ALL SERVICE ACCOUNTS!" + // } + trustedSubjects = "ALL SERVICE ACCOUNTS!" } else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" { trustedProvider = "Cognito" // (" + statement.Principal.Federated[0] + ")" if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" { diff --git a/aws/role-trusts.go b/aws/role-trusts.go index e248903..b01d746 100644 --- a/aws/role-trusts.go +++ b/aws/role-trusts.go @@ -152,12 +152,12 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) } if len(servicesBody) > 0 { - fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody))) + fmt.Printf("[%s][%s] %s service role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody))) } else { fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) } if len(federatedBody) > 0 { - fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody))) + fmt.Printf("[%s][%s] %s federated role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody))) } else { fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) } @@ -417,23 +417,25 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin for _, role := range m.AnalyzedRoles { for _, statement := range role.trustsDoc.Statement { if len(statement.Principal.Federated) > 0 { - provider, subject := parseFederatedTrustPolicy(statement) - RoleTrustRow := RoleTrustRow{ - RoleARN: aws.ToString(role.roleARN), - RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)), - TrustedFederatedProvider: provider, - TrustedFederatedSubject: subject, - IsAdmin: role.Admin, - CanPrivEsc: role.CanPrivEsc, + provider, subjects := parseFederatedTrustPolicy(statement) + for _, subject := range subjects { + RoleTrustRow := RoleTrustRow{ + RoleARN: aws.ToString(role.roleARN), + RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)), + TrustedFederatedProvider: provider, + TrustedFederatedSubject: subject, + IsAdmin: role.Admin, + CanPrivEsc: role.CanPrivEsc, + } + body = append(body, []string{ + aws.ToString(m.Caller.Account), + RoleTrustRow.RoleARN, + RoleTrustRow.RoleName, + RoleTrustRow.TrustedFederatedProvider, + RoleTrustRow.TrustedFederatedSubject, + RoleTrustRow.IsAdmin, + RoleTrustRow.CanPrivEsc}) } - body = append(body, []string{ - aws.ToString(m.Caller.Account), - RoleTrustRow.RoleARN, - RoleTrustRow.RoleName, - RoleTrustRow.TrustedFederatedProvider, - RoleTrustRow.TrustedFederatedSubject, - RoleTrustRow.IsAdmin, - RoleTrustRow.CanPrivEsc}) } } @@ -444,47 +446,59 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin } -func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string, string) { - var column2, column3 string +func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string, []string) { + var provider string + var subjects []string + var trustedEKSSubs []string if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 { - column2 = "GitHub Actions" // (" + statement.Principal.Federated[0] + ")" - trustedRepos := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, "\n") - if trustedRepos == "" { - column3 = "ALL REPOS!!!" + provider = "GitHub Actions" // (" + statement.Principal.Federated[0] + ")" + //trustedRepos := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, "\n") + if len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 { + subjects = append(subjects, statement.Condition.StringLike.TokenActionsGithubusercontentComSub...) } else { - column3 = trustedRepos + subjects = append(subjects, "ALL REPOS!!!") } + } else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" { if strings.Contains(statement.Principal.Federated[0], "AWSSSO") { - column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" + provider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" } else if strings.Contains(statement.Principal.Federated[0], "Okta") { - column2 = "Okta" // (" + statement.Principal.Federated[0] + ")" + provider = "Okta" // (" + statement.Principal.Federated[0] + ")" } - column3 = "Not applicable" - } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" { - column2 = "EKS" // (" + statement.Principal.Federated[0] + ")" - if statement.Condition.StringEquals.OidcEksSub != "" { - column3 = statement.Condition.StringEquals.OidcEksSub - } else if statement.Condition.StringLike.OidcEksSub != "" { - column3 = statement.Condition.StringLike.OidcEksSub + subjects = append(subjects, "Not applicable") + } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != nil || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != nil { + provider = "EKS" // (" + statement.Principal.Federated[0] + ")" + if statement.Condition.StringEquals.OidcEksSub != nil { + if len(statement.Condition.StringEquals.OidcEksSub) > 0 { + trustedEKSSubs = append(trustedEKSSubs, statement.Condition.StringEquals.OidcEksSub...) + } + } + if statement.Condition.StringLike.OidcEksSub != nil { + if len(statement.Condition.StringLike.OidcEksSub) > 0 { + trustedEKSSubs = append(trustedEKSSubs, statement.Condition.StringLike.OidcEksSub...) + } + } + if len(trustedEKSSubs) == 0 { + subjects = append(subjects, "ALL SERVICE ACCOUNTS!") } else { - column3 = "ALL SERVICE ACCOUNTS!" + subjects = append(subjects, trustedEKSSubs...) } + } else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" { - column2 = "Cognito" // (" + statement.Principal.Federated[0] + ")" + provider = "Cognito" // (" + statement.Principal.Federated[0] + ")" if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" { - column3 = statement.Condition.ForAnyValueStringLike.CognitoAMR + subjects = append(subjects, statement.Condition.ForAnyValueStringLike.CognitoAMR) } } else { - if column2 == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") { - column2 = "EKS" // (" + statement.Principal.Federated[0] + ")" - column3 = "ALL SERVICE ACCOUNTS!" - } else if column2 == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") { - column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" + if provider == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") { + provider = "EKS" // (" + statement.Principal.Federated[0] + ")" + subjects = append(subjects, "ALL SERVICE ACCOUNTS!") + } else if provider == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") { + provider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" } } - return column2, column3 + return provider, subjects } func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() { diff --git a/cli/aws.go b/cli/aws.go index 38b0a67..f540a95 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -6,7 +6,6 @@ import ( "log" "os" "path/filepath" - "time" "github.com/BishopFox/cloudfox/aws" "github.com/BishopFox/cloudfox/aws/sdk" @@ -44,8 +43,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/kinesis" "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lightsail" - "github.com/aws/aws-sdk-go-v2/service/neptune" "github.com/aws/aws-sdk-go-v2/service/mq" + "github.com/aws/aws-sdk-go-v2/service/neptune" "github.com/aws/aws-sdk-go-v2/service/opensearch" "github.com/aws/aws-sdk-go-v2/service/organizations" "github.com/aws/aws-sdk-go-v2/service/organizations/types" @@ -64,7 +63,6 @@ import ( "github.com/aws/smithy-go/ptr" "github.com/bishopfox/knownawsaccountslookup" "github.com/dominikbraun/graph" - "github.com/dominikbraun/graph/draw" "github.com/fatih/color" "github.com/kyokomi/emoji" "github.com/spf13/cobra" @@ -1022,18 +1020,20 @@ func runCaperCommand(cmd *cobra.Command, args []string) { fmt.Println("Error importing pmapper data: " + pmapperError.Error()) } + // add pmapper nodes to GlobalNodes (which will also soon include iam roles and users) for _, node := range pmapperMod.Nodes { // add node to GlobalPmapperData //GlobalPmapperData.Nodes = append(GlobalPmapperData.Nodes, node) GlobalNodes = append(GlobalNodes, node) } + // same for adding pmapper edges to GlobalPmapperData for _, edge := range pmapperMod.Edges { // add edge to GlobalPmapperData GlobalPmapperData.Edges = append(GlobalPmapperData.Edges, edge) } - //Gather all role data + //Gather all role data so we can later process all of the role trusts and add external nodes not looked at by pmapper fmt.Println("Getting Roles for " + profile) IAMCommandClient := aws.InitIAMClient(AWSConfig) ListRolesOutput, err := sdk.CachedIamListRoles(IAMCommandClient, ptr.ToString(caller.Account)) @@ -1051,6 +1051,8 @@ func runCaperCommand(cmd *cobra.Command, args []string) { } //Gather all user data + // Currently, there is no need to parse groups and build group-user relationships because + // the permissions command (and common.PermissionRowsFromAllProfiles above already has mapped/assigned group permissions to users within the group fmt.Println("Getting Users for " + profile) ListUsersOutput, err := sdk.CachedIamListUsers(IAMCommandClient, ptr.ToString(caller.Account)) if err != nil { @@ -1066,7 +1068,7 @@ func runCaperCommand(cmd *cobra.Command, args []string) { //GlobalGraph := models.MakeAllVertices(GlobalRoles, GlobalPmapperData) // make vertices - // you can't update verticies - so we need to make all of the vertices that are roles in the in-scope accounts + // you can't update vertices - so we need to make all of the vertices that are roles in the in-scope accounts // all at once to make sure they have the most information possible fmt.Println("Making vertices for all profiles") // for _, role := range GlobalRoles { @@ -1087,6 +1089,8 @@ func runCaperCommand(cmd *cobra.Command, args []string) { } // make pmapper edges + //you can update edges, so we can just merge attributes as needed + // first we add the edges that already exist in pmapper, then later we will make more edges based on the cloudfox role trusts logic for _, edge := range GlobalPmapperData.Edges { err := GlobalGraph.AddEdge( edge.Source, @@ -1120,15 +1124,18 @@ func runCaperCommand(cmd *cobra.Command, args []string) { } //making edges + // these are the cloudfox created edges mainly based on role trusts + // at least for now, we don't need to make edges for users, groups, or anything else because pmapper already has all of the edges we need fmt.Println("Making edges for all profiles") for _, node := range mergedNodes { if node.Type == "Role" { node.MakeRoleEdges(GlobalGraph) } + // if node.Type == "User" { + // node.MakeUserEdges(GlobalGraph) + // } } - // print how many nodes and edges are in the graph to the screen and exit - for _, profile := range AWSProfiles { var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) @@ -1154,11 +1161,16 @@ func runCaperCommand(cmd *cobra.Command, args []string) { } caperCommandClient.RunCaperCommand() - filename := fmt.Sprintf("./mygraph-%s-%s.gv", ptr.ToString(caller.Account), time.Now().Format("2006-01-02-15-04-05")) - file, _ := os.Create(filename) - _ = draw.DOT(GlobalGraph, file, draw.GraphAttribute( - "ranksep", "3", - )) + + // playing around with creating a graphviz file for image rendering. + // the goal here is to be able to export this graph data to a format that can be easily imported in neo4j. + // this is a work in progress and not yet complete + + // filename := fmt.Sprintf("./mygraph-%s-%s.gv", ptr.ToString(caller.Account), time.Now().Format("2006-01-02-15-04-05")) + // file, _ := os.Create(filename) + // _ = draw.DOT(GlobalGraph, file, draw.GraphAttribute( + // "ranksep", "3", + // )) } } diff --git a/go.mod b/go.mod index 089a3e2..d85074c 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/BishopFox/cloudfox go 1.21.2 +toolchain go1.21.6 + require ( github.com/Azure/azure-sdk-for-go v68.0.0+incompatible github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 diff --git a/internal/aws/policy/role-trust-policies.go b/internal/aws/policy/role-trust-policies.go index ec3295a..7c24b81 100644 --- a/internal/aws/policy/role-trust-policies.go +++ b/internal/aws/policy/role-trust-policies.go @@ -26,16 +26,16 @@ type RoleTrustStatementEntry struct { Action string `json:"Action"` Condition struct { StringEquals struct { - StsExternalID string `json:"sts:ExternalId"` - SAMLAud string `json:"SAML:aud"` - OidcEksSub string `json:"OidcEksSub"` - OidcEksAud string `json:"OidcEksAud"` - CognitoAud string `json:"cognito-identity.amazonaws.com:aud"` + StsExternalID string `json:"sts:ExternalId"` + SAMLAud string `json:"SAML:aud"` + OidcEksSub ListOfPrincipals `json:"OidcEksSub"` + OidcEksAud string `json:"OidcEksAud"` + CognitoAud string `json:"cognito-identity.amazonaws.com:aud"` } `json:"StringEquals"` StringLike struct { TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"` TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"` - OidcEksSub string `json:"OidcEksSub"` + OidcEksSub ListOfPrincipals `json:"OidcEksSub"` OidcEksAud string `json:"OidcEksAud"` } `json:"StringLike"` ForAnyValueStringLike struct { From 8fac75ed11d3ed1a14ddf6799cc96ec76f79eb80 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Thu, 21 Mar 2024 09:58:35 -0400 Subject: [PATCH 17/29] merged from main --- cli/aws.go | 1 - internal/aws/policy/role-trust-policies.go | 7 ------- 2 files changed, 8 deletions(-) diff --git a/cli/aws.go b/cli/aws.go index 12ff18a..3732e62 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -43,7 +43,6 @@ import ( "github.com/aws/aws-sdk-go-v2/service/lambda" "github.com/aws/aws-sdk-go-v2/service/lightsail" "github.com/aws/aws-sdk-go-v2/service/mq" - "github.com/aws/aws-sdk-go-v2/service/neptune" "github.com/aws/aws-sdk-go-v2/service/opensearch" "github.com/aws/aws-sdk-go-v2/service/organizations" "github.com/aws/aws-sdk-go-v2/service/organizations/types" diff --git a/internal/aws/policy/role-trust-policies.go b/internal/aws/policy/role-trust-policies.go index a2db51a..40ddcdf 100644 --- a/internal/aws/policy/role-trust-policies.go +++ b/internal/aws/policy/role-trust-policies.go @@ -99,12 +99,6 @@ func ParseRoleTrustPolicyDocument(role types.Role) (TrustPolicyDocument, error) // used to unmarshall. pattern := `(\w+)\:` pattern2 := `".[a-zA-Z0-9\-\.]+/id/` -<<<<<<< HEAD - var reEKSSub = regexp.MustCompile(pattern2 + pattern + "sub") - var reEKSAud = regexp.MustCompile(pattern2 + pattern + "aud") - document = reEKSSub.ReplaceAllString(document, "\"OidcEksSub") - document = reEKSAud.ReplaceAllString(document, "\"OidcEksAud") -======= //auth0Pattern := `"auth0.com\:` circleCIPattern := `"oidc.circleci.com/org/[a-zA-Z0-9\-]+:` var reEKSSub = regexp.MustCompile(pattern2 + pattern + "sub") @@ -118,7 +112,6 @@ func ParseRoleTrustPolicyDocument(role types.Role) (TrustPolicyDocument, error) //document = reAuth0Sub.ReplaceAllString(document, "\"Auth0Amr") document = reCircleCIAud.ReplaceAllString(document, "\"CircleCIAud") document = reCircleCISSub.ReplaceAllString(document, "\"CircleCISub") ->>>>>>> b73cd90323769c8adcb38901304ad165ce16f31d var parsedDocumentToJSON TrustPolicyDocument _ = json.Unmarshal([]byte(document), &parsedDocumentToJSON) From 4ef87d8b98d0a14c33557958ef76738ded1c7510 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Fri, 22 Mar 2024 08:58:05 -0400 Subject: [PATCH 18/29] updated caper to use new version of parseFederatedRoleTrusts from the role-trusts command. Also changed the way vendors and federated identities are labled --- aws/caper.go | 88 +++++++++------------------------------------------- cli/aws.go | 14 ++++++++- 2 files changed, 28 insertions(+), 74 deletions(-) diff --git a/aws/caper.go b/aws/caper.go index cacbe56..a89451a 100644 --- a/aws/caper.go +++ b/aws/caper.go @@ -33,6 +33,7 @@ type CaperCommand struct { SkipAdminCheck bool GlobalGraph graph.Graph[string, string] PmapperDataBasePath string + AnalyzedAccounts map[string]bool output internal.OutputData2 modLog *logrus.Entry @@ -249,60 +250,8 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo } for _, federated := range statement.Principal.Federated { - if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 { - trustedProvider = "GitHub" - //trustedSubjects = strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",") - trustedSubjects = statement.Condition.StringLike.TokenActionsGithubusercontentComSub - if strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, ",") == "" { - trustedSubjects = append(trustedSubjects, "ALL REPOS") - } - // } else { - // trustedSubjects = "Repos: " + trustedSubjects - // } - - } else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" { - if strings.Contains(statement.Principal.Federated[0], "AWSSSO") { - trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" - } else if strings.Contains(statement.Principal.Federated[0], "Okta") { - trustedProvider = "Okta" // (" + statement.Principal.Federated[0] + ")" - } - trustedSubjects = append(trustedSubjects, "Not applicable") - } else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != nil || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != nil { - trustedProvider = statement.Principal.Federated[0] - //providerAccountId := strings.Split(statement.Principal.Federated[0], ":")[4] - // we only care about cross account trusts here, so we only care if the OIDC provider is from another account. - //if providerAccountId != accountId { - //trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" - if statement.Condition.StringEquals.OidcEksSub != nil { - if len(statement.Condition.StringEquals.OidcEksSub) > 0 { - trustedSubjects = append(trustedSubjects, statement.Condition.StringEquals.OidcEksSub...) - } - } - if statement.Condition.StringLike.OidcEksSub != nil { - if len(statement.Condition.StringLike.OidcEksSub) > 0 { - trustedSubjects = append(trustedSubjects, statement.Condition.StringLike.OidcEksSub...) - } - - } - if len(trustedSubjects) == 0 { - trustedSubjects = append(trustedSubjects, "ALL SERVICE ACCOUNTS!") - } - //} - } else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" { - trustedProvider = "Cognito" // (" + statement.Principal.Federated[0] + ")" - if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" { - trustedSubjects = append(trustedSubjects, statement.Condition.ForAnyValueStringLike.CognitoAMR) - } - } else { - if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") { - trustedProvider = "EKS" // (" + statement.Principal.Federated[0] + ")" - trustedSubjects = append(trustedSubjects, "ALL SERVICE ACCOUNTS!") - } else if trustedProvider == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") { - trustedProvider = "AWS SSO" // (" + statement.Principal.Federated[0] + ")" - } - trustedSubjects = append(trustedSubjects, "Not applicable") - } + trustedProvider, trustedSubjects = parseFederatedTrustPolicy(statement) TrustedFederatedProviders = append(TrustedFederatedProviders, TrustedFederatedProvider{ TrustedFederatedProvider: federated, ProviderShortName: trustedProvider, @@ -329,16 +278,6 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo func ConvertIAMUserToNode(user types.User) Node { accountId := strings.Split(aws.ToString(user.Arn), ":")[4] - - //create new object of type models.User - // cfUser := models.User{ - // Id: aws.ToString(user.UserId), - // ARN: aws.ToString(user.Arn), - // Name: aws.ToString(user.UserName), - // IsAdmin: false, - // CanPrivEscToAdmin: false, - // } - node := Node{ Arn: aws.ToString(user.Arn), Type: "User", @@ -386,7 +325,7 @@ func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) [] if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" { newNodes = append(newNodes, Node{ - Arn: fmt.Sprintf("%s [%s]", TrustedPrincipal.TrustedPrincipal, TrustedPrincipal.VendorName), + Arn: fmt.Sprintf("%s [%s]", TrustedPrincipal.VendorName, TrustedPrincipal.TrustedPrincipal), //Arn: TrustedPrincipal.VendorName, Type: "Account", AccountID: trustedPrincipalAccount, @@ -394,12 +333,13 @@ func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) [] VendorName: TrustedPrincipal.VendorName, }) - // } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) { - // newNodes = append(newNodes, Node{ - // Arn: TrustedPrincipal.TrustedPrincipal, - // Type: "Account", - // AccountID: trustedPrincipalAccount, - // }) + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName == "" { + // check to see if the account is one of the analyzed accounts + newNodes = append(newNodes, Node{ + Arn: TrustedPrincipal.TrustedPrincipal, + Type: "Account", + AccountID: trustedPrincipalAccount, + }) } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf(":user")) { newNodes = append(newNodes, Node{ @@ -444,7 +384,8 @@ func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) [] if trustedSubject == "Not applicable" { providerAndSubject = TrustedFederatedProvider.ProviderShortName } else { - providerAndSubject = TrustedFederatedProvider.ProviderShortName + ":" + trustedSubject + //providerAndSubject = TrustedFederatedProvider.ProviderShortName + ":" + trustedSubject + providerAndSubject = fmt.Sprintf("%s [%s]", TrustedFederatedProvider.ProviderShortName, trustedSubject) } newNodes = append(newNodes, Node{ Arn: providerAndSubject, @@ -704,7 +645,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { err := GlobalGraph.AddEdge( //TrustedPrincipal.TrustedPrincipal, //TrustedPrincipal.VendorName, - fmt.Sprintf("%s [%s]", TrustedPrincipal.TrustedPrincipal, TrustedPrincipal.VendorName), + fmt.Sprintf("%s [%s]", TrustedPrincipal.VendorName, TrustedPrincipal.TrustedPrincipal), a.Arn, //graph.EdgeAttribute("VendorAssumeRole", "Cross account root trust and trusted principal is a vendor"), graph.EdgeAttribute("VendorAssumeRole", "can assume (because of a cross account root trust and trusted principal is a vendor) "), @@ -858,7 +799,8 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { if trustedSubject == "Not applicable" { providerAndSubject = TrustedFederatedProvider.ProviderShortName } else { - providerAndSubject = TrustedFederatedProvider.ProviderShortName + ":" + trustedSubject + //providerAndSubject = TrustedFederatedProvider.ProviderShortName + ":" + trustedSubject + providerAndSubject = fmt.Sprintf("%s [%s]", TrustedFederatedProvider.ProviderShortName, trustedSubject) } err := GlobalGraph.AddEdge( diff --git a/cli/aws.go b/cli/aws.go index 3732e62..c643917 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -946,6 +946,7 @@ func runFilesystemsCommand(cmd *cobra.Command, args []string) { } func runGraphCommand(cmd *cobra.Command, args []string) { + for _, profile := range AWSProfiles { //var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version) caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) @@ -987,6 +988,8 @@ func runGraphCommand(cmd *cobra.Command, args []string) { } func runCaperCommand(cmd *cobra.Command, args []string) { + // map of all unique accountIDs and if they are included in the analysis or not + analyzedAccounts := make(map[string]bool) GlobalGraph := graph.New(graph.StringHash, graph.Directed()) //var PermissionRowsFromAllProfiles []common.PermissionsRow @@ -1002,6 +1005,9 @@ func runCaperCommand(cmd *cobra.Command, args []string) { continue } + // add account number to analyzedAccounts map and set the value to true + analyzedAccounts[ptr.ToString(caller.Account)] = true + //Gather all Permissions data fmt.Println("Getting GAAD for " + profile) PermissionsCommandClient := aws.InitPermissionsClient(*caller, profile, cmd.Root().Version, Goroutines, AWSMFAToken) @@ -1081,7 +1087,12 @@ func runCaperCommand(cmd *cobra.Command, args []string) { graph.VertexAttribute("CanPrivEscToAdminString", node.CanPrivEscToAdminString), graph.VertexAttribute("AccountID", node.AccountID), ) - + // for every node, check to see if the accountId exists in the analyzedAccounts map. If it does not, add it to the map and set the value to false only if the node.VendorName is empty + if _, ok := analyzedAccounts[node.AccountID]; !ok { + if node.VendorName == "" { + analyzedAccounts[node.AccountID] = false + } + } } // make pmapper edges @@ -1154,6 +1165,7 @@ func runCaperCommand(cmd *cobra.Command, args []string) { SkipAdminCheck: AWSSkipAdminCheck, GlobalGraph: GlobalGraph, PmapperDataBasePath: PmapperDataBasePath, + AnalyzedAccounts: analyzedAccounts, } caperCommandClient.RunCaperCommand() From fca22ceb6b2f18e2d3da382c142e300fb2f7e61f Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:06:41 -0400 Subject: [PATCH 19/29] renamed to cape, added hop count logic, pulled privesc function out so i can add logic to handle cobra flags --- aws/{caper.go => cape.go} | 233 ++++++++++++++++++++++++++------------ aws/pmapper.go | 8 +- aws/role-trusts.go | 6 +- cli/aws.go | 51 ++++++--- main.go | 13 +++ 5 files changed, 217 insertions(+), 94 deletions(-) rename aws/{caper.go => cape.go} (78%) diff --git a/aws/caper.go b/aws/cape.go similarity index 78% rename from aws/caper.go rename to aws/cape.go index a89451a..5938be5 100644 --- a/aws/caper.go +++ b/aws/cape.go @@ -14,11 +14,13 @@ import ( "github.com/bishopfox/knownawsaccountslookup" "github.com/dominikbraun/graph" "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) -type CaperCommand struct { +type CapeCommand struct { // General configuration data + Cmd cobra.Command Caller sts.GetCallerIdentityOutput AWSRegions []string Goroutines int @@ -34,17 +36,18 @@ type CaperCommand struct { GlobalGraph graph.Graph[string, string] PmapperDataBasePath string AnalyzedAccounts map[string]bool + CapeAdminOnly bool output internal.OutputData2 modLog *logrus.Entry } -func (m *CaperCommand) RunCaperCommand() { +func (m *CapeCommand) RunCapeCommand() { // These struct values are used by the output module m.output.Verbosity = m.Verbosity m.output.Directory = m.AWSOutputDirectory - m.output.CallingModule = "caper" + m.output.CallingModule = "cape" m.modLog = internal.TxtLog.WithFields(logrus.Fields{ "module": m.output.CallingModule, }) @@ -86,10 +89,17 @@ func (m *CaperCommand) RunCaperCommand() { // }) o.WriteFullOutput(o.Table.TableFiles, nil) + fmt.Println("The following accounts are trusted by this account, but were not analyzed as part of this run.") + fmt.Println("As a result, we cannot determine which principals in these accounts have permission to assume roles in this account.") + for account := range m.AnalyzedAccounts { + if m.AnalyzedAccounts[account] == false { + fmt.Println("\t\t" + account) + } + } } -func (m *CaperCommand) generateInboundPrivEscTableData() ([]string, [][]string, []string) { +func (m *CapeCommand) generateInboundPrivEscTableData() ([]string, [][]string, []string) { var body [][]string var tableCols []string var header []string @@ -127,85 +137,86 @@ func (m *CaperCommand) generateInboundPrivEscTableData() ([]string, [][]string, } } - //var reason string - var paths string var privescPathsBody [][]string - //var vertices map[string]map[string]graph.Edge[string] - //vertices, err := graph.TopologicalSort(m.GlobalGraph) - //vertices, err := m.GlobalGraph.AdjacencyMap() - // if err != nil { - // m.modLog.Error(err) - // fmt.Println("Error sorting graph: ", err) - // } - //edges, _ := m.GlobalGraph.Edges() - //var reason string allGlobalNodes, _ := m.GlobalGraph.AdjacencyMap() for destination := range allGlobalNodes { d, destinationVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(destination) - //for the destination vertex, we only want to deal with the ones that are in this account - if destinationVertexWithProperties.Attributes["AccountID"] == aws.ToString(m.Caller.Account) { - // now let's look at every other vertex and see if it has a path to this destination - for source := range allGlobalNodes { - s, sourceVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(source) - //for the source vertex, we only want to deal with the ones that are NOT in this account - if sourceVertexWithProperties.Attributes["AccountID"] != aws.ToString(m.Caller.Account) { - // now let's see if there is a path from this source to our destination - path, _ := graph.ShortestPath(m.GlobalGraph, s, d) - // if we have a path, then lets document this source as having a path to our destination - if path != nil { - if s != d { - paths = "" - // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen - // and then lets print the full path to the screen - for i := 0; i < len(path)-1; i++ { - thisEdge, _ := m.GlobalGraph.Edge(path[i], path[i+1]) - - for _, value := range thisEdge.Properties.Attributes { - value = strings.ReplaceAll(value, ",", " and") - paths += fmt.Sprintf("%s %s %s\n", thisEdge.Source, value, thisEdge.Target) - } - } - - //trim the last newline from csvPaths - paths = strings.TrimSuffix(paths, "\n") - if destinationVertexWithProperties.Attributes["IsAdminString"] == "Yes" { - privescPathsBody = append(privescPathsBody, []string{ - aws.ToString(m.Caller.Account), - s, - magenta(d), - magenta(destinationVertexWithProperties.Attributes["IsAdminString"]), - paths}) - } else { - privescPathsBody = append(privescPathsBody, []string{ - aws.ToString(m.Caller.Account), - s, - d, - destinationVertexWithProperties.Attributes["IsAdminString"], - paths}) - } - } - } + // if the user specified the CapeAdminOnly flag, then we only want to show paths to admin roles + if m.CapeAdminOnly { + // if the user specified the CapeAdminOnly flag, then we only want to show paths to admin roles + if destinationVertexWithProperties.Attributes["IsAdminString"] == "Yes" { + //for the destination vertex, we only want to deal with the ones that are in this account + if destinationVertexWithProperties.Attributes["AccountID"] == aws.ToString(m.Caller.Account) { + privescPathsBody = m.findPathsToThisDestination(allGlobalNodes, d, destinationVertexWithProperties) + body = append(body, privescPathsBody...) } } + } else { + //for the destination vertex, we only want to deal with the ones that are in this account + if destinationVertexWithProperties.Attributes["AccountID"] == aws.ToString(m.Caller.Account) { + privescPathsBody := m.findPathsToThisDestination(allGlobalNodes, d, destinationVertexWithProperties) + body = append(body, privescPathsBody...) + } } - } - - // if destinationVertexWithProperties.Attributes["IsAdminString"] == "Yes" { - // fmt.Println("Admin: ", d) - // } - // if destinationVertexWithProperties.Attributes["CanPrivEscToAdminString"] == "Yes" { - // fmt.Println("Has Path to admin: ", d) - // } - body = append(body, privescPathsBody...) return header, body, tableCols } -func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendors) Node { +func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[string]graph.Edge[string], d string, destinationVertexWithProperties graph.VertexProperties) [][]string { + var privescPathsBody [][]string + var paths string + // now let's look at every other vertex and see if it has a path to this destination + for source := range allGlobalNodes { + s, sourceVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(source) + //for the source vertex, we only want to deal with the ones that are NOT in this account + if sourceVertexWithProperties.Attributes["AccountID"] != aws.ToString(m.Caller.Account) { + // now let's see if there is a path from this source to our destination + path, _ := graph.ShortestPath(m.GlobalGraph, s, d) + // if we have a path, then lets document this source as having a path to our destination + if path != nil { + if s != d { + paths = "" + // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen + // and then lets print the full path to the screen + for i := 0; i < len(path)-1; i++ { + thisEdge, _ := m.GlobalGraph.Edge(path[i], path[i+1]) + j := 0 + for _, value := range thisEdge.Properties.Attributes { + value = strings.ReplaceAll(value, ",", " and") + paths += fmt.Sprintf("[Hop: %d] [Option: %d] [%s] %s [%s]\n", i, j, thisEdge.Source, value, thisEdge.Target) + j++ + } + } + + //trim the last newline from csvPaths + paths = strings.TrimSuffix(paths, "\n") + if destinationVertexWithProperties.Attributes["IsAdminString"] == "Yes" { + privescPathsBody = append(privescPathsBody, []string{ + aws.ToString(m.Caller.Account), + s, + magenta(d), + magenta(destinationVertexWithProperties.Attributes["IsAdminString"]), + paths}) + } else { + privescPathsBody = append(privescPathsBody, []string{ + aws.ToString(m.Caller.Account), + s, + d, + destinationVertexWithProperties.Attributes["IsAdminString"], + paths}) + } + } + } + } + } + return privescPathsBody +} + +func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendors, analyzedAccounts map[string]bool) Node { //var isAdmin, canPrivEscToAdmin string accountId := strings.Split(aws.ToString(role.Arn), ":")[4] @@ -222,13 +233,21 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo var trustedProvider string var trustedSubjects []string var vendorName string + var isAnalyzedAccount bool for _, statement := range trustsdoc.Statement { for _, principal := range statement.Principal.AWS { if strings.Contains(principal, ":root") { - //check to see if the vendorAccountID is known - vendorAccountID := strings.Split(principal, ":")[4] - vendorName = vendors.GetVendorNameFromAccountID(vendorAccountID) + //check to see if the trustedRootAccountID is known + trustedRootAccountID := strings.Split(principal, ":")[4] + vendorName = vendors.GetVendorNameFromAccountID(trustedRootAccountID) + // check to see if trustedRootAccountID is in the m.AnalyzedAccounts map + if _, ok := analyzedAccounts[trustedRootAccountID]; ok { + isAnalyzedAccount = analyzedAccounts[trustedRootAccountID] + } else { + isAnalyzedAccount = false + } + } TrustedPrincipals = append(TrustedPrincipals, TrustedPrincipal{ @@ -237,6 +256,7 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo VendorName: vendorName, //IsAdmin: false, //CanPrivEscToAdmin: false, + AccountIsInAnalyzedAccountList: isAnalyzedAccount, }) } @@ -250,12 +270,15 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo } for _, federated := range statement.Principal.Federated { + // provider accountID + //accountId := strings.Split(federated, ":")[4] trustedProvider, trustedSubjects = parseFederatedTrustPolicy(statement) TrustedFederatedProviders = append(TrustedFederatedProviders, TrustedFederatedProvider{ TrustedFederatedProvider: federated, ProviderShortName: trustedProvider, - TrustedSubjects: trustedSubjects, + //ProviderAccountId: accountId, + TrustedSubjects: trustedSubjects, //IsAdmin: false, //CanPrivEscToAdmin: false, }) @@ -324,6 +347,7 @@ func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) [] // } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName != "" { + // First lets take care of vendor accounts newNodes = append(newNodes, Node{ Arn: fmt.Sprintf("%s [%s]", TrustedPrincipal.VendorName, TrustedPrincipal.TrustedPrincipal), //Arn: TrustedPrincipal.VendorName, @@ -333,8 +357,17 @@ func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) [] VendorName: TrustedPrincipal.VendorName, }) + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && !TrustedPrincipal.AccountIsInAnalyzedAccountList { + // Next lets take care of accounts that are not in the analyzed account list and add the full :root as the node + + newNodes = append(newNodes, Node{ + Arn: fmt.Sprintf("%s [Not analyzed/in-scope]", TrustedPrincipal.TrustedPrincipal), + Type: "Account", + AccountID: trustedPrincipalAccount, + }) + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, ":root") && TrustedPrincipal.VendorName == "" { - // check to see if the account is one of the analyzed accounts + // Now with those out of the way, lets take care of the accounts that are in the analyzed account list newNodes = append(newNodes, Node{ Arn: TrustedPrincipal.TrustedPrincipal, Type: "Account", @@ -387,11 +420,20 @@ func FindVerticesInRoleTrust(a Node, vendors *knownawsaccountslookup.Vendors) [] //providerAndSubject = TrustedFederatedProvider.ProviderShortName + ":" + trustedSubject providerAndSubject = fmt.Sprintf("%s [%s]", TrustedFederatedProvider.ProviderShortName, trustedSubject) } + //fmt.Println("TrustedFederatedProvider: ", TrustedFederatedProvider.TrustedFederatedProvider) + // if the TrustedFederatedProvider.TrustedFederatedProvider is an arn (check to see if it has at least 4 semicolons), grab the account id. Otherwise, use a.AccountID + var accountID string + if strings.Count(TrustedFederatedProvider.TrustedFederatedProvider, ":") >= 4 { + accountID = strings.Split(TrustedFederatedProvider.TrustedFederatedProvider, ":")[4] + } else { + accountID = a.AccountID + } + newNodes = append(newNodes, Node{ Arn: providerAndSubject, Name: TrustedFederatedProvider.ProviderShortName, Type: "FederatedIdentity", - AccountID: a.AccountID, + AccountID: accountID, }) } @@ -575,7 +617,6 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { // if the role we are looking at trusts root in it's own account if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", thisAccount)) { - // iterate over all rows in AllPermissionsRows for _, PermissionsRow := range common.PermissionRowsFromAllProfiles { // but we only care about the rows that have arns that are in this account @@ -680,6 +721,48 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } } + + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) && !TrustedPrincipal.AccountIsInAnalyzedAccountList { + // first lets check to see if the trustedRootAccountID is in the map of analzyeddAccounts + // if it is not, we can't interate over the permissions, so we will just have to create an edge :root princpal and this role + + err := GlobalGraph.AddEdge( + //TrustedPrincipal.TrustedPrincipal, + fmt.Sprintf("%s [Not analyzed/in-scope]", TrustedPrincipal.TrustedPrincipal), + a.Arn, + //graph.EdgeAttribute("CrossAccountRootTrust", "Cross account root trust and trusted principal is not in the analyzed account list"), + graph.EdgeAttribute("CrossAccountRootTrust", "can assume (because of a cross account root trust and trusted principal is not in the analyzed account list) "), + ) + if err != nil { + // fmt.Println(err) + // fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Cross account root trust and trusted principal is not in the analyzed account list") + if err == graph.ErrEdgeAlreadyExists { + // update the ege by copying the existing graph.Edge with attributes and add the new attributes + + // get the existing edge + existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.TrustedPrincipal, a.Arn) + // get the map of attributes + existingProperties := existingEdge.Properties + // add the new attributes to attributes map within the properties struct + // Check if the Attributes map is initialized, if not, initialize it + if existingProperties.Attributes == nil { + existingProperties.Attributes = make(map[string]string) + } + + // Add or update the attribute + existingProperties.Attributes["CrossAccountRootTrust"] = "can assume (because of a cross account root trust and trusted principal is not in the analyzed account list) " + err := GlobalGraph.UpdateEdge( + TrustedPrincipal.TrustedPrincipal, + a.Arn, + graph.EdgeAttributes(existingProperties.Attributes), + ) + if err != nil { + fmt.Println(err) + } + } + + } + } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) { // iterate over all rows in AllPermissionsRows diff --git a/aws/pmapper.go b/aws/pmapper.go index 9148e8f..e296724 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -81,9 +81,10 @@ type Node struct { } type TrustedPrincipal struct { - TrustedPrincipal string - ExternalID string - VendorName string + TrustedPrincipal string + ExternalID string + VendorName string + AccountIsInAnalyzedAccountList bool //IsAdmin bool //CanPrivEscToAdmin bool } @@ -97,6 +98,7 @@ type TrustedService struct { type TrustedFederatedProvider struct { TrustedFederatedProvider string + ProviderAccountId string ProviderShortName string TrustedSubjects []string //IsAdmin bool diff --git a/aws/role-trusts.go b/aws/role-trusts.go index 43fb0cb..5148f31 100644 --- a/aws/role-trusts.go +++ b/aws/role-trusts.go @@ -467,7 +467,11 @@ func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string subjects = append(subjects, "ALL REPOS!!!") } case strings.Contains(statement.Principal.Federated[0], "oidc.eks"): - provider = "EKS" + // extract accountId from statement.Principal.Federated[0] + accountId := strings.Split(statement.Principal.Federated[0], ":")[4] + provider = fmt.Sprintf("EKS-%s", accountId) + //provider = "EKS" + //provider = statement.Principal.Federated[0] if len(statement.Condition.StringLike.OidcEksSub) > 0 { subjects = append(subjects, statement.Condition.StringLike.OidcEksSub...) } else if len(statement.Condition.StringEquals.OidcEksSub) > 0 { diff --git a/cli/aws.go b/cli/aws.go index c643917..a3aab22 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -136,17 +136,18 @@ var ( PostRun: awsPostRun, } - CaperCommand = &cobra.Command{ - Use: "caper", - Aliases: []string{"caperParse"}, + CapeAdminOnly bool + CapeCommand = &cobra.Command{ + Use: "cape", + Aliases: []string{"capeParse"}, Short: "Cross-Account Privilege Escalation Route finder.\n" + "Needs to be run with multiple profiles using -l or -a flag\n" + "Needs pmapper data to be present", Long: "\nUse case examples:\n" + - os.Args[0] + " aws caper -l file_with_profile_names.txt", + os.Args[0] + " aws cape -l file_with_profile_names.txt", PreRun: awsPreRun, - Run: runCaperCommand, + Run: runCapeCommand, PostRun: awsPostRun, } @@ -987,7 +988,7 @@ func runGraphCommand(cmd *cobra.Command, args []string) { } } -func runCaperCommand(cmd *cobra.Command, args []string) { +func runCapeCommand(cmd *cobra.Command, args []string) { // map of all unique accountIDs and if they are included in the analysis or not analyzedAccounts := make(map[string]bool) @@ -998,28 +999,42 @@ func runCaperCommand(cmd *cobra.Command, args []string) { vendors := knownawsaccountslookup.NewVendorMap() vendors.PopulateKnownAWSAccounts() + for _, profile := range AWSProfiles { - var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) if err != nil { continue } - // add account number to analyzedAccounts map and set the value to true analyzedAccounts[ptr.ToString(caller.Account)] = true + } + + for _, profile := range AWSProfiles { + var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) + caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) + if err != nil { + continue + } + //Gather all Permissions data fmt.Println("Getting GAAD for " + profile) PermissionsCommandClient := aws.InitPermissionsClient(*caller, profile, cmd.Root().Version, Goroutines, AWSMFAToken) PermissionsCommandClient.GetGAAD() PermissionsCommandClient.ParsePermissions("") - common.PermissionRowsFromAllProfiles = append(common.PermissionRowsFromAllProfiles, PermissionsCommandClient.Rows...) + if PermissionsCommandClient.Rows != nil { + common.PermissionRowsFromAllProfiles = append(common.PermissionRowsFromAllProfiles, PermissionsCommandClient.Rows...) + } else { + fmt.Println("Error gathering permisisons for " + profile) + analyzedAccounts[ptr.ToString(caller.Account)] = false + } // Gather all Pmapper data. fmt.Println("Importing Pmapper for " + profile) pmapperMod, pmapperError := aws.InitPmapperGraph(*caller, AWSProfile, Goroutines) if pmapperError != nil { fmt.Println("Error importing pmapper data: " + pmapperError.Error()) + analyzedAccounts[ptr.ToString(caller.Account)] = false } // add pmapper nodes to GlobalNodes (which will also soon include iam roles and users) @@ -1043,7 +1058,8 @@ func runCaperCommand(cmd *cobra.Command, args []string) { internal.TxtLog.Error(err) } for _, role := range ListRolesOutput { - node := aws.ConvertIAMRoleToNode(role, vendors) + //node := aws.ConvertIAMRoleToNode(role, vendors) + node := aws.ConvertIAMRoleToNode(role, vendors, analyzedAccounts) // First insert the role itself into the Nodes slice GlobalNodes = append(GlobalNodes, node) @@ -1150,7 +1166,8 @@ func runCaperCommand(cmd *cobra.Command, args []string) { continue } - caperCommandClient := aws.CaperCommand{ + capeCommandClient := aws.CapeCommand{ + Caller: *caller, AWSProfile: profile, Goroutines: Goroutines, @@ -1166,9 +1183,10 @@ func runCaperCommand(cmd *cobra.Command, args []string) { GlobalGraph: GlobalGraph, PmapperDataBasePath: PmapperDataBasePath, AnalyzedAccounts: analyzedAccounts, + CapeAdminOnly: CapeAdminOnly, } - caperCommandClient.RunCaperCommand() + capeCommandClient.RunCapeCommand() // playing around with creating a graphviz file for image rendering. // the goal here is to be able to export this graph data to a format that can be easily imported in neo4j. @@ -2214,9 +2232,12 @@ func init() { // buckets command flags (for bucket policies) BucketsCommand.Flags().BoolVarP(&CheckBucketPolicies, "with-policies", "", false, "Analyze bucket policies (this is already done in the resource-trusts command)") - // pmapper flag for pmapper and caper commands + // cape command flags + CapeCommand.Flags().BoolVar(&CapeAdminOnly, "admin-only", false, "Only return paths that lead to an admin role - much faster") + + // pmapper flag for pmapper and cape commands //PmapperCommand.Flags().StringVarP(&PmapperDataBasePath, "pmapper-data-basepath", "pdata", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)") - //CaperCommand.Flags().StringVarP(&PmapperDataBasePath, "pmapper-data-basepath", "pdata", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)") + //CapeCommand.Flags().StringVarP(&PmapperDataBasePath, "pmapper-data-basepath", "pdata", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)") // Global flags for the AWS modules AWSCommands.PersistentFlags().StringVarP(&AWSProfile, "profile", "p", "", "AWS CLI Profile Name") @@ -2239,7 +2260,7 @@ func init() { AllChecksCommand, ApiGwCommand, BucketsCommand, - CaperCommand, + CapeCommand, CloudformationCommand, CodeBuildCommand, DatabasesCommand, diff --git a/main.go b/main.go index fd6a550..ca81a8d 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,9 @@ package main import ( + "log" "os" + "runtime/pprof" "github.com/BishopFox/cloudfox/cli" "github.com/spf13/cobra" @@ -15,6 +17,17 @@ var ( ) func main() { + cpuProfile := "cpu.prof" + f, err := os.Create(cpuProfile) + if err != nil { + log.Fatal("could not create CPU profile: ", err) + } + if err := pprof.StartCPUProfile(f); err != nil { + log.Fatal("could not start CPU profile: ", err) + } + defer pprof.StopCPUProfile() + + // Your program's main execution logic here rootCmd.AddCommand(cli.AWSCommands, cli.AzCommands) rootCmd.Execute() } From 590a94b5763ea1745b0ab2104ea121135732ab1a Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:10:22 -0400 Subject: [PATCH 20/29] changed println to printf --- aws/pmapper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/pmapper.go b/aws/pmapper.go index e296724..e4e6432 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -495,7 +495,7 @@ func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string if verbosity > 2 { fmt.Println() - fmt.Println("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Beginning of loot file")) + fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Beginning of loot file")) fmt.Print(out) fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file")) } From 3a4558301519d9d0eadc1c4a9aa84ca9219dd532 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:07:37 -0400 Subject: [PATCH 21/29] Got cape working without any aws calls, cleaned up logging messages --- aws/cape-tui.go | 523 ++++++++++++++++++++++++++++++++++++++++++++++ aws/cape.go | 22 +- cli/aws.go | 109 ++++++++-- internal/aws.go | 108 +++++++--- internal/cache.go | 28 +-- 5 files changed, 708 insertions(+), 82 deletions(-) create mode 100644 aws/cape-tui.go diff --git a/aws/cape-tui.go b/aws/cape-tui.go new file mode 100644 index 0000000..6290840 --- /dev/null +++ b/aws/cape-tui.go @@ -0,0 +1,523 @@ +package aws + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + titleStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Right = "├" + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() + + infoStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Left = "┤" + return titleStyle.Copy().BorderStyle(b) + }() + magentaStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("magenta")) +) + +type model struct { + preloadedData *AllAccountData + awsAccountsTable table.Model + awsAccountsViewport viewport.Model + + mainTable table.Model + mainTableViewport viewport.Model + mainTableData map[int][]table.Row + + detailsData map[int]string + detailsViewport viewport.Model // Use viewport for details view + + focusSelector int + defaultTableStyle table.Styles + + terminalWidth int + terminalHeight int // Add terminal height to manage viewport size + awsSelectedRow int + mainSelectedRow int // Track the currently selected row for detail view updates + keys keyMap + help help.Model + lastKey string + quitting bool +} + +type CapeJSON struct { + Account string `json:"account"` + Source string `json:"source"` + Summary string `json:"summary"` + Target string `json:"target"` + IsTargetAdmin string `json:"isTargetAdmin"` +} + +type PerAccountData struct { + FilePath string // Path to the JSON file + PrivescPaths []CapeJSON // All records contained in the file +} + +type AllAccountData struct { + Files map[string]*PerAccountData // Map of file paths to their records +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.terminalWidth = msg.Width + m.terminalHeight = msg.Height - 7 // Adjust as needed + + // Calculate heights based on the specified percentages + awsAccountsHeight := int(float32(m.terminalHeight) * 0.2) + halfHeight := int(float32(m.terminalHeight) * 0.4) // For the other two viewports + + // Update dimensions for all viewports + m.awsAccountsViewport.Width = m.terminalWidth - 4 + m.awsAccountsViewport.Height = awsAccountsHeight + + m.mainTableViewport.Width = m.terminalWidth - 4 + m.mainTableViewport.Height = halfHeight + m.awsAccountsTable.SetHeight(awsAccountsHeight - 2) + m.awsAccountsTable.SetWidth(m.terminalWidth - 4) + m.awsAccountsViewport.Width = m.terminalWidth - 4 + + m.detailsViewport.Width = m.terminalWidth - 4 + m.detailsViewport.Height = halfHeight + m.mainTable.SetHeight(halfHeight - 2) + + case tea.KeyMsg: + switch { + case key.Matches(msg, m.keys.Up): + m.lastKey = "↑" + case key.Matches(msg, m.keys.Down): + m.lastKey = "↓" + case key.Matches(msg, m.keys.Left): + m.lastKey = "←" + case key.Matches(msg, m.keys.Right): + m.lastKey = "→" + case key.Matches(msg, m.keys.Help): + m.help.ShowAll = !m.help.ShowAll + case key.Matches(msg, m.keys.Tab): + // Switch focus between the the three viewports by cycling focusSelector between 0, 1, and 2 + m.focusSelector = (m.focusSelector + 1) % 3 + //m.focusOnTable = !m.focusOnTable + // add a case for shift and tab at the same time to to cycle the focusSelector backwards between 0, 1, and 2 + case key.Matches(msg, m.keys.ShiftTab): + m.focusSelector = (m.focusSelector - 1) % 3 + + case key.Matches(msg, m.keys.Quit): + m.quitting = true + return m, tea.Quit + } + + //var detailsData map[int]string + //var mainTable table.Model + var err error + var awsCurrentRow int + var mainCurrentRow int + switch m.focusSelector { + case 0: + m.awsAccountsTable, cmd = m.awsAccountsTable.Update(msg) + m.awsAccountsViewport.SetContent(m.awsAccountsTable.View()) + awsCurrentRow = m.awsAccountsTable.Cursor() + if m.awsSelectedRow != awsCurrentRow { + m.awsSelectedRow = awsCurrentRow + // Update the viewport content based on the newly selected row + // Load the file for the selected account and show the file content in the main table + m.mainTable, m.detailsData, err = getRecordsForAccount(m.preloadedData.Files[m.awsAccountsTable.Rows()[awsCurrentRow][0]]) + if err != nil { + // Handle error + break + } + m.mainTable.SetStyles(s) + m.mainTableViewport.SetContent(m.mainTable.View()) + m.detailsViewport.SetContent(m.detailsData[0]) + + } + case 1: + m.mainTable, cmd = m.mainTable.Update(msg) + // Check if the selected row has changed + m.mainTableViewport.SetContent(m.mainTable.View()) + + mainCurrentRow = m.mainTable.Cursor() + if m.mainSelectedRow != mainCurrentRow { + m.mainSelectedRow = mainCurrentRow + // Update the viewport content based on the newly selected row + if detail, ok := m.detailsData[m.mainSelectedRow]; ok { + m.detailsViewport.SetContent(detail) + } else { + m.detailsViewport.SetContent(fmt.Sprintf("No details available for main row %d (aws row %d)", m.mainSelectedRow, m.awsSelectedRow)) + } + } + case 2: + m.detailsViewport, cmd = m.detailsViewport.Update(msg) + } + } + return m, cmd +} + +func (m model) View() string { + // Define styles for active and inactive viewports + activeBorderStyle := lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#00FFFF")). // Cyan for active + Padding(0) + activeBorderStyle.PaddingLeft(1) + activeBorderStyle.PaddingRight(1) + + inactiveBorderStyle := lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#808080")). // Gray for inactive + Padding(0) + inactiveBorderStyle.PaddingLeft(1) + inactiveBorderStyle.PaddingRight(1) + + // Conditionally apply border styles based on focus + awsTableViewStyle := activeBorderStyle + m.awsAccountsTable.SetStyles(m.defaultTableStyle) + tableViewStyle := inactiveBorderStyle + m.mainTable.SetStyles(m.defaultTableStyle) + detailsViewStyle := inactiveBorderStyle + + switch m.focusSelector { + case 0: + awsTableViewStyle = activeBorderStyle + m.awsAccountsTable.SetStyles(m.defaultTableStyle) + tableViewStyle = inactiveBorderStyle + detailsViewStyle = inactiveBorderStyle + case 1: + awsTableViewStyle = inactiveBorderStyle + tableViewStyle = activeBorderStyle + m.mainTable.SetStyles(m.defaultTableStyle) + detailsViewStyle = inactiveBorderStyle + case 2: + awsTableViewStyle = inactiveBorderStyle + tableViewStyle = inactiveBorderStyle + detailsViewStyle = activeBorderStyle + } + + // Render the table and details viewports with their styles (adjust based on focus) + awsAccountsView := awsTableViewStyle.Render(m.awsAccountsViewport.View()) + tableView := tableViewStyle.Render(m.mainTableViewport.View()) + detailsView := detailsViewStyle.Render(m.detailsViewport.View()) + + // Combine all views + fullView := lipgloss.JoinVertical(lipgloss.Top, awsAccountsView, tableView, detailsView) + + helpView := m.help.View(m.keys) + + return fmt.Sprintf("%s\n%s", fullView, helpView) +} + +func calculateMaxWidths(rows []table.Row) []int { + + var maxWidths []int + // make sure the length of the rows is greater than 0 + + if len(rows) > 0 { + maxWidths = make([]int, len(rows[0])) + } else { + maxWidths = []int{30, 30, 30, 30} + } + + for _, row := range rows { + for i, cell := range row { + if len(cell) > maxWidths[i] { + maxWidths[i] = len(cell) + } + } + } + + return maxWidths + +} + +func (m model) footerView() string { + info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.detailsViewport.ScrollPercent()*100)) + line := strings.Repeat("─", max(0, m.detailsViewport.Width-lipgloss.Width(info))) + return lipgloss.JoinHorizontal(lipgloss.Center, line, info) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func loadFileRecords(filePath string) (*PerAccountData, error) { + // Open the file + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + // Decode the JSON data + var records []CapeJSON + decoder := json.NewDecoder(file) + if err := decoder.Decode(&records); err != nil { + return nil, err + } + + // Return a new FileRecords instance + return &PerAccountData{ + FilePath: filePath, + PrivescPaths: records, + }, nil +} + +func preloadData(filePaths []string) (*AllAccountData, error) { + appData := &AllAccountData{ + Files: make(map[string]*PerAccountData), + } + + for _, filePath := range filePaths { + fileRecords, err := loadFileRecords(filePath) + if err != nil { + return nil, err + } + appData.Files[filePath] = fileRecords + } + + return appData, nil +} + +func getRecordsForAccount(preloadedData *PerAccountData) (table.Model, map[int]string, error) { + // lets load the records for the first file in the list + + records := preloadedData.PrivescPaths + + // Prepare rows for the table and data for the right view + rows := make([]table.Row, 0, len(records)-1) + detailsData := make(map[int]string) // Initialize the map for the fourth column's data + for i, record := range records[1:] { // Skip the header row + rows = append(rows, table.Row{record.Account, record.Source, record.Target, record.IsTargetAdmin}) + detailsData[i] = expandDetailsData(record.Summary) + } + + // Calculate max widths + maxWidths := calculateMaxWidths(rows) + + // Define columns with calculated widths + columns := make([]table.Column, len(maxWidths)) + colNames := []string{} + for _, header := range []string{"Account", "Source", "Target", "isTargetAdmin"} { + colNames = append(colNames, header) + } + + for i, width := range maxWidths { + colName := colNames[i] + // if column name width is greater than the calculated width, use the column name width + if len(colName) > width { + width = len(colName) + } + columns[i] = table.Column{ + Title: colName, + Width: width, + } + } + + mainTable := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + ) + return mainTable, detailsData, nil +} + +func CapeTUI(outputFiles []string) { + + preloadData, err := preloadData(outputFiles) + if err != nil { + fmt.Printf("Error preloading data: %s\n", err) + os.Exit(1) + } + + var awsAccountsRows []table.Row + for _, file := range preloadData.Files { + // Extract a user-friendly account name from the file path if needed + // For simplicity, here we use the file path itself + awsAccountsRows = append(awsAccountsRows, table.Row{file.FilePath}) + } + + mainTable, detailsData, err := getRecordsForAccount(preloadData.Files[outputFiles[0]]) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + + awsAccountsWidth := calculateMaxWidths(awsAccountsRows) + awsAccountsTable := table.New( + table.WithColumns([]table.Column{ + {Title: "AWS Accounts", Width: awsAccountsWidth[0]}, + }), + table.WithRows(awsAccountsRows), + table.WithFocused(true), // Initially unfocused + ) + + awsAccountsTable.SetStyles(s) + awsAccountsViewport := viewport.New(0, 0) // Initialize with 0 size; will be updated + awsAccountsViewport.SetContent(awsAccountsTable.View()) // Set initial content + + mainTable.SetStyles(s) + mainTableViewport := viewport.New(0, 0) // Initialize with 0 size; it will be updated + mainTableViewport.SetContent(mainTable.View()) // Set the initial content of the table viewport + + // Initialize viewport for details view + detailsViewportModel := viewport.New(0, 0) // Size will be set based on terminal size in Update + // show the first row's details by default + detailsViewportModel.SetContent(detailsData[0]) + + m := model{ + preloadedData: preloadData, + awsAccountsTable: awsAccountsTable, + awsAccountsViewport: awsAccountsViewport, + mainTable: mainTable, + mainTableViewport: mainTableViewport, + detailsData: detailsData, + detailsViewport: detailsViewportModel, + + //focusOnTable: true, + focusSelector: 0, + mainSelectedRow: 0, // Initialize selectedRow with an invalid index + awsSelectedRow: 0, + defaultTableStyle: s, + keys: keys, + help: help.New(), + } + + p := tea.NewProgram(m) + if err := p.Start(); err != nil { + fmt.Printf("Error starting program: %s\n", err) + os.Exit(1) + } +} + +// keyMap defines a set of keybindings. To work for help it must satisfy +// key.Map. It could also very easily be a map[string]key.Binding. +type keyMap struct { + Up key.Binding + Down key.Binding + Left key.Binding + Right key.Binding + Help key.Binding + Quit key.Binding + Tab key.Binding + ShiftTab key.Binding +} + +// ShortHelp returns keybindings to be shown in the mini help view. It's part +// of the key.Map interface. +func (k keyMap) ShortHelp() []key.Binding { + return []key.Binding{k.Tab, k.ShiftTab, k.Up, k.Down, k.Help, k.Quit} +} + +// FullHelp returns keybindings for the expanded help view. It's part of the +// key.Map interface. +func (k keyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Up, k.Down, k.Left, k.Right}, // first column + {k.Tab, k.ShiftTab, k.Help, k.Quit}, // second column + } +} + +var keys = keyMap{ + Up: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("↑/k", "move up"), + ), + Down: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "move down"), + ), + Left: key.NewBinding( + key.WithKeys("left", "h"), + key.WithHelp("←/h", "move left"), + ), + Right: key.NewBinding( + key.WithKeys("right", "l"), + key.WithHelp("→/l", "move right"), + ), + Help: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "toggle help"), + ), + Quit: key.NewBinding( + key.WithKeys("q", "esc", "ctrl+c"), + key.WithHelp("q", "quit"), + ), + Tab: key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "Switch window focus"), + ), + ShiftTab: key.NewBinding( + key.WithKeys("shift+tab"), + key.WithHelp("shift+tab", "Switch window focus"), + ), +} + +func expandDetailsData(input string) string { + output := "" + lines := strings.Split(input, "\n") + + for _, line := range lines { + var hop, option int + var rest string + + // Use Sscanf to extract hop and option values + n, err := fmt.Sscanf(line, "[Hop: %d] [Option: %d]", &hop, &option) + if err != nil || n != 2 { + fmt.Printf("Error parsing line, expected 2 got %d: %v\n", n, err) + continue + } + + // Extract the rest of the line manually + restIndex := strings.Index(line, "]") + 1 + restIndex = strings.Index(line[restIndex:], "]") + restIndex + 1 + if restIndex > 1 && restIndex < len(line) { + rest = line[restIndex+1:] // +1 to skip the space after the second ] + } + + // Construct the formatted output + formattedLine := fmt.Sprintf("[Hop: %d][Option: %d]\n\t%s\n\n", hop, option, rest) + output += formattedLine + } + + return output + +} diff --git a/aws/cape.go b/aws/cape.go index 5938be5..59a9bcc 100644 --- a/aws/cape.go +++ b/aws/cape.go @@ -35,13 +35,21 @@ type CapeCommand struct { SkipAdminCheck bool GlobalGraph graph.Graph[string, string] PmapperDataBasePath string - AnalyzedAccounts map[string]bool + AnalyzedAccounts map[string]CapeJobInfo CapeAdminOnly bool output internal.OutputData2 modLog *logrus.Entry } +type CapeJobInfo struct { + AccountID string + Profile string + AnalyzedSuccessfully bool + AdminOnlyAnalysis bool + Source string +} + func (m *CapeCommand) RunCapeCommand() { // These struct values are used by the output module @@ -68,7 +76,7 @@ func (m *CapeCommand) RunCapeCommand() { o.Table.DirectoryName = filepath.Join(m.AWSOutputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) // Table #1: Inbound Privilege Escalation Paths - fmt.Println("Printing inbound privesc paths for account: ", aws.ToString(m.Caller.Account)) + fmt.Printf("[%s][%s] Printing inbound privesc paths for account: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) header, body, _ := m.generateInboundPrivEscTableData() o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ Header: header, @@ -92,7 +100,7 @@ func (m *CapeCommand) RunCapeCommand() { fmt.Println("The following accounts are trusted by this account, but were not analyzed as part of this run.") fmt.Println("As a result, we cannot determine which principals in these accounts have permission to assume roles in this account.") for account := range m.AnalyzedAccounts { - if m.AnalyzedAccounts[account] == false { + if m.AnalyzedAccounts[account].AnalyzedSuccessfully == false { fmt.Println("\t\t" + account) } } @@ -187,7 +195,7 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s j := 0 for _, value := range thisEdge.Properties.Attributes { value = strings.ReplaceAll(value, ",", " and") - paths += fmt.Sprintf("[Hop: %d] [Option: %d] [%s] %s [%s]\n", i, j, thisEdge.Source, value, thisEdge.Target) + paths += fmt.Sprintf("[Hop: %d] [Option: %d] [%s] [%s] [%s]\n", i, j, thisEdge.Source, value, thisEdge.Target) j++ } } @@ -216,7 +224,7 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s return privescPathsBody } -func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendors, analyzedAccounts map[string]bool) Node { +func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendors, analyzedAccounts map[string]CapeJobInfo) Node { //var isAdmin, canPrivEscToAdmin string accountId := strings.Split(aws.ToString(role.Arn), ":")[4] @@ -243,7 +251,7 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo vendorName = vendors.GetVendorNameFromAccountID(trustedRootAccountID) // check to see if trustedRootAccountID is in the m.AnalyzedAccounts map if _, ok := analyzedAccounts[trustedRootAccountID]; ok { - isAnalyzedAccount = analyzedAccounts[trustedRootAccountID] + isAnalyzedAccount = analyzedAccounts[trustedRootAccountID].AnalyzedSuccessfully } else { isAnalyzedAccount = false } @@ -925,5 +933,3 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } } - -// func (a *Node) MakeUserEdges(GlobalGraph graph.Graph[string, string]) { diff --git a/cli/aws.go b/cli/aws.go index a3aab22..d043748 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -2,10 +2,12 @@ package cli import ( "encoding/gob" + "encoding/json" "fmt" "log" "os" "path/filepath" + "time" "github.com/BishopFox/cloudfox/aws" "github.com/BishopFox/cloudfox/aws/sdk" @@ -70,6 +72,7 @@ var ( cyan = color.New(color.FgCyan).SprintFunc() green = color.New(color.FgGreen).SprintFunc() red = color.New(color.FgRed).SprintFunc() + magenta = color.New(color.FgMagenta).SprintFunc() defaultOutputDir = ptr.ToString(internal.GetLogDirPath()) AWSProfile string @@ -137,6 +140,7 @@ var ( } CapeAdminOnly bool + CapeJobName string CapeCommand = &cobra.Command{ Use: "cape", Aliases: []string{"capeParse"}, @@ -956,7 +960,8 @@ func runGraphCommand(cmd *cobra.Command, args []string) { } //instantiate a permissions client and populate the permissions data - fmt.Println("Getting GAAD for " + profile) + fmt.Printf("[%s][%s] Getting account authorization details (GAAD) for account: %s\n", cyan("cape"), cyan(profile), ptr.ToString(caller.Account)) + PermissionsCommandClient := aws.InitPermissionsClient(*caller, profile, cmd.Root().Version, Goroutines, AWSMFAToken) PermissionsCommandClient.GetGAAD() PermissionsCommandClient.ParsePermissions("") @@ -990,7 +995,8 @@ func runGraphCommand(cmd *cobra.Command, args []string) { func runCapeCommand(cmd *cobra.Command, args []string) { // map of all unique accountIDs and if they are included in the analysis or not - analyzedAccounts := make(map[string]bool) + //analyzedAccounts := make(map[string]bool) + analyzedAccounts := make(map[string]aws.CapeJobInfo) GlobalGraph := graph.New(graph.StringHash, graph.Directed()) //var PermissionRowsFromAllProfiles []common.PermissionsRow @@ -1005,8 +1011,14 @@ func runCapeCommand(cmd *cobra.Command, args []string) { if err != nil { continue } + _, err = internal.InitializeCloudFoxRunData(profile, cmd.Root().Version, AWSMFAToken, AWSOutputDirectory) + if err != nil { + continue + } + // add account number to analyzedAccounts map and set the value to true - analyzedAccounts[ptr.ToString(caller.Account)] = true + //analyzedAccounts[ptr.ToString(caller.Account)] = true + analyzedAccounts[ptr.ToString(caller.Account)] = aws.CapeJobInfo{AccountID: ptr.ToString(caller.Account), Profile: profile, AnalyzedSuccessfully: true, AdminOnlyAnalysis: CapeAdminOnly, Source: "user"} } @@ -1018,7 +1030,7 @@ func runCapeCommand(cmd *cobra.Command, args []string) { } //Gather all Permissions data - fmt.Println("Getting GAAD for " + profile) + fmt.Printf("[%s][%s] Getting account authorization details (GAAD) for account: %s\n", cyan("cape"), cyan(profile), ptr.ToString(caller.Account)) PermissionsCommandClient := aws.InitPermissionsClient(*caller, profile, cmd.Root().Version, Goroutines, AWSMFAToken) PermissionsCommandClient.GetGAAD() PermissionsCommandClient.ParsePermissions("") @@ -1026,15 +1038,19 @@ func runCapeCommand(cmd *cobra.Command, args []string) { common.PermissionRowsFromAllProfiles = append(common.PermissionRowsFromAllProfiles, PermissionsCommandClient.Rows...) } else { fmt.Println("Error gathering permisisons for " + profile) - analyzedAccounts[ptr.ToString(caller.Account)] = false + //analyzedAccounts[ptr.ToString(caller.Account)] = false + analyzedAccounts[ptr.ToString(caller.Account)] = aws.CapeJobInfo{AnalyzedSuccessfully: false} + } // Gather all Pmapper data. - fmt.Println("Importing Pmapper for " + profile) + fmt.Printf("[%s][%s] Importing Pmapper for: %s\n", cyan("cape"), cyan(profile), ptr.ToString(caller.Account)) + pmapperMod, pmapperError := aws.InitPmapperGraph(*caller, AWSProfile, Goroutines) if pmapperError != nil { - fmt.Println("Error importing pmapper data: " + pmapperError.Error()) - analyzedAccounts[ptr.ToString(caller.Account)] = false + fmt.Println("Error importing pmapper data " + pmapperError.Error()) + //analyzedAccounts[ptr.ToString(caller.Account)] = false + analyzedAccounts[ptr.ToString(caller.Account)] = aws.CapeJobInfo{AnalyzedSuccessfully: false} } // add pmapper nodes to GlobalNodes (which will also soon include iam roles and users) @@ -1051,7 +1067,8 @@ func runCapeCommand(cmd *cobra.Command, args []string) { } //Gather all role data so we can later process all of the role trusts and add external nodes not looked at by pmapper - fmt.Println("Getting Roles for " + profile) + fmt.Printf("[%s][%s] Getting IAM roles for %s\n", cyan("cape"), cyan(profile), ptr.ToString(caller.Account)) + IAMCommandClient := aws.InitIAMClient(AWSConfig) ListRolesOutput, err := sdk.CachedIamListRoles(IAMCommandClient, ptr.ToString(caller.Account)) if err != nil { @@ -1071,7 +1088,8 @@ func runCapeCommand(cmd *cobra.Command, args []string) { //Gather all user data // Currently, there is no need to parse groups and build group-user relationships because // the permissions command (and common.PermissionRowsFromAllProfiles above already has mapped/assigned group permissions to users within the group - fmt.Println("Getting Users for " + profile) + fmt.Printf("[%s][%s] Getting IAM users for %s\n", cyan("cape"), cyan(profile), ptr.ToString(caller.Account)) + ListUsersOutput, err := sdk.CachedIamListUsers(IAMCommandClient, ptr.ToString(caller.Account)) if err != nil { internal.TxtLog.Error(err) @@ -1088,7 +1106,8 @@ func runCapeCommand(cmd *cobra.Command, args []string) { // make vertices // you can't update vertices - so we need to make all of the vertices that are roles in the in-scope accounts // all at once to make sure they have the most information possible - fmt.Println("Making vertices for all profiles") + fmt.Printf("[%s] Making vertices for all profiles\n", cyan("cape")) + // for _, role := range GlobalRoles { // role.MakeVertices(GlobalGraph) // } @@ -1106,7 +1125,9 @@ func runCapeCommand(cmd *cobra.Command, args []string) { // for every node, check to see if the accountId exists in the analyzedAccounts map. If it does not, add it to the map and set the value to false only if the node.VendorName is empty if _, ok := analyzedAccounts[node.AccountID]; !ok { if node.VendorName == "" { - analyzedAccounts[node.AccountID] = false + if node.AccountID != "" { + analyzedAccounts[node.AccountID] = aws.CapeJobInfo{AccountID: node.AccountID, Profile: "", AnalyzedSuccessfully: false, AdminOnlyAnalysis: CapeAdminOnly, Source: "cloudfox"} + } } } } @@ -1149,7 +1170,8 @@ func runCapeCommand(cmd *cobra.Command, args []string) { //making edges // these are the cloudfox created edges mainly based on role trusts // at least for now, we don't need to make edges for users, groups, or anything else because pmapper already has all of the edges we need - fmt.Println("Making edges for all profiles") + fmt.Printf("[%s] Making edges for all profiles\n", cyan("cape")) + for _, node := range mergedNodes { if node.Type == "Role" { node.MakeRoleEdges(GlobalGraph) @@ -1188,6 +1210,32 @@ func runCapeCommand(cmd *cobra.Command, args []string) { capeCommandClient.RunCapeCommand() + // write a json file with job information to the output directory. Use the CapeJobName for hte file name, and have the data include the list of AWSProfiles that were analyzed + // this will be used by a TUI to match a job name to the list of accounts that were analyzed + + if CapeJobName == "" { + // create random job name in the format of cape-timmefromepoch + CapeJobName = fmt.Sprintf("cape-%s", time.Now().Format("2006-01-02-15-04-05")) + } + filename := fmt.Sprintf("%s.json", CapeJobName) + filepath := filepath.Join(AWSOutputDirectory, "aws", "capeJobs") + err = os.MkdirAll(filepath, 0755) + if err != nil { + fmt.Println("Error creating directory: " + err.Error()) + } + file, _ := os.Create(filepath + "/" + filename) + defer file.Close() + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + err = encoder.Encode(analyzedAccounts) + if err != nil { + fmt.Println("Error writing job data to file: " + err.Error()) + } else { + fmt.Printf("[%s] Job output written to %s\n", cyan("cape"), file.Name()) + fmt.Printf("[%s] %s\n\n", cyan("cape"), magenta("The results of the cape command are best viewed in the cape terminal user interface (TUI). Use the command below:")) + fmt.Printf("[%s] \tcloudfox aws -l %s cape tui\n\n", cyan("cape"), AWSProfilesList) + } + // playing around with creating a graphviz file for image rendering. // the goal here is to be able to export this graph data to a format that can be easily imported in neo4j. // this is a work in progress and not yet complete @@ -1200,6 +1248,25 @@ func runCapeCommand(cmd *cobra.Command, args []string) { } } +func runCapeTUICommand(cmd *cobra.Command, args []string) { + var capeOutputFileLocations []string + for _, profile := range AWSProfiles { + cloudfoxRunData, err := internal.InitializeCloudFoxRunData(profile, cmd.Root().Version, AWSMFAToken, AWSOutputDirectory) + //caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) + if err != nil { + continue + } + capeOutputFileLocations = append(capeOutputFileLocations, filepath.Join(cloudfoxRunData.OutputLocation, "json", "inbound-privesc-paths.json")) + + } + if len(capeOutputFileLocations) == 0 { + fmt.Printf("[%s] Could not retrieve CAPE data.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version))) + os.Exit(1) + } + aws.CapeTUI(capeOutputFileLocations) + +} + func runIamSimulatorCommand(cmd *cobra.Command, args []string) { for _, profile := range AWSProfiles { var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) @@ -2193,6 +2260,17 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { } } +var CapeTuiCmd = &cobra.Command{ + Use: "tui", + Aliases: []string{"TUI", "view", "report"}, + Short: "View Cape's output in a TUI", + Long: "\nUse case examples:\n" + + os.Args[0] + " aws cape tui -l /path/to/profiles-used-for-cape.txt", + //PreRun: awsPreRun, + Run: runCapeTUICommand, + //PostRun: awsPostRun, +} + func init() { cobra.OnInitialize(initAWSProfiles) @@ -2234,6 +2312,7 @@ func init() { // cape command flags CapeCommand.Flags().BoolVar(&CapeAdminOnly, "admin-only", false, "Only return paths that lead to an admin role - much faster") + CapeCommand.Flags().StringVar(&CapeJobName, "job-name", "", "Name of the cape job") // pmapper flag for pmapper and cape commands //PmapperCommand.Flags().StringVarP(&PmapperDataBasePath, "pmapper-data-basepath", "pdata", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)") @@ -2293,4 +2372,8 @@ func init() { WorkloadsCommand, ) + CapeCommand.AddCommand( + CapeTuiCmd, + ) + } diff --git a/internal/aws.go b/internal/aws.go index 49cb173..aae73aa 100644 --- a/internal/aws.go +++ b/internal/aws.go @@ -4,8 +4,10 @@ import ( "bufio" "context" "encoding/gob" + "encoding/json" "fmt" "os" + "path/filepath" "regexp" "strings" "time" @@ -32,9 +34,81 @@ var ( ConfigMap = map[string]aws.Config{} ) +type CloudFoxRunData struct { + Profile string + AccountID string + OutputLocation string +} + func init() { gob.Register(aws.Config{}) gob.Register(sts.GetCallerIdentityOutput{}) + gob.Register(CloudFoxRunData{}) +} + +func InitializeCloudFoxRunData(AWSProfile string, version string, AwsMfaToken string, AWSOutputDirectory string) (CloudFoxRunData, error) { + var runData CloudFoxRunData + + cacheDirectory := filepath.Join(AWSOutputDirectory, "cached-data", "aws") + filename := filepath.Join(cacheDirectory, fmt.Sprintf("CloudFoxRunData-%s.json", AWSProfile)) + if _, err := os.Stat(filename); err == nil { + // unmarshall the data from the file into type CloudFoxRunData + + // Open the file (this is not actually needed if you use os.ReadFile, so you can skip this) + file, err := os.Open(filename) + if err != nil { + return CloudFoxRunData{}, err + } + defer file.Close() + + // Read the file content + jsonData, err := os.ReadFile(filename) + if err != nil { + return CloudFoxRunData{}, err + } + + // Unmarshal jsonData into runData (make sure to pass a pointer to runData) + err = json.Unmarshal(jsonData, &runData) + if err != nil { + return CloudFoxRunData{}, err + } + + return runData, nil + + } + + CallerIdentity, err := AWSWhoami(AWSProfile, version, AwsMfaToken) + if err != nil { + return CloudFoxRunData{}, err + } + outputLocation := filepath.Join(AWSOutputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", AWSProfile, ptr.ToString(CallerIdentity.Account))) + + runData = CloudFoxRunData{ + Profile: AWSProfile, + AccountID: aws.ToString(CallerIdentity.Account), + OutputLocation: outputLocation, + } + + // Marshall the data to a file + err = os.MkdirAll(cacheDirectory, 0755) + if err != nil { + return CloudFoxRunData{}, err + } + file, err := os.Create(filename) + if err != nil { + return CloudFoxRunData{}, err + } + defer file.Close() + jsonData, err := json.Marshal(runData) + if err != nil { + return CloudFoxRunData{}, err + } + _, err = file.Write(jsonData) + if err != nil { + return CloudFoxRunData{}, err + } + + return runData, nil } func AWSConfigFileLoader(AWSProfile string, version string, AwsMfaToken string) aws.Config { @@ -169,40 +243,6 @@ func GetEnabledRegions(awsProfile string, version string, AwsMfaToken string) [] } -// func GetRegionsForService(awsProfile string, service string) []string { -// SSMClient := ssm.NewFromConfig(AWSConfigFileLoader(awsProfile)) -// var PaginationControl *string -// var supportedRegions []string -// path := fmt.Sprintf("/aws/service/global-infrastructure/services/%s/regions", service) - -// ServiceRegions, err := SSMClient.GetParametersByPath( -// context.TODO(), -// &(ssm.GetParametersByPathInput{ -// NextToken: PaginationControl, -// Path: &path, -// }), -// ) -// if err != nil { -// fmt.Println(err.Error()) - -// } - -// if ServiceRegions.Parameters != nil { -// for _, region := range ServiceRegions.Parameters { -// name := *region.Value -// supportedRegions = append(supportedRegions, name) -// } - -// // The "NextToken" value is nil when there's no more data to return. -// if ServiceRegions.NextToken != nil { -// PaginationControl = ServiceRegions.NextToken -// } else { -// PaginationControl = nil -// } -// } -// return supportedRegions -// } - // txtLogger - Returns the txt logger func TxtLogger() *logrus.Logger { var txtFile *os.File diff --git a/internal/cache.go b/internal/cache.go index 458c3a6..bcc3e00 100644 --- a/internal/cache.go +++ b/internal/cache.go @@ -24,7 +24,6 @@ func SaveCacheToFiles(directory string, accountID string) error { } for key, item := range Cache.Items() { - // only if the key contains the accountID if accountID != "" && strings.Contains(key, accountID) { entry := cacheEntry{ Value: item.Object, @@ -42,6 +41,7 @@ func SaveCacheToFiles(directory string, accountID string) error { return err } } + } return nil } @@ -103,14 +103,6 @@ func SaveCacheToGobFiles(directory string, accountID string) error { } for key, item := range Cache.Items() { - // if accountID != "" && strings.Contains(key, accountID) || - // strings.Contains(key, "AWSConfigFileLoader") || - // strings.Contains(key, "GetEnabledRegions") || - // strings.Contains(key, "GetCallerIdentity") { - // entry := cacheEntry{ - // Value: item.Object, - // Exp: item.Expiration, - // } // only if the key contains the accountID if accountID != "" && strings.Contains(key, accountID) { @@ -126,16 +118,6 @@ func SaveCacheToGobFiles(directory string, accountID string) error { } defer file.Close() - // if config, ok := item.Object.(aws.Config); ok { - // cacheableConfig := converAWSConfigToCacheableAWSConfig(config) - // encoder := gob.NewEncoder(file) - // err = encoder.Encode(cacheableConfig) - // if err != nil { - // sharedLogger.Errorf("Could not encode the following key: %s", key) - // return err - // } - - // } else { encoder := gob.NewEncoder(file) err = encoder.Encode(entry) if err != nil { @@ -148,14 +130,6 @@ func SaveCacheToGobFiles(directory string, accountID string) error { return nil } -// func converAWSConfigToCacheableAWSConfig(config aws.Config) CacheableAWSConfig { -// return CacheableAWSConfig{ -// Region: config.Region, -// //Credentials: config.Credentials, -// //ConfigSources: config.ConfigSources, -// } -// } - var ErrDirectoryDoesNotExist = errors.New("directory does not exist") func LoadCacheFromGobFiles(directory string) error { From 356d57b2dec072ad49f07758009b14dc166e3bef Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:49:33 -0400 Subject: [PATCH 22/29] Added pmapper basepath to all relevent commands. Improved logging for cape/cape-tui. Fixed codebuld cache bug. --- aws/cape-tui.go | 1 + aws/cape.go | 17 +- aws/codebuild.go | 11 +- aws/ecs-tasks.go | 11 +- aws/eks.go | 11 +- aws/graph.go | 27 +-- aws/instances.go | 15 +- aws/lambda.go | 18 +- aws/pmapper.go | 8 +- aws/role-trusts.go | 12 +- aws/sdk/codebuild.go | 1 + aws/shared.go | 9 +- aws/workloads.go | 8 +- cli/aws.go | 380 +++++++++++++++++++++++++------------------ 14 files changed, 313 insertions(+), 216 deletions(-) diff --git a/aws/cape-tui.go b/aws/cape-tui.go index 6290840..c2422b9 100644 --- a/aws/cape-tui.go +++ b/aws/cape-tui.go @@ -358,6 +358,7 @@ func CapeTUI(outputFiles []string) { preloadData, err := preloadData(outputFiles) if err != nil { fmt.Printf("Error preloading data: %s\n", err) + fmt.Println("Either remove this profile from the list of profiles, or make sure cape can run successfully for this profile") os.Exit(1) } diff --git a/aws/cape.go b/aws/cape.go index 59a9bcc..8c9cccf 100644 --- a/aws/cape.go +++ b/aws/cape.go @@ -77,12 +77,26 @@ func (m *CapeCommand) RunCapeCommand() { // Table #1: Inbound Privilege Escalation Paths fmt.Printf("[%s][%s] Printing inbound privesc paths for account: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) + if !m.CapeAdminOnly { + fmt.Printf("[%s][%s] This can take a really long time if the number of vertices/edges is in the thousands. Consider stopping here and re-running cape with --admin-only to speed this step up!\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) + } else { + fmt.Printf("[%s][%s] This can take a really long time if the number of vertices/edges is in the thousands.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) + } + header, body, _ := m.generateInboundPrivEscTableData() + + var fileName string + if m.CapeAdminOnly { + fileName = "inbound-privesc-paths-admin-targets-only" + } else { + fileName = "inbound-privesc-paths-all-targets" + } + o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ Header: header, Body: body, //TableCols: tableCols, - Name: "inbound-privesc-paths", + Name: fileName, SkipPrintToScreen: false, }) @@ -187,6 +201,7 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s // if we have a path, then lets document this source as having a path to our destination if path != nil { if s != d { + fmt.Printf("[%s][%s] Found a path from %s to %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), s, d) paths = "" // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen // and then lets print the full path to the screen diff --git a/aws/codebuild.go b/aws/codebuild.go index 7b373eb..458bd7a 100644 --- a/aws/codebuild.go +++ b/aws/codebuild.go @@ -19,10 +19,11 @@ type CodeBuildModule struct { CodeBuildClient sdk.CodeBuildClientInterface IAMClient sdk.AWSIAMClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - AWSOutputType string - AWSTableCols string + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + PmapperDataBasePath string Goroutines int AWSProfile string @@ -64,7 +65,7 @@ func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosi } fmt.Printf("[%s][%s] Enumerating CodeBuild projects for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) - m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) wg := new(sync.WaitGroup) diff --git a/aws/ecs-tasks.go b/aws/ecs-tasks.go index 366bc1f..49c3fa3 100644 --- a/aws/ecs-tasks.go +++ b/aws/ecs-tasks.go @@ -25,10 +25,11 @@ type ECSTasksModule struct { EC2Client sdk.AWSEC2ClientInterface IAMClient sdk.AWSIAMClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - AWSOutputType string - AWSTableCols string + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + PmapperDataBasePath string AWSProfile string Goroutines int @@ -73,7 +74,7 @@ func (m *ECSTasksModule) ECSTasks(outputDirectory string, verbosity int) { fmt.Printf("[%s][%s] Enumerating ECS tasks in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) // Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { diff --git a/aws/eks.go b/aws/eks.go index 6b50460..b4fe963 100644 --- a/aws/eks.go +++ b/aws/eks.go @@ -22,10 +22,11 @@ type EKSModule struct { EKSClient sdk.EKSClientInterface IAMClient sdk.AWSIAMClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - AWSOutputType string - AWSTableCols string + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + PmapperDataBasePath string Goroutines int AWSProfile string @@ -72,7 +73,7 @@ func (m *EKSModule) EKS(outputDirectory string, verbosity int) { fmt.Printf("[%s][%s] Enumerating EKS clusters for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) // Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { diff --git a/aws/graph.go b/aws/graph.go index 866e79a..0cb25bf 100644 --- a/aws/graph.go +++ b/aws/graph.go @@ -20,18 +20,19 @@ import ( type GraphCommand struct { // General configuration data - Caller sts.GetCallerIdentityOutput - AWSRegions []string - Goroutines int - AWSProfile string - WrapTable bool - AWSOutputType string - AWSTableCols string - Verbosity int - AWSOutputDirectory string - AWSConfig aws.Config - Version string - SkipAdminCheck bool + Caller sts.GetCallerIdentityOutput + AWSRegions []string + Goroutines int + AWSProfile string + WrapTable bool + AWSOutputType string + AWSTableCols string + Verbosity int + AWSOutputDirectory string + AWSConfig aws.Config + Version string + SkipAdminCheck bool + PmapperDataBasePath string pmapperMod PmapperModule pmapperError error @@ -63,7 +64,7 @@ func (m *GraphCommand) RunGraphCommand() { m.modLog.Info("Collecting data for graph ingestor...") - m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath) //////////////// // Accounts diff --git a/aws/instances.go b/aws/instances.go index 1ea073c..52b54fe 100644 --- a/aws/instances.go +++ b/aws/instances.go @@ -24,12 +24,13 @@ import ( type InstancesModule struct { // General configuration data - EC2Client sdk.AWSEC2ClientInterface - IAMClient sdk.AWSIAMClientInterface - Caller sts.GetCallerIdentityOutput - AWSRegions []string - AWSOutputType string - AWSTableCols string + EC2Client sdk.AWSEC2ClientInterface + IAMClient sdk.AWSIAMClientInterface + Caller sts.GetCallerIdentityOutput + AWSRegions []string + AWSOutputType string + AWSTableCols string + PmapperDataBasePath string Goroutines int UserDataAttributesOnly bool @@ -94,7 +95,7 @@ func (m *InstancesModule) Instances(filter string, outputDirectory string, verbo // Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { diff --git a/aws/lambda.go b/aws/lambda.go index b6f5aa5..366a548 100644 --- a/aws/lambda.go +++ b/aws/lambda.go @@ -31,13 +31,15 @@ type LambdasModule struct { AWSOutputType string AWSTableCols string - Goroutines int - AWSProfile string - SkipAdminCheck bool - WrapTable bool - pmapperMod PmapperModule - pmapperError error - iamSimClient IamSimulatorModule + Goroutines int + AWSProfile string + SkipAdminCheck bool + WrapTable bool + pmapperMod PmapperModule + pmapperError error + PmapperDataBasePath string + + iamSimClient IamSimulatorModule // Main module data Lambdas []Lambda @@ -75,7 +77,7 @@ func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) { fmt.Printf("[%s][%s] Enumerating lambdas for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) //fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { diff --git a/aws/pmapper.go b/aws/pmapper.go index e4e6432..495b54a 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -506,7 +506,13 @@ func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string func (m *PmapperModule) readPmapperData(accountID *string) error { - e, n := generatePmapperDataBasePaths(accountID) + var e, n string + if m.PmapperDataBasePath == "" { + e, n = generatePmapperDataBasePaths(accountID) + } else { + e = filepath.Join(m.PmapperDataBasePath, aws.ToString(accountID), "graph", "edges.json") + n = filepath.Join(m.PmapperDataBasePath, aws.ToString(accountID), "graph", "nodes.json") + } nodesFile, err := os.Open(n) if err != nil { diff --git a/aws/role-trusts.go b/aws/role-trusts.go index 5148f31..e3f2ea6 100644 --- a/aws/role-trusts.go +++ b/aws/role-trusts.go @@ -31,8 +31,10 @@ type RoleTrustsModule struct { AWSOutputType string AWSTableCols string - pmapperMod PmapperModule - pmapperError error + pmapperMod PmapperModule + pmapperError error + PmapperDataBasePath string + iamSimClient IamSimulatorModule // Main module data @@ -83,7 +85,7 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int fmt.Printf("[%s][%s] Enumerating role trusts for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) //fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) // if m.pmapperError != nil { // fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) @@ -557,6 +559,10 @@ func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string } else { subjects = append(subjects, "ALL PROJECTS") } + case strings.Contains(statement.Principal.Federated[0], "saml-provider"): + // the provider name is the last part of the ARN + provider = strings.Split(statement.Principal.Federated[0], ":saml-provider/")[1] + subjects = append(subjects, "Not applicable") default: provider = "Unknown Federated Provider" diff --git a/aws/sdk/codebuild.go b/aws/sdk/codebuild.go index 8395155..f27383d 100644 --- a/aws/sdk/codebuild.go +++ b/aws/sdk/codebuild.go @@ -20,6 +20,7 @@ type CodeBuildClientInterface interface { func init() { gob.Register(codeBuildTypes.Project{}) + gob.Register([]codeBuildTypes.Project{}) } diff --git a/aws/shared.go b/aws/shared.go index d2b207a..20fffab 100644 --- a/aws/shared.go +++ b/aws/shared.go @@ -77,11 +77,12 @@ func isRoleAdmin(iamSimMod IamSimulatorModule, principal *string) bool { } -func InitPmapperGraph(Caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) (PmapperModule, error) { +func InitPmapperGraph(Caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int, PmapperDataBasePath string) (PmapperModule, error) { pmapperMod := PmapperModule{ - Caller: Caller, - AWSProfile: AWSProfile, - Goroutines: Goroutines, + Caller: Caller, + AWSProfile: AWSProfile, + Goroutines: Goroutines, + PmapperDataBasePath: PmapperDataBasePath, } err := pmapperMod.initPmapperGraph() if err != nil { diff --git a/aws/workloads.go b/aws/workloads.go index ae7c977..ce3edaf 100644 --- a/aws/workloads.go +++ b/aws/workloads.go @@ -40,8 +40,10 @@ type WorkloadsModule struct { //LightsailClient sdk.MockedLightsailClient //SagemakerClient *sagemaker.Client - pmapperMod PmapperModule - pmapperError error + pmapperMod PmapperModule + pmapperError error + PmapperDataBasePath string + iamSimClient IamSimulatorModule InstanceProfileToRolesMap map[string][]iamTypes.Role @@ -82,7 +84,7 @@ func (m *WorkloadsModule) PrintWorkloads(outputDirectory string, verbosity int) fmt.Printf("[%s][%s] Enumerating compute workloads in all regions for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) fmt.Printf("[%s][%s] Supported Services: App Runner, EC2, ECS, Lambda \n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) - m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines) + m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath) m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines) wg := new(sync.WaitGroup) diff --git a/cli/aws.go b/cli/aws.go index d043748..be15476 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -2,12 +2,10 @@ package cli import ( "encoding/gob" - "encoding/json" "fmt" "log" "os" "path/filepath" - "time" "github.com/BishopFox/cloudfox/aws" "github.com/BishopFox/cloudfox/aws/sdk" @@ -144,10 +142,7 @@ var ( CapeCommand = &cobra.Command{ Use: "cape", Aliases: []string{"capeParse"}, - Short: "Cross-Account Privilege Escalation Route finder.\n" + - "Needs to be run with multiple profiles using -l or -a flag\n" + - "Needs pmapper data to be present", - + Short: "Cross-Account Privilege Escalation Route finder. Needs to be run with multiple profiles using -l or -a flag. Needs pmapper data to be present", Long: "\nUse case examples:\n" + os.Args[0] + " aws cape -l file_with_profile_names.txt", PreRun: awsPreRun, @@ -746,15 +741,16 @@ func runCodeBuildCommand(cmd *cobra.Command, args []string) { continue } m := aws.CodeBuildModule{ - CodeBuildClient: codebuild.NewFromConfig(AWSConfig), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - AWSProfile: profile, - Goroutines: Goroutines, - SkipAdminCheck: AWSSkipAdminCheck, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + CodeBuildClient: codebuild.NewFromConfig(AWSConfig), + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + AWSProfile: profile, + Goroutines: Goroutines, + SkipAdminCheck: AWSSkipAdminCheck, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } m.PrintCodeBuildProjects(AWSOutputDirectory, Verbosity) } @@ -851,14 +847,15 @@ func runEKSCommand(cmd *cobra.Command, args []string) { IAMClient: iam.NewFromConfig(AWSConfig), EKSClient: eks.NewFromConfig(AWSConfig), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - AWSProfile: profile, - Goroutines: Goroutines, - SkipAdminCheck: AWSSkipAdminCheck, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + AWSProfile: profile, + Goroutines: Goroutines, + SkipAdminCheck: AWSSkipAdminCheck, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } m.EKS(AWSOutputDirectory, Verbosity) } @@ -976,18 +973,19 @@ func runGraphCommand(cmd *cobra.Command, args []string) { } graphCommandClient := aws.GraphCommand{ - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, - AWSOutputDirectory: AWSOutputDirectory, - Verbosity: Verbosity, - AWSConfig: AWSConfig, - Version: cmd.Root().Version, - SkipAdminCheck: AWSSkipAdminCheck, + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + AWSOutputDirectory: AWSOutputDirectory, + Verbosity: Verbosity, + AWSConfig: AWSConfig, + Version: cmd.Root().Version, + SkipAdminCheck: AWSSkipAdminCheck, + PmapperDataBasePath: PmapperDataBasePath, } graphCommandClient.RunGraphCommand() } @@ -1022,6 +1020,33 @@ func runCapeCommand(cmd *cobra.Command, args []string) { } + pmapperData := make(map[string]aws.PmapperModule) + + for _, profile := range AWSProfiles { + caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) + if err != nil { + continue + } + fmt.Printf("[%s][%s] Importing Pmapper data for: %s\n", cyan("cape"), cyan(profile), ptr.ToString(caller.Account)) + pmapperMod, pmapperError := aws.InitPmapperGraph(*caller, AWSProfile, Goroutines, PmapperDataBasePath) + if pmapperError != nil { + fmt.Println("Error importing pmapper data " + pmapperError.Error()) + analyzedAccounts[ptr.ToString(caller.Account)] = aws.CapeJobInfo{AnalyzedSuccessfully: false} + // give the user the option to continue or not + // if they choose to continue, we will skip the pmapper data and continue with the rest of the analysis + // if they choose to not continue, we will exit the program + fmt.Printf("Would you like to continue with the analysis without the pmapper data for profile %s? (y/n)", profile) + var continueAnalysis string + fmt.Scanln(&continueAnalysis) + if continueAnalysis == "y" { + continue + } else { + os.Exit(1) + } + } + + pmapperData[profile] = pmapperMod + } for _, profile := range AWSProfiles { var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) @@ -1040,31 +1065,35 @@ func runCapeCommand(cmd *cobra.Command, args []string) { fmt.Println("Error gathering permisisons for " + profile) //analyzedAccounts[ptr.ToString(caller.Account)] = false analyzedAccounts[ptr.ToString(caller.Account)] = aws.CapeJobInfo{AnalyzedSuccessfully: false} - } // Gather all Pmapper data. - fmt.Printf("[%s][%s] Importing Pmapper for: %s\n", cyan("cape"), cyan(profile), ptr.ToString(caller.Account)) + //fmt.Printf("[%s][%s] Importing Pmapper for: %s\n", cyan("cape"), cyan(profile), ptr.ToString(caller.Account)) - pmapperMod, pmapperError := aws.InitPmapperGraph(*caller, AWSProfile, Goroutines) - if pmapperError != nil { - fmt.Println("Error importing pmapper data " + pmapperError.Error()) - //analyzedAccounts[ptr.ToString(caller.Account)] = false - analyzedAccounts[ptr.ToString(caller.Account)] = aws.CapeJobInfo{AnalyzedSuccessfully: false} - } + // pmapperMod, pmapperError := aws.InitPmapperGraph(*caller, AWSProfile, Goroutines) + // if pmapperError != nil { + // fmt.Println("Error importing pmapper data " + pmapperError.Error()) + // //analyzedAccounts[ptr.ToString(caller.Account)] = false + // analyzedAccounts[ptr.ToString(caller.Account)] = aws.CapeJobInfo{AnalyzedSuccessfully: false} + // } + + pmapperMod := pmapperData[profile] // add pmapper nodes to GlobalNodes (which will also soon include iam roles and users) + for _, node := range pmapperMod.Nodes { // add node to GlobalPmapperData //GlobalPmapperData.Nodes = append(GlobalPmapperData.Nodes, node) GlobalNodes = append(GlobalNodes, node) } + fmt.Printf("[%s][%s] Added %d vertices from pmapper for %s\n", cyan("cape"), cyan(profile), len(pmapperMod.Nodes), ptr.ToString(caller.Account)) // same for adding pmapper edges to GlobalPmapperData for _, edge := range pmapperMod.Edges { // add edge to GlobalPmapperData GlobalPmapperData.Edges = append(GlobalPmapperData.Edges, edge) } + fmt.Printf("[%s][%s] Added %d edges from pmapper for %s\n", cyan("cape"), cyan(profile), len(pmapperMod.Edges), ptr.ToString(caller.Account)) //Gather all role data so we can later process all of the role trusts and add external nodes not looked at by pmapper fmt.Printf("[%s][%s] Getting IAM roles for %s\n", cyan("cape"), cyan(profile), ptr.ToString(caller.Account)) @@ -1172,13 +1201,13 @@ func runCapeCommand(cmd *cobra.Command, args []string) { // at least for now, we don't need to make edges for users, groups, or anything else because pmapper already has all of the edges we need fmt.Printf("[%s] Making edges for all profiles\n", cyan("cape")) + fmt.Printf("[%s] Total vertices from pmapper and cape: %d \n", cyan("cape"), len(mergedNodes)) + fmt.Printf("[%s] Total edges from pmapper and cape: %d \n", cyan("cape"), len(GlobalPmapperData.Edges)) + for _, node := range mergedNodes { if node.Type == "Role" { node.MakeRoleEdges(GlobalGraph) } - // if node.Type == "User" { - // node.MakeUserEdges(GlobalGraph) - // } } for _, profile := range AWSProfiles { @@ -1213,28 +1242,28 @@ func runCapeCommand(cmd *cobra.Command, args []string) { // write a json file with job information to the output directory. Use the CapeJobName for hte file name, and have the data include the list of AWSProfiles that were analyzed // this will be used by a TUI to match a job name to the list of accounts that were analyzed - if CapeJobName == "" { - // create random job name in the format of cape-timmefromepoch - CapeJobName = fmt.Sprintf("cape-%s", time.Now().Format("2006-01-02-15-04-05")) - } - filename := fmt.Sprintf("%s.json", CapeJobName) - filepath := filepath.Join(AWSOutputDirectory, "aws", "capeJobs") - err = os.MkdirAll(filepath, 0755) - if err != nil { - fmt.Println("Error creating directory: " + err.Error()) - } - file, _ := os.Create(filepath + "/" + filename) - defer file.Close() - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - err = encoder.Encode(analyzedAccounts) - if err != nil { - fmt.Println("Error writing job data to file: " + err.Error()) - } else { - fmt.Printf("[%s] Job output written to %s\n", cyan("cape"), file.Name()) - fmt.Printf("[%s] %s\n\n", cyan("cape"), magenta("The results of the cape command are best viewed in the cape terminal user interface (TUI). Use the command below:")) - fmt.Printf("[%s] \tcloudfox aws -l %s cape tui\n\n", cyan("cape"), AWSProfilesList) - } + // if CapeJobName == "" { + // // create random job name in the format of cape-timmefromepoch + // CapeJobName = fmt.Sprintf("cape-%s", time.Now().Format("2006-01-02-15-04-05")) + // } + // filename := fmt.Sprintf("%s.json", CapeJobName) + // filepath := filepath.Join(AWSOutputDirectory, "aws", "capeJobs") + // err = os.MkdirAll(filepath, 0755) + // if err != nil { + // fmt.Println("Error creating directory: " + err.Error()) + // } + // file, _ := os.Create(filepath + "/" + filename) + // defer file.Close() + // encoder := json.NewEncoder(file) + // encoder.SetIndent("", " ") + // err = encoder.Encode(analyzedAccounts) + // if err != nil { + // fmt.Println("Error writing job data to file: " + err.Error()) + // } else { + // fmt.Printf("[%s] Job output written to %s\n", cyan("cape"), file.Name()) + // fmt.Printf("[%s] %s\n\n", cyan("cape"), magenta("The results of the cape command are best viewed in the cape terminal user interface (TUI). Use the command below:")) + // fmt.Printf("[%s] \tcloudfox aws -l %s cape tui\n\n", cyan("cape"), AWSProfilesList) + // } // playing around with creating a graphviz file for image rendering. // the goal here is to be able to export this graph data to a format that can be easily imported in neo4j. @@ -1246,17 +1275,36 @@ func runCapeCommand(cmd *cobra.Command, args []string) { // "ranksep", "3", // )) } + + fmt.Printf("[%s] %s\n\n", cyan("cape"), magenta("The results of the cape command are best viewed in the cape terminal user interface (TUI). Use the command below:")) + if CapeAdminOnly { + fmt.Printf("\t\tcloudfox aws -l %s cape tui --admin-only\n\n", AWSProfilesList) + } else { + fmt.Printf("\t\tcloudfox aws -l %s cape tui\n\n", AWSProfilesList) + } } func runCapeTUICommand(cmd *cobra.Command, args []string) { var capeOutputFileLocations []string - for _, profile := range AWSProfiles { + for i, profile := range AWSProfiles { cloudfoxRunData, err := internal.InitializeCloudFoxRunData(profile, cmd.Root().Version, AWSMFAToken, AWSOutputDirectory) //caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) if err != nil { continue } - capeOutputFileLocations = append(capeOutputFileLocations, filepath.Join(cloudfoxRunData.OutputLocation, "json", "inbound-privesc-paths.json")) + var fileName string + if CapeAdminOnly { + fileName = "inbound-privesc-paths-admin-targets-only.json" + } else { + fileName = "inbound-privesc-paths-all-targets.json" + } + capeOutputFileLocations = append(capeOutputFileLocations, filepath.Join(cloudfoxRunData.OutputLocation, "json", fileName)) + //check to see if file exists, and if it doesnt, remove the profile from the list of profiles to analyze and print a message to the console + if _, err := os.Stat(filepath.Join(cloudfoxRunData.OutputLocation, "json", fileName)); os.IsNotExist(err) { + fmt.Printf("[%s] Could not retrieve CAPE data for profile %s.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version)), profile) + //remove the profile from the list of profiles to analyze + AWSProfiles = append(AWSProfiles[:i], AWSProfiles[i+1:]...) + } } if len(capeOutputFileLocations) == 0 { @@ -1306,6 +1354,7 @@ func runInstancesCommand(cmd *cobra.Command, args []string) { WrapTable: AWSWrapTable, AWSOutputType: AWSOutputType, AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } m.Instances(InstancesFilter, AWSOutputDirectory, Verbosity) } @@ -1379,16 +1428,17 @@ func runLambdasCommand(cmd *cobra.Command, args []string) { continue } m := aws.LambdasModule{ - LambdaClient: lambda.NewFromConfig(AWSConfig), - IAMClient: iam.NewFromConfig(AWSConfig), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - AWSProfile: profile, - Goroutines: Goroutines, - SkipAdminCheck: AWSSkipAdminCheck, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + LambdaClient: lambda.NewFromConfig(AWSConfig), + IAMClient: iam.NewFromConfig(AWSConfig), + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + AWSProfile: profile, + Goroutines: Goroutines, + SkipAdminCheck: AWSSkipAdminCheck, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } m.PrintLambdas(AWSOutputDirectory, Verbosity) } @@ -1546,14 +1596,15 @@ func runRoleTrustCommand(cmd *cobra.Command, args []string) { continue } m := aws.RoleTrustsModule{ - IAMClient: iam.NewFromConfig(AWSConfig), - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - SkipAdminCheck: AWSSkipAdminCheck, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + IAMClient: iam.NewFromConfig(AWSConfig), + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + SkipAdminCheck: AWSSkipAdminCheck, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } m.PrintRoleTrusts(AWSOutputDirectory, Verbosity) } @@ -1634,19 +1685,20 @@ func runWorkloadsCommand(cmd *cobra.Command, args []string) { continue } m := aws.WorkloadsModule{ - ECSClient: ecs.NewFromConfig(AWSConfig), - EC2Client: ec2.NewFromConfig(AWSConfig), - LambdaClient: lambda.NewFromConfig(AWSConfig), - AppRunnerClient: apprunner.NewFromConfig(AWSConfig), - IAMClient: iam.NewFromConfig(AWSConfig), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - SkipAdminCheck: AWSSkipAdminCheck, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + ECSClient: ecs.NewFromConfig(AWSConfig), + EC2Client: ec2.NewFromConfig(AWSConfig), + LambdaClient: lambda.NewFromConfig(AWSConfig), + AppRunnerClient: apprunner.NewFromConfig(AWSConfig), + IAMClient: iam.NewFromConfig(AWSConfig), + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + SkipAdminCheck: AWSSkipAdminCheck, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } m.PrintWorkloads(AWSOutputDirectory, Verbosity) } @@ -1663,14 +1715,15 @@ func runECSTasksCommand(cmd *cobra.Command, args []string) { ECSClient: ecs.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)), IAMClient: iam.NewFromConfig(internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken)), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - AWSProfile: profile, - Goroutines: Goroutines, - SkipAdminCheck: AWSSkipAdminCheck, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + AWSProfile: profile, + Goroutines: Goroutines, + SkipAdminCheck: AWSSkipAdminCheck, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } m.ECSTasks(AWSOutputDirectory, Verbosity) } @@ -1867,6 +1920,7 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { WrapTable: AWSWrapTable, AWSOutputType: AWSOutputType, AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } instances.Instances(InstancesFilter, AWSOutputDirectory, Verbosity) route53 := aws.Route53Module{ @@ -1879,16 +1933,17 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { } lambdasMod := aws.LambdasModule{ - LambdaClient: lambdaClient, - IAMClient: iamClient, - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - AWSProfile: profile, - Goroutines: Goroutines, - SkipAdminCheck: AWSSkipAdminCheck, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + LambdaClient: lambdaClient, + IAMClient: iamClient, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + AWSProfile: profile, + Goroutines: Goroutines, + SkipAdminCheck: AWSSkipAdminCheck, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } lambdasMod.PrintLambdas(AWSOutputDirectory, Verbosity) @@ -1969,14 +2024,15 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { ECSClient: ecsClient, IAMClient: iamClient, - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - AWSProfile: profile, - Goroutines: Goroutines, - SkipAdminCheck: AWSSkipAdminCheck, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + AWSProfile: profile, + Goroutines: Goroutines, + SkipAdminCheck: AWSSkipAdminCheck, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } ecstasks.ECSTasks(AWSOutputDirectory, Verbosity) @@ -1984,14 +2040,15 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { EKSClient: eksClient, IAMClient: iamClient, - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - AWSProfile: profile, - Goroutines: Goroutines, - SkipAdminCheck: AWSSkipAdminCheck, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + AWSProfile: profile, + Goroutines: Goroutines, + SkipAdminCheck: AWSSkipAdminCheck, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } eksCommand.EKS(AWSOutputDirectory, Verbosity) @@ -2163,14 +2220,15 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { resourceTrustsCommand.PrintResources(AWSOutputDirectory, Verbosity) codeBuildCommand := aws.CodeBuildModule{ - CodeBuildClient: codeBuildClient, - Caller: *caller, - AWSProfile: profile, - Goroutines: Goroutines, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + CodeBuildClient: codeBuildClient, + Caller: *caller, + AWSProfile: profile, + Goroutines: Goroutines, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } codeBuildCommand.PrintCodeBuildProjects(AWSOutputDirectory, Verbosity) @@ -2239,19 +2297,20 @@ func runAllChecksCommand(cmd *cobra.Command, args []string) { iamSimulator.PrintIamSimulator(SimulatorPrincipal, SimulatorAction, SimulatorResource, AWSOutputDirectory, Verbosity) workloads := aws.WorkloadsModule{ - ECSClient: ecsClient, - EC2Client: ec2Client, - LambdaClient: lambdaClient, - AppRunnerClient: appRunnerClient, - IAMClient: iamClient, - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - SkipAdminCheck: AWSSkipAdminCheck, - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + ECSClient: ecsClient, + EC2Client: ec2Client, + LambdaClient: lambdaClient, + AppRunnerClient: appRunnerClient, + IAMClient: iamClient, + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + SkipAdminCheck: AWSSkipAdminCheck, + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, + PmapperDataBasePath: PmapperDataBasePath, } workloads.PrintWorkloads(AWSOutputDirectory, Verbosity) @@ -2312,11 +2371,10 @@ func init() { // cape command flags CapeCommand.Flags().BoolVar(&CapeAdminOnly, "admin-only", false, "Only return paths that lead to an admin role - much faster") - CapeCommand.Flags().StringVar(&CapeJobName, "job-name", "", "Name of the cape job") + //CapeCommand.Flags().StringVar(&CapeJobName, "job-name", "", "Name of the cape job") - // pmapper flag for pmapper and cape commands - //PmapperCommand.Flags().StringVarP(&PmapperDataBasePath, "pmapper-data-basepath", "pdata", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)") - //CapeCommand.Flags().StringVarP(&PmapperDataBasePath, "pmapper-data-basepath", "pdata", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)") + // cape tui command flags + CapeTuiCmd.Flags().BoolVar(&CapeAdminOnly, "admin-only", false, "Only return paths that lead to an admin role - much faster") // Global flags for the AWS modules AWSCommands.PersistentFlags().StringVarP(&AWSProfile, "profile", "p", "", "AWS CLI Profile Name") @@ -2332,7 +2390,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().StringVar(&PmapperDataBasePath, "pmapper-data-basepath", "", "Supply the base path for the pmapper data files (useful if you have copied them from another machine)") + 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( AccessKeysCommand, From 82635fd44f91a91462423f9bf3d1aa1729b4c2b9 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Tue, 16 Apr 2024 10:28:15 -0400 Subject: [PATCH 23/29] made aws sso like eks, where edges are not created if it's if the provider is in the same account as the role that trusts it. the edges will still show up cross account though. --- aws/cape-tui.go | 14 ++++++++++++++ aws/cape.go | 39 +++++++++++++++++++++++++++++++++++---- aws/role-trusts.go | 6 ++++-- cli/aws.go | 8 ++++++-- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/aws/cape-tui.go b/aws/cape-tui.go index c2422b9..7b87aa8 100644 --- a/aws/cape-tui.go +++ b/aws/cape-tui.go @@ -314,6 +314,20 @@ func getRecordsForAccount(preloadedData *PerAccountData) (table.Model, map[int]s // lets load the records for the first file in the list records := preloadedData.PrivescPaths + if len(records) < 1 { + mainTable := table.New( + table.WithColumns([]table.Column{ + {Title: "Account", Width: 30}, + {Title: "Source", Width: 30}, + {Title: "Target", Width: 30}, + {Title: "isTargetAdmin", Width: 30}, + }), + table.WithRows([]table.Row{{"No records found", "", "", ""}}), + table.WithFocused(true), + ) + return mainTable, nil, nil + + } // Prepare rows for the table and data for the right view rows := make([]table.Row, 0, len(records)-1) diff --git a/aws/cape.go b/aws/cape.go index 8c9cccf..896a440 100644 --- a/aws/cape.go +++ b/aws/cape.go @@ -37,6 +37,7 @@ type CapeCommand struct { PmapperDataBasePath string AnalyzedAccounts map[string]CapeJobInfo CapeAdminOnly bool + AccountsNotAnalyzed []string output internal.OutputData2 modLog *logrus.Entry @@ -113,10 +114,13 @@ func (m *CapeCommand) RunCapeCommand() { o.WriteFullOutput(o.Table.TableFiles, nil) fmt.Println("The following accounts are trusted by this account, but were not analyzed as part of this run.") fmt.Println("As a result, we cannot determine which principals in these accounts have permission to assume roles in this account.") - for account := range m.AnalyzedAccounts { - if m.AnalyzedAccounts[account].AnalyzedSuccessfully == false { - fmt.Println("\t\t" + account) - } + // for account := range m.AnalyzedAccounts { + // if m.AnalyzedAccounts[account].AnalyzedSuccessfully == false { + // fmt.Println("\t\t" + account) + // } + // } + for _, account := range m.AccountsNotAnalyzed { + fmt.Println("\t\t" + account) } } @@ -202,6 +206,16 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s if path != nil { if s != d { fmt.Printf("[%s][%s] Found a path from %s to %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), s, d) + + // check to see if the source account was analyzed. If not, lets add it to the list of accounts that were not analyzed + if strings.Contains(s, "Not analyzed/in-scope") { + // add it to the m.AccountsNotAnalyzed if it doesn't already exist + if !internal.Contains(s, m.AccountsNotAnalyzed) { + m.AccountsNotAnalyzed = append(m.AccountsNotAnalyzed, s) + } + + } + paths = "" // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen // and then lets print the full path to the screen @@ -699,6 +713,15 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } } } + if strings.EqualFold(PermissionsRow.Effect, "Deny") { + // if the action is deny, we need to remove any edges between PermissionsRow.Arn and a.Arn + // if the edge exists, remove it + err := GlobalGraph.RemoveEdge(PermissionsRow.Arn, a.Arn) + if err != nil { + fmt.Println(err) + } + + } } } } @@ -883,6 +906,14 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } } } + if strings.EqualFold(PermissionsRow.Effect, "Deny") { + // if the action is deny, we need to remove any edges between PermissionsRow.Arn and a.Arn + // if the edge exists, remove it + err := GlobalGraph.RemoveEdge(PermissionsRow.Arn, a.Arn) + if err != nil { + fmt.Println(err) + } + } } } diff --git a/aws/role-trusts.go b/aws/role-trusts.go index e3f2ea6..85b351d 100644 --- a/aws/role-trusts.go +++ b/aws/role-trusts.go @@ -502,9 +502,11 @@ func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string subjects = append(subjects, "ALL ISSUERS") } - // AWS SSO case + ///AWS SSO case case strings.Contains(statement.Principal.Federated[0], "AWSSSO"): - provider = "AWS SSO" + //provider = "AWS SSO" + accountId := strings.Split(statement.Principal.Federated[0], ":")[4] + provider = fmt.Sprintf("AWSSSO-%s", accountId) subjects = append(subjects, "Not applicable") // okta case diff --git a/cli/aws.go b/cli/aws.go index be15476..aab6c00 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -1201,8 +1201,7 @@ func runCapeCommand(cmd *cobra.Command, args []string) { // at least for now, we don't need to make edges for users, groups, or anything else because pmapper already has all of the edges we need fmt.Printf("[%s] Making edges for all profiles\n", cyan("cape")) - fmt.Printf("[%s] Total vertices from pmapper and cape: %d \n", cyan("cape"), len(mergedNodes)) - fmt.Printf("[%s] Total edges from pmapper and cape: %d \n", cyan("cape"), len(GlobalPmapperData.Edges)) + //fmt.Printf("[%s] Total vertices from pmapper and cape: %d \n", cyan("cape"), len(mergedNodes)) for _, node := range mergedNodes { if node.Type == "Role" { @@ -1210,6 +1209,11 @@ func runCapeCommand(cmd *cobra.Command, args []string) { } } + // count the edges in the graph + edges, _ := GlobalGraph.Edges() + fmt.Printf("[%s] Total edges from pmapper and cape: %d \n", cyan("cape"), len(mergedNodes)) + fmt.Printf("[%s] Total edges from pmapper and cape: %d \n", cyan("cape"), len(edges)) + for _, profile := range AWSProfiles { var AWSConfig = internal.AWSConfigFileLoader(profile, cmd.Root().Version, AWSMFAToken) caller, err := internal.AWSWhoami(profile, cmd.Root().Version, AWSMFAToken) From 77cd08b8d595a7f3d6e216f16b24db085bff715b Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:24:18 -0400 Subject: [PATCH 24/29] updated gcp verbosity, updated cape command usage, switched version tracking file from main.go to internal/utils.go --- cli/aws.go | 27 ++++++++++++++------------- cli/gcp.go | 2 +- globals/utils.go | 2 +- main.go | 3 ++- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/cli/aws.go b/cli/aws.go index be6f43a..6e92447 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -24,8 +24,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/codecommit" "github.com/aws/aws-sdk-go-v2/service/codedeploy" "github.com/aws/aws-sdk-go-v2/service/datapipeline" - "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/directoryservice" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ecr" "github.com/aws/aws-sdk-go-v2/service/ecs" @@ -142,10 +142,11 @@ var ( CapeJobName string CapeCommand = &cobra.Command{ Use: "cape", - Aliases: []string{"capeParse"}, + Aliases: []string{"CAPE"}, Short: "Cross-Account Privilege Escalation Route finder. Needs to be run with multiple profiles using -l or -a flag. Needs pmapper data to be present", Long: "\nUse case examples:\n" + - os.Args[0] + " aws cape -l file_with_profile_names.txt", + os.Args[0] + " aws cape -l file_with_profile_names.txt --admin-only" + + os.Args[0] + " aws cape -l file_with_profile_names.txt # This default mode shows all inbound paths but is very slow when there are many accounts)", PreRun: awsPreRun, Run: runCapeCommand, PostRun: awsPostRun, @@ -476,9 +477,9 @@ var ( GraphCommand = &cobra.Command{ Use: "graph", - Short: "Graph the relationships between resources", + Short: "INACTIVE (Use cape command instead) Graph the relationships between resources and insert into local Neo4j db", Long: "\nUse case examples:\n" + - os.Args[0] + " aws graph --profile readonly_profile", + os.Args[0] + " aws graph -l /path/to/profiles", PreRun: awsPreRun, Run: runGraphCommand, PostRun: awsPostRun, @@ -1726,14 +1727,14 @@ func runDirectoryServicesCommand(cmd *cobra.Command, args []string) { continue } m := aws.DirectoryModule{ - DSClient: directoryservice.NewFromConfig(AWSConfig), - Caller: *caller, - AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), - AWSProfile: profile, - Goroutines: Goroutines, - WrapTable: AWSWrapTable, - AWSOutputType: AWSOutputType, - AWSTableCols: AWSTableCols, + DSClient: directoryservice.NewFromConfig(AWSConfig), + Caller: *caller, + AWSRegions: internal.GetEnabledRegions(profile, cmd.Root().Version, AWSMFAToken), + AWSProfile: profile, + Goroutines: Goroutines, + WrapTable: AWSWrapTable, + AWSOutputType: AWSOutputType, + AWSTableCols: AWSTableCols, } m.PrintDirectories(AWSOutputDirectory, Verbosity) } diff --git a/cli/gcp.go b/cli/gcp.go index e8b4ec4..e69efb6 100644 --- a/cli/gcp.go +++ b/cli/gcp.go @@ -89,7 +89,7 @@ func init() { // GCPCommands.PersistentFlags().BoolVarP(&GCPAllProjects, "all-projects", "a", false, "Use all project IDs available to activated gloud account or given gcloud account") // GCPCommands.PersistentFlags().BoolVarP(&GCPConfirm, "yes", "y", false, "Non-interactive mode (like apt/yum)") // GCPCommands.PersistentFlags().StringVarP(&GCPOutputFormat, "output", "", "brief", "[\"brief\" | \"wide\" ]") - GCPCommands.PersistentFlags().IntVarP(&Verbosity, "verbosity", "v", 1, "1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n") + GCPCommands.PersistentFlags().IntVarP(&Verbosity, "verbosity", "v", 2, "1 = Print control messages only\n2 = Print control messages, module output\n3 = Print control messages, module output, and loot file output\n") // defaultOutputDir is defined in cli.aws GCPCommands.PersistentFlags().StringVar(&GCPOutputDirectory, "outdir", defaultOutputDir, "Output Directory ") // GCPCommands.PersistentFlags().IntVarP(&Goroutines, "max-goroutines", "g", 30, "Maximum number of concurrent goroutines") diff --git a/globals/utils.go b/globals/utils.go index ca07b6e..1fba2fe 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 = "1.13.5" +const CLOUDFOX_VERSION = "1.14.0" diff --git a/main.go b/main.go index 6b741af..4683f00 100644 --- a/main.go +++ b/main.go @@ -4,13 +4,14 @@ import ( "os" "github.com/BishopFox/cloudfox/cli" + "github.com/BishopFox/cloudfox/globals" "github.com/spf13/cobra" ) var ( rootCmd = &cobra.Command{ Use: os.Args[0], - Version: "1.14.0", + Version: globals.CLOUDFOX_VERSION, } ) From abcc930f207683f850d07d18694f1e4367a9b726 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:29:53 -0400 Subject: [PATCH 25/29] added afero fs back to output2 (needed to pass brew tests) --- internal/output2.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/output2.go b/internal/output2.go index 9fdc432..355dfe5 100644 --- a/internal/output2.go +++ b/internal/output2.go @@ -218,8 +218,8 @@ func (l *LootClient) writeLootFiles() []string { for _, file := range l.LootFiles { contents := []byte(file.Contents) fullPath := path.Join(l.DirectoryName, "loot", file.Name) - - err := os.WriteFile(fullPath, contents, 0644) + err := afero.WriteFile(fileSystem, fullPath, contents, 0644) // Use Afero's WriteFile + //err := os.WriteFile(fullPath, contents, 0644) if err != nil { log.Fatalf("error writing loot file %s: %s", file.Name, err) } From c3be95b3db9ac5e76a4f9e3ea922183eaa5a7087 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:47:31 -0400 Subject: [PATCH 26/29] cleaned up enhanced pmapper loot file --- aws/pmapper.go | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/aws/pmapper.go b/aws/pmapper.go index 495b54a..6cf9ca1 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -341,9 +341,10 @@ func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int) header, body := m.createPmapperTableData(outputDirectory) o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ - Header: header, - Body: body, - Name: "pmapper-privesc-paths-enhanced", + Header: header, + Body: body, + Name: "pmapper-privesc-paths-enhanced", + SkipPrintToScreen: true, }) loot := m.writeLoot(o.Table.DirectoryName, verbosity) @@ -448,7 +449,7 @@ func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string m.CommandCounter.Error++ panic(err.Error()) } - f := filepath.Join(path, "pmapper-privesc-paths-enhanced.txt") + lootFilePath := filepath.Join(path, "pmapper.txt") var admins, out string @@ -499,7 +500,7 @@ func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string fmt.Print(out) fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file")) } - fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f) + fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), magenta(fmt.Sprintf("Loot file with ALL potential paths written to: [%s]", lootFilePath))) return out } @@ -631,25 +632,3 @@ func sanitizeArnForNeo4jLabel(arn string) string { // Add more replacements if needed return sanitized } - -// func GetRelationshipsForRole(roleArn string) []schema.Relationship { -// var relationships []schema.Relationship -// if strings.Contains(node.Arn, "role") { -// ptype = "Role" -// } else if strings.Contains(node.Arn, "user") { -// ptype = "User" -// node.TrustPolicy = "" -// } else if strings.Contains(node.Arn, "group") { -// ptype = "Group" -// } -// for _, edge := range m.Edges { -// if edge.Source == roleArn { -// relationships = append(relationships, schema.Relationship{ -// Source: roleArn, -// SourceProperty: "arn", -// Target: edge.Destination, -// TargetProperty: "arn", -// Type: "CAN_ACCESS", -// }) -// } -// } From 22417f174bd97b2fdb715fd1982ce36ed68bec13 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:54:10 -0400 Subject: [PATCH 27/29] spelling --- aws/cape.go | 18 +++++++++--------- aws/graph.go | 6 +++--- aws/graph/ingester/schema/models/roles.go | 4 ++-- aws/pmapper.go | 6 +++--- cli/aws.go | 8 ++++---- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/aws/cape.go b/aws/cape.go index 896a440..5e9e502 100644 --- a/aws/cape.go +++ b/aws/cape.go @@ -217,7 +217,7 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s } paths = "" - // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen + // if we got here there's a path. Lets print the reason and the short reason for each edge in the path to the screen // and then lets print the full path to the screen for i := 0; i < len(path)-1; i++ { thisEdge, _ := m.GlobalGraph.Edge(path[i], path[i+1]) @@ -622,7 +622,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { //fmt.Println(err) //fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Same account explicit trust") if err == graph.ErrEdgeAlreadyExists { - // update the ege by copying the existing graph.Edge with attributes and add the new attributes + // update the edge by copying the existing graph.Edge with attributes and add the new attributes //fmt.Println("Edge already exists") // get the existing edge @@ -685,7 +685,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { // fmt.Println(err) // fmt.Println(PermissionsRow.Arn + a.Arn + "Same account root trust and trusted principal has permission to assume role") if err == graph.ErrEdgeAlreadyExists { - // update the ege by copying the existing graph.Edge with attributes and add the new attributes + // update the edge by copying the existing graph.Edge with attributes and add the new attributes // get the existing edge existingEdge, _ := GlobalGraph.Edge(PermissionsRow.Arn, a.Arn) @@ -741,7 +741,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { // fmt.Println(err) // fmt.Println(TrustedPrincipal.VendorName + a.Arn + "Cross account root trust and trusted principal is a vendor") if err == graph.ErrEdgeAlreadyExists { - // update the ege by copying the existing graph.Edge with attributes and add the new attributes + // update the edge by copying the existing graph.Edge with attributes and add the new attributes // get the existing edge existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.VendorName, a.Arn) @@ -770,7 +770,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { } else if strings.Contains(TrustedPrincipal.TrustedPrincipal, fmt.Sprintf("%s:root", trustedPrincipalAccount)) && !TrustedPrincipal.AccountIsInAnalyzedAccountList { // first lets check to see if the trustedRootAccountID is in the map of analzyeddAccounts - // if it is not, we can't interate over the permissions, so we will just have to create an edge :root princpal and this role + // if it is not, we can't iterate over the permissions, so we will just have to create an edge :root princpal and this role err := GlobalGraph.AddEdge( //TrustedPrincipal.TrustedPrincipal, @@ -783,7 +783,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { // fmt.Println(err) // fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Cross account root trust and trusted principal is not in the analyzed account list") if err == graph.ErrEdgeAlreadyExists { - // update the ege by copying the existing graph.Edge with attributes and add the new attributes + // update the edge by copying the existing graph.Edge with attributes and add the new attributes // get the existing edge existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.TrustedPrincipal, a.Arn) @@ -843,7 +843,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { //fmt.Println(err) //fmt.Println(PermissionsRow.Arn + a.Arn + "Cross account root trust and trusted principal has permission to assume role") if err == graph.ErrEdgeAlreadyExists { - // update the ege by copying the existing graph.Edge with attributes and add the new attributes + // update the edge by copying the existing graph.Edge with attributes and add the new attributes // get the existing edge existingEdge, _ := GlobalGraph.Edge(PermissionsRow.Arn, a.Arn) @@ -879,7 +879,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { //fmt.Println(err) //fmt.Println(PermissionsRow.Arn + a.Arn + "Cross account root trust and trusted principal has permission to assume role") if err == graph.ErrEdgeAlreadyExists { - // update the ege by copying the existing graph.Edge with attributes and add the new attributes + // update the edge by copying the existing graph.Edge with attributes and add the new attributes // get the existing edge existingEdge, _ := GlobalGraph.Edge(PermissionsRow.Arn, a.Arn) @@ -950,7 +950,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) { //fmt.Println(err) //fmt.Println(TrustedFederatedProvider.TrustedFederatedProvider + a.Arn + "Trusted federated provider") if err == graph.ErrEdgeAlreadyExists { - // update the ege by copying the existing graph.Edge with attributes and add the new attributes + // update the edge by copying the existing graph.Edge with attributes and add the new attributes // get the existing edge existingEdge, _ := GlobalGraph.Edge(TrustedFederatedProvider.TrustedFederatedProvider, a.Arn) diff --git a/aws/graph.go b/aws/graph.go index 0cb25bf..bf5a2b6 100644 --- a/aws/graph.go +++ b/aws/graph.go @@ -73,7 +73,7 @@ func (m *GraphCommand) RunGraphCommand() { accounts := m.collectAccountDataForGraph() // write data to jsonl file for ingestor to read fileName := fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "accounts") - // create file and directory if it doesnt exist + // create file and directory if it doesn't exist if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil { m.modLog.Error(err) return @@ -100,7 +100,7 @@ func (m *GraphCommand) RunGraphCommand() { // users := m.collectUserDataForGraph() // // write data to jsonl file for ingestor to read // fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "users") - // // create file and directory if it doesnt exist + // // create file and directory if it doesn't exist // if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil { // m.modLog.Error(err) // return @@ -127,7 +127,7 @@ func (m *GraphCommand) RunGraphCommand() { roles := m.collectRoleDataForGraph() // write data to jsonl file for ingestor to read fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "roles") - // create file and directory if it doesnt exist + // create file and directory if it doesn't exist if err := os.MkdirAll(fmt.Sprintf("%s/graph/%s", m.output.Directory, aws.ToString(m.Caller.Account)), 0755); err != nil { m.modLog.Error(err) return diff --git a/aws/graph/ingester/schema/models/roles.go b/aws/graph/ingester/schema/models/roles.go index 4ff7a96..57ae324 100644 --- a/aws/graph/ingester/schema/models/roles.go +++ b/aws/graph/ingester/schema/models/roles.go @@ -151,7 +151,7 @@ func (a *Role) MakeRelationships() []schema.Relationship { // if the resource is * or the resource is this role arn, then this principal can assume this role if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) { // make a CAN_ASSUME relationship between the trusted principal and this role - //evalutate if the princiapl is a user or a role and set a variable accordingly + //evaluate if the princiapl is a user or a role and set a variable accordingly //var principalType schema.NodeLabel if strings.EqualFold(PermissionsRow.Type, "User") { relationships = append(relationships, schema.Relationship{ @@ -494,7 +494,7 @@ func (a *Role) MakeEdges(GlobalGraph graph.Graph[string, string]) []schema.Relat // if the resource is * or the resource is this role arn, then this principal can assume this role if PermissionsRow.Resource == "*" || strings.Contains(PermissionsRow.Resource, a.ARN) { // make a CAN_ASSUME relationship between the trusted principal and this role - //evalutate if the princiapl is a user or a role and set a variable accordingly + //evaluate if the princiapl is a user or a role and set a variable accordingly //var principalType schema.NodeLabel if strings.EqualFold(PermissionsRow.Type, "User") { err := GlobalGraph.AddEdge( diff --git a/aws/pmapper.go b/aws/pmapper.go index 6cf9ca1..3368481 100644 --- a/aws/pmapper.go +++ b/aws/pmapper.go @@ -166,7 +166,7 @@ func (m *PmapperModule) createAndPopulateGraph() graph.Graph[string, string] { ) if err != nil { if err == graph.ErrEdgeAlreadyExists { - // update the ege by copying the existing graph.Edge with attributes and add the new attributes + // update the edge by copying the existing graph.Edge with attributes and add the new attributes //fmt.Println("Edge already exists, but adding a new one!") // get the existing edge @@ -410,7 +410,7 @@ func (m *PmapperModule) createPmapperTableData(outputDirectory string) ([]string if len(path) > 0 { if startNode.Arn != destNode.Arn { paths = "" - // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen + // if we got herethere's a path. Lets print the reason and the short reason for each edge in the path to the screen for i := 0; i < len(path)-1; i++ { for _, edge := range m.Edges { if edge.Source == path[i] && edge.Destination == path[i+1] { @@ -467,7 +467,7 @@ func (m *PmapperModule) writeLoot(outputDirectory string, verbosity int) string // if we got here there is a path out += fmt.Sprintf("PATH TO ADMIN FOUND\n Start: %s\n End: %s\n Path(s):\n", startNode.Arn, destNode.Arn) //fmt.Println(path) - // if we got here theres a path. Lets print the reason and the short reason for each edge in the path to the screen + // if we got herethere's a path. Lets print the reason and the short reason for each edge in the path to the screen for i := 0; i < len(path)-1; i++ { for _, edge := range m.Edges { if edge.Source == path[i] && edge.Destination == path[i+1] { diff --git a/cli/aws.go b/cli/aws.go index 6e92447..fde552e 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -1073,7 +1073,7 @@ func runCapeCommand(cmd *cobra.Command, args []string) { if PermissionsCommandClient.Rows != nil { common.PermissionRowsFromAllProfiles = append(common.PermissionRowsFromAllProfiles, PermissionsCommandClient.Rows...) } else { - fmt.Println("Error gathering permisisons for " + profile) + fmt.Println("Error gathering permissions for " + profile) //analyzedAccounts[ptr.ToString(caller.Account)] = false analyzedAccounts[ptr.ToString(caller.Account)] = aws.CapeJobInfo{AnalyzedSuccessfully: false} } @@ -1183,7 +1183,7 @@ func runCapeCommand(cmd *cobra.Command, args []string) { ) if err != nil { if err == graph.ErrEdgeAlreadyExists { - // update the ege by copying the existing graph.Edge with attributes and add the new attributes + // update theedge by copying the existing graph.Edge with attributes and add the new attributes //fmt.Println("Edge already exists") // get the existing edge @@ -1254,7 +1254,7 @@ func runCapeCommand(cmd *cobra.Command, args []string) { capeCommandClient.RunCapeCommand() - // write a json file with job information to the output directory. Use the CapeJobName for hte file name, and have the data include the list of AWSProfiles that were analyzed + // write a json file with job information to the output directory. Use the CapeJobName for the file name, and have the data include the list of AWSProfiles that were analyzed // this will be used by a TUI to match a job name to the list of accounts that were analyzed // if CapeJobName == "" { @@ -1314,7 +1314,7 @@ func runCapeTUICommand(cmd *cobra.Command, args []string) { fileName = "inbound-privesc-paths-all-targets.json" } capeOutputFileLocations = append(capeOutputFileLocations, filepath.Join(cloudfoxRunData.OutputLocation, "json", fileName)) - //check to see if file exists, and if it doesnt, remove the profile from the list of profiles to analyze and print a message to the console + //check to see if file exists, and if it doesn't, remove the profile from the list of profiles to analyze and print a message to the console if _, err := os.Stat(filepath.Join(cloudfoxRunData.OutputLocation, "json", fileName)); os.IsNotExist(err) { fmt.Printf("[%s] Could not retrieve CAPE data for profile %s.\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", cmd.Root().Version)), profile) //remove the profile from the list of profiles to analyze From c0e4301ef3ca76c8dc0388001f6f2eb8b83b7347 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:10:51 -0400 Subject: [PATCH 28/29] Add GCP to readme, fix typo --- README.md | 18 +++++++++++++++++- gcp/commands/bigquery.go | 4 ++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c644cf0..5c8baa6 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF | - | - | | AWS | 34 | | Azure | 4 | -| GCP | Support Planned | +| GCP | 8 | | Kubernetes | Support Planned | @@ -111,6 +111,7 @@ Additional policy notes (as of 09/2022): | AWS | [access-keys](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#access-keys) | Lists active access keys for all users. Useful for cross referencing a key you found with which in-scope account it belongs to. | | AWS | [api-gw](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#api-gw) | Lists API gateway endpoints and gives you custom curl commands including API tokens if they are stored in metadata. | | AWS | [buckets](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#filesystems) | Lists the buckets in the account and gives you handy commands for inspecting them further. | +| AWS | [cape](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#cape) | Enumerates cross-account privilege escalation paths. Requires `pmapper` to be run first | | AWS | [cloudformation](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#cloudformation) | Lists the cloudformation stacks in the account. Generates loot file with stack details, stack parameters, and stack output - look for secrets. | | AWS | [codebuild](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#codebuild) | Enumerate CodeBuild projects | | AWS | [databases](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#databases) | Enumerate RDS databases. Get a loot file with connection strings. | @@ -152,6 +153,21 @@ Additional policy notes (as of 09/2022): | Azure | [storage](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#storage) | The storage command is still under development. Currently it only displays limited data about the storage accounts | | Azure | [vms](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#vms) | Enumerates useful information for Compute instances in all available resource groups and subscriptions | + +# GCP Commands +| Provider | Command Name | Description +| - | - | - | +| GCP | [whoami](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#whoami) | Display the email address of the GCP authenticated user | +| GCP | [all-checks](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#all-checks) | Runs all available GCP commands | +| GCP | [artifact-registry](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#artifact-registry) | Display GCP artifact registry information | +| GCP | [bigquery](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#bigquery) | Display Bigquery datasets and tables information | +| GCP | [buckets](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#buckets) | Display GCP buckets information | +| GCP | [iam](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#iam) | Display GCP IAM information | +| GCP | [instances](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#instances) | Display GCP Compute Engine instances information | +| GCP | [secrets](https://github.com/BishopFox/cloudfox/wiki/GCP-Commands#secrets) | Display GCP secrets information | + + + # Authors * [Carlos Vendramini](https://github.com/carlosvendramini-bf) * [Seth Art (@sethsec](https://twitter.com/sethsec)) diff --git a/gcp/commands/bigquery.go b/gcp/commands/bigquery.go index c324e8b..01b67fd 100644 --- a/gcp/commands/bigquery.go +++ b/gcp/commands/bigquery.go @@ -14,10 +14,10 @@ import ( var GCPBigQueryCommand = &cobra.Command{ Use: "bigquery", Aliases: []string{}, - Short: "Display Bigauery datasets and tables information", + Short: "Display Bigquery datasets and tables information", Args: cobra.MinimumNArgs(0), Long: ` -Display available Bigauery datasets and tables resource information: +Display available Bigquery datasets and tables resource information: cloudfox gcp bigquery`, Run: runGCPBigQueryCommand, } From a8d2bfbfb34c22216ce0624ba2d73f7255024d00 Mon Sep 17 00:00:00 2001 From: sethsec-bf <46326948+sethsec-bf@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:18:47 -0400 Subject: [PATCH 29/29] Removed graph command from cobra for now --- cli/aws.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/aws.go b/cli/aws.go index fde552e..f75990b 100644 --- a/cli/aws.go +++ b/cli/aws.go @@ -2444,7 +2444,7 @@ func init() { EndpointsCommand, EnvsCommand, FilesystemsCommand, - GraphCommand, + //GraphCommand, IamSimulatorCommand, InstancesCommand, InventoryCommand,