ConnectWiseControlAPI icon indicating copy to clipboard operation
ConnectWiseControlAPI copied to clipboard

Hosted instance requires MFA

Open sdubin2 opened this issue 3 years ago • 14 comments

"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?

sdubin2 avatar Nov 15 '21 17:11 sdubin2

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

tcsi-github avatar Feb 17 '22 14:02 tcsi-github

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.

mrmattipants avatar May 05 '22 03:05 mrmattipants

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 avatar Aug 29 '22 22:08 Luke-Williams9

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))
    }
}

Luke-Williams9 avatar Dec 14 '22 21:12 Luke-Williams9

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 avatar Jan 12 '23 00:01 jonwbstr

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')

jonwbstr avatar Jan 12 '23 01:01 jonwbstr

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

  • https://github.com/christaylorcodes/ConnectWiseControlAPI/issues/14

zanderson-aim avatar Apr 02 '23 21:04 zanderson-aim

@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.

Luke-Williams9 avatar Apr 05 '23 00:04 Luke-Williams9

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.

mrmattipants avatar Feb 21 '24 16:02 mrmattipants

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.

Szeraax avatar Jul 08 '24 02:07 Szeraax

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.

xxxmtixxx avatar Jul 08 '24 02:07 xxxmtixxx

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 avatar Jul 08 '24 06:07 Szeraax

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: https://github.com/Szeraax/ConnectWiseControlAPI/commit/ee2a59086b77ab79fb1c93dc91eb2191ce766d31

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 avatar Jul 08 '24 15:07 Szeraax

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.

Szeraax avatar Jul 08 '24 23:07 Szeraax