CSRF Token防护实战:从Pikachu靶场漏洞看Token生成、校验与绕过
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的“唯一性”通常体现在两个维度用户会话唯一同一个用户会话Session内Token可以保持不变或定期刷新。这关联了用户的身份。请求/表单唯一为每一个敏感表单或操作生成一个独立的Token实现“一次一密”安全性最高但实现复杂度也高。Pikachu靶场中多数是会话级Token。生成的Token需要与当前用户的会话Session进行绑定存储// 将Token存入用户Session $_SESSION[csrf_token] $csrf_token;2.2 Token在客户端的存储与携带方式服务器生成Token后需要将其安全地传递到客户端并让客户端在发起请求时能将其带回来。常见的方式有三种嵌入HTML表单的隐藏域Hidden Field这是最经典、Pikachu靶场主要使用的方式。form action/vul/csrf/csrfget/csrf_get_edit.php methodGET input typehidden namecsrf_token value?php echo $_SESSION[csrf_token]; ? !-- 其他表单字段 -- input typesubmit value提交 /form优点实现简单兼容性好。缺点对于单页面应用SPA或大量使用AJAX的场景不够灵活如果网站存在XSS漏洞攻击者可以通过JavaScript轻松窃取到这个Token。放入CookieDouble 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配对使用。通过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_tokenxxx进行传递并且服务端对GET和POST请求都执行了相同的业务逻辑。漏洞复现步骤正常登录Pikachu靶场进入“CSRF(get)”漏洞页面准备修改个人信息。查看页面源代码你会发现表单的action是GET请求Token直接暴露在URL中。form methodget actioncsrf_get_edit.php input typehidden namecsrf_token valuea1b2c3d4e5... / !-- 其他字段 -- /form提交一次表单浏览器地址栏会变成类似csrf_get_edit.php?csrf_tokena1b2c3d4e5...namexxx...的URL。攻击者如何利用攻击者只需构造一个包含该完整URL的链接或图片标签诱骗已登录的用户点击。!-- 攻击者页面上的恶意链接 -- a hrefhttp://target-pikachu.com/vul/csrf/csrfget/csrf_get_edit.php?csrf_tokena1b2c3d4e5...namehackerphone1234567890点击领取红包/a !-- 或者使用图片自动触发 -- img srchttp://target-pikachu.com/vul/csrf/csrfget/csrf_get_edit.php?csrf_tokena1b2c3d4e5...namehacker... styledisplay:none; /用户一旦点击链接或页面加载了该图片浏览器就会自动携带用户的会话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的校验逻辑漏洞复现步骤查看该关卡页面源代码表单确实是POSTToken也在隐藏域中。使用Burp Suite拦截提交请求你会发现请求体中有csrf_token参数。关键步骤直接删除或修改这个csrf_token参数的值然后转发请求。你会发现请求依然成功了个人信息被修改。漏洞分析这暴露了一个非常低级的错误开发者在表单里生成了Token但在服务端处理请求的代码里忘记添加校验Token的逻辑。攻击者完全可以自己构造一个恶意表单完全不用关心Token直接发起POST请求即可。!-- 攻击者构造的恶意表单无需包含Token -- form idmaliciousForm actionhttp://target-pikachu.com/vul/csrf/csrfpost/csrf_post_edit.php methodPOST input typehidden namename valuehacked_by_csrf input typehidden namephone value66666666666 /form scriptdocument.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.comAPI服务部署在api.example.com。你为.example.com设置了CSRF Token Cookie。那么来自attacker.example.com一个你已废弃或被劫持的子域的页面也能读取和操作这个Cookie。攻击场景模拟应用在www.example.com使用Double Submit Cookie方案防护CSRF。攻击者通过子域劫持、历史遗留的脆弱子域如test.example.com存在XSS漏洞等方式控制了attacker.example.com。攻击者在attacker.example.com上部署恶意脚本该脚本可以读取属于.example.com的CSRF Token Cookie。使用这个Token构造一个向www.example.com发起转账的请求。由于请求发自同源attacker.example.com和www.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使用可以增加一道防线。检查请求的Origin或Referer头是否来源于预期的域名。4.2 校验逻辑缺陷Token比较与状态管理即使Token生成、存储、携带都正确校验逻辑本身的缺陷也可能被利用。时序攻击Timing Attack如前所述使用或进行字符串比较比较时间会随字符匹配程度而变化。攻击者通过大量请求和精确的响应时间测量有可能逐位猜出Token。必须使用hash_equalsPHP、secrets.compare_digestPython等恒定时间比较函数。Token解码或解析漏洞如果Token不是简单的随机字符串而是经过编码如JWT的结构化令牌那么可能存在解析逻辑漏洞。例如服务器可能使用不同的算法如none算法来解析JWT或者校验签名时逻辑有误。这要求对JWT等复杂Token的实现有深入理解。Token状态不同步在分布式应用或使用了多台Web服务器的场景下用户的Session可能存储在Redis等外部缓存中。如果Token生成后写入Session但后续请求被负载均衡到另一台服务器而Session数据同步延迟就会导致校验失败误杀正常用户或校验绕过如果某台服务器Session异常。需要确保分布式Session存储的强一致性和及时性。4.3 其他辅助绕过技巧方法覆盖Method Override有些应用框架支持通过请求参数如_methodPUT来覆盖实际的HTTP方法。如果服务器仅对POST进行Token校验但允许通过_method参数将GET请求覆盖为POST那么攻击者就可以用GET请求绕过。服务器端应基于真实的REQUEST_METHOD进行校验而非解析后的方法。Content-Type 校验绕过一些框架的CSRF防护会检查Content-Type是否为application/x-www-form-urlencoded,multipart/form-data或text/plain而拒绝application/json。如果应用API同时支持表单和JSON格式但只在表单处校验Token攻击者可能通过构造Content-Type: application/json的请求来绕过。防护逻辑应与业务逻辑解耦对所有可能修改状态的端点进行统一校验。5. 构建健壮的CSRF防护体系实战建议综合以上分析要构建一个真正有效的CSRF防护体系不能只依赖Token这一道墙而应该实施纵深防御。首选方案使用成熟的框架或库不要自己重复造轮子。现代Web框架如Spring Security, Django, Laravel, Express with csurf/csurf等都内置了经过充分测试的CSRF防护中间件。直接启用并正确配置它们是避免低级错误的最佳实践。实施同步令牌Synchronizer Token模式即本文详细讨论的、Pikachu靶场意图实现的方式。确保做到强随机数生成。Token与会话绑定。Token放在请求体POST/PUT等中。服务端进行恒定时间比较校验。考虑Double Submit Cookie模式对于前后端分离的SPA应用这是更友好的方案。注意Cookie作用域的严格设置。设置SameSite Cookie属性这是现代浏览器提供的强大武器。将关键的会话Cookie设置为SameSiteStrict或SameSiteLax。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 // 关键 ]);关键操作增加二次验证对于资金转账、修改密码、修改邮箱等极高风险操作强制要求用户进行二次验证如输入登录密码、短信验证码、TOTP动态令牌等。CSRF攻击无法获取这些用户本地或记忆中的秘密从而被彻底阻断。实施Origin/Referer检查作为补充虽然不能作为主要防御手段Referer可能为空或被禁用但可以作为额外的验证层。检查请求头中的Origin或Referer是否来源于受信任的域名列表。在实际渗透测试或代码审计中我的检查清单通常是这样的首先看关键操作是否用了GET方法然后检查表单是否有Token隐藏域并抓包修改/删除Token看是否校验接着查看Cookie的SameSite属性最后如果是复杂应用再深入分析Token的生成、存储和分布式校验逻辑。防御从来不是单一技术点而是一套组合拳。理解攻击者的每一种绕过思路才能更好地筑牢自己的防线。

相关新闻