pwnhub-classroom(关于Django中容易导致的一些安全问题)

看到返回包的回显:

Server: gunicorn/19.6.0 Django/1.10.3 CPython/3.5.2

根据服务回显可以确认,这个题目是用的Django,python3.5.2 开发的。发现这里是可以进行任意文件读取的,参考:http://www.lijiejie.com/python-django-directory-traversal/ 

但是它对读取 py 和 conf 文件是禁止的,但是可以读取 pyc 文件,这里它是用python 3.x 开发的,在python 3.x 中,为了提高模块加载的速度,每个模块都会在 __pycache__ 文件夹中放置该模块的预编译模块,命名为module.version.pyc,version是模块的预编译版本编码,一般都包含Python的版本号。例如在CPython 发行版3.4中,fibo.py文件的预编译文件就是:__pycache__/fibo.cpython-34.pyc。这种命名规则可以保证不同版本的模块和不同版本的python编译器的预编译模块可以共存。

首先根据 django 的源码看下,其含有 __init__.py、urls.py 等文件,相应的,就应该产生对应的 pyc 文件,尝试读取。

对于 pyc 文件,我们可以通过 uncompyle6 来反编译,可以直接通过 pip 安装,通过这个方法,得到 python 源码

__init__.py

# uncompyle6 version 2.9.6
# Python bytecode 3.5 (3350)
# Decompiled from: Python 2.7.10 (default, Aug  8 2016, 13:10:03) 
# [GCC 4.4.7 20120313 (Red Hat 4.4.7-16)]
# Embedded file name: /www/students/__init__.py
# Compiled at: 2016-11-16 18:29:59
pass
# okay decompiling __init__.cpython-35.pyc

urls.py

# uncompyle6 version 2.9.6
# Python bytecode 3.5 (3350)
# Decompiled from: Python 2.7.10 (default, Aug  8 2016, 13:10:03) 
# [GCC 4.4.7 20120313 (Red Hat 4.4.7-16)]
# Embedded file name: /www/students/urls.py
# Compiled at: 2016-11-26 16:53:18
# Size of source mod 2**32: 341 bytes
from django.conf.urls import url
from . import views
urlpatterns = [
 url('^$', views.IndexView.as_view(), name='index'),
 url('^login/$', views.LoginView.as_view(), name='login'),
 url('^logout/$', views.LogoutView.as_view(), name='logout'),
 url('^static/(?P<path>.*)', views.StaticFilesView.as_view(), name='static')]
# okay decompiling urls.cpython-35.pyc

views.py

# uncompyle6 version 2.9.6
# Python bytecode 3.5 (3350)
# Decompiled from: Python 2.7.10 (default, Aug  8 2016, 13:10:03) 
# [GCC 4.4.7 20120313 (Red Hat 4.4.7-16)]
# Embedded file name: /www/students/views.py
# Compiled at: 2016-11-26 22:27:33
# Size of source mod 2**32: 2816 bytes
import json
import os
from wsgiref.util import FileWrapper
from django.shortcuts import render, redirect
from django.urls import reverse_lazy
from django.views import generic
from django.http import JsonResponse
from django.core import exceptions
from django.http import HttpResponse, Http404
from django.conf import settings
from django.db.models import F
from . import models

class RequireLoginMixin(object):
    login_url = reverse_lazy('students:login')

    def handle_no_permission(self):
        return redirect(self.login_url)

    def dispatch(self, request, *args, **kwargs):
        if request.session.get('is_login', None) != True:
            return self.handle_no_permission()
        return super(RequireLoginMixin, self).dispatch(request, *args, **kwargs)


class JsonResponseMixin(object):

    def _jsondata(self, msg, status_code=200):
        return JsonResponse({'message': msg}, status=status_code)


class LoginView(JsonResponseMixin, generic.TemplateView):
    template_name = 'login.html'

    def post(self, request, *args, **kwargs):
        data = json.loads(request.body.decode())
        stu = models.Student.objects.filter(**data).first()
        if not stu or stu.passkey != data['passkey']:
            return self._jsondata('', 403)
        else:
            request.session['is_login'] = True
            return self._jsondata('', 200)


class LogoutView(RequireLoginMixin, JsonResponseMixin, generic.RedirectView):
    url = reverse_lazy('students:login')

    def get(self, request, *args, **kwargs):
        request.session.flush()
        return super(LogoutView, self).get(request, *args, **kwargs)


class IndexView(RequireLoginMixin, JsonResponseMixin, generic.TemplateView):
    template_name = 'index.html'

    def post(self, request, *args, **kwargs):
        ret = []
        for group in models.Group.objects.all():
            ret.append(dict(name=group.name, information=group.information, created_time=group.created_time, members=list(group.student_set.values('name', 'id').all())))

        return self._jsondata(ret, status_code=200)


class StaticFilesView(generic.View):
    content_type = 'text/plain'

    def get(self, request, *args, **kwargs):
        filename = self.kwargs['path']
        filename = os.path.join(settings.BASE_DIR, 'students', 'static', filename)
        name, ext = os.path.splitext(filename)
        if ext in ('.py', '.conf', '.sqlite3', '.yml'):
            raise exceptions.PermissionDenied('Permission deny')
            try:
                return HttpResponse(FileWrapper(open(filename, 'rb'), 8192), content_type=self.content_type)
            except BaseException as e:
                raise Http404('Static file not found')
# okay decompiling views.cpython-35.pyc

models.py

# uncompyle6 version 2.9.6
# Python bytecode 3.5 (3350)
# Decompiled from: Python 2.7.10 (default, Aug  8 2016, 13:10:03) 
# [GCC 4.4.7 20120313 (Red Hat 4.4.7-16)]
# Embedded file name: /www/students/models.py
# Compiled at: 2016-11-26 03:04:46
# Size of source mod 2**32: 1033 bytes
from django.db import models

class Student(models.Model):
    name = models.CharField('', max_length=64, unique=True)
    no = models.CharField('', max_length=12, unique=True)
    passkey = models.CharField('', max_length=32)
    group = models.ForeignKey('Group', verbose_name='', on_delete=models.CASCADE, null=True, blank=True)

    class Meta:
        verbose_name = ''
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class Group(models.Model):
    name = models.CharField('', max_length=64)
    information = models.TextField('')
    secret = models.CharField('', max_length=128)
    created_time = models.DateTimeField('', auto_now_add=True)

    class Meta:
        verbose_name = ''
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name
# okay decompiling models.cpython-35.pyc

这里注意到其中 views.py 中的 LoginView 这个类

class LoginView(JsonResponseMixin, generic.TemplateView):
    template_name = 'login.html'

    def post(self, request, *args, **kwargs):
        data = json.loads(request.body.decode())
        stu = models.Student.objects.filter(**data).first()
        if not stu or stu.passkey != data['passkey']:
            return self._jsondata('', 403)
        else:
            request.session['is_login'] = True
            return self._jsondata('', 200)

可以看到这个登陆的验证条件就是,先将post过来的json包存入data字典中,然后通过filter方法去查找,如果查找不到记录或者是查出来的记录中的passkey和提交的passkey不相等,就会403,否则登陆成功。但是这里使用 filter() 函数带入查询的是我们传入的参数值 data,也就是我们可以控制的点,参考官方文档:

http://python.usyiyi.cn/documents/django_182/ref/models/querysets.html

http://python.usyiyi.cn/documents/django_182/topics/db/queries.html

 

 它返回的是包含满足查询参数的对象,拿这个查询参数是什么呢,文档中是这样说明的,

也就是说,通过 filter()、exclude()、get() 传入的参数相当于SQL语句后的where子句,其中以 键=值 的形式传入,其中的键名就相当于我们的字段名,它在 models.py 中会有相关的定义。

常见查询方法如下,如果你的关键字参数不包含双下划线 __,默认假定查询类型是exact。

exact:精确匹配,相当于SQL  = 'xx'
iexact:大小写不敏感的匹配
contains:大小写敏感的包含关系测试,相当于SQL like binary '%xx%'
icontains: 大小写不敏感的包含关系测试,相当于SQL like '%xx%'
in:在给定的表中,相当于SQL in (x1,x2,x3)
startswith:区分大小写,开始位置匹配,相当于SQL like binary 'xx%'
istartswith:不区分大小写,相当于SQL like 'xx%'
endswith:区分大小写,末尾位置匹配,相当于SQL like binary '%xx'
iendswith:不区分大小写,相当于SQL like '%xx'
gt:大于,相当于SQL > 'xx'
gte:大于或等于,相当于SQL >= 'xx'
lt:小于,相当于SQL < 'xx'
lte:小于或等于,相当于SQL <= 'xx'
range:在条件范围之内,相当于SQL between xx1 and xx2
year:对于日期和日期时间字段,确切的年匹配,整数年,eg
      Entry.objects.filter(pub_date__year=2005)
      SELECT ... WHERE pub_date BETWEEN '2005-01-01' AND '2005-12-31'
month:对于日期和日期时间字段,确切的月份匹配,取整数1(1月)至12(12月),eg
       Entry.objects.filter(pub_date__month=12)
       SELECT ... WHERE EXTRACT('month' FROM pub_date) = '12'
day:对于日期和日期时间字段,具体到某一天的匹配,取一个整数的天数。
     Entry.objects.filter(pub_date__day=3)
     SELECT ... WHERE EXTRACT('day' FROM pub_date) = '3'
hour:对于日期时间字段,精确的小时匹配,取0和23之间的整数
minute:对于日期时间字段,精确的分钟匹配,取0和59之间的整数
second:对于datetime字段,精确的第二个匹配,取0和59之间的整数
isnull:值为 True 或 False, 相当于SQL  IS NULL和IS NOT NULL
search:一个Boolean类型的全文搜索,以全文搜索的优势。这个很像 contains ,但是由于全文索引的优势,以使它更显著的快,eg
        Entry.objects.filter(headline__search="+Django -jazz Python")
        SELECT ... WHERE MATCH(tablename, headline) AGAINST (+Django -jazz Python IN BOOLEAN MODE)
regex:区分大小写的正则表达式匹配
iregex:不区分大小写的正则表达式匹配

回到这个代码,注意到这个条件

if not stu or stu.passkey != data['passkey']:

当我们传入的表单不含有 passkey 这个键时,就不会有 data['passkey'] 这个值了,如果查询不到值,会直接进入else语句,查到了值才会去比较 data['passkey'] ,我们如果不传入这个值,python就会报500错误,所以,我们就可以这样构造传入的参数,{"name__contains":"f"},根据它返回页面的不同来逐位获取字段内容

 

 

最后,获取 flag 时,注意到 models.py 中,

class Student(models.Model):
    name = models.CharField('', max_length=64, unique=True)
    no = models.CharField('', max_length=12, unique=True)
    passkey = models.CharField('', max_length=32)
    group = models.ForeignKey('Group', verbose_name='', on_delete=models.CASCADE, null=True, blank=True)
... ...
class Group(models.Model):
    name = models.CharField('', max_length=64)
    information = models.TextField('')
    secret = models.CharField('', max_length=128)
    created_time = models.DateTimeField('', auto_now_add=True)

... ...

在Student 中 有个外键,对应到Group表中,而Group表中又含有一个secret字段,就是我们的flag,用上面的方法去获取就可以了。

posted on 2016-12-04 20:34  镱鍚  阅读(512)  评论(0)    收藏  举报

导航