package aws import ( "fmt" "os" "path/filepath" "strconv" "strings" "sync" "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/ec2/types" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/bishopfox/awsservicemap" "github.com/sirupsen/logrus" ) type ElasticNetworkInterfacesModule struct { EC2Client sdk.AWSEC2ClientInterface Caller sts.GetCallerIdentityOutput AWSRegions []string AWSOutputType string AWSTableCols string AWSProfile string WrapTable bool MappedENIs []MappedENI CommandCounter internal.CommandCounter output internal.OutputData2 modLog *logrus.Entry } type MappedENI struct { ID string Type string ExternalIP string PrivateIP string VPCID string AttachedInstance string Description string } func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputDirectory string, verbosity int) { m.output.Verbosity = verbosity m.output.Directory = outputDirectory m.output.CallingModule = "elastic-network-interfaces" m.modLog = internal.TxtLog.WithFields(logrus.Fields{ "module": m.output.CallingModule, }) if m.AWSProfile == "" { m.AWSProfile = internal.BuildAWSPath(m.Caller) } fmt.Printf("[%s][%s] Enumerating elastic network interfaces in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account)) wg := new(sync.WaitGroup) spinnerDone := make(chan bool) go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks") dataReceiver := make(chan MappedENI) // 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, dataReceiver) } wg.Wait() //time.Sleep(time.Second * 2) spinnerDone <- true <-spinnerDone receiverDone <- true <-receiverDone m.printENIsData(outputDirectory, dataReceiver, verbosity) } func (m *ElasticNetworkInterfacesModule) Receiver(receiver chan MappedENI, receiverDone chan bool) { defer close(receiverDone) for { select { case data := <-receiver: m.MappedENIs = append(m.MappedENIs, data) case <-receiverDone: receiverDone <- true return } } } func (m *ElasticNetworkInterfacesModule) printENIsData(outputDirectory string, dataReceiver chan MappedENI, verbosity int) { m.output.Headers = []string{ "Account", "ID", "Type", "External IP", "Internal IP", "VPC ID", "Attached Instance", "Description", } // 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, ",") // If the user specified wide as the output format, use these columns. } else if m.AWSOutputType == "wide" { tableCols = []string{ "Account", "ID", "Type", "External IP", "Internal IP", "VPC ID", "Attached Instance", "Description", } // Otherwise, use the default columns. } else { tableCols = []string{ "ID", "Type", "External IP", "Internal IP", "VPC ID", "Attached Instance", "Description", } } for _, eni := range m.MappedENIs { m.output.Body = append( m.output.Body, []string{ aws.ToString(m.Caller.Account), eni.ID, eni.Type, eni.ExternalIP, eni.PrivateIP, eni.VPCID, eni.AttachedInstance, eni.Description, }, ) } 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) fmt.Printf("[%s][%s] %s elastic network interfaces found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body))) } else { fmt.Printf("[%s][%s] No elastic network interfaces 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 *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) { path := filepath.Join(outputDirectory, "loot") err := os.MkdirAll(path, os.ModePerm) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ } privateIPsFilename := filepath.Join(path, "elastic-network-interfaces-PrivateIPs.txt") publicIPsFilename := filepath.Join(path, "elastic-network-interfaces-PublicIPs.txt") var publicIPs string var privateIPs string for _, eni := range m.MappedENIs { if eni.ExternalIP != "NoExternalIP" { publicIPs = publicIPs + fmt.Sprintln(eni.ExternalIP) } if eni.PrivateIP != "" { privateIPs = privateIPs + fmt.Sprintln(eni.PrivateIP) } } err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ } err = os.WriteFile(publicIPsFilename, []byte(publicIPs), 0644) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ } fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename) fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), publicIPsFilename) } func (m *ElasticNetworkInterfacesModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedENI) { defer wg.Done() servicemap := &awsservicemap.AwsServiceMap{ JsonFileSource: "DOWNLOAD_FROM_AWS", } res, err := servicemap.IsServiceInRegion("ec2", r) if err != nil { m.modLog.Error(err) } if res { m.CommandCounter.Total++ m.CommandCounter.Pending-- m.CommandCounter.Executing++ m.getDescribeNetworkInterfaces(r, dataReceiver) m.CommandCounter.Executing-- m.CommandCounter.Complete++ } } func (m *ElasticNetworkInterfacesModule) getDescribeNetworkInterfaces(region string, dataReceiver chan MappedENI) { NetworkInterfaces, err := sdk.CachedEC2DescribeNetworkInterfaces(m.EC2Client, aws.ToString(m.Caller.Account), region) // var PaginationControl *string // for { // DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces( // context.TODO(), // &(ec2.DescribeNetworkInterfacesInput{ // NextToken: PaginationControl, // }), // func(o *ec2.Options) { // o.Region = region // }, // ) if err != nil { m.modLog.Error(err.Error()) m.CommandCounter.Error++ return } for _, eni := range NetworkInterfaces { status := string(eni.Status) if status == "available" { continue // unused ENI } mappedENI := MappedENI{ ID: aws.ToString(eni.NetworkInterfaceId), Type: string(eni.InterfaceType), ExternalIP: getPublicIPOfElasticNetworkInterface(eni), PrivateIP: aws.ToString(eni.PrivateIpAddress), VPCID: aws.ToString(eni.VpcId), AttachedInstance: getAttachmentInstanceOfElasticNetworkInterface(eni), Description: aws.ToString(eni.Description), } dataReceiver <- mappedENI } } func getPublicIPOfElasticNetworkInterface(elasticNetworkInterface types.NetworkInterface) string { if elasticNetworkInterface.Association != nil { return aws.ToString(elasticNetworkInterface.Association.PublicIp) } return "NoExternalIP" } func getAttachmentInstanceOfElasticNetworkInterface(elasticNetworkInterface types.NetworkInterface) string { if elasticNetworkInterface.Attachment == nil { return "" } return aws.ToString(elasticNetworkInterface.Attachment.InstanceId) }