Skip to content

Commit

Permalink
Merge pull request #81 from BishopFox/bastien_directoryservice_aws
Browse files Browse the repository at this point in the history
Add Directory Service support for AWS
  • Loading branch information
sethsec-bf committed Apr 16, 2024
2 parents 9920c41 + 67af1bd commit bfa95ac
Show file tree
Hide file tree
Showing 6 changed files with 463 additions and 59 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF

| Provider| CloudFox Commands |
| - | - |
| AWS | 33 |
| AWS | 34 |
| Azure | 4 |
| GCP | Support Planned |
| Kubernetes | Support Planned |
Expand Down Expand Up @@ -140,6 +140,7 @@ Additional policy notes (as of 09/2022):
| AWS | [sqs](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#sqs) | This command enumerates all of the sqs queues and gives you the commands to receive messages from a queue and send messages to a queue (if you have the permissions needed). This command also attempts to summarize queue resource policies if they exist.|
| AWS | [tags](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#tags) | List all resources with tags, and all of the tags. This can be used similar to inventory as another method to identify what types of resources exist in an account. |
| AWS | [workloads](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#workloads) | List all of the compute workloads and what role they have. Tells you if any of the roles are admin (bad) and if you have pmapper data locally, it will tell you if any of the roles can privesc to admin (also bad) |
| AWS | [ds](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#workloads) | List all of the AWS-managed directories and their attributes. Also summarizes the current trusts with their directions and types. |


# Azure Commands
Expand Down
276 changes: 276 additions & 0 deletions aws/directory-services.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
package aws

import (
"fmt"
"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/sts"
dsTypes "github.com/aws/aws-sdk-go-v2/service/directoryservice/types"
"github.com/bishopfox/awsservicemap"
"github.com/sirupsen/logrus"
)

type DirectoryModule struct {
// General configuration data
DSClient sdk.AWSDSClientInterface
Caller sts.GetCallerIdentityOutput
AWSRegions []string
AWSProfile string
Goroutines int
WrapTable bool
AWSOutputType string
AWSTableCols string
AWSMFAToken string
AWSConfig aws.Config
AWSProfileProvided string
AWSProfileStub string
CloudFoxVersion string

Directories []Directory
CommandCounter internal.CommandCounter
output internal.OutputData2
modLog *logrus.Entry
}

type Directory struct {
DirectoryId string
DNS string
NetBios string
AccessURL string
Alias string
OsVersion string
Region string
TrustInfo string
}

func (m *DirectoryModule) PrintDirectories(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "directory-services"
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})

if m.AWSProfileProvided == "" {
m.AWSProfileStub = internal.BuildAWSPath(m.Caller)
} else {
m.AWSProfileStub = m.AWSProfileProvided
}
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileProvided, aws.ToString(m.Caller.Account)))

fmt.Printf("[%s][%s] Enumerating Cloud Directories with resource policies for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account))
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, "tasks")

//create a channel to receive the objects
dataReceiver := make(chan Directory)

// 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
// Send a message to the data receiver goroutine to close the channel and stop
receiverDone <- true
<-receiverDone

// add - if struct is not empty do this. otherwise, dont write anything.
m.output.Headers = []string{
"Account",
"Name",
"Alias",
"Domain",
"NetBIOS name",
"Access URL",
"Version",
"Trusts",
}

// 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",
"Name",
"Alias",
"Domain",
"NetBIOS name",
"Access URL",
"Version",
"Trusts",
}
} else {
tableCols = []string{
"Account",
"Name",
"Domain",
"NetBIOS name",
"Access URL",
"Version",
"Trusts",
}
}


// Table rows
for i := range m.Directories {
m.output.Body = append(
m.output.Body,
[]string{
aws.ToString(m.Caller.Account),
m.Directories[i].DirectoryId,
m.Directories[i].Alias,
m.Directories[i].DNS,
m.Directories[i].NetBios,
m.Directories[i].AccessURL,
m.Directories[i].OsVersion,
m.Directories[i].TrustInfo,
},
)

}
if len(m.output.Body) > 0 {
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.AWSProfileStub
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
o.WriteFullOutput(o.Table.TableFiles, nil)
//m.writeLoot(o.Table.DirectoryName, verbosity)
fmt.Printf("[%s][%s] %s directories found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), strconv.Itoa(len(m.output.Body)))
//fmt.Printf("[%s][%s] Resource policies stored to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
} else {
fmt.Printf("[%s][%s] No directories found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
}
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.AWSProfileStub), m.output.CallingModule)

}

func (m *DirectoryModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Directory) {
defer wg.Done()

servicemap := &awsservicemap.AwsServiceMap{
JsonFileSource: "DOWNLOAD_FROM_AWS",
}
res, err := servicemap.IsServiceInRegion("clouddirectory", r)
if err != nil {
m.modLog.Error(err)
}
if res {
m.CommandCounter.Total++
wg.Add(1)
m.getDirectoriesPerRegion(r, wg, semaphore, dataReceiver)
}
}

func (m *DirectoryModule) Receiver(receiver chan Directory, receiverDone chan bool) {
defer close(receiverDone)
for {
select {
case data := <-receiver:
m.Directories = append(m.Directories, data)
case <-receiverDone:
receiverDone <- true
return
}
}
}
func (m *DirectoryModule) getDirectoriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Directory) {
defer func() {
m.CommandCounter.Executing--
m.CommandCounter.Complete++
wg.Done()

}()
semaphore <- struct{}{}
defer func() {
<-semaphore
}()

// Get directories
directories, err := sdk.CachedDSDescribeDirectories(m.DSClient, aws.ToString(m.Caller.Account), r)
if err != nil {
m.modLog.Error(err)
}
for _, directory := range directories {
trusts, err := sdk.CachedDSDescribeTrusts(m.DSClient, aws.ToString(m.Caller.Account), r, *directory.DirectoryId)
if err != nil {
m.modLog.Error(err)
}
dataReceiver <- Directory{
DirectoryId: *directory.DirectoryId,
DNS: *directory.Name,
NetBios: *directory.ShortName,
Region: r,
AccessURL: *directory.AccessUrl,
Alias: *directory.Alias,
OsVersion: fmt.Sprintf("%s", directory.OsVersion),
TrustInfo: m.formatTrusts(trusts),
}
}
}

func (m *DirectoryModule) formatTrusts(t []dsTypes.Trust) string {
var output string = ""
for idx, trust := range t {
if idx != 0 {
output = output + "\n"
}
if trust.TrustDirection == "One-Way: Outgoing" {
output = output + "→"
} else if trust.TrustDirection == "One-Way: Ingoing" {
output = output + "←"
} else {
output = output + "↔"
}
output = fmt.Sprintf("%s %s", output, *trust.RemoteDomainName)
// check trust type (external or forest)
if trust.TrustType == "External" {
output = fmt.Sprintf("%s (%s)", output, "Domain")
} else {
output = fmt.Sprintf("%s (%s)", output, "Forest")
}
}
return output
}
95 changes: 95 additions & 0 deletions aws/sdk/ds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package sdk

import (
"context"
"encoding/gob"
"fmt"

"github.com/patrickmn/go-cache"
"github.com/BishopFox/cloudfox/internal"
"github.com/aws/aws-sdk-go-v2/service/directoryservice"
dsTypes "github.com/aws/aws-sdk-go-v2/service/directoryservice/types"
)

type AWSDSClientInterface interface {
DescribeDirectories(context.Context, *directoryservice.DescribeDirectoriesInput, ...func(*directoryservice.Options)) (*directoryservice.DescribeDirectoriesOutput, error)
DescribeTrusts(context.Context, *directoryservice.DescribeTrustsInput, ...func(*directoryservice.Options)) (*directoryservice.DescribeTrustsOutput, error)
}

func init() {
gob.Register([]dsTypes.DirectoryDescription{})
gob.Register([]dsTypes.Trust{})

}

func CachedDSDescribeDirectories(client AWSDSClientInterface, accountID string, region string) ([]dsTypes.DirectoryDescription, error) {
var PaginationControl *string
var directories []dsTypes.DirectoryDescription
cacheKey := fmt.Sprintf("%s-ds-DescribeDirectories-%s", accountID, region)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]dsTypes.DirectoryDescription), nil
}
for {
DescribeDirectories, err := client.DescribeDirectories(
context.TODO(),
&directoryservice.DescribeDirectoriesInput{
NextToken: PaginationControl,
},
func(o *directoryservice.Options) {
o.Region = region
},
)

if err != nil {
return directories, err
}

directories = append(directories, DescribeDirectories.DirectoryDescriptions...)

//pagination
if DescribeDirectories.NextToken == nil {
break
}
PaginationControl = DescribeDirectories.NextToken
}

internal.Cache.Set(cacheKey, directories, cache.DefaultExpiration)
return directories, nil
}

func CachedDSDescribeTrusts(client AWSDSClientInterface, accountID string, region string, directoryId string) ([]dsTypes.Trust, error) {
var PaginationControl *string
var trusts []dsTypes.Trust
cacheKey := fmt.Sprintf("%s-ds-DescribeTrusts-%s-%s", accountID, region, directoryId)
cached, found := internal.Cache.Get(cacheKey)
if found {
return cached.([]dsTypes.Trust), nil
}
for {
DescribeDirectoryTrusts, err := client.DescribeTrusts(
context.TODO(),
&directoryservice.DescribeTrustsInput{
DirectoryId: &directoryId,
NextToken: PaginationControl,
},
func(o *directoryservice.Options) {
o.Region = region
},
)
if err != nil {
return trusts, err
}

trusts = append(trusts, DescribeDirectoryTrusts.Trusts...)

//pagination
if DescribeDirectoryTrusts.NextToken == nil {
break
}
PaginationControl = DescribeDirectoryTrusts.NextToken
}
internal.Cache.Set(cacheKey, trusts, cache.DefaultExpiration)

return trusts, nil
}
Loading

0 comments on commit bfa95ac

Please sign in to comment.