~$ Dissecting the Central-Infosec Static Code Analysis challenge
Tags:Information SecurityCTFWrite Up
This is a write-up for the hardest of the "14. Web Exploitation: Advanced (CIS-WEBSRV01)" series of challenges in the context of the Central Infosec CTF.
The challenge consists of finding the statically encoded credentials to then find the flag, so we need to work through the obfuscated code and see how it works.
Summary
- Step 1: Visiting the page
- Step 2: Deobfuscating the script
- Step 3: Determining the password validation scheme
- Step 4: Finding the password
Step 1: Visiting the page
The first step in this challenge is actually finding it. As per the $MACHINE_IP/robots.txt
file, the URL for the challenge is $MACHINE_IP/hack-the-static-hard
, and as we load it up in a browser, we see the following page:
As is intimated with the name of the challenge, we'll have to work with the page's static assets. And since it is a login behavior we are targeting (and not some hidden values), we'll take a look at anything related to scripting, usually in the form of some JavaScript.
To check out the script, we will first check the source code for a <script>
tag, and check if it is either loading a script from somewhere, or if it is embedded into the page. To do this, we right click the page and press Inspect element
, which renders the following in the sidepane:
The two circled areas are the function that is called when the "Login"
button is pressed, and the second is the actual script, which contains that function.
Step 2: Deobfuscating the script
Let us focus on that script and directly start by separating the functions and indenting the code: (Trust me, you do not want this oneliner in a code box on this page...)
There's a saying that goes: "What in the fresh hell is this?". It is apt in the context.
So let's start at the top:
This sets up an array of strings and function names. Quite nice, let's rename all occurences of _0x50da
in our code to data
, because that will make things easier.
What does this section do? Well, it certainly doesn't make itself readable, that's for sure.
What we can say is that it is an unnamed function that is directly executed, and it takes 2 parameters: our list of values and functions, and the number 0x72
(which is the hexadecimal value of 114
. So what we can do is rename _0x374ae7
to data
(again) and rename _0x50da49
to n
:
In the middle of our unnamed function we have a named function called dataGetter49
(which just rolls of the tongue). It takes a value related to n
, which is ++n
. What ++n
really is doing to n
is saying: "Hey, before you go and do anything, here's a 1 on me". Effectively, at the moment it is then used, the value of n
is n + 1
.
Since it seems to be muddling with the contents of data, we will rename the internal function to dataMover
.
Let's make our code reflect those changes:
Finally, and it's a feature of JavaScript that is heavily abused when dealing with obfuscation, since all JavaScript variables have the prototype
structure, you can call a function on them using the string accessor. This is what we see with data['push'](...)
, which is the annoying way of writing data.push(...)
.
Let's follow up on those changes:
If we were to run lines 2 until 11 in a JavaScript console, and then ask the console to give us the contents of the data variable we would see the following:
We can run it as many times as we want, and that isn't going to change, so we might as well replace lines 2 until 11 of the entire script with:
Onwards then, let us now look at the function dataGetter
:
What does this function do? Well, it takes two parameters. The first one is subtracted by 0x0
, which is the hexadecimal format for the number 0
.
This line is however far from useless: Given the dynamic nature of javascript, if a number were to be provided in string format, this simple operation would do the job of converting the value to a number for certain.
What it then does is access our data
variable (an array) at the index defined by the variable we just converted to a number, and then returns that value.
The _0x50da49
has absolutely no effect whatsoever on this function, and thus can be removed.
As such, we've determined that the function acts as a data "getter" sorts, so we can rename it to dataGetter
.
Let's put these changes in writing:
The next bit is the complicated bit: It mixes some calls to our previously defined functions, some prototype access shenanigans, some ternary operators and various JavaScript specific WTF elements.
Let's already replace the things we know, and then we will get to identifying what is going on:
Weirdly enough, this has made things worse for us. Well then, I guess we will have to run dataGetter for all of these various values and run with that!
Now let us play the replacement game:
Ok, this has gotten much better. But we can still play around with a few things, notably replacing the prototype string accessors, transforming the JavaScript WTF values into their actual counterparts (![] => false
and !![] => true
), and maybe transforming the ternary operators (which are one-liner if-else statements in the form of condition ? do_if_condition_true : do_if_condition_false
) into actual if-else statements:
Woow, far out! This has made the code readable, and lets us progress onto the next part!
Step 3: Determining the password validation scheme
Well, we now have found the diagram for how this works. But what makes the engines turn? How does it work?
If you would look at line 11, the first thing to notice is that it is looking for whether or not the username provided in the form is "Admin"
.
If that condition is met, it will set the isAdmin
hidden field to true
.
Then we get into a condition we haven't "decoded" yet. Firstly, window.btoa
and window.atob
are functions that exist by default in JavaScript, so let us check what they produce with our two static values:
So this password validation scheme is the following:
- It checks whether or not the username entered
"Admin"
. It sets theisAdmin
page value to 1, which here stands for true. - If that is the case, it will then check whether the password entered is correct and if that is right, sets a hidden
user_id
field to some obscure value ("XzB4NDI1MjE2"
), which is what the form uses to check for the password's validity. - Otherwise, it resets the
isAdmin
page value. - If the username was incorrect, it sets the hidden
user_id
field to a value that won't be recognised by the password validity checker.
Step 4: Finding the password
Well, this doesn't help us much. Or does it? If you look at the naming convention of the two functions (atob
and btoa
) you might notice that they are two sides of the same coin. The one goes "[from] a
to b
" and the other goes "[from] b
to a
".
So if we apply that principle to our code, and remember our first algebra classes, then we know that if you do one thing to one side, then you do the same to the other:
Which, simplified further is:
We now know of a username ("Admin"
) and a password ("XzB4NWU0NDcz=="
). Let's go grab the flag!
The flag is Central-InfoSec{H@RD357_H@RD_C0D3D_4_7H3_L055}
, and was pretty fun to get. Hope you had fun reading this!