package aws import ( "context" "encoding/json" "fmt" "path/filepath" "strconv" "strings" "sync" "time" "github.com/BishopFox/cloudfox/internal" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudtrail" cloudtrailTypes "github.com/aws/aws-sdk-go-v2/service/cloudtrail/types" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/bishopfox/awsservicemap" "github.com/sirupsen/logrus" ) type OutboundAssumedRolesModule struct { // General configuration data CloudTrailClient *cloudtrail.Client Caller sts.GetCallerIdentityOutput AWSRegions []string AWSOutputType string AWSTableCols string Goroutines int AWSProfile string WrapTable bool // Main module data OutboundAssumeRoleEntries []OutboundAssumeRoleEntry Days int CommandCounter internal.CommandCounter // Used to store output data for pretty printing output internal.OutputData2 modLog *logrus.Entry } type OutboundAssumeRoleEntry struct { AWSService string Region string Type string SourceAccount string SourcePrincipal string DestinationAccount string DestinationPrincipal string Action string LogTimestamp string } type CloudTrailEvent struct { EventVersion string `json:"eventVersion"` UserIdentity struct { Type string `json:"type"` PrincipalID string `json:"principalId"` Arn string `json:"arn"` AccountID string `json:"accountId"` AccessKeyID string `json:"accessKeyId"` SessionContext struct { SessionIssuer struct { Type string `json:"type"` PrincipalID string `json:"principalId"` Arn string `json:"arn"` AccountID string `json:"accountId"` UserName string `json:"userName"` } `json:"sessionIssuer"` WebIDFederationData struct { } `json:"webIdFederationData"` Attributes struct { CreationDate time.Time `json:"creationDate"` MfaAuthenticated string `json:"mfaAuthenticated"` } `json:"attributes"` } `json:"sessionContext"` } `json:"userIdentity"` EventTime time.Time `json:"eventTime"` EventSource string `json:"eventSource"` EventName string `json:"eventName"` AwsRegion string `json:"awsRegion"` SourceIPAddress string `json:"sourceIPAddress"` UserAgent string `json:"userAgent"` RequestParameters struct { RoleArn string `json:"roleArn"` RoleSessionName string `json:"roleSessionName"` } `json:"requestParameters"` ResponseElements struct { Credentials struct { AccessKeyID string `json:"accessKeyId"` SessionToken string `json:"sessionToken"` Expiration string `json:"expiration"` } `json:"credentials"` AssumedRoleUser struct { AssumedRoleID string `json:"assumedRoleId"` Arn string `json:"arn"` } `json:"assumedRoleUser"` } `json:"responseElements"` RequestID string `json:"requestID"` EventID string `json:"eventID"` ReadOnly bool `json:"readOnly"` Resources []struct { AccountID string `json:"accountId"` Type string `json:"type"` Arn string `json:"ARN"` } `json:"resources"` EventType string `json:"eventType"` ManagementEvent bool `json:"managementEvent"` RecipientAccountID string `json:"recipientAccountId"` EventCategory string `json:"eventCategory"` TLSDetails struct { TLSVersion string `json:"tlsVersion"` CipherSuite string `json:"cipherSuite"` ClientProvidedHostHeader string `json:"clientProvidedHostHeader"` } `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 m.output.Directory = outputDirectory m.output.CallingModule = "outbound-assumed-roles" m.modLog = internal.TxtLog.WithFields(logrus.Fields{ "module": m.output.CallingModule, }) if m.AWSProfile == "" { m.AWSProfile = internal.BuildAWSPath(m.Caller) } m.Days = days fmt.Printf("[%s][%s] Enumerating outbound assumed role entries in cloudtrail for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) fmt.Printf("[%s][%s] Going back through %d days of cloudtrail events. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), days) wg := new(sync.WaitGroup) semaphore := make(chan struct{}, m.Goroutines) // Create a channel to signal the spinner aka task status goroutine to finish spinnerDone := make(chan bool) //fire up the the task status spinner/updated go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions") //create a channel to receive the objects dataReceiver := make(chan OutboundAssumeRoleEntry) // Create a channel to signal to stop receiverDone := make(chan bool) go m.Receiver(dataReceiver, receiverDone) for _, region := range m.AWSRegions { wg.Add(1) m.CommandCounter.Pending++ go m.executeChecks(region, wg, semaphore, dataReceiver) } wg.Wait() // Send a message to the spinner goroutine to close the channel and stop spinnerDone <- true <-spinnerDone receiverDone <- true <-receiverDone m.output.Headers = []string{ "Account", "Region", "Type", //"Source Account", "Source Principal", //"Destination Account", "Destination Principal", "Action", "Log Entry Timestamp", } // 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", "Region", "Type", //"Source Account", "Source Principal", //"Destination Account", "Destination Principal", "Action", "Log Entry Timestamp", } // Otherwise, use the default columns. } else { tableCols = []string{ "Region", "Type", //"Source Account", "Source Principal", //"Destination Account", "Destination Principal", "Action", "Log Entry Timestamp", } } // Table rows for i := range m.OutboundAssumeRoleEntries { m.output.Body = append( m.output.Body, []string{ aws.ToString(m.Caller.Account), m.OutboundAssumeRoleEntries[i].Region, m.OutboundAssumeRoleEntries[i].Type, //m.OutboundAssumeRoleEntries[i].SourceAccount, m.OutboundAssumeRoleEntries[i].SourcePrincipal, //m.OutboundAssumeRoleEntries[i].DestinationAccount, m.OutboundAssumeRoleEntries[i].DestinationPrincipal, m.OutboundAssumeRoleEntries[i].Action, m.OutboundAssumeRoleEntries[i].LogTimestamp, }, ) } if len(m.output.Body) > 0 { m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account))) o := internal.OutputClient{ Verbosity: verbosity, CallingModule: m.output.CallingModule, Table: internal.TableClient{ Wrap: m.WrapTable, }, } o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{ Header: m.output.Headers, Body: m.output.Body, TableCols: tableCols, Name: m.output.CallingModule, }) 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) //m.writeLoot(o.Table.DirectoryName, verbosity) fmt.Printf("[%s][%s] %s log entries found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body))) //m.writeLoot() } else { fmt.Printf("[%s][%s] No matching log entries found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile)) } 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 *OutboundAssumedRolesModule) Receiver(receiver chan OutboundAssumeRoleEntry, receiverDone chan bool) { defer close(receiverDone) for { select { case data := <-receiver: m.OutboundAssumeRoleEntries = append(m.OutboundAssumeRoleEntries, data) case <-receiverDone: receiverDone <- true return } } } func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) { defer wg.Done() servicemap := &awsservicemap.AwsServiceMap{ JsonFileSource: "DOWNLOAD_FROM_AWS", } res, err := servicemap.IsServiceInRegion("cloudtrail", r) if err != nil { 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.getCrossAccountBatchGetImageEntriesPerRegion(r, wg, semaphore, dataReceiver) } } func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(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("AssumeRole"), }, }, 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) if cloudtrailEvent.UserIdentity.Type == "AssumedRole" || cloudtrailEvent.UserIdentity.Type == "IAMUser" { if cloudtrailEvent.UserIdentity.Type == "AssumedRole" { sourcePrincipal = cloudtrailEvent.UserIdentity.SessionContext.SessionIssuer.Arn } else { sourcePrincipal = cloudtrailEvent.UserIdentity.Arn } userType = cloudtrailEvent.UserIdentity.Type sourceAccount = "" destinationAccount = "" destinationPrincipal = cloudtrailEvent.RequestParameters.RoleArn logTimestamp := cloudtrailEvent.EventTime.Format("2006-01-02 15:04:05") //fmt.Printf("%s,%s,%s,%s\n", sourceAccount, sourcePrincipal, destinationAccount, destinationPrincipal) dataReceiver <- OutboundAssumeRoleEntry{ AWSService: "CloudTrail", Region: r, Type: userType, SourceAccount: sourceAccount, SourcePrincipal: sourcePrincipal, DestinationAccount: destinationAccount, DestinationPrincipal: destinationPrincipal, Action: "sts:AssumeRole", 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 } } } // 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 } } }