四:php弱比较(md5)
页面上只有一个 Key 输入框和一个与 PHP Version 有关的提示。真正的突破口不在表单,而在 Web 目录里遗留的备份文件。
环境:本地靶场http://192.168.145.1:8104/。
从异常表单入手
查看源码时能发现两个细节:表单使用 POST,但隐藏字段ver没有值;提交按钮也没有显式value,浏览器会自动显示默认的“提交”或Submit文案。
图 1
图 2
继续检查常见备份后缀。目录扫描发现:
/index.php.bak
下载后即可看到 PHP 源码。
审计关键判断
源码实际使用$_GET['ver']取值,而页面表单通过 POST 提交。也就是说,照着页面正常点提交,参数根本不会进入目标分支。
判断逻辑会将传入的ver与 PHP 内置常量PHP_VERSION比较。值相等时,页面输出 key。因此接下来只需确认服务器的 PHP 版本。
图 3
图 4
图 5
在该靶场中,可以从响应头或 WhatWeb 的指纹结果看到版本为5.6.36,于是构造:
http://192.168.145.1:8104/index.php?ver=5.6.36
请求后成功进入目标分支。
为什么备份文件危险
编辑器和人工部署常留下.bak、.swp、.old、.zip等文件。Web 服务器可能不会执行它们,却会把源码原样下载给访问者。数据库口令、内部路径、参数名称和判断逻辑都可能随之暴露。
修复时不应只靠“禁止访问某一个后缀”,而应从发布流程上避免把备份文件放进站点目录,并在 Web 服务器层面设置兜底规则。
图 6
图 7
小结
代码审计时先确认三件事:参数从哪里取、使用什么类型、进入哪个分支。这道题里最容易踩的坑就是 GET/POST 不一致。页面长什么样只是表象,服务端源码才是最终依据。
五:字典泄露与越权
字典泄露、弱口令与越权:一条完整利用链
这道题不是一个孤立漏洞,而是一条连续的利用链:口令字典泄露、前端长度限制绕过、登录后线索发现,以及接口参数越权。
环境:本地靶场http://192.168.145.1:8105/。
图 1
图 2
下载泄露的口令字典
登录页默认用户名为admin,密码框带有maxlength=5。页面标题和 HTML 注释同时指向password.txt:
<!-- password.txt -->
图 3
直接访问/password.txt,可以下载一份约 200 条记录的口令字典。
maxlength只是浏览器端限制。Burp、curl 或其他 HTTP 客户端都可以发送更长的值,因此不能据此假设真实密码只有五位。
用字典验证登录
抓取一次登录请求后,在授权靶场中将密码字段设为变量并加载题目提供的字典。最终命中:
图 4
图 5
admin / Nsf0cuS
密码共有七位,正好说明前端限制没有任何安全作用。
登录后的第二层线索
图 6
图 7
登录后页面仍然声称“这里没有 flag”。此时查看响应头,Cookie 中出现了一个名为newpage的提示值。Base64 解码后得到一个新的 PHP 页面路径。
继续查看新页面源码,表单提交到?act=add,并带有隐藏字段:
<input type="hidden" value="guest" name="userlevel">
隐藏字段同样由客户端控制。结合响应头中的登录状态 Cookie,可以在本地靶场构造:
图 8
图 9
图 10
curl -i \
-H "Cookie: IsLogin=1" \
-X POST \
"http://192.168.145.1:8105/290bca70c7dae93db6644fa00b9d83b9.php?act=add" \
-d "content=hihi&userlevel=root"
图 11
图 12
服务器信任了userlevel=root,并在响应头中返回 URL 编码后的 flag。
漏洞链复盘
图 13
图 14
第一处问题是敏感字典公开可访问;第二处是服务端缺少有效的登录保护;第三处则是把权限级别交给客户端提交。任何一处被修复,都能打断整条链。
生产环境中应移除 Web 目录里的字典和备份文件,为登录设置速率限制和多因素认证,并由服务端根据 Session 中的用户身份计算权限。隐藏字段只适合携带普通表单状态,不能承担授权判断。
小结
当登录成功后没有立即出现结果,不要停在页面文案上。响应头、Cookie、源码注释、隐藏字段和前端接口都可能把下一步线索串起来。
六:php强比较
PHP 弱类型与 MD5 魔术哈希
这道题把源码直接显示在页面上,核心只有一个条件:传入值不能等于指定字符串,但两者的 MD5 在 PHP 松散比较中必须相等。
环境:本地靶场http://192.168.145.1:8106/。
先读源码
<?php
include('config.php');
if (empty($_GET['md5'])) die(show_source(__FILE__));
if ($_GET['md5'] != '240610708' && md5($_GET['md5']) == md5('240610708')) {
echo $flag;
}
?>
图 1
目标字符串的摘要是:
md5("240610708") = 0e462097431906509019562988736854
它以0e开头,后面全是数字。在受影响的 PHP 松散比较语义中,这类字符串会被按科学计数法数字处理,数值为 0。
选择另一组魔术哈希
已知字符串QNKCDZO的摘要为:
图 2
图 3
图 4
md5("QNKCDZO") = 0e830400451993494058024219903391
两个摘要字符串并不相同,但在松散比较时都会转换为数值 0,于是条件成立;同时QNKCDZO又不等于原字符串。
构造请求:
http://192.168.145.1:8106/?md5=QNKCDZO
ps:
其一,empty("0")在 PHP 中为真,所以?md5=0会重新进入显示源码的分支。
其二,题目环境可能把 flag 置空,导致命中后仍然是空响应。此时不能只凭页面是否出现文字判断成功与否,需要结合源码分支、响应差异和靶场配置验证。
修复方式
哈希值比较应使用hash_equals()或至少使用严格比较,并在计算前确认输入是字符串。更重要的是,不要把普通 MD5 当作认证凭据;涉及口令时应使用password_hash()与password_verify()。
图 5
图 6
需要说明的是,改成===会阻止本题这种0e字符串转换,但“数组绕过”等问题属于另一类输入类型错误,不是===本身失效。正确方案始终包含严格的类型校验。
小结
看到 PHP 中的md5(...) == md5(...),应立刻检查是否存在数字字符串转换。真正值得记住的不是某个固定 payload,而是“输入类型 + 比较运算符 + 函数返回值”这三个点要一起审计。