Using PowerShell to Check for Newly Created Local Admins - windows

So, as the title says: I am trying to write up a PowerShell script that checks to see if any user accounts have been added to the Local Administrators group or have been added as an Administrator on the local machine. I have been using Get-EventLog and Get-WinEvent in an attempt to accomplish what I a trying to do. The problem I am having is isolating or extracting the information I want out of the event logs.
This is what I have so far:
$Date = ((Get-Date).AddDays(-1)).Date
$Events = Get-WinEvent -FilterHashtable #{
StartTime = $Date
LogName = 'Security'
ProviderName = 'Microsoft-Windows-Security-Auditing'
ID = 4732
}
I figure, if I can get the Username of the account that was added; which group or permissions it was given; and the date it was created, I can selectively output that information for each log over the last 24 hours. I'm not sure if I should be trying to use Get-Item or Get-Content, or if there is another way I should be trying to tackle this.

Unless you have powershell v6.0+ installed, you'll need to use -FilterXPath instead:
$Alerts = Get-WinEvent -LogName Security -FilterXPath #'
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">*[System[
EventID=4732 and
TimeCreated[timediff(#SystemTime) <= 604800000]]] and
*[EventData[Data[#Name="TargetDomainName"]='BUILTIN']] and
*[EventData[Data[#Name='TargetUserName']='Administrators']]
</Select>
</Query>
</QueryList>
'#
You can use -FilterHashTable if you are running powershell v6.0 or higher. I would use something like this to check for those events. My servers don't all have v6, so I would have to run it remotely like so:
#Requires -Version 6.0
$alerts = Get-WinEvent -ComputerName $computername -FilterHashtable #{
StartTime = ((Get-Date).AddDays(-1)).Date
LogName = 'Security'
ProviderName = 'Microsoft-Windows-Security-Auditing'
ID = 4732
TargetDomainName='Builtin' ## These fields require
TargetUserName='Administrators' ## Powershell v6.0+
}
I'll include how I convert event log's local SIDs to usernames for reports, since it's a pain
# Process event(s)
if ($alerts) { foreach ($event in $alerts) {
# Try and convert SID to account name:
$sid = $event.Properties[1].Value.Value
$localuser = ([System.Security.Principal.SecurityIdentifier]::new($sid)
).Translate([System.Security.Principal.NTAccount])).Value
}
# Use SID in case of failure
if (!$localuser){$localuser=$event.Properties[1].Value}
# Example action
Write-Warning "User $localuser added to local Administrators group on $computername by user $($event.Properties[7].Value)\$($event.Properties[6].Value)"
}}
# Outputs
WARNING: User Server01\local-user01 added to local Administrators group on Server01 by user DOMAIN\AdminUser01

Related

How to fix security within WinSCP SFTP scripts in PowerShell with hard-coded passwords

So my organization has tasked me with cleaning up some of the security issues in regards to some automated scripts that have hard coded passwords within the scripts that are running as automated tasks. One such task contains SFTP scripts that export and import files to and from with the password, host name, credentials, port, and everything exposed within the script. As a result, I would like to first see about how to call such credentials within a separate file that can be hidden and two see about encryption and salting it later. But my main focus is getting them out of the script in case traffic is every intercepted. Here is what the PowerShell code looks like:
param (
$localPath = 'E:\FTP\SchooLinks\course_requests.csv',
$remotePath = '/schoolinks_exports/course_planning/course_requests.csv'
)
try
{
# Load WinSCP .NET assembly
Add-Type -Path "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
# Setup session options
$sessionOptions = New-Object WinSCP.SessionOptions -Property #{
Protocol = [WinSCP.Protocol]::Sftp
HostName = "<domain_name>"
UserName = "<username>"
Password = "<password>"
SshHostKeyFingerprint = "<fingerprint>"
}
$session = New-Object WinSCP.Session
try
{
# Connect
$session.Open($sessionOptions)
# Upload files
$transferOptions = New-Object WinSCP.TransferOptions
$transferOptions.TransferMode = [WinSCP.TransferMode]::Binary
$transferResult =
$session.GetFiles($remotePath, $localPath, $False, $transferOptions)
# Throw on any error
$transferResult.Check()
# Print results
foreach ($transfer in $transferResult.Transfers)
{
Write-Host "Download of $($transfer.FileName) succeeded"
}
}
finally
{
# Disconnect, clean up
$session.Dispose()
}
exit 0
}
catch
{
Write-Host "Error: $($_.Exception.Message)"
exit 1
}
Another one that we have looks like this:
param (
$localPath = 'E:\FTP\TalentEd\SkywardApplicantExportSQL.txt',
$remotePath = '/SkywardApplicantExportSQL.txt'
)
try
{
# Load WinSCP .NET assembly
Add-Type -Path "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
# Setup session options
$sessionOptions = New-Object WinSCP.SessionOptions -Property #{
Protocol = [WinSCP.Protocol]::Sftp
HostName = "<domain>"
UserName = "<username>"
Password = "<password>"
SshHostKeyFingerprint = "<sha_fingerprint>"
}
$session = New-Object WinSCP.Session
try
{
# Connect
$session.Open($sessionOptions)
# Upload files
$transferOptions = New-Object WinSCP.TransferOptions
$transferOptions.TransferMode = [WinSCP.TransferMode]::Binary
$transferResult =
$session.GetFiles($remotePath, $localPath, $False, $transferOptions)
# Throw on any error
$transferResult.Check()
# Print results
foreach ($transfer in $transferResult.Transfers)
{
Write-Host "Download of $($transfer.FileName) succeeded"
}
}
finally
{
# Disconnect, clean up
$session.Dispose()
}
exit 0
}
catch
{
Write-Host "Error: $($_.Exception.Message)"
exit 1
}
I am familiar with Python and json and calling stuff within a json file similar to the following:
import json
with open('secrets.json','r') as f:
config = json.load(f)
and calling it with (config['object']['nested_element']) within the Python script.
I would like to do something similar with PowerShell, however I have very limited knowledge to PowerShell.
Yeppers, of course, never store creds in clear text in files.
There are several ways to store credentials for use. Secure file (xml, etc..), the registry, or Windows Credential Manager and this is well documented on Microsoft sites, as well as in many articles all over the web and via Q&A's on StackOverflow.
Just search for 'securely store credentials PowerShell'
Sample results...
Working with Passwords, Secure Strings and Credentials in Windows
PowerShell
How to run a PowerShell script against multiple Active Directory
domains with different credentials
Accessing Windows Credentials Manager from PowerShell
Save Encrypted Passwords to Registry for PowerShell
...and/or the modules via the MS powershellgallery.com directly installable from your PowerShell environments.
Find-Module -Name '*cred*' |
Format-Table -AutoSize
<#
# Results
Version Name Repository Description
------- ---- ---------- -----------
2.0 CredentialManager PSGallery Provides access to credentials in the Windows Credential Manager
2.0.168 VcRedist PSGallery A module for lifecycle management of the Microsoft Visual C++ Redistributables. Downloads the supp...
1.3.0.0 xCredSSP PSGallery Module with DSC Resources for WSMan CredSSP.
1.1 VPNCredentialsHelper PSGallery A simple module to set the username and password for a VPN connection through PowerShell. Huge tha...
1.0.11 pscredentialmanager PSGallery This module allows management and automation of Windows cached credentials.
4.5 BetterCredentials PSGallery A (compatible) major upgrade for Get-Credential, including support for storing credentials in Wind...
1.0.4 WindowsCredential PSGallery Management module for Windows Credential Store.
...
#>
So many thanks to #postanote and #Martin Prikryl I was able to figure this out.
You can basically use a config.xml file with contents similar to this:
<Configuration>
<localPath>insert_local_file_path</localPath>
<remotePath>insert_remote_file_path</remotePath>
<Protocol>[WinSCP.Protocol]::Sftp</Protocol>
<HostName>insert_hostname</HostName>
<UserName>username</UserName>
<Password>mypassword</Password>
<SshHostKeyFingerPrint>fingerprint</SshHostKeyFingerPrint>
</Configuration>
From here you can use the following at the beginning of your template:
# Read XML configuration file
[xml]$config = Get-Content ".\config.xml"
param (
$localPath = $config.Configuration.localPath
$remotePath = $config.Configuration.remotePath
)
try
{
# Load WinSCP .NET assembly
Add-Type -Path "C:\Program Files (x86)\WinSCP\WinSCPnet.dll"
# Setup session options
$sessionOptions = New-Object WinSCP.SessionOptions -Property #{
Protocol = $config.Configuration.Protocol
HostName = $config.Configuration.HostName
UserName = $config.Configuration.UserName
Password = $config.Configuration.Password
SshHostKeyFingerprint = $config.Configuration.SshHostKeyFingerprint
}
I have more SFTP templates here people can use at
https://github.com/Richard-Barrett/ITDataServicesInfra/tree/master/SFTP

Email Account Lock Out Notification - Powershell

I will like to email the SysAdmin event id 4625 (Account lockout) occurs.
I have the following code, and it works just find. See output attached:
Current code:
$AccountLockOutEvent = Get-EventLog -LogName "Security" -InstanceID 4625 -Newest 1
$LockedAccount = $($AccountLockOutEvent.ReplacementStrings[0])
$AccountLockOutEventTime = $AccountLockOutEvent.TimeGenerated
$AccountLockOutEventMessage = $AccountLockOutEvent.Message
$messageParameters = #{
Subject = "Account Locked Out: $LockedAccount"
Body = "Account $LockedAccount was locked out on $AccountLockOutEventTime..`n`nEvent
Details:`n`n$AccountLockOutEventMessage"
From = ""
To = ""
SmtpServer = ""
}
Send-MailMessage #messageParameters
Question to Powershell gurus
1 - How can I capture the exact reason for lockout instead of %%2313 and other information such as the samaccountname. Instead using Account locked out s-1-0-0 in the subject line, I want to see the Account name there.
2 - is there a way get the ADuser information so that we can email the user at thesame time informing that that their account was locked out to contact the SysAdmin to unlock the account?
You can use this snippet to get an output that contains the fields you need.
SubjectUserName and SubjectDomainName.
$events = Get-WinEvent -FilterHashtable #{logname='Security'; ID=4625; } -MaxEvents 1
$event = $events
[xml]$eventXML = [xml]$Event.ToXml()
$eventXML.Event.EventData.Data
Output will look like this.
Name #text ---- -----
SubjectUserSid S-0-0-00-0000000000-0000000000-0000000000-0000
SubjectUserName MyUsername
SubjectDomainName MyHostname
SubjectLogonId 0x00000000
PrivilegeList SeSecurityPrivilege

How can I set the "Environment" > "Start the following program at logon" property on a local user using powershell?

Background:
I have a server with Windows 2008 R2 installed running as a terminal server session host. I have a long list of local users set-up and configured as remote desktop users. When the users remotely log on using remote desktop connection, a program automatically starts up. When the user closes that program, the session ends. This all works fine if I set it up manually.
My Question:
I have written a script to add a list of local users automatically and setup and configure the properties. The problem is that nowhere can I find how to set the "Environment" > "Start the following program at logon" properties. (See image for the properties I want to set)
A sample portion of my current script is as follow:
$computer = "localhost"
$userName = "aTestUser"
$objComputer = [ADSI]"WinNT://$computer"
$objUser = $objComputer.Create('user', $userName)
$objUser.SetPassword("Password")
$objUser.PSBase.InvokeSet('Description', "Some description for $userName")
$objUser.PSBase.InvokeSet('userflags', 512)
$objUser.PSBase.InvokeSet('passwordExpired', 1)
$objUser.SetInfo();
I also tried this command which doesn't work:
$objUser.PSBase.InvokeSet("TerminalServicesInitialProgram", "C:\programs\a_test_program.exe")
I have searched on Microsoft's MSDN site and Google and StackOverflow but could not find this specific property.
I found a solution here.
$ou = [adsi]"WinNT://127.0.0.1"
$user = $ou.psbase.get_children().find("test")
$user.PSBase.InvokeSet("TerminalServicesInitialProgram", "C:\logoff.bat")
$user.setinfo()
Okay, so I finally got it working. Seems like you have to first create the user then open it again for editing before the InvokeSet sets the TerminalServicesInitialProgram property.
I am not sure, maybe someone can share some experience or explanation.
Thank you to everyone for your help and assistance.
Working Code:
# Read the CSV file and create the users
# The CSV file structure is:
# UserName,FullName,Description
$Users = Import-Csv -Path "C:\Users.csv"
foreach ($User in $Users)
{
# adds user
$computer = "localhost"
$username = $User.UserName
#$username = "atest001"
$fullname = $User.FullName
#$fullname = "My Name"
$description = $User.Description
#$description = "A new user description"
$password = "MyGreatUnbreakableSecretPassword"
$objComputer = [ADSI]"WinNT://$computer"
$objUser = $objComputer.Create('user', $username)
$objUser.SetPassword($password)
$objUser.PSBase.InvokeSet("Description", $description)
$objUser.PSBase.InvokeSet('userflags', 65536)
$objUser.SetInfo();
# set password not to expire
#wmic USERACCOUNT WHERE "Name = '$username'" SET Passwordexpires=FALSE
# Add to groups
$group = [ADSI]"WinNT://./Power Users,group"
$group.Add("WinNT://$username,user")
$group = [ADSI]"WinNT://./WW_Users,group"
$group.Add("WinNT://$username,user")
$ou = [adsi]"WinNT://127.0.0.1"
$user = $ou.psbase.get_children().find($username)
$user.PSBase.InvokeSet("TerminalServicesInitialProgram", "C:\Program Files (x86)\Wonderware\InTouch\view.exe c:\program files (x86)\archestra\framework\bin\sibanyegold-kdce_app_tse1_test")
$user.PSBase.InvokeSet("MaxConnectionTime", 120)
$user.PSBase.InvokeSet("MaxDisconnectionTime", 1)
$user.PSBase.InvokeSet("MaxIdleTime", 30)
$user.PSBase.InvokeSet("BrokenConnectionAction", 1)
$user.PSBase.InvokeSet("ReconnectionAction", 1)
$user.PSBase.InvokeSet("FullName", $fullname)
$user.setinfo()
}

Ask for creds only if some specified

I'm trying to create a module with a bunch of functions, but I'm stuck with a problem: sometimes I need to run functions with a different from current credentials. But the thing is: I don't want to ask for credentials if I didn't specify a username. Like this:
function MyFunction ($ComputerName='localhost', $UserName) {
if ($UserName) {
Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName -Credential $UserName
} else {
Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName
}
}
Can I somehow get rid of the if statement? Can I just let the function use current credentials unless I specify -UserName?
If I leave it like this:
Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName -Credential $UserName
and call a function without specifying -UserName, it asks for credentialls every time. Yes, it uses current if I close the 'get-cred' box, but it's not what I want.
You could use splatting for dynamically building a parameter list, but you'd still need an if statement to decide whether or not to add the -Credential parameter:
function MyFunction ($ComputerName='localhost', $UserName) {
$params = #{
Class = 'Win32_OperatingSystem'
ComputerName = $ComputerName
}
if ($UserName) { $params['Credential'] = $UserName }
Get-WmiObject #params
}
You can populate a PSCredential object with current credentials using the default credential acquired like in here:
if ($UserName) {
$cred = get-credential $username # this prompts for credentials
} else {
$cred=[PSCredential][System.Net.CredentialCache]::DefaultCredentials
}
Then just use -credential $cred whenever you need without building a wall of if's.
EDIT: Apparently, as the current user's PSCredential object has its password as SecureString encrypted by the very same user's private key, if one would be able to get a PSCredential out of default credentials object, he'll be able to decrypt the password into plaintext, so this hole existed but was eventually closed. (Maybe let this answer hang here so that people won't get the asme error as I did) This question has the canonical answer on what arised in here.
Another way to do this might be using a variation of splatting, explained in detail by Ansgar Wiechers and here to construct only credential block in a single if statement and then use that block wherever you need, instead of writing direct -credential parameter.
$creds=#{}
if ($UserName) {
$cred = get-credential $username # this prompts for credentials
$creds['Credential']=$cred
}
And then add #creds wherever you require alternate credentials:
Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName #creds
This way you'll be asked for user's password once if $UserName is supplied, and then the $creds will either be empty or contain valid credentials of $UserName, so all subsequent if's can be replaced by explicit adding of #creds.
Your never really going to lose the IF, but you could create a string on the fly and then invoke it as and expression:
If $UserName is $null the credential parameter will not be added to the expression.
function MyFunction ([String]$ComputerName='localhost', [String]$UserName) {
Invoke-Expression ("Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName $(IF($UserName){'-Credential $UserName'})")
}

Query AD to find all the Computer in an OU with TC in their Name

I'm trying to search AD for all machines in a given OU that have 'TC' in their name, this is what I have so far, but its returning all machines, I need it to return just the machines with 'TC' in their name.
$root = ([adsi]'LDAP://OU=PCs,OU=Student Computers,DC=student,DC=belfastmet,DC=int','objectCategory=computer')
$searcher = new-object System.DirectoryServices.DirectorySearcher($root)
$searcher.filter = "(objectCategory=computer)"
$searcher.pageSize=1000
$searcher.propertiesToLoad.Add("name")
$computers = $searcher.findall()
$computers | foreach {$_.properties.name}
Not really sure what I should be doing from this point, I am a Powershell Newbie.
You have two options. You can get all computers and then filter using Powershell cmdlets, or your ldap filter reflects what you want (better). Try this:
$searcher.filter = "(&(objectCategory=computer)(cn=TN*))"
With ActiveDirectoryModule, you could find machines in a specific OU using filter and limit the search to the OU (assuming YourDomain.com\YourOU in the example below) you want with SearchBase:
$adcomputers = Get-ADComputer -Filter {name -like "TC*"} -Searchbase "OU=YourOU,DC=YourDomain,DC=com"
If you have the AD module available to you, you can do this with a single cmdlet.
get-adcomputer -filter {name -like "*TC*"}

Resources