ConnectWiseControlAPI
ConnectWiseControlAPI copied to clipboard
Hosted instance requires MFA
Your module says: "Requires an account without MFA. Use a complex username and password."
Will you be able to implement MFA into your module?
I am curious about this as well. Is there a plan or workaround for this?
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.
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.
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))
}
}
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.
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')
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 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.
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.
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.
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.
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.
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!
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.