Skip to content

Commit

Permalink
New GCP attack: Inviting an external user (#372)
Browse files Browse the repository at this point in the history
  • Loading branch information
christophetd committed Jun 22, 2023
1 parent d340248 commit 1b3b265
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 102 deletions.
103 changes: 103 additions & 0 deletions docs/attack-techniques/GCP/gcp.persistence.invite-external-user.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
title: Invite an External User to a GCP Project
---

# Invite an External User to a GCP Project


<span class="smallcaps w3-badge w3-blue w3-round w3-text-white" title="This attack technique can be detonated multiple times">idempotent</span>

Platform: GCP

## MITRE ATT&CK Tactics


- Persistence

## Description


Persists in the GCP project by inviting an external (fictitious) user to the project. The attacker could then use the external user to access the project.

<span style="font-variant: small-caps;">Warm-up</span>: None

<span style="font-variant: small-caps;">Detonation</span>:

- Updates the project IAM policy to grant the attacker account the role of <code>roles/editor</code>

!!! note

Since the target e-mail must exist for this attack simulation to work, Stratus Red Team grants the role to stratusredteam@gmail.com by default.
This is a real Google account, owned by Stratus Red Team maintainers and that is not used for any other purpose than this attack simulation. However, you can override
this behavior by setting the environment variable <code>STRATUS_RED_TEAM_ATTACKER_EMAIL</code>, for instance:

```bash
export STRATUS_RED_TEAM_ATTACKER_EMAIL="your-own-gmail-account@gmail.com"
stratus detonate gcp.persistence.invite-external-user
```


## Instructions

```bash title="Detonate with Stratus Red Team"
stratus detonate gcp.persistence.invite-external-user
```
## Detection


The Google Cloud Admin logs event <code>SetIamPolicy</code> is generated when a principal is granted non-owner permissions at the project level.

```javascript hl_lines="5 11 12 13"
{
"protoPayload": {
"@type": "type.googleapis.com/google.cloud.audit.AuditLog",
"serviceName": "cloudresourcemanager.googleapis.com",
"methodName": "SetIamPolicy",
"serviceData": {
"@type": "type.googleapis.com/google.iam.v1.logging.AuditData",
"policyDelta": {
"bindingDeltas": [
{
"action": "ADD",
"role": "roles/editor",
"member": "user:stratusredteam@gmail.com"
}
]
}
},
"request": {
"resource": "target-project",
"policy": {
// ...
},
"@type": "type.googleapis.com/google.iam.v1.SetIamPolicyRequest"
}
}
}
```

Although this attack technique does not simulate it, an attacker can also
<a href="https://support.google.com/googleapi/answer/6158846?hl=en">use the GCP console to invite an external user as owner</a> of a GCP project,
which cannot be done through the SetIamPolicy API call. In that case, an <code>InsertProjectOwnershipInvite</code> event is generated:

```json hl_lines="5 8"
{
"protoPayload": {
"@type": "type.googleapis.com/google.cloud.audit.AuditLog",
"serviceName": "cloudresourcemanager.googleapis.com",
"methodName": "InsertProjectOwnershipInvite",
"resourceName": "projects/target-project",
"request": {
"member": "user:attacker@gmail.com",
"projectId": "target-project",
"@type": "type.googleapis.com/google.internal.cloud.resourcemanager.InsertProjectOwnershipInviteRequest"
},
"response": {
"@type": "type.googleapis.com/google.internal.cloud.resourcemanager.InsertProjectOwnershipInviteResponse"
}
}
}
```



2 changes: 2 additions & 0 deletions docs/attack-techniques/GCP/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Note that some Stratus attack techniques may correspond to more than a single AT

- [Create a GCP Service Account Key](./gcp.persistence.create-service-account-key.md)

- [Invite an External User to a GCP Project](./gcp.persistence.invite-external-user.md)


## Privilege Escalation

Expand Down
1 change: 1 addition & 0 deletions docs/attack-techniques/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ This page contains the list of all Stratus Attack Techniques.
| [Exfiltrate Compute Disk by sharing it](./GCP/gcp.exfiltration.share-compute-disk.md) | [GCP](./GCP/index.md) | Exfiltration |
| [Create an Admin GCP Service Account](./GCP/gcp.persistence.create-admin-service-account.md) | [GCP](./GCP/index.md) | Persistence, Privilege Escalation |
| [Create a GCP Service Account Key](./GCP/gcp.persistence.create-service-account-key.md) | [GCP](./GCP/index.md) | Persistence, Privilege Escalation |
| [Invite an External User to a GCP Project](./GCP/gcp.persistence.invite-external-user.md) | [GCP](./GCP/index.md) | Persistence |
| [Impersonate GCP Service Accounts](./GCP/gcp.privilege-escalation.impersonate-service-accounts.md) | [GCP](./GCP/index.md) | Privilege Escalation |
| [Dump All Secrets](./kubernetes/k8s.credential-access.dump-secrets.md) | [Kubernetes](./kubernetes/index.md) | Credential Access |
| [Steal Pod Service Account Token](./kubernetes/k8s.credential-access.steal-serviceaccount-token.md) | [Kubernetes](./kubernetes/index.md) | Credential Access |
Expand Down
7 changes: 7 additions & 0 deletions docs/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,13 @@ GCP:
- Privilege Escalation
platform: GCP
isIdempotent: false
- id: gcp.persistence.invite-external-user
name: Invite an External User to a GCP Project
isSlow: false
mitreAttackTactics:
- Persistence
platform: GCP
isIdempotent: true
Privilege Escalation:
- id: gcp.persistence.create-admin-service-account
name: Create an Admin GCP Service Account
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import (
"errors"
"fmt"
"github.com/datadog/stratus-red-team/v2/internal/providers"
gcp_utils "github.com/datadog/stratus-red-team/v2/internal/utils/gcp"
"github.com/datadog/stratus-red-team/v2/pkg/stratus"
"github.com/datadog/stratus-red-team/v2/pkg/stratus/mitreattack"
cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1"
iam "google.golang.org/api/iam/v1"

"log"
)

Expand Down Expand Up @@ -49,7 +50,6 @@ Using the following GCP Admin Activity audit logs events:
})
}

// Note: `roles/owner` cannot be granted through the API
const roleToGrant = "roles/owner"

func detonate(params map[string]string, providers stratus.CloudProviders) error {
Expand All @@ -61,7 +61,7 @@ func detonate(params map[string]string, providers stratus.CloudProviders) error
return err
}

if err := assignProjectRole(gcp, serviceAccountEmail, roleToGrant); err != nil {
if err := gcp_utils.GCPAssignProjectRole(gcp, "serviceAccountEmail:"+serviceAccountEmail, roleToGrant); err != nil {
return err
}

Expand Down Expand Up @@ -90,103 +90,21 @@ func createServiceAccount(gcp *providers.GCPProvider, serviceAccountName string)
return nil
}

// assignProjectRole grants a project-wide role to a specific service account
// it works the same as 'gcloud projects add-iam-policy-binding':
// * Step 1: Read the project's IAM policy using [getIamPolicy](https://cloud.google.com/resource-manager/reference/rest/v1/projects/getIamPolicy)
// * Step 2: Create a binding, or add the service account to an existing binding for the role to grant
// * Step 3: Update the project's IAM policy using [setIamPolicy](https://cloud.google.com/resource-manager/reference/rest/v1/projects/setIamPolicy)
func assignProjectRole(gcp *providers.GCPProvider, serviceAccountEmail string, roleToGrant string) error {
resourceManager, err := cloudresourcemanager.NewService(context.Background(), gcp.Options())
if err != nil {
return errors.New("unable to instantiate the GCP cloud resource manager: " + err.Error())
}

projectPolicy, err := resourceManager.Projects.GetIamPolicy(gcp.GetProjectId(), &cloudresourcemanager.GetIamPolicyRequest{}).Do()
if err != nil {
return err
}
var bindingFound = false
bindingValue := fmt.Sprintf("serviceAccount:" + serviceAccountEmail)
for _, binding := range projectPolicy.Bindings {
if binding.Role == roleToGrant {
bindingFound = true
log.Println("Adding the service account to an existing binding in the project's IAM policy to grant " + roleToGrant)
binding.Members = append(binding.Members, bindingValue)
}
}
if !bindingFound {
log.Println("Creating a new binding in the project's IAM policy to grant " + roleToGrant)
projectPolicy.Bindings = append(projectPolicy.Bindings, &cloudresourcemanager.Binding{
Role: roleToGrant,
Members: []string{bindingValue},
})
}

_, err = resourceManager.Projects.SetIamPolicy(gcp.GetProjectId(), &cloudresourcemanager.SetIamPolicyRequest{
Policy: projectPolicy,
}).Do()

if err != nil {
return fmt.Errorf("Failed to update project IAM policy: " + err.Error())
}
return nil
}

func revert(params map[string]string, providers stratus.CloudProviders) error {
gcp := providers.GCP()
serviceAccountName := params["service_account_name"]
serviceAccountEmail := getServiceAccountEmail(serviceAccountName, gcp.ProjectId)

// Attempt to remove the role from the service account in the project's IAM policy
// fail with a warning (but continue) in case of error
unassignProjectRole(gcp, serviceAccountEmail, roleToGrant)
if err := gcp_utils.GCPUnassignProjectRole(gcp, "serviceAccount:"+serviceAccountEmail, roleToGrant); err != nil {
// display a warning (but continue) in case of error
log.Println("Warning: unable to remove role from service account: " + err.Error())
}

// Remove service account itself
return removeServiceAccount(gcp, serviceAccountName)
}

// unassignProjectRole un-assigns a project-wide role to a specific service account
// it works the same as 'gcloud projects remove-iam-policy-binding':
// * Step 1: Read the project's IAM policy using [getIamPolicy](https://cloud.google.com/resource-manager/reference/rest/v1/projects/getIamPolicy)
// * Step 2: Remove a binding, or remove the service account from an existing binding for the role to grant
// * Step 3: Update the project's IAM policy using [setIamPolicy](https://cloud.google.com/resource-manager/reference/rest/v1/projects/setIamPolicy)
func unassignProjectRole(gcp *providers.GCPProvider, serviceAccountEmail string, roleToGrant string) {
resourceManager, err := cloudresourcemanager.NewService(context.Background(), gcp.Options())
if err != nil {
log.Println("Warning: unable to instantiate the GCP cloud resource manager: " + err.Error())
return
}

projectPolicy, err := resourceManager.Projects.GetIamPolicy(gcp.GetProjectId(), &cloudresourcemanager.GetIamPolicyRequest{}).Do()
if err != nil {
log.Println("warning: unable to retrieve the project's IAM policy")
return
}
var bindingFound = false
bindingValue := fmt.Sprintf("serviceAccount:" + serviceAccountEmail)
for _, binding := range projectPolicy.Bindings {
if binding.Role == roleToGrant {
index := indexOf(binding.Members, bindingValue)
if index > -1 {
bindingFound = true
binding.Members = remove(binding.Members, index)
}
}
}
if bindingFound {
log.Println("Updating project's IAM policy to remove reference to the service account")
_, err := resourceManager.Projects.SetIamPolicy(gcp.GetProjectId(), &cloudresourcemanager.SetIamPolicyRequest{
Policy: projectPolicy,
}).Do()
if err != nil {
log.Println("Warning: unable to update project's IAM policy: " + err.Error())
}
} else {
log.Println("Warning: did not find reference to the service account in the project's IAM policy")
}

}

func removeServiceAccount(gcp *providers.GCPProvider, serviceAccountName string) error {
iamClient, err := iam.NewService(context.Background(), gcp.Options())
if err != nil {
Expand All @@ -210,16 +128,3 @@ func getServiceAccountPath(name string, projectId string) string {
func getServiceAccountEmail(name string, projectId string) string {
return fmt.Sprintf("%s@%s.iam.gserviceaccount.com", name, projectId)
}

func remove(slice []string, index int) []string {
return append(slice[:index], slice[index+1:]...)
}

func indexOf(slice []string, searchValue string) int {
for i, current := range slice {
if current == searchValue {
return i
}
}
return -1
}
Loading

0 comments on commit 1b3b265

Please sign in to comment.