If you have devices that is connected to an on-premise, you would certainly configure the Local Administrator Password Solution (LAPS), which allows unique password for each local administrator across the enterprise network.
Unfortunately this method only works when you have on-premise devices, but what about Azure AD Joined machines? – A short answer is “no”.
LAPS takes advantage of 2 attributes in the local Active Directory, these attributes are not available in Azure AD.
Therefor I have created a small application that mimic the same behavior for Azure AD devices, which I call “iLAPS” for Intune Local Administrator Password Solution.
There are some requirements before you can use this solution in your environment.
- You need a Azure subscription with an storage account for BLOB and Azure Tables.
- In this example we are using Intune, but you could also use some other mobile device management (MDM).
- Your devices must run Windows 10 (haven’t tested previous Windows versions).
So how does it work?
If you take a look at the picture where the post title is, there is two flows.
The client part is installed on the device, and is responsible for changing the user password and storing it.
- Intune pushes a script to the managed Azure AD device.
- The device executes the script under “SYSTEM”.
- The script request the executable from the Azure BLOB storage.
- When the executable is downloaded the script proceeds by executing the program.
- The program does various checks, gather information and reset wanted local passwords.
- The gathered information with the newly created passwords will be transmitted securely with HTTPS to Azure Storage Table.
The administration part is just a tool to view the usernames and passwords:
- The help desk user runs the executable.
- The executable request data from Azure Storage Tables.
- Azure Storage Tables transmits usernames and passwords to the executable across with HTTPS for secure communications.
What about the code?
There are 3 pieces of scripts/executable in the solution, I will go through them on a high level. You can always study the code your self. I have tried to make the code easy to read and understand.
- Install-iLAPS.ps1
- This script is responsible for downloading and running the executable, it does nothing else.
- Reset-LocalAdministratorPassword.ps1
- This is where the “magic” happens. The program is reponsible for resetting the passwords and transporting the username/passwords safely.
- Get-LocalAdministratorPassword.ps1
- Tool for viewing the username/passwords.
This is just PowerShell that could easily be converted to an .exe (see PS2EXE-GUI). This way users are not able to see the vital logic and endpoints easily (Yes, you can use WireShark to sniff the traffic or a hex editor to see the content). Also the program uses symmetric encryption to hash the passwords, so even if you get unwanted access to Azure storage tables, you can’t see passwords in clear text.
The program will also install a schedule task that will change the password every 3 months. But only if it detects there is access to the internet.
Enough speaking, more code. See the components code below:
Install-iLAPS.ps1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
#Requires -Version 3.0 <# .SYNOPSIS Download executable from Azure Storage BLOB, and execute it. .DESCRIPTION This downloads and executes an execuatable from Azure Storage BLOB. .PARAMETER .EXAMPLE .NOTES Author: Alex Ø. T. Hansen Date: 17-05-2018 Last Updated: 17-05-2018 #> ################################################ <# Bootstrap - Start #> #Create log folder. New-Item -ItemType Directory -Force -Path "C:\Logs\Intune LAPS" | Out-Null; <# Bootstrap - End #> ################################################ <# Input - Start #> #Azure. $AzureEndpoint = 'https://<storage account>.file.core.windows.net'; $AzureSharedAccessSignature = '<SAS>'; $AzureFileShare = "<table>"; #Log. $LogFile = ("C:\Logs\Intune LAPS\" + ((Get-Date).ToString("ddMMyyyy") + ".log")); <# Input - End #> ################################################ <# Functions - Start #> Function Write-Log { [CmdletBinding()] Param ( [parameter(Mandatory=$true)][string]$File, [parameter(Mandatory=$true)][string]$Text, [parameter(Mandatory=$true)][string][ValidateSet("Information", "Error", "Warning")]$Status ) #Construct output. $Output = ("[" + (((Get-Date).ToShortDateString()) + "][" + (Get-Date).ToLongTimeString()) + "][" + $Status + "] " + $Text); #Output. $Output | Out-File -Encoding UTF8 -Force -FilePath $File -Append; Return Write-Output $Output; } <# Functions - End #> ################################################ <# Main - Start #> #Write out to the log file. Write-Log -File $LogFile -Status Information -Text "Starting download request."; #Get temporary location. $Path = "C:\Windows\system32"; #Installation package. $Installer = "Reset-LocalAdministratorPassword.ps1"; #Request application from BLOB storage. Invoke-WebRequest ($AzureEndpoint + "/" + $AzureFileShare + "/" + $Installer + $AzureSharedAccessSignature) -OutFile ($Path + "\" + $Installer); #Write out to the log file. Write-Log -File $LogFile -Status Information -Text "Finished download request."; Write-Log -File $LogFile -Status Information -Text "Running executable."; #Execute the application. Start-Process -FilePath ($Path + "\" + $Installer) -Verb RunAs -Wait; <# Main - End #> ################################################ |
Reset-LocalAdministratorPassword.ps1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 |
#Requires -Version 3.0 <# .SYNOPSIS Reset local administrator accounts. .DESCRIPTION This script reset all locally created user accounts. .PARAMETER .EXAMPLE .NOTES Author: Alex Ø. T. Hansen Date: 17-05-2018 Last Updated: 17-05-2018 #> ################################################ <# Parameters - Start #> [CmdletBinding()] Param ( #Encryption key. [parameter(Mandatory=$true)][string]$SecretKey = "MySecretEncryptionPassword", #Azure endpoint. [parameter(Mandatory=$true)][string]$AzureEndpoint = 'https://<storage account>.table.core.windows.net', #Azure Shared Access SIgnature. [parameter(Mandatory=$true)][string]$AzureSharedAccessSignature = '<SAS>', #Azure Storage Table. [parameter(Mandatory=$true)][string]$AzureTable = "<Azure Table>" ) <# Parameters - End #> ################################################ <# Bootstrap - Start #> #Create log folder. New-Item -ItemType Directory -Force -Path "C:\Logs\Intune LAPS" | Out-Null; <# Bootstrap - End #> ################################################ <# Input - Start #> #Schedule Task. $ScheduleTaskName = "Password Change"; @" <?xml version="1.0" encoding="UTF-16"?> <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <RegistrationInfo> <Date>2018-05-17T13:06:42.9241814</Date> <Author>NCLAN\alh</Author> <URI>\Password Change</URI> </RegistrationInfo> <Triggers> <CalendarTrigger> <StartBoundary>2018-01-30T13:00:00+02:00</StartBoundary> <Enabled>true</Enabled> <ScheduleByMonth> <DaysOfMonth> <Day>30</Day> </DaysOfMonth> <Months> <January /> <April /> <July /> <October /> </Months> </ScheduleByMonth> </CalendarTrigger> </Triggers> <Principals> <Principal id="Author"> <UserId>S-1-5-18</UserId> <RunLevel>HighestAvailable</RunLevel> </Principal> </Principals> <Settings> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries> <AllowHardTerminate>true</AllowHardTerminate> <StartWhenAvailable>true</StartWhenAvailable> <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable> <IdleSettings> <StopOnIdleEnd>true</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings> <AllowStartOnDemand>true</AllowStartOnDemand> <Enabled>true</Enabled> <Hidden>false</Hidden> <RunOnlyIfIdle>false</RunOnlyIfIdle> <DisallowStartOnRemoteAppSession>false</DisallowStartOnRemoteAppSession> <UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine> <WakeToRun>false</WakeToRun> <ExecutionTimeLimit>PT1H</ExecutionTimeLimit> <Priority>7</Priority> <RestartOnFailure> <Interval>PT1H</Interval> <Count>999</Count> </RestartOnFailure> </Settings> <Actions Context="Author"> <Exec> <Command>C:\Windows\System32\Reset-LocalAdministratorPassword.ps1</Command> </Exec> </Actions> </Task> "@ | Out-File -FilePath ("$ScheduleTaskName" + ".xml"); #Log. $LogFile = ("C:\Logs\Intune LAPS\" + ((Get-Date).ToString("ddMMyyyy") + ".log")); <# Input - End #> ################################################ <# Functions - Start #> Function Test-InternetConnection { [CmdletBinding()] Param ( [parameter(Mandatory=$true)][string]$Target ) #Test the connection to target. $Result = Test-NetConnection -ComputerName ($Target -replace "https://","") -Port 443 -WarningAction SilentlyContinue; #Return result. Return $Result; } #Generate passwords. Function New-Password { [CmdletBinding(DefaultParameterSetName='FixedLength',ConfirmImpact='None')] [OutputType([String])] Param ( # Specifies minimum password length [Parameter(Mandatory=$false, ParameterSetName='RandomLength')] [ValidateScript({$_ -gt 0})] [Alias('Min')] [int]$MinPasswordLength = 8, # Specifies maximum password length [Parameter(Mandatory=$false, ParameterSetName='RandomLength')] [ValidateScript({ if($_ -ge $MinPasswordLength){$true} else{Throw 'Max value cannot be lesser than min value.'}})] [Alias('Max')] [int]$MaxPasswordLength = 12, # Specifies a fixed password length [Parameter(Mandatory=$false, ParameterSetName='FixedLength')] [ValidateRange(1,2147483647)] [int]$PasswordLength = 8, # Specifies an array of strings containing charactergroups from which the password will be generated. # At least one char from each group (string) will be used. [String[]]$InputStrings = @('abcdefghijkmnpqrstuvwxyz', 'ABCEFGHJKLMNPQRSTUVWXYZ', '123456789', '!'), # Specifies a string containing a character group from which the first character in the password will be generated. # Useful for systems which requires first char in password to be alphabetic. [String] $FirstChar, # Specifies number of passwords to generate. [ValidateRange(1,2147483647)] [int]$Count = 1 ) Begin { Function Get-Seed{ # Generate a seed for randomization $RandomBytes = New-Object -TypeName 'System.Byte[]' 4 $Random = New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider' $Random.GetBytes($RandomBytes) [BitConverter]::ToUInt32($RandomBytes, 0) } } Process { For($iteration = 1;$iteration -le $Count; $iteration++){ $Password = @{} # Create char arrays containing groups of possible chars [char[][]]$CharGroups = $InputStrings # Create char array containing all chars $AllChars = $CharGroups | ForEach-Object {[Char[]]$_} # Set password length if($PSCmdlet.ParameterSetName -eq 'RandomLength') { if($MinPasswordLength -eq $MaxPasswordLength) { # If password length is set, use set length $PasswordLength = $MinPasswordLength } else { # Otherwise randomize password length $PasswordLength = ((Get-Seed) % ($MaxPasswordLength + 1 - $MinPasswordLength)) + $MinPasswordLength } } # If FirstChar is defined, randomize first char in password from that string. if($PSBoundParameters.ContainsKey('FirstChar')){ $Password.Add(0,$FirstChar[((Get-Seed) % $FirstChar.Length)]) } # Randomize one char from each group Foreach($Group in $CharGroups) { if($Password.Count -lt $PasswordLength) { $Index = Get-Seed While ($Password.ContainsKey($Index)){ $Index = Get-Seed } $Password.Add($Index,$Group[((Get-Seed) % $Group.Count)]) } } # Fill out with chars from $AllChars for($i=$Password.Count;$i -lt $PasswordLength;$i++) { $Index = Get-Seed While ($Password.ContainsKey($Index)){ $Index = Get-Seed } $Password.Add($Index,$AllChars[((Get-Seed) % $AllChars.Count)]) } Write-Output -InputObject $(-join ($Password.GetEnumerator() | Sort-Object -Property Name | Select-Object -ExpandProperty Value)) } } } #Insert data to Azure tables. Function Add-AzureTableData { [CmdletBinding()] Param ( [parameter(Mandatory=$true)][string]$Endpoint, [parameter(Mandatory=$true)][string]$SharedAccessSignature, [parameter(Mandatory=$true)][string]$Table, [parameter(Mandatory=$true)][hashtable]$TableData ) #Create request header. $Headers = @{ "x-ms-date"=(Get-Date -Format r); "x-ms-version"="2016-05-31"; "Accept-Charset"="UTF-8"; "DataServiceVersion"="3.0;NetFx"; "MaxDataServiceVersion"="3.0;NetFx"; "Accept"="application/json;odata=nometadata" }; $URI #Construct URI. $URI = ($Endpoint + "/" + $Table + "/" + $SharedAccessSignature); #Convert table data to JSON and encode to UTF8. $Body = [System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject $TableData)); #Insert data to Azure storage table. Invoke-WebRequest -Method Post -Uri $URI -Headers $Headers -Body $Body -ContentType "application/json" -UseBasicParsing | Out-Null; } #Generate a secret key. Function Set-SecretKey { [CmdletBinding()] Param ( [string]$Key ) #Get key length. $Length = $Key.Length; #Pad length. $Pad = 32-$Length; #If the length is less than 16 or more than 32. If(($Length -lt 16) -or ($Length -gt 32)) { #Throw exception. Throw "String must be between 16 and 32 characters"; } #Create a new ASCII encoding object. $Encoding = New-Object System.Text.ASCIIEncoding; #Get byte array. $Bytes = $Encoding.GetBytes($Key + "0" * $Pad); #Return byte array. Return $Bytes; } #Encrypt data with a secret key. Function Set-EncryptedData { [CmdletBinding()] Param ( $Key, [string]$TextInput ) #Create a new secure string object. $SecureString = New-Object System.Security.SecureString; #Convert the text input to a char array. $Chars = $TextInput.ToCharArray(); #Foreach char in the array. ForEach($Char in $Chars) { #Append the char to the secure string. $SecureString.AppendChar($Char); } #Encrypt the data from the secure string. $EncryptedData = ConvertFrom-SecureString -SecureString $SecureString -Key $Key; #Return the encrypted data. return $EncryptedData; } Function ConvertTo-HashTable { [cmdletbinding()] Param ( [Parameter(Position=0,Mandatory=$True,ValueFromPipeline=$True)] [object]$InputObject, [switch]$NoEmpty ) Process { #Get propery names. $Names = $InputObject | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name; #Define an empty hash table. $Hash = @{}; #Go through the list of names and add each property and value to the hash table. $Names | ForEach-Object {$Hash.Add($_,$InputObject.$_)}; #If NoEmpty is set. If ($NoEmpty) { #Define a new hash. $Defined = @{}; #Get items from $hash that have values and add to $Defined. $Hash.Keys | ForEach-Object { #If hash item is not empty. If ($Hash.item($_)) { #Add to hashtable. $Defined.Add(($_,$Hash.Item($_))); } } #Return hashtable. Return $Defined; } #Return hashtable. Return $Hash; } } Function Test-ScheduleTask { [CmdletBinding()] Param ( [parameter(Mandatory=$true)][string]$Name ) #Create a new schedule object. $Schedule = New-Object -com Schedule.Service; #Connect to the store. $Schedule.Connect(); #Get schedule tak folders. $Task = $Schedule.GetFolder("\").GetTasks(0) | Where-Object {$_.Name -eq $Name -and $_.Enabled -eq $true}; #If the task exists and is enabled. If($Task) { #Return true. Return $true; } #If the task doesn't exist. Else { #Return false. Return $false; } } Function Write-Log { [CmdletBinding()] Param ( [parameter(Mandatory=$true)][string]$File, [parameter(Mandatory=$true)][string]$Text, [parameter(Mandatory=$true)][string][ValidateSet("Information", "Error", "Warning")]$Status ) #Construct output. $Output = ("[" + (((Get-Date).ToShortDateString()) + "][" + (Get-Date).ToLongTimeString()) + "][" + $Status + "] " + $Text); #Output. $Output | Out-File -Encoding UTF8 -Force -FilePath $File -Append; Return Write-Output $Output; } Function Get-LocalGroupMembers { [CmdletBinding()] Param ( [parameter(Mandatory=$true)][string]$LocalGroup ) #Get local machine name. $Machine = $env:COMPUTERNAME; #Get group through ADSI. $Group = [ADSI]"WinNT://$Machine/$LocalGroup,group"; #Get members. $Members = $Group.psbase.Invoke("Members"); #Get members of the group. $GroupMembers = $Members | ForEach-Object { $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}; #Return group members. Return $GroupMembers; } Function Get-LocalUsers { [CmdletBinding()] #Get local user accounts. $LocalUserAccounts = Get-WmiObject -Class Win32_UserAccount -Filter "LocalAccount='True'"; #Object array. $Accounts = @(); #Foreach local user account. Foreach($LocalUserAccount in $LocalUserAccounts) { #Split the SID to get the last octet. $SIDSplit = ((($LocalUserAccount.SID -split "-")[-1]).ToString()); #If the SID last octet starts with 1, or is 500. If(($SIDSplit.StartsWith("1")) -or ($SIDSplit -eq "500")) { #Add to the object array. $Accounts += $LocalUserAccount; } } #Return accounts. Return $Accounts; } <# Functions - End #> ################################################ <# Main - Start #> #Write out to the log file. Write-Log -File $LogFile -Status Information -Text "Starting password reset."; #Test if the machine have internet connection. If(!((Test-InternetConnection -Target $AzureEndpoint).TcpTestSucceeded -eq "true")) { #Write out to the log file. Write-Log -File $LogFile -Status Error -Text "No internet access."; #Exit the script with an error. Exit 1; } Else { #Write out to the log file. Write-Log -File $LogFile -Status Information -Text "The machine have internet access."; } #Check if the schedule task exist. If(!(Test-ScheduleTask -Name $ScheduleTaskName)) { #Write out to the log file. Write-Log -File $LogFile -Status Warning -Text "Schedule task doesn't exist."; Write-Log -File $LogFile -Status Information -Text "Creating schedule task."; #Add schedule task. Register-ScheduledTask -Xml (Get-Content ($ScheduleTaskName + ".xml") | Out-String) -TaskName "$ScheduleTaskName" | Out-Null; #Write out to the log file. Write-Log -File $LogFile -Status Information -Text "Removing schedule task XML file."; #Remove XML file. Remove-Item ($ScheduleTaskName + ".xml"); } Else { #Write out to the log file. Write-Log -File $LogFile -Status Warning -Text "Schedule task already exist."; } #Write out to the log file. Write-Log -File $LogFile -Status Information -Text "Setting encryption key."; #Secret key. $EncryptionKey = Set-SecretKey -Key ($SecretKey); #Get hostname. $Hostname = Invoke-Command {hostname}; #Write out to the log file. Write-Log -File $LogFile -Status Information -Text ("Hostname: " + $Hostname); #Get serial number. $SerialNumber = (Get-WmiObject win32_bios).SerialNumber; #Write out to the log file. Write-Log -File $LogFile -Status Information -Text ("SerialNumber: " + $SerialNumber); #Get machine guid. $MachineGuid = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Cryptography" -Name MachineGuid; #Write out to the log file. Write-Log -File $LogFile -Status Information -Text ("MachineGuid: " + $MachineGuid); #Get public IP. $PublicIP = ((Invoke-RestMethod "http://ipinfo.io/json").IP); #Write out to the log file. Write-Log -File $LogFile -Status Information -Text ("PublicIP: " + $PublicIP); #Write out to the log file. Write-Log -File $LogFile -Status Information -Text ("Getting all local users."); #Get all local users. $LocalUsers = Get-LocalUsers; #Write out to the log file. Write-Log -File $LogFile -Status Information -Text ("Getting all members of administrators."); #Get members of the administrator group. $LocalGroupUsers = Get-LocalGroupMembers -LocalGroup "Administrators"; #Object array. $Accounts = @(); $LocalAdministrators = @(); #Foreach local user. Foreach($LocalUser in $LocalUsers) { #If the local user is in the group. If($LocalUser | Where-Object {$_.Name -in $LocalGroupUsers}) { #Add user to the object array. $LocalAdministrators += $LocalUser; } } #Foreach administrator. Foreach ($LocalAdministrator in $LocalAdministrators) { #Generate GUID. $GUID = (New-Guid).Guid; #Write out to the log file. Write-Log -File $LogFile -Status Information -Text ("Looping through local administrator '" + ($LocalAdministrator.Name).ToString() + "'."); Write-Log -File $LogFile -Status Information -Text ("Generating new password for '" + ($LocalAdministrator.Name).ToString() + "'."); #Generate password. $Password = (New-Password -MinPasswordLength 12 -MaxPasswordLength 15 -FirstChar "N"); #Write out to the log file. Write-Log -File $LogFile -Status Information -Text ("Encrypting password for '" + ($LocalAdministrator.Name).ToString() + "'."); #Encrypt password. $EncryptedPassword = Set-EncryptedData -Key $EncryptionKey -TextInput $Password; #Get date. $Time = (Get-Date); #Unix time. $UnixTime = [System.Math]::Truncate((Get-Date -Date (Get-Date).ToUniversalTime() -UFormat %s)); #Reset the password and change the description. Set-LocalUser -SID $($LocalAdministrator.SID) -Password $($Password | ConvertTo-SecureString -AsPlainText -Force) -Description "Managed by Netcompany" -Confirm:$false; #Create a new object. $AccountObject = New-Object -TypeName PSObject; #Add value to the object. Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "PartitionKey" -Value ($GUID).ToString(); Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "RowKey" -Value ($UnixTime).ToString(); Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "MachineGuid" -Value ($MachineGuid).ToString(); Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "SerialNumber" -Value ($SerialNumber).ToString(); Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "Hostname" -Value ($Hostname).ToString(); Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "Account" -Value ($LocalAdministrator.Name).ToString(); Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "SID" -Value ($LocalAdministrator.SID).ToString(); Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "Password" -Value ($EncryptedPassword).ToString(); Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "PasswordChanged" -Value ($Time).ToString("o"); Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "PasswordNextChange" -Value ($Time).AddMonths(3).ToString("o"); Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "PublicIP" -Value ($PublicIP).ToString(); #Add the object to the array. $Accounts += $AccountObject; } #Foreach account. Foreach($Account in $Accounts) { #Write out to the log file. Write-Log -File $LogFile -Status Information -Text ("Uploading data to Azure tables for '" + ($Account.Account).ToString() + "'."); #Insert data to the Azure table. Add-AzureTableData -Endpoint $AzureEndpoint -SharedAccessSignature $AzureSharedAccessSignature -Table $AzureTable -TableData (ConvertTo-HashTable -InputObject $Account); } #Write out to the log file. Write-Log -File $LogFile -Status Information -Text "Stopping password reset."; <# Main - End #> ################################################ |
Get-LocalAdministratorPassword.ps1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
#Requires -Version 3.0 <# .SYNOPSIS Get password for local administrator accounts that was reset with iLAPS. .DESCRIPTION This script get all locally created user accounts and their passwords intergrated with iLAPS. .PARAMETER .EXAMPLE .NOTES Author: Alex Ø. T. Hansen Date: 17-05-2018 Last Updated: 17-05-2018 #> ################################################ <# Parameters - Start #> [CmdletBinding()] Param ( #Encryption key. [parameter(Mandatory=$true)][string]$SecretKey = "MySecretEncryptionPassword", #Azure endpoint. [parameter(Mandatory=$true)][string]$AzureEndpoint = 'https://<storage account>.table.core.windows.net', #Azure Shared Access SIgnature. [parameter(Mandatory=$true)][string]$AzureSharedAccessSignature = '<SAS>', #Azure Storage Table. [parameter(Mandatory=$true)][string]$AzureTable = "<Azure Table>" ) <# Parameters - End #> ################################################ <# Bootstrap - Start #> #Create log folder. New-Item -ItemType Directory -Force -Path "C:\Logs\Intune LAPS" | Out-Null; <# Bootstrap - End #> ################################################ <# Input - Start #> #Log. $LogFile = ("C:\Logs\Intune LAPS\" + ((Get-Date).ToString("ddMMyyyy") + ".log")); <# Input - End #> ################################################ <# Functions - Start #> Function Test-InternetConnection { [CmdletBinding()] Param ( [parameter(Mandatory=$true)][string]$Target ) #Test the connection to target. $Result = Test-NetConnection -ComputerName ($Target -replace "https://","") -Port 443 -WarningAction SilentlyContinue; #Return result. Return $Result; } #Get data from Azure tables. Function Get-AzureTableData { [CmdletBinding()] Param ( [parameter(Mandatory=$true)][string]$Endpoint, [parameter(Mandatory=$true)][string]$SharedAccessSignature, [parameter(Mandatory=$true)][string]$Table ) #Create request header. $Headers = @{ "x-ms-date"=(Get-Date -Format r); "x-ms-version"="2016-05-31"; "Accept-Charset"="UTF-8"; "DataServiceVersion"="3.0;NetFx"; "MaxDataServiceVersion"="3.0;NetFx"; "Accept"="application/json;odata=nometadata" }; #Construct URI. $URI = ($Endpoint + "/" + $Table + $SharedAccessSignature); #Insert data to Azure storage table. $Response = Invoke-WebRequest -Method Get -Uri $URI -Headers $Headers -UseBasicParsing; #Return table data. Return ,($Response.Content | ConvertFrom-Json).Value; } #Generate a secret key. Function Set-SecretKey { [CmdletBinding()] Param ( [string]$Key ) #Get key length. $Length = $Key.Length; #Pad length. $Pad = 32-$Length; #If the length is less than 16 or more than 32. If(($Length -lt 16) -or ($Length -gt 32)) { #Throw exception. Throw "String must be between 16 and 32 characters"; } #Create a new ASCII encoding object. $Encoding = New-Object System.Text.ASCIIEncoding; #Get byte array. $Bytes = $Encoding.GetBytes($Key + "0" * $Pad); #Return byte array. Return $Bytes; } #Encrypt data with a secret key. Function Set-EncryptedData { [CmdletBinding()] Param ( $Key, [string]$TextInput ) #Create a new secure string object. $SecureString = New-Object System.Security.SecureString; #Convert the text input to a char array. $Chars = $TextInput.ToCharArray(); #Foreach char in the array. ForEach($Char in $Chars) { #Append the char to the secure string. $SecureString.AppendChar($Char); } #Encrypt the data from the secure string. $EncryptedData = ConvertFrom-SecureString -SecureString $SecureString -Key $Key; #Return the encrypted data. return $EncryptedData; } #Decrypt data with a secret key. Function Get-EncryptedData { [CmdletBinding()] Param ( $Key, $TextInput ) #Decrypt the text input with the secret key. $Result = $TextInput | ConvertTo-SecureString -key $Key | ForEach-Object {[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($_))}; #Return the decrypted data. Return $Result; } <# Functions - End #> ################################################ <# Main - Start #> #Test if the machine have internet connection. If(!((Test-InternetConnection -Target $AzureEndpoint).TcpTestSucceeded -eq "true")) { #Write out to the log file. Write-Log -File $LogFile -Status Error -Text "No internet access."; #Exit the script with an error. Exit 1; } #Secret key. $EncryptionKey = Set-SecretKey -Key ($SecretKey); #Get all passwords. $Data = Get-AzureTableData -Endpoint $AzureEndpoint -SharedAccessSignature $AzureSharedAccessSignature -Table $AzureTable; #Object array. $Accounts = @(); #If there is any data. If($Data) { #Foreach password. Foreach($Account in $Data) { #Decrypt password. $Password = Get-EncryptedData -Key $EncryptionKey -TextInput $Account.Password; #Create a new object. $AccountObject = New-Object -TypeName PSObject; #Add value to the object. Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "SerialNumber" -Value ($Account).SerialNumber; Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "Hostname" -Value ($Account).Hostname; Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "Username" -Value ($Account).Account; Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "Password" -Value ($Password).ToString(); Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "PasswordChanged" -Value ([datetime]($Account).PasswordChanged); #Add to object array. $Accounts += $AccountObject; } } #If no entries are returned. Else { #Create a new object. $AccountObject = New-Object -TypeName PSObject; #Add value to the object. Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "SerialNumber" -Value "<empty>"; Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "Hostname" -Value "<empty>"; Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "Username" -Value "<empty>"; Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "Password" -Value "<empty>"; Add-Member -InputObject $AccountObject -Membertype NoteProperty -Name "PasswordChanged" -Value "<empty>"; #Add to object array. $Accounts += $AccountObject; } #Create GUI. $Accounts | Out-GridView -Title "iLAPS" -PassThru; <# Main - End #> ################################################ |
If you don’t know about Azure Storage Tables, Azure BLOB Storage and so on, here are some resources that might be helpful.
- Table Storage with PowerShell.
- Create a storage account.
- What is Shared Access Signatures?
- Azure Storage Services REST API
That’s all from now, if you need any further explanation please comment below!
Good morning Alex,
I’m starting to live the experience in Intune and would like to deploy Ilaps, would it be possible to send a detailed Tutorial on how to carry out the implementation?
Thank you
Hi Ronny,
Do you have access to Intune, Azure Storage Tables and a Windows 10 device?
Here is a short summary, what you need to do:
1. Create a table on Azure Storage, and generate a SAS key.
More info: https://www.michaelcrump.net/azure-tips-and-tricks82/
2. Edit the “Reset-LocalAdministratorPassword.ps1” with the newly create Azure Table information (table name, URI and SAS). Upload it to Azure blob storage.
3. Edit “Install-iLAPS.ps1” with blob storage and SAS key, then upload it to Intune to push to devices.
4. To view the passwords either view the raw data on the azure table, or edit “Get-LocalAdministratorPassword.ps1” with the information created above.
Let me know if you need more detailed information.
So, when does the script actually apply / how does it re-trigger? I dont see that you’re making a scheduled task, so.. how does it fire off when the number of days expires?
Also, what if the server is not available at that time? How does it re-try and how often?
Hi Jeremy,
The password(s) is changed as soon as the script is pushed to the device from Intune. The script ‘Reset-LocalAdministratorPassword.ps1’ creates a schedule task that runs every 90 days.
If the machine doesn’t have access to the internet and Azure Storage Table, it will not proceed changing passwords.
You can edit the task schedule that is getting installed to suit your demands.
Let me know if you have any further questions, or want me to demonstrate it.
This is excellent work, well done. We use LAPS and are now moving to modern management and i thought i’d have to do something like this when i stumbled upon this. You saved me a lot of time.
Thanks, let me know how it works out for you.
First of all this is great. Thank you for letting others use it. Since it was published about a year ago I thought I would ask if there are any tweaks or changes you would suggest since it was originally published?
Not really. Instead of Azure Storage Tables, I created my own API to post and get the data (passwords).
Alex,
I really appreciate this post. This tool will definitely help me out as we’re looking to move off of on-prem laps to azad bound devices with intune. I have some question on implementing this myself.
* Install–iLAPS.ps1\$AzureFileShare param: is this the container for the blob storage account where Reset-LocalAdministratorPassword.ps1 is stored?
* are we to change the $secretKey param in Get-LocalAdministratorPassword.ps1/Reset-LocalAdministratorPassword.ps1?
* What are the recommended settings you’re using for both the table storage and blob storage account?
Thank you for all of this!
as check the password is added to the table can we able to update it in the storage table??
Nice solution, I have the same comment as “sabari”, is it possible to update the storage table? Now every time the password is changed, a new entry is created in the Azure storage table.
Hi,
Yes that is possible, you need to change the PowerShell code to modift instead of creating a new entry.
Pingback: Challenges while managing administrative privileges on your Azure AD joined Windows 10 devices | Modern Workplace Blog
Hi Alex,
I just read the ResetLaps script and was looking for the code to modify an existing entry instead of inserting one. I think I had to modify the Add-AzureTableData function in this case?
I am not a Powershell expert, maybe you can give me tip for the right direction.
Would love some details on updating the table instead of add all new entries.
Does anyone has done the updating insted of creating an new entry yet? I would like to know how you did that.
Hi Peter,
Instead of pushing the data to Azure Tables I would suggest push it to a blob storage. Then make a function that picks up this blob file (Azure Function) and insert it in a database or keyvault.
This solution looks really useful. Two questions:
1. Does the password have to be written to an Azure Table, or could we use an API to write it to somewhere like LastPass?
2. Does the managed computer have to be Azure AD Joined or would it work for it to be Hybrid Joined?
1) Sure, you could make use of an API no need to Azure Tables.
2) No, works on any Windows machine
I am running into an issue where the scripts work fine when I run them manually, but when I deploy through Intune, the script doesn’t actually change the password, it only writes the intended password to BLOB storage, but the passwords remain unchanged on the PC. Any ideas?
Great guide and method Alex.
Any thoughts to using Azure Key Vault for storing the passwords more securely than Azure Tables? Interested for your take on this.
Thanks,
Hi Jack,
Yes, this would definitely be more secure.
Hi Alex,
i got below error for ilpas.ps1 , please suggest what must be modified in the script.
Invoke-WebRequest :
ResourceNotFound
Line 31, 32 and 33
I savour, cause I found exactly what I was looking for.
You have ended my 4 day long hunt! God Bless you man. Have a great day.
Bye Rp (Adultgallerywcoxa.Blogspot.Com)