用代码来看懂CSRF,再也不怕面试题记不住啦

127 阅读5分钟

CSRF简介

CSRF跨站请求伪造是一种冒充受信任用户,向服务器发送非预期请求的攻击方式。

攻击原理

  1. 用户输入账号信息请求登录A网站,并且生成了登录信息在cookie,在cookie的合法配置下(域名/路由等)的http请求会自动带在请求头上。
  2. 攻击者构造一个B网站,令B网站能合法访问到A网站的cookie,并再次网站对A网站进行http请求,让浏览器自己把cookie带上。
  3. A网站接收到了B网站的请求,并且校验了浏览器自动带上的cookie信息,通过校验成功后给B网站返回一些权限信息
  4. 这个时候B网站就已经拿到了该用户的A网站的一些权限信息了。

当然以上是属于获取信息型,另外也可以对A网站的信息数据库进行修改,最主要的还是利用了A网站的cookie去通过鉴权。

防御手段

最常见的防御手段有两种:

  1. 不用cookie进行鉴权,像现在最常用的token,http使用自定义请求/响应头字段,浏览器存储在storage,并由开发者自己放在请求头上。服务的从请求头获取。模拟了cookie的部分行为,但取消了自动负载在http请求头上,另外一个就是取消了Set-Cookie。
  2. 验证Referer头字段。

talk is cheap,show me the code

构建一个正牌网站

要构造一个能被攻击的网站,需要满足两个条件:

  1. 使用cookie来存储鉴权信息。
  2. 不验证Referer头字段。

作为一个前端,我们就是用最熟悉的 nodejs + express 来完成我们的示例代码。

package.json

简单看看需要哪些依赖,就两个

{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cookie-parser": "^1.4.6",
    "express": "^4.18.2"
  }
}

index.js

主要路由就四个,一个主页面,一个详情页,然后一个登录接口和一个需要鉴权的用户信息接口。

const express = require('express')
const fs = require('fs')
const path = require('path')
var cookieParser = require('cookie-parser')
const app = express()
app.use(cookieParser())
const port = 3000
const secret = '12345'

// 页面
app.get('/', (req, res) => {
    let htmlStr = fs.readFileSync(path.resolve(__dirname, './static/index.html'));
    htmlStr = htmlStr.toString();
    res.send(htmlStr);
})
app.get('/detail', (req, res) => {
    let htmlStr = fs.readFileSync(path.resolve(__dirname, './static/detail.html'));
    htmlStr = htmlStr.toString();
    res.send(htmlStr);
})

// 登录
app.get('/login', (req, res) => {
    res.cookie('token', secret, {
        // expires: new Date(Date.now() + 10 * 1000),
    });
    res.send({ success: true });
})

// 列表
app.get('/userinfo', (req, res) => {
    if (req.cookies.token === secret) {
        res.send({
            success: true,
            data: {
                username: 'abc',
                password: '123',
            },
        });
    } else {
        res.status(401);
        res.send({
            success: false,
        });
    }
})

app.listen(port, () => {
    console.log(`Example app listening on http://localhost:${port}`);
})
两个页面
index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <button>login</button>
  <button>get userinfo</button>
  <a href="/detail" target="_blank">我要买这个</a>
  <script>
    let buttons = document.querySelectorAll('button');
    buttons[0].addEventListener('click', e => {
      fetch('/login').then(_ => _.json()).then(res => console.log(res));
    })
    buttons[1].addEventListener('click', e => {
      fetch('/userinfo').then(_ => _.json()).then(res => console.log(res));
    })
  </script>
</body>

</html>
detail.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    details
</body>
</html>
简单部署

为了更相似于真实场景,还有一些后续的操作,这里我用了wsl去运行这个 A网站 了。

image.png 记住我们这里的正牌站点的ip是172.19.174.136

构建一个冒牌网站

冒牌网站我们想清楚需求: 如果是网站的index.html,我们不需要做任何操作,用户能在这里正常的登录。但对于detail.html,我们希望能在此入手,能获取到cookie登录信息

虚假的detail.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    details
    <button>美女荷官,在线发牌</button>
    <script>
        let button = document.querySelector('button');
        button.addEventListener('click', () => {
            fetch('/userinfo').then(_ => _.json()).then(res => console.log(res));
        })
    </script>
</body>

</html>

可以看到我们的的冒牌网站,只是多了一个button,并且多了一些非常诱惑人的信息,引诱用户进行点击。

冒牌网站如何获取到正牌网站的cookie?

我们可以知道同一个域名下cookie可以共享,那么如果我们用nginx进行代理,让网站的其他页面直接转发源站,但detail页就访问我们自己的冒牌站点,nginx配置如下:

server {
       listen       80;
       server_name  localhost;

       location /detail {
        proxy_pass 这里替换冒牌网站的地址;
       }

       location / {
        proxy_pass 这里替换正牌网站的地址;
       }
    }

配置完之后,我们分别访问正牌站点和冒牌站点的detail页看看

正牌站点

可以看到我们的正牌站点,是没有广告的,并且cookie的域名是http://172.19.174.136:3000

image.png

冒牌站点

而我们的冒牌网站则是有广告,并且cookie域名与正牌站点的域名是不一样的

image.png

但是我们这里只是测试,所以域名用了localhost,但也可以修改的,比如host文件,nginx代理,openssl,云服务的话就更麻烦了,但总的来说这里是可以改的,并且应该改成让用户相信我们这个是正规网站。

然后有了这个站点之后,就可以群发骚扰短信了,后面就等一个大怨种上当就行啦 哈哈哈。

假装自己是大怨种

收到了一个诈骗信息

image.png

点进了冒牌网站领福利了

image.png

点击发财按钮

然后我们就到了下面这个界面,并且经受不住诱惑,点击了发财按钮

image.png

然后

然后,你的信息就已经泄露了,我们去看看控制台 -> 网络

自动带上了cookie鉴权信息

image.png

成功通过了正牌网站的鉴权,获取了用户信息

image.png

真实开发问题分享

经验总结:开发中利用csrf解决的问题 - 掘金 (juejin.cn)

总结

我们这里只是一个场景,有可能获取了信息之后,还会用来做其他事情,或者是修改用户信息,或者是登录你的账户给自己买了一支花西子等等等。

另外我们的演示过程也不是很完整,正牌网站和冒牌网站的cookie不是共享的(域名不一样),冒牌网站在正常页面没登陆时,详情页也没有cookie去通过鉴权等这些场景都没有演示。希望各位看完之后自己去实现一个试一试吧,代码几乎都已经贴出来了,其中用nginx代理,已经整个过程思路才是关键。

另外有像burpsuite之类的软件可以直接检测出存在csrf漏洞的接口,并一键生成相应的冒牌站点。

最后

如果这篇文章有什么错误非常欢迎评论区进行指正。

如果觉得帮到你的话求一个点赞关注!!!