mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-09 09:39:33 -10:00
419 lines
15 KiB
Go
419 lines
15 KiB
Go
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/bishopfox/knownawsaccountslookup"
|
|
"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
|
|
Version string
|
|
SkipAdminCheck bool
|
|
PmapperDataBasePath string
|
|
|
|
pmapperMod PmapperModule
|
|
pmapperError error
|
|
|
|
vendors *knownawsaccountslookup.Vendors
|
|
|
|
// Main module data
|
|
// Used to store output data for pretty printing
|
|
output internal.OutputData2
|
|
|
|
modLog *logrus.Entry
|
|
}
|
|
|
|
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.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, m.PmapperDataBasePath)
|
|
|
|
////////////////
|
|
// 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 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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
////////////////
|
|
// 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 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
|
|
// }
|
|
|
|
// 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
|
|
fileName = fmt.Sprintf("%s/graph/%s/%s.jsonl", m.output.Directory, aws.ToString(m.Caller.Account), "roles")
|
|
// 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
|
|
}
|
|
|
|
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 _, 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) 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.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) {
|
|
|
|
//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
|
|
}
|
|
}
|
|
|
|
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.Error(err)
|
|
}
|
|
|
|
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 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) {
|
|
|
|
// // 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
|
|
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: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
|
|
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 != 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!"
|
|
// }
|
|
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
|
|
role := 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 roles
|
|
}
|