2

It was pointed out to me (in PowerShell, replicate bash parallel ping) that I can load a function from the internet as follows:

iex (irm https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1)

The url referenced Test-ConnectionAsync.ps1 contains two functions: Ping-Subnet and Test-ConnectionAsync

This made me wonder if I could then define bypass functions in my personal module that are dummy functions that will be permanently overridden as soon as they are invoked. e.g.

function Ping-Subnet <mimic the switches of the function to be loaded> {
    if <function is not already loaded from internet> {
        iex (irm https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1)
    }
    # Now, somehow, permanently overwrite Ping-Subnet to be the function that loaded from the URL
    Ping-Subnet <pass the switches that we mimicked to the required function that we have just loaded>
}

This would very simply allow me to reference a number of useful scripts directly from my module but without having to load them all from the internet upon loading the Module (i.e. the functions are only loaded on demand, when I invoke them, and I will often never invoke the functions unless I need them).

YorSubs
  • 3,194
  • 7
  • 37
  • 60
  • A function that updates itself ? This wouldnt be robust by any means – Santiago Squarzon Apr 30 '22 at 15:00
  • For a repeatable exercise that I would use to perform an enterprise task, I agree, it would not be robust, but for my day to day console usage, it would be *incredibly* useful to be able to dynamically access various reliable function repositories online. – YorSubs Apr 30 '22 at 15:11
  • What happens when the remote repo gets compromised and suddenly you are running dramatically different code? I wouldn't go this route, personally. – FoxDeploy Apr 30 '22 at 17:25
  • I will of course only use repos that are old/stable/reliable. I would never recommend doing this for any serious project, but for home/console use, to have quick access to useful-and-stable repositories, I think it's pretty useful. What happens if VS Code gets compromised? What happens if components of Windows get compromised? There are a billion such scenarios of course. Santiago's method below looks really great (but your concern is not invalid!). – YorSubs Apr 30 '22 at 18:04

4 Answers4

3

You could use the Parser to find the functions in the remote script and load them into your scope. This will not be a self-updating function, but should be safer than what you're trying to accomplish.

using namespace System.Management.Automation.Language

function Load-Function {
    [cmdletbinding()]
    param(
        [parameter(Mandatory, ValueFromPipeline)]
        [uri] $URI
    )

    process {
        try {
            $funcs = Invoke-RestMethod $URI
            $ast = [Parser]::ParseInput($funcs, [ref] $null, [ref] $null)
            foreach($func in $ast.FindAll({ $args[0] -is [FunctionDefinitionAst] }, $true)) {
                if($func.Name -in (Get-Command -CommandType Function).Name) {
                    Write-Warning "$($func.Name) is already loaded! Skipping"
                    continue
                }
                New-Item -Name "script:$($func.Name)" -Path function: -Value $func.Body.GetScriptBlock()
            }
        }
        catch {
            Write-Warning $_.Exception.Message
        }
    }
}

Load-Function https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1
Ping-Subnet # => now is available in your current session.
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    I like the concept of this very much. Is there a way to contain the `using namespace System.Management.Automation.Language` part completely within the function in some way? The reason I ask is that in my main personal Module, I try to have nothing other than functions and each to be completely self contained (also so that each function is completely portable, as much as is feasibly possible of course). I tried putting the `using` inside the function and it throws errors. – YorSubs Apr 30 '22 at 18:21
  • 1
    @YorSubs `using` statements must be at the top of the script, if that's not an option you can use the type full name instead and remove the statement, in example, `[System.Management.Automation.Language.Parser]::ParseInput(...)` and `-is [System.Management.Automation.Language.FunctionDefinitionAst]`. – Santiago Squarzon Apr 30 '22 at 18:25
2
function Ping-Subnet{
    $toImport = (IRM "https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1").
                Replace([Text.Encoding]::UTF8.GetString((239,187,191)),"")
    NMO([ScriptBlock]::Create($toImport))|Out-Null
    $MyInvocation.Line|IEX
}
function Test-ConnectionAsync{
    $toImport = (IRM "https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1").
                Replace([Text.Encoding]::UTF8.GetString((239,187,191)),"")
    NMO([ScriptBlock]::Create($toImport))|Out-Null
    $MyInvocation.Line|IEX
}

Ping-Subnet -Result Success

Test-ConnectionAsync -Computername $env:COMPUTERNAME

Result:

Computername   Result
------------   ------
192.168.1.1   Success
192.168.1.2   Success
192.168.1.146 Success

Computername IPAddress                  Result
------------ ---------                  ------
HOME-PC      fe80::123:1234:ABCD:EF12  Success
mklement0
  • 382,024
  • 64
  • 607
  • 775
Fors1k
  • 490
  • 2
  • 7
  • This is fascinating and works perfectly from my tests, but I don't understand the `New-Module` line; could you expand on how that works please? Does it create a temporary Module in some way (and what name does that Module have?) Does this Module only exist within the context of the function and is then after use? – YorSubs May 01 '22 at 06:10
  • 1
    This line exports functions from the scrptblock. The new module is not added to the session. – Fors1k May 01 '22 at 08:41
  • Kudos for a clever solution; there are some edge cases that would be difficult to explain in comments, so I explained them in an answer. As an aside: `IRM` removes a UTF-8 BOM for you, as all file-reading cmdlets do. – mklement0 May 05 '22 at 19:48
  • Also: The new module _is_ added to the session - it is what exports the downloaded functions. Dynamic modules are not easy to discover, however, as they're not included in `Get-Module`'s output. You can see that the functions come from a dynamic module with `Get-Command Ping-Subnet`, for instance (column `Source`). – mklement0 May 05 '22 at 19:54
1

Yes, it should work. Calling Test-ConnectionAsync.ps1 from with-in a function will create the functions defined with-in, in the wrapping function's scope. You will be able to call any wrapped functions until the function's scope ends.

enter image description here

If you name the wrapper and wrapped functions differently, you can check whether the function has been declared with something like...

enter image description here

Otherwise, you need to get more creative.

This said, PROCEED WITH CAUTION. Remote code execution, like this, is fraught with security issues, especially in the way we're talking about it i.e., no validation of Test-ConnectionAsync.ps1.

Adam
  • 3,891
  • 3
  • 19
  • 42
1

Fors1k's answer deserves the credit for coming up with the clever fundamentals of the approach:

  • Download and execute the remote script's content in a dynamic module created with
    New-Module (whose built-in alias is nmo), which causes the script's functions to be auto-exported and to become available session-globally[1]

    • Note that dynamic modules aren't easy to discover, because they're not shown in
      Get-Module's output; however, you can discover them indirectly, via the .Source property of the command-info objects output by Get-Command:

      Get-Command | Where Source -like __DynamicModule_*
      
    • That the downloaded functions become available session-globally may be undesired if you're trying to use the technique inside a script that shouldn't affect the session's global state - see the bottom section for a solution.

  • Then re-invoke the function, under the assumption that the original stub function has been replaced with the downloaded version of the same name, passing the received arguments through.

While Fors1k's solution will typically work, here is a streamlined, robust alternative that prevents potential, inadvertent re-execution of code:

function Ping-Subnet{
  $uri = 'https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1'
  # Define and session-globally import a dynamic module based on the remote
  # script's content.
  # Any functions defined in the script would automatically be exported.
  # However, unlike with persisted modules, *aliases* are *not* exported by 
  # default, which the appended Export-ModuleMember call below compensates for.
  # If desired, also add -Variable * in order to export variables too.
  # Conversely, if you only care about functions, remove the Export-ModuleMember call.
  $dynMod = New-Module ([scriptblock]::Create(
    ((Invoke-RestMethod $uri)) + "`nExport-ModuleMember -Function * -Alias *")
  )
  # If this stub function shadows the newly defined function in the dynamic
  # module, remove it first, so that re-invocation by name uses the new function.
  # Note: This happens if this stub function is run in a child scope, such as
  #       in a (non-dot-sourced) script rather than in the global scope.
  #       If run in the global scope, curiously, the stub function seemingly
  #       disappears from view right away - not even Get-Command -All shows it later.
  $myName = $MyInvocation.MyCommand.Name
  if ((Get-Command -Type Function $myName).ModuleName -ne $dynMod.Name) {
    Remove-Item -LiteralPath "function:$myName"
  }
  # Now invoke the newly defined function of the same name, passing the arguments
  # through.
  & $myName @args
}

Specifically, this implementation ensures:

  • That aliases defined in the remote script are exported as well (just remove + "`nExport-ModuleMember -Function * -Alias *" from the code above if that is undesired.

  • That the re-invocation robustly targets the new, module-defined implementation of the function - even if the stub function runs in a child scope, such as in a (non-dot-sourced) script.

    • When run in a child scope, $MyInvocation.Line|IEX (iex is a built-in alias of the Invoke-Expression cmdlet) would result in an infinite loop, because the stub function itself is still in effect at that time.
  • That all received arguments are passed through on re-invocation without re-evaluation.

    • Using the built-in magic of splatting the automatic $args variable (@args) passes only the received, already expanded arguments through, supporting both named and positional arguments.[2]

    • $MyInvocation.Line|IEX has two potential problems:

      • If the invoking command line contained multiple commands, they are all repeated.

        • You can solve this particular problem by substituting (Get-PSCallStack)[1].Position.Text for $MyInvocation.Line, but that still wouldn't address the next problem.
      • Both $MyInvocation.Line and (Get-PSCallStack)[1].Position.Text contain the arguments that were passed in unexpanded (unevaluated) form, which causes their re-evaluation by Invoke-Expression, and the perils of that are that, at least hypothetically, this re-evaluation could involve lengthy commands whose output served as arguments or, worse, commands that had side effects that cannot or should not be repeated.


Scoping the technique to a given local script:

That the downloaded functions become available session-globally may be undesired if you're trying to use the technique inside a script that shouldn't affect the session's global state; that is, you may want the functions exported via the dynamic module to disappear when the script exits.

This requires two extra steps:

  • Piping the dynamic module to Import-Module, which is the prerequisite for being able to unload it before exiting with Remove-Module

  • Calling Remove-Module with the dynamic module before exiting in order to unload it.

function Ping-Subnet{
  $uri = 'https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1'
  # Save the module in a script-level variable, and pipe it to Import-Module
  # so that it can be removed before the script exits.
  $script:dynMod = New-Module ([scriptblock]::Create(
    ((Invoke-RestMethod $uri)) + "`nExport-ModuleMember -Function * -Alias *")
  ) | Import-Module -PassThru
  # If this stub function shadows the newly defined function in the dynamic
  # module, remove it first, so that re-invocation by name use the new function.
  # Note: This happens if this stub function is run in a child scope, such as
  #       in a (non-dot-sourced) script rather than in the global scope.
  #       If run in the global scope, curiously, the stub function seemingly
  #       disappears from view right away - not even Get-Command -All shows it later.
  $myName = $MyInvocation.MyCommand.Name
  if ((Get-Command -Type Function $myName).ModuleName -ne $dynMod.Name) {
    Remove-Item -LiteralPath "function:$myName"
  }
  # Now invoke the newly defined function of the same name, passing the arguments
  # through.
  & $myName @args
}

# Sample commands to perform in the script.
Ping-Subnet -?
Get-Command Ping-Subnet, Test-ConnectionAsync | Format-Table

# Before exiting, remove (unload) the dynamic module.
$dynMod | Remove-Module

[1] This assumes that the New-Module call itself is made outside of a module; if it is made inside a module, at least that module's commands see the auto-exported functions; if that module uses implicit exporting behavior (which is rare and not advisable), the auto-exported functions from the dynamic module would be included in that module's exports and therefore again become available session-globally.

[2] This magic has one limitation, which, however, will only rarely surface: [switch] parameters with a directly attached Boolean argument aren't supported (e.g., -CaseSensitive:$true) - see this answer.

mklement0
  • 382,024
  • 64
  • 607
  • 775