Invoke-expression: les bons, les brutes et les méchants
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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é.