Notice: Due to size constraints and loading performance considerations, scripts referenced in blog posts are not attached directly. To request access, please complete the following form: Script Request Form Note: A Google account is required to access the form.
Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

Powershell Forensics : Server-based Command History Hunter


Auditing of PowerShell usage across multiple servers in our corporate environment is quite a good idea whether it's for security investigations, compliance audits, or simply understanding what commands are being run on critical systems, having access to PowerShell command history can be invaluable.

Recently, I developed a PowerShell script that simplifies this process by collecting ConsoleHost_history.txt files from all users across remote servers using SMB shares

Why PowerShell History Collection Matters in Corporate Environments

In enterprise environments, PowerShell has become the go-to tool for system administration, automation, and even some security operations. However, this widespread adoption creates several challenges:

Security Auditing: When investigating potential security incidents, knowing what PowerShell commands were executed can provide crucial forensic evidence. Did someone run suspicious scripts? Were administrative commands executed during off-hours?

Troubleshooting: When systems behave unexpectedly, reviewing recent PowerShell activity can help identify what changes were made and when.

The challenge is that these history files are stored locally in each user's profile directory: C:\Users\<username>\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadline\ConsoleHost_history.txt. Manually collecting these files from multiple servers and users is time-consuming and error-prone.

The SMB Share Approach

Rather than using PowerShell remoting, I opted for SMB shares. This approach has several advantages in corporate environments:

# Test server connectivity via SMB
$TestPath = "\\$Server\C$"
$TestAccess = Test-Path $TestPath
if (!$TestAccess) {
    Write-LogAndConsole "Cannot access SMB share \\$Server\C$ - Check network connectivity and permissions" "ERROR"
    exit 1
}

SMB shares are typically already enabled and don't require additional configuration like WinRM. Most Windows administrators already have the necessary permissions to access administrative shares.

Discovering User Profiles

The script automatically discovers all user profiles on the target server:

$RemoteUsersPath = "\\$Server\C$\Users"
$UserProfiles = Get-ChildItem -Path $RemoteUsersPath -Directory | Where-Object { 
    $_.Name -notlike "Public" -and $_.Name -notlike "Default*" -and $_.Name -notlike "All Users" 
}

This approach filters out system accounts and default profiles, focusing only on actual user accounts that might have PowerShell history.

Error Handling

One of the key requirements was handling missing files without throwing errors. Not every user will have a PowerShell history file:

try {
    if (Test-Path $RemoteHistoryPath) {
        Copy-Item -Path $RemoteHistoryPath -Destination $LocalFilePath -Force
        Write-LogAndConsole "Successfully copied history file for user: $Username"
        $SuccessCount++
    } else {
        Write-LogAndConsole "History file not found for user: $Username - Skipping" "WARN"
        $SkippedCount++
    }
} catch {
    Write-LogAndConsole "Error processing user $Username : $($_.Exception.Message)" "ERROR"
    $ErrorCount++
}

This ensures the script continues processing even when individual files are missing or inaccessible.

Organized File Structure

The script creates a logical folder structure that makes it easy to manage collections from multiple servers:

Servers/
├── Server01/
│   ├── john_ConsoleHost_history.txt
│   ├── jane_ConsoleHost_history.txt
│   └── admin_ConsoleHost_history.txt
├── Server02/
│   ├── bob_ConsoleHost_history.txt
│   └── alice_ConsoleHost_history.txt
└── DatabaseServer/
    ├── sqluser_ConsoleHost_history.txt
    └── backup_ConsoleHost_history.txt

Comprehensive Logging

Every operation is logged both to the console and to a persistent log file:

function Write-LogAndConsole {
    param([string]$Message, [string]$Level = "INFO")
    $LogEntry = "$Timestamp [$Level] $Message"
    Write-Host $LogEntry
    Add-Content -Path $LogFile -Value $LogEntry
}

This dual logging approach ensures you can monitor the script's progress in real-time while maintaining a permanent record of all operations.

Script : Collect-PSHistory.ps1

Here's the complete PowerShell script that implements all the features discussed:

param(
    [Parameter(Mandatory=$true)]
    [string]$Server
)

# Initialize variables
$ScriptPath = $PSScriptRoot
$ServersFolder = Join-Path $ScriptPath "Servers"
$ServerFolder = Join-Path $ServersFolder $Server
$LogFile = Join-Path $ScriptPath "PowerShellHistoryCollector.log"
$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

# Function to write to log and console
function Write-LogAndConsole {
    param([string]$Message, [string]$Level = "INFO")
    $LogEntry = "$Timestamp [$Level] $Message"
    Write-Host $LogEntry
    Add-Content -Path $LogFile -Value $LogEntry
}

# Create necessary folders
try {
    if (!(Test-Path $ServersFolder)) {
        New-Item -ItemType Directory -Path $ServersFolder -Force | Out-Null
        Write-LogAndConsole "Created Servers folder: $ServersFolder"
    }
    
    if (!(Test-Path $ServerFolder)) {
        New-Item -ItemType Directory -Path $ServerFolder -Force | Out-Null
        Write-LogAndConsole "Created server folder: $ServerFolder"
    }
} catch {
    Write-LogAndConsole "Error creating folders: $($_.Exception.Message)" "ERROR"
    exit 1
}

Write-LogAndConsole "Starting PowerShell history collection from server: $Server (using SMB shares)"

# Test server connectivity via SMB
try {
    $TestPath = "\\$Server\C$"
    $TestAccess = Test-Path $TestPath
    if (!$TestAccess) {
        Write-LogAndConsole "Cannot access SMB share \\$Server\C$ - Check network connectivity and permissions" "ERROR"
        exit 1
    }
    Write-LogAndConsole "Successfully accessed SMB share: \\$Server\C$"
} catch {
    Write-LogAndConsole "Error accessing SMB share \\$Server\C$ : $($_.Exception.Message)" "ERROR"
    Write-LogAndConsole "Make sure you have admin rights and the server is accessible via SMB" "ERROR"
    exit 1
}

# Get all user profiles from the remote server using SMB share
try {
    $RemoteUsersPath = "\\$Server\C$\Users"
    $UserProfiles = Get-ChildItem -Path $RemoteUsersPath -Directory | Where-Object { $_.Name -notlike "Public" -and $_.Name -notlike "Default*" -and $_.Name -notlike "All Users" }
    
    Write-LogAndConsole "Found $($UserProfiles.Count) user profiles on server $Server"
} catch {
    Write-LogAndConsole "Error accessing user profiles via SMB share \\$Server\C$\Users : $($_.Exception.Message)" "ERROR"
    Write-LogAndConsole "Make sure you have admin access to the server and C$ share is accessible" "ERROR"
    exit 1
}

$SuccessCount = 0
$SkippedCount = 0
$ErrorCount = 0

# Process each user profile
foreach ($UserProfile in $UserProfiles) {
    $Username = $UserProfile.Name
    $RemoteHistoryPath = "\\$Server\C$\Users\$Username\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadline\ConsoleHost_history.txt"
    $LocalFileName = "${Username}_ConsoleHost_history.txt"
    $LocalFilePath = Join-Path $ServerFolder $LocalFileName
    
    Write-LogAndConsole "Processing user: $Username"
    
    try {
        # Check if the remote file exists
        if (Test-Path $RemoteHistoryPath) {
            # Copy the file
            Copy-Item -Path $RemoteHistoryPath -Destination $LocalFilePath -Force
            Write-LogAndConsole "Successfully copied history file for user: $Username"
            $SuccessCount++
        } else {
            Write-LogAndConsole "History file not found for user: $Username - Skipping" "WARN"
            $SkippedCount++
        }
    } catch {
        Write-LogAndConsole "Error processing user $Username : $($_.Exception.Message)" "ERROR"
        $ErrorCount++
    }
}

# Summary
Write-LogAndConsole "=" * 50
Write-LogAndConsole "COLLECTION SUMMARY FOR SERVER: $Server"
Write-LogAndConsole "=" * 50
Write-LogAndConsole "Successfully copied: $SuccessCount files"
Write-LogAndConsole "Skipped (file not found): $SkippedCount users"
Write-LogAndConsole "Errors encountered: $ErrorCount users"
Write-LogAndConsole "Total users processed: $($UserProfiles.Count)"
Write-LogAndConsole "Files saved to: $ServerFolder"
Write-LogAndConsole "Log file: $LogFile"
Write-LogAndConsole "Collection completed at: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"

if ($SuccessCount -gt 0) {
    Write-LogAndConsole "Collection completed successfully!" "SUCCESS"
} else {
    Write-LogAndConsole "No files were collected. Check the log for details." "WARN"
}

Usage Commands

Save the script as and run it with:

.\Collect-PSHistory.ps1 -Server "<server-name>"

The script will create  a "Servers" folder in the same directory, organize files by server name, and provide detailed logging of the entire process.

Previous Post Next Post

نموذج الاتصال