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,用上面的方法去获取就可以了。
浙公网安备 33010602011771号