- Welcoming gift
- Truth seeker
- Gamer
- L337 gamer
- Shinobi
- Star warrior
- All about the fashion
- Deep pocket
- Easter egg
- Solve script
- Hint:
It's free
We start at the title screen at https://halloween.itemize.no/, here we get some input instructions for the game. The first flag is given for free at the "Flags"-page. The flags showing ???
indicates that it is yet to be unlocked and would therefore be updated automatically if the criteria is met, the flags with the text ?
, on the other hand, indicates that it is to be found through other means.
Itemize{sp00ky_hall0w33n}
- Hint:
Seek the source
At the "Flags"-page we can see that hints are also given for all the flags. For this flag, we are told to seek the source, meaning the source code of the game. As this is a web-game, using devtools we can quickly see that the source code is available for us, and luckily it is not obfuscated in any way either. Navigating through the javascript files one by one, we find the flag at player.js
Itemize{y0u_f0und_th3_truth}
- Hint:
Clear 6 stages
Trying to navigate around the game shows that this is actually a sokoban clone. For this and the next flag, we have 3 intended solutions:
The first few levels should be easily solveable with little moves, and as the flag title says. One could just simply solve the first 6 stages by just playing the game normally. This does mean lots of manual work though, and a more effective approach could be done!
From solving the second flag truth seeker
, we know that the source code is available. A good start to any ctf or web challenges when the source code is given is to therefore read the source code and understand it's functionalities. Looking at the constant.js
file we find some constants which includes the variable MAP
with a comment indicating that a level from Microban is being used. We can see that almost all the stages of the game is essentially taken from that exact game.
Knowing this, one can search for a game walkthrough/solution on the internet, and then manually follow the moves. An example of a video: https://www.youtube.com/watch?v=6LtYGUZYWI4
However, as this is a hacking/ctf challenge. Getting the flag through automatic means and hacking is more fun!
After manually solving a stage or by reading the source code, we can see that a POST request is being sent to the endpoint /api/levels/[id]
with the payload {"moves": moves}
, being the moves it took to solve a level/stage.
Note:
Looking at HTTP requests usually gives us a lot of information on the web application. We can for example see that the server is written in
javascript
andExpress
through thex-powered-by
response header for instance, as well as the different exposed endpoints existing such as/api/
in our case.
Knowing that there is no validation or signature other than the total amount of moves it took to solve a stage, one can figure that solving the stage is easily manipulated by just simply sending a HTTP request with any chosen move amount.
$ curl 'https://halloween.itemize.no/api/levels/1' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' -H 'content-type: application/json' --data-raw '{"moves":1}'
{"success":"Level clear submitted"}
...
$ curl 'https://halloween.itemize.no/api/levels/6' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' -H 'content-type: application/json' --data-raw '{"moves":1}'
{"success":"Level clear submitted"}
Changing the id from 1
to 2
, 3
, and so on, respectively until we reach 6 stages solved, gives us the flag.
Itemize{that_was_3asy}
- Hint:
Clear 12 stages
Similarly, we can solve all the stages by sending HTTP requests on all the different level id's up to 12
.
$ curl -X POST 'https://halloween.itemize.no/api/levels/7' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' -H 'content-type: application/json' --data-raw '{"moves":1}'
{"success":"Level clear submitted"}
...
$ curl -X POST 'https://halloween.itemize.no/api/levels/12' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' -H 'content-type: application/json' --data-raw '{"moves":1}'
{"success":"Level clear submitted"}
Note:
All stages up until stage
11
were actually solveable if played normally, stage12
were not so, however, as the player had no method of going to the right side of the map to move the boulder.From the given source, we can see that the
game
-variable were set globally, one could therefore access the game object through the devtools console and from there change any fields as much as wanted. The stage could therefore be solved by simply changing the user position with code, essentially teleporting the player. Alternatively, one could also access the boulders and therefore move them to the slots.
Itemize{l337_gam3r}
- Hint:
Clear the hidden stage
The hidden stage pointed to the WIP stage of 13
, were made such that one could not solve it manually by playing normally, since it is disabled by default. Similarly to the flag above, it can be solved by sending a POST request to /api/levels/13
.
$ curl -X POST 'https://halloween.itemize.no/api/levels/13' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' -H 'content-type: application/json' --data-raw '{"moves":1}'
{"success":"Level clear submitted"}
Note:
For those interested, one can retrieve the level by sending a GET request to the endpoint
/api/levels/[id]
.$ curl -X GET -s 'https://halloween.itemize.no/api/levels/13' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' | jq { "level": [ "", " ###### ####", "#####*# ################# ##", "# ### #", "# ######## #### ## #", "### #### # #### #### ##", "#*# # .# # # # # # #", "#*# # # # ## # ## ## #", "### ### ### # ## # ## ##", " # # #*# # # # #", " # # ### ##### #### # #", " ##### ##### ####### ######", " # # # #**# #", "## # # #**# ####### ## #", "# ######### # ##### ###", "# # # $ #*#", "# ######### ### @##### #*#", "##### #### #### ######" ] }
Itemize{hack_n0_jutsu}
- Hint:
Collect all stars
Stars are given by solving the stages, depending on the moves done you'll get stars ranging from 1-3. By utilizing the ability to specifically choose any amount of moves through HTTP requests, we could send in the lowest move amount such as 1
for example on the different stages.
$ curl -X POST 'https://halloween.itemize.no/api/levels/1' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' -H 'content-type: application/json' --data-raw '{"moves":1}'
{"success":"Level clear submitted"}
...
$ curl -X POST 'https://halloween.itemize.no/api/levels/12' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' -H 'content-type: application/json' --data-raw '{"moves":1}'
{"success":"Level clear submitted"}
Itemize{d3f3nd3rs_0f_th3_galaxy}
- Hint:
Collect all skins
A "Shop"-page is also available for us indicating that player character skins could be bought by using the pumpkin currency given to us after clearing any stage. From this, one could guess that playing the stages repetively should give us enough currencies to buy all the skins.
The solution can be as easy as spamming some requests for clearing the levels till we get enough pumpkins, or write a quick script looping the requests.
import os
for i in range(100):
os.system("""curl -X POST 'https://halloween.itemize.no/api/levels/1' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' -H 'content-type: application/json' --data-raw '{"moves":1}'""")
Itemize{pr0ud_sk1n_c0ll3ct0r}
- Hint:
Richer than admin
A "Leaderboard"-page displaying the scores of each player is also available. Similarly to the solution to the flag above, we can send more requests till we have more pumpkins than the Admin user with 666 pumpkins.
Itemize{m0th3rl0d3}
- Hint:
LvL. 1337
The last flag comes with the hint indicating that a level id of 1337
might exist. Similarly to the other challenges, we can try to see if we are able to clear the level. Sending a POST requests with the id of 14
and 1337
does indeed confirm that it exist. However, clearing the level wasn't enough to give us the flag, as it is marked with a ?
in the "Flags"-page, meaning that we have to get some other way.
$ curl -X POST 'https://halloween.itemize.no/api/levels/14' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' -H 'content-type: application/json' --data-raw '{"moves":1}'
{"error":"Value of level not found"}
$ curl -X POST 'https://halloween.itemize.no/api/levels/1337' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' -H 'content-type: application/json' --data-raw '{"moves":1}'
{"success":"Level clear submitted"}
Instead of clearing the level, sending a GET request will give us the map of the stage, which looks very much like a QR-code:
$ curl -s 'https://halloween.itemize.no/api/levels/1337' -H 'cookie: auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliNjMzN2QwLWEzMmQtNGJmNy05Yzk1LWNlY2E5NmNkMTc5NCIsImlhdCI6MTY3NDY4MzEwMX0.ci0hmR_XioO46zBMxtIefGBSaqZLN0o1qhq5y0OSlbA' | jq
{
"level": [
"",
"############## #### ## #### ##############",
"## ## ## ## ## ## ##",
"## ###### ## ###### #### ## #### ## ###### ##",
"## ###### ## ###### ## ## ## ## ###### ##",
"## ###### ## ## #### ## ###### ##",
"## ## ########## #### #### ## ##",
"############## ## ## ## ## ## ## ## ##############",
" ## ## ##########",
"## ## #### ########## ## ## #### ##",
" ######## ## ## ######## ## #### ######",
" ## ###### ########## ######## ## ####",
"######## ## ## ########## ## ###### ###### ##",
"#### ######## #### #### ## ######## ####",
" #### ## #### ## ############## ## ####",
" ## #### ###### ## ######## ####",
" ###### ## ## ## ## ## ## ##",
" #### ## ###### #### ########## ## ##",
" ######## #################### ## ## ## ##",
"## ## ## ## #### ## ########## ## ## ####",
" ###### ## ## ## ########## ## ## ##",
"## ## #### ## ############ ##",
" ## #### #### #### ## ## ##",
"############## ## ########## ## ## ####",
"## ## ## #### #### ## ## ##",
"## ###### ## #### #### ############ ##",
"## ###### ## #### ## ## #### ## ## ## ##",
"## ###### ## #### ############ ## ######## ##",
"## ## ######## ## ###### ## #### ##",
"############## ## #### ## ## ## ## #### ##",
""
]
}
Scanning it, however, proved to not be that easy as some parsing and string manipulation work had to be done to effectively make the QR-code more clear and easier to be scanned. In my case, using HTML and CSS were the most intuitive for me to draw the QR code (qr.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#display {
display: flex;
flex-direction: column;
}
div {
height: 16px;
display: flex;
}
span {
display: inline-block;
width: 8px;
height: 16px;
color: transparent;
background-color: white;
}
span.black {
background-color: black;
}
</style>
</head>
<body>
<div id="display"></div>
<script>
const qr = [
"",
"############## #### ## #### ##############",
"## ## ## ## ## ## ##",
"## ###### ## ###### #### ## #### ## ###### ##",
"## ###### ## ###### ## ## ## ## ###### ##",
"## ###### ## ## #### ## ###### ##",
"## ## ########## #### #### ## ##",
"############## ## ## ## ## ## ## ## ##############",
" ## ## ##########",
"## ## #### ########## ## ## #### ##",
" ######## ## ## ######## ## #### ######",
" ## ###### ########## ######## ## ####",
"######## ## ## ########## ## ###### ###### ##",
"#### ######## #### #### ## ######## ####",
" #### ## #### ## ############## ## ####",
" ## #### ###### ## ######## ####",
" ###### ## ## ## ## ## ## ##",
" #### ## ###### #### ########## ## ##",
" ######## #################### ## ## ## ##",
"## ## ## ## #### ## ########## ## ## ####",
" ###### ## ## ## ########## ## ## ##",
"## ## #### ## ############ ##",
" ## #### #### #### ## ## ##",
"############## ## ########## ## ## ####",
"## ## ## #### #### ## ## ##",
"## ###### ## #### #### ############ ##",
"## ###### ## #### ## ## #### ## ## ## ##",
"## ###### ## #### ############ ## ######## ##",
"## ## ######## ## ###### ## #### ##",
"############## ## #### ## ## ## ## #### ##",
"",
];
const display = document.querySelector("#display");
for (const line of qr) {
let arr = line.split("");
arr = arr.map((a) =>
a == " " ? `<span>${a}</span>` : `<span class="black">${a}</span>`
);
display.innerHTML += `<div>${arr.join("")}</div>`;
}
</script>
</body>
</html>
Scanning the QR-code gives us the flag.
Itemize{t00_3arly_f0r_3ast3r}
An amazing solve script written by Jo#5283 that solves all of the above challenges automatically can be seen here (solve.py):
import numpy as np
import requests
import json
import math
import cv2
import re
from pyzbar import pyzbar
from PIL import Image
session = requests.Session()
assert session.get("https://halloween.itemize.no/game").status_code == 200
for level in range(1, 14):
assert session.post(f"https://halloween.itemize.no/api/levels/{level}", json={"moves": 0}).status_code == 200
needed_pumpkin_balance = 666 + 350
pumpkin_balance = session.get("https://halloween.itemize.no/api/skins").json()["pumpkin"]
for i in range(int(math.ceil((needed_pumpkin_balance - pumpkin_balance) / 3))):
assert session.post("https://halloween.itemize.no/api/levels/13", json={"moves": 0}).status_code == 200
for skin_id in range(2, 5):
assert session.post("https://halloween.itemize.no/api/skins", json={"skinId": skin_id}).status_code == 200
pumpkin_balance = session.get("https://halloween.itemize.no/api/skins").json()["pumpkin"]
extra_flags = {}
resp = session.get("https://halloween.itemize.no/api/levels/1337")
assert resp.status_code == 200
img = np.array([[int(c.isspace()) for c in r.ljust(58)] for r in resp.json()["level"] if r], dtype=np.uint8) * 255
flag = pyzbar.decode(Image.fromarray(cv2.resize(img, (512, 512), interpolation=cv2.INTER_NEAREST)))[0].data.decode()
extra_flags["Easter egg"] = flag
resp = session.get("https://halloween.itemize.no/js/player.js")
assert resp.status_code == 200
flag = re.search(r"Itemize{.*}", resp.text).group(0)
extra_flags["Truth seeker"] = flag
for flag_data in session.get("https://halloween.itemize.no/api/flags").json()["flags"]:
flag = extra_flags[flag_data["flag"]] if flag_data["flag"] in extra_flags else flag_data["display"]
print(f"{flag_data['flag'].ljust(24)}: {flag}")