CSRF Token防护实战:从Pikachu靶场漏洞看Token生成、校验与绕过
2026/7/3 14:25:05 网站建设 项目流程

1. 项目概述:从靶场实战到CSRF防护本质

最近在带团队做安全审计和渗透测试培训,Pikachu靶场是绕不开的经典。很多新手在过CSRF关卡时,往往卡在Token验证这一环,要么不理解为什么加了Token就安全了,要么在实战中遇到一些“奇怪”的场景,明明有Token却依然被绕过。这让我意识到,很多人对CSRF(跨站请求伪造)防护,尤其是Token验证机制的理解,还停留在“有Token就安全”的浅层认知上。实际上,Token的生成、存储、校验和传输,任何一个环节的疏忽都可能让防护形同虚设。今天,我就结合Pikachu靶场的几个典型场景,把CSRF Token验证的底层实现逻辑、常见的错误配置,以及攻击者可能采用的绕过思路,掰开揉碎了讲清楚。无论你是正在学习Web安全的新手,还是需要加固自家应用安全的开发,这篇文章都能帮你建立起一个更立体、更实战化的认知。

CSRF攻击的核心在于“借用”用户的身份和权限,在用户不知情的情况下发起非预期的操作。而Token验证的思路,就是为每一个敏感请求绑定一个唯一的、不可预测的“令牌”,服务器通过校验这个令牌来确认请求的合法性。听起来很简单,对吧?但在Pikachu靶场里,你会发现从GET请求的Token泄露,到POST请求的校验逻辑缺陷,再到同源策略下的子域信任危机,每一个点都可能是突破口。我们不仅要会“用”Token,更要懂它“为什么”能防,以及“怎么”会被破。

2. CSRF Token验证的底层实现逻辑拆解

要理解如何绕过,必须先透彻理解它是如何工作的。一个健壮的CSRF Token防护体系,通常包含四个核心环节:生成、存储、携带和验证。很多漏洞就源于这四个环节的脱节或设计缺陷。

2.1 Token的生成与唯一性保证

Token的本质是一个密码学安全的随机数,其核心要求是不可预测性会话/请求相关性。在服务端,生成Token的典型代码如下(以PHP为例):

// 一种常见的生成方式:使用随机字节生成,并转换为十六进制字符串 $csrf_token = bin2hex(random_bytes(32)); // 生成一个256位(32字节)的强随机令牌

这里的关键是random_bytes()函数,它依赖于操作系统的密码学安全伪随机数生成器(CSPRNG),保证了生成的令牌攻击者无法通过计算预测。绝对禁止使用rand()mt_rand()或基于时间戳的简单哈希来生成Token,这些方式都存在被爆破或预测的风险。

Token的“唯一性”通常体现在两个维度:

  1. 用户会话唯一:同一个用户会话(Session)内,Token可以保持不变或定期刷新。这关联了用户的身份。
  2. 请求/表单唯一:为每一个敏感表单或操作生成一个独立的Token,实现“一次一密”,安全性最高,但实现复杂度也高。Pikachu靶场中多数是会话级Token。

生成的Token需要与当前用户的会话(Session)进行绑定存储:

// 将Token存入用户Session $_SESSION['csrf_token'] = $csrf_token;

2.2 Token在客户端的存储与携带方式

服务器生成Token后,需要将其安全地传递到客户端,并让客户端在发起请求时能将其带回来。常见的方式有三种:

  1. 嵌入HTML表单的隐藏域(Hidden Field):这是最经典、Pikachu靶场主要使用的方式。

    <form action="/vul/csrf/csrfget/csrf_get_edit.php" method="GET"> <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>"> <!-- 其他表单字段 --> <input type="submit" value="提交"> </form>

    优点:实现简单,兼容性好。缺点:对于单页面应用(SPA)或大量使用AJAX的场景不够灵活;如果网站存在XSS漏洞,攻击者可以通过JavaScript轻松窃取到这个Token。

  2. 放入Cookie(Double Submit Cookie):服务器在设置会话Cookie的同时,设置一个独立的、内容相同的CSRF Token Cookie。前端JavaScript(如框架)在发起请求时,需要从Cookie中读取Token,并将其作为自定义HTTP Header(如X-CSRF-TOKEN)或请求参数附加到请求中。服务器端同时校验请求中的Token和Cookie中的Token是否一致。

    // 前端使用JavaScript从Cookie读取Token并添加到请求头 function getCookie(name) { // ... 获取Cookie值的函数 } const csrfToken = getCookie('csrf_token'); fetch('/api/update', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken // 将Token放在自定义头部 }, body: JSON.stringify(data) });

    优点:前端无需将Token嵌入每个表单,更适合现代Web应用。由于浏览器同源策略限制,恶意网站无法读取目标站的Cookie,因此攻击者无法伪造正确的自定义Header。缺点:依赖于前端JavaScript的执行,如果应用禁用JS则可能失效。同时,必须确保Cookie的HttpOnly属性为false(否则JS读不了),这会略微降低Cookie被盗的防护(但CSRF Token Cookie本身被盗影响有限,因为它需要和Session Cookie配对使用)。

  3. 通过HTTP响应头传递:服务器在HTTP响应头(如X-CSRF-Token)中返回Token,前端JS读取后存储(如在内存或非HttpOnly的Cookie中),并在后续请求中携带。这种方式常与上述第二种结合。

在Pikachu靶场的“CSRF(get)”关卡中,采用的就是第一种方式:Token直接放在表单的URL参数里(因为是GET请求),这本身就埋下了隐患。

2.3 服务端的校验逻辑与状态管理

客户端携带Token发起请求后,服务端的校验是最后一道防线。一个完整的校验逻辑应该包括:

// 服务端校验示例 session_start(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 首先检查请求方法是否允许 // 1. 检查Token是否存在 if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token'])) { die('CSRF token missing!'); } // 2. 进行恒定时间比较,防止时序攻击 if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { // 记录安全日志 error_log('Potential CSRF attack detected for session: ' . session_id()); die('CSRF token validation failed!'); } // 3. 校验通过后,可以选择使旧Token失效(一次性Token) // unset($_SESSION['csrf_token']); // 4. 执行正常的业务逻辑 // ... }

这里有几个极易被忽略但至关重要的细节

  • hash_equals的使用:比较字符串是否相等时,必须使用hash_equals这类恒定时间比较函数。如果使用普通的=====,攻击者可能通过测量服务器响应时间的细微差异,来暴力猜测Token,这就是时序攻击(Timing Attack)。
  • Token的生命周期管理:是“一次一用”(使用后立即销毁并生成新的)还是“一会一用”(整个会话周期有效)?前者更安全,但可能影响用户体验(如浏览器后退、多标签操作)。Pikachu靶场默认是“一会一用”。
  • 校验失败的处理:不能简单地返回一个400 Bad Request就了事。应该记录详细的日志(会话ID、IP、请求时间、目标操作),并可能触发安全告警。同时,返回给用户的页面应该清晰说明原因,而不是一个晦涩的错误码。

注意:很多开发者在实现时,只做了“是否存在”和“是否相等”的检查,却忽略了请求方法的过滤。如果一个本应只接受POST请求的敏感操作,同时也允许GET请求,那么即使有Token,也可能通过GET请求泄露在URL中(如浏览器历史、Referer头、访问日志),这就是Pikachu “CSRF(get)”关卡的核心漏洞点。

3. Pikachu靶场中的CSRF Token场景与漏洞复现

Pikachu靶场为我们提供了几个绝佳的、由浅入深的学习场景。我们逐一分析其防护机制的实现和突破点。

3.1 CSRF(get):Token在URL中的泄露与利用

这是最经典的案例。漏洞代码的关键在于,它将CSRF Token作为URL的查询参数(?csrf_token=xxx)进行传递,并且服务端对GET和POST请求都执行了相同的业务逻辑。

漏洞复现步骤:

  1. 正常登录Pikachu靶场,进入“CSRF(get)”漏洞页面,准备修改个人信息。
  2. 查看页面源代码,你会发现表单的action是GET请求,Token直接暴露在URL中。
    <form method="get" action="csrf_get_edit.php"> <input type="hidden" name="csrf_token" value="a1b2c3d4e5..." /> <!-- 其他字段 --> </form>
  3. 提交一次表单,浏览器地址栏会变成类似csrf_get_edit.php?csrf_token=a1b2c3d4e5...&name=xxx&...的URL。
  4. 攻击者如何利用?攻击者只需构造一个包含该完整URL的链接或图片标签,诱骗已登录的用户点击。
    <!-- 攻击者页面上的恶意链接 --> <a href="http://target-pikachu.com/vul/csrf/csrfget/csrf_get_edit.php?csrf_token=a1b2c3d4e5...&name=hacker&phone=1234567890">点击领取红包</a> <!-- 或者使用图片自动触发 --> <img src="http://target-pikachu.com/vul/csrf/csrfget/csrf_get_edit.php?csrf_token=a1b2c3d4e5...&name=hacker&..." style="display:none;" />
  5. 用户一旦点击链接或页面加载了该图片,浏览器就会自动携带用户的会话Cookie向目标地址发起GET请求。由于Token正确且会话有效,修改操作就会被执行。

根本原因与修复方案:

  • 原因:将CSRF Token用于防范非幂等的操作(如修改、删除),但却使用了幂等的GET方法。GET请求的语义是“获取资源”,其参数天然暴露在URL中,容易被记录和转发。
  • 修复严格遵守HTTP方法语义。对于任何会产生副作用的操作(POST, PUT, DELETE),必须使用对应的HTTP方法,并且仅在POST请求(或PUT/DELETE)的请求体(Body)中传递CSRF Token,绝不在URL中传递。服务器端应严格校验请求方法,拒绝用GET处理更新操作。

3.2 CSRF(post):前端校验的不可靠性

在Pikachu的“CSRF(post)”关卡中,表单采用了POST方法提交,Token也放在了请求体中,看似安全了。但它的漏洞在于:服务端根本没有实现CSRF Token的校验逻辑!

漏洞复现步骤:

  1. 查看该关卡页面源代码,表单确实是POST,Token也在隐藏域中。
  2. 使用Burp Suite拦截提交请求,你会发现请求体中有csrf_token参数。
  3. 关键步骤:直接删除或修改这个csrf_token参数的值,然后转发请求。
  4. 你会发现,请求依然成功了!个人信息被修改。

漏洞分析:这暴露了一个非常低级的错误:开发者在表单里生成了Token,但在服务端处理请求的代码里,忘记添加校验Token的逻辑。攻击者完全可以自己构造一个恶意表单,完全不用关心Token,直接发起POST请求即可。

<!-- 攻击者构造的恶意表单,无需包含Token --> <form id="maliciousForm" action="http://target-pikachu.com/vul/csrf/csrfpost/csrf_post_edit.php" method="POST"> <input type="hidden" name="name" value="hacked_by_csrf"> <input type="hidden" name="phone" value="66666666666"> </form> <script>document.getElementById('maliciousForm').submit();</script>

修复方案:这是一个意识问题。必须建立“生成即校验”的强制关联。只要在服务端生成了Token并下发到前端,那么对应接口的服务端逻辑里,就必须存在校验该Token的代码块。可以通过编写中间件(Middleware)或过滤器(Filter)来统一处理,确保所有需要防护的接口都经过校验。

3.3 CSRF Token的“一次一用”与“一会一用”策略

Pikachu靶场默认使用的是“一会一用”(Per-Session)策略。即用户登录后生成一个Token,在整个会话期间有效。这带来了一个潜在风险:Token重用

如果Token长时间不变,一旦因为某种原因(如XSS漏洞、不安全的日志记录)导致Token泄露,那么这个泄露的Token在会话过期前一直有效,攻击者可以随时利用。相比之下,“一次一用”(Per-Request)策略在每次请求后都使旧Token失效并生成新Token,安全性更高,但实现更复杂,需要处理好单页面应用的多请求并发和浏览器后退问题。

实操心得:在实战中,折中的方案是“按操作刷新”或“短时间有效”。例如,在用户执行完一个敏感操作(如转账)后,立即刷新Token。或者为Token设置一个较短的有效期(如5分钟),即使泄露,攻击窗口也很小。在Pikachu的环境中,你可以尝试修改源码,在csrf_post_edit.php校验成功后,立即调用session_regenerate_id(true)并生成新的csrf_token,这样旧的Token即刻失效。

4. 高级绕过思路:当Token防护并非无懈可击

理解了基础实现和常见漏洞,我们来看看更高级的、在真实环境中可能遇到的绕过场景。这些思路部分源于网络上的安全研究,部分来自实战审计经验。

4.1 子域信任与同源策略的边界

这是开头引用的知乎专栏文章里提到的核心思路。同源策略(SOP)是Web安全的基石,它规定来自不同源(协议、域名、端口任一不同)的脚本无法访问对方的资源。但是,同源策略对Cookie的处理有一个关键例外:默认情况下,Cookie不区分子域

假设你的主站是www.example.com,API服务部署在api.example.com。你为.example.com设置了CSRF Token Cookie。那么,来自attacker.example.com(一个你已废弃或被劫持的子域)的页面,也能读取和操作这个Cookie。

攻击场景模拟:

  1. 应用在www.example.com使用Double Submit Cookie方案防护CSRF。
  2. 攻击者通过子域劫持、历史遗留的脆弱子域(如test.example.com存在XSS漏洞)等方式,控制了attacker.example.com
  3. 攻击者在attacker.example.com上部署恶意脚本,该脚本可以:
    • 读取属于.example.com的CSRF Token Cookie。
    • 使用这个Token,构造一个向www.example.com发起转账的请求。
  4. 由于请求发自同源(attacker.example.comwww.example.com同属.example.com),浏览器会自动携带用户的会话Cookie和CSRF Token Cookie(如果是Double Submit方案,攻击脚本可以手动将Token添加到请求头),请求校验通过,攻击成功。

防护措施:

  • 严格设置Cookie的作用域:为CSRF Token Cookie设置严格的Domain属性,避免使用顶域(如.example.com)。最好明确指定为www.example.com
  • 实施严格的子域管理:定期审计和清理无用的子域,确保所有子域的安全水平与主站一致。
  • 使用Origin/Referer校验作为补充:虽然Referer头可能被篡改或缺失,但结合Token使用,可以增加一道防线。检查请求的OriginReferer头是否来源于预期的域名。

4.2 校验逻辑缺陷:Token比较与状态管理

即使Token生成、存储、携带都正确,校验逻辑本身的缺陷也可能被利用。

  • 时序攻击(Timing Attack):如前所述,使用=====进行字符串比较,比较时间会随字符匹配程度而变化。攻击者通过大量请求和精确的响应时间测量,有可能逐位猜出Token。必须使用hash_equals(PHP)、secrets.compare_digest(Python)等恒定时间比较函数。
  • Token解码或解析漏洞:如果Token不是简单的随机字符串,而是经过编码(如JWT)的结构化令牌,那么可能存在解析逻辑漏洞。例如,服务器可能使用不同的算法(如none算法)来解析JWT,或者校验签名时逻辑有误。这要求对JWT等复杂Token的实现有深入理解。
  • Token状态不同步:在分布式应用或使用了多台Web服务器的场景下,用户的Session可能存储在Redis等外部缓存中。如果Token生成后写入Session,但后续请求被负载均衡到另一台服务器,而Session数据同步延迟,就会导致校验失败(误杀正常用户)或校验绕过(如果某台服务器Session异常)。需要确保分布式Session存储的强一致性和及时性。

4.3 其他辅助绕过技巧

  • 方法覆盖(Method Override):有些应用框架支持通过请求参数(如_method=PUT)来覆盖实际的HTTP方法。如果服务器仅对POST进行Token校验,但允许通过_method参数将GET请求覆盖为POST,那么攻击者就可以用GET请求绕过。服务器端应基于真实的REQUEST_METHOD进行校验,而非解析后的方法。
  • Content-Type 校验绕过:一些框架的CSRF防护会检查Content-Type是否为application/x-www-form-urlencoded,multipart/form-datatext/plain,而拒绝application/json。如果应用API同时支持表单和JSON格式,但只在表单处校验Token,攻击者可能通过构造Content-Type: application/json的请求来绕过。防护逻辑应与业务逻辑解耦,对所有可能修改状态的端点进行统一校验。

5. 构建健壮的CSRF防护体系:实战建议

综合以上分析,要构建一个真正有效的CSRF防护体系,不能只依赖Token这一道墙,而应该实施纵深防御。

  1. 首选方案:使用成熟的框架或库:不要自己重复造轮子。现代Web框架(如Spring Security, Django, Laravel, Express with csurf/csurf等)都内置了经过充分测试的CSRF防护中间件。直接启用并正确配置它们,是避免低级错误的最佳实践。

  2. 实施同步令牌(Synchronizer Token)模式:即本文详细讨论的、Pikachu靶场意图实现的方式。确保做到:

    • 强随机数生成
    • Token与会话绑定
    • Token放在请求体(POST/PUT等)中
    • 服务端进行恒定时间比较校验
  3. 考虑Double Submit Cookie模式:对于前后端分离的SPA应用,这是更友好的方案。注意Cookie作用域的严格设置。

  4. 设置SameSite Cookie属性:这是现代浏览器提供的强大武器。将关键的会话Cookie设置为SameSite=StrictSameSite=Lax

    • Strict:浏览器在任何跨站请求中都不会发送该Cookie。最安全,但可能影响从外部链接跳转到站内的用户体验(因为跳转是GET请求,也不带Cookie)。
    • Lax:在安全的顶级导航(如点击链接)中会发送Cookie,但在跨站的POST请求或通过<script><img>等标签发起的请求中不发送。这是一个很好的平衡点,能阻止大多数CSRF攻击。这是当前推荐的主流配置。
    // 在设置会话Cookie时 session_set_cookie_params([ 'lifetime' => ..., 'path' => '/', 'domain' => 'www.example.com', 'secure' => true, // 仅HTTPS 'httponly' => true, 'samesite' => 'Lax' // 关键! ]);
  5. 关键操作增加二次验证:对于资金转账、修改密码、修改邮箱等极高风险操作,强制要求用户进行二次验证,如输入登录密码、短信验证码、TOTP动态令牌等。CSRF攻击无法获取这些用户本地或记忆中的秘密,从而被彻底阻断。

  6. 实施Origin/Referer检查作为补充:虽然不能作为主要防御手段(Referer可能为空或被禁用),但可以作为额外的验证层。检查请求头中的OriginReferer是否来源于受信任的域名列表。

在实际渗透测试或代码审计中,我的检查清单通常是这样的:首先看关键操作是否用了GET方法;然后检查表单是否有Token隐藏域,并抓包修改/删除Token看是否校验;接着查看Cookie的SameSite属性;最后,如果是复杂应用,再深入分析Token的生成、存储和分布式校验逻辑。防御从来不是单一技术点,而是一套组合拳。理解攻击者的每一种绕过思路,才能更好地筑牢自己的防线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询