法律边界声明(请务必先读)
本文所有操作均在本人搭建或公开授权的靶场环境中完成,仅用于学习漏洞原理。
未经授权对他人系统进行 CSRF、SSRF 等测试,违反《中华人民共和国网络安全法》,
情节严重的触犯《刑法》第 285 条、第 286 条,是刑事犯罪。
请只在自己的环境或明确授权的靶场里练习。
为什么接着学这两个
上一篇写的是 SQL 注入和 XSS,这两个漏洞的共同点是:攻击者往目标里塞东西。
CSRF 和 SSRF 不一样,它们的关键不是"塞进去什么",而是"让谁去干活":
- CSRF 是让受害者自己的浏览器替攻击者发请求
- SSRF 是让服务器自己替攻击者发请求
一个是借用人的身份,一个是借用机器的位置。搞清楚这个区别,这两个漏洞就通了。
靶场与实验环境
| 靶场 | 特点 | 适合阶段 |
|---|---|---|
| 皮卡丘(Pikachu) | CSRF、SSRF 各有一关,中文,一个页面一类漏洞 | 入门,先把流程跑通 |
| DVWA | CSRF 分 low / medium / high / impossible 四档 | 进阶,想看防御怎么被绕过时必刷 |
| 自己写的最小 demo | Flask 或 Express 十分钟搭一个,能看服务端日志 | 最推荐——理解本质靠这个 |
我一开始贪快只刷靶场,结果卡了两天没搞懂 CSRF 为什么"能成立"。后来自己写了个最小 demo,把服务端收到的每个请求打印出来,五分钟就想通了。能打印日志的环境比任何靶场都值钱。
一、CSRF
1.1 一句话原理
浏览器会自动带上目标站点的 Cookie,而服务端没法区分这个请求是用户自己点的、还是第三方页面偷偷发的。
所以 CSRF 成立需要三个条件同时满足:
| 条件 | 说明 | 缺了会怎样 |
|---|---|---|
| 凭据自动携带 | 鉴权靠 Cookie(或 Basic Auth),浏览器自动带上 | 用 Bearer Token 存 localStorage 就天然免疫 |
| 参数可预测 | 请求的参数能被攻击者提前构造出来 | 有随机 nonce 且强校验就挡掉 |
| 无二次校验 | 服务端不做 CSRF Token、Origin、验证码等检查 | 任意一道加上就大幅缓解 |
三个条件拆开看都很朴素,但同时满足的场景非常多——几乎所有传统表单站点都符合。
1.2 为什么说它"打的是人"
这是理解 CSRF 最关键的一步。
SQL 注入打的是数据库,XSS 打的是其他用户的浏览器,而 CSRF 打的是受害者本人的身份和权限。
服务器视角看到的请求是这样的:
1 | POST /user/password HTTP/1.1 |
这个请求在协议层面完全合法——Cookie 是真的,Session 是有效的,格式没问题。服务器没有任何理由拒绝它。
唯一的问题是:这个请求不是用户想发的,是用户访问了攻击者页面后,被那段页面"替"他发的。
1.3 同源策略到底限制了啥
很多人以为"有同源策略所以跨站发不了请求",这是个经典误解。
同源策略限制的是读取响应,不是发送请求:
| 行为 | 是否受限 | 说明 |
|---|---|---|
| 跨站发 GET 请求 | ❌ 不受限 | <img src>、<script src>、<link> 随便发 |
| 跨站发 POST 表单 | ❌ 不受限 | <form action="跨站"> + JS 自动 submit 就行 |
| 跨站读响应内容 | ✅ 受限 | 拿不到响应体,只能通过时序/侧信道猜 |
| 跨站发 JSON(带自定义头) | ✅ 触发预检 | Content-Type: application/json 会先 OPTIONS |
关键结论:浏览器允许跨站"写",不允许跨站"读"。
而 CSRF 只需要"写"——改密码、发帖、转账、加管理员,全是写操作,根本不需要读响应。所以同源策略对 CSRF 几乎没有防护作用。
1.4 GET 型 CSRF:最容易理解的起点
最原始的形态,一个 img 标签就够了:
1 | <!-- 攻击者页面 evil.html --> |
页面一加载,浏览器就去请求那个 URL,并且自动带上 victim.com 的 Cookie。用户看不到任何东西(图片尺寸为 0),删除就已经执行完了。
这种漏洞现在少见了,因为:
- 现代框架默认要求 POST
SameSite=Lax会阻止跨站的非顶级导航请求带 Cookie
但它仍然是理解 CSRF 最直观的例子——"一个 src 属性就是一次攻击"。
1.5 POST 型 CSRF:最常见的实战形态
POST 表单同样可以跨站提交,这是 CSRF 最主要的形态:
1 | <form action="http://victim.com/user/password" method="POST" id="f"> |
要点:
- 表单的
action可以是任意域名,浏览器不会拦 submit()会自动跳转,页面一变用户就发现了——所以实战中攻击者会提交到iframe里,页面看起来毫无变化- 更隐蔽的做法:
target指向一个隐藏 iframe
1 | <iframe name="sink" style="display:none"></iframe> |
这样用户完全感知不到,密码就被改了。
1.6 JSON 型:一个流传很广的错误认知
网上常见说法:"我的接口是 JSON 的,不是表单,所以不怕 CSRF"。
这个说法是错的。
它的依据是:Content-Type: application/json 属于非简单请求,会触发 CORS 预检(OPTIONS),而预检通不过就没法发。
但这里有两个漏洞:
漏洞一:服务端不校验 Content-Type
很多后端的写法是"拿到 body 就 json_decode",根本不管请求头。这时候攻击者可以用表单发过去:
1 | <form action="http://victim.com/api/update" method="POST" enctype="text/plain"> |
enctype="text/plain" 发出去的内容长这样:
1 | {"username":"admin","role":"1","x":"="} |
后端如果宽松解析,或者用正则提取字段,就中招了。
漏洞二:某些接口接受 text/plain / form 格式
只要服务端对多种 Content-Type 都做了兼容(很常见,为了对接老客户端),CORS 预检就被绕过了。
判断方法:抓包看接口是否严格校验 Content-Type,以及是否对不合法的类型直接返回 415。
1.7 防御:按可靠程度排序
① 不用 Cookie 鉴权(最根本)
这条最彻底:如果鉴权凭据不由浏览器自动携带,CSRF 从原理上就不成立。
| 方案 | 凭据位置 | CSRF 风险 | 代价 |
|---|---|---|---|
| Session Cookie | Cookie | 有 | 传统方案,简单 |
| Bearer Token | localStorage / 内存 | 无 | 要防 XSS(XSS 能偷 token) |
| Cookie + CSRF Token | Cookie + 请求体 | 低 | 需要额外实现 |
所以现代 SPA + API 架构天然免疫 CSRF——不是因为他们做对了什么,而是他们根本没给浏览器"自动带凭据"的机会。
代价是:XSS 变成了致命的(能直接偷 token),所以这条路是"防了 CSRF,把风险集中到 XSS"。
② CSRF Token
原理:服务端给每个会话(或每个表单)生成一个随机串,表单提交时必须带上,服务端校验。
实现正确的三个要求:
- 不可预测:用 CSPRNG,不能用时间戳、不能自增
- 绑定用户:token 必须和当前会话关联,不能全局共用
- 至少校验所有"写"操作:包括 GET、包括 AJAX、包括上传接口
常见实现错误:
| 错误写法 | 为什么没用 |
|---|---|
| token 存在 Cookie 里,且校验逻辑是"Cookie 里的值 == 表单里的值" | 攻击者无法读 Cookie,但可以通过别的漏洞(XSS/子域)写——而且这种写法本身容易被绕过 |
| 全局共享一个 token | 一个用户泄露就全站沦陷 |
| 只在"改密码"接口校验 | 攻击者改打"改邮箱""绑定手机"接口 |
| token 校验不严(只查存在性不查值) | 形同虚设 |
③ SameSite Cookie
给 Cookie 加 SameSite 属性,浏览器就不再对跨站请求携带它:
| 值 | 行为 | 防护效果 |
|---|---|---|
Strict | 完全不带 | 最强,但用户从外链点进来会"掉登录" |
Lax | 顶级导航的 GET 带,其他不带 | 推荐默认值,挡住绝大多数跨站 POST |
None | 全都带 | 等于没防(且必须配 Secure) |
Lax 是现在浏览器的默认值,这是为什么很多老的 CSRF 漏洞"自己就好了"。
但它不是万能的:
- 旧的浏览器/App 内嵌 WebView 可能不支持
Lax仍然允许 top-level 的 GET 导航带 Cookie——所以用 GET 做写操作依然是漏洞
④ 校验 Origin / Referer
1 | // 现代浏览器对跨域请求一定会带 Origin 头,而且 JS 改不了它 |
| 头 | 可靠性 | 坑 |
|---|---|---|
Origin | 高——不可伪造,现代浏览器都发 | 同源 GET 导航可能不带 |
Referer | 中——可被 Referrer-Policy 清空 | 必须处理"空值",否则正常用户被误杀或攻击者用空值绕过 |
⑤ 二次验证
改密码要输原密码、转账要短信验证码、高危操作要邮箱确认。
这是性价比最高的一道——因为它防的不只是 CSRF,还有账号被盗后的横向操作。
1.8 几个流传很广的误区
- "上了 HTTPS 就安全了" —— 错。CSRF 不关心加密,HTTPS 只防中间人。
- "我有 CSRF Token 就安全" —— token 本身可能通过 XSS、日志、Referer 泄露,实现错误等于没有。
- "我的接口是 JSON 的" —— 见 1.6,不成立。
- "只有后台才需要防" —— 前台用户的"发帖""改资料""绑定"一样中招。
- "攻击者读不到响应就没危害" —— CSRF 全部是写操作,根本不需要读。
二、SSRF
2.1 一句话原理
服务端接收了一个 URL,然后"替你去请求"它。
典型场景:图片抓取、URL 预览、远程下载、Webhook 回调、代理接口、XML 解析器加载外部实体。
这些功能的共同点是:它们都需要服务器主动发起一个网络请求,而请求的地址来自用户输入。
如果服务端不校验这个地址,攻击者就能把请求指向任何它够得着的地方——包括 127.0.0.1、内网段、云厂商的元数据服务。
2.2 为什么 SSRF 通常比 CSRF 更严重
| 维度 | CSRF | SSRF |
|---|---|---|
| 攻击的是 | 用户的身份 | 服务器的网络位置 |
| 视野范围 | 受害者在目标站的权限 | 服务器能访问的所有内网资源 |
| 能否横向 | 一般不能 | 能——内网穿透、打内网应用 |
| 能否 RCE | 不能 | 能,通过 gopher 打 Redis/FastCGI 等 |
核心差别在于:服务器往往站在内网的门口。
一台 Web 服务器能看到的东西,比你在外网能看到的多得多——内网的数据库、Redis、管理后台、K8s API、云元数据,全都只有它够得着。SSRF 就是把这个"位置优势"借给攻击者。
2.3 皮卡丘 SSRF 实操
皮卡丘的 SSRF 关卡长这样:
1 | http://靶场地址/vul/ssrf/ssrf_curl.php?url=http://127.0.0.1/pikachu/vul/sqli/sqli_str.php |
它用 curl 请求你传入的 url,然后把结果返回。第一关连任何过滤都没有,直接把 url 换成:
1 | ?url=http://127.0.0.1/ |
就能看到内网的首页。换成 file:///etc/passwd(如果 curl 支持)就能读文件。
这一步的意义:你亲眼看到"我人在外网,却拿到了 127.0.0.1 的内容"。这个瞬间是理解 SSRF 的核心。
2.4 危害链:从"读个文件"到"拿下服务器"
SSRF 的价值不是"能读一个文件",而是它是一条通往内网的跳板。递进关系:
| 阶段 | 动作 | 典型目标 |
|---|---|---|
| 1 | 探活 | 内网段扫描:http://192.168.1.1~254,看响应时间/状态码差异 |
| 2 | 读文件 | file:///etc/passwd、file:///proc/self/environ |
| 3 | 打内网应用 | 无认证的 Jenkins、K8s API、Consul、Elasticsearch |
| 4 | 读云元数据 | 拿到临时凭据 → 接管云资源 |
| 5 | 协议走私 RCE | gopher:// 打 Redis 写 SSH key / 写 crontab、打 MySQL、打 FastCGI |
第 4 和第 5 是 SSRF 从"信息泄露"升级为"严重"的分水岭。
2.5 云元数据:SSRF 的最高价值目标
云服务器上有个特殊的本地地址,能拿到实例的临时凭据。这是 SSRF 的"金矿":
| 云厂商 | 元数据地址 |
|---|---|
| 阿里云 | http://100.100.100.200/latest/meta-data/ |
| AWS | http://169.254.169.254/latest/meta-data/ |
| 腾讯云 | http://metadata.tencentyun.com/latest/meta-data/ |
| GCP | http://metadata.google.internal/computeMetadata/v1/(需 Metadata-Flavor 头) |
阿里云的信息泄露路径通常是:
1 | /latest/meta-data/ram/security-credentials/ -> 角色名 |
拿到这组凭据,就等于拿到了绑定在该实例上的 RAM 角色权限——如果角色权限给大了(比如能操作 OSS、能读 RDS),后果很严重。
加固演进:AWS 的 IMDSv2 要求先拿一个 token(PUT /latest/api/token 带 X-aws-ec2-metadata-token-ttl-seconds 头),再带 token 去读。因为 PUT 请求无法用简单的 GET/表单构造出来,所以能挡住大部分基础 SSRF。阿里云也对部分实例做了类似加固。
2.6 绕过技巧:这才是 SSRF 的精髓
防御方常见的做法是"过滤掉 127.0.0.1、过滤掉 192.168."。而绕过方式有一大把:
① IP 地址的多种写法
同一个 127.0.0.1,可以用这些形式表示:
| 写法 | 说明 |
|---|---|
127.0.0.1 | 常规 |
2130706433 | 十进制整数 |
0x7f000001 | 十六进制 |
0177.0.0.1 | 八进制 |
127.1 | 省略中间段(等价 127.0.0.1) |
0 | 在多数系统里等价于 127.0.0.1 |
[::1] | IPv6 回环 |
[::ffff:127.0.0.1] | IPv4 映射的 IPv6 |
为什么有效:URL 解析器和后续的 socket 连接用的可能是两套 IP 解析逻辑。过滤器看到的是字符串,而真正连接时用的是解析后的 IP。
② 利用 DNS 指向内网
比变形更稳的办法——用一个域名,指向内网地址:
1 | http://127.0.0.1.nip.io/ |
这类服务会把子域名里的 IP 直接解析出来。过滤器只看到"域名",但实际连的是 127.0.0.1。
更彻底的是用自己的域名做 DNS Rebinding:第一次解析返回外网 IP(通过校验),TTL 设为 0,第二次解析返回内网 IP(发起真实请求)。这能绕过"先解析后校验"的防御。
③ URL 解析差异
1 | http://evil.com@127.0.0.1/ # @ 前面的被当作用户名 |
不同的语言/库对 URL 的解析规则不一致,防御方校验的和实际请求的可能是两个不同的主机。
④ 302 跳转绕过
防御方校验了 URL 是 http://evil.com/safe(外网,合法),但这个地址返回 302 Location: http://127.0.0.1/。如果客户端跟随重定向,第二跳就打到内网了。
关键:只校验第一跳是无效的,必须校验每一跳,或者干脆禁止跟随重定向。
⑤ 协议走私
除了 http,很多 HTTP 客户端支持(或可被诱导支持)其他协议:
| 协议 | 用途 |
|---|---|
file:// | 读本地文件 |
gopher:// | 任意 TCP 流量——打 Redis / MySQL / FastCGI |
dict:// | 打 Redis(构造简单命令) |
ftp:// | 反弹、绕过 |
http+unix:// | 打 unix socket |
gopher:// 是 SSRF 的终极武器,因为它能发任意字节到任意 TCP 端口。用它打 Redis 的典型思路是:发一条 CONFIG SET dir + CONFIG SET dbfilename + SET payload + BGSAVE,把恶意内容写成 authorized_keys 或计划任务。
(这里不展开完整 payload——靶场里 Gopherus 这个工具能一键生成,自己起 Redis 玩就够了。)
2.7 盲 SSRF:没有回显怎么办
如果目标只返回"请求成功/失败",看不到内容,就是盲 SSRF。
判断方法:
- 时间差异:让目标请求一个"会挂起很久"的地址,对比响应时间
DNSLog 外带:让目标请求
http://你的dnslog子域名/,看 DNS 记录里有没有出现- 国内可用
dnslog.cn、ceye.io - 最稳的是自己起一台 VPS 看 access log——我用的就是这个笨办法,但最可靠
- 国内可用
- 错误信息差异:不同内网端口返回的错误不一样(
Connection refusedvstimeout)
我最开始用公共 DNSLog,发现经常收不到——后来才明白是目标服务器可能有出网限制或者 DNS 被劫持。自建 VPS 加 tcpdump 看包,比什么工具都直接。
2.8 防御:白名单是唯一可靠的
SSRF 防御的困难在于:黑名单永远有绕过方式。所以正确顺序是:
① 白名单(首选)
如果业务允许,直接限定目标域名/IP 白名单——只有已知的安全地址才允许请求。
适用于:固定的第三方 API 对接、固定的图片源。
② 协议白名单
只允许 http:// 和 https://,显式拒绝 file://、gopher://、dict://、ftp://。
③ 解析后校验(关键细节)
错误做法:先校验 URL 字符串里的主机名是不是内网 IP,再去请求。
正确做法:
- 用 DNS 解析出 IP
- 校验这个 IP 是否是内网地址(
127.0.0.0/8、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、169.254.0.0/16、::1、fc00::/7) - 用这个已经解析好的 IP 去请求(而不是再次用域名解析)
第 3 步是防 DNS Rebinding 的核心——解析和请求必须用同一个 IP。
④ 禁止跟随重定向,或对每一跳都校验
如果必须跟随,那每一跳都要重新走一遍上面的校验流程。
⑤ 网络层兜底
- 出口防火墙限制服务器能访问的内网段
- 使用独立的出网代理,代理上做策略
- 容器化 + 网络隔离
网络层是最可靠的——因为代码里的校验总有写漏的一天。
⑥ 云上加固
- 开启 IMDSv2(AWS)/ 元数据加固(阿里云)
- RAM 角色最小权限
- 元数据服务限制为只有特定进程可访问
2.9 一个真实的防御翻车点
见过这样的代码:
1 | if (strpos($url, '127.0.0.1') !== false || strpos($url, '192.168.') !== false) { |
绕过方式:http://127.0.0.1.nip.io/。
字符串里没有 127.0.0.1(虽然子域名里有——等等,其实有)。换个更狠的:http://2130706433/,字符串里既没有 127.0.0.1 也没有 192.168.,但它就是 127.0.0.1。
教训:字符串过滤不是防御。 必须解析成 IP 再判断。
三、两者的本质对比
| CSRF | SSRF | |
|---|---|---|
| 一句话 | 借用户的浏览器发请求 | 借服务器的网络位置发请求 |
| 攻击目标 | 用户的身份/权限 | 服务器的内网可达范围 |
| 需要受害者 | 访问恶意页面(或点链接) | 触发一次服务端请求(传个 URL) |
| 需要浏览器配合 | 需要(自动带 Cookie) | 不需要 |
| 典型入口 | 改密码、转账、发帖、绑定 | 图片抓取、URL 预览、远程下载、Webhook |
| 同源策略有用吗 | 基本没用(只防读不防写) | 无关 |
| 危害等级 | 中 ~ 高 | 高 ~ 严重 |
| 防御核心 | CSRF Token / SameSite / Origin | 白名单 / 解析后校验 / 禁协议 |
| 黑名单有效吗 | Token 混淆有效 | 无效,必然被绕过 |
一个统一的认知
写了这么多,我觉得这两个漏洞背后其实是同一个错误:
混淆了信任边界。
- CSRF:服务端把「浏览器自动带来的凭据」当成了「用户本人的真实意愿」。
凭据能证明"这个人是谁",但证明不了"这个操作是他想做的"。 - SSRF:服务端把「用户提供的地址」当成了「可信的内部目标」。
地址是个字符串,但字符串背后可能是内网。
两者都是在"接受了一个不该直接信任的输入"之后,没有做二次确认。
这也是为什么所有防御都指向同一个原则:
不要相信任何来自外部的输入,哪怕它看起来"格式正确"。
四、工具
| 工具 | 用途 |
|---|---|
| Burp Suite | 抓包改包;Engagement tools → Generate CSRF PoC 能直接生成 PoC |
| dnslog.cn / ceye.io | 盲 SSRF 的 DNS 外带 |
| Gopherus | 一键生成 gopher 协议 payload(Redis / MySQL / FastCGI) |
| SSRFmap | SSRF 利用自动化 |
| 自建 VPS + tcpdump | 盲打回显最可靠的方案 |
| 浏览器 DevTools → Network | 观察 SameSite Cookie 实际有没有被带上 |
五、我的心得
5.1 CSRF 的认知转折
开始时一直想不通:"攻击者又不知道我的密码,怎么能改我东西?"
后来才明白:CSRF 根本不需要知道密码。 它利用的是"你已经登录了"这个状态。浏览器每次请求都会自动带上 Cookie——这个设计本来是为了方便,结果成了攻击面。
想通这一点之后,看所有"自动携带凭据"的设计都会本能地问一句:"这个请求怎么证明是用户主动发起的?"
5.2 SSRF 的认知转折
第一眼看 SSRF:?url=http://127.0.0.1/ 能读到内网首页——"哦,能看个页面而已"。
直到看到 gopher 打 Redis 写 SSH key 的链子,才意识到:SSRF 的真正价值是把 GET 变成了任意协议的 POST,把"外网访问"变成了"服务器视角的访问"。
服务器站在内网门口,你借了它的眼睛和手。
5.3 学习方法的反思
这两个漏洞我都试过"先看文章再打靶场",效率很低——因为文章里的 payload 是"结果",不是"过程"。
真正让我开窍的是自己搭最小 demo:
- 写一个 Flask 站点,登录用 Cookie,提供一个"改昵称"的 POST 接口
- 写一个攻击页面,用 form 自动提交
- 在服务端把每个请求的 Cookie / Origin / Referer 都
print出来
当我在日志里看到那个"带着合法 Cookie、但不是用户发起的请求"时,CSRF 就彻底通了。
SSRF 同理——写个接收 URL 的接口,把 curl 的返回打印出来,然后传各种变形地址,看哪一个能绕过自己写的过滤器。自己写过滤器再自己绕,比刷十个靶场有用。
下一步
Web 这块还欠着几个大头:XXE(和 SSRF 有大量交叉,因为外部实体也能发请求)、反序列化、以及越权——尤其是水平越权,实际项目里比 SQL 注入常见得多。
另外,把这篇的防御部分做成一份自查清单,以后写接口时对着过一遍。