做 DVWA 靶场的时候,很多人会在 Weak Session IDs(弱会话标识)这一关卡上卡很久。现象非常统一:安全级别切到 High,打开页面看到 Cookie Value 显示了一串数字,然后你刷新也好、点页面上的 Generate 也好,这串数字纹丝不动,抓包工具里也搜不到新的 Set-Cookie。更诡异的是,之前切到 Medium 的时候明明每次刷新都有新值,怎么一到 High 就哑火了。于是不少人开始怀疑是 Burp 配置问题、浏览器缓存问题,甚至想重装 DVWA。这篇就把"High 的 dvwaSession 为什么刷新不出来"从头到尾拆一遍,告诉你根因在哪、怎么复现、以及这一关真正要你掌握的东西。
1. 先把关卡目标说清楚:这关不是靠刷新练出来的
1.1 认清对象:dvwaSession 不是 PHP 的 Session
新手最容易把 Weak Session IDs 和菜单里的 Brute Force 弄混。Brute Force 那关是爆破密码,Weak Session IDs 这关跟密码没关系,它的核心对象就是那个叫 dvwaSession 的 Cookie。
这里有必要先把概念捋一下:DVWA 登录后,浏览器里一般会有两个 Cookie——一个是 PHP 自己维持会话用的 PHPSESSID,另一个才是应用逻辑里手动创建的 dvwaSession。PHPSESSID 是 PHP 框架自动生成和维护的,而 dvwaSession 完全是源码里用setcookie()函数塞进去的业务 Cookie。它走不走 PHP 的 session 机制,它安不安全、可不可预测、能不能被伪造,完全取决于 DVWA 源码里那几行代码怎么写。
所以做这一关前,我强烈建议先打开 DVWA 目录下的vulnerabilities/weak_session_ids/source/,把low.php、medium.php、high.php三个文件通读一遍。代码短得可怜,但信息量极大。
1.2 三个级别的生成逻辑差异
下面是 DVWA 1.9 / 2.x 常见版本的源码逻辑归纳(不同小版本可能有细节差异,核心分支结构基本一致):
Low 级别的逻辑是:如果$_COOKIE["dvwaSession"]已存在,就只把当前值显示出来;如果不存在,就用当前时间戳time()生成一个 Cookie。这意味着 Low 生成的值就是时间戳,肉眼可读,直接就能还原出生成时间。
Medium 级别稍微绕了一点:Cookie 已存在时,它取上一条 Cookie 值的md5()作为新值,重新写入;不存在时,才用时间戳初始化。所以你切到 Medium 会发现每次刷新都有新的 Set-Cookie,因为每个新值都是对旧值做一次 md5 运算,形成一条哈希链。
High 级别则回到和 Low 一样的"存在就显示、不存在才生成"的模式,只是生成函数换成了 PHP 的伪随机数mt_rand()。部分 DVWA 版本用rand(),本质是一样的。
三个级别的差异可以看下表:
| 安全级别 | 首次生成方式 | 后续刷新 | 弱点 |
|---|---|---|---|
| Low | 时间戳 time() | 只显示,不重新生成 | 值直接可读,等于知道时间 |
| Medium | 时间戳 time() | 每次重算 md5(旧值) 并写入 | 哈希链,知道起点就能推 |
| High | mt_rand() / rand() | 只显示,不重新生成 | 伪随机种子可预测 |
1.3 High 级别真正想考的东西
很多人以为切到 High 之后,页面上的 Cookie Value 应该像 Medium 那样每次刷新都变,这才叫"难度高"。其实正好相反。High 级别的考点是:这个看起来随机的大整数,比如1081417437,它真的随机吗?
mt_rand()是 PHP 的梅森旋转伪随机数生成器,它需要一个种子,默认配置下种子往往跟时间相关。只要你能连续拿到多个 Cookie 值,再结合大致的生成时间窗口,是可以推算种子的。知道种子,就能预测下一个值,等于可以伪造别人的会话令牌。这就是"弱会话 ID"的含义——不是没做随机,而是随机得不够安全。
所以当你在 High 卡在"刷新不出来"这个问题上时,先别急着找工具,换个角度想:DVWA 是故意让 Cookie 只在不存在时才生成的。它想让你亲眼看到,"会话令牌生成后不更换"和"会话令牌由弱随机源产生"这两个问题叠加起来是什么效果。
2. 为什么刷新不出新 Cookie:根因在源码的 isset 判断
2.1 结论先给出来
刷新不出新 Cookie,十有八九不是 Burp 的问题,而是这一行判断:
if (isset($_COOKIE["dvwaSession"])) { // Cookie 已存在,直接显示当前值 } else { // Cookie 不存在,才生成并调用 setcookie() }只要请求头里带着 dvwaSession,服务端就认定"你已经有会话标识了",直接走显示分支,既不生成新值,也不调用setcookie()。因此响应里不会有 Set-Cookie,页面上的 Cookie Value 自然也不变。
这个设计对 Low 和 High 是同一个套路,Medium 反而每次刷新都有新值。所以你从 Medium 切到 High 会觉得"怎么一下子没反应了",这正是最容易产生"是不是我环境坏了"错觉的原因。
2.2 浏览器会自动带上旧 Cookie
第二个容易忽略的细节是 Cookie 的自动携带机制。Cookie 一旦由服务端种下,浏览器之后对同域名的每次请求都会自动带上它。你手动刷新也好,在 Burp 里重放也好,只要请求头里带着 dvwaSession,服务端的isset分支就成立,永远不会再生成。
也就是说,"刷新"这个动作在服务端看来不是"请给我一个新令牌",而是"我拿着旧令牌来了"。
2.3 Burp 的 Cookie Jar 会在中间添乱
如果你开着 Burp 测试,还会遇到第二层坑:Burp 自带 Cookie Jar(Cookie 仓库)。默认配置下,Burp 会记录响应里的 Set-Cookie,并在之后的请求里自动把对应 Cookie 加进请求头。这对测试是一把双刃剑——方便是方便,但会掩盖很多现象。
具体到 DVWA 这个场景:你第一次访问页面时,Burp 记录了Set-Cookie: dvwaSession=xxx,之后你再刷新,Burp 自动替你把这个 Cookie 补进请求头。服务端一看 Cookie 存在,就不再生成。于是 HTTP History 里只有第一条响应有 Set-Cookie,后面的请求统统没有。
所以"Burp 里刷新不出来"本质上还是根因一在起作用,Burp 只是忠实地把"服务端没有生成新值"这件事展示给了你。我见过不少同学以为把 Burp 的自动 Cookie 功能关掉就能解决,实际上关不关,服务端都不会生成,真正的开关在"请求里是否携带旧 Cookie"。
2.4 缓存和 304 是更小概率的干扰
还有一个小概率原因:浏览器缓存。DVWA 这类 PHP 页面默认不太容易被缓存,但如果你的浏览器插件、Burp 的拦截规则或者本地反向代理配置介入,刷新时可能命中 304 Not Modified,直接用本地缓存渲染页面,请求可能都没真正发出去。排障时第一步永远是打开开发者工具的 Network 面板,确认"这一下到底有没有发出请求、有没有拿到 200 响应",再谈其他的。
3. 三组对照实验:把问题链路彻底看穿
3.1 实验一:纯浏览器 + 开发者工具
打开 DVWA 的 Weak Session IDs 页面,按 F12 进入开发者工具,在 Application 面板的 Cookies 里找到dvwaSession,记下当前值,然后按 F5 刷新。
正常情况下你会看到:请求发出了,响应 200,但 Set-Cookie 一栏为空,Cookie 的 dvwaSession 值不变。这证明服务端走的是isset成立的分支,压根没有重新生成。
接下来做一个对照组:在 Application 面板里选中站点域名,把 Cookie 全部删除,或者干脆开一个无痕窗口重新登录。登录后把安全级别切回 High,再访问 Weak Session IDs,这时的 Network 面板里,第一趟请求的响应头就出现了Set-Cookie: dvwaSession=xxx。
这里有一个必须提醒的坑:清掉 Cookie 等于把 DVWA 的登录态也清掉了,重新登录后安全级别会回到默认值。别以为刚才设了 High 就一直有效,你要重新到 DVWA Security 页把它切成 High,否则你会发现生成的 Cookie 变成了时间戳。
3.2 实验二:开着 Burp 的 HTTP History 观察
用 Burp 的标准做题姿势:浏览器走 Burp 拦截,先清干净浏览器 Cookie,然后访问 Weak Session IDs 页面。
HTTP History 里第一条请求的响应能看到Set-Cookie: dvwaSession=xxx。之后你再怎么刷新,History 里新增的请求响应都没有 Set-Cookie 了。
此时去 Burp 的 Project options → Sessions → Cookie Jar 里看,dvwaSession 已经被自动记录进去。这正是自动 Cookie 管理在替你做"带旧 Cookie 重放"这件事。
3.3 实验三:Repeater 手动剥离 Cookie
把 Weak Session IDs 首页的请求 Send to Repeater。第一次直接 Send,响应没有 Set-Cookie,因为请求头里的 Cookie 带着 dvwaSession。
第二次,手动把请求头里的Cookie: dvwaSession=xxx删掉,注意保留登录需要的 PHPSESSID(以及 DVWA 安全级别相关的 dvwaSecurity 等),再 Send。这时响应头里出现了 Set-Cookie,响应正文里的 Cookie Value 也变成了新值。连续删连续发,每次都能拿到一个新值——这才是收集 High 样本的正确姿势。
三组实验的结论对比如下:
| 方式 | 请求是否携带旧 dvwaSession | 能否看到 Set-Cookie | 结论 |
|---|---|---|---|
| 浏览器直接刷新 | 带 | 否 | 服务端不生成 |
| Burp History 刷新 | 带(Cookie Jar 自动补) | 否 | 同上,但 Cookie Jar 有记录 |
| Repeater 删掉 dvwaSession | 不带 | 是 | 服务端才重新生成 |
4. 四招逼出新 Cookie:按场景选着用
4.1 无痕窗口 / 清站点 Cookie
新手最推荐的方式:开一个无痕窗口,重新登录 DVWA,安全级别切到 High,再打开 Weak Session IDs。每次关闭并重开无痕窗口,都是一次全新的干净状态,服务端必然重新生成 dvwaSession。把想要的目标访问路径完整走一遍:首页 → 登录 → DVWA Security 切 High → Weak Session IDs,就能拿到一个新 Cookie。
缺点是手动重复太繁琐,如果只是验证现象没问题,但你要攒几十个样本做分析,这个办法效率太低。
4.2 Burp Repeater 删掉 Cookie 头
最推荐用于采样。按照实验三的流程,在 Repeater 里把请求头整理干净,只保留 Host、User-Agent、Cookie(里面只放 PHPSESSID 和 dvwaSecurity),把 dvwaSession 去掉,然后每次 Send 都会收到一个新 Set-Cookie。
如果你需要大批量采集,可以更进一步:把请求发到 Intruder,给请求配置一条"删除 dvwaSession 请求头"的宏或正则替换规则,用空 payload 跑几十次,一次拿到几十个新值。这里的关键始终是同一个:让服务端认为你没有 dvwaSession。
4.3 curl 脚本采集
不想开 GUI 的时候,curl 也能干。思路一样:每次请求都不携带 dvwaSession。最省事的方式是,从浏览器里复制一个有效的 PHPSESSID,直接放在 Cookie 头里手动指定:
for i in $(seq 1 20); do curl -s "http://127.0.0.1/dvwa/vulnerabilities/weak_session_ids/" \ -H "Cookie: PHPSESSID=你的会话ID; dvwaSecurity=high" \ | grep -oE "Cookie Value: [0-9]+" done注意三点:第一,DVWA 的实际路径按你的安装位置调整,可能是/DVWA/而不是/dvwa/;第二,PHPSESSID 每次登录都会变,要用当前有效值;第三,如果你的 DVWA 版本较新,登录页可能有user_token之类的 CSRF 防护,从浏览器会话复制 PHPSESSID 可以绕开登录接口的 token 问题。
4.4 本地靶场改源码强制生成
如果你就是想在页面上反复观察生成过程,可以在自己搭的 DVWA 里改代码,比如把 High 源码改成"只要有 POST 请求就重新生成",或者加一个专门触发setcookie()的调试接口。这样每点一次按钮都能看到 Set-Cookie 出现。
这套操作只适合本地自建靶场,目的是直观理解"服务端何时生成、何时不生成"的分支逻辑。改完记得改回去,否则后续做题逻辑就不对了。
5. 刷新出来以后:把 High 这关真正做完
5.1 先采集足够样本
拿到新 Cookie 只是开始。High 考查的是可预测性,一两个值没有分析价值。用第 4.2 或 4.3 的方法连续采集 30~50 个 Cookie 值,并且把每个值对应的触发时间记录下来——Burp 响应里的 Time 字段,或者脚本里的时间戳都行。
样本量太少时,很多统计特征出不来;样本量超过 50 个,你会看到 mt_rand() 输出在数值范围上的分布特征。
5.2 用直觉扫一遍数据特征
拿到样本先别急着上工具,用肉眼做三件事。
第一,看位数:mt_rand()默认输出范围是 0 到 231-1,通常表现为 9~10 位整数;如果你拿到的是 32 位十六进制,说明这版 DVWA 可能用了别的生成方式。
第二,看相邻值的变化:Low 的时间戳是线性递增,一眼能看出来;mt_rand() 看起来毫无规律,但如果你把几十个值按时间排列,结合大致的生成时间窗口,用 php_mt_seed 这类工具去爆破种子,时间窗猜得够近的话往往能跑出来。
第三,看有没有固定前缀或固定高位:如果某些 bit 位总是相近,说明熵源不足或者种子变化范围小而引起的周期性。
5.3 验证思路:从源码反推
最直接的验证还是读源码。High 里用的是mt_rand(),没有调用random_bytes()、openssl_random_pseudo_bytes()这类加密安全随机函数,所以在理论上就站不住。
你可以在本地写一个 PHP 脚本做对照实验:设定一个种子,调用mt_rand()生成一串值,跟抓到的 dvwaSession 对比。实验环境里完全允许这样玩,目的就是建立"弱随机数 = 会话令牌可预测"这个直觉。
5.4 一份会话令牌的检查清单
做完这一关,你脑子里应该形成一份可复用的检查清单,以后再看别的系统时直接套用:
- 熵要够:会话 ID 至少应该有 128 位熵,通常用 CSPRNG 生成。
- 来源要对:
time()、md5(time())、mt_rand()这类组合都是一眼不过的雷区。 - 生命周期要短:登录成功、权限提升、修改密码后必须调用会话 ID 更换机制,比如 PHP 的
session_regenerate_id()。 - 属性要全:HttpOnly、SameSite、Secure 这些 Cookie 属性要按需设置。
- 服务端要判:不能无条件信任浏览器传来的 Cookie,超时、异地、异常指纹要能触发重新认证。
这些对应到真实漏洞就是会话固定、会话预测和会话侧信道问题,OWASP 的 Session Management Cheat Sheet 里都有,值得拉出来对照。
6. 回到真实 Web 安全:这个坑对应什么问题
6.1 会话固定攻击的现实版本
"Cookie 只在不存在时才生成"这个逻辑,在真实系统里对应的就是会话固定攻击。攻击者先自己拿到一个合法会话 ID,然后想方设法让受害者用这个 ID 访问系统;只要服务端不在登录成功后更换会话 ID,攻击者就能用同一个 ID 冒充受害者的身份。DVWA 这里因为你手动清掉 Cookie 才会生成新值,而真实系统里用户根本不会主动清 Cookie,所以"登录后不更换会话 ID"是实打实的漏洞。
6.2 可预测会话导致越权
High 级别的mt_rand()对应的是会话预测。攻击者如果能算出下一个会话值,就能直接伪装成任意在线用户。历史上不少 CMS 和框架都出过这类问题,某个版本里用可预测随机数生成重置令牌,导致任意账号密码重置的漏洞,原理和这个一模一样——随机数不可预测性不足。
6.3 我给自己留的排障习惯
最后分享几个我实际做事时的习惯,你可以直接抄:
第一,凡是遇到"Cookie 不更新"这类问题,第一步不是折腾 Burp,而是去服务端源码里找setcookie()的调用条件,一个isset判断往往就是全部答案。
第二,在 Burp 里观察会话相关行为时,把 Cookie Jar 的自动更新规则看清楚。需要看原始流量时,临时关掉自动携带 Cookie 的功能,避免 Burp 替你补请求头掩盖真相。
第三,在 Repeater 里重放请求时,请求头要自己审一遍。该删的 Cookie 删干净,否则你看到的只是浏览器旧行为的重演,而不是服务端对新请求的响应。
第四,记录样本时把时间和请求流水号一起记录,后面做随机性分析时,时间轴就是最重要的线索。
最后补一个小技巧:做完这一关,建议把 Low / Medium / High 三份源码放在一起做 diff。你会发现 DVWA 的作者想表达的东西,全都在三份代码的差异里——从时间戳到哈希链,再到伪随机数,每一级都在演示一个特定的失败模式。搞懂了这三份 diff,Weak Session IDs 这关才算真正通关。