☰
CSRF与SSRF实战:从原理绕过到内网渗透利用
2026/10/20 9:06:16 网站建设 项目流程

做Web安全做久了都会有一个感觉:有些漏洞名头不大,但实战里真能救命,CSRF和SSRF就是典型。这两个洞都属于“通用漏洞”,跟具体语言、具体框架没有强绑定,只要架构里有服务端、有请求流转,就可能踩中。特别是SSRF,表面看只是“服务器帮你去请求了一个URL”,但在攻防场景里,它往往是从外网打进内网的第一块跳板,配合协议玩法还能直接打穿内网应用。CSRF则更隐蔽,扫描器基本扫不出来,全靠人工看业务逻辑,一旦找到利用点,就能借用户身份做一些他自己根本不知道的操作。

这篇文章我想把这两个漏洞放在一起讲,围绕它们的原理、绕过思路、协议利用和内网探针的实战经验展开。适合正在做渗透测试、攻防演练、安全开发或者是想搞懂漏洞本质的读者。我不会只贴Payload,会尽量把“为什么这么做”和“踩过哪些坑”一起写清楚,这样你在授权测试或者自己搭环境复现的时候,能少走很多弯路。

1. 通用漏洞的定位与思维模型

1.1 为什么CSRF和SSRF被称为“通用漏洞”

先解释“通用”这两个字。SQL注入要求拼接查询、XSS要求输出点可控、反序列化要求目标使用了特定的序列化机制,多多少少都有语言或框架的门槛。CSRF和SSRF不是这样,它们属于“逻辑型漏洞”,只要程序里存在“浏览器代用户发起请求”或“服务器代用户发起请求”这两种行为,就可能出问题,跟你用的是Java、PHP还是Python完全没有关系。

拿CSRF举例,无论前端是React、Vue还是传统JSP,只要后端通过Cookie或者Session识别登录态,且对敏感操作没有做来源校验,攻击者就能构造一个第三方页面,诱导浏览器带着用户的身份凭证去提交请求。这个问题的根源不在某一段代码,而在HTTP协议本身的“无状态”设计——服务器默认“携带合法Cookie的请求就是用户本人发的”。

SSRF的“通用”更彻底,它本质上是“服务端跳板”。大量业务功能需要服务器主动去请求某个地址,比如图片处理、URL预览、Webhook回调、文档转换,这些功能只要对传入的URL过滤不严,攻击者就能让服务器去访问任意地址,包括内网IP、本机回环地址,甚至任意端口。发起请求的主体是服务器,所以流量自带“外网主机”的身份,这让很多只针对内网开放的服务一下子暴露在攻击者面前。

1.2 借刀杀人的攻击思维

这两个漏洞有一个共同的气质:攻击者并不是直接伤害目标,而是利用信任关系“借”了一把刀。

CSRF借的是浏览器的信任。浏览器把用户的Cookie当成身份凭证,攻击者要做的是让这个凭证在不知情的情况下替自己办坏事。这个过程很像“陌生人拿你的身份证复印件,去银行代办业务”——银行认的是身份证,至于业务是不是你本人办的,银行没验证。

SSRF借的是服务器的信任。内网服务通常认为,能连上来的都“默认可信”,因为内网IP本身也是一种信任边界。攻击者控制Web应用发起的请求,在内网服务看来就是一台受信任的主机发来的请求。这个思维可以类比成“你让小区保安帮你去邻居家敲门看看有没有人”——邻居听到保安的声音就开门了,完全没意识到背后是谁在指挥。

理解了这个模型,后面看具体利用手法就会快很多。CSRF的重点是怎么骗过浏览器自动发请求、怎么绕过各类校验;SSRF的重点是怎么控制请求的目标地址、怎么选取合适的协议来扩大伤害面。

2. CSRF漏洞的原理与实战绕过

2.1 从浏览器机制说起

CSRF能成立的前提是浏览器会自动携带与目标域名相关的Cookie。当你访问攻击者构造的页面时,页面里藏了一个指向目标站点的请求,浏览器不会区分这个请求是用户主动点的还是页面自动触发的,它只负责带上Cookie发出去。

最简单的GET型CSRF,一段HTML就够了:

<img src="http://target.com/changePassword?newPass=hacked">

用户一旦在登录状态下打开了这个页面,浏览器就会自动请求上面这个URL,密码直接被改成攻击者指定的值。这类写法的思路就是:只要目标系统的敏感操作是用GET完成的,且没有校验Referer或Token,就能用图片标签、script标签、iframe这些“无需用户点击就能自动发起请求”的标签触发。

POST型CSRF稍微麻烦一点,但也就是一个自动提交表单的距离:

<form action="http://target.com/user/update" method="POST"> <input name="email" value="attacker@evil.com"> </form> <script>document.forms[0].submit();</script>

表单提交会自动带上Content-Type为application/x-www-form-urlencoded的POST请求,浏览器同样会携带Cookie。很多开发觉得“POST请求比GET安全”,实际上如果没有Token校验,POST和GET在CSRF面前没有本质区别。

2.2 实际场景中的绕过技巧

真实环境里的CSRF不会像教科书那么好打,尤其是现在框架默认加了防护之后,我总结一下常见的校验方式和对应的绕过思路。

第一种是Token校验。最常见的是把Token放在请求参数里,同时存在Cookie或者Session中。这种如果Token值不随用户变化、不绑定会话,就可以直接“借用”别人请求里的Token去构造PoC,也就是把Token硬编码进攻击页面。如果Token绑定了会话,就要先看有没有办法“预取”或者通过JSONP等接口跨域拿到半合法Token。很多系统在登录后的首页或初始接口会把Token输出到全局变量,攻击者如果能通过子域或者CORS配置错误碰巧读到,就绕过去了。

第二种是SameSite策略。Chrome默认把Cookie的SameSite设为Lax之后,跨站POST不再携带Cookie,但注意Lax模式下跨站GET在某些条件下仍会携带Cookie。另外,SameSite只对“顶层导航”和“跨站请求”生效,如果攻击者在自己的域名下通过iframe引用目标接口,部分老浏览器或异常配置仍然不会拦。遇到过一些系统,虽然主域Cookie加了SameSite=Lax,但子域或者CDN节点上的接口并没有继承同样严格的安全头,这时候找到子域的一个JSONP接口或者一个可控跳转接口就能把链路打通。

第三种是请求格式限制。很多后端把接口写成只接受Content-Type为application/json,这时候自动提交表单没法直接用,可以尝试把请求改成text/plain,再观察后端是否仍然能解析其中的JSON体。某些PHP老版本忽略Content-Type的边界时就存在这种绕过;另外还可以把POST请求改成GET或PUT,如果后端路由本身是宽松定义的,这种“方法覆盖”就能绕过校验。

2.3 挖掘与排查CSRF的几条思路

自己挖CSRF的时候,我习惯先做这么几件事:

  • 登录系统,用抓包工具把每个敏感操作的请求翻一遍,标记出哪些接口没有自定义Header、没有Token参数、也没有校验Referer。
  • 看Cookie里设置的SameSite属性是什么。如果完全没设置,老版本浏览器默认是None或者Strict兼容模式,你就要实测到底哪种请求能带上。
  • 关注“查询类接口”和“写操作接口”之间的URL规律,不少系统把参数校验写在GET上,POST反而没有,可以通过修改请求方法绕过。
  • 观察Token是否存在于GET参数里。如果Token出现在URL上,大概率会泄漏到Referer或日志中,参考价值很高。

排查的时候还要注意一个细节:CSRF只能发起请求,不能读取响应。所以就算利用成功了,你看到的“成功页面”也是受害者的视角,攻击者这边通常只能通过“业务是否发生变更”来反推。比如密码改了原来的密码能不能登录、邮箱改了之后有没有收到重置邮件,这些侧信道信息才是判断利用是否成功的依据。

3. SSRF漏洞原理与内网探针

3.1 SSRF的触发面

SSRF的核心是:服务端替用户发起了请求,而请求地址可以被用户控制。审计的时候,我会优先盯下面这几种功能点:

  • 图片处理与下载:用户上传图片URL,服务器远程拉取并生成缩略图。
  • URL预览与链接解析:聊天系统、邮件系统中的链接卡片预览。
  • PDF生成:传入URL,服务端调用渲染器转成PDF。
  • Webhook回调配置:填写回调地址,服务器定时或事件触发后请求该地址。
  • 弹窗分享:输入一个网址,服务端去抓取页面标题和描述。
  • 文件导入:导入XML、CSV时支持远程引用外部资源。

这些功能都有一个共同特征:用户输入里含有URL,且由后端发起请求。你只需要把这些URL改成内网地址或本机回环地址,就能探测出目标内网开放了哪些服务。

我早期做审计的时候,最喜欢找的就是那种“关键词搜索无结果时展示第三方图片”的功能。很多搜索系统有一个“图片代理”接口,作用是把搜索结果的图做缓存,参数里直接带原图URL,我只需要把原图URL换成一个内网端口,就能通过响应时间、报错信息判断端口开放情况。

3.2 内网探针的探测技巧

拿到SSRF之后,第一步不是急着打内网业务,而是先做端口和主机的探测。SSRF探测内网跟传统扫描器不太一样,你不能直接发TCP包,只能用HTTP请求、协议返回和响应时间做侧信道判断。

常见的内网网段就这么几段:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16。先拿最小网段试,例如从一个看起来像内网的IP或者一个已知的管理后台IP开始试。如果响应没有任何反馈,再扩大范围。

探测开放端口时,我习惯先扫高频端口:22、80、443、3306、6379、9200、2181、8080、8888、27017。不同情况下的反馈区别比较明显:

  • 端口完全关闭:连接被重置,速度快,报错信息里常常出现“Connection refused”。
  • 端口开放但未监听HTTP:HTTP请求发过去后长时间等待,最终超时或返回空内容。
  • 端口开放且是HTTP服务:直接能拿到响应头、状态码、页面标题。
  • 端口被防火墙静默丢弃:请求发出后没有响应,和超时看起来一样,但总时长比开放端口更长。

如果用dict协议去探测,会更有意思。dict协议本身会先发一个命令,服务端如果支持就会回banner,不支持则会返回错误。这样即使目标端口没有HTTP服务,只要它是一台有banner的IP服务(比如Redis、Memcached、SSH),你也能通过返回内容直接判断服务类型和版本。

3.3 确认SSRF的几种验证方法

我不建议一上来就扫内网,先稳健地确认漏洞是否存在。最干净的办法是准备一台自己能完全控制的公网服务器,或者用专门接收HTTP请求的服务,把SSRF的URL指向那里,如果接收端能看到来自目标服务器的请求,就说明出网请求已发出。

确认漏洞存在后,再去验证它能访问哪些协议。比较通用的验证路径是:

  1. http://127.0.0.1看能否访问到本机Web服务。
  2. file:///etc/passwd看能否读取本地文件。
  3. 用dict://127.0.0.1:6379探测内网Redis是否回Banner。

这三个验证点分别对应了:内网HTTP探测能力、本地文件读取能力、内网非HTTP服务探测能力。只有把这三类能力都测清楚,才能规划下一步的攻击路径。

4. 协议玩法深度拆解

4.1 各类协议的利用差异

SSRF的威力很大程度上取决于后端代码使用的库支持哪些协议。同样是发起请求,PHP的cURL默认支持http://、https://、file://、dict://、gopher://、ftp://;Java原生的HttpURLConnection则更保守,默认只能访问HTTP和HTTPS,但部分框架封装后也可能支持其他协议。这一步必须先确认语言和框架,再决定用哪种协议。

常见的协议利用方式如下:

  • http://:常规HTTP请求,可以探测内网Web服务、直接攻击内网HTTP应用。
  • file://:读取服务器本地文件,Linux下看/etc/passwd、配置文件;Windows下看C:\Windows\win.ini,在知道了真实路径后还能读源码。
  • dict://:发送TCP命令,常用于端口指纹识别,因为很多TCP服务在连接后会自动返回Banner。
  • gopher://:可以构造任意TCP字节流,等于让SSRF变成一个“可编程的Socket客户端”,对内网非HTTP协议的攻击主要靠它。
  • ftp://:可以尝试匿名登录内网FTP,或者用它做端口探测。

4.2 gopher协议的核心原理与威力

gopher协议当初设计出来是为了浏览文本菜单用的,但它的URL格式在安全利用上给了极大的自由度。gopher://host:port/_后跟的数据会被当作TCP数据直接发给目标端口,这意味着你可以自己拼一个完整的HTTP请求、Redis命令、MySQL登录包,任何基于TCP的协议都能被模拟。

构造gopher包有个核心难点:数据编码。直接把原始字节放进URL会被各种解析器拆掉,尤其是\r\n这类控制字符。正确做法是先把要发的TCP数据做一次URL编码,然后根据实际情况再做一次编码,因为不少后端框架会对URL再做一层解码处理。

以前段时间常见的Redis利用为例,Redis天然支持文本协议,未授权时可执行任意命令。利用过程可以拆解如下:

  1. 准备一段Redis命令,比如设置定时任务、写SSH公钥、拉取恶意模块。
  2. 把这段命令按Redis的RESP格式整理,即每条命令由*参数数量开头,每个参数以$长度开头,后跟参数内容。
  3. 把整段原始字节做一次URL编码。
  4. 构造gopher://127.0.0.1:6379/_加编码后的数据。
  5. 通过SSRF触发,让目标服务器带着编码后的gopher请求去访问本机的Redis端口。

这里有个很容易犯的错:忘了处理编码导致命令里的空格或换行符被服务器截断。我习惯先写一个小脚本在本地生成编码数据,然后在可控环境下先复现一遍,确保数据包可用,再拿到目标环境上去打。实战中一旦Redis返回了+OK之类的响应,SSRF页面上通常不会直接显示,但可以通过一些回显技巧间接确认。

4.3 不同语言环境下的协议支持

这个点每次都要强调,因为很多人卡在“为什么同样的Payload在别人的环境能用,我的环境就是不行”。我整理一张表方便对照:

语言/库支持的协议利用潜力
PHP cURLhttp, https, file, dict, gopher, ftp极高,几乎能覆盖所有玩法
Java HttpURLConnectionhttp, https偏低,但可以尝试重定向绕协议限制
Python requests库http, https偏低,默认不支持file/gopher
Python urllibhttp, https, file中低,支持file读取但难以打非HTTP服务
Go net/httphttp, https偏低
Ruby open-urihttp, https, file中低

如果是Java环境遇到重定向,很多库会默认跟随跳转,这时可以用一个http://的302跳转到file://或gopher://,达到协议切换的效果。这个方法同样适用于某些封堵非HTTP协议的过滤逻辑,核心是“服务端第一次校验域名,重定向后的地址不再校验”。

5. 从外网打向内网的完整利用链路

5.1 攻击链路整体设计

SSRF单点利用率再高,也只是突破口。真正有价值的利用链是:外网Web存在SSRF,利用它做内网探针,找到内网可达的高价值服务,再用协议玩法打穿其中一个服务,最后以内网某台机器为跳板横向移动。

在攻防演练里,我最常用的一条链路是:

  1. 外网Web应用存在SSRF,触发点是一个图片URL预处理接口。
  2. 用dict://探测内网多个IP的Redis端口,确认某台内网主机开放6379。
  3. 确认Redis存在未授权访问,通过gopher://写入SSH公钥或者计划任务。
  4. 建立会话后,在内网做二次探测,拿下更多内网系统。

整条链路看着不算复杂,但每一步都有不少细节。下面展开讲第二、第三步的实操过程。

5.2 内网服务利用的实操步骤

第一步:确认内网网段与存活主机SSRF没有太多探路手段,只能用穷举。假设前面探测出目标内网使用10.0.0.0/16这样的网段,先用少量IP做端口探测。因为SSRF发请求比较慢,全量扫不现实,我的策略是先扫网关、常见网管的IP末位,比如10.0.0.1、10.0.0.2这类通常分配给路由和管理机的地址。如果某台机器返回了Banner或HTTP响应头,再顺藤摸瓜扫同一C段。

第二步:利用dict协议探测Redisdict://10.0.0.2:6379/info可以直接把info命令交给Redis执行,如果返回了$...之类的块格式和一堆Redis配置信息,基本就是未授权访问状态。这时候记录下Redis版本号,版本越老,利用成功率越高;新版本Redis默认开启了Protected Mode,只允许本机访问,就需要先找一个SSRF能访问到“Redis本机”的路径,比如让服务器请求自己的回环地址。

第三步:构造gopher命令写入计划任务以Linux系统为例,Redis写计划任务的本质是让Redis把一个cron条目写入系统计划任务文件,再由系统的cron服务定时执行。整个过程通过Redis的SET和CONFIG SET dir/dbfilename实现。构造编码的时候,记得把整个Redis命令流按RESP格式组织好,然后用脚本编码成gopher包。请求长一点也没有关系,很多SSRF接口没有对URL长度做严格限制。

第四步:处理回显与验证SSRF通常不直接返回内网响应,所以这一步主要靠“盲打”。我的做法是:先让Redis执行一个无害命令,比如SET testflag 1,如果后面能通过dict协议读回这个key,说明命令已经执行成功,再替换成计划任务写入命令。这样分步验证比一次性丢一个大payload稳得多,也方便定位是哪一步编码出了问题。

5.3 利用链路上的注意点

实操中容易翻车的地方集中在编码、协议可达性和权限三方面。

编码方面,最重要的一条:gopher协议里_后面跟的数据,服务端不会自动做URL解码,编码和解码的时机差一层就会导致Redis收到乱码命令。我的办法是写个本地Python小脚本,把gopher://host:port/_作为整体生成,然后用浏览器或抓包工具确认最终的访问URL长什么样,确保万无一失。

协议可达性方面,真实内网不一定只有Redis,有时候扫出来的是无密码的MongoDB,或者是可匿名登录的FTP,这时候gopher依然能用,只是Payload要换成对应协议的格式。有些服务不按常理出牌,比如直接TCP连接时无banner,但发送指定命令后才返回,这时用dict就是贻笑大方了,得换gopher构造完整交互。

权限方面,即使Redis命令执行成功,也可能因为运行Redis的账号权限不足,写入不了系统目录,这时候可以尝试写当前用户可写的目录,比如Web目录或者临时目录。绝大多数情况下,通过计划任务写入并等待cron执行需要1分钟左右,这个等待窗口里可能出现误判,建议在写入后多等一轮,确认没有其他报错再收工。

6. 常见问题与排错经验实录

6.1 典型问题速查表

问题现象可能原因排查与处理建议
SSRF请求内网IP时全部超时目标机器防火墙静默丢弃,或存在出网限制用多个内网IP做对比,确认是否有特定网段可通;观察请求总时长判断
dict协议探测Redis无响应Redis版本开启保护模式,或banner不主动返回改用gopher协议发送PING命令验证,或尝试访问回环地址绕过保护
gopher执行Redis命令无效果URL编码层数不对、RESP格式错误先在本地Python环境模拟Redis服务,把生成的gopher包完整复现一遍
SSRF只支持http协议后端用Java/Go等保守库尝试302重定向绕协议限制,或者在HTTP请求中注入换行构造TCP数据
CSRF PoC明明设置了表单却未执行目标接口校验了Content-Type或自定义Header改用fetch或XMLHttpRequest构造,观察请求样式是否与正常请求一致
Token校验一直绕不过Token绑定会话且参与服务端校验转找同域JSONP接口或CORS配置错误,先拿到Token再构造请求

6.2 我的实战心得

做了不少漏洞挖掘和攻防项目之后,我对这两个漏洞的体会是:CSRF的难点在“发现”,SSRF的难点在“转化”。

CSRF想靠扫描器发现基本不现实,因为扫描器不理解业务逻辑,它不知道“修改邮箱”和“修改密码”哪个接口更敏感。我一般选择手工把整个系统的“高危操作”梳理出来,形成一个清单,再针对清单做校验机制分析。做分析的时候要特别注意“前置校验”和“二次确认”页面的差异。有些系统在修改手机号的流程里做了两步校验,第一步是发验证码,第二步是提交新手机号,攻击者如果只看第一步可能觉得“有验证码,打不了”,但仔细看第二步接口,可能根本没有校验验证码和Token,直接提交就是漏洞。

SSRF的难点在于“拿到SSRF之后怎么变现”。很多人扫了一圈内网没发现Redis、没发现未授权MongoDB就放弃了,但实际上内网里还有大量的HTTP管理后台、Spring Boot Actuator、Jenkins、GitLab这类服务。SSRF的一个独特优势就是“它替你带着内网的身份去访问”,很多内网管理系统没有强认证,只要来源是内网IP就直接放行。这时候优先打那些“只要内网可达就能操作”的Web服务,往往比挑战高危中间件协议要轻松得多。

最后分享一个细节:处理SSRF时,如果应用层有“禁止IP”之类的过滤,优先看看它过滤的是不是字符串。比如拦截了127.0.0.1,但没拦截127.0.0.0的回环地址别名;拦截了localhost,但没拦截localtest.me这类域名;拦截了点分十进制IP,但忘了还有十六进制和整数IP表示法。这些都是老生常谈的绕过方式,但在真实代码里命中率比我预想的高很多。

挖漏洞和技术研究是长期积累的过程,一次两次的失败不要紧,关键是把每次踩坑的编码细节、版本细节和业务判断记下来,下一次遇到类似的场景,你的判断速度会快得多。这两个“通用漏洞”不算难,但用好了,绝对是你实战工具箱里最顺手的两个工具。

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

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

立即咨询