salut,

invoke-expression, ou comment générer du code dynamiquement depuis une chaine de caractères, on va tout au long de ce petit how-to comprendre des techniques diverses pour travailler avec cette cmdlet, j'ai choisi de decouper ce howto en 3 sections qui sortent de l'ordinaire:


LES BONS:

- ajouter des fonctionnalités au code:

parfois on veux créer rapidement une fonctionnalité qui n'existe pas par defaut, prenons le cas de l'operateur "-replace" cet opérateur nous permet d'utiliser les RegExp's mais ne permet pas d'utiliser de delegués 'expression-lambda', pour palier a ce problème on peux utiliser une fonction externe et 'essayer de simuler les délégués'

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
PS > function foo($m) {"$m" * 2}
PS > $str="Aa1b2c3B"
PS > $str -creplace '([a-z])',(foo '$1')
Aaa1bb2cc3B
c'est parfait 'dans certains cas', mais ça reste limité:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
PS > function foo($m) {"$m".ToUpper()}
PS > $str="Aa1b2c3B"
PS > $str -creplace '([a-z])',(foo '$1')
Aa1b2c3B
une autre méthode plus puissante mais aussi plus 'artisanale' est de générer du code avec 'invoke-expression', Laurent nous a fait un bel exemple avec ceci:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
$S='www.developpez.com'
 #http://stormimon.developpez.com/dotnet/expressions-regulieres/#L3.7.2
$Pattern='(?<=\.)(.)'
#En plus compliqué : génération de code
#Ne nécessite pas de délégué, mais de l'aspirine
Invoke-Expression "`"$($S -replace $Pattern,'$(''$1''.ToUpper())')`""

LES BRUTES

- executer du code externe

je voulais mettre ceci danbs la section des 'MECHANTS' mais puisque ça m'arrive souvent de le faire, alors je le mets dans cette section.
parfois, on téléchage du NET, (de source fiable bien sur) des fichiers 'txt' contenant du code PS issu de conférence sur techdays ou autres,j'essaye d'executer ces codes dans un environment restreint, et puisque je suis un 'Lazy Man' j'utilise 'invoke-expression' pour executer tout le code en une seul fois, surtout si c'est une fonction ayant une centaine de lignes:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS > cat source_fiable.txt
function test-fonctionde10000ligne {
  write-host 'Hello'
}
# PS est intelligent il nous retourne une erreur parlante
PS > powershell -file source_fiable.txt
Échec du traitement de -File «*source_fiable.txt*», car le fichier ne comporte pas d'extension «*.ps1*». Spécifiez un nom de fichier de script PowerShell valide, puis réessayez.

# on execute le code contenu dans le fichier et non pas le fichier en lui même.
# 'iex' est l'alias de 'invoke-expression'
PS > cat source_fiable.txt | Out-String  | iex
PS > test-fonctionde10000ligne
Hello
REMARQUE: je sais que ceci n'est pas une bonne habitude , surtout si les codes à executer ne sont pas de sources fiables, mais le but est de montrer que cette technique est possible.

LES MECHANTS


securité

- injection de code:

l'injection de code est le pire ennemi des commandes de saisi d'utilisateur, 'Read-Host' n'echappe pas de cette "règle".
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PS > $prompt = read-host "Password"
Password: developpez.com ; rm $env:temp -whatif

PS > Invoke-Expression "echo $prompt"
developpez.com
WhatIf*: Opération «*Supprimer le répertoire*» en cours sur la cible «*x:\Documents and Settings\xxxx\Local Settings\Temp*».

PS > $prompt = read-host "Password"
Password: developpez.com ; &{echo "attaque reussi ;)"}

PS > Invoke-Expression "echo $prompt"
developpez.com
attaque reussi ;)

PS > $prompt = read-host "Password"
Password: developpez.com ; .{ stop-process -n *ss -whatif}
PS > Invoke-Expression "echo $prompt"
developpez.com
WhatIf*: Opération «*Stop-Process*» en cours sur la cible «*csrss (580)*».
WhatIf*: Opération «*Stop-Process*» en cours sur la cible «*lsass (668)*».
WhatIf*: Opération «*Stop-Process*» en cours sur la cible «*smss (512)*».
une solution, pour remedier à ce problème est de tester la saisi utilisateur avant de la passé à 'invoke-expression', pour ceci on a créer une petite fonction 'pouvant être extensible' pour verifier nos chaines de caractères:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
PS > function Protect-Code {
>>  param([Parameter(ValueFromPipeLine=$true)][string]$InputObject)
>>  $test = $false
>>  $test = $inputObject -match "(?<!'|"")[\.&]{.+}(?!""|')"
>>  if($test)
>>  {  write-Error $matches[0]
>>     return $false
>>  }
>>  $Parts = $InputObject.Split(';')
>>  if($Parts)
>>  {
>>      foreach($Part in $Parts) {
>>          $token,$null = $Part.Trim().Split(' ')
>>          if($token | get-command 2>$null)
>>          { write-error "$token"
>>            return $false
>>          }
>>      }
>>  }
>>  $InputObject
>> }

PS > $prompt = Read-Host -Prompt "Password" | Protect-Code
Password: pa@sw@rd; .{rm . -whatif}
Protect-Code : .{rm . -whatif}

PS > $prompt = Read-Host -Prompt "Password" | Protect-Code
Password: pa@sw@rd; ls
Protect-Code : ls
- passer au delà de la restriction de la session

en générant du code et en l'appelant dynamiquement, invoke-expression va creer son propre environement d'execution et va executer notre code:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
PS > function TopSecret {'Hello'}
PS > TopSecret
Hello
PS > (Get-Command TopSecret).Visibility = 'Private'
PS > TopSecret
Le terme «*TopSecret*» n'est pas reconnu comme nom d'applet de commande, fonction, fichier de script ou proamme exécutable.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PS > $tokens = 't' + 'o' + 'p' + 's' + 'e' + 'c' + 'r' + 'e' + 't'
PS > Invoke-Expression "$tokens"
Hello

PS > Invoke-Expression "get-command $tokens"

CommandType     Name                                           Definition
-----------     ----                                           ----------
Function        TopSecret                                      'Hello'


PS > Invoke-Expression "(get-command $tokens).Visibility = 'Public'"
PS > TopSecret
Hello
Remarque: on pouvez aussi appelez notre code avec le nom de la fonction directement:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
PS II> Invoke-Expression "topsecret"
mais j'ai voulu montré la flexibilité et le dynamisme de cette cmdlet.
Revenons maintenant à notre fonction 'TopSecret', Powershell a plusieurs couche de sécurités dans son runspace, la première couche, comme nous l'avons vu est le choix entre rendre notre 'code' privée ou public, la deuxième couche est de mettre une restriction sur le langage, les restrictions possibles sont:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
 PS > [enum]::getvalues('Management.Automation.PSLanguageMode')
FullLanguage
RestrictedLanguage
NoLanguage
par défaut c'est 'FullLanguage', essayons maintenant avec 'RestrictedLanguage' pour voir si cette 'barrière' peux être vraiment une 'barrière' devant du code générés:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
 PS > function TopSecret {'Hello'}
 PS > (gcm TopSecret).visibility="private"
 PS > &{TopSecret}
Hello
 PS > $ExecutionContext.SessionState.LanguageMode = 'restrictedLanguage'
 PS > &{TopSecret}
Les littéraux de blocs de script ne sont pas autorisés en mode de langage restreint ou dans une section Data.
 PS > iex "TopSecret"
Hello
il faut une couche de securite de plus pour securiser cette faille:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
 PS > powershell
 PS > function TopSecret {'Hello'}
 PS > (gcm TopSecret).visibility="private"
 PS > $ExecutionContext.SessionState.LanguageMode = 'nolanguage'
 PS > iex "Hello"
La syntaxe n'est pas prise en charge par cette instance d'exécution. Cela peut être dû au fait qu'elle est en mode sans langue.

performence

l'execution du code avec 'invoke-expression' souffre de problèmes de performance, c'est plus gourmand en mémoire et plus long en temps d'execution, parceque le parseur parse essaye de parser en deux étapes, la première 'invoke-expression' en elle-même puis le code à générer

prenons l'exemple des boucles, l'execution de 'invoke-expression' dans une boucle est extrement longue , il vaux mieux générer le code de la boucle.

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
PS > (measure-command {for($i=0; $i -lt 1000; $i++) {iex '"test"'}}).TotalMilliseconds
376,3031
PS > (measure-command  {iex 'for($i=0; $i -lt 1000; $i++) {"test"}'}).TotalMilliseconds
4,869
etapes d'interpretation des variables:

ces étapes peuvent rendre nos codes plus compliqué à comprendre mais comme à dit Laurent necessite de l'aspirine
voici un petit exemple montrant différentes étapes d'execution de variables:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
PS > $cmd='write-host -fore red -object '
PS > $inp='$Mystr'
PS > $Mystr = '$Another'
PS >
PS > invoke-expression "$cmd ```$inp"
$inp
 PS > invoke-expression "$cmd ``````$inp"
`$Mystr
PS > invoke-expression "$cmd ````````````$inp"
```$Another
 PS >
en raison, de "ces levels d'executions" et "les règles d'utilisation des guillemets simples et doubles" le debogage du code généré avec "invoke-expression" peux devenir extrement compliqué.