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

Hosted instance requires MFA #12

Open
sdubin2 opened this issue Nov 15, 2021 · 14 comments
Open

Hosted instance requires MFA #12

sdubin2 opened this issue Nov 15, 2021 · 14 comments

Comments

@sdubin2
Copy link

sdubin2 commented Nov 15, 2021

"To protect your users, you are required to turn on two-factor authentication for all of your internal users."

Your module says: "Requires an account without MFA. Use a complex username and password."

Will you be able to implement MFA into your module?

@tcsi-github
Copy link

I am curious about this as well. Is there a plan or workaround for this?

@mrmattipants
Copy link

While Researching & Investigating potential MFA/2FA related processes, I believe that I may have stumbled upon what are essentially API Endpoints, in the “script.ashx” File, that is associated with the current Control Login Page.

In fact, these EndPoints are extremely similar to those found in the following ConnectWise Control Documentation.

ConnectWise Control - External API Calls:
https://docs.connectwise.com/ConnectWise_Control_Documentation/Developers/External_API_calls_to_ConnectWise_Control

With that being said, I thought I’d upload the “Script.ashx” File to my own Public Repo, for your Review, as I thought that it might just help your Team, as far as the development of an MFA/2FA Supported Module, which is something that is desperately needed by many.

ConnectWise Control - Script.ashx:
https://raw.githubusercontent.com/mrmattipants/ConnectWiseControlAPI/main/Script.ashx

In particular, I’d like to direct your gaze to the following “TryLogin” Function, which contains a “oneTimePassword” Parameter, among a few others, which appear to be MFA/2FA Related.

TryLogin":function (userName, password, oneTimePassword, shouldTrust, securityNonce, onSuccess, onFailure, userContext, userNameOverride, passwordOverride) { return SC.http.invokeService('Services/AuthenticationService.ashx', 'TryLogin', [userName, password, oneTimePassword, shouldTrust, securityNonce], onSuccess, onFailure, userContext, userNameOverride, passwordOverride);

Regardless of whether you can utilize this Information or Not, I figured that it was, at the very least, something worth sharing.

@Luke-Williams9
Copy link

Luke-Williams9 commented Aug 29, 2022

I've been working on this today, and I think I got it. You can still use the same basic auth that the module uses, but add 'X-One-Time-Password' to the header, with the 6 digit OTP as its value.

The module needs an extra function like 'Get-GoogleAuthenticatorPin' from this module:
https://github.com/HumanEquivalentUnit/PowerShell-Misc/blob/master/GoogleAuthenticator.psm1

I'm still testing, but the header of each request sent to CWC needs to have the current OTP in it.

All of this are modifications I've been adding to this module locally. I'd be happy to submit the changes here if you want - I'm just new to working with git repositories in general, so I'm not sure how to go about it.

@Luke-Williams9
Copy link

Please update this module to support MFA - its basically useless now without. Here is what I manually did to your module, to make it work:

Modified /Public/Authentication/Connect-CWC.ps1

function Connect-CWC {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Server,
        [Parameter(Mandatory = $True)]
        [pscredential]$Credentials,
        [string]$secret,
        [switch]$Force
    )

    if ($script:CWCServerConnection -and !$Force) {
        Write-Verbose "Using cached Authentication information."
        return
    }

    $Server = $Server -replace("http.*:\/\/",'')
    $EncodedCredentials = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("$($Credentials.UserName):$($Credentials.GetNetworkCredential().Password)"))
    $Headers = @{
        'authorization' = "Basic $EncodedCredentials"
        'content-type' = "application/json; charset=utf-8"
        'X-One-Time-Password' = (Get-OTP $secret).code
        'origin' = "https://$Server"
    }

    $FrontPage = Invoke-WebRequest -Uri $Headers.origin -Headers $Headers -UseBasicParsing
    $Regex = [Regex]'(?<=antiForgeryToken":")(.*)(?=","isUserAdministrator)'
    $Match = $Regex.Match($FrontPage.content)
    if($Match.Success){ $Headers.'x-anti-forgery-token' = $Match.Value.ToString() }
    else{ Write-Verbose 'Unable to find anti forgery token. Some commands may not work.' }
    $script:CWCServerConnection = @{
        Server = $Server
        Headers = $Headers
        Secret = $secret
    }
    Write-Verbose ($script:CWCServerConnection | Out-String)

    try{
        $null = Get-CWCSessionGroup -ErrorAction Stop
        Write-Verbose '$CWCServerConnection, variable initialized.'
    }
    catch{
        Remove-Variable CWCServerConnection -Scope script
        Write-Verbose 'Authentication failed.'
        Write-Error $_
    }
}

Added new private function /Private/Get-OTP.ps1
Thanks to
https://github.com/HumanEquivalentUnit/PowerShell-Misc/blob/master/GoogleAuthenticator.psm1

function Get-OTP {
    [CmdletBinding()]
    Param (
        # BASE32 encoded Secret e.g. 5WYYADYB5DK2BIOV
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [string]
        $Secret,

        # OTP time window in seconds
        $TimeWindow = 30
    )

    $Base32Charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
    # Convert the secret from BASE32 to a byte array
    # via a BigInteger so we can use its bit-shifting support,
    # instead of having to handle byte boundaries in code.
    $bigInteger = [Numerics.BigInteger]::Zero
    foreach ($char in ($secret.ToUpper() -replace '[^A-Z2-7]').GetEnumerator()) {
        $bigInteger = ($bigInteger -shl 5) -bor ($Base32Charset.IndexOf($char))
    }

    [byte[]]$secretAsBytes = $bigInteger.ToByteArray()
    

    # BigInteger sometimes adds a 0 byte to the end,
    # if the positive number could be mistaken as a two's complement negative number.
    # If it happens, we need to remove it.
    if ($secretAsBytes[-1] -eq 0) {
        $secretAsBytes = $secretAsBytes[0..($secretAsBytes.Count - 2)]
    }


    # BigInteger stores bytes in Little-Endian order, 
    # but we need them in Big-Endian order.
    [array]::Reverse($secretAsBytes)
    

    # Unix epoch time in UTC and divide by the window time,
    # so the PIN won't change for that many seconds
    $epochTime = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
    
    # Convert the time to a big-endian byte array
    $timeBytes = [BitConverter]::GetBytes([int64][math]::Floor($epochTime / $TimeWindow))
    if ([BitConverter]::IsLittleEndian) { 
        [array]::Reverse($timeBytes) 
    }

    # Do the HMAC calculation with the default SHA1
    # Google Authenticator app does support other hash algorithms, this code doesn't
    $hmacGen = [Security.Cryptography.HMACSHA1]::new($secretAsBytes)
    $hash = $hmacGen.ComputeHash($timeBytes)


    # The hash value is SHA1 size but we want a 6 digit PIN
    # the TOTP protocol has a calculation to do that
    #
    # Google Authenticator app may support other PIN lengths, this code doesn't
    
    # take half the last byte
    $offset = $hash[$hash.Length-1] -band 0xF

    # use it as an index into the hash bytes and take 4 bytes from there, #
    # big-endian needed
    $fourBytes = $hash[$offset..($offset+3)]
    if ([BitConverter]::IsLittleEndian) {
        [array]::Reverse($fourBytes)
    }

    # Remove the most significant bit
    $num = [BitConverter]::ToInt32($fourBytes, 0) -band 0x7FFFFFFF
    
    # remainder of dividing by 1M
    # pad to 6 digits with leading zero(s)
    # and put a space for nice readability
    $PIN = ($num % 1000000).ToString().PadLeft(6, '0')

    [PSCustomObject]@{
        'code' = $PIN
        'timeout' = ($TimeWindow - ($epochTime % $TimeWindow))
    }
}

@jonwbstr
Copy link
Contributor

The Headers hashtable stores the value generated at connection time, so the connection is only good for 60s. Not sure how to update the OTP each time a command is run.

@jonwbstr
Copy link
Contributor

jonwbstr commented Jan 12, 2023

In \Private\Invoke-CWCWebRequest.ps1, I added this to always get a new OTP code.

$script:cwcserverconnection.Headers.'X-One-Time-Password' = $(Get-OTP -Secret $script:cwcserverconnection.Secret).Code
        return Write-Error ($ErrorMessage | Out-String)
    }

+   $script:cwcserverconnection.Headers.'X-One-Time-Password' = $(Get-OTP -Secret $script:cwcserverconnection.Secret).Code
    $BaseURI = "https://$($script:CWCServerConnection.Server)"
    $Arguments.URI = Join-Url $BaseURI $Arguments.Endpoint
    $Arguments.remove('Endpoint')

@zanderson-aim
Copy link

zanderson-aim commented Apr 2, 2023

These updates still working? Running Version 23.2.9.8466 and I get a 401 access denied error. Confirmed the OTP code matches the authenticator app, and I can login with user/password/code just fine through the website.

Found my problem it was permissions, solution here

@Luke-Williams9
Copy link

@zanderson-aim I've created a fork of this project.
https://github.com/Luke-Williams9/ConnectWiseControlAPI

I just put it up today, so it may not be bug-free yet. Its working for me though. Let me know how it goes.

@mrmattipants
Copy link

I didn't realize that there had actually been some progress, in regard to this Issue. I will test-out all of the suggestions and see if there is anything I can do, to contribute, as well.

A huge "Thank You!" to Luke-Williams9 and jonwbstr. I greatly appreciate the work you did, on this Issue.

Of course, this isn't my Repo, but this API will definitely be useful, as I need to cleanup and implement a large number of updates, in my employer's CWC Environment. I'm hoping that this API is going to help streamline those tasks.

@Szeraax
Copy link

Szeraax commented Jul 8, 2024

I am loving this module, Thanks Chris so much for all your work. I feel the exact same way that Luke does: This module MUST support MFA. As such, I guess I'll uninstall it and download the one from Luke instead.

@xxxmtixxx
Copy link

xxxmtixxx commented Jul 8, 2024

I am maintaining a fork with changes from Chris and Luke, if you care to check it out.

https://github.com/xxxmtixxx/ConnectWiseControlAPI

I've also included a script to create a user and assign a machine. Please let me know if you test and how it works for you.

@Szeraax
Copy link

Szeraax commented Jul 8, 2024

Good news: I figured out how to get this to work using a real WebSession! Works with Email or Authy or Google OTP or Yubikey too :D And its really pretty darn simple.

@Szeraax
Copy link

Szeraax commented Jul 8, 2024

I went the same route that @mrmattipants was pointing after inspecting the login process and implemented TryLogin. As such, the module with this patch now uses a WebSession to stay authenticated rather than a collection of headers. This also means that when you do MFA one time to login, you don't need to do MFA again until the session expires.

Future work could include things such as: using the WebSession and doing the password again without having to do the MFA (like the "trust this computer for 7 days" option that appears in web) or allow for the export/saving of the WebSession to disk so that you can password auth and not need MFA auth for several days or across powershell sessions. But I wanted to keep this patch small so that it was as easy to digest as possible.

Here's the commit: Szeraax@ee2a590

If there is interest, I will release this as a separate module since we don't see Chris being too active with this module. Again, mad props to all the work that Chris has done to make this module. It is working awesome!

@Szeraax
Copy link

Szeraax commented Jul 8, 2024

Alright, I've been busy today. I present, ConnectWiser: A wiser way to use ConnectWise Control. Github link.

Supports non-interactive use like so:

Connect-CWC -Server contoso.screenconnect.com -Credentials $cred -OtpCode abcdeabcdeabceabceabceabceabcebacbdea

Should also support 6-digit codes to sign in and then NOT PROMPT you about them every minute :D. If you use email for 2nd auth factor, it'll prompt you for the code that you get from the instance after you attempt to login. I'm really happy with how easy it has been to extend what Chris has written thus far. PRs/issues welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants