ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP入侵检测系统源码解析:规则匹配、日志采集与拦截处置实战

PHP入侵检测系统源码解析:规则匹配、日志采集与拦截处置实战 简介这份资源是一套基于PHP实现的入侵检测与防御系统IDPS源码面向Web安全初学者、PHP开发者以及希望研究防护机制的安全研究人员可用于学习如何在应用层识别并拦截SQL注入、XSS等常见攻击。压缩包共6个文件约11KB以php脚本为核心辅以rules规则文件、txt计数文件、log日志、md说明文档与license许可文件分别承担检测逻辑、攻击签名匹配、运行记录与项目说明等职责结构精简便于快速通读。目前已有348人学习下载具备一定的参考热度。读者可借此深入理解数据收集、异常检测引擎、规则库匹配与响应机制等模块的实现思路掌握在PHP环境中落地安全防护的方法并在此基础上自定义规则、扩展检测算法或将其作为教学与实战演练的案例提升整体Web安全意识与开发能力。1. 从一份 PHP 入侵检测源码说起它能替你盯住什么很多人对 PHP 安全的第一反应是装个 WAF 插件或者干脆把display_errors关掉眼不见为净。但真被扫过日志的人都知道攻击流量从来不是单一形态SQL 注入的union select、一句话木马的eval($_POST)、路径穿越的../../etc/passwd、批量爆破的wp-login.php高频请求这些特征分散在请求参数、Header、URI 和请求体里靠人肉翻 access.log 基本等于大海捞针。这份基于 PHP 的入侵检测与防御系统源码解决的就是这件事——用 PHP 自己写一套轻量的 IDS/IPS把规则匹配、日志采集、告警和拦截串成一条链路。它适合有 LNMP/LAMP 环境、想在自己项目里嵌一层检测逻辑的后端开发者也适合拿来做安全课程设计或二次开发底座。整套逻辑不依赖外部中间件纯 PHP 可跑改规则、加检测点都直接改代码可控性比黑盒 WAF 强得多。2. 拆开这套 PHP IDS目录结构、检测引擎与规则怎么落地拿到源码包第一件事不是急着部署而是先搞清楚它把「检测」这件事拆成了几层。PHP 写 IDS 有个天然优势它本身就活在请求生命周期里能在auto_prepend_file阶段就介入比反向代理层更贴近业务参数。但劣势也明显——如果检测逻辑写得太重每个请求都拖慢所以这套源码的架构取舍值得先看懂。2.1 典型目录结构与各模块职责这类 PHP IDS 源码通常按「采集 → 规则 → 匹配 → 处置」四段划分目录大致长这样不同版本命名会有差异以实际包内为准ids/ ├── config/ │ ├── config.php # 数据库、告警开关、白名单等全局配置 │ └── rules.php # 规则定义核心文件 ├── core/ │ ├── Request.php # 请求数据采集与归一化 │ ├── Detector.php # 规则匹配引擎 │ ├── Logger.php # 命中日志落库/落文件 │ └── Blocker.php # 拦截与封禁处置 ├── lib/ │ └── ip.php # IP 获取、CIDR 判断、代理头处理 ├── logs/ │ └── ids.log # 运行日志 └── index.php # 入口通常被 auto_prepend_file 引入Request.php负责把$_GET、$_POST、$_SERVER、php://input里的原始数据统一收集并做 URL 解码、大小写归一这一步决定了后面规则能不能匹配到变形攻击。Detector.php是引擎核心遍历规则逐条比对。Blocker.php决定命中后是只记录还是直接exit拦截。理解这个分层后面改规则、加检测点才不会乱。2.2 检测引擎的匹配逻辑与规则写法规则匹配是整套系统的灵魂。常见做法是把规则写成数组每条包含匹配目标、匹配方式正则/关键字/函数、危险等级和处置动作。下面是一段贴近这类源码风格的规则定义示例?php // config/rules.php return [ // 规则1SQL注入特征匹配GET/POST参数 [ id SQLI-001, target [get, post], // 检测目标请求参数 pattern /(union[\s\/\*]select|select.from|sleep\(|benchmark\()/i, level high, // 危险等级 action block, // 命中后拦截 desc 疑似SQL注入, ], // 规则2一句话木马常见函数 [ id WEBSHELL-001, target [post, cookie], pattern /(eval|assert|system|passthru|shell_exec)\s*\(/i, level critical, action block, desc 疑似webshell执行, ], // 规则3路径穿越 [ id LFI-001, target [get, post, uri], pattern /(\.\.\/|\.\.\\\\|%2e%2e%2f)/i, level high, action log, // 只记录不拦截 desc 疑似路径穿越, ], ];逻辑说明引擎读取这个数组后对每条规则按target取出对应来源的数据用preg_match跑pattern。命中后根据action决定是block拦截并记录还是log仅记录。level用于告警分级方便后续按严重程度过滤。参数说明target数组决定检测面加uri能覆盖路径型攻击加header能覆盖 User-Agent 注入pattern用i修饰符做大小写不敏感因为攻击者常混用大小写绕过action建议对高误报规则先设log观察一段时间再改block否则容易把正常业务请求拦掉。2.3 请求采集与归一化别让变形攻击溜过去攻击者最常用的绕过手段就是编码变形union select写成union%20select、UNION/**/SELECT、unionselect。如果采集阶段不做归一化再好的正则也白搭。这类源码里Request.php一般会做这几件事?php // core/Request.php 关键片段 class Request { public static function collect() { $data []; // 1. 收集GET/POST递归处理数组型参数 $data[get] self::flatten($_GET); $data[post] self::flatten($_POST); // 2. 原始请求体覆盖非表单提交如JSON $data[raw] file_get_contents(php://input); // 3. URI与Header $data[uri] urldecode($_SERVER[REQUEST_URI] ?? ); $data[header] self::getHeaders(); // 4. 统一URL解码 去注释符对抗编码绕过 foreach ($data as $k $v) { if (is_string($v)) { $data[$k] self::normalize($v); } } return $data; } private static function normalize($str) { $str urldecode($str); // 解一次URL编码 $str str_replace([/**/, /* */], , $str); // 去SQL注释绕过 $str preg_replace(/\s/, , $str); // 多空格归一 return $str; } private static function flatten($arr) { $out []; array_walk_recursive($arr, function($v) use ($out) { $out[] $v; }); return implode( , $out); } }逻辑说明flatten把嵌套数组拍平成一串避免?id[]1id[]union这种数组绕过。normalize先 URL 解码再去注释符能挡住大部分编码变形。raw单独取php://input是因为 JSON 请求体不进$_POST漏了这块等于对 API 攻击视而不见。参数说明urldecode只解一次解多次会误伤正常含%的参数去注释符的正则要覆盖/**/和/* */两种写法如果业务本身允许富文本normalize里的空格归一要谨慎可能改变正常内容。2.4 接入现有项目auto_prepend_file 与手动引入部署方式有两种。第一种是全局接入在php.ini里配置; php.ini auto_prepend_file /www/ids/index.php这样每个 PHP 请求执行前都会先跑 IDS 入口适合整站防护。第二种是手动引入在项目入口文件顶部加?php require_once /www/ids/index.php; // 引入IDS内部完成检测逻辑说明auto_prepend_file的优势是无需改业务代码缺点是所有请求包括静态资源走的 PHP都会过一遍检测性能敏感站点要评估。手动引入更可控能只对特定入口生效。参数说明auto_prepend_file路径必须是绝对路径如果 IDS 入口里做了exit拦截要确保它不会影响正常的 404、跳转逻辑。建议先在测试环境用log模式跑一周看误报率再决定是否开block。3. 把检测跑起来规则调优、日志落库与拦截处置架构看懂只是第一步真正决定这套系统好不好用的是规则准不准、日志能不能查、拦截会不会误伤。这一章把配置、调优和处置三件事拆开讲都是能直接抄的。3.1 全局配置与数据库表设计先看config/config.php里几个关键开关?php // config/config.php return [ enabled true, // 总开关调试时可关 mode log, // log只记录, block拦截 db [ host 127.0.0.1, name ids, user ids_user, pass your_password, ], whitelist_ip [127.0.0.1, 192.168.1.0/24], // 白名单CIDR支持 alert_email , // 告警邮箱留空则不发 log_rotate 7, // 日志保留天数 ];逻辑说明mode是全局处置策略建议初期设log观察规则命中情况后再切block。whitelist_ip支持 CIDR内网段和本机直接放行避免自测时把自己拦了。参数说明db配置对应下面这张命中日志表log_rotate控制清理周期日志表增长很快不清理几个月就上百万行。对应的建表语句CREATE TABLE ids_hits ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, rule_id VARCHAR(32) NOT NULL COMMENT 规则编号, level VARCHAR(16) NOT NULL COMMENT 危险等级, ip VARCHAR(45) NOT NULL COMMENT 来源IP, uri VARCHAR(512) NOT NULL COMMENT 请求URI, payload TEXT COMMENT 命中内容片段, action VARCHAR(16) NOT NULL COMMENT 处置动作, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_ip (ip), KEY idx_created (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明payload存命中片段而非完整请求避免日志表被大请求体撑爆。idx_ip和idx_created两个索引支撑「按 IP 查攻击历史」和「按时间查近期告警」两个高频查询。参数说明ip字段留 45 位是为了兼容 IPv6payload用 TEXT 但写入前建议截断到 500 字符防止单条记录过大。3.2 规则调优从误报里找平衡规则不是越多越好误报才是 IDS 最大的敌人。我一般按这个流程调第一步全量开log模式跑三天把ids_hits按rule_id分组统计SELECT rule_id, COUNT(*) AS cnt FROM ids_hits WHERE created_at DATE_SUB(NOW(), INTERVAL 3 DAY) GROUP BY rule_id ORDER BY cnt DESC;第二步对命中量异常高的规则逐条看payload判断是真攻击还是业务误伤。比如select.from这条如果业务里有搜索功能传了select from这种词就会误报。第三步对误报规则做收窄。常见手法是加白名单参数、提高正则精度、或者把action降级为log// 收窄前太宽容易误伤 pattern /(select.from)/i, // 收窄后要求select和from之间是合法SQL结构且排除常见业务词 pattern /select\s[\w\*,\s]\sfrom\s\w/i,逻辑说明宽泛正则命中率高但误报多收窄后精度提升代价是可能漏掉部分变形攻击。这是 IDS 永恒的权衡没有银弹。参数说明收窄正则时一定要拿真实攻击样本回归测试别改完发现连union select都匹配不上了。建议维护一个攻击样本集每次改规则都跑一遍。3.3 拦截处置与封禁策略命中高危规则后Blocker.php的处置逻辑大致如下?php // core/Blocker.php class Blocker { public static function handle($hit, $config) { // 1. 记录命中 Logger::save($hit); // 2. 根据全局模式和规则动作决定处置 if ($config[mode] block $hit[action] block) { // 3. 累计同一IP的命中次数达到阈值则临时封禁 $count Logger::countByIp($hit[ip], 300); // 5分钟内命中次数 if ($count 5) { self::banIp($hit[ip], 3600); // 封禁1小时 } http_response_code(403); exit(Request blocked by IDS); } } private static function banIp($ip, $seconds) { // 写入封禁表或缓存后续请求先查封禁 $key ids_ban_ . md5($ip); file_put_contents(/tmp/ . $key, time() $seconds); } }逻辑说明不是一命中就封 IP而是先累计次数5 分钟内命中 5 次才封避免正常用户偶尔触发一次就被误封。封禁状态写文件生产环境建议换 Redis后续请求在入口先查封禁表。参数说明300是统计窗口秒数5是阈值3600是封禁时长这三个值要按站点流量调。流量大的站点阈值可以调高避免攻击者用少量请求就把正常 IP 段带封。3.4 日志分析与告警联动光记录不分析等于没记。建议加一个简单的告警脚本定时扫高频攻击 IP#!/bin/bash # 每10分钟跑一次找出5分钟内命中超过20次的IP mysql -uids_user -pyour_password ids -N -e SELECT ip, COUNT(*) c FROM ids_hits WHERE created_at DATE_SUB(NOW(), INTERVAL 5 MINUTE) GROUP BY ip HAVING c 20; | while read ip cnt; do echo [$(date)] 高频攻击IP: $ip 命中 $cnt 次 /var/log/ids_alert.log done逻辑说明用 SQL 聚合代替逐条读日志效率高。命中阈值以上的 IP 写入告警日志后续可对接邮件或消息通知。参数说明INTERVAL 5 MINUTE和c 20按实际流量调生产环境别把密码写死在脚本里用配置文件或环境变量。4. 避坑与排查这套 PHP IDS 最容易翻车的五个地方规则写得再漂亮部署环节翻车一样白搭。下面五条都是我在实际环境里踩过的按「现象 → 原因 → 解决」记下来。现象一部署后所有请求返回 403包括正常访问。原因mode设成了block而某条宽泛规则比如匹配select命中了正常业务参数。解决先把mode改回log查ids_hits找出误报规则收窄后再切block。别一上来就开拦截这是血泪经验。现象二IDS 完全不生效攻击请求照常通过。原因auto_prepend_file配了但没生效常见于 PHP-FPM 未重启或者配在了 CLI 的 php.ini 而非 FPM 的。解决用phpinfo()确认auto_prepend_file的实际值改完php.ini必须重启 PHP-FPM。手动引入的话检查require_once路径是否正确、入口文件是否真的被执行。现象三日志表暴涨几天就几个 G。原因payload存了完整请求体或者高频规则把正常爬虫流量全记了。解决写入前截断payload到 500 字符对已知爬虫 IP 加白名单配置定时清理任务删除超过log_rotate天的记录。现象四攻击者用大小写和编码轻松绕过。原因规则正则没加i修饰符或者采集阶段没做 URL 解码。解决所有规则正则统一加iRequest::normalize里确保urldecode执行对%252e这种双重编码考虑解两次但只对路径类参数。现象五封禁逻辑把公司出口 IP 封了整个办公室上不了站。原因封禁阈值太低或者白名单没配全。解决把办公网、监控、压测 IP 段全部加进whitelist_ip封禁前先查白名单阈值按流量调高。这个坑一旦踩了恢复起来很尴尬。5. 进阶玩法把规则热更新和误报回归做成习惯基础跑通之后真正拉开差距的是两件事规则能不能不改代码就更新以及每次改规则后能不能快速验证没引入新误报。这两点做好了这套 PHP IDS 才算从「能跑」变成「敢用」。先说规则热更新。把规则从rules.php硬编码改成从数据库或 JSON 文件读取配合一个简单的管理接口就能做到不改代码加规则?php // core/Detector.php 改造规则从JSON加载 class Detector { private static $rules null; private static function loadRules() { if (self::$rules null) { $file /www/ids/config/rules.json; // 文件修改时间变化才重新加载避免每次请求都读盘 $mtime filemtime($file); $cache /tmp/ids_rules_cache.php; if (is_file($cache) filemtime($cache) $mtime) { self::$rules include $cache; } else { $json file_get_contents($file); self::$rules json_decode($json, true); // 写PHP缓存文件include比json_decode快 file_put_contents($cache, ?php return . var_export(self::$rules, true) . ;); } } return self::$rules; } }逻辑说明用文件修改时间做缓存判断规则没变就不重复解析 JSON。缓存成 PHP 数组文件是因为include一个纯数组文件比json_decode快适合高频请求场景。参数说明rules.json的格式和原来的rules.php数组结构一致只是换成 JSON缓存文件放/tmp要注意多机部署时各机器缓存独立规则更新后需要触发各机器刷新或者干脆用共享存储。再说误报回归。维护一个samples/目录里面放两类文件attack_*.txt存真实攻击样本normal_*.txt存正常业务请求样本。每次改规则后跑一遍验证脚本#!/bin/bash # 规则回归测试攻击样本必须命中正常样本必须不命中 PASS0; FAIL0 for f in samples/attack_*.txt; do if php test_rule.php $f | grep -q HIT; then PASS$((PASS1)) else echo 漏报: $f; FAIL$((FAIL1)) fi done for f in samples/normal_*.txt; do if php test_rule.php $f | grep -q HIT; then echo 误报: $f; FAIL$((FAIL1)) else PASS$((PASS1)) fi done echo 通过 $PASS, 失败 $FAIL逻辑说明攻击样本要求命中检测漏报正常样本要求不命中检测误报两个方向都验证才算合格。test_rule.php是个小脚本把样本内容喂给Detector看是否命中。参数说明样本集要持续积累每次线上发现漏报或误报就把对应请求脱敏后加进样本集。样本越多规则改动越有底气。最后说一个我自己的习惯任何规则从log升级到block之前我都会强制走一遍「三天观察 样本回归 白名单核对」这三步缺一步都不切。这套流程帮我挡掉过好几次差点把正常业务拦掉的改动。IDS 这东西宁可漏一点也别误伤一片因为误伤的代价往往是用户直接流失。希望这套源码和上面的调优思路能帮你在自己的项目里把入侵检测这层真正落地下来。本文还有配套的精品资源点击获取
返回列表