
做Web渗透的朋友估计都遇到过这样的画面把一个参数值改成带%0d%0a的字符串丢出去响应头里竟然凭空多了一行自己构造的内容。我第一次看到这个结果时心里想的是“这玩意居然真能成”。CRLF注入在Web渗透里算不上什么新鲜手法原理简单到可以用一句话讲完但它常年被低估测试的时候也特别容易被一带而过。这篇就把CRLF注入掰开揉碎讲清楚包括它背后的HTTP协议逻辑、不同HTTP方法下的注入场景、实测过程、危害面以及开发运维测试三端怎么防守。不管你是刚入门Web渗透的新手还是要给团队补安全底子的开发这篇都可以当一份可落地的参考。1. 先搞清楚CRLF注入到底是什么1.1 HTTP报文里的回车换行是一个“规矩”要理解CRLF注入先要回到HTTP协议最底层的格式。CRLF是Carriage Return Line Feed的缩写对应ASCII码里的13和10十六进制分别是0x0D和0x0A翻译过来就是回车加换行。HTTP协议从诞生那天起就规定报文里的行分隔符必须是CRLF不能用单独的\n也不能用其他字符代替。我们看到的HTTP响应报文内部其实是这样的结构状态行例如HTTP/1.1 200 OK多个响应头每个头一行例如Content-Type: text/html一个空行也就是一个单独的CRLF响应体内容请求报文也类似只是第一行变成了请求行包含请求方法、路径和协议版本。这套格式看起来枯燥却是整个CRLF注入的地基协议用CRLF来“分行”那一行从哪里开始、到哪里结束全由这个控制字符说了算。用一个生活化的类比来理解HTTP报文就像一张设计好的表格你在格子里填写内容。正常情况下你只能在格子内部写字一个格子写完换下一个格子顺序由表格设计者定。但如果你能在格子的边框线上划一道口子那原本的“一行”就能被你拆成“两行”甚至你能直接在表格底部撕开一个口子往表格外面的空白区域塞内容。CRLF就是那道口子攻击者要做的事情就是想办法在参数里塞进这个“换行符”让服务端在拼接响应时把用户的输入当成协议控制字符来解析。1.2 注入的本质把一个分隔符当成了拼接器CRLF注入的原理其实非常朴素服务端在构造HTTP响应时如果某个响应头的值来自用户的输入并且没有做任何过滤那么攻击者就可以在这个值里插入CRLF字符。服务端一旦把这个包含换行的字符串直接拼到响应头里HTTP解析器就会认为“这里应该换行了”于是后面的内容就变成了一个新的响应头。这还不是最严重的。如果攻击者插入的是%0d%0a%0d%0a即两组CRLF那么在协议层看来响应头到这里已经结束了后面的内容会被当作响应体输出。这就是所谓的“响应拆分”相当于攻击者不但能往响应头里加内容还能直接决定响应体里输出什么。一个看起来只影响“显示格式”的换行符在HTTP协议里等于给了攻击者半支笔让他在原本只有服务端能写字的纸上乱画。很多开发同学第一次听到这里会问不就是多了一行头吗能有多大影响这种想法恰恰是CRLF注入被低估的原因。单看一个响应头确实不起眼但攻击者注入的可以是Set-Cookie、Location甚至直接让响应体变成钓鱼页面破坏力完全取决于攻击者想做什么而不是这个漏洞本身看起来多不起眼。1.3 为什么它常年被低估CRLF注入之所以在漏洞报告里经常排不上号有几个现实原因。第一它触发起来有条件。并不是所有参数都会被拼到响应头里很多后端框架默认会把换行符过滤掉或者编码掉导致测试人员试了半天没反应就放弃了。第二很多扫描器对CRLF注入的检测规则不够灵敏常常把真实的注入点漏掉或者把普通反射误报成注入时间久了测试人员对这个漏洞就有点“狼来了”的心态。第三CRLF注入不像SQL注入那样有一个数据库在背后影响范围不是立竿见影的很多人不知道它到底能干什么。但实际在授权测试中我见过不少系统栽在这个小问题上。尤其是那些用老旧框架、自己拼响应头、对输入输出没有统一过滤的存量系统测一圈下来总会碰到几个能把CRLF打进去的接口。而且CRLF注入经常是“低危漏洞撬动高危后果”的典型代表单独看问题不大组合上会话固定、XSS、缓存投毒之后评级直接往上翻。2. 从HTTP方法的角度看CRLF注入2.1 请求行、请求头、请求体三层关系要摸清CRLF注入在不同HTTP方法下的表现得先明白一个请求是怎么分层的。一个HTTP请求由三部分组成请求行方法、路径、协议版本、请求头一堆键值对、请求体POST/PUT等携带的数据。这三层之间全部靠CRLF来分隔。从注入的角度看凡是外部输入能被拼进响应头或响应体的地方都有可能是CRLF注入点。GET请求的参数通常在URL里URL又经常被服务端拼进响应头的Location、Referer、自定义调试头里POST请求的参数在请求体里服务端处理完表单后可能把某些字段反射到页面上也可能拼进跳转地址HEAD请求没有响应体只有响应头反而是探测注入的“好工具”OPTIONS和TRACE这类方法则可能把请求头原样回显形成独特的反射场景。理解这层关系后你会意识到一件事CRLF注入的重点不在于你用的是GET还是POST而在于“输入有没有进入输出位置”。不同的HTTP方法只是决定了这个输入在什么位置、以什么格式出现最终的判断逻辑是通用的。2.2 不同HTTP方法的注入场景差异把不同方法过一遍能帮你在测试时有意识地分配精力。GET是最常见也最容易测的。参数全部在URL里你只需要把参数值改成带%0d%0a的内容观察响应头或响应体有没有变化。很多服务端的日志、跳转、错误页都会把原始URL拼进去顺带就把参数里的CRLF带到了响应头中。POST的测试难度稍微高一点。参数在请求体里浏览器和代理对请求体的处理方式更杂而且有些服务端只读取了表单字段却忽略了对响应头的净化。测试的时候要特别关注那些“提交后跳到结果页”的接口因为跳转逻辑经常会把用户输入拼进Location头。HEAD是个很有意思的探测手法。它和GET唯一的区别是响应只包含头部没有响应体。如果服务端对HEAD的处理逻辑和GET不一致或者某些框架对HEAD做了特殊缓存你注入的CRLF会不会出现在响应头里用HEAD方法扫一遍反馈特别干净。我自己的习惯是批量检测的时候先用HEAD探一遍命中后再换成GET确认完整效果。OPTIONS和TRACE比较特殊。TRACE方法历史上会让服务器把请求内容原样返回攻击者可以用它做跨站追踪XST现在大多数服务器都禁用了。OPTIONS方法通常返回服务器支持的HTTP方法列表部分实现会把用户传入的一些头或路径片段回显到响应头里。实测中如果目标允许这些方法我会顺手在路径和自定义头里放一组CRLF看看回显。PUT和DELETE偏存储型场景。PUT上传的内容可能被存储下来后续访问时展示在页面或文件下载头里这时候如果文件名或元数据里有CRLF就会形成存储型CRLF注入。DELETE则经常出现在管理后台测试时要看它的响应里是否会返回被删除资源的路径。HTTP方法常见注入位置典型场景GETURL参数、Referer、路径片段跳转、日志、错误页反射POST表单字段、JSON字段结果页跳转、文件上传元数据HEAD请求路径、自定义头批量静默探测OPTIONS路径、自定义头方法回显测试TRACE整个请求历史遗留反射场景PUT文件名、内容元数据存储型CRLF注入DELETE资源标识、路径参数管理接口响应2.3 把“HTTP方法”当注入面来枚举很多人的测试习惯是从参数出发看到哪个参数就测哪个这样容易漏掉方法和路径本身的问题。我建议换个思路先枚举HTTP方法再结合CRLF注入一起测。具体操作不复杂。对每个接口跑一遍方法枚举拿到它支持的HTTP方法列表后在每个方法下用固定格式的CRLF payload去探测。路径本身也可以作为注入点比如/api/user/%0d%0aX-Injected:%201有些框架会把路径参数拼进调试头或错误信息。这样把“方法 × 位置”变成一张测试矩阵覆盖面比单测参数大得多。这么做还有个额外好处方法枚举和CRLF测试都是“低噪声”动作不会像SQL注入那样动不动触发WAF告警很适合在授权测试的前期做信息收集。我在实际项目里就用这个方法发现过一个管理后台的PUT接口文件名参数里能打进去CRLF最终通过响应头注入Set-Cookie做到了会话固定单看每个环节都很小组合起来就是高危害链路。3. CRLF注入测试实操探测、构造、利用一条龙3.1 先找出所有可能被反射的参数动手测试之前最重要的一步是找“反射点”。CRLF注入的核心是输入被拼进响应所以你得先知道哪些输入会进入响应。一个实用的做法是给请求标上一个唯一标记比如paramREFLECTED_MARKER_12345然后观察响应里哪些位置出现了这个标记。手动的话用Burp Suite的搜索功能在响应里搜标记值批量的话可以用脚本把参数替换成随机值自动对比响应中和标记相关的位置。找到反射点之后再判断反射点是在响应头还是响应体在响应体里的反射点对CRLF注入意义不大重点放在响应头。需要注意反射点不一定只来自URL参数。Referer、User-Agent、X-Forwarded-For这类请求头也经常被服务端拼进响应。尤其是Referer在“用户分享页”“来源统计页”这类功能里服务端会把来源地址拼进去测试的时候给Referer加上CRLF标记往往会有意外收获。3.2 Payload构造细节别被URL编码坑了构造CRLF payload时最常见的坑就是编码层数。你在浏览器地址栏或者Burp里输入%0d%0a这个字符串本身可能被URL解码一次也可能被WAF改写还可能被框架又编码了一次最后真正到达应用层的不一定是CRLF控制字符。先明确一个基础点在URL里CRLF对应的编码是%0d%0a不区分大小写写成%0D%0A也行。但在测试工具里如果Burp的默认设置帮你做了URL编码那%0d%0a可能会被再次编码成%250d%250a导致实际发送的请求里只有一个百分号被解析真正的CRLF并没有发出去。比较好的做法是在Burp Repeater里先确认发送请求的编码情况或者干脆用curl配合--data-urlencode手工控制编码层。我自己测试时的习惯是这两种方式都用Burp用来精细控制请求头curl用来快速验证。无论用哪种目标都是确保服务端最终接受到的字节流里包含0x0D 0x0A这两个字节而不是它们被编码后的字符串。如果目标在前端或WAF层拦截了%0d%0a可以尝试双重编码%250d%250a、大小写混合%0D%0a、Unicode变体等绕过方式。但这里有个前提你必须先搞清楚后端的解码链有几层否则绕过只是碰运气。我在测试中见过一些系统Nginx层不做解码、WAF层放行、到Tomcat层才解一次URL编码这时候双重编码反而能成功。这个链路分析没有捷径只能靠手动发包逐步观察。3.3 实战演示Set-Cookie注入与响应拆分下面用一个典型的场景演示完整过程。假设目标站有个搜索接口/search?qkeyword服务端把搜索关键词拼到了响应头X-Search-Term里而这个拼接没有过滤CRLF。第一步确认注入点。用curl发送一个带CRLF的请求curl -i http://target.com/search?qtest%0d%0aX-Injected:%201如果响应头里出现了X-Injected: 1说明CRLF注入成功。这时候你已经在响应头里凭空多写了一个字段。第二步升级成响应拆分。发送curl -i http://target.com/search?qtest%0d%0a%0d%0ahtmlscriptalert(document.domain)/script/html如果响应里出现了两部分第一部分是原本的响应头第二部分是你构造的HTML内容说明走完两组CRLF之后服务端协议层认为响应头已经结束把后面的内容当成了响应体。这个状态下你实际上可以控制页面内容输出相当于拿到了一次性的XSS机会。第三步注入Set-Cookie做会话固定curl -i http://target.com/search?qtest%0d%0aSet-Cookie:%20sessionidattacker_controlled;%20Path/响应头里出现攻击者可控的Set-Cookie后攻击者可以先用自己的账号登录拿到合法会话ID再通过这个注入点把会话ID种到受害者浏览器里。受害者用这个会话登录后攻击者就能冒充受害者的身份。这就是典型的会话固定攻击链路单独看CRLF注入只是个“写头”漏洞走到这一步就成了能打穿认证体系的高危问题。在靶场环境里验证时可以用PortSwigger提供的CRLF注入实验环境也可以在自己本地搭一套简单的PHP或Node服务故意写成拼串的形式来复现。我强烈建议新手先把本地环境跑通再上真实目标因为只有清楚看到“注入前”和“注入后”的报文差异才能理解整个协议层面的变化。3.4 整理成可复用的一页纸清单测试做得多了我习惯把CRLF注入的步骤固化下来避免每次现想抓包确定接口支持的HTTP方法给每个输入点打标记找出反射到响应头的点用%0d%0a做最小验证用%0d%0a%0d%0a验证是否存在响应拆分确认编码链路尝试双重编码绕过用Set-Cookie、脚本标签等payload评估实际危害这个清单本质上就是一套最小验证流程。第一次手动做会有点繁琐熟练之后凭这套流程能在半小时内把一个接口的CRLF风险摸个大概。再加上用脚本把GET、HEAD、POST、PUT等方法轮询一遍基本不会漏。4. 影响面评估CRLF注入能造成什么真实危害4.1 从“多一个响应头”到“改写一整个页面”CRLF注入最直观的攻击效果是往响应头里塞内容。但响应头里的“内容”是有讲究的不同字段对应完全不同的危害路径。注入Set-Cookie能直接做会话固定攻击者可以指定受害者的会话ID这是CRLF注入最经典的利用方式。注入Location可以让服务端返回一个强制跳转到钓鱼站点的响应形成开放重定向。注入Content-Security-Policy可以放宽或破坏页面原有的CSP策略为后续XSS攻击打开大门。如果响应里有自定义调试头攻击者还能伪造调试信息干扰排障。比单个响应头更严重的是响应拆分。两组CRLF之后内容变成了响应体攻击者可以把整个HTML页面直接输出给用户不需要依赖站点本身存在XSS漏洞。这个攻击面相当于把网站的“内容输出权”部分交给了攻击者社会工程学攻击配合这个能力可以做出和原站几乎一模一样的钓鱼页面。4.2 不同业务场景下的危害分级不是所有CRLF注入都能造成同样等级的危害评估时要结合具体业务场景。登录系统和后台管理接口权重最高。能注入Set-Cookie就意味着可能劫持会话直接威胁账号体系。带用户生成内容的公开页面是第二梯队例如个人主页、评论图片URL这类位置CRLF注入可以配合缓存投毒或XSS影响大量访客。最轻的是那些纯日志接口或内部调试接口危害范围有限但也不能直接忽略因为攻击者可能拿它做日志伪造干扰安全团队的溯源分析。加一层CDN或反向代理的场景要格外小心。如果上游边缘节点缓存了攻击者注入后的响应那么接下来所有命中缓存的人都会拿到被污染的页面这就是缓存投毒。危害半径从一个直接受害者扩大到所有访问该URL的用户量级完全不一样。测试报告中遇到CDN场景时我通常会把CRLF注入的危害评级往上提一档。4.3 组合攻击才是真正可怕的CRLF注入单独拎出来往往谈不上“致命”。但安全测试看的是组合链路CRLF注入特别容易和别的漏洞串在一起。最典型的组合是CRLF注入加上XSS。通过在响应拆分中输出脚本配合社交工程让受害者点击构造好的链接攻击者不需要在页面上找到任何反射型XSS点就能执行任意JavaScript。另一个高发组合是CRLF注入加缓存投毒把恶意页面缓存到CDN让大量用户在无感知的情况下访问到攻击者构造的内容。还有个容易被忽略的组合是和日志注入配合攻击者在User-Agent或Referer里塞CRLF伪造出带有恶意行为的日志条目干扰威胁情报和溯源工作。单看CRLF注入不少安全团队会把它排在“低危”里但一旦看到它能成为XSS、会话劫持、缓存投毒的攻击支点你就明白为什么现在主流漏洞评级里CRLF注入只要可利用通常都会被评为中危甚至高危。5. 防护落地开发、运维、测试各做一半5.1 开发侧把CRLF扼杀在拼接之前CRLF注入的根因只有一个服务端在拼接响应时把不可信输入直接当成了合法内容。所以开发侧的防护思路核心就是“别拼”“验一下”“用框架”。“别拼”是最治本的。响应头里的每个字段都应该来自可信配置或经过校验的固定值而不是直接拼接用户输入。如果业务确实需要把用户输入放进响应头比如自定义跟踪ID那就必须对输入做严格校验只允许RFC 7230里明确定义的头部字段字符即字母、数字、部分符号绝对不允许出现0x0D和0x0A。“用框架”是省事的方案。Java的HttpServletResponse、Node.js的setHeader、Python Flask的response.headers这些框架API在多数版本里会自带一定防护但要注意它们只保护“通过API设置”的值如果开发自己手工拼了原始报文再写回socket那框架也救不了。代码评审的时候看到手工拼响应头的写法就要拉响警报。一段简单的Java示例String value request.getParameter(q); if (value ! null) { value value.replace(\r, ).replace(\n, ); } response.setHeader(X-Search-Term, value);这个例子只是最低限度的过滤生产环境更应该用白名单校验加编码输出。但核心逻辑是一样的进入响应头之前CRLF必须没有立身之地。5.2 运维侧边缘节点和WAF的配合运维侧能做的主要是两层。第一层是WAF或网关的过滤规则。检测请求里是否包含%0d%0a、%0D%0A、\r\n等特征命中后直接拦截。这里要注意编码绕过问题规则最好同时覆盖双重编码%250d%250a和大小写变体有条件的话配合解码模块做二次检测。如果网关用的是Nginx还可以在$http_cookie、$request_uri等变量进入后端之前做统一清洗。第二层是缓存策略。给带用户输入参数的响应设置合理的缓存范围尤其是登录态相关的Cookie不能进CDN缓存键。多语言站点要谨慎处理Vary头防止缓存键碰撞导致污染页面被复用。如果CRLF注入已经被利用过一次运维侧的紧急处置措施是清空指定URL的缓存同时在源站临时加一层header校验。5.3 测试侧把CRLF检查做成渗透必备项CRLF注入的测试方法并不复杂但很容易被遗漏。我建议把它做成一个固定检查项而不是想起来才测。手动测试时针对每个接口至少测GET和POST两种方法给URL参数、表单字段、JSON字段、常用请求头分别打CRLF标记。自动化方面Burp Suite的扫描器自带CRLF注入检测ZAP也有对应插件可以直接接入CI流水线。更轻量一点的办法是写个小脚本读取接口列表批量发送带%0d%0a标记的请求自动比对响应头中是否出现自定义字段。我最常推荐的姿势是“自动化打底手动跟进”。自动化负责把接口清单扫一遍锁定疑似注入点手动测试负责验证和利用。这样既不漏面也不容易被误报带偏。新写的接口上线前也可以把这条检查写进代码评审的checklist里。6. 常见问题速查与踩坑纪实6.1 为什么我加了%0d%0a却毫无反应这是新手最容易碰到的困惑。我见过很多人拿着%0d%0a往参数里一塞看到响应里没有新增头字段就断定不存在CRLF注入。实际上没反应的原因可能有很多。最常见的是框架帮你过滤了。Spring Boot、Express这类主流框架的响应头设置方法内部会做字符校验直接把非法头字段拦掉。其次是注入点选错了服务端虽然接收了参数但根本没有把它拼进任何响应头自然不会有反应。还有一种是编码层问题你发送的内容经过两次URL编码后到达应用层的是一个字符串字面量%0d%0a而不是真正的换行控制符。排查思路是先确认反射点再用Burp或原始字节视角确认发送内容最后用本地靶场对照验证。如果在真实目标上实在测不出不要浪费时间换个反射点继续测CRLF注入讲究的是“位置对了才有效果”。6.2 浏览器、代理和服务器对CRLF的理解差异CRLF注入在客户端和服务端之间存在明显的“理解差”。浏览器在发起请求之前会自动规范化URL把%0d%0a转成字面字符甚至直接拦掉所以在浏览器地址栏里手工测试基本是无效的必须用Burp或curl这类工具发送原始请求。代理和WAF处于中间层它们可能按自己的规则改写请求删除或编码CRLF字符这会导致你本地的payload和服务端实际收到的内容不一样。服务器端的差异更明显。Apache、Nginx、Tomcat、Node.js对响应头的解析规则不完全一致有些会拒绝包含非法字符的响应头字段有些不校验直接透传。这也解释了为什么同一个payload在A站能打进去在B站却毫无反应。测试时要注意记录服务器类型和版本方便后续评估。6.3 容易误判的三个场景我在实际项目中总结出三个容易误判的场景值得专门提一下。场景一响应体里出现了%0d%0a字符串就认为注入成功。这个判断不严谨因为服务端可能只是把用户输入做了HTML实体编码后输出到页面CRLF并没有真正生效。看响应头的变化才是准确信号。场景二因为响应头顺序问题漏判。有些代理或CDN会重排响应头导致你注入的字段排在很后面被工具窗口或日志截断看不到。建议用curl加-i参数直接看完整响应头或者把响应保存成文件再搜索。场景三把“重定向响应”里的Location头变化当成CRLF注入实际上是开放重定向。两者可以共存但利用路径不同。CRLF注入看的是有没有新增头字段或响应体提前出现开放重定向只看跳转地址是否可控。问题现象可能原因排查方向参数带%0d%0a无反应框架过滤/注入点错误/编码层数不对确认反射点所在位置检查Burp发送的原始字节响应体显示%0d%0a文本服务端做了HTML编码CRLF未生效观察响应头是否有新增字段而不是看响应体自定义头字段看不到代理重排响应头/日志截断用curl -i保存完整响应头再搜索WAF拦截%0d%0a特征规则命中分析解码链路评估双重编码可行性浏览器测试无效浏览器自动规范化URL改用Burp或curl直接发送原始请求HEAD能注入但GET不行不同方法处理逻辑不一致分别验证后以实际可利用方法为准7. 写在后面CRLF注入给你的测试方法论带来什么做了这些年安全测试我越来越觉得CRLF注入这种“小而基础”的漏洞反而最能检验一个人对协议的理解深度。它不需要复杂的利用链也不需要高深的技术栈就是对HTTP协议里一个换行符的掌控。你能不能在测试时想到它、能不能在各种编码层面前保持清醒、能不能在响应头里精准定位那个反射点——这些能力练出来之后放到HTTP请求走私、参数污染、多部分解析绕过这些更复杂的议题上同样是通用的。最后分享一个我自己的小习惯所有接口测试模板里我都固定保留一个CRLF注入的探测步骤不需要每轮都做完整利用但一定会发那两三组标准payload。这个动作成本很低却不止一次帮我提前锁定了后续可能被利用的高危点。如果你刚开始接触CRLF注入建议先从PortSwigger的靶场练手把响应拆分和Set-Cookie注入两条链路跑通再带着这套思路去测真实项目。安全这条路先把这些不起眼的小漏洞摸透后面面对复杂攻击面的时候底气会完全不一样。