PSNTDS

NTDS.dit (Microsoft Active Directory) forensics module for Powershell.

Find it on Github here.

Current bug: Unencrypted NT hashes are incorrect!

This Powershell module is intended to aid in the process of examining and extracting information from the Active Directory database known as NTDS.dit.

Special thanks to Csaba Barta (csaba.barta@gmail.com, www.ntdsxtract.com) without whom this module would not be possible.

Also thanks to @moyix, moyix.blogspot.com, brendan@cs.columbia.edu

Also thanks to Brendan Dolan-Gavitt, author of creddump

Q: So why did I reinvent the wheel?

A: Because:

  1. Python shouldn't have all the fun. I wanted to port the ideas into idiomatic Powershell.
  2. I wanted to gain in-depth knowledge of the subject for myself, not just run someone else's scripts.
  3. I have further plans with this data and it will be very handy to have it in Powershell format.

Currently exported Cmdlets:

Export-NTDSFromNTDSUtilSnapshot
Export-SystemRegistryHive
Get-BootKeyFromSystemRegistryHive
Get-DecryptedHash
Get-DecryptedPEK
Get-RC4EncryptedData
Import-NTDSDatabaseFromFile

A couple of screenshots:

alt tag

alt tag

alt tag



Send-OutlookMailMessage with Voting Options

I needed a way to programmatically send emails through Outlook that contained "voting options" ... the little buttons that let the recipients vote yes or no or whatever on a given topic.  So I whipped up a reusable cmdlet right quick.

#
Function Send-OutlookMailMessage
{
    [CmdletBinding()]
    Param([Parameter(Mandatory=$True)][String[]]$To,
          [Parameter(Mandatory=$False)][String]$Subject,
          [Parameter(Mandatory=$False)][String]$Body,
          [Parameter(Mandatory=$False)][String[]]$Attachments,
          [Parameter(Mandatory=$False)][String[]]$VotingOptions,
          [Parameter(Mandatory=$False)][ValidateSet('High','Low','Normal')][String]$Priority = 'Normal',
          [Parameter(Mandatory=$False)][Switch]$BodyAsHTML = $False)

    BEGIN
    {
        Set-StrictMode -Version Latest
        [Bool]$ShouldProcess = $True
        [Bool]$OutlookWasAlreadyRunning = $True

        Try
        {
            Get-Process Outlook -ErrorAction Stop | Out-Null
        }
        Catch
        {
            $OutlookWasAlreadyRunning = $False
        }

        Try
        {
            $Outlook = New-Object -ComObject Outlook.Application -ErrorAction Stop
            If ($Outlook -EQ $Null)
            {
                Throw 'COM Object was null.'
            }
            Write-Verbose -Message 'Outlook COM object loaded.'
        }
        Catch
        {
            Write-Error "Failed to load Outlook! ($($_.Exception.Message))"        
            $ShouldProcess = $False
        }
    }
    PROCESS
    {
        If ($ShouldProcess)
        {
            Try
            {
                $Mail = $Outlook.CreateItem(0)
                If ($Mail -EQ $Null)
                {
                    Throw
                }
                Write-Verbose -Message 'Mail item created.'
            }
            Catch
            {
                Write-Error "Failed to create mail item! ($($_.Exception.Message))"
                $ShouldProcess = $False
                Return
            }

            Foreach ($Recipient In $To)
            {
                Try
                {
                    $Mail.Recipients.Add($Recipient) | Out-Null
                }
                Catch
                {
                    Write-Error "Failed to add recipient $Recipient"
                    $ShouldProcess = $False
                    Return
                }
                Write-Verbose -Message "Added recipient $Recipient"
            }

            If ($Subject.Length -GT 0)
            {
                $Mail.Subject = $Subject
            }
            Else
            {
                Write-Warning 'No email subject was supplied.'
            }
            If ($Body.Length -GT 0)
            {
                If ($BodyAsHTML)
                {
                    $Mail.HTMLBody = $Body
                }
                Else
                {
                    $Mail.Body = $Body
                }
            }
            Else
            {
                Write-Warning 'No email body was supplied.'
            }

            Foreach ($File In $Attachments)
            {
                Try
                {
                    # TODO: Suppress warning dialog box when attempting to send an 'unsafe' attachment?
                    $Mail.Attachments.Add($File) | Out-Null
                }
                Catch
                {
                    Write-Error "Error adding $File as an attachment!"
                    $ShouldProcess = $False
                    Return
                }
            }

            If ($VotingOptions.Count -GT 0)
            {
                $Mail.VotingOptions = $VotingOptions -Join ';'
            }

            Switch ($Priority)
            {
                'High'
                { 
                    $Mail.Importance = 2
                }
                'Low' 
                { 
                    $Mail.Importance = 0
                }
                'Normal' 
                { 
                    $Mail.Importance = 1
                }
                Default 
                { 
                    Write-Error "Debug priority!"
                    $ShouldProcess = $False
                    Return
                }
            }

            $Mail.Send()
        }
    }
    END
    {
        If ($Outlook)
        {
            If (-Not($OutlookWasAlreadyRunning))
            {
                $Outlook.Quit()
            }
            [System.Runtime.Interopservices.Marshal]::ReleaseComObject($Outlook) | Out-Null
        }
    }
}

Send-OutlookMailMessage -To 'ryan@myotherpcisacloud.com' -Subject 'Can you create an Outlook poll via Powershell?' -Body "

Well, can you?

" -VotingOptions 'Yes','No','IDK' -BodyAsHTML

Universal Pause Button

I like to play video games.  I also have a significant other, and she often walks into the room to talk to me while I'm playing a video game.  I would like to pause the game so that I can give her my undivided attention while she's talking to me, but a lot of games (particularly single-player ones) have these "un-pausable" cut scenes or other areas of the game where the normal pause functionality doesn't work.  This annoys both me and her, because I'm supposed to be the computer expert, and it looks like I don't even know how to pause my stupid video game.  So usually what ends up happening is I skip the cut scene and miss the story, or upset my SO by not paying attention to her as well as I should.

So that is why I wrote Universal Pause Button. It's a very simple Windows desktop app that sits in the system tray. Its icon resembles a pause button.  When you hit the actual Pause key (also known as Break) on your keyboard, the program determines which window is currently in the foreground (i.e. your game's window,) and pauses it.  No matter where you are in the game. Even in the middle of one of those pesky cut scenes that would otherwise be un-pausable.  When you press the key again (as long as you haven't since re-focused to another window,) the game will un-pause.

As of v1.0.3 you can now customize the "Pause" key that you want to use. Read the settings.txt file. The program reads the custom pause key from the settings.txt file during startup.

I've currently been testing this with The Witcher 3, and it is working great.  However, your mileage may vary. "Pausing" processes is something that usually only debuggers do, and I can't predict how your game will react to it.  Pausing processes may lead to race conditions among the threads of that process, but like I said, testing has been very positive for me so far.  I've already gotten great value out of the program, as there are lots of cut scenes in The Witcher 3, that I don't want to skip. The main use case for this app is single player games, as pausing your multi-player game will undoubtedly just get you kicked from the session, as if your computer had just crashed or hung. So don't use it in multi-player games.  It also works on applications that are not games at all.

Also, it's open source!  You can find the source on Github here.

If you prefer to just download the (signed) executable, here it is:

UniversalPauseButton.exe (124.3KB)

Poking Around DNS Scavenging Settings with Powershell

I've been working toward getting DNS scavenging turned on in a domain.  DNS scavenging, as you may know, takes a good deal of patience and forethought.  It's not something you want to just blindly enable without doing any reconnaissance first.  First off, since I'm new to this environment, let me scan all the domain controllers (which are also the DNS servers in this case) and see what the scavenging and aging settings currently look like:
 
#
$Servers = @()
ForEach ($DC In Get-ADDomainController -Filter *)
{
  $Server = New-Object PSObject -Property @{ 
     Hostname   = $DC.HostName
     Scavenging = $((Get-DnsServerScavenging `
                        -ComputerName $DC.HostName).ScavengingState)
     Aging      = $((Get-DnsServerZoneAging `
                        -Name 'acme.com' `
                        -ComputerName $DC.HostName).AgingEnabled)
    }
  $Servers += $Server
}

$Servers | FT -AutoSize
#
Hostname Scavenging Aging
-------- ---------- -----
DC01     False      True
DC02     False      True
DC03     False      True
DC04     False      True
DC05     False      True

So record aging is already turned on for the zone.  All that's left to do is enable scavenging on one of the DNS servers. (I don't like having all of the domain controllers scavenging - just one.)  But before I do that, I want to wait a while (like, a couple weeks) and see what hosts are updating their DNS records and which ones aren't. Lucky us - DNS server has a WMI provider.

#
$Records = Get-WmiObject 
              -Namespace 'Root\MicrosoftDNS' 
              -Query 'SELECT * FROM MicrosoftDNS_ResourceRecord WHERE Timestamp != 0'

$Records | Select TextRepresentation, `
           @{n='Timestamp'; e={([DateTime]'1/1/1601').AddHours($_.Timestamp)}} `
         | Where Timestamp -LT (Get-Date).AddDays(-30) | FT -AutoSize

The only tough bit is that the record's timestamp comes as a 32-bit integer that represents the number of hours elapsed since January 1st, 1601.  So you'd want to convert that into a meaningful date.  Now we can see which resource records in DNS aren't refreshing themselves on a regular basis.  After checking that list for sanity and correcting any problems, we can turn on scavenging. 

Get-Quotation: Something For Your Powershell Profile

I was playing around with my Powershell profile (again,) and I wanted to put a message-of-the-day style gizmo in there.  Now, a random quotation greets me every time I open Powershell:

  • Where do the quotes come from?

The function downloads them from www.quotationspage.com using Invoke-WebRequest.

  • What if I am not connected to the internet?

The function locally stores each unique quotation that it downloads. In the event that you are disconnected from the internet, the function will simply draw one of the locally cached quotes at random.

  • Where is my Powershell profile stored?

$Env:HOMEPATH\Documents\WindowsPowershell\Microsoft.Powershell_profile.ps1

  • Can I use this outside of my Powershell profile?

Sure.  It is just a function named Get-Quotation.

Here it is:

(Edit: A couple hours later, added wordwrap)

#
Function Get-Quotation
{
    Set-StrictMode -Version Latest

    $Form = @{'number'='1'; 
              'collection[0]'  = 'devils';
              'collection[1]'  = 'mgm';
              'collection[2]'  = 'motivate';
              'collection[3]'  = 'classic';
              'collection[4]'  = 'coles';
              'collection[5]'  = 'lindsly';
              'collection[6]'  = 'poorc';
              'collection[7]'  = 'altq';
              'collection[8]'  = '20thcent';
              'collection[9]'  = 'bywomen';
              'collection[10]' = 'contrib'}
    
    [String[]]$FormattedQuote = @()
    [Int]$MaxWidth = 0
    If ($Host.Name -EQ 'ConsoleHost')
    {
        $MaxWidth = $Host.UI.RawUI.WindowSize.Width
    }
    Else
    {
        $MaxWidth = 80
    }

    Try
    {
        $Page = Invoke-WebRequest http://www.quotationspage.com/random.php3 -Method Post -ContentType 'application/x-www-form-urlencoded' -Body $Form -ErrorAction Stop -TimeoutSec 5 -MaximumRedirection 0

        Foreach ($Element In $Page.AllElements)
        {
            If ($Element.tagName -EQ 'DL')
            {
                [String[]]$PreFormattedQuote = $Element.outerText -Split [Environment]::NewLine                

                For ($Index = 0; $Index -LT $PreFormattedQuote.Count; $Index++)
                {
                    If (($PreFormattedQuote[$Index].Length -GT 0) -AND -Not($PreFormattedQuote[$Index].Contains('More quotations on:')))
                    {
                        $FormattedQuote += $PreFormattedQuote[$Index]
                    }                    
                }
                $FormattedQuote[-1] = "`t-- $($FormattedQuote[-1])"                
            }
        }

        $Hasher = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider
        $Hashed = $Hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($FormattedQuote[0]))
        [String]$HashString = [BitConverter]::ToString($Hashed).Replace('-', $Null)

        If (-Not(Test-Path (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') -PathType Container))
        {
            New-Item (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') -ItemType Directory | Out-Null
        }

        If (-Not(Test-Path (Join-Path (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') $HashString) -PathType Leaf))
        {            
            $FormattedQuote | Out-File (Join-Path (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') $HashString)
        }
    }
    Catch
    {
        Write-Warning "Failed to get quotation from www.quotationspage.com. ($($_.Exception.Message))"

        $FormattedQuote = Get-Content ((Get-ChildItem (Join-Path $Env:LOCALAPPDATA 'Get-Quotation') | Get-Random).FullName)
    }

    # Word wrap!
    [Int]$Column = 0
    Foreach ($Line in $FormattedQuote)
    {
        If ($FormattedQuote.IndexOf($Line) -EQ ($FormattedQuote.Count -1))
        {
            Write-Host "`n$Line" -ForegroundColor DarkGray
            Continue
        }
        
        [String[]]$Words = $Line -Split ' '
        Foreach ($Word In $Words)
        {
            # Strip any control characters from the word.
            $Word = $Word.Replace('`r', $Null).Replace('`n', $Null).Replace('`t', $Null)

            $Column += $Word.Length + 1   
            If ($Column -GT ($MaxWidth - 8))
            {
                Write-Host
                $Column = 0
            }
            Write-Host "$Word " -NoNewline -ForegroundColor DarkCyan            
        }
        
    }
    Write-Host
}

Get-Quotation