The fourth challenge hints on there being something hidden
There are people who think you can hide important things by making it hard to read.
The page welcomes us with the following message:

Let’s try inspect the page with Developer tools. We immediately notice that JavaScript is in play here.

Navigating to the Debugger section of Developer Tools, we can see the source of the login.js
code and parts of it are obfuscated. There are some readable parts, so let’s focus on what we can read. Maybe we won’t need to deobfuscate anything (or so I hope).
function startup() {
key = localStorage.getItem('key');
if (key === null) {
localStorage.setItem('key', 'eyJ1c2VyaWQiOjB9.1074');
}
}
var _0x1fde = [
'charCodeAt'
];
(function (_0x93ff3a, _0x1fded8) {
var _0x39b47b = function (_0x54f1d3) {
while (--_0x54f1d3) {
_0x93ff3a['push'](_0x93ff3a['shift']());
}
};
_0x39b47b(++_0x1fded8);
}(_0x1fde, 402));
var _0x39b4 = function (_0x93ff3a, _0x1fded8) {
_0x93ff3a = _0x93ff3a - 0;
var _0x39b47b = _0x1fde[_0x93ff3a];
return _0x39b47b;
};
function calculate(_0x54f1d3) {
var _0x58628b = _0x39b4,
_0xc289d4 = 0;
for (let _0x19ddf3 in text) {
_0xc289d4 += text[_0x58628b('0x0')](_0x19ddf3);
}
return _0xc289d4;
}
function check() {
key = localStorage.getItem('key');
hash = window.location.search.split('?') [1];
if (key !== null && hash != 'token=' + key) {
parts = key.split('.');
text = atob(parts[0]);
checksum = parseInt(parts[1]);
count = calculate(text);
if (count == checksum) {
setTimeout(function () {
window.location = 'index.php?token=' + key;
}, 5000);
}
}
}
startup();
check();
The first part of the function is the startup()
function. We see it getting a key from the localStorage and if that key is missing, it assigns the value eyJ1c2VyaWQiOjB9.1074
. Ok, this part is clear. I do not really know what the local storage function refers to yet but I get the idea of what this function does.
function startup() {
key = localStorage.getItem('key');
if (key === null) {
localStorage.setItem('key', 'eyJ1c2VyaWQiOjB9.1074');
}
Moving on to the check()
function, it seems to fetch the same key locally and also gets a hash with which it does some operations. If the count value equals the checksum, then some type of access is granted.
function check() {
key = localStorage.getItem('key');
hash = window.location.search.split('?') [1];
if (key !== null && hash != 'token=' + key) {
parts = key.split('.');
text = atob(parts[0]);
checksum = parseInt(parts[1]);
count = calculate(text);
if (count == checksum) {
setTimeout(function () {
window.location = 'index.php?token=' + key;
}, 5000);
}
}
}
Let’s delve deeper into this function starting with what exactly are the key and hash values referring to.
key = localStorage.getItem('key');
hash = window.location.search.split('?') [1];
We learn from here and here that the localStorage.getItem('key')
will return the value of the key from the local browser storage. The function window.location.search.split('?') [1]
will return the query string part of the URL including the question mark (?). However, we are interested in just the second parameter as indicated by the value [1]
. Reference explanation can be found here and here.
Once we have the values of the key and the hash, there is a check to ensure that the key and the hash values are not empty before executing that part of the code.
if (key !== null && hash != 'token=' + key) {
First, we split the key value by the dot (.)
. We then perform a function atop()
which according to this resource here simply decodes the base-64 encoded string. So in this case, the value of the text will be the first part of the decoded string after the key is split. There is then a parseInt()
function that is performed on the second value of parts. It simply parses the string and returns an integer based on this resource. Finally the calculate()
function is called that works on the decoded text.
parts = key.split('.');
text = atob(parts[0]);
checksum = parseInt(parts[1]);
count = calculate(text);
The calculate function as we noted earlier is obfuscated and am not sure how to go about it for now. I follow the rule, deobfuscate only if necessary. We will, therefore, skip it for now unless it comes in handy.
function calculate(_0x54f1d3) {
var _0x58628b = _0x39b4,
_0xc289d4 = 0;
for (let _0x19ddf3 in text) {
_0xc289d4 += text[_0x58628b('0x0')](_0x19ddf3);
}
return _0xc289d4;
}
After the calculation is performed, there is another check that is done whereby the value from the calculate function is compared with the checksum value, and if they match, the setTimeout()
function is called after 5000 milliseconds (5 seconds) to evaluate the expression window.location()
. The latter function redirects the current page address to a new page. Read more about those two functions here and here.
if (count == checksum) {
setTimeout(function () {
window.location = 'index.php?token=' + key;
}, 5000);
This last part of the code seems to be the deciding factor, whether or not we will get access. So we have a general idea of what the code is doing. The main thing is to ensure that this part if met and maybe, that should work for us. The count should be equal to the checksum. The checksum value is parts which is intern derived from the key.
The final piece that actually calls the functions is in the very end. We see that the startup()
function which set the key is called first and then the check()
function to verify before access can be granted.
startup();
check();
Let’s go back and try to resolve the challenge now. I know that am supposed to take note of the URL so let’s navigate to https://04.adventofctf.com/ and check what the console shows us.

Sure enough, the URL seems to be redirected after a couple of seconds to the URL https://04.adventofctf.com/index.php?token=eyJ1c2VyaWQiOjB9.1074. We see that token to be equivalent to the key that was set in the startup()
function.
localStorage.setItem('key', 'eyJ1c2VyaWQiOjB9.1074');
That eyJ1c2VyaWQiOjB9
looks like a base64 text. Let’s doublecheck in CyberChef. Sure enough, it points to {"userid":0}
.

Remember there was some decoding going on. Parts
is derived from the stored key split by the dot. In that case eyJ1c2VyaWQiOjB9.1074
becomes eyJ1c2VyaWQiOjB9
and 1074
.
parts = key.split('.');
The calculate()
function uses the text part that was derived after decoding part of the first part of the key value eyJ1c2VyaWQiOjB9
. Let’s then try call that function in the browser.

No joy! Going back to the code, we saw that calculate()
was using a variable text
which was the decoded first part of the split key.
text = atob(parts[0]);
count = calculate(text);
Since we know that that’s the userid and that {"userid":0}
has no access, let’s try a different userid {"userid":5}
. Am also comparing the output with that of {"userid":0}
.

From the calculation note that the value 1074
for {"userid":0}
is the one our browser redirects to after 5 seconds. In that case, if userid is {"userid":5}
, then the value should be 1079
. Let’s encode the {"userid":5}
to get the first part of the key. We get eyJ1c2VyaWQiOjV9
.

Now, let’s try combine that into a token value eyJ1c2VyaWQiOjV9.1079
. When I input URL https://04.adventofctf.com/index.php?token=eyJ1c2VyaWQiOjV9.1079 into the browser, I get the flag for just a few seconds. Good enough for me to get my FLAG NOVI{0bfusc@t3_all_U_w@n7}
:).

The timeout might be an issue for some so a better way would be to look for the location where the key is being saved in the browser. If you remember, the code does set the key value in the localStorage
, let’s try find that. I looked into the storage section of the browser and found Local storage. In here, we do see the key values. I was easily able to change that value by double clicking the value field.

Once the change is done, I get the flag persistently :). Don’t forget to update the Badge server to get your picture of the day :).
