CSRF攻击模拟与防御——以Flask为例

GET型CSRF

先写一个有问题地网站吧:

from flask import Flask, request, render_template_string, session

app = Flask(__name__)
app.secret_key='random_secret_key'

@app.route('/csrf', methods=['GET'])
def csrf():
    if session.get('user','')=='admin':
        return "Admin do something!"
    else:
        return "No Privilege..."

@app.route('/login', methods=['GET'])
def login():
    user=request.args.get("user", "Null")
    session["user"]=user
    template="""
    <h3> Login as {{ user }}... </h3>
    """
    return render_template_string(template, user=user)

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8888)

使用http://127.0.0.1:8888/login?user=aaa模拟用户aaa登陆,从代码可以看到登陆后网站将用户身份(简单起见就是用户名)保存到session中。

1543924963235.png

再访问http://127.0.0.1:8888/csrf提示没有权限

1543924950598.png

以Admin登陆,提示Admin用户权限,假设这个请求是某个操作,比如admin重置自身密码为1234567:

1543925057392.png

此时如果用户访问了恶意网页,恶意网页诱导用户访问http://127.0.0.1:8888/csrf,那么用户由于sessionID还存在于浏览器中,因此会在无意间使用自己的身份重置密码,如诱导用户点击“ClickHere”链接:

1543925784330.png

接着可以看到由于用户的session存在,因此用户点击诱导链接后就会无意地使用自己的权限:

1543925854107.png

换用POST——POST型CSRF

有人说,换用POST不就好了吗?的确,修改方法为POST方法确实是我们要做的第一步

POST型的表单请求

假设服务器已经使用了POST型请求,如下:

@app.route('/reset', methods=['GET'])
def reset():
    template="""
    <form action="http://127.0.0.1:8888/csrf_post" method="POST">
    <input name="action" type="text">
    <input type="submit">
    </form>
    """
    return render_template_string(template)

@app.route('/csrf_post', methods=['POST'])
def csrf_post():
    print(request.form)
    data=request.form["action"]
    print("session:",session)
    if session.get('user','')=='admin':
        print("Admin do", data)
        return "Admin do "+data

    else:
        print("No Privilege2...")
        return "No Privilege2..."

正常用户通过/reset发送post请求修改密码:

1543978510721.png

密码修改成功:

1543978495643.png

那么攻击者也可以模拟post请求,同样造成csrf:

<html>
    <title>JSON CSRF POC</title>
    <body>
    <center>
    <h1> JSON CSRF POC </h1>
    <script>
    function submitRequest()
    {
      fetch('http://127.0.0.1:8888/csrf_post',{method: 'POST', credentials: 'include', headers:{'Content-Type': 'application/x-www-form-urlencoded'}, body:'action=reset+password'})
    }
    </script>
    <form action="#">
    <input type="button" value="Submit request" onclick="submitRequest()"/>
    </form>
    </center>
    </body>
</html>

把按钮去了就可以变成一个自动POST的恶意网站,只要用户访问即可重置密码:

1543979180735.png

防御

对于flask,使用

from flask_wtf.csrf import CSRFProtect

app.config['SECRET_KEY'] = 'you never guess'
CSRFProtect(app)

打开csrf保护,接着再对所有的表单添加一个隐藏字段即可:

<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />

增加隐藏字段后,每次POST时都会带有一个csrf_token,攻击者由于同源策略是无法获取这个token的,另外token写进session里面,即session和token是一对一关系,因此攻击者也无法通过自己的token猜测别人的token,而服务器再POST请求过来时就会验证这个token是否与session一致,若不一致则拒绝服务,这样一来攻击者就无法攻击成功了(除了把token放表单里,还可以放cookie里,攻击者仍然无法获取):

1543979704237.png

POST型的json请求

如果浏览器严格限制了Content-Type=application/json(flask的获取方法为request.get_json())——这得益于同源策略,使用POST方法进行CSRF攻击本身就已经很难了:

当然,如果不判断content-type,那么还是有机会的,:

模拟脆弱的服务器:

@app.route('/action', methods=['GET'])
def normalAction():
    template="""
    <html>
    <title>Normal</title>
    <center>
    <h1> Reset Password </h1>
    <head>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script type="text/javascript">
    $(document).ready(function(){
      $("button").click(function(){
        // body为"data=json"的请求
        // var data = {data: JSON.stringify({"action": "reset password"})}
        // body直接为json
        var data = JSON.stringify({"action": "reset password"})
        $.ajax({
            url:"http://127.0.0.1:8888/csrf2",
            contentType: "application/json",
            dataType: "json",
            type: 'POST',
            data: data,
            success: function (msg) {
                        alert(msg.status);
                    }
            })
      });
    });
    </script>
    </head>
    <body>
    <button>Conform</button>
    </body>
    </html>
    """
    return render_template_string(template)

@app.route('/csrf2', methods=['POST'])
def csrf2():
    print(request.get_data())
    data=json.loads(request.get_data(as_text=True))
    print("session:",session)
    if session.get('user','')=='admin':
        print("Admin do", data["action"])
        return jsonify(dict(status="success"))

    else:
        print("No Privilege2...")
        return jsonify(dict(status="failed"))

这里,正常的访问Nomal进行密码重置时可行的:

1543977341098.png

但是攻击者使用相同的代码则会失败,原因是同源策略

1543977364148.png

但是,攻击者可以使用如下方法攻击,注意enctype是成功的关键,他能保证发过去的东西不被url编码,并且使用name和value配合来达到json合法的目的:

<html>
    <form action="http://127.0.0.1:8888/csrf2" method="POST" enctype="text/plain">
    <input name='{"action":"reset", "test":"' value='test"}' type='hidden'>
    <input type=submit>
    </form>
</html>
1543931144169.png

可以看到攻击成功

1543977726816.png

如果不考虑回显,我们还可以使用如下脚本,有人会怀疑,为什么这里同源没有效果呢?因为注意这里的Content-Type=text/plain,这时的策略时js的请求仍然能够发出去,但是不能获取结果,若Content-Type=text/json时,浏览器将会首先发送一个OPTIONS预检请求,如果服务器不允许跨域则不能发送请求。

<html>
    <title>JSON CSRF POC</title>
    <body>
    <center>
    <h1> JSON CSRF POC </h1>
    <script>
    function submitRequest()
    {
      fetch('http://127.0.0.1:8888/csrf2',{method: 'POST', credentials: 'include', headers:{'Content-Type': 'text/plain'}, body:'{"action":"reset password"}'})
    }
    </script>
    <form action="#">
    <input type="button" value="Submit request" onclick="submitRequest()"/>
    </form>
    </center>
    </body>
</html>

可以看到,虽然ajax获取结果失败,但是依然可以发送请求:

1543980433752.png

通过抓包可以看到操作实际是成功的:

1543980472782.png

防御

同样还是先开启CSRF防御:

from flask_wtf.csrf import CSRFProtect


app = Flask(__name__)
app.secret_key = 'random_secret_key'
CSRFProtect(app)

我们要想办法让自己站的ajax拿到csrf_token,就像之前说的,将token放进cookie里,使用app.after_request修饰使得每个页面返回时都执行:

@app.after_request
def after_request(response):
    # 调用函数生成 csrf_token
    csrf_token = generate_csrf()
    # 通过 cookie 将值传给前端
    response.set_cookie("csrf_token", csrf_token)
    return response

接着ajax从cookie拿到token并放到headers里,不用担心攻击者,因为由于同源策略,他们没法获取其他网站的cookie:

@app.route('/reset2', methods=['GET'])
def reset2():
    template = """
    <html>
    <title>Normal</title>
    <center>
    <h1> Reset Password </h1>
    <head>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script type="text/javascript">
    $(document).ready(function(){
      $("button").click(function(){
        // body为"data=json"的请求
        // var data = {data: JSON.stringify({"action": "reset password"})}
        // body直接为json
        var data = JSON.stringify({"action": "reset password"})
        $.ajax({
            url:"http://127.0.0.1:8888/csrf2",
            contentType: "application/json",
            headers:{'X-CSRFToken':$.cookie('csrf_token')},
            dataType: "json",
            type: 'POST',
            data: data,
            success: function (msg) {
                        alert(msg.status);
                    }
            })
      });
    });
    </script>
    </head>
    <body>
    <button>Conform</button>
    </body>
    </html>
    """
    return render_template_string(template)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容