HTB University CTF 2024: Binary Badlands

Security In The Front

Reverse

View on GitHub

Analysis

The problem gives us a single index.html file. If rendered, it has a login form asking for a guard name and an access code.

Solution

By analyzing the code for the page, we will notice that the validation of the login credentials are handled in JavaScript directly on the client. The function checkCredentials is written on a single line and is obfuscated. This is where the solution lies, and it's where we will focus our efforts.

By running the line through a JS Beautifier we will have a better idea of the code. First we notice some single-letter variables (c, i, a, o p, etc) which hold the value of prototype functions of known classes. Our first step is to replace all instances of these variables for their values. We will then notice that there is a big if condition with multiple values inside, eventually reaching a call to .reduce(). This means that each element of the outermost array is going to be sent to the function along with the current value, starting with true. The function used is (e, t) => e && n.apply(undefined, t), where e is the current value of the reduction, and t is the element of the array. The element of the array is send to function n, which is in itself a .reduce(). This element t is going to be called the block from this point forward. The outermost array is a collection of blocks, and each call of n handles a single block.

The function n has three parameters which will be taken from the block: the first is a list of functions, the second is a list of arguments, and the third is a value. The function essentially applies the functions of the first parameter, e, in sequence using the respective parameters, using the value as the current "context". For example, the first block is the following:

[String.prototype.split, Array.prototype.map, f, h],
[
    [""], 
    [e => -1 == Array.prototype.indexOf(c2, e) ? e : c1[Array.prototype.indexOf(c2, e)]],
    [
        ["n", "q", "z", "v", "a"]
    ],
    [0]
], 
access_user

This will apply the function String.prototype.split to the value (access_user.split()) with the parameter "", meaning that it will break the string into its individual characters. Next, it will apply the function e => -1 == Array.prototype.indexOf(c2, e) ? e : c1[Array.prototype.indexOf(c2, e)] to each character using .map(). This function finds the character inside the c2 variable and, if it exists, replaces it with the character at the same index in c1. Next, it uses function f to compare the current value to that of ["n", "q", "z", "v", "a"]. Function f will give us -1, 0, or 1 depending on the result of the comparison. Equality is represented by 0. Finally, it uses function h to test if our final value is equal to 0. If it is, the final value of the block is true .

It is our job to reverse every step of this block, which is essentially reversing the translation made in the second step where every character from c2 became its counterpart on c1. We will do this by finding n in c1 and checking its counterpart in c2, which is a. The same for q, which is d, z becomes m, v becomes i, and a becomes n. With that, we conclude that the expected value for access_user is admin .

We now have another 8 blocks to reverse to find clues about the access_code. Explanations will be summarized for brevity:

[String.prototype.slice, String.prototype.repeat, String.prototype.split, Array.prototype.map, Array.prototype.filter, f, h],
[
    [0, 4],
    [3],
    [""],
    [e => -1 == Array.prototype.indexOf(c2, e) ? e : c1[Array.prototype.indexOf(c2, e)]],
    [(e, t) => t % 3 == 1],
    [
        ["G", "U", "{", "O"]
    ],
    [0]
], access_code

In this block we get the first 4 characters of the code, repeat it 3 times, split into the individual characters, perform the translation from c2 to c1, get the character at every index where index % 3 == 1 (indexes 1, 4, 7, and 10), compare it to ["G", "U", "{", "O"], and make sure they are the same.

Reversing this block tells us that the first 4 characters of the access code are HTB{ .

[String.prototype.slice, function () {
    return encodeURI(this)
}, String.prototype.slice, function (e) {
    return parseInt(this, e)
}, function (e) {
    return this ^ e
}, h],
[
    [-1],
    [],
    [-2],
    [16],
    [96],
    [29]
], access_code

In this block we get the last character of the code, encode it using URI encoding, get the last 2 characters of that (essentially stripping the % part of the result), parse it as an integer using base 16, perform an XOR with 96, and compare it to 29 .

Reversing this block tells us that the last character of the code is } .

(At this point we just figured out the obvious, that is, that the flag starts with HTB{ and ends in }, but its reassuring to see that our process makes sense)

[String.prototype.split, Array.prototype.reduce, h],
[
    [""], 
    [e => e + e, 1],
    [16777216]
], access_code

This block splits all the characters of the code and performs a reduce adding the value of each character to the initial value of 1 and comparing it to 16777216. This means that the sum of all characters in the result is 16777215 (subtract the initial 1).

[String.prototype.repeat, String.prototype.split, Array.prototype.map, Array.prototype.reduce, h],
[
    [21], 
    [""], 
    [e => n1[Array.prototype.indexOf(n2, e)]],
    [(e, t) => e + h.apply(t, [8]), 0],
    [63]
], access_code

This block repeats the code 21 times and splits it into individual characters. It then finds the index of each character in the n2 array and replaces it with its counterpart in n1. It then adds the value of the equality between each value and 8. In JavaScript, true has the value of 1 and false the value of 0, meaning that it will add 1 for each value 8 it finds. Finally, it compares it to 63 .

The value in n2 that will result in 8 in c1 is the character 3. Since we repeated the code 21 times, this blocks says that our original code contains 3 (63/21) characters 3 .

[String.prototype.split, Array.prototype.filter, Array.prototype.map, Array.prototype.reverse, Array.prototype.join, h],
[
    [""], 
    [(e, t) => ~Array.prototype.indexOf([4, 11, 13, 14, 16, 17, 20, 22], t)],
    [e => c1[Array.prototype.indexOf(c2, e)]], // FDPWCHKR
    [],
    ["-"], // SQCJPUXE
    ["E-X-U-P-J-C-Q-S"]
], access_code

This block splits the code into individual characters and then removes all the elements that are not on indexes 4, 11, 13, 14, 16, 17, 20, and 22. It then performs the replacement from c2 to c1, reverses the array, joins the elements using -, and compares the result to E-X-U-P-J-C-Q-S .

Reversing this block tells us that the code, from the beginning to index 22, fits the pattern ____F______D_PW_CH__K_R (where _ means unknown values).

[function () { return Array.from(this) }, f, h],
[
    [],
    [
        ["_"]
    ],
    [0]
], new Set(
    n(
        [String.prototype.slice, String.prototype.split, Array.prototype.reverse, Array.prototype.filter], 
        [
            [12, 16],
            [""],
            [],
            [(e, t) => ~Array.prototype.indexOf([0, 3], [t])]
        ], access_code)
    )
]

This block converts the value into an array and compares it to ["_"], meaning that what really matters here is what is inside of the Set() call, which is in itself another block. In that block, it takes the values from indexes 12, 13, 14, and 15 from the code, splits into individual characters, reverses, and removes all values but for the ones in indexes 0 and 3. This would be the equivalent of keeping the values from positions 12 and 15 from the original code. Since this is a Set, the same value would not exist twice, which explains how a set with two elements can become an array with a single element. This means that both characters are the same and are both _ .

[String.prototype.split, Array.prototype.reverse, Array.prototype.filter, function () {
    return this.slice(2, this.length).concat(this.slice(0, 2))
}, Array.prototype.reverse, Array.prototype.join, h],
[
    [""], 
    [],
    [(e, t) => ~Array.prototype.indexOf([18, 13, 4, 16, 15], [t])],
    [],
    [],
    [""],
    ["ncrnt"]
], access_code

This block splits the code into individual characters, reverses it, removes all elements but the ones on indexes 4, 13, 15, 16, and 18, takes the two first elements and places them at the end, reverses the array, joins it back into a string, and compares it to ncrnt .

Reversing this block gives us that the code follows the pattern r_nt_n________c____ from the end (we are sure that there are 4 characters between the end of the string and c, but not how many there are before r).

[String.prototype.charAt, h],
[
    [6],
    ["0"]
], access_code

Our final block tells us that the character at index 6 is the character 0 .

With that, we can join all the hints to determine the code:

0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19 20 21 22 23
H  T  B  {  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .
.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  }
.  .  .  .  F  .  .  .  .  .  .  D  .  P  W  .  C  H  .  .  K  .  R  .
.  .  .  .  .  .  .  .  .  .  .  .  _  .  .  _  .  .  .  .  .  .  .  .
.  .  .  .  .  r  .  n  t  .  n  .  .  .  .  .  .  .  .  c  .  .  .  .
.  .  .  .  .  .  0  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

H  T  B  {  F  r  0  n  t  .  n  D  _  P  W  _  C  H  .  c  K  .  R  }

The three missing characters are replaced by the three characters 3 that we know must exist, which gives us the final flag:

HTB{Fr0nt3nD_PW_CH3cK3R}

Last Updated in 2025
Halifax, NS
Canada