This page looks best with JavaScript enabled

CSRF攻击防御/Hackbar攻击原理及Flask防御策略研究

 ·  ☕ 4 min read · 👀... views

前言

CSRF的防护主要思想就是保证请求是从正确的网站发出的。

CSRF攻击

攻击原理图如下,图片来自[1]

CSRF攻击原理

GET型攻击

get型攻击仅需伪造一次get请求即可,危害程度大

1
<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>

POST型攻击

1
2
3
4
<form action=http://wooyun.org/csrf.php method=POST>
    <input type="text" name="xx" value="11" />
</form>
<script> document.forms[0].submit(); </script> 

CSRF检测

CSRFTester

源码阅读:入口类为CSRFTester,构造函数创建UI,main中创建proxy,Proxy(new Framework()).run()

CSRF防御

referer

网站B发出的请求referer为网站B,只需在后端判断referer是否同站即可防御CSRF。这样相当于把安全寄托于第三方,因为浏览器都不支持js对referer头的修改[4]&[5],有的文章[6]说可以使用axios进行referer绕过,但是我测试后并不行。

Refused to set unsafe header "referer"

js不允许修改referer头,但是hackbar可以添加任意头,是如何实现的呢。

Hackbar源码阅读

文件系统中搜索chrome插件id就很容易找到解包后源码的位置,可以发现连Hackbar都是大名鼎鼎的vue写的。

搜索referer可以在index.js中看到commonRequestHeaders的函数列出常见的request头

发现主要逻辑如下,就是通过v-model动态绑定vue对象data数据中headers中的值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

<v-btn id="add-header-button" @click="addHeader()">Add Header</v-btn>
<v-layout class="header-settings" align-center v-for="(header, index) in request.headers" :key="index">
    <v-checkbox v-model="header.enabled"></v-checkbox>
    <v-combobox dense class="px-1 pt-2" label="Name" v-model="header.name" :items="commonRequestHeaders()" @focus="onFocus($event)" :menu-props='{ "maxHeight": 200 }'></v-combobox>
    <v-text-field class="px-1" label="Value" v-model="header.value" @focus="onFocus($event)" @keydown.stop></v-text-field>
    <v-btn icon text @click="deleteHeader(index)">
        <v-icon small>mdi-close</v-icon>
    </v-btn>
</v-layout>

headers只是存储数据,执行时调用chrome.runtime.connect()这个接口,他的第一个参数是extension id。

The ID of the extension or app to connect to. If omitted, a connection will be attempted with your own extension. Required if sending messages from a web page for web messaging.[7]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
this.backgroundPageConnection = chrome.runtime.connect()
...
execute: function () {
    if (this.request.url.length === 0) {
        return
    }

    this.backgroundPageConnection.postMessage({
        tabId: chrome.devtools.inspectedWindow.tabId,
        type: 'execute',
        data: this.request
    })
},

到这里可以知道了,hackbar使用的是浏览器提供的扩展程序接口,所以可以修改header

csrf token

也正是由于CSRF产生的原理,csrf_token不能放在cookie中,否则达不到防御CSRF的效果;csrf_token也无需每次都不一样(每次不同是防止重放攻击,若仅考虑CSRF则无需不同)。

一般form表单加个hidden属性的input标签来存放。由于csrf_token存放在网站A的DOM中,这样网站B就无法获取token,即使伪造使得流量器访问网站A,A后端检测到没有token也不会允许请求。

至于hidden这个属性,个人认为即使是明文也不影响对CSRF的防御,主要是减少用户感知。

1
2
3
4
5
6
7
8
<body>
    <form action="" method="post">
        <input type="hidden" name="csrf_token" value="{{csrf_token()}}">
        账号:<input type="text" name="username"><br><br>
        密码:<input type="password" name="password"><br><br>
        <input type="submit" value="提交">
    </form>
</body>

csrf_token的设计:基于攻击者无法得知cookie,最简单的可直接将sessionid等作为token(md5值等同理);或者直接生成随机字符串

添加http request头

添加额外的 HTTP 头属性,并把 token 值放入其中,比如叫 csrftoken

从底层到高层XMLHttpRequest、Ajax、 Axios等都支持手动添加header头,较方便。个人认为不存在[1]中描述的局限性较大的问题。

添加验证机制

在请求数据提交前,需填写验证码信息,以增加对用户来源的有效认证,防止恶意未授权的操作产生。

Flask CSRF Strategy

如果没有指定允许POST,提交表单的时候会提示Method Not Allowed

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@app.route('/')
def root():
    if request.method == "GET":
        return render_template("form.html")
    else:
        print(request.form)
        return "ok"
========================================
Method Not Allowed
The method is not allowed for the requested URL.

如果允许了POST,再次尝试提交表单会Bad Request,这也是CSRF攻击未携带token的场景

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@app.route('/', methods=['GET','POST'])
def root():
    if request.method == "GET":
        return render_template("form.html")
    else:
        print(request.form)
        return "ok"
========================================
Bad Request
The CSRF token is missing.

如果template中添加token,在访问网站的时候请求到token,然后提交表单的时候将token也post出去,这样就能正确响应。尝试手动修改DOM中的token再次post也是bad request。

1
2
3
4
5
6
7
8
<body>
    <form action="" method="post">
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
        账号:<input type="text" name="username"><br><br>
        密码:<input type="password" name="password"><br><br>
        <input type="submit" value="提交">
    </form>
</body>

源码分析

https://github.com/wtforms/flask-wtf/blob/main/src/flask_wtf/csrf.py

CSRFProtect类中init_app初始先对app进行一些参数的配置并用generate_csrf方法获取随机token并保存到session中;然后对app添加@app.before_request的hook,请求来时先检测app的参数配置,不符合直接return,然后进到protect方法,其中先validate_csrf(self._get_csrf_token())检测token是否与session中一致再使用same_origin方法检测referrer是否同源,如果是最后再表示请求是CSRF valid

参考文献

[1] https://www.cnblogs.com/54chensongxia/p/11693666.html [csrf]
[2] https://www.cnblogs.com/leijiangtao/p/11770647.html [flask csrf]
[3] http://luckyzmj.cn/posts/a1b686d3.html#! [CSRFTester]
[4] https://blog.csdn.net/neal1991/article/details/114609935 [js referer]
[5] https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name [forbidden header name]
[6] https://blog.csdn.net/Forever201295/article/details/80237134 [axios referer forgery]
[7] https://developer.chrome.com/docs/extensions/reference/runtime/#method-connect [runtime.connect]
[8] https://uknowsec.cn/posts/notes/CSRF%E6%BC%8F%E6%B4%9E%E7%9A%84%E5%88%A9%E7%94%A8%E4%B8%8E%E5%AD%A6%E4%B9%A0.html [attack csrftester]
[9] https://github.com/wtforms/flask-wtf/blob/main/src/flask_wtf/csrf.py [flask csrf source code]

Share on

ruokeqx
WRITTEN BY
ruokeqx