-
Notifications
You must be signed in to change notification settings - Fork 191
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #81 from BishopFox/bastien_directoryservice_aws
Add Directory Service support for AWS
- Loading branch information
Showing
6 changed files
with
463 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.