likes
comments
collection
share

XSS 攻击案例

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

今天,我们来谈谈 XSS 攻击。

XSS 是什么

XSS 攻击指的是攻击者通过在受信任的网站上注入恶意的脚本,使得用户的浏览器在访问该网站时执行这些恶意脚本,从而导致信息泄露等安全问题。

XSS 英文名 Cross-Site-Scripting,即跨站脚本。为什么不叫 CSS 呢?因为 CSS 的缩写已经被 Cascading Style Sheets,即层叠样式表占用。

XSS 攻击案例

XSS 分类和演示

XSS 攻击主要分成三类:DOMXSS 攻击、反射型 XSS 攻击和存储型 XSS 攻击。

我们接下来需要演示下 XSS 攻击,我们做点前期准备。

案例的演示环境:

macOS Monterey - Apple M1

node version - v14.18.1

Visual Studio Code 及其 Live Server 插件

首先,我们添加个 hostname, 方便测试,当然你可以直接使用 ip 地址测试。

通过 sudo vim /etc/hosts 添加 127.0.0.1 a.example.com 的映射:

XSS 攻击案例

本文所有的案例通过 SSR 应用进行演示。

DOMXSS 攻击

DOMXSS 攻击利用了前端 Javascript 在浏览器中动态操作 DOM 的特性DOMXSS 攻击的原理是攻击者通过注入恶意代码或者脚本到网页中的 DOM 元素中,然后通过浏览器执行这些恶意的代码。

案例,如下:

const Koa = require('koa');
const Router = require('koa-router');
const views = require('koa-views');
const path = require('path');

const app = new Koa();
const router = new Router();

app.use(
  views(path.join(__dirname, 'views'), {
    extension: 'ejs'
  })
);

router.get('/', async (ctx) => {
  await ctx.render('index', {
    xss: '<script>alert("XSS")</script>',
    content: 'DOM - XSS Attack'
  })
});

app.use(router.routes());
app.listen(3000, () => {
  console.log("listening on http://localhost:3000");
})

上面👆,我们渲染的模版(如下)index,并将数据 xsscontent 传递给模版。

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>DOM - XSS - Jimmy</title>
</head>
<body>
  <h3 style="text-align: center;"><%= content %></h3>
  <%- xss %>
</body>
</html>

在模版中,我们读取了字符串 content,和 html 数据 xss。运行之后,会弹出攻击成功的提示:

XSS 攻击案例

反射型 XSS 攻击

反射型 XSS 攻击,指攻击者通过构造恶意的 URL,利用用户的输入参数将恶意的代码注入到目标站点的响应内容中,然后将注入的恶意代码发送给浏览器执行,从而实现攻击。简而言之:就是把用户输入的数据从服务端反射给用户浏览器。

下面是一个小案例:

const Koa = require('koa');
const Router = require('koa-router');
const views = require('koa-views');
const path = require('path');

const app = new Koa();
const router = new Router();

app.use(
  views(path.join(__dirname, 'views'), {
    extension: 'ejs'
  })
);

router.get('/', async (ctx) => {
  await ctx.render('index')
});

router.get('/api/username', async (ctx) => {
  let url = ctx.request.url;
  let _username = '';
  if(url.split('username=')[1]) {
    _username = url.split('username=')[1];
  }
  ctx.body = {
    username: _username // <img src="XX" onerror='alert("XSS")' style="display: inline-block; width: 0; height: 0;"/> Jimmy
  }
})

app.use(router.routes());
app.listen(3000, () => {
  console.log("listening on http://localhost:3000");
})

上面我们渲染了首页 index 模版,然后提供了一个 /api/username 的接口,返回 username 数据。

首页模版如下:

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>反射型 - XSS - Jimmy</title>
</head>
<body>
  <h3 style="text-align: center;">反射型 - XSS</h3>
  <textarea type="text" id="input" placeholder="please enter username" rows="8"></textarea>
  <button id="trigger">Say Hi</button>
  <p style="color: #f00;" id="hint"></p>
  <script>
    let inputDom = document.getElementById("input");
    let triggerDom = document.getElementById("trigger");
    let hintDom = document.getElementById("hint");
    triggerDom.addEventListener("click", () => {
      const params = {
        username: inputDom.value
      }
      const queryString = Object.keys(params)
        .map(key => encodeURIComponent(key) + "=" + encodeURIComponent(params[key]))
        .join("&"); 
      
      fetch(`/api/username?${queryString}`, {
        method: "GET"
      })
        .then(response => response.json())
        .then(data => {
          let _username = data.username;
          hintDom.innerHTML = `Hello, ${ decodeURIComponent(_username) }.`;
        })
    })
  </script>
</body>
</html>

我们提供了一个输入框 textarea,然后触发按钮,调用接口获取返回的数据,然后在页面中展示 username。比如,Jimmy 写入输入框,会在页面展示 Hello, Jimmy. 的效果。但是,对于 Hacker 来说,我可以输入:

<img src="XX" onerror='alert("XSS")' style="display: inline-block; width: 0; height: 0;"/> Jimmy

页面的展示内容还是 Hello, Jimmy.,却多做了弹窗的动作。

XSS 攻击案例

注意⚠️ 现代浏览器通常会自动阻止通过 innerHTML 插入的包含脚本的内容

储存型 XSS 攻击

存储型攻击,指攻击者利用它在目标站点上储存的恶意脚本,当用户访问该页面时,恶意脚本被执行。该类攻击在评论区常见。

一般的攻击步骤:

  • hacker 在评论区输入了攻击的脚本
  • 服务度端存储了该脚本
  • 用户浏览网页,拉取了该脚本
  • 脚本执行,攻击生效
const Koa = require('koa');
const Router = require('koa-router');
const views = require('koa-views');
const path = require('path');

const app = new Koa();
const router = new Router();

const dataList = [{
  name: 'Ivy',
  comment: 'Nice Day!'
}]

app.use(
  views(path.join(__dirname, 'views'), {
    extension: 'ejs'
  })
);

router.get('/', async (ctx) => {
  await ctx.render('index')
});

router.get('/api/comments', async (ctx) => {
  ctx.body = {
    list: dataList
  }
})

router.get('/api/comment/add', async (ctx) => {
  let query = ctx.request.url.split('?')[1];
  let usernamePart = query.split('&')[0];
  let commentPart = query.split('&')[1];
  dataList.push({
    name: usernamePart.split('=')[1],
    comment: commentPart.split('=')[1]
  })
  ctx.body = {
    message: 'ok' 
  }
})

app.use(router.routes());
app.listen(3000, () => {
  console.log("listening on http://localhost:3000");
})

我们通过变量 dataList 来模拟从数据库数据。接口 /api/comment/add 是添加评论,接口 /api/comments 是拉取评论,读取 dataList 变量值。

模版的设置如下:

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>存储型 - XSS - Jimmy</title>
</head>
<body>
  <h3 style="text-align: center;">存储型 - XSS</h3>
  <textarea type="text" id="input" placeholder="comment here..." rows="8"></textarea>
  <button id="trigger">Comment</button>
  <ul id="list"></ul>
  <script>
    let inputDom = document.getElementById("input");
    let triggerDom = document.getElementById("trigger");
    let listDom = document.getElementById("list");

    initList();
    triggerDom.addEventListener("click", () => {
      const params = {
        username: 'Jimmy',
        comment: inputDom.value // <img src="XX" onerror='alert("XSS")' style="display: inline-block; width: 0; height: 0;"/> XSS happen.
      }
      const queryString = Object.keys(params)
        .map(key => encodeURIComponent(key) + "=" + encodeURIComponent(params[key]))
        .join("&"); 
      
      fetch(`/api/comment/add?${queryString}`, {
        method: "GET"
      })
        .then(response => response.json())
        .then(data => {
          if(data.message === 'ok') {
            initList();
          }
        })
    })

    function initList() {
      fetch('/api/comments')
        .then(response => response.json())
        .then(data => {
          let list = data.list;
          let domLis = ``;
          for(let i = 0; i < list.length; i += 1) {
            let person = list[i];
            domLis += `<li><b>${ person.name }</b> said: ${ decodeURIComponent(person.comment) }</li>`
          }
          listDom.innerHTML = domLis;
        })
    }
  </script>
</body>
</html>

我们一进来页面,默认拉取了评论列表。触发按钮,添加评论,当评论添加成功后,重新拉取评论列表数据。

XSS 攻击案例

XSS 避免

那么,我们应该如何避免 XSS 攻击呢?

  • 输入验证和过滤:用户输入的内容不能相信,要对用户输入的数据进行验证,只接受可信任的数据。比如对脚本标签 script 处理,剔除该标签的潜在危险
  • 使用安全的框架或者库:比如选择前端开发框架 Angular,其内置了安全机制,默认 XSS 防护;又比如你可以使用库 xss 来避免此类攻击
  • 设置 HTTP 头部:我们可以设置适当的 HTTP 头部,比如 Content-Security-Policy (CSP) 内容安全策略X-XSS-Protection等。增强应用程序的安全性。

CSP 比如:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src data: https://images.example.com; font-src 'self' https://fonts.gstatic.com;

default-src 指定了默认的加载源限制为智能从同源地址加载。script-src 指定了可以从同源地址和 https://cdn.example.com 地址加载脚本。后面的 imgfont 类推。

当然,定期更新和修补漏洞也是不可少的。减少给 Hacker 攻击的机会。

参考