Skip to content

Commit f7d1cbc

Browse files
authored
Merge pull request #96 from BishopFox/aws-principals-with-admin-check
Bug fixes and small feature updates
2 parents 785b693 + b3c25bb commit f7d1cbc

11 files changed

Lines changed: 244 additions & 45 deletions

File tree

aws/cape-tui.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ func preloadData(filePaths []string) (*AllAccountData, error) {
301301

302302
for _, filePath := range filePaths {
303303
fileRecords, err := loadFileRecords(filePath)
304-
if err != nil {
304+
if err != nil {
305305
return nil, err
306306
}
307307
appData.Files[filePath] = fileRecords

aws/cape.go

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package aws
22

33
import (
4+
"bufio"
45
"fmt"
6+
"os"
57
"path/filepath"
68
"strings"
79

@@ -200,7 +202,12 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s
200202
s, sourceVertexWithProperties, _ := m.GlobalGraph.VertexWithProperties(source)
201203
//for the source vertex, we only want to deal with the ones that are NOT in this account
202204
if sourceVertexWithProperties.Attributes["AccountID"] != aws.ToString(m.Caller.Account) {
205+
// skip if the source Name contains AWSSSO-
206+
if strings.Contains(sourceVertexWithProperties.Attributes["Name"], "AWSSSO-") {
207+
continue
208+
}
203209
// now let's see if there is a path from this source to our destination
210+
204211
path, _ := graph.ShortestPath(m.GlobalGraph, s, d)
205212
// if we have a path, then lets document this source as having a path to our destination
206213
if path != nil {
@@ -235,7 +242,7 @@ func (m *CapeCommand) findPathsToThisDestination(allGlobalNodes map[string]map[s
235242
privescPathsBody = append(privescPathsBody, []string{
236243
aws.ToString(m.Caller.Account),
237244
s,
238-
magenta(d),
245+
d,
239246
magenta(destinationVertexWithProperties.Attributes["IsAdminString"]),
240247
paths})
241248
} else {
@@ -289,7 +296,7 @@ func ConvertIAMRoleToNode(role types.Role, vendors *knownawsaccountslookup.Vendo
289296

290297
TrustedPrincipals = append(TrustedPrincipals, TrustedPrincipal{
291298
TrustedPrincipal: principal,
292-
ExternalID: statement.Condition.StringEquals.StsExternalID,
299+
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
293300
VendorName: vendorName,
294301
//IsAdmin: false,
295302
//CanPrivEscToAdmin: false,
@@ -650,6 +657,48 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
650657
}
651658
}
652659

660+
// if the role trusts a principal in another account explicitly, then the principal can assume the role
661+
if thisAccount != trustedPrincipalAccount {
662+
// make a CAN_ASSUME relationship between the trusted principal and this role
663+
664+
err := GlobalGraph.AddEdge(
665+
TrustedPrincipal.TrustedPrincipal,
666+
a.Arn,
667+
//graph.EdgeAttribute("AssumeRole", "Cross account explicit trust"),
668+
graph.EdgeAttribute("AssumeRole", "can assume (because of an explicit cross account trust) "),
669+
)
670+
if err != nil {
671+
//fmt.Println(err)
672+
//fmt.Println(TrustedPrincipal.TrustedPrincipal + a.Arn + "Cross account explicit trust")
673+
if err == graph.ErrEdgeAlreadyExists {
674+
// update the edge by copying the existing graph.Edge with attributes and add the new attributes
675+
//fmt.Println("Edge already exists")
676+
677+
// get the existing edge
678+
existingEdge, _ := GlobalGraph.Edge(TrustedPrincipal.TrustedPrincipal, a.Arn)
679+
// get the map of attributes
680+
existingProperties := existingEdge.Properties
681+
// add the new attributes to attributes map within the properties struct
682+
// Check if the Attributes map is initialized, if not, initialize it
683+
if existingProperties.Attributes == nil {
684+
existingProperties.Attributes = make(map[string]string)
685+
}
686+
687+
// Add or update the attribute
688+
existingProperties.Attributes["AssumeRole"] = "can assume (because of an explicit cross account trust) "
689+
err = GlobalGraph.UpdateEdge(
690+
TrustedPrincipal.TrustedPrincipal,
691+
a.Arn,
692+
graph.EdgeAttributes(existingProperties.Attributes),
693+
)
694+
if err != nil {
695+
fmt.Println(err)
696+
}
697+
}
698+
699+
}
700+
}
701+
653702
// 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
654703
// if the role we are looking at trusts root in it's own account
655704

@@ -667,6 +716,7 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
667716
if PermissionsRowAccount == thisAccount {
668717
// lets only look for rows that have sts:AssumeRole permissions
669718
if policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") {
719+
670720
// lets only focus on rows that have an effect of Allow
671721
if strings.EqualFold(PermissionsRow.Effect, "Allow") {
672722
// if the resource is * or the resource is this role arn, then this principal can assume this role
@@ -820,10 +870,10 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
820870
fmt.Sprintf("Could not get account number from this PermissionsRow%s", PermissionsRow.Arn)
821871
}
822872
if PermissionsRowAccount == trustedPrincipalAccount {
823-
// lets only look for rows that have sts:AssumeRole permissions
824-
if policy.MatchesAfterExpansion(PermissionsRow.Action, "sts:AssumeRole") {
873+
// lets only look for rows that have sts:AssumeRole permis sions
874+
if policy.MatchesAfterExpansion("sts:AssumeRole", PermissionsRow.Action) {
825875
// if strings.EqualFold(PermissionsRow.Action, "sts:AssumeRole") ||
826-
// strings.EqualFold(PermissionsRow.Action, "*") ||
876+
// strings.EqualFold(PermissionsRow.Action, "*") {
827877
// strings.EqualFold(PermissionsRow.Action, "sts:Assume*") ||
828878
// strings.EqualFold(PermissionsRow.Action, "sts:*") {
829879
// lets only focus on rows that have an effect of Allow
@@ -979,3 +1029,21 @@ func (a *Node) MakeRoleEdges(GlobalGraph graph.Graph[string, string]) {
9791029
}
9801030

9811031
}
1032+
1033+
// 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.
1034+
// the function accepts a string with the filename
1035+
1036+
func ReadArnIgnoreListFile(filename string) ([]string, error) {
1037+
var arnIgnoreList []string
1038+
file, err := os.Open(filename)
1039+
if err != nil {
1040+
return arnIgnoreList, err
1041+
}
1042+
defer file.Close()
1043+
1044+
scanner := bufio.NewScanner(file)
1045+
for scanner.Scan() {
1046+
arnIgnoreList = append(arnIgnoreList, scanner.Text())
1047+
}
1048+
return arnIgnoreList, scanner.Err()
1049+
}

aws/graph.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ func (m *GraphCommand) collectRoleDataForGraph() []models.Role {
331331

332332
TrustedPrincipals = append(TrustedPrincipals, models.TrustedPrincipal{
333333
TrustedPrincipal: principal,
334-
ExternalID: statement.Condition.StringEquals.StsExternalID,
334+
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
335335
VendorName: vendorName,
336336
//IsAdmin: false,
337337
//CanPrivEscToAdmin: false,

aws/iam-simulator.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ type IamSimulatorModule struct {
3333
WrapTable bool
3434

3535
// Main module data
36-
SimulatorResults []SimulatorResult
37-
CommandCounter internal.CommandCounter
36+
SimulatorResults []SimulatorResult
37+
CommandCounter internal.CommandCounter
38+
IamSimulatorAdminCheckOnly bool
3839
// Used to store output data for pretty printing
3940
output internal.OutputData2
4041
modLog *logrus.Entry
@@ -104,6 +105,11 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
104105

105106
go m.Receiver(dataReceiver, receiverDone)
106107

108+
if m.IamSimulatorAdminCheckOnly {
109+
// set defaultActionNames to an empty slice
110+
defaultActionNames = []string{}
111+
}
112+
107113
// This double if/else section is here to handle the cases where --principal or --action (or both) are specified.
108114
if principal != "" {
109115
if action != "" {
@@ -354,7 +360,9 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r
354360
Decision: "",
355361
}
356362
} else {
357-
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
363+
if !m.IamSimulatorAdminCheckOnly {
364+
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
365+
}
358366
}
359367

360368
}
@@ -394,7 +402,9 @@ func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, r
394402
Decision: "",
395403
}
396404
} else {
397-
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
405+
if !m.IamSimulatorAdminCheckOnly {
406+
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
407+
}
398408
}
399409

400410
}
@@ -443,7 +453,7 @@ func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
443453
"iam:PutRolePolicy",
444454
"iam:AttachRolePolicy",
445455
"secretsmanager:GetSecretValue",
446-
"ssm:GetDocument",
456+
"ssm:GetParameters",
447457
}
448458
for {
449459
SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy(

aws/principals.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ type IamPrincipalsModule struct {
2626
AWSProfile string
2727
WrapTable bool
2828

29+
SkipAdminCheck bool
30+
iamSimClient IamSimulatorModule
31+
pmapperMod PmapperModule
32+
pmapperError error
33+
PmapperDataBasePath string
34+
2935
// Main module data
3036
Users []User
3137
Roles []Role
@@ -43,6 +49,8 @@ type User struct {
4349
Name string
4450
AttachedPolicies []string
4551
InlinePolicies []string
52+
Admin string
53+
CanPrivEsc string
4654
}
4755

4856
type Group struct {
@@ -62,13 +70,16 @@ type Role struct {
6270
Name string
6371
AttachedPolicies []string
6472
InlinePolicies []string
73+
Admin string
74+
CanPrivEsc string
6575
}
6676

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

7990
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))
8091

92+
m.pmapperMod, m.pmapperError = InitPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines, m.PmapperDataBasePath)
93+
m.iamSimClient = InitIamCommandClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
94+
8195
// wg := new(sync.WaitGroup)
8296

8397
// done := make(chan bool)
@@ -101,6 +115,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
101115
"Arn",
102116
"AttachedPolicies",
103117
"InlinePolicies",
118+
"IsAdminRole?",
119+
"CanPrivEscToAdmin?",
104120
}
105121

106122
// If the user specified table columns, use those.
@@ -122,6 +138,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
122138
"Arn",
123139
//"AttachedPolicies",
124140
//"InlinePolicies",
141+
"IsAdminRole?",
142+
"CanPrivEscToAdmin?",
125143
}
126144

127145
// Otherwise, use the default columns.
@@ -132,11 +150,25 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
132150
"Arn",
133151
// "AttachedPolicies",
134152
// "InlinePolicies",
153+
"IsAdminRole?",
154+
"CanPrivEscToAdmin?",
135155
}
136156
}
137157

158+
// Remove the pmapper row if there is no pmapper data
159+
if m.pmapperError != nil {
160+
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
161+
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
162+
}
163+
138164
//Table rows
139165
for i := range m.Users {
166+
if m.pmapperError == nil {
167+
m.Users[i].Admin, m.Users[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Users[i].Arn)
168+
} else {
169+
m.Users[i].Admin, m.Users[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Users[i].Arn, m.iamSimClient, localAdminMap)
170+
}
171+
140172
m.output.Body = append(
141173
m.output.Body,
142174
[]string{
@@ -146,12 +178,19 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
146178
m.Users[i].Arn,
147179
strings.Join(m.Users[i].AttachedPolicies, " , "),
148180
strings.Join(m.Users[i].InlinePolicies, " , "),
181+
m.Users[i].Admin,
182+
m.Users[i].CanPrivEsc,
149183
},
150184
)
151185

152186
}
153187

154188
for i := range m.Roles {
189+
if m.pmapperError == nil {
190+
m.Roles[i].Admin, m.Roles[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Roles[i].Arn)
191+
} else {
192+
m.Roles[i].Admin, m.Roles[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Roles[i].Arn, m.iamSimClient, localAdminMap)
193+
}
155194
m.output.Body = append(
156195
m.output.Body,
157196
[]string{
@@ -161,6 +200,8 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosi
161200
m.Roles[i].Arn,
162201
strings.Join(m.Roles[i].AttachedPolicies, " , "),
163202
strings.Join(m.Roles[i].InlinePolicies, " , "),
203+
m.Roles[i].Admin,
204+
m.Roles[i].CanPrivEsc,
164205
},
165206
)
166207

aws/role-trusts.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,10 @@ func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]strin
219219
RoleARN: aws.ToString(role.roleARN),
220220
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
221221
TrustedPrincipal: principal,
222-
ExternalID: statement.Condition.StringEquals.StsExternalID,
223-
IsAdmin: role.Admin,
224-
CanPrivEsc: role.CanPrivEsc,
222+
// if there is more than one externalID concat them using newlines
223+
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
224+
IsAdmin: role.Admin,
225+
CanPrivEsc: role.CanPrivEsc,
225226
}
226227
body = append(body, []string{
227228
aws.ToString(m.Caller.Account),
@@ -281,7 +282,7 @@ func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string)
281282
for _, role := range m.AnalyzedRoles {
282283
for _, statement := range role.trustsDoc.Statement {
283284
for _, principal := range statement.Principal.AWS {
284-
if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == "" {
285+
if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == nil {
285286
accountID := strings.Split(principal, ":")[4]
286287
vendorName := m.vendors.GetVendorNameFromAccountID(accountID)
287288
if vendorName != "" {
@@ -292,7 +293,7 @@ func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string)
292293
RoleARN: aws.ToString(role.roleARN),
293294
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
294295
TrustedPrincipal: principal,
295-
ExternalID: statement.Condition.StringEquals.StsExternalID,
296+
ExternalID: strings.Join(statement.Condition.StringEquals.StsExternalID, "\n"),
296297
IsAdmin: role.Admin,
297298
CanPrivEsc: role.CanPrivEsc,
298299
}

0 commit comments

Comments
 (0)