Skip to content

Commit 125c1ea

Browse files
authored
Merge pull request #80 from BishopFox/seth-dev
Fix API gatway bug and improve role trust processing
2 parents f415f99 + 00e63b0 commit 125c1ea

5 files changed

Lines changed: 341 additions & 143 deletions

File tree

aws/role-trusts.go

Lines changed: 130 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
package aws
22

33
import (
4-
"encoding/json"
5-
"errors"
64
"fmt"
7-
"net/url"
85
"path/filepath"
9-
"regexp"
106
"sort"
117
"strconv"
128
"strings"
139

1410
"github.com/BishopFox/cloudfox/aws/sdk"
1511
"github.com/BishopFox/cloudfox/internal"
12+
"github.com/BishopFox/cloudfox/internal/aws/policy"
1613
"github.com/aws/aws-sdk-go-v2/aws"
1714
"github.com/aws/aws-sdk-go-v2/service/iam"
18-
"github.com/aws/aws-sdk-go-v2/service/iam/types"
1915
"github.com/aws/aws-sdk-go-v2/service/sts"
2016
"github.com/bishopfox/knownawsaccountslookup"
2117
"github.com/sirupsen/logrus"
@@ -65,46 +61,12 @@ type RoleTrustRow struct {
6561

6662
type AnalyzedRole struct {
6763
roleARN *string
68-
trustsDoc trustPolicyDocument
64+
trustsDoc policy.TrustPolicyDocument
6965
// trustType string // UNUSED FIELD, PLEASE REVIEW
7066
Admin string
7167
CanPrivEsc string
7268
}
7369

74-
type trustPolicyDocument struct {
75-
Version string `json:"Version"`
76-
Statement []RoleTrustStatementEntry `json:"Statement"`
77-
}
78-
79-
type RoleTrustStatementEntry struct {
80-
Sid string `json:"Sid"`
81-
Effect string `json:"Effect"`
82-
Principal struct {
83-
AWS ListOfPrincipals `json:"AWS"`
84-
Service ListOfPrincipals `json:"Service"`
85-
Federated ListOfPrincipals `json:"Federated"`
86-
} `json:"Principal"`
87-
Action string `json:"Action"`
88-
Condition struct {
89-
StringEquals struct {
90-
StsExternalID string `json:"sts:ExternalId"`
91-
SAMLAud string `json:"SAML:aud"`
92-
OidcEksSub string `json:"OidcEksSub"`
93-
OidcEksAud string `json:"OidcEksAud"`
94-
CognitoAud string `json:"cognito-identity.amazonaws.com:aud"`
95-
} `json:"StringEquals"`
96-
StringLike struct {
97-
TokenActionsGithubusercontentComSub ListOfPrincipals `json:"token.actions.githubusercontent.com:sub"`
98-
TokenActionsGithubusercontentComAud string `json:"token.actions.githubusercontent.com:aud"`
99-
OidcEksSub string `json:"OidcEksSub"`
100-
OidcEksAud string `json:"OidcEksAud"`
101-
} `json:"StringLike"`
102-
ForAnyValueStringLike struct {
103-
CognitoAMR string `json:"cognito-identity.amazonaws.com:amr"`
104-
} `json:"ForAnyValue:StringLike"`
105-
} `json:"Condition"`
106-
}
107-
10870
func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int) {
10971
m.output.Verbosity = verbosity
11072
m.output.Directory = outputDirectory
@@ -140,6 +102,7 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
140102

141103
}
142104
}
105+
143106
o := internal.OutputClient{
144107
Verbosity: verbosity,
145108
CallingModule: m.output.CallingModule,
@@ -189,12 +152,12 @@ func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int
189152
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
190153
}
191154
if len(servicesBody) > 0 {
192-
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody)))
155+
fmt.Printf("[%s][%s] %s service role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody)))
193156
} else {
194157
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
195158
}
196159
if len(federatedBody) > 0 {
197-
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody)))
160+
fmt.Printf("[%s][%s] %s federated role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody)))
198161
} else {
199162
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
200163
}
@@ -373,7 +336,7 @@ func (m *RoleTrustsModule) printServiceTrusts(outputDirectory string) ([]string,
373336
tableCols = strings.Split(m.AWSTableCols, ",")
374337
// If the user specified wide as the output format, use these columns.
375338
} else if m.AWSOutputType == "wide" {
376-
tableCols = []string{"Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
339+
tableCols = []string{"Account", "Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
377340
// Otherwise, use the default columns for this module (brief)
378341
} else {
379342
tableCols = []string{"Role Name", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
@@ -441,7 +404,7 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
441404
tableCols = strings.Split(m.AWSTableCols, ",")
442405
// If the user specified wide as the output format, use these columns.
443406
} else if m.AWSOutputType == "wide" {
444-
tableCols = []string{"Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
407+
tableCols = []string{"Account", "Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
445408
// Otherwise, use the default columns for this module (brief)
446409
} else {
447410
tableCols = []string{"Role Name", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
@@ -454,23 +417,25 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
454417
for _, role := range m.AnalyzedRoles {
455418
for _, statement := range role.trustsDoc.Statement {
456419
if len(statement.Principal.Federated) > 0 {
457-
provider, subject := parseFederatedTrustPolicy(statement)
458-
RoleTrustRow := RoleTrustRow{
459-
RoleARN: aws.ToString(role.roleARN),
460-
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
461-
TrustedFederatedProvider: provider,
462-
TrustedFederatedSubject: subject,
463-
IsAdmin: role.Admin,
464-
CanPrivEsc: role.CanPrivEsc,
420+
provider, subjects := parseFederatedTrustPolicy(statement)
421+
for _, subject := range subjects {
422+
RoleTrustRow := RoleTrustRow{
423+
RoleARN: aws.ToString(role.roleARN),
424+
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
425+
TrustedFederatedProvider: provider,
426+
TrustedFederatedSubject: subject,
427+
IsAdmin: role.Admin,
428+
CanPrivEsc: role.CanPrivEsc,
429+
}
430+
body = append(body, []string{
431+
aws.ToString(m.Caller.Account),
432+
RoleTrustRow.RoleARN,
433+
RoleTrustRow.RoleName,
434+
RoleTrustRow.TrustedFederatedProvider,
435+
RoleTrustRow.TrustedFederatedSubject,
436+
RoleTrustRow.IsAdmin,
437+
RoleTrustRow.CanPrivEsc})
465438
}
466-
body = append(body, []string{
467-
aws.ToString(m.Caller.Account),
468-
RoleTrustRow.RoleARN,
469-
RoleTrustRow.RoleName,
470-
RoleTrustRow.TrustedFederatedProvider,
471-
RoleTrustRow.TrustedFederatedSubject,
472-
RoleTrustRow.IsAdmin,
473-
RoleTrustRow.CanPrivEsc})
474439
}
475440

476441
}
@@ -481,47 +446,120 @@ func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]strin
481446

482447
}
483448

484-
func parseFederatedTrustPolicy(statement RoleTrustStatementEntry) (string, string) {
485-
var column2, column3 string
486-
if statement.Condition.StringLike.TokenActionsGithubusercontentComAud != "" || len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
487-
column2 = "GitHub Actions" // (" + statement.Principal.Federated[0] + ")"
488-
trustedRepos := strings.Join(statement.Condition.StringLike.TokenActionsGithubusercontentComSub, "\n")
489-
if trustedRepos == "" {
490-
column3 = "ALL REPOS!!!"
449+
func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string, []string) {
450+
var provider string
451+
var subjects []string
452+
if len(statement.Principal.Federated) > 1 {
453+
sharedLogger.Warnf("Multiple federated providers found in the trust policy. This is not currently supported. Please review the trust policy for specifics.")
454+
provider = "Multiple Federated Providers"
455+
subjects = append(subjects, "Review policy for specifics\nand submit issue to cloudfox repo.")
456+
}
457+
458+
switch {
459+
// lets use the Federated field to determine the provider, then based on the provider we can grab the list of subjects
460+
case strings.Contains(statement.Principal.Federated[0], "token.actions.githubusercontent.com"):
461+
provider = "GitHub"
462+
if len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
463+
subjects = append(subjects, statement.Condition.StringLike.TokenActionsGithubusercontentComSub...)
464+
} else if len(statement.Condition.StringEquals.TokenActionsGithubusercontentComSub) > 0 {
465+
subjects = append(subjects, statement.Condition.StringEquals.TokenActionsGithubusercontentComSub...)
491466
} else {
492-
column3 = trustedRepos
467+
subjects = append(subjects, "ALL REPOS!!!")
493468
}
494-
} else if statement.Condition.StringEquals.SAMLAud == "https://signin.aws.amazon.com/saml" {
495-
if strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
496-
column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
497-
} else if strings.Contains(statement.Principal.Federated[0], "Okta") {
498-
column2 = "Okta" // (" + statement.Principal.Federated[0] + ")"
469+
case strings.Contains(statement.Principal.Federated[0], "oidc.eks"):
470+
provider = "EKS"
471+
if len(statement.Condition.StringLike.OidcEksSub) > 0 {
472+
subjects = append(subjects, statement.Condition.StringLike.OidcEksSub...)
473+
} else if len(statement.Condition.StringEquals.OidcEksSub) > 0 {
474+
subjects = append(subjects, statement.Condition.StringEquals.OidcEksSub...)
475+
} else {
476+
subjects = append(subjects, "ALL SERVICE ACCOUNTS!!!")
477+
}
478+
// terraform case
479+
case strings.Contains(statement.Principal.Federated[0], "app.terraform.io"):
480+
provider = "Terraform Cloud"
481+
if len(statement.Condition.StringLike.TerraformSub) > 0 {
482+
subjects = append(subjects, statement.Condition.StringLike.TerraformSub...)
483+
} else if len(statement.Condition.StringEquals.TerraformSub) > 0 {
484+
subjects = append(subjects, statement.Condition.StringEquals.TerraformSub...)
485+
} else {
486+
subjects = append(subjects, "ALL WORKSPACES")
499487
}
500-
column3 = "Not applicable"
501-
} else if statement.Condition.StringEquals.OidcEksAud != "" || statement.Condition.StringEquals.OidcEksSub != "" || statement.Condition.StringLike.OidcEksAud != "" || statement.Condition.StringLike.OidcEksSub != "" {
502-
column2 = "EKS" // (" + statement.Principal.Federated[0] + ")"
503-
if statement.Condition.StringEquals.OidcEksSub != "" {
504-
column3 = statement.Condition.StringEquals.OidcEksSub
505-
} else if statement.Condition.StringLike.OidcEksSub != "" {
506-
column3 = statement.Condition.StringLike.OidcEksSub
488+
// Azure AD case
489+
case strings.Contains(statement.Principal.Federated[0], "http://sts.windows.net"):
490+
provider = "Azure AD"
491+
if len(statement.Condition.StringLike.AzureADIss) > 0 {
492+
subjects = append(subjects, statement.Condition.StringLike.AzureADIss...)
493+
} else if len(statement.Condition.StringEquals.AzureADIss) > 0 {
494+
subjects = append(subjects, statement.Condition.StringEquals.AzureADIss...)
507495
} else {
508-
column3 = "ALL SERVICE ACCOUNTS!"
496+
subjects = append(subjects, "ALL ISSUERS")
509497
}
510-
} else if statement.Principal.Federated[0] == "cognito-identity.amazonaws.com" {
511-
column2 = "Cognito" // (" + statement.Principal.Federated[0] + ")"
498+
499+
// AWS SSO case
500+
case strings.Contains(statement.Principal.Federated[0], "AWSSSO"):
501+
provider = "AWS SSO"
502+
subjects = append(subjects, "Not applicable")
503+
504+
// okta case
505+
case strings.Contains(statement.Principal.Federated[0], "Okta"):
506+
provider = "Okta"
507+
subjects = append(subjects, "Not applicable")
508+
509+
// cognito case
510+
case statement.Principal.Federated[0] == "cognito-identity.amazonaws.com":
511+
provider = "Cognito"
512512
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
513-
column3 = statement.Condition.ForAnyValueStringLike.CognitoAMR
513+
subjects = append(subjects, statement.Condition.ForAnyValueStringLike.CognitoAMR)
514+
} else {
515+
subjects = append(subjects, "ALL IDENTITIES")
514516
}
515-
} else {
516-
if column2 == "" && strings.Contains(statement.Principal.Federated[0], "oidc.eks") {
517-
column2 = "EKS" // (" + statement.Principal.Federated[0] + ")"
518-
column3 = "ALL SERVICE ACCOUNTS!"
519-
} else if column2 == "" && strings.Contains(statement.Principal.Federated[0], "AWSSSO") {
520-
column2 = "AWS SSO" // (" + statement.Principal.Federated[0] + ")"
517+
// google workspace case
518+
case strings.Contains(statement.Principal.Federated[0], "workspace.google.com"):
519+
provider = "Google Workspace"
520+
if len(statement.Condition.StringLike.GoogleWorkspaceSub) > 0 {
521+
subjects = append(subjects, statement.Condition.StringLike.GoogleWorkspaceSub...)
522+
} else if len(statement.Condition.StringEquals.GoogleWorkspaceSub) > 0 {
523+
subjects = append(subjects, statement.Condition.StringEquals.GoogleWorkspaceSub...)
524+
} else {
525+
subjects = append(subjects, "ALL USERS")
526+
}
527+
// GCP case
528+
case strings.Contains(statement.Principal.Federated[0], "accounts.google.com"):
529+
provider = "GCP"
530+
if len(statement.Condition.StringLike.GCPSub) > 0 {
531+
subjects = append(subjects, statement.Condition.StringLike.GCPSub...)
532+
} else if len(statement.Condition.StringEquals.GCPSub) > 0 {
533+
subjects = append(subjects, statement.Condition.StringEquals.GCPSub...)
534+
} else {
535+
subjects = append(subjects, "ALL USERS")
521536
}
537+
// auth0 case
538+
//not ready yet
539+
// case strings.Contains(statement.Principal.Federated[0], "auth0.com"):
540+
// provider = "Auth0"
541+
// if len(statement.Condition.ForAnyValueStringLike.Auth0Amr) > 0 {
542+
// subjects = append(subjects, statement.Condition.ForAnyValueStringLike.Auth0Amr...)
543+
// } else {
544+
// subjects = append(subjects, "ALL GROUPS")
545+
// }
546+
// circleci case
547+
case strings.Contains(statement.Principal.Federated[0], "oidc.circleci.com"):
548+
provider = "CircleCI"
549+
if len(statement.Condition.StringLike.CircleCIAud) > 0 {
550+
subjects = append(subjects, statement.Condition.StringLike.CircleCIAud...)
551+
} else if len(statement.Condition.StringEquals.CircleCIAud) > 0 {
552+
subjects = append(subjects, statement.Condition.StringEquals.CircleCIAud...)
553+
} else {
554+
subjects = append(subjects, "ALL PROJECTS")
555+
}
556+
557+
default:
558+
provider = "Unknown Federated Provider"
559+
subjects = append(subjects, "Review policy for specifics\nand submit issue to cloudfox repo.")
522560

523561
}
524-
return column2, column3
562+
return provider, subjects
525563
}
526564

527565
func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
@@ -541,7 +579,7 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
541579
}
542580

543581
for _, role := range ListRoles {
544-
trustsdoc, err := parseRoleTrustPolicyDocument(role)
582+
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
545583
if err != nil {
546584
m.modLog.Error(err.Error())
547585
m.CommandCounter.Error++
@@ -561,40 +599,3 @@ func (m *RoleTrustsModule) getAllRoleTrusts() {
561599
}
562600

563601
}
564-
565-
func parseRoleTrustPolicyDocument(role types.Role) (trustPolicyDocument, error) {
566-
document, _ := url.QueryUnescape(aws.ToString(role.AssumeRolePolicyDocument))
567-
568-
// These next six lines are a hack, needed because the EKS OIDC json field name is dynamic
569-
// and therefore can't be used to unmarshall in a predictable way. The hack involves replacing
570-
// the random pattern with a predictable one so that we can add the predictable one in the struct
571-
// used to unmarshall.
572-
pattern := `(\w+)\:`
573-
pattern2 := `".[a-zA-Z0-9\-\.]+/id/`
574-
var reEKSSub = regexp.MustCompile(pattern2 + pattern + "sub")
575-
var reEKSAud = regexp.MustCompile(pattern2 + pattern + "aud")
576-
document = reEKSSub.ReplaceAllString(document, "\"OidcEksSub")
577-
document = reEKSAud.ReplaceAllString(document, "\"OidcEksAud")
578-
579-
var parsedDocumentToJSON trustPolicyDocument
580-
_ = json.Unmarshal([]byte(document), &parsedDocumentToJSON)
581-
return parsedDocumentToJSON, nil
582-
}
583-
584-
// A custom unmarshaller is necessary because the list of principals can be an array of strings or a string.
585-
// https://stackoverflow.com/questions/65854778/parsing-arn-from-iam-policy-using-regex
586-
type ListOfPrincipals []string
587-
588-
func (r *ListOfPrincipals) UnmarshalJSON(b []byte) error {
589-
var s string
590-
if err := json.Unmarshal(b, &s); err == nil {
591-
*r = append(*r, s)
592-
return nil
593-
}
594-
var ss []string
595-
if err := json.Unmarshal(b, &ss); err == nil {
596-
*r = ss
597-
return nil
598-
}
599-
return errors.New("cannot unmarshal neither to a string nor a slice of strings")
600-
}

0 commit comments

Comments
 (0)