My user at this CTF: https://play.fe-ctf.dk/users/21
[baby's 1st], [curling], [remote]
Ha! Too easy!, I hear you say. Then try this:
Log in, get flag
Like in login lvl 1, we are presented with a login form consisting of a username and password field.
Like last time, the page loads some javascript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
let error_timeout = null;
const clear_error = () => {
document.getElementById("error").innerText = "";
};
const error = (msg) => {
if (error_timeout) {
window.clearTimeout(error_timeout);
}
document.getElementById("error").innerText = msg;
error_timeout = window.setTimeout(clear_error, 5000);
};
const key = [8, 131, 214, 191, 186, 15, 133, 27, 56, 78, 231, 188];
const password_enc = "5bf6a6dac85ce0784a7d93ec69f0a5c88a7de13a";
const encrypt = (bytes, key) => {
for (var i = 0; i < bytes.length; i++) {
bytes[i] = bytes[i] ^ key[i % key.length];
}
return bytes;
};
const check_password = (password) => {
const bytes = [];
for (let i = 0; i < password.length; i++) {
bytes.push(password.charCodeAt(i));
}
encrypt(bytes, key);
let s = "";
for (var i = 0; i < bytes.length; i++) {
s += bytes[i].toString(16);
}
return s == password_enc;
}
window.onload = (_event) => {
document.getElementById("form").onsubmit = (event) => {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
clear_error();
if (username == "") {
error("empty username");
event.preventDefault();
}
else if (!check_password(password)) {
error("invalid password");
event.preventDefault();
}
};
};
|
Solving challenges like this, is like eating an elephant - We start from one end and keep going!
Let’s follow the flow of the code and break this down:
- Looking at the script, the first thing that happens is the
window.onload event. Like last time we can supply any username, as long as it isn’t empty. The interesting part is the check_password() function. Let’s take a closer look:
25
26
27
28
29
30
31
32
33
34
35
36
|
const check_password = (password) => {
const bytes = [];
for (let i = 0; i < password.length; i++) {
bytes.push(password.charCodeAt(i));
}
encrypt(bytes, key);
let s = "";
for (var i = 0; i < bytes.length; i++) {
s += bytes[i].toString(16);
}
return s == password_enc;
}
|
-
This script takes an argument ("password" in this case) and loops through each character in the supplied string. Every character is converted to a byte and added to the bytes array.
-
The byte array is sent to the function encrypt() along with the key in line 15.
-
In the encrypt function our byte array and the supplied key is used in an XOR operation. Every character of our byte array is being XOR’ed with the key. A neat trick here is using the modulus operator ^ to ensure, that if the key is shorter than the supplied byte array, we don’t run out of bytes to XOR with.
18
19
20
21
22
23
|
const encrypt = (bytes, key) => {
for (var i = 0; i < bytes.length; i++) {
bytes[i] = bytes[i] ^ key[i % key.length];
}
return bytes;
};
|
-
After encrypting, the code converts the encrypted bytes back into a hexadecimal string s. In toString(16) the “16” converts the bytes into a base 16 (hexadecimal) string.
-
Finally the s variable is compared to the encrypted password password_enc and the result is returned. If the encrypted password matches the hardcoded password_enc we have a winner. This is the part we need to figure out.
-
Thankfully, XOR is reversible and we have both the key and encrypted password. What we need to do, is convert the hardcoded password back to bytes and XOR with the key. I wrote a small script for that:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#!/usr/bin/python3
password_enc = "5bf6a6dac85ce0784a7d93ec69f0a5c88a7de13a"
key = [8, 131, 214, 191, 186, 15, 133, 27, 56, 78, 231, 188]
# Convert the hexadecimal string to bytes
password_bytes = bytes.fromhex(password_enc)
# Initialize an array to store the decrypted bytes
decrypted_bytes = []
# XOR the encrypted bytes with the key
for i in range(len(password_bytes)):
decrypted_byte = password_bytes[i] ^ key[i % len(key)]
decrypted_bytes.append(decrypted_byte)
# Convert the decrypted bytes to a string
password = ''.join(chr(byte) for byte in decrypted_bytes)
print(password)
|
Presto, there´s our flag 🍻: SuperSecr3tPassw0rd!