做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 srchttp://target.com/changePassword?newPasshacked用户一旦在登录状态下打开了这个页面浏览器就会自动请求上面这个URL密码直接被改成攻击者指定的值。这类写法的思路就是只要目标系统的敏感操作是用GET完成的且没有校验Referer或Token就能用图片标签、script标签、iframe这些“无需用户点击就能自动发起请求”的标签触发。POST型CSRF稍微麻烦一点但也就是一个自动提交表单的距离form actionhttp://target.com/user/update methodPOST input nameemail valueattackerevil.com /form scriptdocument.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加了SameSiteLax但子域或者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”。端口开放但未监听HTTPHTTP请求发过去后长时间等待最终超时或返回空内容。端口开放且是HTTP服务直接能拿到响应头、状态码、页面标题。端口被防火墙静默丢弃请求发出后没有响应和超时看起来一样但总时长比开放端口更长。如果用dict协议去探测会更有意思。dict协议本身会先发一个命令服务端如果支持就会回banner不支持则会返回错误。这样即使目标端口没有HTTP服务只要它是一台有banner的IP服务比如Redis、Memcached、SSH你也能通过返回内容直接判断服务类型和版本。3.3 确认SSRF的几种验证方法我不建议一上来就扫内网先稳健地确认漏洞是否存在。最干净的办法是准备一台自己能完全控制的公网服务器或者用专门接收HTTP请求的服务把SSRF的URL指向那里如果接收端能看到来自目标服务器的请求就说明出网请求已发出。确认漏洞存在后再去验证它能访问哪些协议。比较通用的验证路径是http://127.0.0.1看能否访问到本机Web服务。file:///etc/passwd看能否读取本地文件。用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天然支持文本协议未授权时可执行任意命令。利用过程可以拆解如下准备一段Redis命令比如设置定时任务、写SSH公钥、拉取恶意模块。把这段命令按Redis的RESP格式整理即每条命令由*参数数量开头每个参数以$长度开头后跟参数内容。把整段原始字节做一次URL编码。构造gopher://127.0.0.1:6379/_加编码后的数据。通过SSRF触发让目标服务器带着编码后的gopher请求去访问本机的Redis端口。这里有个很容易犯的错忘了处理编码导致命令里的空格或换行符被服务器截断。我习惯先写一个小脚本在本地生成编码数据然后在可控环境下先复现一遍确保数据包可用再拿到目标环境上去打。实战中一旦Redis返回了OK之类的响应SSRF页面上通常不会直接显示但可以通过一些回显技巧间接确认。4.3 不同语言环境下的协议支持这个点每次都要强调因为很多人卡在“为什么同样的Payload在别人的环境能用我的环境就是不行”。我整理一张表方便对照语言/库支持的协议利用潜力PHP cURLhttp, https, file, dict, gopher, ftp极高几乎能覆盖所有玩法Java HttpURLConnectionhttp, https偏低但可以尝试重定向绕协议限制Python requests库http, https偏低默认不支持file/gopherPython 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利用它做内网探针找到内网可达的高价值服务再用协议玩法打穿其中一个服务最后以内网某台机器为跳板横向移动。在攻防演练里我最常用的一条链路是外网Web应用存在SSRF触发点是一个图片URL预处理接口。用dict://探测内网多个IP的Redis端口确认某台内网主机开放6379。确认Redis存在未授权访问通过gopher://写入SSH公钥或者计划任务。建立会话后在内网做二次探测拿下更多内网系统。整条链路看着不算复杂但每一步都有不少细节。下面展开讲第二、第三步的实操过程。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表示法。这些都是老生常谈的绕过方式但在真实代码里命中率比我预想的高很多。挖漏洞和技术研究是长期积累的过程一次两次的失败不要紧关键是把每次踩坑的编码细节、版本细节和业务判断记下来下一次遇到类似的场景你的判断速度会快得多。这两个“通用漏洞”不算难但用好了绝对是你实战工具箱里最顺手的两个工具。
阅读完成 · 觉得有帮助?