IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Scripts/Batch Discussion :

Invoke-expression: les bons, les brutes et les méchants


Sujet :

Scripts/Batch

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre Expert
    Avatar de I'm_HERE
    Homme Profil pro
    Inscrit en
    Juillet 2008
    Messages
    1 013
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Tunisie

    Informations forums :
    Inscription : Juillet 2008
    Messages : 1 013
    Par défaut 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 : 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é.

  2. #2
    Rédacteur


    Profil pro
    Inscrit en
    Janvier 2003
    Messages
    7 171
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2003
    Messages : 7 171
    Billets dans le blog
    1
    Par défaut
    Bonjour Walid,
    qq remarques sur le contenu de ton post.
    Citation Envoyé par I'm_HERE Voir le message
    je sais que ceci n'est pas une bonne habitude
    Disons qu'Invoke-Expression n'est pas fait pour ça, mais pour exécuter du code construit dynamiquement. C'est plus un détournement.

    Citation Envoyé par I'm_HERE Voir le message
    une solution, pour remedier à ce problème
    La solution, dans un environnement de production, est de ne pas coupler la saisie utilisateur avec Invoke-Expression.
    Et te lisant, je me suis souvenu que l'usage de paramètre de type scriptblock porte aussi qq risques...

    Citation Envoyé par I'm_HERE Voir le message
    en générant du code et en l'appelant dynamiquement, invoke-expression va creer son propre environement d'execution
    Je reste dubitatif sur ta formulation, ceci dit je n'avais jamais utilisé cette possiblité.
    Si je reprend ton code en y ajoutant deux appels de scriptblock, ceux-ci, si on parle bien de la même chose, ne créent pas de nouvel environnement d'exécution (Runspace) :
    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
    function TopSecret {'Hello'}
    TopSecret
    #Hello
    (Get-Command TopSecret).Visibility = 'Private'
    TopSecret
    #Erreur
    
    .{TopSecret}
    #Hello
    
    &{TopSecret}
    #Hello
    dir function:topsecret
    #Erreur
    
    &{dir function:topsecret}
    #ok
    Autre exemple, le message d'erreur est plus explicite :
    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
    $MaVariable='Test'
    $MaVariable
    function test { "var=$MaVariable" }
    Test
    #var=Test
    
    (Get-Variable MaVariable).Visibility='private'
    $MaVariable
    #Erreur
    #Impossible d'accéder à la variable « $MaVariable », car il s'agit d'une variable privée
    Test
    #var=Test
    
     .{(Get-Variable MaVariable).visibility='Public'}
    $MaVariable
    Et avec un job, qui lui crée un nouveau runspace, que la visibilité soit public ou private ne change rien:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    $j=start-job {topsecret}|Wait-job
    Receive-Job $j -Keep
    #Receive-Job : Le terme « topsecret » n'est pas reconnu comme nom d'applet de commande, fonction, fichier de script o
    Remove-Job $j
    Enfin avec un module qui lui ne crée pas de Runspace, mais une portée différente :
    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
    $Mymodule="c:\temp\test.psm1"
    @"
    Write-Warning ([runspace]::DefaultRunspace.InstanceID)
    
    function go {
     topsecret
    }
    
    "@ > $Mymodule
    
    [runspace]::DefaultRunspace.InstanceID
    #53b0f23f-d581-4483-99a5-c5513807e251
    
    ipmo $Mymodule -force
    
    Go
    #Hello
    On voit que l'appel à la fonction go se comporte correctement.

    Ainsi, avec (Visibility='private'), PS empêche l'accès direct à la commande ou à la variable, mais autorise son accès via un appel interne.

    C'est ce qui permet de la masquer, tout en laissant la possibilité de l'utiliser dans une fonction.
    Pour ceci, Invoke-Expression fait la même chose et ce n'est pas un comportement spécifique.

    Citation Envoyé par I'm_HERE Voir le message
    Il faut une couche de securite de plus pour securiser cette faille:
    Je dirais plutot un niveau de restriction, ici le plus restreint.
    Quand on parle de 'Runspace contraint' il n'y pas plusieurs contraintes, mais une seule qui permet de jouer sur le niveau souhaité.
    Mais il y a bien plusieurs fonctionnalités qui soient s'imbriquent ou se complètent lors de la construction de 'Runspace contraint' (bien que ce ne soit pas le sujet d'origine).
    Citation Envoyé par I'm_HERE Voir le message
    prenons l'exemple des boucles, l'execution de 'invoke-expression' dans une boucle est extrement longue
    C'est le nombre d'itération qui influe sur le résultat :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    (measure-command {iex "'test'"}).TotalMilliseconds;(measure-command {"'test'"}).TotalMilliseconds
    #0,2057
    #0,0327
    On reste en dessous de la milliseconde tout de même...
    C'est plus long, mais pas plus que la première exécution d'un script.
    Citation Envoyé par I'm_HERE Voir le message
    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é.
    Certes, mais lors du développement on s'en rend vite compte et on enclenche rapidement la marche arrière, en tout cas c'est ce que je fais.
    C'est la relecture et l'évolution du code, c'est à dire la maintenance, par un tiers ou par soi même qq mois plus tard qui est difficile.
    Il faut obligatoirement documenter ce type de code.

    Il reste aussi un usage à IEX qui est de construire une ligne d'appel de cmd externe + facilement.

    Un sujet intéressant !

  3. #3
    Membre Expert
    Avatar de I'm_HERE
    Homme Profil pro
    Inscrit en
    Juillet 2008
    Messages
    1 013
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Tunisie

    Informations forums :
    Inscription : Juillet 2008
    Messages : 1 013
    Par défaut
    Salut Laurent,

    Citation Envoyé par Laurent Dardenne Voir le message
    Disons sous-expression n'est pas fait pour ça, mais pour exécuter du code construit dynamiquement. C'est plus un détournement.

    je suis d'accord avec toi sur le fait que IEX est concu pour construire du code dynamique mais ceci ne veux pas dire que chaque chose doit seulement faire ce qu'elle est sensé de faire par convention ou par sa spécification, prenons l'exemple du domaine WEB et plus précisement du developpement WEB, les balises HTML "table" ont été concu pour faire des données tabulaires et/ou des tableaux, mais ceci n'a pas empecher le génie des developpeurs de la rendre la base des balises de mise en page même si ceci a impliqué par la suite des codes sources illisibles...
    connaitre les 'règles' d'une chose et la suivre pas à pas c'est bien mais allez au delà des regles ça reste une autre histoire


    Citation Envoyé par Laurent Dardenne Voir le message
    Enfin avec un module qui lui ne crée pas de Runspace, mais une portée différente :
    j'ai fais un petit test est il s'est avéré que la portée du module n'autorise pas un acces privée uniquement mais permet aussi de changé du code à la volée:

    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
    
    PS > $modPath = "$env:temp\test.psm1"
    
    PS > @'
    >>   $arg = 'walid'
    >>   $function:sayhello = { write-host "hello $arg" -fore red }
    >> '@ > $modPath
    >>
    
    PS > Import-Module $modPath -Verbose -PassThru | sv modObj
    COMMENTAIRES*: Exportation de la fonction «*sayhello*».
    COMMENTAIRES*: Importation de la fonction «*sayhello*».
    
    PS > sayhello
    hello walid
    
    PS > &$modObj {$script:arg = 'World'}
    
    PS > sayhello
    hello World
    Citation Envoyé par Laurent Dardenne Voir le message
    Il reste aussi un usage à IEX qui est de construire une ligne d'appel de cmd externe + facilement.
    et dans quel but

  4. #4
    Rédacteur


    Profil pro
    Inscrit en
    Janvier 2003
    Messages
    7 171
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2003
    Messages : 7 171
    Billets dans le blog
    1
    Par défaut
    Bonjour Walid,
    Citation Envoyé par I'm_HERE Voir le message
    mais ceci ne veux pas dire que chaque chose doit seulement faire ce qu'elle est sensé de faire par convention ou par sa spécification
    D'où l'usage du mot 'détournement' qui n'est pas un interdit ;-)

    Citation Envoyé par I'm_HERE Voir le message
    j'ai fais un petit test est il s'est avéré que la portée du module n'autorise pas un acces privée uniquement mais permet aussi de changé du code à la volée:
    Oui tout à fait.
    Ma remarque pointait juste le fait qu'Invoke-Expression ne crée pas son propre environement d'exécution à la différence d'un runspace.
    Ici on est plus dans les notions de portée, de contexte de module et d'état de session, mais comme leur implémentation n'est pas documenté difficile de dire où commence l'un et ou finit l'autre.

    Ceci dit, pour cette syntaxe :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
     
    PS > &$modObj {$script:arg = 'World'}
    Je ne l'utilise pas, sauf lors des tests des méthodes et/ou des données privées déclarées dans un module.
    Citation Envoyé par I'm_HERE Voir le message
    et dans quel but
    En v1,v2 le parseur est trop 'strict' quant à l'interprétation de certaines ligne d'arguments d'un programme, Invoke-Expression permet de la construire en utilisant un mode de parsing plus 'souple'.
    La v3 permet, par l'utilisation du token '--%', d'indiquer où s'arrête le parsing dans une ligne d'arguments, voir ce blog ou l'aide en ligne de la v3 :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    #ou 
    Help Parsing # env 3
    C'est ici aussi un usage détourné d'Invoke-Expression...

  5. #5
    Rédacteur


    Profil pro
    Inscrit en
    Janvier 2003
    Messages
    7 171
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2003
    Messages : 7 171
    Billets dans le blog
    1
    Par défaut
    Concernant l'usage d'Invoke-Expression, voici un article sur la prévention d'injection de code sous Powershell.

  6. #6
    Membre Expert
    Avatar de I'm_HERE
    Homme Profil pro
    Inscrit en
    Juillet 2008
    Messages
    1 013
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Tunisie

    Informations forums :
    Inscription : Juillet 2008
    Messages : 1 013
    Par défaut
    salut Lauent,

    Citation Envoyé par Laurent Dardenne Voir le message
    Concernant l'usage d'Invoke-Expression, voici un article sur la prévention d'injection de code sous Powershell.
    merci Laurent pour ce lien...j'espère qu'ils vont mettre la video en public.

Discussions similaires

  1. Réponses: 7
    Dernier message: 23/09/2009, 10h02
  2. Réponses: 0
    Dernier message: 21/09/2009, 23h41
  3. choisir ds une liste charge une autre liste par les bons elements
    Par kamaldev dans le forum Général JavaScript
    Réponses: 2
    Dernier message: 25/07/2006, 10h06
  4. Réponses: 4
    Dernier message: 10/07/2006, 20h55

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo