Skip to content

Commit

Permalink
Merge pull request #96 from BishopFox/aws-principals-with-admin-check
Browse files Browse the repository at this point in the history
Bug fixes and small feature updates
  • Loading branch information
bishopfaure authored Oct 17, 2024
2 parents 785b693 + b3c25bb commit f7d1cbc
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 45 deletions.
2 changes: 1 addition & 1 deletion aws/cape-tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ func preloadData(filePaths []string) (*AllAccountData, error) {

for _, filePath := range filePaths {
fileRecords, err := loadFileRecords(filePath)
if err != nil {
if err != nil {
return nil, err
}
appData.Files[filePath] = fileRecords
Expand Down
78 changes: 73 additions & 5 deletions aws/cape.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package aws

import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"

Expand Down Expand Up @@ -200,7 +202,12 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s
s, sourceVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(source)
//for the source vertex, we only want to deal with the ones that are NOT in this account
if sourceVertexWithProperties.Attributes["AccountID"] != aws.ToString(m.Caller.Account) {
// skip if the source Name contains AWSSSO-
if strings.Contains(sourceVertexWithProperties.Attributes["Name"], "AWSSSO-") {
continue
}
// now let's see if there is a path from this source to our destination

path, _ := graph.ShortestPath(m.GlobalGraph, s, d)
// if we have a path, then lets document this source as having a path to our destination
if path != nil {
Expand Down Expand Up @@ -235,7 +242,7 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s
privescPathsBody = append(privescPathsBody, []string{
aws.ToString(m.Caller.Account),
s,
magenta(d),
d,
magenta(destinationVertexWithProperties.Attributes["IsAdminString"]),
paths})
} else {
Expand Down Expand Up @@ -289,7 +296,7 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo

TrustedPrincipals = append(TrustedPrincipals, TrustedPrincipal{
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
VendorName: vendorName,
//IsAdmin: false,
//CanPrivEscToAdmin: false,
Expand Down Expand Up @@ -650,6 +657,48 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
}
}

// if the role trusts a principal in another account explicitly, then the principal can assume the role
if thisAccount != trustedPrincipalAccount {
// make a CAN_ASSUME relationship between the trusted principal and this role

err := GlobalGraph.AddEdge(
TrustedPrincipal.TrustedPrincipal,
a.Arn,
//graph.EdgeAttribute("AssumeRole", "Cross account explicit trust"),
graph.EdgeAttribute("AssumeRole", "can assume (because of an explicit cross account trust) "),
)
if err != nil {
//fmt.Println(err)
//fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Cross account explicit trust")
if err == graph.ErrEdgeAlreadyExists {
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
//fmt.Println("Edge already exists")

// get the existing edge
existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.TrustedPrincipal, a.Arn)
// get the map of attributes
existingProperties := existingEdge.Properties
// add the new attributes to attributes map within the properties struct
// Check if the Attributes map is initialized, if not, initialize it
if existingProperties.Attributes == nil {
existingProperties.Attributes = make(map[string]string)
}

// Add or update the attribute
existingProperties.Attributes["AssumeRole"] = "can assume (because of an explicit cross account trust) "
err = GlobalGraph.UpdateEdge(
TrustedPrincipal.TrustedPrincipal,
a.Arn,
graph.EdgeAttributes(existingProperties.Attributes),
)
if err != nil {
fmt.Println(err)
}
}

}
}

// If the role trusts a principal in this account or another account using the :root notation, then we need to iterate over all of the rows in AllPermissionsRows to find the principals that have sts:AssumeRole permissions on this role
// if the role we are looking at trusts root in it's own account

Expand All @@ -667,6 +716,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
if PermissionsRowAccount == thisAccount {
// lets only look for rows that have sts:AssumeRole permissions
if policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") {

// lets only focus on rows that have an effect of Allow
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
// if the resource is * or the resource is this role arn, then this principal can assume this role
Expand Down Expand Up @@ -820,10 +870,10 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
}
if PermissionsRowAccount == trustedPrincipalAccount {
// lets only look for rows that have sts:AssumeRole permissions
if policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") {
// lets only look for rows that have sts:AssumeRole permis sions
if policy.MatchesAfterExpansion("sts:AssumeRole", PermissionsRow.Action) {
// if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
// strings.EqualFold(PermissionsRow.Action, "*") ||
// strings.EqualFold(PermissionsRow.Action, "*") {
// strings.EqualFold(PermissionsRow.Action, "sts:Assume*") ||
// strings.EqualFold(PermissionsRow.Action, "sts:*") {
// lets only focus on rows that have an effect of Allow
Expand Down Expand Up @@ -979,3 +1029,21 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
}

}

// function to read file specified in CapeArnIgnoreList which is separated by newlines, and convert it to a slice of strings with each line as an entry in the slice.
// the function accepts a string with the filename

func ReadArnIgnoreListFile(filename string) ([]string, error) {
var arnIgnoreList []string
file, err := os.Open(filename)
if err != nil {
return arnIgnoreList, err
}
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
arnIgnoreList = append(arnIgnoreList, scanner.Text())
}
return arnIgnoreList, scanner.Err()
}
2 changes: 1 addition & 1 deletion aws/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ func (m *GraphCommand) collectRoleDataForGraph() []models.Role {

TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
VendorName: vendorName,
//IsAdmin: false,
//CanPrivEscToAdmin: false,
Expand Down
20 changes: 15 additions & 5 deletions aws/iam-simulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ type IamSimulatorModule struct {
WrapTable bool

// Main module data
SimulatorResults []SimulatorResult
CommandCounter internal.CommandCounter
SimulatorResults []SimulatorResult
CommandCounter internal.CommandCounter
IamSimulatorAdminCheckOnly bool
// Used to store output data for pretty printing
output internal.OutputData2
modLog *logrus.Entry
Expand Down Expand Up @@ -104,6 +105,11 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,

go m.Receiver(dataReceiver, receiverDone)

if m.IamSimulatorAdminCheckOnly {
// set defaultActionNames to an empty slice
defaultActionNames = []string{}
}

// This double if/else section is here to handle the cases where --principal or --action (or both) are specified.
if principal != "" {
if action != "" {
Expand Down Expand Up @@ -354,7 +360,9 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r
Decision: "",
}
} else {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
if !m.IamSimulatorAdminCheckOnly {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
}

}
Expand Down Expand Up @@ -394,7 +402,9 @@ func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, r
Decision: "",
}
} else {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
if !m.IamSimulatorAdminCheckOnly {
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
}
}

}
Expand Down Expand Up @@ -443,7 +453,7 @@ func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
"iam:PutRolePolicy",
"iam:AttachRolePolicy",
"secretsmanager:GetSecretValue",
"ssm:GetDocument",
"ssm:GetParameters",
}
for {
SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy(
Expand Down
41 changes: 41 additions & 0 deletions aws/principals.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ type IamPrincipalsModule struct {
AWSProfile string
WrapTable bool

SkipAdminCheck bool
iamSimClient IamSimulatorModule
pmapperMod PmapperModule
pmapperError error
PmapperDataBasePath string

// Main module data
Users []User
Roles []Role
Expand All @@ -43,6 +49,8 @@ type User struct {
Name string
AttachedPolicies []string
InlinePolicies []string
Admin string
CanPrivEsc string
}

type Group struct {
Expand All @@ -62,13 +70,16 @@ type Role struct {
Name string
AttachedPolicies []string
InlinePolicies []string
Admin string
CanPrivEsc string
}

func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosity int) {
// These struct values are used by the output module
m.output.Verbosity = verbosity
m.output.Directory = outputDirectory
m.output.CallingModule = "principals"
localAdminMap := make(map[string]bool)
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
"module": m.output.CallingModule,
})
Expand All @@ -78,6 +89,9 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi

fmt.Printf("[%s][%s] Enumerating IAM Users and Roles for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))

m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)

// wg := new(sync.WaitGroup)

// done := make(chan bool)
Expand All @@ -101,6 +115,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
"Arn",
"AttachedPolicies",
"InlinePolicies",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}

// If the user specified table columns, use those.
Expand All @@ -122,6 +138,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
"Arn",
//"AttachedPolicies",
//"InlinePolicies",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}

// Otherwise, use the default columns.
Expand All @@ -132,11 +150,25 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
"Arn",
// "AttachedPolicies",
// "InlinePolicies",
"IsAdminRole?",
"CanPrivEscToAdmin?",
}
}

// Remove the pmapper row if there is no pmapper data
if m.pmapperError != nil {
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
}

//Table rows
for i := range m.Users {
if m.pmapperError == nil {
m.Users[i].Admin, m.Users[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Users[i].Arn)
} else {
m.Users[i].Admin, m.Users[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Users[i].Arn, m.iamSimClient, localAdminMap)
}

m.output.Body = append(
m.output.Body,
[]string{
Expand All @@ -146,12 +178,19 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
m.Users[i].Arn,
strings.Join(m.Users[i].AttachedPolicies, " , "),
strings.Join(m.Users[i].InlinePolicies, " , "),
m.Users[i].Admin,
m.Users[i].CanPrivEsc,
},
)

}

for i := range m.Roles {
if m.pmapperError == nil {
m.Roles[i].Admin, m.Roles[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Roles[i].Arn)
} else {
m.Roles[i].Admin, m.Roles[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Roles[i].Arn, m.iamSimClient, localAdminMap)
}
m.output.Body = append(
m.output.Body,
[]string{
Expand All @@ -161,6 +200,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
m.Roles[i].Arn,
strings.Join(m.Roles[i].AttachedPolicies, " , "),
strings.Join(m.Roles[i].InlinePolicies, " , "),
m.Roles[i].Admin,
m.Roles[i].CanPrivEsc,
},
)

Expand Down
11 changes: 6 additions & 5 deletions aws/role-trusts.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,10 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]strin
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
// if there is more than one externalID concat them using newlines
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
body = append(body, []string{
aws.ToString(m.Caller.Account),
Expand Down Expand Up @@ -281,7 +282,7 @@ func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string)
for _, role := range m.AnalyzedRoles {
for _, statement := range role.trustsDoc.Statement {
for _, principal := range statement.Principal.AWS {
if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == "" {
if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == nil {
accountID := strings.Split(principal, ":")[4]
vendorName := m.vendors.GetVendorNameFromAccountID(accountID)
if vendorName != "" {
Expand All @@ -292,7 +293,7 @@ func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string)
RoleARN: aws.ToString(role.roleARN),
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
TrustedPrincipal: principal,
ExternalID: statement.Condition.StringEquals.StsExternalID,
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
IsAdmin: role.Admin,
CanPrivEsc: role.CanPrivEsc,
}
Expand Down
Loading

0 comments on commit f7d1cbc

Please sign in to comment.