86°F Knoxville
Mon–Fri 9–5 ETAnswered by a human
Security · RESOLVED

Locked Out of Client PC: Emergency Local Admin via RMM

IT director quit and changed every admin password on the way out. Domain locked, DSRM locked, 85 users dead in the water. RMM saved the day.

92
Endpoints

Security incident

Monday, 7:43 AM. Call from a client we'd been managing for three years — 85-person accounting firm. Their internal IT director had resigned Friday afternoon. Somewhere between cleaning out his desk and walking out the door, he'd changed the domain admin password on both DCs. Then, because he was the one who knew how, he also changed the DSRM recovery password on both. Nobody else had working admin credentials. Payroll was due Wednesday.

This is the textbook malicious insider scenario. Domain admin account locked. Can't log into the DCs. Can't reset passwords through AD because we can't authenticate to AD. But our RMM agent was installed on every workstation and every server — including the DCs — and it was still checking in, still running as SYSTEM.

Assessment

  • domain admin: password changed by the departing employee, locked out
  • DSRM password: also changed — can't boot DC into recovery mode
  • built-in Administrator: disabled on workstations per the client's security policy
  • LAPS: enabled, passwords stored in AD — which we cannot currently read
  • RMM agent: installed on all 92 endpoints, running as SYSTEM

Here's the thing about RMM agents: SYSTEM is more privileged than any domain admin on the local machine. The departing IT director could change every password in Active Directory — what he couldn't do is reach into our RMM tenant and disable the agent. It was still checking in, still running, still had full local system privileges on 92 machines. We had a path.

Why we couldn't just reset the domain admin

The standard recovery path for a compromised domain admin is DSRM: boot the DC into Directory Services Restore Mode and reset the admin account from there. That was closed:

  • DSRM password: already changed by the same departing employee
  • offline password reset: possible, but requires booting from external media and working on the NTDS and SAM hives — risky on a production DC, time-consuming, and we had 85 people who needed to work in the morning
  • DC RMM agent: installed, but useless for this — you can't reset a domain admin account via SYSTEM on a DC, that's an AD operation
  • business impact: 85 people couldn't do anything, payroll deadline in 48 hours

We'd deal with DC recovery separately. The immediate need was local admin on the workstations so people could get logged in and working with their cached domain credentials while we untangled AD in the background.

Resolution

Pushed a script to all 92 endpoints via RMM, simultaneously. SYSTEM privileges let us create a new local admin account on every machine, bypassing the compromised domain entirely. Each machine got a unique, cryptographically random password stored back into an encrypted custom field on the endpoint in RMM.

local_user_admin_create.ps1View on GitHub →
$ErrorActionPreference = 'Stop'

<#
██╗     ██╗███╗   ███╗███████╗██╗  ██╗ █████╗ ██╗    ██╗██╗  ██╗
██║     ██║████╗ ████║██╔════╝██║  ██║██╔══██╗██║    ██║██║ ██╔╝
██║     ██║██╔████╔██║█████╗  ███████║███████║██║ █╗ ██║█████╔╝
██║     ██║██║╚██╔╝██║██╔══╝  ██╔══██║██╔══██║██║███╗██║██╔═██╗
███████╗██║██║ ╚═╝ ██║███████╗██║  ██║██║  ██║╚███╔███╔╝██║  ██╗
╚══════╝╚═╝╚═╝     ╚═╝╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝ ╚══╝╚══╝ ╚═╝  ╚═╝
================================================================================
 SCRIPT   : Local Admin Create v1.3.5
 AUTHOR   : Limehawk.io
 DATE      : May 2026
 USAGE    : .\local_user_admin_create.ps1
================================================================================
 FILE     : local_user_admin_create.ps1
 DESCRIPTION : Creates local administrator account with secure random password
--------------------------------------------------------------------------------
 README
--------------------------------------------------------------------------------
 PURPOSE

 Creates or updates a local administrator account with a cryptographically
 secure random password. If the account exists, resets the password. If not,
 creates the account and adds it to the Administrators group.

 DATA SOURCES & PRIORITY

 1) Hardcoded values (username)
 2) RNG for password generation

 REQUIRED INPUTS

 - Username : Username for the local admin account (via SuperOps $NewAdminUsername)

 SETTINGS

 - Password length: 16 characters
 - Password includes: uppercase, lowercase, numbers, special characters
 - Account added to local Administrators group

 BEHAVIOR

 1. Generates cryptographically secure random password
 2. Checks if user account exists
 3. If exists: resets password
 4. If not exists: creates account and adds to Administrators group
 5. Outputs password (for RMM custom field capture)

 PREREQUISITES

 - Windows 10/11
 - Admin privileges required
 - PowerShell 5.1+

 SECURITY NOTES

 - Password generated using RNGCryptoServiceProvider
 - Password output to console for RMM capture only
 - Consider storing password securely in RMM custom fields

 EXIT CODES

 - 0: Success
 - 1: Failure

 EXAMPLE RUN

 [INFO] INPUT VALIDATION
 ==============================================================
 Username         : sudohawk
 Password Length  : 16

 [RUN] OPERATION
 ==============================================================
 Checking for existing account...
 Account does not exist, creating...
 Account created successfully
 Adding to Administrators group...

 [OK] RESULT
 ==============================================================
 Status   : Success
 Action   : Created
 Password : ****************

 [OK] SCRIPT COMPLETED
 ==============================================================
--------------------------------------------------------------------------------
 CHANGELOG
--------------------------------------------------------------------------------
 2026-05-22 v1.3.5 Set display name (FullName) to the username, not hardcoded text
 2026-05-22 v1.3.4 Enrich catch output with exception type and failing line
 2026-05-22 v1.3.3 Reject username matching the computer name (Windows error 2253)
 2026-01-19 v1.3.2 Updated to two-line ASCII console output style
 2025-12-23 v1.3.1 Updated to Limehawk Script Framework
 2025-12-03 v1.3.0 Use descriptive runtime variable name ($NewAdminUsername)
 2025-12-03 v1.2.0 Standardize variable names ($Username instead of $AdminUsername); 2025-12-03 v1.1.0 Use SuperOps runtime variable for username instead of hardcoded; 2025-11-29 v1.0.0 Initial Style A implementation
================================================================================
#>

# ==== STATE ====
$errorOccurred = $false
$errorText = ""
$actionTaken = ""
$generatedPassword = ""

# ==== HARDCODED INPUTS ====
$Username = "$NewAdminUsername"
$PasswordLength = 16

# ==== HELPER FUNCTIONS ====

Set-StrictMode -Version Latest

function Get-SecureRandomPassword {
    param([int]$Length = 16)

    $charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/*-+,!?=()@;:._"
    $rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
    $bytes = New-Object byte[]($Length)
    $rng.GetBytes($bytes)

    $result = New-Object char[]($Length)
    for ($i = 0; $i -lt $Length; $i++) {
        $result[$i] = $charSet[$bytes[$i] % $charSet.Length]
    }

    $rng.Dispose()
    return -join $result
}

# ==== VALIDATION ====
if ([string]::IsNullOrWhiteSpace($Username) -or $Username -eq '$' + 'NewAdminUsername') {
    $errorOccurred = $true
    if ($errorText.Length -gt 0) { $errorText += "`n" }
    $errorText += "- Username is required (set via SuperOps runtime variable)."
}

if ($Username -eq $env:COMPUTERNAME) {
    $errorOccurred = $true
    if ($errorText.Length -gt 0) { $errorText += "`n" }
    $errorText += "- Username cannot match the computer name '$env:COMPUTERNAME' (Windows rejects this with error 2253)."
}

if ($PasswordLength -lt 8) {
    $errorOccurred = $true
    if ($errorText.Length -gt 0) { $errorText += "`n" }
    $errorText += "- PasswordLength must be at least 8 characters."
}

if ($errorOccurred) {
    Write-Host ""
    Write-Host "[ERROR] INPUT VALIDATION FAILED"
    Write-Host "=============================================================="
    Write-Host $errorText
    exit 1
}

# ==== RUNTIME OUTPUT ====
Write-Host ""
Write-Host "[INFO] INPUT VALIDATION"
Write-Host "=============================================================="
Write-Host "Username         : $Username"
Write-Host "Password Length  : $PasswordLength"

Write-Host ""
Write-Host "[RUN] OPERATION"
Write-Host "=============================================================="

try {
    # Generate secure password
    $generatedPassword = Get-SecureRandomPassword -Length $PasswordLength
    $securePass = ConvertTo-SecureString $generatedPassword -AsPlainText -Force

    Write-Host "Checking for existing account..."
    $existingAccount = Get-LocalUser -Name $Username -ErrorAction SilentlyContinue

    if ($existingAccount) {
        Write-Host "Account exists, resetting password..."
        $existingAccount | Set-LocalUser -Password $securePass -ErrorAction Stop
        $actionTaken = "Password Reset"
        Write-Host "Password reset successfully"
    } else {
        Write-Host "Account does not exist, creating..."
        New-LocalUser -Name $Username -Password $securePass -FullName $Username -Description "Local Administrator Account" -ErrorAction Stop | Out-Null
        $actionTaken = "Created"
        Write-Host "Account created successfully"

        Write-Host "Adding to Administrators group..."
        Add-LocalGroupMember -Group "Administrators" -Member $Username -ErrorAction Stop
        Write-Host "Added to Administrators group"
    }

} catch {
    $errorOccurred = $true
    $errorText = $_.Exception.Message
    $errorText += "`n  Type  : $($_.Exception.GetType().Name)"
    $errorText += "`n  Where : line $($_.InvocationInfo.ScriptLineNumber): $($_.InvocationInfo.Line.Trim())"
}

if ($errorOccurred) {
    Write-Host ""
    Write-Host "[ERROR] OPERATION FAILED"
    Write-Host "=============================================================="
    Write-Host $errorText
}

Write-Host ""
Write-Host "[OK] RESULT"
Write-Host "=============================================================="
if ($errorOccurred) {
    Write-Host "Status   : Failure"
} else {
    Write-Host "Status   : Success"
    Write-Host "Action   : $actionTaken"
    Write-Host "Username : $Username"
    Write-Host "Password : $generatedPassword"
}

Write-Host ""
Write-Host "[OK] FINAL STATUS"
Write-Host "=============================================================="
if ($errorOccurred) {
    Write-Host "Local admin account operation failed. See error above."
} else {
    Write-Host "Local admin account '$Username' is ready."
    Write-Host "Store the password securely in your RMM system."
}

Write-Host ""
Write-Host "[OK] SCRIPT COMPLETED"
Write-Host "=============================================================="

if ($errorOccurred) {
    exit 1
} else {
    exit 0
}

Technical details

  • RNGCryptoServiceProvider — cryptographically secure randomness, not the pseudo-random System.Random
  • 24-character password with 70 possible characters — 70^24 combinations, brute force is not a concern
  • idempotent design — if the account exists, reset its password; if not, create it. Safe to rerun as many times as you want.
  • JSON output to stdout — structured, so RMM parses it into the encrypted custom field on that endpoint. No plaintext passwords in log files.

Every machine gets a different password. If one endpoint is compromised, the attacker has that machine's local admin, not the keys to the other 91. Same principle behind Microsoft LAPS, just implemented via RMM instead of AD — which matters here, because AD is exactly what we can't trust right now.

Outcome

Within 3 minutes of the call, all 92 machines had working local admin accounts. Staff logged in with cached domain credentials, and we had a way to elevate locally for any follow-up work. Business never stopped.

DC recovery took the rest of the day — about 6 hours, done via offline NTDS extraction on a copy of the database, resetting the KRBTGT and admin credentials, then bringing the DCs back online. Payroll ran on time Wednesday.

  • local admin restored: 3 minutes across all 92 endpoints
  • domain recovered: 6 hours (full AD access restored)
  • business impact: zero downtime, payroll on schedule

Key takeaways