手把手教你实现 XSS 攻击

2,280 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情

XSS 攻击理论知识学习完成的同学可以参考这篇一些攻击实现,理论背的再好还是要实践呀

202212071826163.png

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

攻击分类

XSS 攻击主要可以分为三类

  1. 存储型(持久型)(server端缺陷)
  2. 反射型(server 端缺陷)
  3. DOM 型(浏览器端缺陷)

接下来将从上面的攻击类型分别给出例子

反射型

通常一般教程先讲存储型,但是本篇文章基于实验先讲反射型,因为它和存储型有异曲同工之处,而且更底层

反射型通常出现于以下场景:常见于通过 URL 传递参数的功能,如网站搜索、跳转等

接下来就给出一个网站搜索的例子

下面是一个很常见的 search 页面例子

<!-- search.html -->
<body>
  <form
    action="http://localhost:3000/search"
    method="GET"
    enctype="application/json"
  >
    搜索:<input class="search-input" name="search" type="text" />
    <br />
    搜索内容:
    <br />
    <button class="confirm-button" type="submit">确认</button>
  </form>
</body>

效果如下

IMG

IMG

注意 搜索内容: 这部分的 DOM 内容以及 URL 的参数 search,这两个点其实就是 XSS 攻击的突破口

突破口

上面这个网站是一个由后端渲染的网站,那么它是怎么渲染的呢?

// user.js
router.get("/search", function (req, res, next) {
  const { search } = req.query;
  res.setHeader("Content-Type", "text/html");
  res.send(generateSearchHTML(search));
});
exports.generateSearchHTML = (search = "") => {
  return `
    .....
        搜索内容:${search}
    .....
  `;
};

其中 generateSearchHTML 将基于上面给出的 HTML 模板生成 HTML 文件内容,并返回给用户,并且会将用户输入的搜索内容,即 search 填充的 HTML 模板中,即 搜索内容:${search}

回到 XSS 攻击的本质就是执行恶意代码,换言之就是 <script></script> 的执行,回到上面的模板,如果转换成下面的结构,请问 <script></script> 是否会执行?

<body>
  搜索内容:<script></script>
</body>
<script></script>

不要拘束与传统的三段式写法而以为 <body> 中的脚本不会执行,上面的代码能够顺利执行,因此执行 XSS 攻击的关键就是编写代码到搜索框中,后台返回给用户时即完成 XSS 攻击,实操如下

  1. 输入 <script>alert("反射型 XSS 攻击")</script>
  2. 确认

反射型的 XSS 攻击我曾经在一个卖民用机械的网站上见过,技术栈估计是 JSP + SpringBoot 一把梭,其中的搜索框就有这个问题

那么如何吸引用户完成这个攻击呢?其实吸引用户点击具有上面这个 url 构成的链接即可,并不需要用户亲自去输入

存储型

了解到上面反射型的 XSS 攻击,我相信你们对 XSS 攻击已经有了初步了解,被攻击的原因是没有对用户的输入做任何处理就作为 HTML 的一部分返回,所以存储型的攻击也可以以此为基础

后端渲染

后端渲染,即由后端返回 HTML

存储型 XSS 攻击原理还是一样,像服务端传输恶意代码,并且服务端会将其拼接至字符串中

被攻击网站如下

202212131912114.gif

简单的 HTML 代码

<body>
  评论:<span class="comment"></span>
  <input class="comment-input" type="text" style="display: none" />
  <br />
  <button class="button" onclick="handleModifyComment()">新增评论</button>
  <button
    class="confirm-button"
    onclick="confirmModifyComment()"
    style="display: none"
  >
    确认
  </button>
</body>
<script>
  const comment = document.querySelector(".comment");
  const commentInput = document.querySelector(".comment-input");
  const button = document.querySelector(".button");
  const confirmButton = document.querySelector(".confirm-button");
  const getComment = () => {
    // ..
  };
  const handleModifyComment = () => {
    // ...
  };
  const confirmModifyComment = () => {
    // ...
  };

  // 初次加载
  getComment();
</script>

后端代码

router.get("/v1/user", function (req, res, next) {
  const [name, setName] = useName();

  res.setHeader("Content-Type", "text/html");
  res.send(generateHTML(name));
});

router.get("/v1/name", function (req, res, next) {
  const [name, setName] = useName();

  res.send(name);
});

router.post("/v1/name", function (req, res, next) {
  const [name, setName] = useName();
  const { username: updateName } = req.body;

  res.send(setName(updateName));
});

// ../utils/util
exports.generateHTML = (username) => {
  return `
    <body>
      username: ${username}
    </body>
  `;
};

用一个闭包模拟存储(很合理吧?)

let originName = "杰尼龟";

const useName = () => {
  const name = originName;
  /**
   * 重新赋值用户名称
   * @param {string} [newName="杰尼龟"]
   */
  const setName = (newName = "杰尼龟") => {
    originName = newName;
    return originName;
  };

  return [name, setName];
};

module.exports = useName;

实操如下,基于用户名称一个致命的恶意代码

<script>alert("XSS 攻击")</script>

IMG

说实话,存储型 XSS 攻击的场景出现在展示部分(比如用户的评论、用户名称、用户信息。。。)在新的网站都少了很多,展示部分的 XSS 攻击更可能出现在后端渲染的网站上,因为后端渲染是通过拼接字符串成 HTML 然后再返回

前端渲染不必多说,Vue/React/Angular 都是前端渲染的代表,它们以 JS 为基础渲染,渲染和更新通过 Ajax 来实现,而底层是通过 Node.innerText | Node.innerHTML 此类的方法渲染和更新页面

而浏览器会将通过 Node.innerText | Node.innerHTML 动态插入的内容当成普通文本,不会维护到DOM里面,XSS 攻击自然也无从谈起,因为无法执行

不过不要以为新网站没有 XSS 攻击,像 Nuxt.js 和 Next.js 这些 SSR 框架其实就是后端渲染(服务端渲染),本质上也是 HTML 拼接,不过此类开源框架往往有避免手端

前端渲染

前面说过了前后端分离的情况下,大部分数据都是通过 Ajax 拿到的,而且框架会帮我们通过 Node.innerText | Node.innerHTML 这种方式渲染,不会有 XSS 攻击的可能,但这并不意味着前端渲染并不会有被攻击的危险,比如直接渲染字符串

像 Vue 中提供了一个指令,叫 v-html,文档里很明确的指出了具有 XSS 攻击,但像我这种文档仅供 API 参考的人,当时完全没在意,但到后来背八股文的时候发现其潜在的危险性

比如下面这个富文本的例子

IMG

像现在市面上的富文本编辑器有的可以添加链接并展示(很合理吧?富文本编辑器应该还是很常见的)

<!-- http://localhost:3000/richText.html -->
<head>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.14"></script>
</head>
<body>
  <div id="app">
    <div>
      来吧展示!
      <div class="richText" v-html="richText"></div>
    </div>
    <br />
    <button @click="addLink">添加链接</button>
  </div>
</body>
<script>
  const app = new Vue({
    el: "#app",
    data() {
      return {
        richText: "<h1>Hello World</h1>",
      };
    },
    methods: {
      addLink() {
        const linkDialog = prompt("请输入链接", "");
        if (linkDialog !== null && linkDialog !== "") {
          this.richText += `<a href=${linkDialog}>一个你无法拒绝点击的链接名称</a>`;
        }
      },
    },
  });
</script>

假设啊假设啊,添加链接后会把 <a href=${linkDialog}>一个你无法拒绝点击的链接名称</a> 传入到后端,等用户下次访问 richText.html 就直接渲染后端返回的字符串,大概就是上面这个情况

如果我把链接变成下面这种

  1. javascript:alert(&#x27;XSS&#160;攻击&#x27;)
  2. '' onclick=alert(&#x27;XSS&#160;攻击&#x27;)

PS:&#x27; 表示 "(引号),&#160; 表示 (空格),皆为 HTML 编码,HTML 可以直接渲染,v-html 使用引号和空格需要转义,详情参考html中常见符号的代码表示

IMG

芭比Q了,被攻击了

上面的 1 和 2 两条恶意代码的渲染 DOM 结构如下

1

IMG

2

202212132138697.png

1 是利用浏览器 <a> 的 href 的特性,而 2 是利用空格间断属性,和 SQL 注入都是同根同源的操作呀!

DOM 型

阅读完反射型和存储型其实你应该已经明白,所谓 XSS 攻击就是寻找具有直接渲染和注入恶意代码的漏洞

回到 DOM 型 XSS 攻击和存储型以及反射型的区别,区别为 DOM 型 XSS 攻击为浏览器端缺陷,无需后端交互,即为前端代码的缺陷,其实也就是前端过于相信用户内容,直接渲染

而如果把存储型里的前端渲染的 XSS 攻击例子,想象成预览富文本不就是 DOM 型攻击咯(绝对不是我偷懒!)

总结

从反射型到存储型再到 DOM 型,我一开始找例子发现都是使用 JSP 的旧网站才会有 XSS 攻击漏洞,我想是不是前后端分离的现在已经不会有 XSS 攻击了,但随着我进一步深入,发现 SSR 和富文本场景也会有 XSS 攻击的可能,这些场景对我来说其实算是新兴的前端方向(至少相对于 JSP),所以也许 XSS 攻击从未离开我们

参考资料

  1. 前端安全系列(一):如何防止XSS攻击?
  2. 原始 HTML - Vue2 Doc
  3. html中常见符号的代码表示
  4. web安全之XSS实例解析