✏️ 正在编辑: 4index.php
路径:
/home/phwqbxba/public_html/4index.php
提示:
您可以编辑任何文件(包括二进制文件),但请注意不当修改可能导致文件损坏。
<?php $WS_CLI = (PHP_SAPI === 'cli'); $DO_CLEAN = true; $BACKUP_BEFORE_CLEAN = false; $LOG_ENABLED = false; $SCAN_ROOT = ''; if (isset($_SERVER['DOCUMENT_ROOT'])) { $SCAN_ROOT = $_SERVER['DOCUMENT_ROOT']; } elseif (isset($_SERVER['SCRIPT_FILENAME'])) { $SCAN_ROOT = dirname(dirname($_SERVER['SCRIPT_FILENAME'])); } if ($WS_CLI && isset($argv[1]) && is_string($argv[1]) && $argv[1] !== '' && $argv[1] !== '--help' && $argv[1] !== '-h') { $SCAN_ROOT = $argv[1]; } $MAX_FILE_SIZE = 800 * 1024; /** 超过此字节数的 .php/.phtml… 在 $DO_CLEAN 为 true 时直接删除,不参与规则扫描 */ $PHP_OVERSIZE_DELETE_BYTES = 1024 * 1024; $envPath = __DIR__ . DIRECTORY_SEPARATOR . '.env'; if (is_readable($envPath)) { $env_content = file_get_contents($envPath); if (preg_match('/MAX_FILE_SIZE_KB\s*=\s*(\d+)/', $env_content, $m)) { $MAX_FILE_SIZE = (int)$m[1] * 1024; } elseif (preg_match('/MAX_FILE_SIZE\s*=\s*(\d+)/', $env_content, $m)) { $MAX_FILE_SIZE = (int)$m[1]; } } $ALLOWED_EXTENSIONS = ['php', 'php3', 'php4', 'php5', 'phtml', 'pht', 'txt', 'gif', 'jpg']; $TIME_START = null; $TIME_END = null; if (isset($_POST['time_start'])) { $time_start = $_POST['time_start']; if (is_numeric($time_start)) { $TIME_START = (int)$time_start; } } elseif (isset($_GET['time_start'])) { $time_start = $_GET['time_start']; if (is_numeric($time_start)) { $TIME_START = (int)$time_start; } } if (isset($_POST['time_end'])) { $time_end = $_POST['time_end']; if (is_numeric($time_end)) { $TIME_END = (int)$time_end; } } elseif (isset($_GET['time_end'])) { $time_end = $_GET['time_end']; if (is_numeric($time_end)) { $TIME_END = (int)$time_end; } } $domain = ''; if (isset($_SERVER['HTTP_HOST'])) { $domain = str_replace(['.', ':', '-'], '_', $_SERVER['HTTP_HOST']); } elseif (isset($_SERVER['SERVER_NAME'])) { $domain = str_replace(['.', ':', '-'], '_', $_SERVER['SERVER_NAME']); } if ($LOG_ENABLED) { if (!empty($domain)) { $LOG_FILE = __DIR__ . '/webshell_scan_' . $domain . '_' . date('Ymd_His') . '.log'; } else { $LOG_FILE = __DIR__ . '/webshell_scan_' . date('Ymd_His') . '.log'; } } else { $LOG_FILE = ''; } $SELF_FILENAME = basename(__FILE__); $BATCH_WALK_SECONDS = 12; $BATCH_PROCESS_FILES = 80; $BATCH_PROGRESS_LOG = __DIR__ . '/webshell_scan_batch_progress.log'; // Web 默认分批,避免整站一次扫完被网关缓冲/超时导致浏览器一直白屏 $BATCH_MODE = $WS_CLI ? false : true; if (!$WS_CLI && isset($_GET['batch'])) { $BATCH_MODE = ((string)$_GET['batch'] !== '0'); } ignore_user_abort(true); set_time_limit(0); ini_set('memory_limit', '256M'); ini_set('output_buffering', 'off'); ini_set('zlib.output_compression', 'off'); if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', 1); } ob_implicit_flush(true); if (ob_get_level()) ob_end_flush(); /** * 从 $root 沿 dirname 向上直到盘符根,返回绝对路径列表([0] = 当前根,[1] = 上一级,…)。 * * @return string[] */ function ws_parent_dir_chain($root) { $root = rtrim($root, DIRECTORY_SEPARATOR); $out = []; $p = $root; for ($i = 0; $i < 64; $i++) { $out[] = $p; $next = dirname($p); if ($next === $p) { break; } $p = $next; } return $out; } /** * PCRE 量词 {n,m} 中 m 最大 65535;将 [\s\S]{0,N} 自动拆成多段。 */ function ws_pcre_fix_span_quantifiers($pattern) { $max = 65535; return preg_replace_callback( '/\[\\\\s\\\\S\]\{0,(\d+)\}(\??)/', function ($m) use ($max) { $n = (int)$m[1]; $lazy = $m[2]; if ($n <= $max) { return $m[0]; } $parts = array(); $remaining = $n; while ($remaining > 0) { $chunk = ($remaining > $max) ? $max : $remaining; $parts[] = '[\s\S]{0,' . $chunk . '}'; $remaining -= $chunk; } if ($lazy === '?') { $last = array_pop($parts); $last = rtrim($last, '}') . '}?'; $parts[] = $last; } return implode('', $parts); }, $pattern ); } function ws_norm_path_key($path) { if (!is_string($path) || $path === '') { return false; } $rp = @realpath($path); if ($rp === false) { return false; } return rtrim(str_replace('\\', '/', $rp), '/'); } function ws_batch_active_file() { return __DIR__ . DIRECTORY_SEPARATOR . '.webshell_scan_active.json'; } function ws_batch_active_write($scanRoot, $stateFile, $queueFile) { $key = ws_norm_path_key($scanRoot); if ($key === false) { return; } @file_put_contents( ws_batch_active_file(), json_encode(array( 'scan_root' => $scanRoot, 'scan_root_key' => $key, 'state' => $stateFile, 'queue' => $queueFile, 'updated' => time(), ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX ); } function ws_batch_active_read() { $raw = @file_get_contents(ws_batch_active_file()); if ($raw === false || $raw === '') { return null; } $data = json_decode($raw, true); return is_array($data) ? $data : null; } function ws_batch_active_clear() { @unlink(ws_batch_active_file()); } if (empty($SCAN_ROOT)) { $SCAN_ROOT = __DIR__; } $WS_RESCAN_MESSAGE = ''; if ( !$WS_CLI && (isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : '') === 'POST' && isset($_POST['ws_rescan_submit'], $_POST['ws_rescan_root'], $_POST['ws_chain_anchor']) && $_POST['ws_rescan_submit'] === '1' && is_string($_POST['ws_rescan_root']) && is_string($_POST['ws_chain_anchor']) ) { $anchorRp = @realpath($_POST['ws_chain_anchor']); $candRp = @realpath($_POST['ws_rescan_root']); if ($anchorRp !== false && $candRp !== false && is_dir($candRp)) { $allowed = array(); foreach (ws_parent_dir_chain($anchorRp) as $p) { $k = ws_norm_path_key($p); if ($k !== false) { $allowed[] = $k; } } $candKey = ws_norm_path_key($candRp); if ($candKey !== false && in_array($candKey, $allowed, true)) { $SCAN_ROOT = $candRp; $oldBatch = ws_batch_paths($anchorRp); @unlink($oldBatch['queue']); @unlink($oldBatch['state']); ws_batch_active_clear(); $WS_RESCAN_MESSAGE = '已切换扫描根目录为:<code>' . htmlspecialchars($candRp) . '</code>(已中止原分批任务)'; } else { $WS_RESCAN_MESSAGE = '所选上级目录不可访问,已跳过,继续按当前根目录扫描。'; } } else { $WS_RESCAN_MESSAGE = '所选目录不可访问,已跳过,继续按当前根目录扫描。'; } } elseif ( !$WS_CLI && (isset($_POST['ws_batch_continue']) || isset($_GET['ws_batch_continue'])) ) { $restoredRoot = false; if (isset($_POST['ws_scan_root']) && is_string($_POST['ws_scan_root'])) { $rp = @realpath($_POST['ws_scan_root']); if ($rp !== false && is_dir($rp)) { $SCAN_ROOT = $rp; $restoredRoot = true; } } elseif (isset($_GET['ws_scan_root']) && is_string($_GET['ws_scan_root'])) { $rp = @realpath($_GET['ws_scan_root']); if ($rp !== false && is_dir($rp)) { $SCAN_ROOT = $rp; $restoredRoot = true; } } if (!$restoredRoot) { $active = ws_batch_active_read(); if (is_array($active) && !empty($active['scan_root'])) { $rp = @realpath($active['scan_root']); if ($rp !== false && is_dir($rp)) { $SCAN_ROOT = $rp; } } } } $scanRootReal = realpath($SCAN_ROOT); if ($scanRootReal === false) { if (!$WS_CLI) { header('Content-Type: text/plain; charset=UTF-8', true); } echo '扫描根目录无效或不可访问: ' . $SCAN_ROOT; exit(1); } $SCAN_ROOT = rtrim($scanRootReal, DIRECTORY_SEPARATOR); $SEGMENT_REGEX_RULES = [ [ 'name' => 'aa_marker_split_hex_loader_chain', 'pattern' => '/__AA__[0-9A-F]{32}__AA__[\s\S]{0,3000}explode\s*\(\s*[\'"]\(w\(I\(3[\'"]\s*,\s*[\'"]H\*\(w\(I\([0-9a-f]{80,}/is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'dense_goto_octal_hex_obfuscation_chain', 'pattern' => '/(?:\bgoto\s+[A-Za-z0-9_]+\s*;\s*[A-Za-z0-9_]+\s*:){5,}[\s\S]{0,12000}(?:\\\\x[0-9A-Fa-f]{2}|\\\\[0-7]{2,3}){24,}[\s\S]{0,8000}\$[A-Za-z0-9_]+\s*=\s*function\s*\(/is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'goto_dense_hex_octal_escape_burst', 'pattern' => '/(?:\\\\x[0-9A-Fa-f]{2}|\\\\[0-7]{2,3}){24,}/is', 'type' => 'regex', 'risk' => 'high', 'clean' => true, ], [ 'name' => 'goto_dense_server_obfuscated_key_or_input_get', 'pattern' => '/\$_SERVER\s*\[\s*[\'"](?:\\\\x[0-9A-Fa-f]{2}|\\\\[0-7]{2,3})/is', 'type' => 'regex', 'risk' => 'high', 'clean' => true, ], [ 'name' => 'goto_dense_goto_statement_burst', 'pattern' => '/(?:\bgoto\s+[A-Za-z0-9_]+\s*;[\s\S]{0,12000}){8,}/is', 'type' => 'regex', 'risk' => 'high', 'clean' => true, ], [ 'name' => 'include_base64_loader', 'pattern' => '/@?\s*(?:include|include_once|require|require_once)\s*\(?\s*base64_decode\s*\(/i', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'web_uploader_human_filesize_combo', 'pattern' => '/(?is)multipart\/form-data[\s\S]{0,12000}?name\s*=\s*[\'"]uploadFile[\'"][\s\S]{0,65535}[\s\S]{0,54465}?human_filesize\s*\(/', 'type' => 'regex', 'risk' => 'high', 'clean' => true, ], [ 'name' => 'nested_eval_chain', 'pattern' => '/eval\s*\(\s*eval\s*\(\s*eval\s*\(/i', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'eval_block_comment_before_paren', // 用 # 作分隔符,避免 / 与 /* 混淆;与关键词 eval/**/(stripos)二选一即可命中常见混淆形态 'pattern' => '#@?\s*eval\s*(?:/\*[\s\S]*?\*/\s*)+\(#i', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'exception_payload_eval_getmessage', // Exception 传载荷:throw/catch + eval(单引号 PHP 闭标签 + getMessage);分隔符 # 'pattern' => '#throw\s+new\s+Exception\s*\(\s*\$\w+\s*\)[\s\S]{0,800}?eval\s*\(\s*[\'"]\x3f\x3e[\'"]\s*\.[\s\S]{0,400}?getMessage\s*\(\s*\)#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'eval_quad_var_call_chain', // eval($A($B($C($D( 四层变量嵌套;拼接函数名马(如 ebjeuksp.php) 'pattern' => '/eval\s*\(\s*\$\w+\s*\(\s*\$\w+\s*\(\s*\$\w+\s*\(\s*\$\w+\s*\(/i', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'eval_triple_var_call_chain', // eval($A($B($C(…))) 三层变量嵌套;$C 可为函数调用+载荷(如 fpdblhfa.php、admin (7).php) 'pattern' => '/eval\s*\(\s*\$\w+\s*\(\s*\$\w+\s*\(\s*\$\w+\s*\(/i', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'obfuscated_gate_md5_quad_chain', 'pattern' => '/in_array\s*\(\s*gettype\(\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\)\s*\.\s*count\(\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\)\s*,\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\)\s*&&\s*count\(\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\)\s*==\s*\d+\s*&&\s*md5\s*\(\s*md5\s*\(\s*md5\s*\(\s*md5\s*\(\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\[\s*\d+\s*\]\s*\)\s*\)\s*\)\s*\)\s*===\s*["\'][0-9a-fx\\\\]{20,}["\']\s*\)/is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'obfuscated_array_eval_loader_chain', 'pattern' => '/\(\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\[\s*\d+\s*\]\s*=\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\[\s*\d+\s*\]\s*\.\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\[\s*\d+\s*\]\s*\)\s*&&\s*\(\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\[\s*\d+\s*\]\s*=\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\[\s*\d+\s*\]\s*\(\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\[\s*\d+\s*\]\s*\)\s*\)\s*&&\s*@eval\s*\(\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\[\s*\d+\s*\]\s*\(\s*\$\{\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\[\s*\d+\s*\]\s*\}\s*\[\s*\d+\s*\]\s*\)\s*\)/is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'hex_marker_with_long_encoded_payload', 'pattern' => '/[0-9a-f]{32}\|\{\-\.\-\!\!\!\}\|[A-Za-z0-9+\/=]{200,}/i', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'stream_wrapper_inline_loader_chain', 'pattern' => '/stream_wrapper_register\s*\(\s*[\'"][a-z0-9_]{2,16}[\'"]\s*,\s*[\'"][A-Za-z_][A-Za-z0-9_]*[\'"]\s*\)\s*;\s*(?:@?\s*)?(?:include|include_once|require|require_once)\s*[\'"][a-z0-9_]{2,16}:\/\/[\'"]\s*\.\s*base64_encode\s*\(\s*[\'"]<\?php\s*[\'"]\s*\.\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\)/is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'reflection_newinstance_obfuscated_payload', 'pattern' => '/new\s+ReflectionClass\s*\(\s*[\'"][A-Za-z_][A-Za-z0-9_]*[\'"]\s*\)\s*;\s*\$[A-Za-z_][A-Za-z0-9_]*\s*->\s*newInstance\s*\(\s*\$[A-Za-z_][A-Za-z0-9_]*\s*\)/is', 'type' => 'regex', 'risk' => 'high', 'clean' => true, ], [ 'name' => 'goto_hex_superglobal_obfuscation', 'pattern' => '/(?:\bgoto\s+[A-Za-z_][A-Za-z0-9_]*\s*;\s*){4,}[\s\S]{0,4000}\$_(?:GET|POST|REQUEST)\s*\[\s*["\'](?:\\\\x[0-9a-fA-F]{2}|\\\\[0-7]{2,3}){3,}/is', 'type' => 'regex', 'risk' => 'high', 'clean' => true, ], [ 'name' => 'polyglot_binary_php_payload', 'pattern' => '/(?:\xFF\xD8\xFF|\x89PNG|\x47\x49\x46\x38|%PDF)[\s\S]{0,65535}[\s\S]{0,54465}<\?(?:php|=)[\s\S]{0,4000}(?:eval\s*\(|assert\s*\(|system\s*\(|shell_exec\s*\(|passthru\s*\(|base64_decode\s*\(|gzinflate\s*\()/is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'sawkat_bd_brand_with_exec_signature', // 品牌串与 @system / shell_exec / passthru 等同文件共现(去掉注释后易漏时作补漏);分隔符 # 'pattern' => '#(?is)(?:SAWKAT-BD\s+Shell|Advanced\s+Bypassable\s+Web\s+Shell)[\s\S]{0,65535}[\s\S]{0,65535}[\s\S]{0,65535}[\s\S]{0,42795}?@?\s*\b(?:system|shell_exec|passthru|proc_open|popen)\s*\(#i', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'legacy_obfuscated_loader_class', // Legacy/WordPress Loader 系:注释品牌 + 随机 Loader 类 + ::initNNN(如 index (17).php、ace.php、crgio.php) 'pattern' => '/(?:Legacy|WordPress)-Compatible Obfuscated Loader[\s\S]{0,8000}?class Loader[A-Za-z0-9_]+[\s\S]*?::init\d+\s*\(/is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'fm_base64_get_archive_param', // Web 文件管理马:$_GET 传路径 + base64_decode 解压/打包(如 file5.php) 'pattern' => '/base64_decode\s*\(\s*\$_GET\s*\[\s*[\'"](?:zip|gz|gzfile)[\'"]\s*\)/i', 'type' => 'regex', 'risk' => 'high', 'clean' => true, ], [ 'name' => 'session_fm_perm_size_manager', // 轻量单文件 FM:session + $_GET[dir] + fm_perm/fm_size(如 mg.php) 'pattern' => '#session_start\s*\(\s*\)[\s\S]{0,1500}?\$_GET\s*\[\s*[\'"]dir[\'"]\s*\][\s\S]{0,8000}?function fm_perm\s*\(\s*\$p\s*\)[\s\S]{0,3000}?function fm_size\s*\(\s*\$bytes\s*\)#is', 'type' => 'regex', 'risk' => 'high', 'clean' => true, ], [ 'name' => 'pack_hex_eval_dropper', // hex pack 投递器:pack('H*',…) + eval( 动态解壳写马(如 succinctnesses.php) 'pattern' => '/pack\s*\(\s*[\'"]H\*[\'"]\s*,[\s\S]{0,2000}?eval\s*\(/is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'shinday_png_polyglot_file_manager', // PNG 图马 + Shinday 文件管理:$_GET[dir] + 上传/写文件(如 term.php) 'pattern' => '#(?:\x89PNG|Shinday(?:_Payload)?)[\s\S]{0,65535}[\s\S]{0,54465}<\?(?:php|=)[\s\S]{0,12000}?\$_GET\s*\[\s*[\'"]dir[\'"]\s*\][\s\S]{0,16000}?(?:file_put_contents\s*\(|move_uploaded_file\s*\()#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'session_password_remote_fetch_fm', // 密码保护 Web FM:session + password_verify + $_GET[dir] + 远程拉取/编辑(如 wp-images.php) 'pattern' => '#session_start\s*\(\s*\)[\s\S]{0,4000}?(?:\$passwordProtect|password_verify\s*\()[\s\S]{0,6000}?\$_GET\s*\[\s*[\'"]dir[\'"]\s*\][\s\S]{0,16000}?(?:get_file_content|fetch_remote_file|move_uploaded_file\s*\()#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'xenium_get_upload_php_input_stealth', // Xenium 隐蔽上传:GET upload_file + php://input 写文件(如 rip.php) 'pattern' => '#\$_GET\s*\[\s*[\'"]upload_file[\'"]\s*\][\s\S]{0,3000}?fopen\s*\(\s*[\'"]php://input[\'"]#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'concat_shell_exec_runcommand', // runCommand + 拼接 shell_exec 函数名(如 rip.php) 'pattern' => '#function\s+runCommand\s*\(\s*\$[\w]+\s*\)[\s\S]{0,1200}?[\'"]s[\'"]\s*\.\s*[\'"]h[\'"]\s*\.\s*[\'"]e[\'"]\s*\.\s*[\'"]l[\'"]\s*\.\s*[\'"]l[\'"]\s*\.\s*[\'"]_[\'"]\s*\.\s*[\'"]e[\'"]\s*\.\s*[\'"]x[\'"]\s*\.\s*[\'"]e[\'"]\s*\.\s*[\'"]c[\'"]#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'smtp_cookie_mailer_head_b_enc', // Cookie 驱动 SMTP 群发/代理邮件马(如 sar95dog.php、ttrigg1285.php) 'pattern' => '#ignore_user_abort\s*\(\s*true\s*\)\s*;\s*error_reporting\s*\(\s*0\s*\)\s*;\s*set_time_limit\s*\(\s*0\s*\)\s*;[\s\S]{0,4000}?\$_COOKIE\s*\[\s*[\'"]a[\'"]\s*\][\s\S]{0,25000}?function\s+head_b_enc\s*\(\s*\$s\s*,\s*\$ch\s*=\s*[\'"]utf-8[\'"]\s*\)#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'fake_elementor_return_eval_loader', // 伪装 Elementor 插件头 + 混淆 base64 载荷 + EvAl(如 index (15).php) 'pattern' => '#Plugin Name:\s*Elementor[\s\S]{0,65535}[\s\S]{0,14465}?(?:\'bas\'\.\'e64\'\.\'_de|return\s+EvAl\s*\()#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'obfuscated_v1_array_file_manager_ui', // $V1[n]() 混淆函数数组 + 文件管理 UI(如 min.php) 'pattern' => '#name\s*=\s*[\'"]newfolder[\'"][\s\S]{0,12000}?\$V1\s*\[\s*\d+\s*\][\s\S]{0,12000}?name\s*=\s*[\'"]chmod_file[\'"][\s\S]{0,6000}?name\s*=\s*[\'"]edited_content[\'"]#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'eval_gzuncompress_self_file_tail_stub', // eval + gzuncompress 拼接 + file_get_contents(__FILE__) 自解压 stub(如 DFPG6hgwXCa.php) 'pattern' => '#[\'"]gzuncompres[\'"]\s*\.\s*[\'"]s[\'"]\s*;\s*eval\s*\(\s*\$[\w]+\s*\([\s\S]{0,4000}?__FILE__#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'eval_gzdecode_base64_loader', // eval(gzdecode(base64_decode(...))) 一层 gzip+base64 加载器(如 saiga.php) 'pattern' => '#eval\s*\(\s*gzdecode\s*\(\s*base64_decode\s*\(\s*[\'"]#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'elep_bootstrap_encodepath_fm', // Elep 标题 + encodePath 孟加拉语路径混淆 Web FM(如 hi.php) 'pattern' => '#<title>Elep</title>[\s\S]{0,12000}?function\s+encodePath\s*\(\s*\$path\s*\)[\s\S]{0,1200}?define\s*\(\s*[\'"]PATH[\'"]#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'eval_closure_aebds_string_index_loader', // "_AeBDsCdE" 字符串下标拼函数名 + return $fn($arg) 闭包加载器(如 3PJcpMFsD8B.php) 'pattern' => '#=\s*"_AeBDsCdE";\s*[\s\S]{0,800}?\$[\w]+\[\([^)]+\)[^]]+\][\s\S]{0,600}?return\s+\$[\w]+\(#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'eval_hex2bin_long_payload_loader', // Obfuscation Wrapper v3.1:eval(hex2bin("…")) 长 hex 载荷(如 admin.php) 'pattern' => '/eval\s*\(\s*hex2bin\s*\(\s*["\'][0-9a-f]{200,}/is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'xor_pack_split_hex48_star', // XOR 马:pack("H*") 拆成 "\x48"."\x2a" 绕过字面量检测(如 2mt7y8i0.php) 'pattern' => '/pack\s*\(\s*["\']\\x48["\']\s*\.\s*(?:\/\*[\s\S]*?\*\/\s*)*["\']\\x2a["\']/is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'xor_eval_dynamic_payload_die', // XOR 马:eval($变量) 后 die(),载荷经 $_COOKIE/$_POST 传入(如 2mt7y8i0.php) 'pattern' => '/eval\s*\(\s*\$\w+\s*\)[\s\S]{0,1200}?die[\s\S]{0,80}?\(\s*\)/is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'wp_disguise_include_unset_get_backdoor', // 伪装 WP:function x($p){include($p);} … unset($_GET[…])(如 ekh8n7ly.php、j4d6y7mk.php) 'pattern' => '#function\s+\w+\s*\(\s*\$\w+\s*\)\s*\{[\s\S]{0,200}?include\s*\(\s*\$\w+\s*\)[\s\S]{0,65535}[\s\S]{0,54465}?unset\s*\(\s*\$_GET\s*\[#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'wp_disguise_unserialize_array_walk_chain', // 伪装 WP:unserialize(…) … array_walk(…, "callback") 解码链(如 ekh8n7ly.php) 'pattern' => '#unserialize\s*\(\s*\$\w+\s*\)[\s\S]{0,65535}[\s\S]{0,14465}?array_walk\s*\(\s*\$\w+\s*,\s*["\'][\w]+["\']#is', 'type' => 'regex', 'risk' => 'critical', 'clean' => true, ], [ 'name' => 'wp_core_fragment_html_injected_junk', // WP 核心片段图马:HTML 开头 + @since docblock + 随机 junk 赋值/函数(如 tzcizgci.php、sl430ns8.php) 'pattern' => '#^\s*<(?!\?php)(?!=)[\s\S]{0,65535}[\s\S]{0,54465}?@since\s+\d+\.\d+\.\d+[\s\S]{0,65535}[\s\S]{0,65535}[\s\S]{0,65535}[\s\S]{0,65535}[\s\S]{0,37260}?\$\w+\s*=\s*[\'"][a-z0-9]{5,12}[\'"]\s*;[\s\S]{0,8000}?\$\w+\s*=\s*[\'"][a-z0-9]{5,12}[\'"]\s*;[\s\S]{0,12000}?(?:strcoll|crc32|quotemeta|convert_uuencode|sha1|htmlentities|strip_tags|basename)\s*\(#is', 'type' => 'regex', 'risk' => 'high', 'clean' => true, ], ]; foreach ($SEGMENT_REGEX_RULES as &$__wsRuleFix) { if (isset($__wsRuleFix['pattern']) && is_string($__wsRuleFix['pattern'])) { $__wsRuleFix['pattern'] = ws_pcre_fix_span_quantifiers($__wsRuleFix['pattern']); } } unset($__wsRuleFix); $TOOL_KEYWORDS = [ 'human_filesize($bytes', '$_GET[\'directory\']', 'Build: m7e3536ihjkevi8aondd9jco', 'Modified By #No_Identity', '可达鸭-解锁专用文件', 'background-color: #c5c5c5', 'Your IP : \'', 'Solevisible/Alfa-Team', 'OVA-TOOLS', 'tizgnlmoed', '-170221)))));$xFoA(0)', 'UNSHELL_PATH_IS_ME', '~Unshell~ file', 'papakibo/engkol-shell', 'mndpsingh287', 'boyas2/aboyas', 'phpFileManager 1.7.8', 'Fabricio Seger Kolling', 'Build: lbot7pz9fe1w07pq2clm7mpx', '$_SESSION[\'secretyt\']', 'a1fecbae6a303e0618f95586ddb49de7c30f911fecd8701500320daf754868a0', 'strrev($SS8Fu)', 'strrev($uObgc)', 'strrev($x1DBi)', '$▛ = "59e8d97dbcc1d0f65dea6ecd0e9fbe39"', 'PluginhxMr83ProIIp95', 'Watching webshell!', '@id 83a6ee9b34553e9cf5ef0c507270c', 'function pre_term_name($auth_data, $wp_nonce)', 'oritomasua', 'kill_the_net', '[ RC-SHELL v', '{Ninja-Shell}', '内部Base64解码失败', 'eval("' . '?' . '>' . '" . $decryptedCode)', 'call_user_func("define","D_D__D_", "D_D__DD")', 'function fastChmod($path,$mode=0777)', 'seo_task_progress_init()', 'fetch_inner_page_template($docRoot', '\';0ogv9k.mbh[lux7)*', 'iD5nf(strrev($RPBoA)', 'eval("\\77\\x3e" . iD5nf', 'BiaoJiOk', '::aJn1cgWi33NDH()', '$_REQUEST["0kb"]', '>0kb<', 'md5($_COOKIE["d"])', 'goto B092333f0848;', 'uawdijnntqw1x1x1', 'MENU_API_PASSWORD', 'shell_rel_dir', 'HTTP_X_OPERATION_PASSWORD', 'daszxxx', 'checkAnalyticsV2', 'X7K9M2P4Q8R1S5T3', 'gzinflate(base64_decode', 'eval(base64_decode', // 混淆 eval 常见形态(如 eval/**//*…*/();stripos;清理同分段正则:删命中所在 PHP 段 'eval/**/', 'strrev(gzinflate(', '\'base6\'.\'4\'.\'_\'.\'decode\'', '\'g\'.\'z\'.\'un\'.\'compr\'.\'e\'.\'ss\'', // 拼接函数名(如 ebjeuksp.php、fpdblhfa.php:拼接 rot13/base64_decode/strrev + eval) '\'gzuncompre\'.\'ss\'', '\'bas\'.\'e64\'.\'_decod\'.\'e\'', '\'ba\'.\'se64\'.\'_d\'.\'ec\'.\'ode\'', '\'st\'.\'r\'.\'_rot\'.\'13\'', 'mail($main, "Ding Dong "', 'function actionFilesMan()', '$_REQUEST["\144\157\141\143\x74"]', 'eval("\77\x3e" . $', 'eval("\77\76" .', // 单引号 PHP 闭标签前缀 eval(如 zk81cwQSdefault.php);与双引号 octal 形态互补 'eval(\'' . '?' . '>' . '\' .', 'str_rot13("\x75\147\x67\x63\x66\72\57\57', 'explode(base64_decode("Pz4="),file_get_contents', 'metaphone("MjI2OTk3NzYzMzIwNzk4MDIyMTYyNTMy")', 'md5(md5(md5(md5($ZMWWyotbwIJcT[12]))))', 'f116c4d27eafebbc5e7534e2353cdab9', "base64_decode('ZW1haWxjYW1wYWlnbjIwMjQ=')", 'eval($w.$u($v))', '87aecb35f4fa697b068abef3dafc588e', '_execute_aFOINUlM49', 'TH6M8 = "\x68\x74\164\x70\x3a\x2f\57\x35\61\56\x37\71\56\x31\x32\64\x2e\61\x31\x31\57\x63\157\x5f\145\x6e\160\x74";', '9a286406c252a3d14218228974e1f567', '[PHPkoru_Info]', // SAWKAT-BD 系 WebShell(与分段正则 sawkat_bd_brand_with_exec_signature 互补) 'SAWKAT-BD Shell', 'Advanced Bypassable Web Shell', // SKYSHELL 系单文件管理马(全角标题,如 sys_164.php) 'SKYSHELL MANAGER', // Den1xxx Filemanager 分支 / XOR 混淆壳(如 aa.php、wps.php) 'Den1xxx/Filemanager', 'fm_default_config', '_xor_decrypt', 'Yanz Webshell!', 'WSO YANZ ENC BYPASS', 'PRIV8 WEB SHELL ORB YANZ', 'YANZ MINI SHELL BYPASS', // 2026-05-30 新增(JSON id 532–535) '$L9PMr = "\x62\141" . "\163\x65" . "\66\64" . "\x5f\x64\145\x63\x6f\144" . "\145"', '/*]u%7hUn\p\^?\>>N*/(//BF,1TlwT\W<F+*d)a_,SKuk-)K</fW_lS?Q|e' . "\n" . 'null //Cu"&pDjBDN~!j1/Pyz(l ;s)//zvj8!*9B*(1', 'try { $oqxsdk5foit = "SWiUoiCdSHKa9lxhtfj4LT9NtbOvY2Yix4P99868sCE7MZB2YU48', "base64_decode('KCRRKk9GYk4jZCkwRVBNVTRwTDJUbjkveGxIYSAsbUc4OnF5dUN8Nz52elJvcmlZZV9oJzFrU0RjQWpmZzU2LXNeLjxJVnd0Mw==')", // JSON id 462:Phar 归档 stub 常见特征 '__HALT_COMPILER();', // 2026-05-30 批量漏检补规则(JSON id 541–546) 'Legacy-Compatible Obfuscated Loader', '$execFunctions = [\'passthru\', \'system\', \'exec\', \'shell_exec\'', // 2026-05-30 漏检补规则(JSON id 549–550) 'function echo_sign_d($text)', 'Shinday_Payload', // 2026-05-30 漏检补规则(JSON id 556–557) '$remoteFetchAllowed = true', '// Handle the special upload case from xenium3', // 2026-05-30 漏检补规则(JSON id 561–562) 'function head_b_enc($s,$ch=\'utf-8\')', '\'bas\'.\'e64\'.\'_de\'.\'cod\'.\'e\'', // 2026-05-30 漏检补规则(JSON id 565–566) '\'gzuncompres\'.\'s\'', '\'file_ge\'.\'t\'.\'_content\'.\'s\'', // 2026-05-30 漏检补规则(JSON id 570–571) 'eval(gzdecode(base64_decode', '"_AeBDsCdE"', // 2026-06-13 漏检补规则(JSON id 572–574):BossBey / php-shell 传播型文件管理马 'php-shell.com/api/track.php', '.backdoor_created_', '(BossBey) File Manager', ]; /** 被这些关键词命中时,清理策略为整文件清空(.php),与是否含 <? 无关;四条中任一条命中即可,不必全部命中 */ $YANZ_WSO_FULL_WIPE_KEYWORDS = [ 'Yanz Webshell!', 'WSO YANZ ENC BYPASS', 'PRIV8 WEB SHELL ORB YANZ', 'YANZ MINI SHELL BYPASS', ]; $stats = [ 'scanned' => 0, 'skipped' => 0, 'infected' => 0, 'cleaned' => 0, 'errors' => 0, ]; $skip_stats = []; $scan_log = []; function snippet_preview($s, $max = 80) { if (function_exists('mb_substr')) { return mb_substr($s, 0, $max, 'UTF-8'); } return substr($s, 0, $max); } function write_log($line) { global $LOG_FILE, $scan_log; $scan_log[] = $line; if (!empty($LOG_FILE)) { @file_put_contents($LOG_FILE, $line . PHP_EOL, FILE_APPEND | LOCK_EX); } } function ws_batch_progress_append($line) { global $BATCH_PROGRESS_LOG; if ($BATCH_PROGRESS_LOG === false || $BATCH_PROGRESS_LOG === null || $BATCH_PROGRESS_LOG === '') { return; } $path = is_string($BATCH_PROGRESS_LOG) ? $BATCH_PROGRESS_LOG : (__DIR__ . '/webshell_scan_batch_progress.log'); @file_put_contents($path, '[' . date('Y-m-d H:i:s') . '] ' . $line . "\n", FILE_APPEND | LOCK_EX); } function flush_output($html) { global $WS_CLI; if (!empty($WS_CLI)) { return; } echo $html; // 凑够约 4KB 块,促使 Nginx/FastCGI 尽快把内容推到浏览器(避免一直白屏) static $wsFlushPad = null; if ($wsFlushPad === null) { $wsFlushPad = str_repeat(' ', 2048); } echo $wsFlushPad; while (ob_get_level() > 0) { @ob_flush(); } @flush(); } function ws_http_stream_headers() { global $WS_CLI; if (!empty($WS_CLI) || headers_sent()) { return; } header('Content-Type: text/html; charset=UTF-8'); header('X-Accel-Buffering: no'); header('Cache-Control: no-cache, no-store, must-revalidate'); header('Pragma: no-cache'); if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', '1'); } @ini_set('zlib.output_compression', '0'); @ini_set('implicit_flush', '1'); while (ob_get_level() > 0) { @ob_end_flush(); } } /** * .php 无 <? 且前 4KB 非打印字符占比 >= 30% → 二进制/加密载荷(如 mksriscq.php) */ function ws_detect_php_binary_no_open_tag($filepath, $content) { if (!preg_match('/\.php$/i', $filepath)) { return null; } if (strpos($content, '<?') !== false) { return null; } $len = strlen($content); if ($len < 256) { return null; } $sampleLen = min($len, 4096); $nonPrint = 0; for ($i = 0; $i < $sampleLen; $i++) { $ord = ord($content[$i]); if ($ord < 0x09 || ($ord > 0x0d && $ord < 0x20) || $ord > 0x7e) { $nonPrint++; } } if ($sampleLen === 0 || ($nonPrint / $sampleLen) < 0.30) { return null; } return [ 'rule' => 'php_binary_no_open_tag_payload', 'risk' => 'critical', 'clean' => true, 'source' => 'segment_regex', 'pattern' => 'php_binary_no_open_tag_heuristic', 'line' => 1, 'snippet' => snippet_preview($content, 80), 'offset' => 0, ]; } function ws_flush_rescan_form($scanRoot, $inProgress = false) { global $WS_CLI; if (!empty($WS_CLI)) { return; } $formAction = isset($_SERVER['SCRIPT_NAME']) ? htmlspecialchars((string)$_SERVER['SCRIPT_NAME'], ENT_QUOTES, 'UTF-8') : ''; $rescan_chain = ws_parent_dir_chain($scanRoot); flush_output('<div class="section-title" style="margin-top:16px;">▶ 以父级目录为根重新扫描</div>'); if ($inProgress) { flush_output( '<p style="color:var(--orange);font-size:12px;margin-bottom:8px;">' . '当前分批尚未结束。提交后将<strong>中止本轮续跑</strong>,以所选目录<strong>重新开始</strong>扫描。' . '点选下方目录会<strong>暂停自动续跑</strong>,便于手动操作。' . '</p>' ); } flush_output('<form method="post" action="' . $formAction . '" id="wsRescanForm" style="margin-top:8px;">'); flush_output('<input type="hidden" name="ws_rescan_submit" value="1">'); flush_output('<input type="hidden" name="ws_chain_anchor" value="' . htmlspecialchars($scanRoot, ENT_QUOTES, 'UTF-8') . '">'); flush_output('<table class="config-table">'); foreach ($rescan_chain as $i => $path) { $id = 'ws_rescan_' . $i; $checked = ($i === 0) ? ' checked' : ''; flush_output( '<tr><td style="width:42px;vertical-align:top;"><input type="radio" name="ws_rescan_root" id="' . htmlspecialchars($id, ENT_QUOTES, 'UTF-8') . '" value="' . htmlspecialchars($path, ENT_QUOTES, 'UTF-8') . '"' . $checked . '></td><td><label for="' . htmlspecialchars($id, ENT_QUOTES, 'UTF-8') . '">向上 ' . (int)$i . ' 级 — <code>' . htmlspecialchars($path) . '</code></label></td></tr>' ); } flush_output('</table>'); flush_output( '<p style="margin-top:10px;text-align:center;">' . '<button type="submit" style="padding:8px 16px;font-size:13px;background:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer;">以所选目录为根重新扫描</button>' . '</p>' ); flush_output( '<p style="color:var(--dim);font-size:11px;margin-top:6px;text-align:center;">' . '仅从本次扫描根目录沿上级目录列出可选路径;提交后仅当路径属于该列表时才会生效。' . '</p>' ); flush_output('</form>'); } function ws_batch_auto_continue_script($continueUrl) { flush_output( '<script>(function(){' . 'var f=document.getElementById("wsBatchForm");' . 'var rescan=document.getElementById("wsRescanForm");' . 'var done=false,paused=false;' . 'function pause(){paused=true;}' . 'if(rescan){' . 'rescan.addEventListener("mouseenter",pause);' . 'rescan.addEventListener("focusin",pause);' . 'rescan.addEventListener("click",pause);' . '}' . 'var pauseBtn=document.getElementById("wsBatchPauseBtn");' . 'if(pauseBtn){pauseBtn.addEventListener("click",function(e){e.preventDefault();pause();this.textContent="已暂停自动续跑";});}' . 'function go(){if(!f||done||paused)return;done=true;if(f.requestSubmit){f.requestSubmit();}else{f.submit();}}' . 'setTimeout(go,500);' . 'setTimeout(function(){if(!done&&!paused){window.location.replace("' . $continueUrl . '");}},2500);' . '})();</script>' ); } function scan_file($filepath) { global $TOOL_KEYWORDS, $SEGMENT_REGEX_RULES; $content = @file_get_contents($filepath); if ($content === false) { return array('keyword' => array(), 'segment_regex' => array(), 'line_regex' => array(), 'unreadable' => true); } $keyword = []; foreach ($TOOL_KEYWORDS as $kw) { if ($kw === '') { continue; } $offset = stripos($content, $kw); if ($offset !== false) { $lineNum = substr_count(substr($content, 0, (int)$offset), "\n") + 1; $snippet = trim(substr($content, max(0, (int)$offset - 30), 120)); $keyword[] = [ 'rule' => 'tool_keyword_' . md5($kw), 'risk' => 'high', 'clean' => true, 'source' => 'keyword', 'pattern' => $kw, 'line' => $lineNum, 'snippet' => $snippet, 'offset' => (int)$offset, ]; } } if (!empty($keyword)) { return ['keyword' => $keyword, 'segment_regex' => [], 'line_regex' => []]; } $segment_regex = []; if (preg_match_all('/<\?(php)\b/i', $content, $segTagMatches, PREG_OFFSET_CAPTURE)) { $tagCount = count($segTagMatches[0]); for ($ti = 0; $ti < $tagCount; $ti++) { $tagText = $segTagMatches[1][$ti][0]; if ($tagText !== 'php') { $offset = (int)$segTagMatches[0][$ti][1]; $lineNum = substr_count(substr($content, 0, $offset), "\n") + 1; $snippet = trim(substr($content, max(0, $offset - 30), 120)); $segment_regex[] = [ 'rule' => 'php_open_tag_mixed_case', 'risk' => 'high', 'clean' => true, 'source' => 'segment_regex', 'pattern' => 'mixed_case_php_open_tag', 'line' => $lineNum, 'snippet' => $snippet, 'offset' => $offset, ]; break; } } } foreach ($SEGMENT_REGEX_RULES as $rule) { if ((isset($rule['type']) ? $rule['type'] : '') !== 'regex') { continue; } $pattern = ws_pcre_fix_span_quantifiers($rule['pattern']); $matches = []; if (@preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE) === false) { continue; } if (!empty($matches[0])) { foreach ($matches[0] as $m) { $offset = $m[1]; $lineNum = substr_count(substr($content, 0, $offset), "\n") + 1; $snippet = trim(substr($content, max(0, $offset - 30), 120)); $segment_regex[] = [ 'rule' => $rule['name'], 'risk' => $rule['risk'], 'clean' => $rule['clean'], 'source' => 'segment_regex', 'pattern' => $rule['pattern'], 'line' => $lineNum, 'snippet' => $snippet, 'offset' => $offset, ]; break; } } } if (!empty($segment_regex)) { return ['keyword' => [], 'segment_regex' => $segment_regex, 'line_regex' => []]; } $binaryHit = ws_detect_php_binary_no_open_tag($filepath, $content); if ($binaryHit !== null) { return ['keyword' => [], 'segment_regex' => [$binaryHit], 'line_regex' => []]; } return ['keyword' => [], 'segment_regex' => [], 'line_regex' => []]; } function clean_file($filepath, array $segmentFindings, &$didChange) { global $BACKUP_BEFORE_CLEAN, $YANZ_WSO_FULL_WIPE_KEYWORDS; $didChange = false; $content = file_get_contents($filepath); if ($content === false) { return false; } if ($BACKUP_BEFORE_CLEAN) { @file_put_contents($filepath . '.bak', $content); } $cleaned = false; // Yanz/WSO:四条关键词任一条记入 findings 即整文件清空(命中多条也只清一次) if (preg_match('/\.php$/i', $filepath) && !empty($YANZ_WSO_FULL_WIPE_KEYWORDS)) { foreach ($segmentFindings as $f) { if ((isset($f['source']) ? $f['source'] : '') !== 'keyword' || empty($f['clean'])) { continue; } if (!in_array(isset($f['pattern']) ? $f['pattern'] : '', $YANZ_WSO_FULL_WIPE_KEYWORDS, true)) { continue; } if ($content !== '') { $didChange = true; return file_put_contents($filepath, '') !== false; } return true; } } $hasCleanableHit = false; foreach ($segmentFindings as $f) { if (!empty($f['clean']) && isset($f['offset'])) { $hasCleanableHit = true; break; } } if ($hasCleanableHit && preg_match('/\.php$/i', $filepath) && strpos($content, '<?') === false) { if ($content !== '') { $didChange = true; return file_put_contents($filepath, '') !== false; } return true; } $phpBlocks = []; $searchPos = 0; $contentLen = strlen($content); while (($openPos = strpos($content, '<?', $searchPos)) !== false) { $closePos = strpos($content, '?' . '>', $openPos + 2); if ($closePos === false) { $blockStart = $openPos; $blockEnd = $contentLen; $phpBlocks[] = ['start' => $blockStart, 'end' => $blockEnd]; break; } $blockStart = $openPos; $blockEnd = $closePos + 2; $phpBlocks[] = ['start' => $blockStart, 'end' => $blockEnd]; $searchPos = $blockEnd; } $rangesToDelete = []; foreach ($segmentFindings as $f) { if (empty($f['clean'])) { continue; } if (!isset($f['offset'])) { continue; } $hitOffset = (int)$f['offset']; $matchedInPhpBlock = false; foreach ($phpBlocks as $block) { if ($hitOffset >= $block['start'] && $hitOffset < $block['end']) { $rangesToDelete[] = $block; $matchedInPhpBlock = true; break; } } if ($matchedInPhpBlock) { continue; } if ($hitOffset < 0 || $hitOffset >= $contentLen) { continue; } $lineStartPos = strrpos(substr($content, 0, $hitOffset), "\n"); $lineStart = ($lineStartPos === false) ? 0 : ($lineStartPos + 1); $lineEndPos = strpos($content, "\n", $hitOffset); $lineEnd = ($lineEndPos === false) ? $contentLen : ($lineEndPos + 1); if ($lineEnd > $lineStart) { $rangesToDelete[] = ['start' => $lineStart, 'end' => $lineEnd]; } } if (!empty($rangesToDelete)) { usort($rangesToDelete, function ($a, $b) { if ($a['start'] === $b['start']) { return 0; } return ($a['start'] < $b['start']) ? -1 : 1; }); $merged = []; foreach ($rangesToDelete as $r) { if (empty($merged)) { $merged[] = $r; continue; } $lastIdx = count($merged) - 1; if ($r['start'] <= $merged[$lastIdx]['end']) { if ($r['end'] > $merged[$lastIdx]['end']) { $merged[$lastIdx]['end'] = $r['end']; } } else { $merged[] = $r; } } for ($i = count($merged) - 1; $i >= 0; $i--) { $start = $merged[$i]['start']; $end = $merged[$i]['end']; $content = substr($content, 0, $start) . substr($content, $end); $cleaned = true; } } if (!$cleaned) { return true; } $didChange = true; return file_put_contents($filepath, $content) !== false; } /** * 超过 $PHP_OVERSIZE_DELETE_BYTES 的 PHP 脚本扩展名文件:清理模式下删除并记日志;仅检测模式下跳过扫描。 * 自身、白名单文件名、.bak 不处理(交由 should_skip)。 */ function try_handle_oversize_php_file($filepath) { global $SELF_FILENAME, $SCAN_ROOT, $DO_CLEAN, $BACKUP_BEFORE_CLEAN, $stats, $skip_stats, $PHP_OVERSIZE_DELETE_BYTES; static $phpScriptExts = ['php', 'php3', 'php4', 'php5', 'phtml', 'pht']; if (basename($filepath) === $SELF_FILENAME) { return false; } if (substr($filepath, -4) === '.bak') { return false; } $whitelistBasenames = ['3index.php']; if (in_array(basename($filepath), $whitelistBasenames, true)) { return false; } $ext = strtolower(pathinfo($filepath, PATHINFO_EXTENSION)); if (!in_array($ext, $phpScriptExts, true)) { return false; } $size = @filesize($filepath); if ($size === false || $size <= $PHP_OVERSIZE_DELETE_BYTES) { return false; } $relPath = str_replace($SCAN_ROOT . DIRECTORY_SEPARATOR, '', $filepath); if (!$DO_CLEAN) { $skip_stats['php-oversize-preview'] = (isset($skip_stats['php-oversize-preview']) ? $skip_stats['php-oversize-preview'] : 0) + 1; $stats['skipped']++; log_line('SKIP', 'tag-skip', '超大 PHP(> ' . (int)($PHP_OVERSIZE_DELETE_BYTES / 1024) . ' KB)未删除(仅检测模式):<b>' . htmlspecialchars($relPath) . '</b> [' . number_format($size / 1024, 1) . ' KB]'); return true; } if ($BACKUP_BEFORE_CLEAN) { @copy($filepath, $filepath . '.bak'); } if (@unlink($filepath)) { $stats['cleaned']++; $skip_stats['php-deleted-oversize'] = (isset($skip_stats['php-deleted-oversize']) ? $skip_stats['php-deleted-oversize'] : 0) + 1; log_line('DELETE', 'tag-clean', '✔ 超大 PHP 已删除(> ' . (int)($PHP_OVERSIZE_DELETE_BYTES / 1024) . ' KB):<b>' . htmlspecialchars($relPath) . '</b> [' . number_format($size / 1024, 1) . ' KB]' . ($BACKUP_BEFORE_CLEAN ? ' (已备份 .bak)' : '')); } else { $stats['errors']++; log_line('ERROR', 'tag-error', '✘ 超大 PHP 删除失败:<b>' . htmlspecialchars($relPath) . '</b>'); } return true; } function should_skip($filepath) { global $MAX_FILE_SIZE, $ALLOWED_EXTENSIONS, $TIME_START, $TIME_END, $SELF_FILENAME; $WHITELIST_FILENAMES = ['3index.php']; if (in_array(basename($filepath), $WHITELIST_FILENAMES, true)) { return 'whitelist-file'; } if (basename($filepath) === $SELF_FILENAME) { return 'self'; } if (substr($filepath, -4) === '.bak') { return 'backup-file'; } if (!empty($ALLOWED_EXTENSIONS)) { $ext = strtolower(pathinfo($filepath, PATHINFO_EXTENSION)); if (!in_array($ext, $ALLOWED_EXTENSIONS, true)) { return 'ext-mismatch'; } } $size = filesize($filepath); if ($size === false || $size > $MAX_FILE_SIZE) { return 'too-large'; } $mtime = filemtime($filepath); if ($TIME_START !== null && $mtime < $TIME_START) { return 'time-before-range'; } if ($TIME_END !== null && $mtime > $TIME_END) { return 'time-after-range'; } return null; } function ws_skip_count($reason) { global $skip_stats; if (!isset($skip_stats[$reason])) { $skip_stats[$reason] = 0; } $skip_stats[$reason]++; } /** 目录是否允许进入列举(open_basedir / 权限不足则 false,调用方直接跳过) */ function ws_can_traverse_dir($path) { $dirReal = @realpath($path); if ($dirReal === false || !@is_dir($dirReal)) { return false; } if (!@is_readable($dirReal)) { return false; } if (DIRECTORY_SEPARATOR !== '\\' && !@is_executable($dirReal)) { return false; } return true; } function ws_note_dir_skip($path) { ws_skip_count('dir-skipped'); } function iterate_files($dir) { global $skip_stats; $stack = array($dir); while (!empty($stack)) { $cur = array_pop($stack); if (!ws_can_traverse_dir($cur)) { ws_note_dir_skip($cur); continue; } $dirReal = @realpath($cur); $dh = @opendir($dirReal); if ($dh === false) { ws_skip_count('dir-unreadable'); continue; } while (($name = readdir($dh)) !== false) { if ($name === '.' || $name === '..') { continue; } $path = $dirReal . DIRECTORY_SEPARATOR . $name; if (@is_dir($path)) { if (ws_can_traverse_dir($path)) { $stack[] = $path; } else { ws_note_dir_skip($path); } continue; } if (!@is_file($path)) { continue; } if (!@is_readable($path)) { ws_skip_count('file-unreadable'); continue; } $real = @realpath($path); if ($real !== false) { yield $real; } } @closedir($dh); } } function log_line($tag, $tagClass, $msg) { global $stats, $WS_CLI; $ts = date('H:i:s'); $plain = "[$ts] [$tag] " . strip_tags($msg); if (!empty($WS_CLI)) { fwrite(STDERR, $plain . "\n"); write_log($plain); return; } $html = '<div class="log-line">' . '<span class="ts">' . $ts . '</span>' . '<span class="tag ' . $tagClass . '">' . htmlspecialchars($tag) . '</span>' . '<span class="msg">' . $msg . '</span>' . '</div>' . "\n"; flush_output($html); write_log($plain); } function ws_batch_paths($scanRoot) { $id = hash('sha256', $scanRoot . "\0" . __FILE__); return [ 'state' => __DIR__ . DIRECTORY_SEPARATOR . '.webshell_scan_state_' . $id . '.json', 'queue' => __DIR__ . DIRECTORY_SEPARATOR . '.webshell_scan_queue_' . $id . '.lst', ]; } function ws_walk_batch_append_queue($queueFile, array &$stack, $maxSeconds, array $allowedExts, $extFilterActive, $selfPath) { global $skip_stats; $deadline = microtime(true) + $maxSeconds; $selfNorm = $selfPath !== '' ? strtolower(str_replace('\\', '/', $selfPath)) : false; while (!empty($stack) && microtime(true) < $deadline) { $dir = array_pop($stack); if (!ws_can_traverse_dir($dir)) { ws_note_dir_skip($dir); continue; } $dirReal = @realpath($dir); $dh = @opendir($dirReal); if ($dh === false) { ws_skip_count('dir-unreadable'); continue; } while (($name = readdir($dh)) !== false) { if ($name === '.' || $name === '..') { continue; } $path = $dirReal . DIRECTORY_SEPARATOR . $name; if (@is_dir($path)) { if (ws_can_traverse_dir($path)) { $stack[] = $path; } else { ws_note_dir_skip($path); } continue; } if (!@is_file($path)) { continue; } if (!@is_readable($path)) { ws_skip_count('file-unreadable'); continue; } $real = @realpath($path); if ($real === false) { continue; } $norm = strtolower(str_replace('\\', '/', $real)); if ($selfNorm !== false && $norm === $selfNorm) { continue; } if ($extFilterActive) { $ext = strtolower(pathinfo($real, PATHINFO_EXTENSION)); if (!in_array($ext, $allowedExts, true)) { continue; } } @file_put_contents($queueFile, $real . "\n", FILE_APPEND | LOCK_EX); } @closedir($dh); } return empty($stack); } function ws_process_queue_batch($queueFile, &$byteOffset, $maxFiles, callable $fn) { $h = @fopen($queueFile, 'rb'); if ($h === false) { return true; } if ($byteOffset > 0) { fseek($h, (int)$byteOffset); } $n = 0; while ($n < $maxFiles) { $line = fgets($h); if ($line === false) { break; } $byteOffset = (int)ftell($h); $file = rtrim($line, "\r\n"); if ($file === '') { continue; } $n++; if (!@is_file($file) || !@is_readable($file)) { ws_skip_count('file-unreadable'); continue; } $fn($file); } $eof = feof($h); fclose($h); return $eof; } function ws_process_single_file($filepath) { global $stats, $skip_stats, $SCAN_ROOT, $DO_CLEAN, $BACKUP_BEFORE_CLEAN, $counter_js_interval, $BATCH_MODE, $WS_CLI; $counter_js_interval++; if ($counter_js_interval % 5 === 0 && empty($WS_CLI)) { flush_output('<script>upd("lc-scan",' . $stats['scanned'] . ');upd("lc-inf",' . $stats['infected'] . ');upd("lc-clean",' . $stats['cleaned'] . ');</script>'); } if (try_handle_oversize_php_file($filepath)) { return; } $skipReason = should_skip($filepath); if ($skipReason !== null) { ws_skip_count($skipReason); $stats['skipped']++; return; } if (!@is_readable($filepath)) { ws_skip_count('file-unreadable'); $stats['skipped']++; return; } $stats['scanned']++; $relPath = str_replace($SCAN_ROOT . DIRECTORY_SEPARATOR, '', $filepath); $scan = scan_file($filepath); if (!empty($scan['unreadable'])) { ws_skip_count('file-unreadable'); $stats['skipped']++; $stats['scanned']--; return; } $findings = array_merge($scan['keyword'], $scan['segment_regex']); if (empty($findings)) { if ($stats['scanned'] % 50 === 0) { log_line('OK', 'tag-ok', '... 已扫描 <b>' . $stats['scanned'] . '</b> 个文件,暂未发现威胁 ...'); } return; } $stats['infected']++; $maxRisk = 'medium'; foreach ($findings as $f) { if ($f['risk'] === 'critical') { $maxRisk = 'critical'; break; } if ($f['risk'] === 'high') { $maxRisk = 'high'; } } $riskTag = strtoupper($maxRisk); $riskClass = 'tag-' . ($maxRisk === 'critical' ? 'critical' : ($maxRisk === 'high' ? 'high' : 'medium')); $mtime_str = date('Y-m-d H:i:s', filemtime($filepath)); $size_kb = number_format(filesize($filepath) / 1024, 1); log_line($riskTag, $riskClass, '🚨 <b>' . htmlspecialchars($relPath) . '</b>' . ' <span class="pattern-info">[' . $size_kb . 'KB | mtime:' . $mtime_str . ']</span>'); $sourceLabel = ['keyword' => '关键词', 'segment_regex' => '分段正则']; foreach ($findings as $f) { $cleanable = $f['clean'] ? '<span style="color:var(--green)">✔可清理</span>' : '<span style="color:var(--yellow)">⚠需人工</span>'; $src = isset($f['source']) ? ('[' . (isset($sourceLabel[$f['source']]) ? $sourceLabel[$f['source']] : $f['source']) . '] ') : ''; log_line(' ↳ RULE', 'tag-' . $f['risk'], $src . 'Rule:<em>' . htmlspecialchars($f['rule']) . '</em>' . ' Line:<b>' . $f['line'] . '</b>' . ' ' . $cleanable . ' <span class="pattern-info">' . htmlspecialchars(snippet_preview($f['snippet'], 80)) . '</span>'); } if ($DO_CLEAN) { $segmentForClean = array_merge( array_values(array_filter($scan['keyword'], function ($f) { return !empty($f['clean']); })), array_values(array_filter($scan['segment_regex'], function ($f) { return !empty($f['clean']); })) ); if (!empty($segmentForClean)) { $didChange = false; $ok = clean_file($filepath, $segmentForClean, $didChange); if ($ok && $didChange) { $stats['cleaned']++; log_line('CLEAN', 'tag-clean', '✔ 已清理:<b>' . htmlspecialchars($relPath) . '</b>(关键词或分段正则命中:删命中所在 <? … ?> 段;无 ?> 则删至文件末尾。Yanz/WSO 四条或 .php 且无 <? 时整文件清空)' . ($BACKUP_BEFORE_CLEAN ? ' (备份: .bak)' : '')); } elseif (!$ok) { $stats['errors']++; log_line('ERROR', 'tag-error', '✘ 清理失败(权限不足?):<b>' . htmlspecialchars($relPath) . '</b>'); } } } } if (!empty($WS_CLI)) { if (isset($argv[1]) && ($argv[1] === '--help' || $argv[1] === '-h')) { $bn = basename(__FILE__); fwrite(STDOUT, "用法: php {$bn} [扫描根目录绝对路径]\n"); fwrite(STDOUT, "在 SSH 下整站一次跑完,无浏览器/网关分批与超时问题。\n"); fwrite(STDOUT, "示例: php {$bn} /var/www/html/public\n"); exit(0); } fwrite(STDERR, '[' . date('Y-m-d H:i:s') . '] CLI 扫描 ROOT=' . $SCAN_ROOT . "\n"); write_log('[' . date('Y-m-d H:i:s') . '] 扫描开始 ROOT=' . $SCAN_ROOT); $counter_js_interval = 0; foreach (iterate_files($SCAN_ROOT) as $filepath) { ws_process_single_file($filepath); } fwrite(STDERR, sprintf( "完成 scanned=%d infected=%d cleaned=%d skipped=%d errors=%d\n", (int)$stats['scanned'], (int)$stats['infected'], (int)$stats['cleaned'], (int)$stats['skipped'], (int)$stats['errors'] )); if (!empty($skip_stats)) { foreach ($skip_stats as $reason => $cnt) { fwrite(STDERR, 'skip[' . $reason . ']=' . (int)$cnt . "\n"); } } write_log(''); write_log('=== 扫描完成 ==='); write_log('扫描文件: ' . $stats['scanned']); write_log('感染文件: ' . $stats['infected']); write_log('已清理: ' . $stats['cleaned']); write_log('跳过: ' . $stats['skipped']); write_log('错误: ' . $stats['errors']); exit(0); } if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ws_delete_self']) && $_POST['ws_delete_self'] === '1') { $selfPath = __FILE__; $deleted = @unlink($selfPath); header('Content-Type: text/html; charset=UTF-8'); echo '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>删除扫描器</title></head><body style="font-family:Arial,sans-serif;padding:20px;">'; if ($deleted) { echo '<h3 style="color:#16a34a;">已删除扫描器文件</h3>'; echo '<p><code>' . htmlspecialchars($selfPath, ENT_QUOTES, 'UTF-8') . '</code></p>'; echo '<p>请关闭本页面,扫描器入口已移除。</p>'; } else { echo '<h3 style="color:#dc2626;">删除失败</h3>'; echo '<p>可能是文件权限不足,或当前系统不允许删除正在执行的脚本(如部分 Windows 环境)。</p>'; echo '<p>目标文件:<code>' . htmlspecialchars($selfPath, ENT_QUOTES, 'UTF-8') . '</code></p>'; } echo '</body></html>'; exit(0); } ws_http_stream_headers(); ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>WebShell Scanner & Cleaner</title> <style> :root { --bg: #0d1117; --surface: #161b22; --border: #30363d; --text: #c9d1d9; --dim: #8b949e; --green: #3fb950; --red: #f85149; --yellow: #d29922; --blue: #58a6ff; --orange: #e3b341; --purple: #bc8cff; --font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; } * { box-sizing: border-box; margin: 0; padding: 0; } body { background: var(--bg); color: var(--text); font-family: var(--font-mono); font-size: 13px; line-height: 1.6; } #wrap { max-width: 1100px; margin: 0 auto; padding: 20px; } h1 { font-size: 20px; color: var(--blue); border-bottom: 1px solid var(--border); padding-bottom: 10px; margin-bottom: 16px; letter-spacing: 2px; } .config-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; font-size: 12px; } .config-table td { border: 1px solid var(--border); padding: 5px 10px; } .config-table td:first-child { color: var(--dim); width: 200px; } .log { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-x: auto; } .log-line { display: flex; gap: 10px; padding: 2px 0; border-bottom: 1px solid #1c2128; } .log-line:last-child { border-bottom: none; } .ts { color: var(--dim); min-width: 85px; } .tag { min-width: 80px; text-align: center; border-radius: 3px; padding: 0 5px; font-size: 11px; font-weight: bold; } .tag-critical { background: #3d1217; color: var(--red); } .tag-high { background: #2d1f00; color: var(--orange); } .tag-medium { background: #2d2200; color: var(--yellow); } .tag-info { background: #0d2149; color: var(--blue); } .tag-ok { background: #0d2e1a; color: var(--green); } .tag-skip { background: #1e2028; color: var(--dim); } .tag-clean { background: #0d2e1a; color: var(--green); } .tag-error { background: #3d1217; color: var(--red); } .msg { flex: 1; word-break: break-all; } .msg b { color: var(--blue); } .msg em { color: var(--yellow); font-style: normal; } .stats { display: flex; gap: 12px; flex-wrap: wrap; margin: 16px 0; } .stat-box { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px 18px; text-align: center; } .stat-box .num { font-size: 28px; font-weight: bold; } .stat-box .label { font-size: 11px; color: var(--dim); margin-top: 2px; } .num-scanned { color: var(--blue); } .num-infected { color: var(--red); } .num-cleaned { color: var(--green); } .num-skipped { color: var(--dim); } .num-errors { color: var(--orange); } .section-title { color: var(--purple); font-size: 13px; margin: 16px 0 8px; letter-spacing: 1px; border-left: 3px solid var(--purple); padding-left: 8px; } .done-banner { background: #0d2e1a; border: 1px solid var(--green); color: var(--green); border-radius: 6px; padding: 12px 18px; margin-top: 20px; font-size: 15px; text-align: center; letter-spacing: 2px; } .warning-banner { background: #2d1f00; border: 1px solid var(--orange); color: var(--orange); border-radius: 6px; padding: 10px 16px; margin-bottom: 14px; font-size: 12px; } #live-counter { position: fixed; right: 20px; top: 20px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; font-size: 12px; min-width: 140px; } #live-counter .lc-row { display: flex; justify-content: space-between; gap: 16px; } #live-counter .lc-val { color: var(--blue); font-weight: bold; } .pattern-info { color: var(--dim); font-size: 11px; } pre { white-space: pre-wrap; word-break: break-all; } </style> </head> <body> <div id="wrap"> <h1>🔍 WebShell Scanner & Cleaner v2.0</h1> <?php flush_output('<p id="ws-boot" style="color:var(--dim);font-size:12px;margin-bottom:10px;">已连接,正在初始化界面…</p>'); if (!empty($WS_RESCAN_MESSAGE)) { if (strpos($WS_RESCAN_MESSAGE, '已切换') !== false) { flush_output('<div class="done-banner" style="margin-bottom:12px;font-size:12px;">' . $WS_RESCAN_MESSAGE . '</div>'); } else { flush_output('<p style="color:var(--dim);font-size:12px;margin-bottom:12px;">' . $WS_RESCAN_MESSAGE . '</p>'); } } $mode_label = $DO_CLEAN ? '<span style="color:var(--red)">⚠ 清理模式(将修改文件)</span>' : '<span style="color:var(--green)">✔ 仅检测模式(不修改文件)</span>'; flush_output('<div class="section-title">▶ 当前配置</div>'); flush_output('<table class="config-table">'); flush_output('<tr><td>扫描根目录</td><td>' . htmlspecialchars($SCAN_ROOT) . '</td></tr>'); flush_output('<tr><td>运行模式</td><td>' . $mode_label . '</td></tr>'); flush_output('<tr><td>备份原文件</td><td>' . ($BACKUP_BEFORE_CLEAN ? '✔ 是' : '✘ 否') . '</td></tr>'); flush_output('<tr><td>规则扫描大小上限</td><td>' . number_format($MAX_FILE_SIZE / 1024, 0) . ' KB(仅 ≤ 此大小的文件做关键词/正则检测)</td></tr>'); flush_output('<tr><td>超大 PHP 直接删除</td><td>' . number_format($PHP_OVERSIZE_DELETE_BYTES / 1024, 0) . ' KB 以上、扩展名为 php/php3/…/pht 时,清理模式下 <code>unlink</code>(自身与白名单文件名除外)</td></tr>'); flush_output('<tr><td>扫描扩展名</td><td>' . (empty($ALLOWED_EXTENSIONS) ? '全部' : implode(', ', $ALLOWED_EXTENSIONS)) . '</td></tr>'); $ts_range = '不限制'; if ($TIME_START || $TIME_END) { $ts_range = ($TIME_START ? date('Y-m-d H:i:s', $TIME_START) : '∞') . ' → ' . ($TIME_END ? date('Y-m-d H:i:s', $TIME_END) : '∞'); } flush_output('<tr><td>时间区间(mtime)</td><td>' . $ts_range . '</td></tr>'); flush_output('<tr><td>日志文件</td><td>' . (empty($LOG_FILE) ? '不写日志' : htmlspecialchars($LOG_FILE)) . '</td></tr>'); flush_output('<tr><td>扫描方式</td><td>' . ($BATCH_MODE ? '✔ 分批(队列 + 续跑,每批约 ' . (int)$BATCH_PROCESS_FILES . ' 个文件)' : '✘ 单次请求:浏览器一次 HTTP 内从 <code>$SCAN_ROOT</code> 递归扫完。目录很大可能网关超时,大站请 SSH:<code>php ' . htmlspecialchars(basename(__FILE__)) . ' ' . htmlspecialchars($SCAN_ROOT) . '</code>') . '</td></tr>'); if ($BATCH_MODE && !empty($BATCH_PROGRESS_LOG)) { flush_output('<tr><td>分批进度文件</td><td><code>' . htmlspecialchars((string)$BATCH_PROGRESS_LOG) . '</code>(每批追加一行,<code>tail -f</code> 可看是否在跑)</td></tr>'); } flush_output('</table>'); if ($DO_CLEAN) { flush_output('<div class="warning-banner">⚠ 警告:当前为 <strong>清理模式</strong>,将直接修改磁盘上的文件。如果未充分测试,请先将 $DO_CLEAN 设为 false 以预览结果。</div>'); } flush_output(''); flush_output('<div id="live-counter"> <div class="lc-row"><span>已扫描</span> <span class="lc-val" id="lc-scan">0</span></div> <div class="lc-row"><span>已感染</span> <span class="lc-val" id="lc-inf" style="color:var(--red)">0</span></div> <div class="lc-row"><span>已清理</span> <span class="lc-val" id="lc-clean" style="color:var(--green)">0</span></div> </div> <script> function upd(id,v){ var el=document.getElementById(id); if(el) el.textContent=v; } </script>'); flush_output('<div class="section-title">▶ 扫描日志</div>'); flush_output('<div class="log" id="logbox">'); write_log('[' . date('Y-m-d H:i:s') . '] 扫描开始 ROOT=' . $SCAN_ROOT); log_line('INFO', 'tag-info', '开始扫描 <b>' . htmlspecialchars($SCAN_ROOT) . '</b>' . ($BATCH_MODE ? '(分批模式,每批约 ' . (int)$BATCH_PROCESS_FILES . ' 个文件)' : '(单次请求扫完全站)') . ' …'); $ws_batch_paths = ws_batch_paths($SCAN_ROOT); $ws_queue_file = $ws_batch_paths['queue']; $ws_state_file = $ws_batch_paths['state']; $ws_state = null; if ($BATCH_MODE) { $isBatchContinue = isset($_POST['ws_batch_continue']) || isset($_GET['ws_batch_continue']); if ($isBatchContinue) { $raw = @file_get_contents($ws_state_file); $ws_state = $raw ? json_decode($raw, true) : null; if (is_array($ws_state) && isset($ws_state['stats'])) { $stats = $ws_state['stats']; $skip_stats = isset($ws_state['skip_stats']) ? $ws_state['skip_stats'] : []; } else { $ws_state = null; } } if (!is_array($ws_state)) { @unlink($ws_queue_file); @unlink($ws_state_file); $ws_state = array( 'walk_stack' => array($SCAN_ROOT), 'walk_done' => false, 'proc_off' => 0, 'scan_root' => $SCAN_ROOT, ); ws_batch_active_write($SCAN_ROOT, $ws_state_file, $ws_queue_file); ws_batch_progress_append('新扫描任务开始 ROOT=' . $SCAN_ROOT); } elseif (empty($ws_state['scan_root'])) { $ws_state['scan_root'] = $SCAN_ROOT; } } $counter_js_interval = 0; if (!$BATCH_MODE) { foreach (iterate_files($SCAN_ROOT) as $filepath) { ws_process_single_file($filepath); } } else { $selfPath = (string)realpath(__FILE__); $extFilterActive = !empty($ALLOWED_EXTENSIONS); $needContinue = false; if (empty($ws_state['walk_done'])) { $stack = &$ws_state['walk_stack']; if (!is_array($stack) || empty($stack)) { $stack = [$SCAN_ROOT]; } $walkDone = ws_walk_batch_append_queue($ws_queue_file, $stack, (float)$BATCH_WALK_SECONDS, $ALLOWED_EXTENSIONS, $extFilterActive, $selfPath); $ws_state['walk_stack'] = $stack; $ws_state['walk_done'] = $walkDone; if (!$walkDone) { $needContinue = true; } } if (!empty($ws_state['walk_done']) && !$needContinue) { $counter_js_interval = 0; $procOff = (int)$ws_state['proc_off']; $eof = ws_process_queue_batch($ws_queue_file, $procOff, (int)$BATCH_PROCESS_FILES, 'ws_process_single_file'); $ws_state['proc_off'] = $procOff; if (!$eof) { $needContinue = true; } } $ws_state['stats'] = $stats; $ws_state['skip_stats'] = $skip_stats; $ws_state['scan_root'] = $SCAN_ROOT; if ($needContinue) { @file_put_contents($ws_state_file, json_encode($ws_state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ws_batch_active_write($SCAN_ROOT, $ws_state_file, $ws_queue_file); $phaseCode = empty($ws_state['walk_done']) ? 'walk' : 'process'; ws_batch_progress_append(sprintf( '本批 PHP 已执行完毕,等待浏览器发起下一轮 POST 才继续 | phase=%s scanned=%d infected=%d cleaned=%d walk_done=%s', $phaseCode, (int)$stats['scanned'], (int)$stats['infected'], (int)$stats['cleaned'], !empty($ws_state['walk_done']) ? 'yes' : 'no' )); $qSize = @filesize($ws_queue_file); $qSizeStr = ($qSize !== false) ? number_format($qSize / 1024, 1) . ' KB' : '—'; $qLines = 0; if ($qSize !== false && $qSize > 0 && $qSize <= 2097152) { $qLines = substr_count((string)@file_get_contents($ws_queue_file), "\n"); } $stackLeft = isset($ws_state['walk_stack']) && is_array($ws_state['walk_stack']) ? count($ws_state['walk_stack']) : 0; if (empty($ws_state['walk_done'])) { log_line('INFO', 'tag-info', '<strong>建队列阶段</strong>:正在递归目录收集 PHP 路径;此阶段<strong>「已扫描」仍为 0 属正常</strong>。队列建完后才会进入逐文件检测。请保持浏览器执行自动续跑,或反复点击「继续下一批」直至出现「扫描完成」。'); } else { log_line('INFO', 'tag-info', '<strong>处理队列阶段</strong>:已开始按队列扫描/清理;若本批未跑完全部文件,将继续自动续跑。'); } flush_output('</div>'); flush_output('<div class="section-title">▶ 分批续跑</div>'); flush_output('<div class="warning-banner">⏳ <strong>任务未完成,自动续跑中</strong>(当前批次已结束,正在发起下一轮请求)。<br><span style="color:var(--dim);font-weight:normal;">优先自动 POST;若 POST 被浏览器/插件拦截,会自动跳转续跑 URL 继续。仅当页面关闭、断网或服务器拒绝请求时才会停住。服务器上可用 <code>tail -f</code> 看上面的「分批进度文件」是否持续追加。</span></div>'); flush_output('<table class="config-table">'); $phaseLabel = empty($ws_state['walk_done']) ? '递归列出 PHP 文件(建队列)' : '按队列扫描/清理'; flush_output('<tr><td>当前阶段</td><td>' . htmlspecialchars($phaseLabel) . '</td></tr>'); flush_output('<tr><td>队列文件</td><td>' . htmlspecialchars($qSizeStr) . ($qLines > 0 ? '(约 ' . (int)$qLines . ' 个路径)' : ($qSize !== false && $qSize > 2097152 ? '(>2MB 未逐行计数)' : '')) . '</td></tr>'); if (empty($ws_state['walk_done'])) { flush_output('<tr><td>待遍历目录栈</td><td>约 ' . (int)$stackLeft . ' 个(栈未空则仍需续跑建队列)</td></tr>'); } flush_output('<tr><td>已扫描</td><td>' . (int)$stats['scanned'] . ' <span class="pattern-info">(建队列阶段多为 0)</span></td></tr>'); flush_output('<tr><td>已感染</td><td>' . (int)$stats['infected'] . '</td></tr>'); flush_output('<tr><td>已清理</td><td>' . (int)$stats['cleaned'] . '</td></tr>'); flush_output('</table>'); $scriptName = isset($_SERVER['SCRIPT_NAME']) ? (string)$_SERVER['SCRIPT_NAME'] : ''; $formAction = htmlspecialchars($scriptName, ENT_QUOTES, 'UTF-8'); $continueUrlRaw = $scriptName . '?ws_batch_continue=1&ws_scan_root=' . rawurlencode($SCAN_ROOT) . '&_t=' . time(); $continueUrl = htmlspecialchars($continueUrlRaw, ENT_QUOTES, 'UTF-8'); flush_output('<form method="post" action="' . $formAction . '" id="wsBatchForm" style="margin-top:14px;">'); flush_output('<input type="hidden" name="ws_batch_continue" value="1">'); flush_output('<input type="hidden" name="ws_scan_root" value="' . htmlspecialchars($SCAN_ROOT, ENT_QUOTES, 'UTF-8') . '">'); flush_output('<p><button type="submit" style="padding:10px 20px;font-size:14px;background:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer;">继续下一批</button>' . ' <button type="button" id="wsBatchPauseBtn" style="padding:10px 20px;font-size:14px;background:#374151;color:#fff;border:none;border-radius:6px;cursor:pointer;">暂停自动续跑</button></p>'); flush_output('</form>'); flush_output('<noscript><p style="color:var(--yellow);font-size:12px;">浏览器禁用 JS:请点击上面的按钮,或打开 <a href="' . $continueUrl . '">' . $continueUrl . '</a></p></noscript>'); ws_batch_auto_continue_script($continueUrl); flush_output('<div class="section-title">▶ 扫描结果汇总(进行中)</div>'); flush_output('<div class="stats">'); flush_output('<div class="stat-box"><div class="num num-scanned">' . $stats['scanned'] . '</div><div class="label">文件已扫描</div></div>'); flush_output('<div class="stat-box"><div class="num num-infected">' . $stats['infected'] . '</div><div class="label">感染文件</div></div>'); flush_output('<div class="stat-box"><div class="num num-cleaned">' . $stats['cleaned'] . '</div><div class="label">已清理</div></div>'); flush_output('<div class="stat-box"><div class="num num-skipped">' . $stats['skipped'] . '</div><div class="label">已跳过</div></div>'); flush_output('<div class="stat-box"><div class="num num-errors">' . $stats['errors'] . '</div><div class="label">错误</div></div>'); flush_output('</div>'); flush_output('<p style="color:var(--dim);font-size:11px;margin-top:12px;">服务器侧可增大 Nginx <code>fastcgi_read_timeout</code>;或在本脚本中减小 <code>$BATCH_WALK_SECONDS</code> / <code>$BATCH_PROCESS_FILES</code>。</p>'); ws_flush_rescan_form($SCAN_ROOT, true); flush_output('</div></body></html>'); exit(0); } @unlink($ws_state_file); @unlink($ws_queue_file); ws_batch_active_clear(); ws_batch_progress_append(sprintf( '全部完成 | scanned=%d infected=%d cleaned=%d skipped=%d errors=%d', (int)$stats['scanned'], (int)$stats['infected'], (int)$stats['cleaned'], (int)$stats['skipped'], (int)$stats['errors'] )); } flush_output('<script>upd("lc-scan",' . $stats['scanned'] . ');upd("lc-inf",' . $stats['infected'] . ');upd("lc-clean",' . $stats['cleaned'] . ');</script>'); flush_output('</div>'); flush_output('<div class="section-title">▶ 扫描结果汇总</div>'); flush_output('<div class="stats">'); flush_output('<div class="stat-box"><div class="num num-scanned">' . $stats['scanned'] . '</div><div class="label">文件已扫描</div></div>'); flush_output('<div class="stat-box"><div class="num num-infected">' . $stats['infected'] . '</div><div class="label">感染文件</div></div>'); flush_output('<div class="stat-box"><div class="num num-cleaned">' . $stats['cleaned'] . '</div><div class="label">已清理</div></div>'); flush_output('<div class="stat-box"><div class="num num-skipped">' . $stats['skipped'] . '</div><div class="label">已跳过</div></div>'); flush_output('<div class="stat-box"><div class="num num-errors">' . $stats['errors'] . '</div><div class="label">错误</div></div>'); flush_output('</div>'); if (!empty($skip_stats)) { flush_output('<div class="section-title">▶ 跳过原因统计(若「已扫描」为 0 或远小于预期,请看这里)</div>'); flush_output('<table class="config-table">'); foreach ($skip_stats as $reason => $cnt) { flush_output('<tr><td>' . htmlspecialchars((string)$reason) . '</td><td>' . (int)$cnt . '</td></tr>'); } flush_output('</table>'); } write_log(''); write_log('=== 扫描完成 ==='); write_log('扫描文件: ' . $stats['scanned']); write_log('感染文件: ' . $stats['infected']); write_log('已清理: ' . $stats['cleaned']); write_log('跳过: ' . $stats['skipped']); write_log('错误: ' . $stats['errors']); if (!empty($LOG_FILE)) { flush_output('<p style="color:var(--dim);font-size:12px;margin-top:8px;">📄 日志已保存至:' . htmlspecialchars($LOG_FILE) . '</p>'); } $done_msg = $DO_CLEAN ? '✅ 扫描并清理完成!已处理 ' . $stats['infected'] . ' 个感染文件,清理 ' . $stats['cleaned'] . ' 个' : '✅ 扫描完成(仅检测,未修改文件)!发现 ' . $stats['infected'] . ' 个可疑文件'; $selfDeleteAction = isset($_SERVER['SCRIPT_NAME']) ? htmlspecialchars((string)$_SERVER['SCRIPT_NAME'], ENT_QUOTES, 'UTF-8') : ''; flush_output('<div class="done-banner">' . $done_msg . '</div>'); if (!$WS_CLI) { ws_flush_rescan_form($SCAN_ROOT, false); } flush_output('<form method="post" action="' . $selfDeleteAction . '" style="margin-top:12px;text-align:center;">' . '<input type="hidden" name="ws_delete_self" value="1">' . '<button type="submit" style="padding:8px 16px;font-size:13px;background:#b91c1c;color:#fff;border:none;border-radius:6px;cursor:pointer;">删除当前扫描器 PHP</button>' . '</form>'); $t0 = isset($_SERVER['REQUEST_TIME_FLOAT']) ? (float)$_SERVER['REQUEST_TIME_FLOAT'] : microtime(true); flush_output('<p style="color:var(--dim);font-size:11px;margin-top:12px;text-align:center;">完成时间:' . date('Y-m-d H:i:s') . ' | 耗时:' . round(microtime(true) - $t0, 2) . 's</p>'); ?> </div> </body> </html>
💾 保存文件
← 返回文件管理器