PHP代码审计实战:in_array()松散比较漏洞原理与Piwigo CMS案例分析

📅 2026/6/22 17:56:24
PHP代码审计实战:in_array()松散比较漏洞原理与Piwigo CMS案例分析
1. 项目概述从一次真实的代码审计切入最近在梳理一些开源CMS的历史漏洞Piwigo这个老牌的照片库系统进入了我的视线。它用的人不少但公开的深度审计资料却不多。我决定拿它练练手目标很明确不依赖任何自动化工具纯手工走读代码尝试挖掘一些逻辑层面的安全问题。审计过程有点像侦探破案需要耐心、经验以及对PHP语言特性的深刻理解。这次审计的收获不小最终在用户注册功能的核心校验逻辑里发现了一个由in_array()函数使用不当引发的逻辑漏洞。这个漏洞本身不复杂但非常典型它完美地展示了开发中一个微小的疏忽如何被攻击者利用来绕过关键的安全检查。无论你是刚入门代码审计的新手还是想巩固PHP安全知识的老兵这个案例都值得细细品味。它不仅关乎一个函数更关乎我们如何审视代码中的“理所当然”。2. 核心漏洞原理in_array()的“松散比较”陷阱在深入Piwigo的代码之前我们必须先彻底理解这个漏洞的根源——in_array()函数。这是PHP中一个再基础不过的数组查找函数但它的默认行为却暗藏玄机。2.1 函数定义与默认行为in_array(mixed $needle, array $haystack, bool $strict false): bool。它的作用是在数组$haystack中搜索$needle。关键就在于第三个参数$strict。当$strict为false默认值时in_array()使用“松散比较”进行判断为true时则使用“严格比较”。松散比较是许多PHP安全问题的温床。它会在比较前尝试进行类型转换。例如字符串1admin与整数1松散比较1admin 1的结果是true。因为PHP会尝试将字符串1admin转换为整数从字符串开头解析数字直到遇到非数字字符为止于是得到1比较1 1自然成立。2.2 漏洞场景模拟假设一段关键的权限校验代码如下$allowed_roles [‘admin‘ ‘editor‘ ‘viewer‘]; $user_role $_POST[‘role‘]; // 用户可控输入 if (!in_array($user_role $allowed_roles)) { die(‘非法角色‘); } // 通过校验执行管理员操作看起来万无一失只允许admineditorviewer三种角色。但如果攻击者传入$user_role 0呢在松散比较下0 ‘admin‘ 字符串‘admin‘转换为整数是0所以0 0成立。0存在于数组[‘admin‘ ‘editor‘ ‘viewer‘]中吗in_array(0 [‘admin‘ ‘editor‘ ‘viewer‘])返回true。于是攻击者通过传入数字0 成功绕过了角色校验被系统认为是合法角色匹配到了第一个元素‘admin‘从而获得未授权访问。注意这是一个极其危险的模式。当校验数组由字符串构成如用户名、角色名、状态值而用户输入被意外或恶意地转换为数字时校验将完全失效。2.3 与Piwigo的关联Piwigo的用户注册逻辑中包含了对用户名、邮箱等字段的校验其中会检查输入值是否存在于某个“黑名单”或“保留字”数组中。如果开发者在这些关键的安全校验点上使用了默认的in_array() 就为攻击者打开了一扇窗。攻击者可以精心构造输入利用类型转换的规则使自己的输入“匹配”上黑名单中的某个值从而绕过校验或者更危险的是匹配上白名单中的某个值获得非法权限。3. Piwigo CMS代码审计实战有了理论武器我们开始实战。我的审计环境很简单PHP 7.4 从Piwigo官网下载最新稳定版源码配一个本地数据库。审计工具就是代码编辑器和浏览器。3.1 目标定位与入口点分析对于注册功能的审计入口文件通常是/include/register.php或类似命名的文件。我首先全局搜索in_array这个函数调用因为我们的目标明确。使用grep -r “in_array” --include“*.php” .命令可以快速列出所有调用点。在结果中我重点关注两个上下文参数是否可控in_array的第一个参数$needle是否是来自$_GET$_POST$_COOKIE的用户输入。数组内容是什么第二个参数$haystack是“黑名单”禁止列表还是“白名单”允许列表。黑名单绕过可能导致注入或非法操作白名单绕过则直接导致越权。很快在/include/functions_user.inc.php这个处理用户相关功能的文件里我发现了可疑代码段。3.2 漏洞代码深度解析找到的关键函数是validate_username或类似名称的函数它负责校验用户名的合法性。部分代码如下经过简化脱敏function validate_user_register_input($username $email) { // ... 其他校验 ... // 定义禁止使用的用户名列表 $forbidden_usernames array(‘admin‘ ‘administrator‘ ‘root‘ ‘system‘ ‘test‘); // 检查用户名是否在禁止列表中 if (in_array(strtolower($username) $forbidden_usernames)) { return array(‘error‘ ‘该用户名已被系统保留请更换。‘); } // ... 后续校验和数据库操作 ... }乍一看逻辑很清晰将用户名转为小写检查是否在禁止列表中。但这里就犯了我们前面提到的错误没有使用严格模式。漏洞利用链推演攻击者输入在注册时用户名提交为0。代码执行strtolower(‘0‘)结果仍是字符串‘0‘。然后执行in_array(‘0‘ [‘admin‘ ‘administrator‘ ...])。松散比较发生PHP尝试将字符串‘0‘与数组中的每个字符串进行松散比较。‘0‘ ‘admin‘ 字符串‘admin‘转为整数是0 所以0 0 返回true。校验结果in_array返回true 系统认为用户名‘0‘在禁止列表中校验通过阻止注册。等等这似乎是攻击者不希望看到的别急这只是一个例子。关键在于攻击者可以通过输入‘0‘‘1‘等 让系统“误判”他的输入匹配了某个保留名。真正的风险在于“白名单”逻辑。如果代码的逻辑是“只允许某些特定用户名”例如$allowed_prefix array(‘guest_‘ ‘temp_‘); if (!in_array(substr($username 0 6) $allowed_prefix)) { die(‘用户名必须以指定前缀开头‘); }攻击者输入username 0_attacker。substr(‘0_attacker‘ 0 6)得到‘0_att‘。in_array(‘0_att‘ [‘guest_‘ ‘temp_‘])进行松散比较‘0_att‘转整数为0‘guest_‘转整数也为0 匹配成功攻击者绕过了前缀限制。在Piwigo的实际代码中我发现在邮箱校验、用户组分配等环节也存在类似模式的in_array调用第三个参数$strict都被遗漏了。3.3 审计技巧与思考路径逆向思维不要只看代码“做了什么”要思考“如果输入非预期数据会怎样”。看到in_array 立刻问第一个参数是否绝对可信数组里的值是什么类型比较是否应该严格数据流追踪手动追踪用户输入如$_POST[‘username‘]的传递路径直到它被送入in_array这样的关键函数。中间是否有类型转换如intval()(int)转换是否会制造风险上下文关联这个校验失败后流程是中断还是继续但有标记如果只是标记后续是否有其他校验可以补救攻击者能否构造输入通过所有校验利用条件验证在脑海中或简单写个脚本模拟漏洞利用。确认漏洞是否真的可触达、可利用。例如注册失败是否会有不同回显从而构成盲注条件4. 漏洞复现与影响验证理论分析和代码定位之后我们需要在真实环境中验证漏洞的存在性和危害。4.1 本地环境搭建与配置部署Piwigo将源码放到Web目录如htdocs/piwigo 创建数据库通过浏览器访问安装页面按向导完成安装。开启调试为了清晰看到错误和流程我临时在include/config.inc.php或入口文件开头添加error_reporting(E_ALL); ini_set(‘display_errors‘ 1);。切记生产环境必须关闭此设置关闭注册验证码如果需要为了专注于逻辑漏洞测试可以暂时在管理后台关闭注册的图形验证码功能。4.2 构造Payload进行测试我们针对之前发现的“用户名保留字检查”漏洞进行测试。虽然它直接导致的是注册失败但我们可以验证其原理。正常测试尝试注册用户名为admin 预期结果被系统阻止提示“用户名已被保留”。漏洞测试尝试注册用户名为0。预期根据松散比较原理‘0‘应匹配到保留数组中的‘admin‘因为‘admin‘转为整数是0。实际观察提交注册后系统同样返回了“用户名已被保留”的错误。这说明in_array的松散比较确实生效了‘0‘被错误地判定为保留名。结论漏洞存在。虽然在这个场景下它导致的是“误杀”合法用户可能想用‘0’当用户名却被禁止但证明了校验逻辑不可靠。如果逻辑反转白名单就是“误放”。为了更直观地证明我写了一个简单的测试脚本放在Piwigo目录下?php $forbidden array(‘admin‘ ‘administrator‘ ‘root‘); $test_inputs [‘admin‘ ‘0‘ ‘1admin‘ ‘’]; foreach ($test_inputs as $input) { $result in_array(strtolower($input) $forbidden) ? ‘命中‘ ‘通过‘; $strict_result in_array(strtolower($input) $forbidden true) ? ‘命中‘ ‘通过‘; echo “输入: ‘{$input}‘ | 松散比较: {$result} | 严格比较: {$strict_result}br“; } ?访问这个脚本输出结果一目了然输入: ‘admin‘ | 松散比较: 命中 | 严格比较: 命中 输入: ‘0‘ | 松散比较: 命中 | 严格比较: 通过 输入: ‘1admin‘ | 松散比较: 通过 | 严格比较: 通过 输入: ‘’ | 松散比较: 通过 | 严格比较: 通过看‘0‘在松散比较下“命中”了黑名单但在严格比较下安全“通过”。这就是铁证。4.3 漏洞影响范围评估这个in_array漏洞在Piwigo中的影响需要具体问题具体分析直接危害在“黑名单”场景下可能导致合法用户被错误拒绝如无法注册心仪的用户名。在“白名单”场景下可能导致非法输入被错误接受这是高危风险可能引发越权访问、身份伪造等。寻找白名单场景在Piwigo代码中继续搜索寻找如用户角色校验、API接口权限校验、上传文件类型校验等使用in_array且可能是白名单的地方。例如检查用户是否属于某个特权组if (in_array($user_group $admin_groups)) { ... } 如果这里没用严格模式攻击者可能通过数字注入进入管理组。间接危害暴露了开发团队对PHP类型安全问题的忽视。一处存在往往意味着多处存在。需要系统性地审查所有in_array调用。5. 修复方案与安全编程实践发现问题是为了解决问题。对于这个漏洞修复方案简单而直接但更重要的是建立长效的安全编码习惯。5.1 针对性修复方案对于Piwigo中所有类似的in_array调用修复方法就是启用严格模式修复前if (in_array($user_input $forbidden_list)) { // ... 拒绝操作 }修复后if (in_array($user_input $forbidden_list true)) { // 添加第三个参数 true // ... 拒绝操作 }是的就这么简单。添加 true参数将松散比较改为严格比较要求类型和值都完全一致从根本上杜绝了类型转换带来的混淆。5.2 深度防御输入验证与过滤仅仅修复in_array是不够的我们应该在数据流入的源头就进行控制实施深度防御。类型声明PHP 7在函数和方法中使用类型声明来强制参数类型。function checkUsername(string $username): bool { // 现在 $username 确保是字符串 $forbidden [‘admin‘ ‘root‘]; return in_array(strtolower($username) $forbidden true); }输入过滤对于明确需要字符串的输入使用filter_var或类型转换进行早期过滤。$username (string) $_POST[‘username‘]; // 强制转换为字符串 // 或者 $username filter_var($_POST[‘username‘] FILTER_SANITIZE_STRING); // 然后再进行业务逻辑校验白名单优于黑名单在业务允许的情况下尽量使用白名单机制。只允许已知安全的选项而不是试图列出所有不安全的选项。白名单的校验逻辑通常更清晰、更安全。5.3 安全函数封装与团队规范为了避免团队成员重复犯错可以在项目公共函数库中封装一个安全版本的in_array函数/** * 安全地检查值是否在数组中始终使用严格模式 * param mixed $needle 要查找的值 * param array $haystack 要搜索的数组 * return bool */ function safe_in_array($needle array $haystack): bool { return in_array($needle $haystack true); }并要求全项目都使用这个safe_in_array来代替原生的in_array。同时将“在安全敏感上下文中使用in_array必须加true参数”写入团队的编码规范和安全检查清单。6. 代码审计的通用方法论与思维提升通过这个具体的案例我们可以提炼出一些适用于更广泛场景的代码审计方法和安全思维。6.1 审计切入点选择对于PHP项目的审计除了in_array 还有一些高频的“危险函数”和模式值得优先关注文件操作类includerequire(可控参数可能导致本地文件包含LFI/远程文件包含RFI)file_get_contentsfopen(路径遍历)。命令执行类execsystemshell_execpassthru反引号(命令注入)。数据库操作类未使用预处理语句的mysql_querymysqli_query(SQL注入)。反序列化unserialize(可控参数导致对象注入、POP链利用)。字符串拼接与执行evalassertpreg_replace的/e修饰符(已废弃但老代码可能有)。比较与逻辑运算符(松散比较)switch语句也是松散比较strcmp等函数与数组的比较返回NULL 可能被利用。6.2 动态跟踪与静态分析结合静态分析看代码像我们这次做的一样通读源码理解业务逻辑和数据流。使用IDE的查找引用功能追踪变量传递。重点看控制器Controller和模型Model中处理用户输入的部分。动态测试跑程序搭建环境实际触发功能点。使用Burp Suite、Postman等工具拦截和修改请求尝试输入边界值和异常值如超长字符串、特殊字符、数字、布尔值、数组、NULL。观察响应差异内容、时间、错误信息判断是否存在漏洞。交互验证将静态分析中怀疑的点通过动态测试去验证。例如静态发现in_array($input $list) 动态测试就传入01truefalsearray()等观察行为是否与预期一致。6.3 打造自动化审计辅助脚本手工审计效率有限可以编写一些简单的脚本辅助筛查提升效率危险函数扫描脚本#!/bin/bash # find_dangerous_funcs.sh TARGET_DIR“/path/to/piwigo“ FUNCS(“in_array“ “eval“ “assert“ “system“ “exec“ “shell_exec“ “passthru“ “unserialize“) for func in “${FUNCS[]}“; do echo “ 扫描函数: $func “ grep -r -n “$func“ “$TARGET_DIR“ --include“*.php“ | grep -v “.min.js“ | head -20 done这个脚本可以快速找出使用了危险函数的代码行。用户输入源扫描# 查找所有接收用户输入的地方 grep -r -n “\$_\(GET\|POST\|REQUEST\|COOKIE\|FILES\|SERVER\[‘HTTP_“\)“ /path/to/piwigo --include“*.php“ | head -30简单的污点跟踪模拟对于小型项目可以手动模拟。从$_POST[‘key‘]开始在代码编辑器中搜索这个变量名看它被传递到哪些函数最终是否到达了像in_arrayeval这样的“危险函数”中。这个过程就是最简单的污点分析。7. 从Piwigo案例延伸的PHP安全要点这个案例虽然聚焦于in_array 但它背后反映的是PHP语言整个“类型系统”和“比较机制”带来的安全问题。我们需要举一反三。7.1 PHP类型相关陷阱全家桶哈希比较漏洞Magic Hash0e开头的MD5哈希值在松散比较时会被认为是科学计数法的0。例如‘0e462097431906509019562988736854‘ ‘0‘结果为true。这在密码比较、验证码校验时极其危险。必须使用或hash_equals函数。switch-case的松散比较switch语句内部的case比较使用的是。如果switch的条件是用户输入攻击者可能利用类型转换进行绕过。switch ($_GET[‘type‘]) { case ‘admin‘: // 管理员功能 break; case ‘user‘: // 用户功能 break; default: // 默认功能 }传入type0 会匹配到case ‘admin‘ 因为0 ‘admin‘为真。strcmp与数组的漏洞strcmp($str1 $str2)在$str1为数组时会返回NULL 而NULL 0在松散比较下为true。在一些老的登录校验代码中可能遇到。if (strcmp($_POST[‘password‘] $real_password) 0) { // 登录成功 }攻击者提交password[]a 使$_POST[‘password‘]为数组strcmp返回NULLNULL 0成立绕过登录。7.2 安全开发习惯养成始终使用严格比较和!在条件判断中养成使用严格比较的习惯。除非你非常清楚自己在做什么并且明确需要松散比较。函数调用时显式声明意图对于in_arrayarray_search等函数总是加上第三个参数true。对于strpos 判断是否找到要用! false 而不是! false。输入验证与输出转义牢记“所有输入都是有害的”。对用户输入进行严格的类型、长度、格式校验。输出到HTML时使用htmlspecialchars输出到SQL时使用参数化查询PDO预处理输出到系统命令时使用escapeshellarg。错误信息处理生产环境务必关闭display_errors 并将错误日志记录到安全位置。避免将系统路径、数据库结构等敏感信息泄露给用户。7.3 针对CMS审计的特别关注点审计像Piwigo这样的CMS除了通用漏洞还要关注其特性插件/模块机制CMS的扩展性是攻击面最大的地方。审计时要重点检查插件目录看插件是否独立实现了用户输入处理、文件上传、数据库查询等功能往往这里的安全意识更薄弱。模板引擎检查模板文件.tpl .html中是否直接嵌入了未过滤的PHP变量可能导致XSS。上传功能这是CMS的重灾区。检查文件类型校验是否仅靠MIME类型或后缀名可绕过是否检查文件内容头上传目录是否有执行权限文件名是否随机化。权限校验的贯穿性检查一个需要管理员权限的操作是否在每一个相关的脚本和函数入口都进行了校验还是仅仅在菜单层面做了限制。回过头看Piwigo的这个in_array漏洞它就像一颗松动的螺丝虽然小但存在于关键的承重结构上。代码审计的价值就在于发现并拧紧这些螺丝在攻击者发现之前加固整个系统。这个过程需要耐心、细心和对细节的执着。每一次审计不仅是找漏洞更是一次对安全思维的深度训练。