Disable Invoke-Expression


There was an interesting discussion in one of our internal discussion groups about disabling Invoke-Expression, so I decided to roll up my sleeves and check if it can really be done.

 

First of all, what is Invoke-Expression?

The Invoke-Expression cmdlet evaluates or runs a specified string as a command and returns the results of the expression or command” (Get-Help Invoke-Expression). Meaning Invoke-Expression is considered harmful and opens you up to code injection attacks.

Even the notes in the help include: “Take reasonable precautions when using the Invoke-Expression cmdlet in scripts. When using Invoke-Expression to run a command that the user enters, verify that the command is safe to run before running it. In general, it is best to design your script with predefined input options, rather than allowing freeform input.

For example, let’s look at the function below:

function Test-InvokeExpression($Path) {

    Invoke-Expression “Get-ChildItem -Path $Path

}

While calling the function with Test-InvokeExpression -Path ‘C:\’ would output the expected results,

calling the function with Test-InvokeExpression -Path ‘C:\; Get-Process’ is probably not the way we thought the function would be used. This is similar to the notorious SQL injection attacks.

 

And since hackers love PowerShell (what’s not to like?), and they love Invoke-Expression, I guess someone at an InfoSec team thought it would be clever to ask their IT team to disable the cmdlet and all it’s aliases (iex).

So, can we really disable a built-in cmdlet like Invoke-Expression?

 

Taking advantage of the Command Precedence rules:

Functions take precedence to cmdlets, so if we were to create a function named Invoke-Expression:

function Invoke-Expression {

    Write-Warning ‘Invoking expressions is not permitted in this runspace’

}

and then run Invoke-Expression, it would run our function and not the built-in cmdlet:

PS C:\> Invoke-Expression “Get-Random”

WARNING: Invoking expressions is not permitted in this runspace

 

PS C:\> iex “Get-Random”

WARNING: Invoking expressions is not permitted in this runspace

 

 

But… one could easily call the fully qualified cmdlet and get around our little hack:

PS C:\> Microsoft.PowerShell.Utility\Invoke-Expression “Get-Random”

3141529653

 

But #2, since aliases take precedence as well, we can create an alias:

New-Alias -Name Microsoft.PowerShell.Utility\Invoke-Expression -Value Invoke-Expression -Option Constant

(The -Option Constant will make sure our alias cannot be removed from the runspace) 

 

But #3, If you’ve ever played with proxy functions, you know you can get a cmdlet with it’s metadata and execute it. Using the same technique we can get the real Invoke-Expression cmdlet:

$iex = $ExecutionContext.InvokeCommand.GetCommand(‘Invoke-Expression’, `

    [System.Management.Automation.CommandTypes]::Cmdlet)

 

And run it using the invoke operator (&):

PS C:\> & $iex “Get-Random”

2718281828

 

That got me thinking about the execution context…

 

Manipulating ExecutionContexts’ PostCommandLookupAction:

The $ExecutionContext variable contains an EngineIntrinsics object that represents the execution context of the Windows PowerShell host. You can use this variable to find the execution objects that are available to cmdlets (Get-Help about_Automatic_Variables)

So I ended up with the code below, to intercept the call to Invoke-Expression, it’s iex alias or any other user-created alias that points to it:

$ExecutionContext.SessionState.InvokeCommand.PostCommandLookupAction = {

    param($CommandName, $CommandLookupEventArgs)

 

    $allowedCommands = ‘Out-Default’, ‘prompt’, ‘TabExpansion2’, ‘PSConsoleHostReadLine’

    if($CommandLookupEventArgs.CommandOrigin -eq ‘Runspace’ `

        -and $allowedCommands -notcontains $CommandName) {           

 

        $CommandLookupEventArgs.CommandScriptBlock = {

           

            $disableCommands = @(Get-Alias |

                Where-Object { $_.ResolvedCommand.Name -eq ‘Invoke-Expression’ } |

                    Select-Object -ExpandProperty Name) + ‘Invoke-Expression’

 

            if($disableCommands -contains $CommandName) {

                Write-Warning ‘Invoking expressions is not permitted in this runspace’

            } else {

                & $CommandName @args

            }

        }.GetNewClosure()

    }

}

Now, the hacker can’t call the Invoke-Expression cmdlet. Can he?

But #4, just as you and I can manipulate the PostLookupAction, so can the hacker. As there is no way (AFAIK) to make the scriptblock constant (as we did with the alias in but #2).

 

Conclusion:

You cannot really disable the Invoke-Expression cmdlet. The steps described above will only “slow down” the hacker. Remember, once an attacker can execute arbitrary code on your system there’s not much that can help you. Blocking Invoke-Expression really isn’t solving any security problems any more than disabling PowerShell is.

BTW, setting the Execution Policy to AllSigned (or similar) is also not the answer. As the Execution Policy is not a security mechanism, it’s just a safety latch. But I should probably leave this for a future post.

One last note: If you need to run an executable or complex external command, be sure to check all the options, don’t go just go with the notorious Invoke-Expression.

 

HTH,
\Martin.

Comments (2)

  1. June Blender says:

    Excellent post. Another strategy is to create a custom endpoint (session configuration) without Invoke-Expression. You would then have to set the ACLs to require that all users use the new endpoint.

    about_Session_Configurations.help.txt
    about_Session_Configuration_Files.help.txt

    1. I agree. Constrained endpoints (and JEA) are great.
      But the customer’s requirement was to disable the Invoke-Expression cmdlet even in the interactive shell.

Skip to main content