☰
CRLF、CSRF与SSRF:三类Web安全漏洞的原理与防御实践
2026/10/20 22:07:01 网站建设 项目流程

如果你做过几次CTF,或者跟过完整的授权渗透测试流程,会发现CRLF、CSRF、SSRF这三兄弟出现的频率非常高。不少新手一开始记不住它们,因为三个缩写长得像,攻击位置又不在一个地方。我这些年带过不少新人,发现只要把三个漏洞放进同一个框架里理解,后面的学习会顺畅很多——它们本质上都是在“信任”上出了问题,只是被信任的对象不同。这篇东西我尽量按从零到能用的顺序写,把原理、攻击构造、防御落地都串起来,适合刚入门的安全爱好者,也适合后端开发顺手做安全自查。

1. 先建立整体认知:CRLF、CSRF、SSRF到底在打什么

1.1 攻击者的视角:Web应用哪里不牢靠

任何Web攻击,归根结底都是某个环节产生了“预期之外的行为”。CRLF注入发生在协议解析层,攻击者往HTTP报文里塞了本不该存在的换行符;CSRF发生在浏览器会话层,攻击者借用户已经登录的Cookie干坏事;SSRF发生在服务端网络层,攻击者让服务器代替自己访问内网。三个漏洞都依赖同一个前提:服务端没有建立严格的信任边界。

把HTTP报文、浏览器会话、服务端网络访问能力这三个词拆开看,你就知道它们分别对应Web应用的哪一层。这也是为什么安全圈经常把三个缩写放在一起讲——它们不是一个漏洞的三种叫法,而是三类典型弱点,学懂它们就基本摸清了Web攻击里的“输入不可信”逻辑。

1.2 三个漏洞的共性与区别

  • CRLF注入:攻击者控制的内容出现在HTTP请求头或响应头里,利用\r\n改变报文结构,让客户端或代理解析出多个头部,甚至分割响应体。
  • CSRF:攻击者无法直接读取受害者的Cookie,但可以利用浏览器自动携带Cookie的机制,构造一个从第三方站点发出的请求,让受害者浏览器带着认证信息访问目标站。
  • SSRF:攻击者输入的URL被服务端拿去请求,服务端网络边界被突破后,攻击者就能访问本不该能访问的内网资源。

它们最明显的共性是都和数据来源有关系:CRLF信任了URL参数里的未过滤字符,CSRF信任了“请求来自浏览器且带上了我的Cookie”,SSRF信任了用户提交的URL。区别在于利用结果不同,CRLF影响报文完整性,CSRF影响业务操作的合法性,SSRF直接影响内网安全边界。

1.3 为什么总被放在一起讲

实际业务里三个漏洞经常互相配合。比如一个URL参数拼接进响应头,顺手做CRLF注入,再结合缓存机制污染别人看到的页面;又比如一个SSRF能访问内网Redis,如果Redis上恰好有可被CSRF触发的写操作,整个攻击链就完整了。所以学习的时候不能孤立地看某一个漏洞,要把“输入点-信任点-影响面”这条链路打通。这篇文章的内容编排也是按这个思路来的:先单个吃透,再组合实战,最后回到防御和排查。

2. CRLF注入:在HTTP协议行里做文章

2.1 从HTTP报文格式讲起:CRLF到底是什么

CRLF是Carriage Return Line Feed的缩写,也就是回车加换行,ASCII码对应\r\n。HTTP协议从诞生起就用CRLF作为一行的结束标志,请求行是一行,每个Header是一行,Header和Body之间用一个空行隔开。就这么一个简单的约定,成了CRLF注入的基础。

正常情况下,服务端拼接响应头时,每个Header单独占一行,行与行之间用\r\n分隔。如果开发把一个用户可控字符串直接拼进了响应头,比如构造“Location: ”的时候塞进一个URL字段,攻击者在URL里放入%0d%0a(URL编码的\r\n),服务器拼接时就会把这一行截断,剩下的内容被当成新的Header行。更危险的是,如果继续注入\r\n\r\n,就相当于把Header区域结束掉,后面内容会被当成响应体返回给浏览器。这一下就从一个“头注入”升级成了“响应体伪造”。

2.2 典型注入场景与利用方式

开发里最常见的CRLF注入点有几种:重定向跳转、自定义Header拼接、日志字段回显、框架里直接拿用户输入设置Cookie。我去年在本地靶场复现过一个Flask接口,代码大概是这样的(仅用于本地测试环境):

from flask import Flask, request, Response app = Flask(__name__) @app.route('/redirect') def redirect(): url = request.args.get('url', '/') resp = Response('') resp.headers['Location'] = url return resp, 302 if __name__ == '__main__': app.run(host='127.0.0.1', port=8000)

正常请求是访问/redirect?url=/home,服务端返回302并带上Location: /home。如果用户在url参数里放%0d%0aSet-Cookie: session=Hijacked,响应头就变成两行:

Location: /home Set-Cookie: session=Hijacked

这个注入成功以后,攻击者可以干几件事:

  • 注入Set-Cookie做会话固定,诱导受害者使用攻击者指定的会话ID,之后攻击者就能冒充受害者。
  • 注入Refresh头,比如Refresh: 0;url=javascript:alert(1),在支持的浏览器里还能直接触发脚本执行。
  • 配合空行注入响应体,把正常页面替换成钓鱼页或恶意脚本页面。

我实际测试下来的经验是,URL编码在不同环节会被多次解码。有时候你用Burp直接发%0d%0a成功了,但在浏览器地址栏里敲同样的参数却失败了,因为浏览器和服务器之间还有一层解码。这不是漏洞不存在,而是你测试链路上某个环节把编码吃掉了。最好的办法是在Burp的Repeater里直接观察服务端返回的原始报文,别只看浏览器渲染。

2.3 CRLF注入的进阶玩法:缓存污染与请求走私

头注入如果只影响当前用户,危害始终有限。CRLF注入真正可怕的地方在于它可以污染共享缓存。比如某些CDN或反向代理会根据响应头里的某个字段决定缓存键,攻击者通过CRLF注入伪造一个缓存相关字段,再配合可控内容,就能让后面的普通用户拿到恶意缓存页面。这相当于一次“投毒”,受害者不需要直接访问攻击者构造的链接,只要正常访问站点就中招。

比缓存污染更进阶的是请求走私(Request Smuggling)。后端服务和前置代理对HTTP报文边界的解析不一致时,攻击者通过CRLF注入构造一个Content-Length和Transfer-Encoding冲突的请求,让代理认为是两个请求、后端认为是一个请求,从而把攻击者请求“走私”到其他用户的会话里去。请求走私的利用成本高、细节多,但一旦成功,影响面远超单个响应头注入。这部分我建议初学者先了解概念,把基础CRLF注入吃透后再深入。

2.4 防御思路与开发落地建议

CRLF注入的防御没有太多花活,核心就是一条:不能让用户输入直接进入响应头拼接。具体来说:

  • 响应头里的参数值必须做白名单校验,比如重定向目标只允许相对路径或固定前缀,不许出现协议头和任意域名。
  • 如果必须拼接用户输入,对\r、\n以及它们的URL编码变体做过滤或转义,别只过滤明文,要考虑%0d、%0a、%0D%0A等多种写法。
  • 使用语言和框架自带的Header设置函数时,先确认这些函数是否在最新版本里已经禁止换行注入,但别完全依赖它,自己代码里的校验才是兜底。
  • 加WAF是辅助手段,不是主要防线,因为WAF本身也存在被编码绕过的可能,必须配合代码层修复。

我在测试过程中发现,很多团队会把过滤器写在业务入口,但是不同接口对参数的处理方式不一样,有的地方URL解码在前、过滤在后,就漏了。建议把校验收敛成统一的安全组件,所有拼接响应头的地方都走同一个函数。

3. CSRF:借用户之手,发攻击者之请求

3.1 CSRF的核心逻辑:浏览器自动携带凭据

CSRF(Cross-Site Request Forgery)跨站请求伪造,攻击者不需要偷用户的Cookie,也不需要知道用户的密码,只要能构造一个请求,让已经登录的浏览器自己发出去就行。

关键机制是浏览器的Cookie策略:只要某个Cookie没被显式禁止,访问目标域名时浏览器就会自动带上它。很多站点靠Cookie里的会话ID识别登录状态,并没有校验请求来源。于是攻击者可以做这样一个页面:一张图片、一个自动提交的表单,或者一小段脚本,浏览器一加载,就向目标站点发出一个带Cookie的请求。服务端收到请求时看到会话合法,就当成用户本人操作执行了。整个流程里,攻击者从头到尾没接触过用户的Cookie,靠的是浏览器会“主动”携带凭据这个机制。

需要特别强调一点:CORS(跨域资源共享)并不能阻止CSRF发生。CORS限制的是JavaScript读取跨域响应的内容,但请求本身照样发得出去。也就是说,即使目标站点没有配置任何跨域许可,攻击者的页面照样可以把POST请求发出去,只是读不到响应而已。很多刚入门的朋友在这里混淆了“请求发出”和“读取响应”两件事。

3.2 三种典型的攻击构造方式

第一种是GET型CSRF。如果目标操作走的是GET请求,攻击成本最低,一个图片标签就能搞定:

<img src="http://target.com/user/delete?id=1" style="display:none">

浏览器加载图片时发起GET请求,Cookie自动携带,删除操作就完成了。这种利用方式存活至今,说明很多系统仍然把“删除”“改密”等敏感操作放在GET上。

第二种是POST型CSRF,也是最常见的。可以用表单自动提交:

<form id="f" method="POST" action="http://target.com/user/changePassword"> <input name="newPassword" value="attacker123"> <input name="confirmPassword" value="attacker123"> </form> <script>document.getElementById('f').submit();</script>

受害者只要打开这个页面,表单自动提交,密码就被改了。这个方案兼容性极好,不需要JavaScript参与,随便一个静态页都能承载。

第三种是针对JSON接口的CSRF。现代接口喜欢接收application/json,普通表单无法直接构造这种Content-Type。但浏览器允许form表单的enctype设为text/plain,再配合服务端对Content-Type的宽松解析,依旧能发包。也就是把body写成一串JSON文本直接提交,不少后端框架能正常解析。这种情况下,CSRF的坑比很多人想象中更深。

我自己的实操经验是,判断一个接口能不能被CSRF利用,先不管它是不是JSON,而是看三点:状态变更操作有没有校验Token、Cookie的SameSite属性是什么、接口能不能接受非标准Content-Type。三个条件里只要有宽松空间,就有利用可能。

3.3 绕过CSRF防护的常见思路

很多系统已经加了防护,但细节做得不严谨,存在几种典型绕过:

  • Token校验不完整。只校验Token存不存在,不校验Token是否正确或是否和当前会话绑定。攻击者可以自己预先取一个Token塞进请求里,有时就能绕过。
  • Token通过URL参数或Referer传递。比如Token出现在Referer里,或者放在GET参数里,浏览器历史记录、日志系统都会泄露,攻击者拿到后直接复用。
  • Origin/Referer校验绕过。如果服务端只判断字符串包含或前缀匹配,攻击者用target.com.attacker.com这类域名,或者构造一个http://target.com@attacker.com的URL,就可能骗过校验。还有一类情况是Referer为空,很多服务端逻辑是“Referer为空就放行”,攻击者用data:协议或sandbox属性的iframe就能触发空Referer。
  • SameSite属性默认Lax的情况下,通过顶层导航触发的GET请求依然会携带Cookie。有些系统把敏感操作写成GET,或者业务上允许通过导航触发状态变更,Lax并不能保证CSRF免疫。
  • 部分浏览器或插件环境里SameSite策略行为不一致,如果审计的系统用户群比较复杂,不能把所有安全寄托在一个属性上。

3.4 开发侧的正确防御姿势

防御CSRF第一条原则是:敏感操作不能只靠Cookie识别用户。必须引入额外的、攻击者无法预测的校验因子。

比较可靠的做法是CSRF Token。Token要足够随机、不可预测,并且绑定会话。服务端生成后嵌入表单或自定义Header,提交时对比;攻击者无法从跨域页面读取Token,所以无法带入请求。这个方法只要实现正确,效果很好。但要注意,Token不能放在URL里,也不能通过Cookie里的某个值来生成,否则都有暴露面。

第二条是正确配置Cookie的SameSite属性。同源安全模型帮了大忙,新浏览器默认Lax,已经挡掉了很多POST型CSRF。对于敏感业务可以设Strict,代价是用户体验稍微变差,比如从第三方站点跳转回本域时会话不生效。第三条是给超高敏操作加验证码或二次确认,密码修改、资金操作这类业务建议直接上。第四条是校验Origin和Referer,但作为辅助,不能作为唯一防线。

最后一点经验是,后端开发经常忽略“幂等性”设计。很多CSRF漏洞中的操作逻辑本身就有问题——删除操作应该幂等但反复执行会出乱子,密码修改应该带旧密码校验但实际却是无脑覆盖。把业务逻辑本身收紧,能让CSRF的攻击半径小很多。

4. SSRF:让服务器帮你“越狱”

4.1 SSRF的本质:服务端发起的不可信请求

SSRF(Server-Side Request Forgery)服务端请求伪造,核心一句话:攻击者提交的URL最终由服务器去请求。常见的业务场景有头像远程拉取、URL预览转码、PDF截图生成、Webhook回调、API网关中的自定义转发等。这些业务看起来各不相同,本质上都是“接收一个URL,然后让服务器去访问”。

当服务端处理URL时没有严格限制目标地址,攻击者就能让服务器去访问任何它网络可达的地址。最直接的危害是访问内网IP。比如一个只允许内网访问的管理后台、一台没有密码保护的Redis、一套内网监控系统,攻击者自己访问不到,但服务器能访问到。于是SSRF就成为突破网络边界的手段。

我写过一个最简演示环境来理解SSRF(仅限本地靶场测试):

import requests from flask import Flask, request app = Flask(__name__) @app.route('/fetch') def fetch(): url = request.args.get('url') if not url: return 'no url' resp = requests.get(url, timeout=5) return resp.text

访问/fetch?url=http://127.0.0.1:8080/admin,服务端就会拿着自己的身份去请求本机的8080端口,并返回内容。攻击者等于借服务器搭了一个“代理”,还是代理到内网的那种。

4.2 从探测内网到getshell的利用链

SSRF不是单一的攻击手法,而是一条利用链的入口。

第一步是内网端口探测。攻击者通过传不同的URL观察响应差异:返回内容长度、状态码、响应时间。http://192.168.1.1:80和http://192.168.1.1:9999的返回不一样,就能判断哪些端口存活。有些实现还会在异常时返回错误信息,比如“Connection refused”和“Connection timeout”的区别非常明显,直接暴露了端口状态。这一步虽然信息量低,但是后续一切利用的基础。

第二步是访问未授权服务。很多内网服务默认没有鉴权,比如Redis、某些数据库、调试接口。通过SSRF往Redis发命令,在命令可控的情况下,可以写入计划任务或SSH公钥,进而拿到内网机器权限,这就是经典“SSRF打Redis”的利用链。不同协议还可以配合使用,比如gopher://协议可以传递任意TCP报文,能构造出比普通HTTP更复杂的请求;file://协议直接读取服务器本地文件;dict://协议探测端口服务指纹。

第三步是攻击内网其他Web应用。内网Web往往信任内网来源,CSRF和SSRF组合后,攻击者可以用服务器作为跳板,对内部系统发起二次攻击。

我强调的是,整个过程里“探测-定位-攻击”三步缺一不可。很多初学者一上来就想用SSRF访问Redis getshell,但没先摸清楚内网拓扑和端口服务,很容易撞墙。SSRF利用不是无脑打,而是有节奏的信息收集过程。

4.3 各种绕过内网限制的思路

现在很多系统已经知道要过滤内网地址,但过滤方式如果太粗糙,绕过方法也多。我常遇到的有这么几类:

  • IP地址格式变形。127.0.0.1不是唯一写法,http://2130706433/是十进制整数形式,http://0x7f000001/是十六进制形式,http://0177.0.0.1/是八进制形式,甚至http://127.1/在某些解析器下也指向本机。如果代码只用字符串匹配127.0.0.1,全都可以绕过。
  • IPv6映射和缩写。http://[::1]:8080/指向本机,http://[0:0:0:0:0:ffff:127.0.0.1]/是IPv4映射地址,很多过滤只针对IPv4文本格式,处理不了IPv6。
  • URL解析差异。http://user@password@host/的语义在不同语言里不一样,@前面的部分可能被当成用户信息忽略掉。还有http://evil.com#@127.0.0.1/这类写法,如果服务端取host的方式和底层请求库不一致,就会出现“校验的是A地址,实际请求的是B地址”。
  • 重定向。攻击者先访问自己的域名,该域名302跳到内网地址,如果服务端自动跟随重定向,就绕过了初始校验。
  • DNS Rebinding(DNS重绑定)。攻击者域名第一次解析返回公网IP,通过校验后,实际请求时再次解析解析到内网IP。这种绕过需要自己控制DNS服务器,成本高一些,但对付“请求前解析一次就完事”的校验很有效。

工具上,Burp Suite的Collaborator适合用来验证外带(Out-of-band)请求;如果接口返回里能体现请求结果,用FFUF配合一个字典批量测试内网常见端口也很顺手。但无论如何都要记住:这些测试必须在授权的靶场或测试环境里进行。

4.4 防御SSRF的层次化方案

SSRF防御不能靠一道墙,而是多层叠加:

  • 限制协议,业务只需要HTTP,就别开放file、gopher、dict这些协议。
  • 地址解析后校验。拿到URL后先解析出Host,再做一次DNS解析,把IP和端口都做白名单判断。尤其注意解析结果不能是内网网段、环回地址、链路本地地址。
  • 禁止跟随重定向,或者跟随重定向后重新执行一次校验。这是很多人容易漏的点:第一次检查通过了,重定向跳到一个内网地址就没人管了。
  • 出口ACL收严。就算应用层防护被绕过了,也不能让服务器真的能访问内网任意角落。在云网络或防火墙层面,限制服务器只允许访问必要的公网地址,内网段一律禁止。
  • 如果业务确实需要拉取外部图片,可以统一走一个代理服务,由代理做白名单,业务出口不直接暴露。

还有一层容易被忽略:给内网服务本身“上锁”。Redis设置密码和绑定网卡、数据库做最小权限、内网管理后台强制认证——就算SSRF突破了网络边界,也进不了服务。安全建设讲究纵深,SSRF不会成为唯一的救命稻草。

5. 常见问题与排查技巧实录

5.1 判断一个接口是否存在这三类漏洞的检查清单

我判断接口时有一套固定的排查顺序,分享出来供参考:

  • 看响应头:Web应用是否把用户可控参数拼接到任何Header里?如果是,测CRLF注入。
  • 看待校验的操作:有没有不校验Token、直接改密码/发邮件/删除资源的接口?看Cookie的SameSite属性是什么,能否从跨域自动发出请求,测CSRF。
  • 看是否有URL参数被后端拉取:图片、视频、PDF、回调、预览类功能里有没有?url=、?target=、?next=这类参数?如果有,测SSRF。
  • 看报错信息:凡是地址解析失败或请求失败时报出主机名/IP/端口的地方,都值得多看一眼。

这个清单适合从头到尾过一遍目标系统,也适合做代码审计时对照查。

5.2 摸不到规律时怎么用工具辅助

手工构造是基本功,但效率高的还是工具辅助。我用Burp Suite时会把请求扔到Repeater,修改一个值看响应变化。判断CRLF注入时,直接在Header参数里带上%0d%0aX-Test: 1,看返回头里有没有多出来的X-Test。判断CSRF时,一个页面里放好自动提交的form,本地起HTTP服务做验证。判断SSRF时,用我自己写的一个简单脚本批量探测内网IP段:

import requests base = "http://192.168.1.%d:8080/login" for i in range(1, 255): url = "http://192.168.1.{}:8080/login".format(i) try: r = requests.get(url, timeout=3) if r.status_code != 502 and r.status_code != 504: print("[+] alive:", url, r.status_code, len(r.content)) except Exception as e: pass

脚本的逻辑很简单:遍历内网小C段,通过状态码和返回长度差异判断端口服务是否存在。只有在授权环境里我才会这么跑,否则就是违法探测。

5.3 这几个漏洞在实战中的高频组合

CRLF、CSRF、SSRF单独出现当然容易识别,但实战里更常见的是组合:

  • SSRF + 内网Redis:通过SSRF进入内网,用gopher协议向Redis发命令,最后拿到机器权限。这是SSRF利用链里最具杀伤力的一条,也是为什么安全团队对内网服务做SSRF防御如此在意。
  • CSRF + 内网管理后台:管理后台一般在内网,或者信任来源。攻击者如果能在目标用户的浏览器里触发一个CSRF请求,而这个请求的接收端恰好是内网管理后台,可能导致管理操作被恶意触发。
  • CRLF + 缓存投毒:注入响应头配合CDN缓存,让正常用户浏览到被污染的内容,单点攻击变成污染面攻击。

组合利用的排查难度在于,每个环节单独看好像问题不大,连起来就成了完整链路。防御端要做的是一层一层收口,攻击端则要顺着数据流找弱信任点。

5.4 安全意识养成:测试红线与实操感悟

最后说几句题外话。这三类漏洞的利用手法学起来很快,但有件事比技术更重要——边界感。没有授权随便对线上的、别人的系统测试,性质是完全不同的。新手如果要练手,建议在本地自建靶场环境里实验,或者参加主流的CTF比赛,又或者只在拿到了书面授权的众测项目里测试。

我带过的新人里,最容易栽跟头的不是不会构造payload,而是拿到一个目标什么都往上怼,不考虑这个流量是否影响了别人。我自己的习惯是动手前先写清楚测试范围和测试计划,每一步操作都留下记录。安全圈的长期玩家,靠的都是克制和严谨,不是谁的工具多、谁的脚本狠。这里的尺度把住了,技术才能真正用在正道上。

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

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

立即咨询