Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Technique: Access Virtual Machine using Bastion shareable link #583

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: Access Virtual Machine using Bastion shareable link
---

# Access Virtual Machine using Bastion shareable link

<span class="smallcaps w3-badge w3-orange w3-round w3-text-sand" title="This attack technique is slow to warm up and cleanup">slow</span>


Platform: Azure

## MITRE ATT&CK Tactics


- Persistence

## Description


By utilizing the 'shareable link' feature on Bastions where it is enabled, an attacker can create a link to allow access to a virtual machine (VM) from untrusted networks. Public links generated for an Azure Bastion can allow VM network access to anyone with the generated URL.
NOTE: This technique will take 10-15 minutes to warmup, and 10-15 minutes to cleanup. This is due to the time to deploy an Azure Bastion.

References:

- https://blog.karims.cloud/2022/11/26/yet-another-azure-vm-persistence.html
- https://learn.microsoft.com/en-us/azure/bastion/shareable-link
- https://microsoft.github.io/Azure-Threat-Research-Matrix/Persistence/AZT509/AZT509/

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

- Create a VM and VNet
- Create an Azure Bastion host with access to the VM, and shareable links enabled
NOTE: Warm-up and cleanup can each take 10-15 minutes to create and destroy the Azure Bastion instance

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

- Create an Azure Bastion shareable link with access to the VM

## Instructions

```bash title="Detonate with Stratus Red Team"
stratus detonate azure.persistence.bastion-shareable-link
```
## Detection

Identify Azure events of type <code>Microsoft.Network/bastionHosts/createshareablelinks/action</code> and <code>Microsoft.Network/bastionHosts/getShareablelinks/action</code>. A sample of <code>createshareablelinks</code> is shown below (redacted for clarity).

```json hl_lines="7"
{
"category": {
"value": "Administrative",
"localizedValue": "Administrative"
},
"level": "Informational",
"operationName": {
"value": "Microsoft.Network/bastionHosts/createshareablelinks/action",
"localizedValue": "Creates shareable urls for the VMs under a bastion and returns the urls"
},
"resourceGroupName": "stratus-red-team-shareable-link-rg-tz6o",
"resourceProviderName": {
"value": "Microsoft.Network",
"localizedValue": "Microsoft.Network"
},
"resourceType": {
"value": "Microsoft.Network/bastionHosts",
"localizedValue": "Microsoft.Network/bastionHosts"
},
"resourceId": "[removed]/resourceGroups/stratus-red-team-shareable-link-rg-tz6o/providers/Microsoft.Network/bastionHosts/stratus-red-team-shareable-link-bastion-tz6o",
"status": {
"value": "Succeeded",
"localizedValue": "Succeeded"
},
"subStatus": {
"value": "",
"localizedValue": ""
},
"properties": {
"eventCategory": "Administrative",
"entity": "[removed]/resourceGroups/stratus-red-team-shareable-link-rg-tz6o/providers/Microsoft.Network/bastionHosts/stratus-red-team-shareable-link-bastion-tz6o",
"message": "Microsoft.Network/bastionHosts/createshareablelinks/action",
"hierarchy": "[removed]"
},
}
```
3 changes: 2 additions & 1 deletion v2/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
github.com/aws/aws-sdk-go-v2 v1.30.3
github.com/aws/aws-sdk-go-v2/config v1.25.11
github.com/aws/aws-sdk-go-v2/credentials v1.16.9
Expand Down Expand Up @@ -47,6 +47,7 @@ require (

require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.1.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
Expand Down
4 changes: 4 additions & 0 deletions v2/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0/go.mod h1:gM3K25LQlsET3QR+4V74zxCsFAy0r6xMNN9n80SZn+4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0 h1:lMW1lD/17LUA5z1XTURo7LcVG2ICBPlyMHjIUrcFZNQ=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.0.0 h1:nBy98uKOIfun5z6wx6jwWLrULcM0+cjBalBFZlEZ7CA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.1.0 h1:Fd+iaEa+JBwzYo6OTWYSNqyvlPSLciMGsmsnYCKcXM0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6 v6.1.0/go.mod h1:ulHyBFJOI0ONiRL4vcJTmS7rx18jQQlEPmAgo80cRdM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0/go.mod h1:s1tW/At+xHqjNFvWU4G0c0Qv33KOhvbGNj0RCTQDV8s=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package azure

import (
"context"
_ "embed"
"github.com/datadog/stratus-red-team/v2/pkg/stratus"
"github.com/datadog/stratus-red-team/v2/pkg/stratus/mitreattack"
"log"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"
"fmt"
)

//go:embed main.tf
var tf []byte

func init() {
const codeBlock = "```"
stratus.GetRegistry().RegisterAttackTechnique(&stratus.AttackTechnique{
ID: "azure.persistence.bastion-shareable-link",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something more action oriented like the below might work better?

Suggested change
ID: "azure.persistence.bastion-shareable-link",
ID: "azure.persistence.create-bastion-sharing-link",

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping "shareable" if we can as this is the feature name, but can use "sharing" if we're up against a character limit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah makes sense, so wdyt of azure.persistence.create-bastion-shareable-link?

FriendlyName: "Access Virtual Machine using Bastion shareable link",
Description: `
By utilizing the 'shareable link' feature on Bastions where it is enabled, an attacker can create a link to allow access to a virtual machine (VM) from untrusted networks. Public links generated for an Azure Bastion can allow VM network access to anyone with the generated URL.

References:

- https://blog.karims.cloud/2022/11/26/yet-another-azure-vm-persistence.html
- https://learn.microsoft.com/en-us/azure/bastion/shareable-link
- https://microsoft.github.io/Azure-Threat-Research-Matrix/Persistence/AZT509/AZT509/

Warm-up:

- Create a VM and VNet
- Create an Azure Bastion host with access to the VM, and shareable links enabled
NOTE: Warm-up and cleanup can each take 10-15 minutes to create and destroy the Azure Bastion instance

Detonation:

- Create an Azure Bastion shareable link with access to the VM
`,
Detection: `
Identify Azure events of type <code>Microsoft.Network/bastionHosts/createshareablelinks/action</code> and <code>Microsoft.Network/bastionHosts/getShareablelinks/action</code>. A sample of <code>createshareablelinks</code> is shown below (redacted for clarity).

` + codeBlock + `json hl_lines="7"
{
{
"category": {
"value": "Administrative",
"localizedValue": "Administrative"
},
"level": "Informational",
"operationName": {
"value": "Microsoft.Network/bastionHosts/createshareablelinks/action",
"localizedValue": "Creates shareable urls for the VMs under a bastion and returns the urls"
},
"resourceGroupName": "stratus-red-team-shareable-link-rg-tz6o",
"resourceProviderName": {
"value": "Microsoft.Network",
"localizedValue": "Microsoft.Network"
},
"resourceType": {
"value": "Microsoft.Network/bastionHosts",
"localizedValue": "Microsoft.Network/bastionHosts"
},
"resourceId": "[removed]/resourceGroups/stratus-red-team-shareable-link-rg-tz6o/providers/Microsoft.Network/bastionHosts/stratus-red-team-shareable-link-bastion-tz6o",
"status": {
"value": "Succeeded",
"localizedValue": "Succeeded"
},
"subStatus": {
"value": "",
"localizedValue": ""
},
"properties": {
"eventCategory": "Administrative",
"entity": "[removed]/resourceGroups/stratus-red-team-shareable-link-rg-tz6o/providers/Microsoft.Network/bastionHosts/stratus-red-team-shareable-link-bastion-tz6o",
"message": "Microsoft.Network/bastionHosts/createshareablelinks/action",
"hierarchy": "[removed]"
},
}
` + codeBlock + `
`,
Platform: stratus.Azure,
IsSlow: true,
IsIdempotent: false,
MitreAttackTactics: []mitreattack.Tactic{mitreattack.Persistence},
PrerequisitesTerraformCode: tf,
Detonate: detonate,
Revert: revert,
})
}

func detonate(params map[string]string, providers stratus.CloudProviders) error {
bastionName := params["bastion_name"]
resourceGroup := params["resource_group_name"]
vmId := params["vm_id"]
vmName := params["vm_name"]
tenantId := params["tenant_id"]

ctx := context.Background()
cred := providers.Azure().GetCredentials()
subscriptionID := providers.Azure().SubscriptionID
clientOptions := providers.Azure().ClientOptions

client, err := armnetwork.NewClientFactory(subscriptionID, cred, clientOptions)
if err != nil {
log.Fatalf("failed to create client: %v", err)
}

// Create Bastion shareable link
// Reference method: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/put-bastion-shareable-link/put-bastion-shareable-link
log.Println("Getting Bastion shareable link for VM " +vmName)

poller, err := client.NewManagementClient().BeginPutBastionShareableLink(ctx, resourceGroup, bastionName, armnetwork.BastionShareableLinkListRequest{
VMs: []*armnetwork.BastionShareableLink{
{
VM: &armnetwork.VM{
ID: to.Ptr(vmId),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ID: to.Ptr(vmId),
ID: &vmId,

should be enough?

},
},
},
}, nil)
if err != nil {
log.Fatalf("failed to create shareable link: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we generally try to return errors here (i.e. return fmt.Errorf("failed to create shareable link: %v", err);)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.Fatalf("failed to create shareable link: %v", err)
return fmt.Errorf("failed to create shareable link: %v", err)

}

_, err = poller.PollUntilDone(ctx, nil)
if err != nil {
log.Fatalf("failed to poll results: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.Fatalf("failed to poll results: %v", err)
return fmt.Errorf("failed to retrieve shareable link: %v", err)

}
log.Println("Shareable link created")

// Provide URL to access Bastion shareable link
// NOTE: Response via Go SDK methods does not return any page contents, so we'll supply a Portal URL to fetch the link for now. (The example cited in reference link above is not clear on how to resolve this.)
url := fmt.Sprintln("https://portal.azure.com/#@" + tenantId + "/resource/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/bastionHosts/" + bastionName + "/shareablelinks")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds like Sprintf would be a bit cleaner to build this with %s ?


log.Println("You can view and fetch the shareable link URL here: " + url)

return nil
}

func revert(params map[string]string, providers stratus.CloudProviders) error {
// Reference method: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/delete-bastion-shareable-link/delete-bastion-shareable-link?view=rest-virtualnetwork-2024-03-01&tabs=Go
bastionName := params["bastion_name"]
resourceGroup := params["resource_group_name"]
vmId := params["vm_id"]
vmName := params["vm_name"]

ctx := context.Background()
cred := providers.Azure().GetCredentials()
subscriptionID := providers.Azure().SubscriptionID
clientOptions := providers.Azure().ClientOptions

client, err := armnetwork.NewClientFactory(subscriptionID, cred, clientOptions)
if err != nil {
log.Fatalf("failed to create client: %v", err)
}

// Delete shareable link that was previously created
log.Println("Deleting shareable Bastion link to VM " + vmName)

poller, err := client.NewManagementClient().BeginDeleteBastionShareableLink(ctx, resourceGroup, bastionName, armnetwork.BastionShareableLinkListRequest{
VMs: []*armnetwork.BastionShareableLink{
{
VM: &armnetwork.VM{
ID: to.Ptr(vmId),
},
},
},
}, nil)
if err != nil {
log.Fatalf("failed to finish the request: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.Fatalf("failed to finish the request: %v", err)
return fmt.Errorf("failed to delete shareable bastion link: %v", err)

}
_, err = poller.PollUntilDone(ctx, nil)
if err != nil {
log.Fatalf("failed to pull the result: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.Fatalf("failed to pull the result: %v", err)
return fmt.Errorf("failed to retrieve shareable bastion link deletion result: %v", err)

}

log.Println("Shareable link deleted")

return nil
}
Loading
Loading