MSP Automator

Technical and Operational Content for MSPs and IT – No ads, no sponsors, no bullshit.

, , ,

Purpose Built Local Account Password Rotation for NinjaRMM

For my introductory post I’ll be demonstrating an easy and fast way of implementing a LAPS like solution using NinjaRMM and PowerShell. Although my system is purpose built for NinjaRMM there’s no reason this couldn’t work with other RMMs, providing they allow to set custom attributes locally on an endpoint that will then sync to the management console. This system is cloud driven from the NinjaRMM console and only downloads a plain text dictionary file to the endpoint itself. We rely only on the NinjaRMM agent to trigger and rotate the password while simultaneously capturing the result into the console.

Implementing the Microsoft Local Account Password Solution (LAPS for short) can be a challenge for MSPs. Some clients may not have AD infrastructure or might be using a complicated hybrid solution that doesn’t mesh well with LAPS. Other concerns could be COVID and WFH related – machines might not check in to a domain controller for a long time (or at all!). Further still, technicians can be lazy, and when they have to go searching through multiple systems quality of work and response times can suffer. Luckily, NinjaRMM custom properties allow us to generate a fully automated LAPS replacement directly inside the web console itself.

Steps we’ll take:

  1. Generate a global attribute in NinjaRMM Dashboard
  2. Construct the Download Script for the Dictionary
  3. Construct the Auto Rotate script and the trigger

Creating the Global Custom Field

First things first – we need to create a Global Custom Field in the NinjaRMM console. Global Custom Fields are accessible to all devices in the console across all organizations. If you’re only setting this up for one Organization you can create a custom field just in that org to test. You can add a custom field in Configuration -> Devices -> Global Custom Fields (beta). Call your field whatever you prefer but keep that name handy as we’ll need it shortly.

The NinjaRMM Create Field screen. Use a Text field.

Assemble the Dictionary

The dictionary file we will use is about 750 words long. You can use any dictionary you can find or make your own. The formatting is one word per line in a CSV file. Here’s a link to the dictionary file I’ll be using. I’m also a big fan of this EFF Long Dictionary.

The longer your dictionary the better. Because we’ll be using phrases based on these dictionaries with custom conjunctions our possible combinations will be in the trillions.

Script #1 – Dictionary Download

The first script we’ll be adding to NinjaRMM is the mechanism to download the dictionary to your target endpoints. We create an empty directory and download the dictionary CSV from our repository using Invoke-RESTMethod. Is it insecure to store the dictionary file in plain text on each endpoint? Yes and no. There are no real “gotchas” here because you should be using a dictionary file long enough that it introduces enough entropy for it to not matter if an attacker has access to it. If I pull the latest dictionary off the shelf at a bookstore I could similarly launch a dictionary attack but the pool of words would be so massive to make it unreasonable or impractical. The addition of conjunction words and random order of the phrases also introduces enough combinations to prevent brute force dictionary attacks from being practical. Your local lockout policy should also prevent this from being effective. I plan on implementing an encrypted zip file for storing the dictionary file in the future but since PowerShell does not support this natively without the addition of the 7Zip module, this solution will work for now.

Write-Host "Attempting to create scratch directories...."
	New-Item -Path "C:\" -Name "#######" -ItemType "directory" -ErrorAction Stop
	$StopError = $_.Exception.Message
	Write-Host "No worky - error is: $StopError"

Write-Host "Scratch space configured successfully....attempting to download dictionary..."

$Url1 = 'http://PATH.TO.YOUR.REPO/dict.csv'
$File1 = 'C:\#######\' + $(Split-Path -Path $Url1 -Leaf)

	 Invoke-WebRequest -Uri $Url1 -OutFile $File1 -ErrorAction Stop
	$StopError = $_.Exception.Message
	Write-Host "No worky - error is: $StopError"
	$script:ExitCode = 0

Write-Host "Download successful...terminating..."

You will need to set the ##### to the name of the folder you want to create/use for dictionary storage and change the $URL1 value to the web address of your uploaded dictionary CSV.

Script #2 – The Secret Sauce

Finally we can now deploy the actual password changing script. I recommend deploying this via a scheduled script in NinjaRMM. I set mine to change daily but any rotation frequency is fine.

$Username = "####"
$Path = "####"

$rand = new-object System.Random
$conjunction = "THE","MY","WE","OUR","AND","BUT","PLUS"
$conjunction2 = "need","your","mine","their","or","if","also"
$words = import-csv $Path
$word1 = ($words[$rand.Next(0,$words.Count)]).Word
$con = ($conjunction[$rand.Next(0,$conjunction.Count)])
$word2 = ($words[$rand.Next(0,$words.Count)]).Word
$con2 = ($conjunction2[$rand.Next(0,$conjunction2.Count)])
$word3 = ($words[$rand.Next(0,$words.Count)]).Word
$NewPW = $word1 + "-" + $con + "-" + $word2 + "-" + $con2 + "-" + $word3

	net user $Username $NewPW
	$Err = $_.Exception.Message
	Write-Host $Err

Write-host "New password for $Username is $NewPW"

Ninja-Property-Set LocalPass $NewPW

Again you’re going to want to replace the $Username hashes with the local admin account name and the $Path variable to where you downloaded the Dictionary file to. Let’s see what output from this script looks like.

New password for localadmin is turmoil-OUR-imminent-also-expurgate
New password for localadmin is vigorous-BUT-highbrow-their-fledged
New password for localadmin is sinuous-WE-bereft-also-veritable
New password for localadmin is taciturn-PLUS-stanch-your-conceit

Neato. But what happened in NinjaRMM?

This updates in basically real time.

There you have it. A 10 minute solution to LAPS in the cloud using NinjaRMM. Although uploading these passwords to the portal is not ideal from a security standpoint, the fact that all machines have different long passphrases that rotate daily makes it more secure than not. An attacker would have to breach NinjaRMM 2FA to retrieve this information and even then would be able to access the scripting interface to change passwords themselves if they wanted at that point. Really, we’re losing nothing and gaining a ton security-wise. I’m always interested in hearing ways you’d improve this script – please drop me a comment or an email.

Comments (