" . ""; echo "

✅ 本文件已删除

感谢你及时清理。服务器上不再残留此检测脚本。

"; @unlink(__FILE__); echo ""; exit; } // ---------- 小工具 ---------- function ccGe($a, $b) { return version_compare($a, $b, '>='); } function ccArch() { return PHP_INT_SIZE === 8 ? '64 位 (x86_64 / arm64)' : '32 位 (x86)'; } function ccSafeEcho($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } function ccCut($s, $n) { $s = (string)$s; return strlen($s) <= $n ? $s : substr($s, 0, $n) . '…'; } $phpVer = PHP_VERSION; $sapi = php_sapi_name(); $osFam = defined('PHP_OS_FAMILY') ? PHP_OS_FAMILY : PHP_OS; $arch = ccArch(); $isWindows = (strncasecmp($osFam, 'WIN', 3) === 0); $loaded = get_loaded_extensions(); $loadedLower = array_map('strtolower', $loaded); function ccExtLoaded($name) { global $loadedLower; return in_array(strtolower($name), $loadedLower, true); } // ---------- 防火墙(cc-shield)拦截自查 ---------- // 目的:当站点「偶尔打不开 / 刷新几次又好了」时,用这里一次性定位是防火墙的哪一层拦的。 // 本段只读取配置与日志,不执行防火墙主流程(避免自己把自己拦掉)。 $fwDir = ''; foreach (array(__DIR__ . '/cc-shield', __DIR__ . '/../cc-shield') as $cand) { if (is_file($cand . '/bootstrap.php') && is_file($cand . '/src/Shield.php')) { $fwDir = $cand; break; } } $fwErr = ''; $fw = null; $gcMsg = ''; if ($fwDir === '') { $fwErr = '未找到 cc-shield 目录。请把本文件放在 cc-shield 的同级目录(通常是网站根目录)后重试。'; } else { require_once $fwDir . '/src/Store.php'; require_once $fwDir . '/src/Shield.php'; try { $cfg = Shield::configure(); $store = Shield::store(); // 一键清理过期缓存(限频槽文件只写不读,长期不清理会拖慢站点) if (isset($_POST['do_gc']) && $_POST['do_gc'] === '1') { $n = $store->gcNow(); $gcMsg = '已清理 ' . (int)$n . ' 个过期缓存文件。'; } $rc = new ReflectionClass('Shield'); $mIp = $rc->getMethod('clientIp'); $mIp->setAccessible(true); $seenIp = $mIp->invoke(null, !empty($cfg['trust_xff'])); $backend = $store->backendName(); // 更具体的后端描述(能区分是哪个 Memcache 扩展,还是退化到了文件存储) $backendDetail = method_exists($store, 'backendDetail') ? $store->backendDetail() : $backend; // 缓存键前缀:多站点共用同一台 Memcache/Redis 时靠它隔离 $storePrefix = method_exists($store, 'prefix') ? $store->prefix() : '(本版本无此功能)'; $storePrefixFrom = method_exists($store, 'prefixFrom') ? $store->prefixFrom() : ''; // 跨请求计数器:刷新本页看这个数字是否递增 $probe = $store->incr('diag:probe', 300); $stats = $store->cacheStats(); // data/ 与盐值 $dataDir = $fwDir . '/data'; $writable = is_dir($dataDir) && is_writable($dataDir); $saltFile = isset($cfg['salt_file']) ? $cfg['salt_file'] : ($dataDir . '/.salt'); $saltOk = is_file($saltFile) && trim((string)@file_get_contents($saltFile)) !== ''; $saltVol = (isset($cfg['_salt_persist']) && $cfg['_salt_persist'] === false); // 日志分析(只取尾部,避免大文件拖慢) $logFile = isset($cfg['log_file']) ? $cfg['log_file'] : ($dataDir . '/cc-shield.log'); $logTotal = 0; $logRows = array(); if (is_file($logFile)) { $all = @file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); if (is_array($all)) { $logTotal = count($all); foreach (array_slice($all, -3000) as $ln) { $f = explode("\t", $ln); if (count($f) >= 7) { $logRows[] = array_map('trim', $f); } } } } $stageCnt = array(); $reasonCnt = array(); $ipCnt = array(); $lastRows = array(); $whyCnt = array(); $floodMax = 0; foreach ($logRows as $r) { $st = strpos($r[2], ':') !== false ? substr($r[2], 0, strpos($r[2], ':')) : $r[2]; $stageCnt[$st] = (isset($stageCnt[$st]) ? $stageCnt[$st] : 0) + 1; $rs = preg_replace('/\s*count=\d+\/\d+|\s*count=\d+/', '', $r[4]); $rs = strpos($rs, '=') !== false ? substr($rs, 0, strpos($rs, '=')) : $rs; $reasonCnt[$rs] = (isset($reasonCnt[$rs]) ? $reasonCnt[$rs] : 0) + 1; if ($r[3] !== '') { $ipCnt[$r[3]] = (isset($ipCnt[$r[3]]) ? $ipCnt[$r[3]] : 0) + 1; } if (preg_match('/why=([a-z0-9_:.]+)/i', $r[4], $mw)) { $k = strpos($mw[1], 'ip_mismatch') === 0 ? 'ip_mismatch(IP 漂移)' : $mw[1]; $whyCnt[$k] = (isset($whyCnt[$k]) ? $whyCnt[$k] : 0) + 1; } if (preg_match('/challenge_flood count=(\d+)/', $r[4], $m)) { if ((int)$m[1] > $floodMax) { $floodMax = (int)$m[1]; } } } arsort($stageCnt); arsort($reasonCnt); arsort($ipCnt); arsort($whyCnt); $lastRows = array_slice($logRows, -12); // 最近 12 条(保持时间顺序) /* 挑战令牌往返自测 —— 直接回答「这个浏览器为什么老被挑战」: · 没带 cookie → 首次访问属正常;反复如此就是 cookie 没写进去 / 没被带回 · 带了但不过 → why 就是结论(ip_mismatch ⇒ IP 不稳定;bad_hex ⇒ 盐或 IP 对不上) */ $ckName = isset($cfg['cookie_name']) ? $cfg['cookie_name'] : 'cc_vt'; $ckVal = isset($_COOKIE[$ckName]) ? (string)$_COOKIE[$ckName] : ''; $ckOk = null; // null = 本次请求根本没带 cookie $ckWhy = ''; if ($ckVal !== '') { $mTok = $rc->getMethod('tokenCheck'); $mTok->setAccessible(true); $tr = $mTok->invoke(null, $cfg, $seenIp, $ckVal); $ckOk = (bool)$tr[0]; $ckWhy = (string)$tr[1]; } $fw = array( 'dir' => $fwDir, 'cfg' => $cfg, 'backend' => $backend, 'backendDetail' => $backendDetail, 'storePrefix' => $storePrefix, 'storePrefixFrom' => $storePrefixFrom, 'seenIp' => $seenIp, 'probe' => $probe, 'stats' => $stats, 'writable' => $writable, 'saltOk' => $saltOk, 'saltVol' => $saltVol, 'logFile' => $logFile, 'logTotal' => $logTotal, 'stageCnt' => $stageCnt, 'reasonCnt' => $reasonCnt, 'ipCnt' => $ipCnt, 'lastRows' => $lastRows, 'floodMax' => $floodMax, 'whyCnt' => $whyCnt, 'ckName' => $ckName, 'ckVal' => $ckVal, 'ckOk' => $ckOk, 'ckWhy' => $ckWhy, ); } catch (\Throwable $e) { $fwErr = '读取防火墙配置失败:' . ccSafeEcho($e->getMessage()); } } // 主动探测:php-cgi.exe 是否能被 Web 直接访问(仅用户点击时执行) $cgiExposed = null; // null = 未检测 if (strtoupper(isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET') === 'POST' && isset($_POST['probe_cgi']) && $_POST['probe_cgi'] === '1') { $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''; $scheme = (!empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') !== 0) ? 'https' : 'http'; if ($host !== '') { $url = $scheme . '://' . $host . '/php-cgi.exe'; $ctx = @stream_context_create(array('http' => array('timeout' => 4, 'method' => 'GET', 'header' => "User-Agent: cc-env-check\r\n"))); $res = @file_get_contents($url, false, $ctx); if ($res === false) { $cgiExposed = '无法判定(请求失败 / allow_url_fopen 关闭 / 被防火墙拦截)'; } elseif ($httpCode = (isset($http_response_header) ? ccParseCode($http_response_header) : 0)) { $cgiExposed = ($httpCode >= 200 && $httpCode < 400) ? '⚠️ 可被访问(HTTP ' . $httpCode . ')—— php-cgi.exe 直接暴露,风险极高!' : '正常(返回 HTTP ' . $httpCode . ',不直接暴露)'; } else { $cgiExposed = '返回了内容但无法判定状态码,请人工确认。'; } } else { $cgiExposed = '无法确定主机名,无法检测。'; } } function ccParseCode($headers) { foreach ($headers as $h) { if (preg_match('#^HTTP/\d\.\d\s+(\d{3})#i', $h, $m)) return (int)$m[1]; } return 0; } // CVE 列表(针对当前 PHP 7.1.x 等已 EOL 分支):固定版本均 > 当前版本,且分支已停止维护 $cves = array( array( 'id' => 'CVE-2024-4577', 'name' => 'PHP-CGI 参数注入(未授权 RCE,Critical)', 'fixed' => '7.x 无修复(EOL);官方仅 8.1.29 / 8.2.20 / 8.3.8+', 'scope' => 'Windows + CGI 模式', 'note' => '攻击者在 query string 塞入 %ad(软连字符),利用 Windows “最佳适配”编码转成 “-”,向 php-cgi.exe 注入 -d 指令实现远程代码执行。中文(简体)Windows 为高危环境。', ), array( 'id' => 'CVE-2018-19518', 'name' => 'imap_open() RCE', 'fixed' => '7.1.25 / 7.2.13 / 7.3.0', 'scope' => '加载了 imap 扩展时', 'note' => '通过 imap_open 触发 rsh 命令注入,可远程执行命令。', ), array( 'id' => 'CVE-2019-6977', 'name' => 'gd 堆缓冲区溢出', 'fixed' => '7.1.27 / 7.2.16 / 7.3.3', 'scope' => '加载了 gd 扩展时', 'note' => '处理特制图像时堆溢出,可能导致崩溃或代码执行。', ), array( 'id' => 'CVE-2019-9020~9024', 'name' => '多组件内存安全(mbstring/exif/xmlrpc/gd 等)', 'fixed' => '7.1.27 / 7.2.16 / 7.3.3', 'scope' => '取决于加载的扩展', 'note' => '一批越界读 / 越界写 / 空指针解引用问题。', ), array( 'id' => 'CVE-2019-9640 / 9641', 'name' => '多个内存安全漏洞', 'fixed' => '7.1.28 / 7.2.17 / 7.3.4', 'scope' => '取决于加载的扩展', 'note' => '与上面同类,修复于 7.1.28。', ), array( 'id' => 'CVE-2019-11036', 'name' => 'gd 堆溢出', 'fixed' => '7.1.30 / 7.2.19 / 7.3.6', 'scope' => '加载了 gd 扩展时', 'note' => '又一处 gd 图像处理堆溢出。', ), array( 'id' => 'CVE-2019-11043', 'name' => 'PHP-FPM 缓冲区溢出 RCE', 'fixed' => '7.1.33 / 7.2.24 / 7.3.11', 'scope' => '仅 PHP-FPM(FastCGI 进程管理器)', 'note' => '仅影响 PHP-FPM;CGI / FastCGI(常规) / mod_php 不受影响。', ), ); $eolDate = '2019-12-01'; $branchEol = ccGe($phpVer, '7.2.0') ? null : (ccGe($phpVer, '7.1.0') ? $eolDate : null); // CVE-2024-4577 暴露评估 if ($isWindows) { $cve4577Risk = '取决于 IIS 处理程序映射:若 .php 映射的「模块」是 CgiModule(经典 CGI)→ 高危;' . '若是 FastCgiModule(FastCGI 协议)→ 该路径通常安全。请到 IIS 管理器确认。'; } else { $cve4577Risk = '不适用(CVE-2024-4577 仅影响 Windows 上的 PHP)。'; } ?> CC Shield 环境检测(用完即删)
🛑 安全提醒:本页面会显示服务器敏感环境信息,查看完毕请立即删除本文件! 文件名已写明「用完即删」。若遗留在网站上,等于把服务器信息免费送给攻击者。

CC Shield · 网站环境检测

生成时间: · 本工具只读,不修改任何服务器配置。

一、运行环境

PHP 版本 Server API CPU 架构 32 位 防火墙高位 IP 匹配可能偏差,建议换 64 位 PHP'; ?> 操作系统 / PHP 分支状态已停止维护 (EOL ' . ccSafeEcho($branchEol) . ') 自该日起所有漏洞均无官方补丁'; } else { echo '仍在维护'; } ?> 危险扩展' . ccSafeEcho(implode('、', $risky)) . '' : '未检测到高风险扩展'; ?> 全部已加载扩展

二、历史漏洞逐步检查

CVE-2024-4577(PHP-CGI 参数注入 RCE,Critical) 暴露评估:

提示:CC Shield 已内置「链接/查询串注入防护」,会在 PHP 脚本层拦截 %ad / %2d 等注入特征(纵深防御)。 但它不能替代升级 PHP / 改用 FastCGI 等根本修复——经典 CGI + auto_prepend_file 变体下注入发生在脚本运行前。

漏洞编号说明官方修复版本影响范围当前状态
未修补
分支 EOL,无补丁
请对照版本

说明:本机为 ,上述 CVE 的修复版本均高于当前版本; 且 PHP 7.1 分支已于 停止维护,官方不会再为 7.1.x 回溯补丁,因此全部处于「未修补」状态。

php-cgi.exe 是否可被 Web 直接访问?

若可直接访问(返回 200),等于把 PHP 解释器入口暴露在外,CVE-2024-4577 等攻击面会显著放大。建议确保 php-cgi.exe 不在网站根目录、不能被浏览器直接请求。

结果:

三、处置建议

四、防火墙(cc-shield)拦截自查

用于排查「站点偶尔打不开 / 刷新几次又可以打开」。本段只读配置与日志,不会改动你的站点。

1) 防火墙看到的客户端 IP

REMOTE_ADDR(直连 IP)
X-Forwarded-For(原始)
X-Real-IP
防火墙最终采用的 IP
trust_xff(是否信任代理头)

⚠️ 检测到代理头,但 trust_xff 是关闭的。如果站点前面有 CDN / Nginx 反代 / 负载均衡, 防火墙看到的会是节点 IP;而节点 IP 每个请求都可能不同 —— 这会造成「令牌永远校验不上 → 反复人机验证 → 刷新几次才好」。请开启 trust_xff = 1, 或(拿不到真实 IP 时)设 token_bind_ip = 0。

⚠️ 防火墙看到的 IP 是内网地址,说明请求经过了代理。若你确定前面有 CDN / 反代, 请开启 trust_xff = 1;否则所有访客会共用同一个 IP 计数,容易被限频误伤。

判断方法:连续刷新本页 3~5 次,若下面第 2 节的 IP 每次都一样,说明取到的 IP 是稳定的。

2) 状态是否跨请求共享(关键)

存储后端 ← APCu 按 PHP 进程隔离,限频/封禁在各进程间结论不一致,会出现间歇性拦截!建议改 store_type = file 或上 Redis。 ← 正在进行磁盘存储:每个请求都会在 data/cache/ 下读写文件。若服务器其实装了 Memcache / Memcached 扩展,说明没探到(会拖慢请求,Windows 上还易被杀软扫描拖出周期性卡顿),请确认用的是最新 src/Store.php,并检查扩展是否已加载。
缓存键前缀 (来源:)
同一台服务器上多个项目共用这个 时,靠这串前缀隔离: 前缀不同 → 限频计数、临时封禁、爬虫 DNS 配额互不干扰。 如果两个站显示的前缀完全一样,说明它们共用了同一份 cc-shield 目录且文档根也相同,请在 ini 里给各站写不同的 store_prefix。
跨请求计数器(刷新页面看是否 +1) 次
缓存文件数 / 占用
data/ 目录可写 ❌ 不可写(日志/盐值/缓存都写不进,会引发挑战反复与限频失效)'; ?>
盐值文件(令牌签名用) ❌ 缺失'; ?> (无法持久化 → 挑战已自动放行)' : ''; ?>

判断方法:连续刷新本页,计数器应该每次 +1。如果它老是卡在 1,说明存储没有跨请求共享(几乎都是 APCu 或多进程问题), 限频与封禁就会时灵时不灵。

3) 当前生效的防护参数

运行模式 mode (观察模式:只记日志不拦截,所有拦截都会「看起来失效」)
链接注入防护
JS 挑战
挑战窗口 秒
挑战软上限 / 硬上限 软 次 —— 超出只记日志、继续下发挑战; 硬 0 ? ('' . $ccHard . ' 次 —— 超出才 403') : '0(永不封禁)'; ?> ← 软上限偏低。公司/校园/运营商 NAT 下很多真人各自被挑战一次,累计很容易超阈值;旧版会直接 403,整片出口都会「该页无法显示」。建议 ≥300。
令牌是否绑定 IP 不绑定(token_bind_ip = 0) 绑定,粒度 IPv4 /、IPv6 / = 32): ?>(精确到单个 IP;移动网络/CGNAT 出口 IP 会漂移时建议改为 24)
爬虫来源校验静态 IP 名单:; 反向 DNS:; 仅凭 UA 放行:开启(不安全)' : '关闭'; ?>
限频分组

4) 挑战令牌往返自测(直接定位「为什么老被挑战」)

本节读取的是你当前这个浏览器真实携带的 cookie, 再用服务器完全相同的规则验一遍。结论就是「浏览器为什么被反复挑战」。

本次请求携带的 cookie 没有携带 (长度 )
令牌校验结果 未携带 cookie,无法校验'; } elseif ($fw['ckOk']) { echo '✅ 通过 —— 令牌能走通,你的浏览器没问题'; } else { echo '❌ 未通过,原因 ' . ccSafeEcho($fw['ckWhy']) . ''; } ?>

第一次访问本页还没令牌,属正常。请刷新本页 3~5 次后再看这一节:
· 刷新后变成「✅ 通过」→ 挑战链路完全正常,之前的反复验证与令牌无关;
· 刷新后仍是「没有携带」→ cookie 没被写入或没被带回(浏览器禁用了 cookie,或中间有代理改写);
· 刷新后变成「❌ 未通过」→ 按上表的 why 值对照处理。

这就是浏览器被反复挑战的直接原因。按 why 对症处理:

why 取值含义处置
ip_mismatch:… 令牌本身有效,但签名用的是另一个 IP IP 身份不稳定(CDN / 双栈 v4↔v6 / CGNAT)。开 trust_xff,或设 token_bind_prefix_v4 = 24,或退一步 token_bind_ip = 0
bad_hex / malformed 令牌对不上签名(盐变了,或不是本 IP 签发的) 确认 data/ 可写(盐必须能落盘);多台服务器 / 负载均衡请共用同一 data/,或统一指定 CC_SALT
b64_fail / too_long 值被改写,或异常长 中间有代理 / 网关改写了 cookie。本版令牌已改用 URL-safe base64(不含 + / =),可规避绝大多数改写
iter_low 计算量不足=没真正执行 JS 这是正常拦截,不是故障

✅ 令牌校验通过。若仍有个别客户端被反复挑战,请看日志里它们的 why= 值; 若 why=no_cookie 且 UA 是 Chrome,多半是「不执行 JS 的扫描器伪装成 Chrome」。

5) 防火墙日志分析

未找到日志或日志为空()。若 log_level = off 请先改为 action。

日志文件:,共 行(下面分析最近 3000 行)。

按拦截阶段统计 $v) { echo '' . ccSafeEcho($k) . ' ' . (int)$v . ' 条 '; } } ?>
按原因统计(TOP) $v) { if ($i++ >= 6) break; echo '' . ccSafeEcho($k) . ' ' . (int)$v . ' 条 '; } } ?>
被拦最多的 IP(TOP 5) $v) { if ($i++ >= 5) break; echo '' . ccSafeEcho($k) . ' ' . (int)$v . ' 次 '; } } ?>
挑战未通过的原因(why) $v) { echo '' . ccSafeEcho($k) . ' ' . (int)$v . ' 条 '; } } ?>
0): ?>

⚠️ 日志里出现过 challenge_flood count=, 即某 IP 在窗口内的挑战次数超过了硬上限 0 ? (int)$ch : '(当前为 0,不再封禁)'; ?> 并被 403。
· 新版只在超过硬上限时才封禁,超过软上限()只记 over_soft= 不封禁; 旧的 challenge_flood 记录若阈值很小(如 40),很可能就是「共享出口 IP 整片被误伤」的来源。
· 若被 403 的 IP 对应的是真人浏览器(见下表 UA),请把硬上限保持较大值,并按第 4 节的 why 解决令牌问题。

最近 12 条拦截记录

时间动作 阶段IP 原因UA(截断)

6) 现象 → 原因对照

看到「403 请求被安全防火墙拦截」 是防火墙拦的。看上面日志的阶段:iplist=黑名单命中;ua=UA 规则;challenge=挑战洪水(硬上限);rate=限频;url-injection=URL 特征。
日志里大量 CHALLENGE … served count=N why=… 有客户端被反复挑战。count 持续增长且 UA 是真人浏览器 ⇒ 看 why:no_cookie=cookie 没回来;bad_hex=盐或 IP 对不上;ip_mismatch:IP=IP 漂移。结论见第 4 节。
页面一直转、反复出现「正在验证您的浏览器」 JS 挑战没通过。看第 4 节的令牌自测(why 直接给结论),再核对第 1 节 IP 是否稳定、第 2 节盐值是否稳定、浏览器是否禁用了 JS / Cookie。
浏览器提示「该页无法显示 / 无法访问此网站」(不是 403) 请求被挂住或超时,不是拦截。多为存储后端阻塞或 data/ 不可写。本版已把文件锁改为「非阻塞 + 有界重试」,请确认已用上最新 src/Store.php。
刷新几次又可以打开 典型的两类原因:① 令牌间歇失效(IP 漂移 / 盐不稳定 / cookie 被改写);② 旧版把 共享出口 IP 因挑战量超阈值整片 403,窗口重置后又恢复。
本版已改为「软上限只记日志、硬上限才封禁」,② 基本消除;① 按第 4 节 why 处理。
急停开关 入口最顶部 define('CC_SHIELD_MODE', 'off'); 立刻整体关闭;define('CC_URL_INJECTION_GUARD', false); 只关链接注入防护;define('CC_CHALLENGE_HARD_MAX', 0); 关掉挑战封禁但保留人机验证。

提示:本检测页自身也可能被防火墙挑战/拦截。若打不开,请临时把它的文件名加进 allow_paths, 或先把 mode 改为 observe。

五、用完即删

本文件含有环境敏感信息,请确认已记录所需结论后,立即删除:

删除是物理 unlink,不会留在回收站。删除后刷新本页会 404,属正常现象。